import clientSource from "./frame-client.js?raw"; import { toBase64 } from "../crypto"; async function sha256Base64(text: string): Promise { return toBase64(new Uint8Array(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text)))); } const STYLE = ` *{box-sizing:border-box}html,body{margin:0;height:100%;background:#0b0d10;color:#d7dce2;font:13px/1.4 "IBM Plex Sans",system-ui,sans-serif} #app{display:grid;grid-template-columns:280px 1fr;grid-template-rows:36px 1fr 24px;height:100%} header{grid-column:1/3;display:flex;align-items:center;gap:12px;padding:0 12px;border-bottom:1px solid #1f242b;background:#111419} header b{font-weight:600}#ref{color:#8b949e;font-family:"IBM Plex Mono",monospace;font-size:12px} aside{border-right:1px solid #1f242b;overflow:auto;display:flex;flex-direction:column;background:#0f1216} .section{padding:6px 8px;font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:#8b949e;display:flex;justify-content:space-between;align-items:center} .row{padding:2px 8px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.row:hover{background:#1a1f26} .row.active{background:#1f2a37}.row.dirty{color:#e3b341}.dir{color:#aeb6bf} .change{display:flex;gap:6px;align-items:center}.change span:nth-child(2){flex:1;overflow:hidden;text-overflow:ellipsis} .badge{font-family:monospace;font-size:11px;width:14px}.sA{color:#3fb950}.sM{color:#e3b341}.sD{color:#f85149} button{background:#21262d;color:#d7dce2;border:1px solid #30363d;border-radius:4px;padding:3px 8px;cursor:pointer;font:inherit} button:disabled{opacity:.5;cursor:default}button.primary{background:#238636;border-color:#2ea043;color:#fff} button.link{background:none;border:none;padding:0 4px;color:#8b949e} #commit-box{padding:8px;display:flex;flex-direction:column;gap:6px;border-top:1px solid #1f242b} textarea,input[type=text]{background:#0b0d10;color:#d7dce2;border:1px solid #30363d;border-radius:4px;padding:4px 6px;font:inherit} .push{margin-top:auto}#message{min-height:56px;resize:vertical}#branch-hint{color:#8b949e;font-size:12px} main{display:flex;flex-direction:column;min-width:0;min-height:0} #tabs{display:flex;border-bottom:1px solid #1f242b;background:#111419;overflow-x:auto;min-height:32px} .tab{display:flex;align-items:center;gap:4px;padding:0 10px;border-right:1px solid #1f242b;cursor:pointer;color:#8b949e} .tab.active{background:#0b0d10;color:#fff} #crumb{padding:4px 12px;color:#8b949e;font-size:12px;border-bottom:1px solid #1f242b;min-height:24px} #body{flex:1;display:flex;min-height:0;font:13px/20px "IBM Plex Mono",ui-monospace,monospace} #gutter{margin:0;padding:8px 8px 8px 12px;color:#484f58;text-align:right;overflow:hidden;user-select:none;white-space:pre;font:inherit} #editor{flex:1;margin:0;border:0;border-radius:0;padding:8px;resize:none;outline:none;white-space:pre;overflow:auto;tab-size:2;font:inherit} #notice{padding:24px;color:#8b949e} footer{grid-column:1/3;display:flex;align-items:center;padding:0 12px;border-top:1px solid #1f242b;background:#111419;font-size:12px;color:#8b949e} #status-msg.error{color:#f85149} `; const BODY = `
Loading…
Select a file to start editing.
Waiting for session…
`; let cachedHashes: Promise<{ script: string; style: string }> | null = null; function hashes() { cachedHashes ??= Promise.all([sha256Base64(clientSource), sha256Base64(STYLE)]).then(([script, style]) => ({ script, style, })); return cachedHashes; } /** * The editor frame document. The CSP `sandbox` directive keeps it in an opaque origin with no access to * forge cookies even when opened top-level; CSP also pins it to the inline code and the editor API. */ export async function editorFrameResponse(url: URL): Promise { const { script, style } = await hashes(); const html = `gitfor editor${BODY}`; const csp = [ "default-src 'none'", `script-src 'sha256-${script}'`, `style-src 'sha256-${style}'`, `connect-src ${url.origin}`, "img-src data:", "font-src 'none'", "base-uri 'none'", "form-action 'none'", "frame-ancestors 'self'", "sandbox allow-scripts allow-modals", ].join("; "); return new Response(html, { headers: { "content-type": "text/html; charset=utf-8", "content-security-policy": csp, "cross-origin-resource-policy": "same-origin", "referrer-policy": "no-referrer", "x-content-type-options": "nosniff", "cache-control": "no-store", }, }); }