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 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 | 31 | // Cloudflare's Web Analytics beacon, when the zone turns it on, and |
| 32 | // HeyCatch's helper for product analytics (lib/analytics.client.ts). | |
| 33 | `script-src 'self' 'nonce-${nonce}' https://static.cloudflareinsights.com https://in.heycatch.ai`, | |
| 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 | 34 | "style-src 'self' 'unsafe-inline'", |
| 35 | `img-src 'self' https: data: blob:${files}`, | |
| 36 | `media-src 'self' https:${files}`, | |
| 37 | "font-src 'self' data:", | |
| 38 | "connect-src 'self' https:", | |
| 39 | "frame-src 'none'", | |
| 40 | "object-src 'none'", | |
| 41 | "base-uri 'self'", | |
| 42 | "frame-ancestors 'none'", | |
| 43 | ].join("; "); | |
| 44 | } | |
| 45 | ||
| 46 | /** | |
| 47 | * The answer with the site's headers added. A header the answer already | |
| 48 | * has is kept. An upgrade to a WebSocket is passed on as it is. | |
| 49 | */ | |
| 50 | export function withSiteHeaders(response: Response): Response { | |
| 51 | if (response.status === 101 || (response as Response & { webSocket?: unknown }).webSocket) return response; | |
| 52 | // A redirect's headers cannot be changed, so the answer is copied. | |
| 53 | const answer = new Response(response.body, response); | |
| 54 | const headers = answer.headers; | |
| 55 | if (!headers.has("x-content-type-options")) headers.set("x-content-type-options", "nosniff"); | |
| 56 | if (!headers.has("referrer-policy")) headers.set("referrer-policy", "strict-origin-when-cross-origin"); | |
| 57 | if (/^text\/html\b/i.test(headers.get("content-type") ?? "") && !headers.has("x-frame-options")) { | |
| 58 | headers.set("x-frame-options", "DENY"); | |
| 59 | } | |
| 60 | return answer; | |
| 61 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.