Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Merge g1tusercontent.com: registry answers run nothing in a browser, the site's pages run only their own scripts, repository files and avatars on their own origin, raw files rate limited per address | 1 | /** |
| 2 | * The headers every answer from the site carries, and the policy its pages | |
| 3 | * run under. No Workers imports, so it can be tested under Node. | |
| 4 | * | |
| 5 | * - Nothing is sniffed: a download or a data request is only the type it says. | |
| 6 | * - A link to another site sends the origin, never the path. | |
| 7 | * - No other site may put g1t's pages in a frame. | |
| 8 | * - A page runs only the scripts the site served it: its own files, and the | |
| 9 | * inline scripts React and React Router write, each carrying the page's | |
| Merge site-audit: SEO routes, analytics with consent first in Europe, security page, founder and X account, docs header; the tour opens on the pull request and leads with Code, on the real shell, with Agents and Docs working today | 10 | * nonce, plus Cloudflare's and HeyCatch's analytics scripts. Styles may |
| 11 | * be inline (highlighting and layout set them); images may come from any | |
| 12 | * HTTPS address (pictures in a README); requests may go to any HTTPS | |
| 13 | * address (the status page's summary, analytics events). | |
| Merge g1tusercontent.com: registry answers run nothing in a browser, the site's pages run only their own scripts, repository files and avatars on their own origin, raw files rate limited per address | 14 | */ |
| 15 | ||
| 16 | /** A fresh nonce for one page: 128 random bits, base64. */ | |
| 17 | export function makeNonce(): string { | |
| 18 | const bytes = crypto.getRandomValues(new Uint8Array(16)); | |
| 19 | return btoa(String.fromCharCode(...bytes)); | |
| 20 | } | |
| 21 | ||
| 22 | /** | |
| 23 | * The Content-Security-Policy of a page rendered with `nonce`. `usercontent` | |
| 24 | * is where repository files and avatars are served (lib/usercontent.ts), | |
| 25 | * named for an installation that serves them over plain HTTP. | |
| 26 | */ | |
| 27 | export function pagePolicy(nonce: string, usercontent?: string): string { | |
| 28 | const files = usercontent && /^http:/.test(usercontent) ? ` ${new URL(usercontent).origin}` : ""; | |
| 29 | return [ | |
| 30 | "default-src 'self'", | |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 31 | // Cloudflare's Web Analytics beacon, when the zone turns it on, |
| 32 | // HeyCatch's helper for product analytics (lib/analytics.client.ts), and | |
| 33 | // Cloudflare Turnstile on sign-up when it is on (lib/turnstile.ts). | |
| 34 | `script-src 'self' 'nonce-${nonce}' https://static.cloudflareinsights.com https://in.heycatch.ai https://challenges.cloudflare.com`, | |
| Merge g1tusercontent.com: registry answers run nothing in a browser, the site's pages run only their own scripts, repository files and avatars on their own origin, raw files rate limited per address | 35 | "style-src 'self' 'unsafe-inline'", |
| 36 | `img-src 'self' https: data: blob:${files}`, | |
| 37 | `media-src 'self' https:${files}`, | |
| 38 | "font-src 'self' data:", | |
| preparing for cloud work | 39 | // `ipc:` and ipc.localhost are the desktop app's own channel to its |
| 40 | // shell, which the page calls through window.g1tDesktop | |
| 41 | // (lib/notify-client.ts); no browser resolves either. | |
| 42 | "connect-src 'self' https: ipc: http://ipc.localhost", | |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 43 | // Turnstile's widget draws in a frame of its own; nothing else is framed. |
| 44 | "frame-src https://challenges.cloudflare.com", | |
| Merge g1tusercontent.com: registry answers run nothing in a browser, the site's pages run only their own scripts, repository files and avatars on their own origin, raw files rate limited per address | 45 | "object-src 'none'", |
| 46 | "base-uri 'self'", | |
| 47 | "frame-ancestors 'none'", | |
| 48 | ].join("; "); | |
| 49 | } | |
| 50 | ||
| 51 | /** | |
| 52 | * The answer with the site's headers added. A header the answer already | |
| 53 | * has is kept. An upgrade to a WebSocket is passed on as it is. | |
| 54 | */ | |
| 55 | export function withSiteHeaders(response: Response): Response { | |
| 56 | if (response.status === 101 || (response as Response & { webSocket?: unknown }).webSocket) return response; | |
| 57 | // A redirect's headers cannot be changed, so the answer is copied. | |
| 58 | const answer = new Response(response.body, response); | |
| 59 | const headers = answer.headers; | |
| 60 | if (!headers.has("x-content-type-options")) headers.set("x-content-type-options", "nosniff"); | |
| 61 | if (!headers.has("referrer-policy")) headers.set("referrer-policy", "strict-origin-when-cross-origin"); | |
| 62 | if (/^text\/html\b/i.test(headers.get("content-type") ?? "") && !headers.has("x-frame-options")) { | |
| 63 | headers.set("x-frame-options", "DENY"); | |
| 64 | } | |
| 65 | return answer; | |
| 66 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.