import { expect, test } from "bun:test"; import { inlineScriptHashes, spaCsp, spaWithCsp } from "../src/server/spa"; import { RUNNER_FRAME_CSP, RUNNER_FRAME_PATH } from "../src/server/runnerFrame"; import { connect } from "@stc/db"; import { runMigrations } from "../src/server/migrate"; import { makeApp } from "./app"; const scriptSrc = (csp: string) => csp.split(";").map((d) => d.trim()).find((d) => d.startsWith("script-src"))!; test("the SPA CSP allows scripts from 'self' only, never inline", () => { const s = scriptSrc(spaCsp()); expect(s).toBe("script-src 'self'"); expect(spaCsp()).toContain("object-src 'none'"); expect(spaCsp()).toContain("frame-ancestors 'self'"); expect(inlineScriptHashes('')).toEqual([ `sha256-${new Bun.CryptoHasher("sha256").update("x()").digest("base64")}`, ]); }); // A stand-in for the bundled index.html (bundling inside bun test is unreliable); the real bundle is checked // against the dev server (see wp-learner-report.md, fix round 1). const DOC = '
'; const upstream = (req: Request) => new URL(req.url).pathname.endsWith(".js") ? new Response("console.log(1)", { headers: { "Content-Type": "text/javascript" } }) : new Response(DOC, { headers: { "Content-Type": "text/html;charset=utf-8" } }); for (const development of [false, true]) { test(`the SPA document is served with the CSP header (development: ${development})`, async () => { const spa = spaWithCsp(upstream, { development }); const server = Bun.serve({ hostname: "127.0.0.1", port: 0, routes: { "/api/x": () => new Response("api"), ...spa.routes } }); try { const r = await fetch(`http://127.0.0.1:${server.port}/c/sample/m1-intro`); expect(r.status).toBe(200); const csp = r.headers.get("content-security-policy")!; expect(await r.text()).toBe(DOC); const s = scriptSrc(csp); expect(s).not.toContain("unsafe-inline"); expect(s).not.toContain("unsafe-eval"); // Production never allows inline script; development allows only the exact injected helper, by hash. expect(s).toBe(development ? `script-src 'self' '${inlineScriptHashes(DOC)[0]}'` : "script-src 'self'"); const asset = await fetch(`http://127.0.0.1:${server.port}/_bun/client/app.js`); expect(asset.status).toBe(200); expect(asset.headers.get("content-security-policy")).toBeNull(); expect(await (await fetch(`http://127.0.0.1:${server.port}/api/x`)).text()).toBe("api"); } finally { server.stop(true); spa.stop(); } }); } test("the runner frame is served sandboxed, with no connections and only its own script", async () => { const db = await connect("sqlite://:memory:"); await runMigrations(db); const { call } = await makeApp(db); const r = await call("GET", RUNNER_FRAME_PATH); expect(r.status).toBe(200); const csp = r.headers.get("content-security-policy")!; expect(csp).toBe(RUNNER_FRAME_CSP); expect(csp).toContain("connect-src 'none'"); expect(csp).toContain("sandbox allow-scripts"); expect(csp).not.toContain("allow-same-origin"); expect(scriptSrc(csp)).toMatch(/^script-src 'sha256-[A-Za-z0-9+/=]+' blob:$/); await db.close(); });