import { describe, expect, test } from "bun:test"; import { hkdfSha256, utf8 } from "./sync-hash"; import { decodeGrant, encodeGrant, isPublicUserContentPath, isUserContentPath, USERCONTENT_GRANT_TTL_MS, userContentOriginProblem, userContentOriginWarning, } from "./usercontent-policy"; const FORGE = "forge.example.com"; const DOMAINS = { sites: "sites.example.net", editor: "https://editor.example.org" }; const check = (raw: string) => userContentOriginProblem(raw, FORGE, DOMAINS); describe("userContentOriginProblem", () => { test("accepts a separate https registrable domain", () => { expect(check("https://usercontent.example.io")).toBeNull(); }); test("accepts a localhost dev origin over http", () => { expect(check("http://usercontent.localhost:3000")).toBeNull(); }); test("rejects the forge host and its case variants", () => { expect(check("https://forge.example.com")).toContain("forge host"); expect(check("https://FORGE.example.com")).toContain("forge host"); }); test("warns, without disabling isolation, for origins on the same site as the forge", () => { expect(check("https://uc.forge.example.com")).toBeNull(); expect(userContentOriginWarning("https://uc.forge.example.com", FORGE)).toContain("registrable domain"); expect(userContentOriginWarning("https://example.com", FORGE)).toContain("registrable domain"); expect(userContentOriginWarning("https://uc.example.com", FORGE)).toContain("registrable domain"); expect(userContentOriginWarning("https://usercontent.example.io", FORGE)).toBeNull(); expect(userContentOriginWarning("http://usercontent.localhost:3000", "localhost")).toBeNull(); }); test("rejects hosts under the sites and editor origins", () => { expect(check("https://a.sites.example.net")).toContain("SITES_DOMAIN"); expect(check("https://editor.example.org")).toContain("editor origin"); }); test("rejects http, paths, queries and garbage", () => { expect(check("http://usercontent.example.io")).toContain("https"); expect(check("https://usercontent.example.io/files")).toContain("bare origin"); expect(check("https://usercontent.example.io/?x=1")).toContain("bare origin"); expect(check("not a url")).toContain("valid URL"); }); }); describe("user-content grants", () => { const key = hkdfSha256(utf8("test-secret"), "gitfor-usercontent", "grant-v1"); const otherKey = hkdfSha256(utf8("other-secret"), "gitfor-usercontent", "grant-v1"); const now = 1_700_000_000_000; const path = "/alice/project/raw/main/README.md"; test("round-trips the viewer id for the signed path", () => { const grant = encodeGrant(key, "user_1", path, now + 60_000); expect(decodeGrant(key, grant, path, now)?.viewerId).toBe("user_1"); }); test("is bound to the path", () => { const grant = encodeGrant(key, "user_1", path, now + 60_000); expect(decodeGrant(key, grant, "/alice/project/raw/main/secrets.env", now)).toBeNull(); }); test("expires and rejects far-future expiries", () => { expect(decodeGrant(key, encodeGrant(key, "user_1", path, now - 1), path, now)).toBeNull(); const tooLong = encodeGrant(key, "user_1", path, now + USERCONTENT_GRANT_TTL_MS + 1000); expect(decodeGrant(key, tooLong, path, now)).toBeNull(); }); test("rejects grants from another key or with a tampered viewer", () => { const grant = encodeGrant(key, "user_1", path, now + 60_000); expect(decodeGrant(otherKey, grant, path, now)).toBeNull(); const [expires, , mac] = grant.split("."); const forged = `${expires}.${Buffer.from("user_2").toString("hex")}.${mac}`; expect(decodeGrant(key, forged, path, now)).toBeNull(); }); test("rejects malformed grants", () => { expect(decodeGrant(key, "", path, now)).toBeNull(); expect(decodeGrant(key, "a.b", path, now)).toBeNull(); expect(decodeGrant(key, "zz.zz.zz.zz", path, now)).toBeNull(); expect(decodeGrant(key, "x".repeat(300), path, now)).toBeNull(); }); }); describe("isPublicUserContentPath", () => { test("only avatars skip the viewer grant", () => { expect(isPublicUserContentPath("/avatars/abc-def")).toBe(true); expect(isPublicUserContentPath("/avatars/u/alice")).toBe(true); for (const path of ["/alice/project/raw/main/README.md", "/attachments/abc123/shot.png", "/camo/0011/2233"]) { expect(isPublicUserContentPath(path)).toBe(false); } }); }); describe("isUserContentPath", () => { test("matches every read-only user-bytes route", () => { for (const path of [ "/alice/project/raw/main/README.md", "/alice/project/archive/main.tar.gz", "/alice/project/releases/download/v1.0/app.zip", "/attachments/abc123/shot.png", "/avatars/abc-def", "/avatars/u/alice", "/camo/0011/2233", ]) { expect(isUserContentPath(path)).toBe(true); } }); test("leaves pages, uploads and APIs on the forge", () => { for (const path of [ "/alice/project", "/alice/project/attachments", "/alice/project/releases/rel1/assets", "/settings/avatar", "/api/v1/repos/alice/project", "/camo/0011", "/alice/project/blob/main/README.md", ]) { expect(isUserContentPath(path)).toBe(false); } }); });