import { env } from "cloudflare:workers"; import { manifestKey, objectKey } from "./config"; import type { Manifest, ManifestFile, SiteManifest } from "./config"; const MANIFEST_CACHE_SECONDS = 3600; const HTTP_OK = 200; const CONTENT_TYPES: Record = { html: "text/html; charset=utf-8", htm: "text/html; charset=utf-8", css: "text/css; charset=utf-8", js: "text/javascript; charset=utf-8", mjs: "text/javascript; charset=utf-8", json: "application/json", map: "application/json", webmanifest: "application/manifest+json", txt: "text/plain; charset=utf-8", md: "text/plain; charset=utf-8", xml: "application/xml", svg: "image/svg+xml", png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp", avif: "image/avif", ico: "image/x-icon", woff: "font/woff", woff2: "font/woff2", ttf: "font/ttf", otf: "font/otf", wasm: "application/wasm", pdf: "application/pdf", mp4: "video/mp4", webm: "video/webm", mp3: "audio/mpeg", wav: "audio/wav", }; /** Headers for untrusted user content; served on a separate registrable domain from the forge. */ export const SECURITY_HEADERS: Record = { "x-content-type-options": "nosniff", "strict-transport-security": "max-age=31536000; includeSubDomains", "referrer-policy": "strict-origin-when-cross-origin", "cross-origin-opener-policy": "same-origin", "cross-origin-resource-policy": "same-site", "content-security-policy": "frame-ancestors 'self'; base-uri 'self'; form-action 'self' https:", "permissions-policy": "camera=(), microphone=(), geolocation=(), payment=(), usb=(), interest-cohort=()", "x-robots-tag": "noarchive", }; /** Unclaimed scratch content is anonymous: no posting forms off-site (phishing) and no indexing. */ export const SCRATCH_HEADERS: Record = { ...SECURITY_HEADERS, "content-security-policy": "frame-ancestors 'self'; base-uri 'self'; form-action 'self'", "x-robots-tag": "noindex, nofollow, noarchive", }; export function contentType(path: string): string { const extension = path.slice(path.lastIndexOf(".") + 1).toLowerCase(); return CONTENT_TYPES[extension] ?? "application/octet-stream"; } export function decodePath(path: string): string | null { try { const decoded = decodeURIComponent(path).replace(/^\/+/, ""); return decoded.split("/").some((part) => part === "..") ? null : decoded; } catch { return null; } } type Lookup = { path: string; file: ManifestFile; status: number } | { redirect: string } | null; export function lookup(manifest: Manifest, spa: boolean, path: string, base: string): Lookup { const files = manifest.files; const direct = path === "" || path.endsWith("/") ? `${path}index.html` : path; if (files[direct]) return { path: direct, file: files[direct], status: 200 }; if (!path.endsWith("/") && path !== "") { if (files[`${path}/index.html`]) return { redirect: `${base}${path}/` }; if (files[`${path}.html`]) return { path: `${path}.html`, file: files[`${path}.html`], status: 200 }; } if (spa && files["index.html"]) return { path: "index.html", file: files["index.html"], status: 200 }; if (files["404.html"]) return { path: "404.html", file: files["404.html"], status: 404 }; return null; } export function plain(status: number, body: string, extra: Record = {}): Response { return new Response(body, { status, headers: { ...SECURITY_HEADERS, "content-type": "text/plain; charset=utf-8", "cache-control": "no-store", ...extra, }, }); } export function html(status: number, body: string, extra: Record = {}): Response { return new Response(body, { status, headers: { ...SECURITY_HEADERS, "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'", ...extra, }, }); } export function loadManifest(deploymentId: string): Promise { return env.KV.get(manifestKey(deploymentId), { type: "json", cacheTtl: MANIFEST_CACHE_SECONDS }); } /** Serves a static asset from a deployment's manifest; with clean URLs, SPA fallback and `404.html`. */ export async function serveStatic( request: Request, manifest: SiteManifest, spa: boolean, gated = false, ): Promise { if (request.method !== "GET" && request.method !== "HEAD") return plain(405, "Method not allowed"); const path = decodePath(new URL(request.url).pathname); if (path === null) return plain(400, "Bad path"); const found = lookup({ files: manifest.files }, spa, path, "/"); if (!found) return plain(404, "Not found"); if ("redirect" in found) return new Response(null, { status: 308, headers: { ...SECURITY_HEADERS, location: found.redirect } }); const [sha, size] = found.file; const etag = `"${sha}"`; const headers: Record = { ...SECURITY_HEADERS, "content-type": contentType(found.path), etag, "cache-control": `${gated ? "private" : "public"}, ${found.path.endsWith(".html") ? "max-age=0, must-revalidate" : "max-age=600"}`, }; if (gated) headers.vary = "cookie"; if (found.status === HTTP_OK && request.headers.get("if-none-match") === etag) { return new Response(null, { status: 304, headers }); } if (request.method === "HEAD") { return new Response(null, { status: found.status, headers: { ...headers, "content-length": String(size) } }); } const object = await env.BUCKET.get(objectKey(sha)); if (!object) return plain(503, "Deployment object missing"); return new Response(object.body, { status: found.status, headers: { ...headers, "content-length": String(object.size) }, }); }