| 1 | import { createHash, generateKeyPairSync, sign } from "node:crypto"; |
| 2 | import { execFileSync, spawnSync } from "node:child_process"; |
| 3 | import { readFileSync, mkdtempSync, writeFileSync, rmSync, symlinkSync, linkSync } from "node:fs"; |
| 4 | import { tmpdir } from "node:os"; |
| 5 | import { join } from "node:path"; |
| 6 | import { fileURLToPath } from "node:url"; |
| 7 | import { describe, expect, it, vi } from "vitest"; |
| 8 | import { |
| 9 | etagFor, resolveCloudFacts, verifyEnvelope, signingMessage, responseFor, |
| 10 | MAX_ENVELOPE_BYTES, type CloudFactsEnvelope, type FactsCurrentRow, type CloudFactsResult, |
| 11 | } from "./cloud-facts"; |
| 12 | import { TRUSTED_KEYS, type TrustedKey } from "./cloud-facts/keys"; |
| 13 | import { GET, HEAD } from "../app/api/facts/v1/[channel]/route"; |
| 14 | import { activePublishingKey, emitSql, readBoundedFile, readBoundedResponse, verifyEnvelope as verifyForPublisher } from "../scripts/facts-publish.mjs"; |
| 15 | import { parseRustKeys, parseTsKeys } from "../scripts/check-cloud-facts.mjs"; |
| 16 | |
| 17 | const fixturePath = new URL("../../docs/cloud-facts/fixtures/envelope-stable-v7.json", import.meta.url); |
| 18 | const fixture = JSON.parse(readFileSync(fixturePath, "utf8")) as CloudFactsEnvelope; |
| 19 | const futureFixture = JSON.parse(readFileSync(new URL("../../docs/cloud-facts/fixtures/envelope-future-only-v8.json", import.meta.url), "utf8")) as CloudFactsEnvelope; |
| 20 | const TEST_KEY: TrustedKey = { keyId: "cwf-test-only", publicKey: "8+FLDW4OorUETUVks0hpQAi5Lj4wg3kjKjfYFzLbJ7U=", status: "active" }; |
| 21 | const NOW = Date.parse("2026-09-07T00:00:00Z"); |
| 22 | const basePayload = JSON.parse(Buffer.from(fixture.payload_b64, "base64").toString("utf8")); |
| 23 | // Ephemeral test keys exist only in memory and never enter production anchors. |
| 24 | const ephemeral = generateKeyPairSync("ed25519"); |
| 25 | const EPHEMERAL_KEY: TrustedKey = { keyId: "cwf-ephemeral-test", publicKey: ephemeral.publicKey.export({ type: "spki", format: "der" }).subarray(-32).toString("base64"), status: "active" }; |
| 26 | |
| 27 | function signed(overrides: Record<string, unknown> = {}): CloudFactsEnvelope { |
| 28 | const payload = { ...basePayload, ...overrides }; |
| 29 | const bytes = Buffer.from(JSON.stringify(payload)); |
| 30 | return { |
| 31 | envelope: 1, channel: payload.channel, facts_version: payload.facts_version, |
| 32 | schema_version: payload.schema_version, key_id: EPHEMERAL_KEY.keyId, alg: "ed25519", |
| 33 | applies_to: payload.applies_to, published_at: payload.published_at, |
| 34 | not_after: payload.not_after ?? null, payload_b64: bytes.toString("base64"), |
| 35 | sig_b64: sign(null, signingMessage(EPHEMERAL_KEY.keyId, bytes), ephemeral.privateKey).toString("base64"), |
| 36 | sigs: [], sha256: createHash("sha256").update(bytes).digest("hex"), |
| 37 | }; |
| 38 | } |
| 39 | |
| 40 | function rowFrom(envelope = fixture, overrides: Partial<FactsCurrentRow> = {}): FactsCurrentRow { |
| 41 | return { channel: envelope.channel, release_id: "00000000-0000-0000-0000-000000000001", |
| 42 | facts_version: envelope.facts_version, schema_version: envelope.schema_version, |
| 43 | envelope_version: envelope.envelope, applies_to: envelope.applies_to, key_id: envelope.key_id, |
| 44 | payload_b64: envelope.payload_b64, sig_b64: envelope.sig_b64, sigs: envelope.sigs, |
| 45 | payload_sha256: envelope.sha256, published_at: envelope.published_at, |
| 46 | not_after: envelope.not_after ?? null, ...overrides }; |
| 47 | } |
| 48 | |
| 49 | function supabaseFetch(rows: unknown, status = 200): typeof fetch { |
| 50 | return vi.fn(async () => new Response(JSON.stringify(rows), { status, headers: { "Content-Type": "application/json" } })) as typeof fetch; |
| 51 | } |
| 52 | |
| 53 | class MemKV { |
| 54 | store = new Map<string, string>(); |
| 55 | async get(key: string, type: "stream") { |
| 56 | expect(type).toBe("stream"); |
| 57 | const raw = this.store.get(key); |
| 58 | return raw === undefined ? null : new Response(raw).body; |
| 59 | } |
| 60 | async put(key: string, value: string) { this.store.set(key, value); } |
| 61 | } |
| 62 | const env = { SUPABASE_URL: "https://example.supabase.co", SUPABASE_PUBLISHABLE_KEY: "sb_publishable_test" }; |
| 63 | const opts = { keys: [TEST_KEY], now: () => NOW }; |
| 64 | const failing = supabaseFetch(null, 503); |
| 65 | |
| 66 | function overflowingStream() { |
| 67 | let pulls = 0; |
| 68 | const cancel = vi.fn(); |
| 69 | return { cancel, pulls: () => pulls, stream: new ReadableStream<Uint8Array>({ |
| 70 | pull(controller) { pulls += 1; controller.enqueue(new Uint8Array(MAX_ENVELOPE_BYTES / 2 + 1)); }, |
| 71 | cancel, |
| 72 | }) }; |
| 73 | } |
| 74 | |
| 75 | describe("cloud facts verification", () => { |
| 76 | it("authenticates public fixtures, including a future-only client applicability range", async () => { |
| 77 | for (const envelope of [fixture, futureFixture]) { |
| 78 | expect(await verifyEnvelope(envelope, [TEST_KEY], { channel: "stable", now: NOW })).toEqual({ ok: true, keyId: TEST_KEY.keyId, mode: "verified" }); |
| 79 | expect(verifyForPublisher(envelope, TEST_KEY.publicKey).ok).toBe(true); |
| 80 | } |
| 81 | const result = await resolveCloudFacts("stable", env, { ...opts, fetchImpl: supabaseFetch([rowFrom(futureFixture)]) }); |
| 82 | expect(result).toMatchObject({ kind: "ok", envelope: { applies_to: ">=99.0.0", facts_version: 8 } }); |
| 83 | }); |
| 84 | |
| 85 | it("pins a well-formed production anchor and refuses empty or retired-only trust before any reads", async () => { |
| 86 | // The anchor itself is checked for shape, not for a specific key: pinning a |
| 87 | // second key or rotating must not fail this test, but a malformed one must. |
| 88 | // Byte-for-byte agreement with the Rust table is `check-cloud-facts.mjs`. |
| 89 | expect(TRUSTED_KEYS.length).toBeGreaterThan(0); |
| 90 | for (const key of TRUSTED_KEYS) { |
| 91 | expect(key.keyId).toMatch(/^cwf-[A-Za-z0-9._-]+$/); |
| 92 | expect(["active", "retired"]).toContain(key.status); |
| 93 | // Standard base64 of a raw 32-byte Ed25519 public key. |
| 94 | expect(Buffer.from(key.publicKey, "base64")).toHaveLength(32); |
| 95 | } |
| 96 | expect(TRUSTED_KEYS.some((key) => key.status === "active")).toBe(true); |
| 97 | |
| 98 | // The property that actually matters is unchanged: with no usable key the |
| 99 | // layer fails closed *before* any network or cache read. |
| 100 | const fetchImpl = vi.fn(); |
| 101 | const get = vi.fn(); |
| 102 | for (const keys of [[], [{ ...TEST_KEY, status: "retired" as const }]]) { |
| 103 | expect(await verifyEnvelope(fixture, keys)).toEqual({ ok: false, reason: "no-active-keys" }); |
| 104 | expect(await resolveCloudFacts("stable", { ...env, CURATED_KV: { get, put: vi.fn() } }, { keys, fetchImpl })).toEqual({ kind: "unavailable", reason: "no-active-keys" }); |
| 105 | } |
| 106 | expect(fetchImpl).not.toHaveBeenCalled(); |
| 107 | expect(get).not.toHaveBeenCalled(); |
| 108 | }); |
| 109 | |
| 110 | it("rejects invalid signatures, unknown keys, retired keys and ambiguous key tables", async () => { |
| 111 | expect(await verifyEnvelope({ ...fixture, sig_b64: `A${fixture.sig_b64.slice(1)}` }, [TEST_KEY])).toEqual({ ok: false, reason: "bad-signature" }); |
| 112 | expect(await verifyEnvelope(fixture, [EPHEMERAL_KEY])).toEqual({ ok: false, reason: "unknown-key" }); |
| 113 | expect(await verifyEnvelope(fixture, [{ ...TEST_KEY, status: "retired" }, EPHEMERAL_KEY])).toEqual({ ok: false, reason: "retired-key" }); |
| 114 | expect(await verifyEnvelope(fixture, [TEST_KEY, TEST_KEY])).toEqual({ ok: false, reason: "no-active-keys" }); |
| 115 | }); |
| 116 | |
| 117 | it("cross-checks every unsigned metadata field with the signed payload", async () => { |
| 118 | for (const change of [{ channel: "beta" }, { facts_version: 8 }, { schema_version: 2 }, |
| 119 | { applies_to: ">=99.0.0" }, { published_at: "2026-09-01T00:00:00Z" }, |
| 120 | { not_after: "2026-10-01T00:00:00Z" }, { sha256: "0".repeat(64) }]) { |
| 121 | expect((await verifyEnvelope({ ...fixture, ...change }, [TEST_KEY], { channel: "stable", now: NOW })).ok).toBe(false); |
| 122 | expect(verifyForPublisher({ ...fixture, ...change }, TEST_KEY.publicKey).ok).toBe(false); |
| 123 | } |
| 124 | expect(await verifyEnvelope(signed({ channel: "beta" }), [EPHEMERAL_KEY], { channel: "stable", now: NOW })).toEqual({ ok: false, reason: "wrong-channel" }); |
| 125 | }); |
| 126 | |
| 127 | it("rejects signed bad versions, applicability, schema and UTC dates", async () => { |
| 128 | for (const change of [{ facts_version: "7; DROP TABLE public.facts_key;" }, { facts_version: 0 }, |
| 129 | { facts_version: Number.MAX_SAFE_INTEGER + 1 }, { schema_version: 2 }, { applies_to: "><=3" }, |
| 130 | { published_at: "2026-02-30T00:00:00Z" }, { published_at: "not-a-date" }]) { |
| 131 | const envelope = signed(change); |
| 132 | expect((await verifyEnvelope(envelope, [EPHEMERAL_KEY], { now: NOW })).ok).toBe(false); |
| 133 | expect(verifyForPublisher(envelope, EPHEMERAL_KEY.publicKey).ok).toBe(false); |
| 134 | } |
| 135 | }); |
| 136 | |
| 137 | it("rejects future publication, expiry and reversed signed time windows", async () => { |
| 138 | expect(await verifyEnvelope(fixture, [TEST_KEY], { now: Date.parse("2026-08-29T00:00:00Z") })).toEqual({ ok: false, reason: "bad-payload" }); |
| 139 | const expired = signed({ not_after: "2026-09-06T00:00:00Z" }); |
| 140 | expect(await verifyEnvelope(expired, [EPHEMERAL_KEY], { now: NOW })).toEqual({ ok: false, reason: "expired" }); |
| 141 | expect((await resolveCloudFacts("stable", env, { keys: [EPHEMERAL_KEY], now: () => NOW, fetchImpl: supabaseFetch([rowFrom(expired)]) })).kind).toBe("unverifiable"); |
| 142 | expect((await verifyEnvelope(signed({ not_after: "2026-08-29T00:00:00Z" }), [EPHEMERAL_KEY], { now: NOW })).ok).toBe(false); |
| 143 | }); |
| 144 | |
| 145 | it("rejects oversized/noncanonical base64 and hostile signature shapes without throwing", async () => { |
| 146 | for (const value of [null, [], {}, { ...fixture, payload_b64: "A".repeat(MAX_ENVELOPE_BYTES) }, |
| 147 | { ...fixture, payload_b64: `${fixture.payload_b64}\n` }, { ...fixture, sig_b64: `${fixture.sig_b64}garbage` }, |
| 148 | { ...fixture, sigs: "not-an-array" }, { ...fixture, sigs: Array(9).fill({ key_id: TEST_KEY.keyId, sig_b64: fixture.sig_b64 }) }, |
| 149 | { ...fixture, sigs: [null] }]) { |
| 150 | expect((await verifyEnvelope(value, [TEST_KEY])).ok).toBe(false); |
| 151 | expect(verifyForPublisher(value, TEST_KEY.publicKey).ok).toBe(false); |
| 152 | } |
| 153 | }); |
| 154 | |
| 155 | it("reports the authenticating rotation key and changes ETag for signature-only updates", async () => { |
| 156 | const rotated = { ...fixture, sigs: [{ key_id: EPHEMERAL_KEY.keyId, |
| 157 | sig_b64: sign(null, signingMessage(EPHEMERAL_KEY.keyId, Buffer.from(fixture.payload_b64, "base64")), ephemeral.privateKey).toString("base64") }] }; |
| 158 | const keys = [{ ...TEST_KEY, status: "retired" as const }, EPHEMERAL_KEY]; |
| 159 | expect(await verifyEnvelope(rotated, keys, { now: NOW })).toEqual({ ok: true, keyId: EPHEMERAL_KEY.keyId, mode: "verified" }); |
| 160 | expect(await etagFor(rotated)).not.toBe(await etagFor(fixture)); |
| 161 | const result = await resolveCloudFacts("stable", env, { keys, now: () => NOW, fetchImpl: supabaseFetch([rowFrom(rotated)]) }); |
| 162 | expect(result).toMatchObject({ kind: "ok", keyId: EPHEMERAL_KEY.keyId }); |
| 163 | const request = new Request("https://example.test", { headers: { "if-none-match": await etagFor(fixture) } }); |
| 164 | const response = responseFor(result, request, "stable", "GET"); |
| 165 | expect(response.status).toBe(200); |
| 166 | expect(response.headers.get("x-facts-key")).toBe(EPHEMERAL_KEY.keyId); |
| 167 | }); |
| 168 | }); |
| 169 | |
| 170 | describe("cloud facts transport", () => { |
| 171 | it("uses one global channel query with a publishable credential and writes only a verified cache", async () => { |
| 172 | const kv = new MemKV(); |
| 173 | const fetchImpl = supabaseFetch([rowFrom(fixture, { published_at: "2026-08-30T00:00:00+00:00" })]); |
| 174 | const result = await resolveCloudFacts("stable", { ...env, CURATED_KV: kv }, { ...opts, fetchImpl }); |
| 175 | expect(result).toMatchObject({ kind: "ok", source: "supabase", verified: "verified" }); |
| 176 | const [url, init] = vi.mocked(fetchImpl).mock.calls[0]; |
| 177 | const parsed = new URL(String(url)); |
| 178 | expect(parsed.origin).toBe(env.SUPABASE_URL); |
| 179 | expect(parsed.pathname).toBe("/rest/v1/facts_current"); |
| 180 | expect(Object.fromEntries(parsed.searchParams)).toMatchObject({ channel: "eq.stable", scope: "eq.global", limit: "1" }); |
| 181 | expect(init?.headers).toMatchObject({ apikey: env.SUPABASE_PUBLISHABLE_KEY, Authorization: `Bearer ${env.SUPABASE_PUBLISHABLE_KEY}` }); |
| 182 | expect(init?.redirect).toBe("error"); |
| 183 | if (result.kind !== "ok") throw new Error("expected verified result"); |
| 184 | expect(kv.store.get("facts:cloud:stable")).toBe(result.body); |
| 185 | expect(result.etag).toBe(`"${createHash("sha256").update(result.body).digest("hex")}"`); |
| 186 | }); |
| 187 | |
| 188 | it("rejects service/secret credentials before dispatch", async () => { |
| 189 | const fetchImpl = vi.fn(); |
| 190 | const serviceJwt = `a.${Buffer.from(JSON.stringify({ role: "service_role" })).toString("base64url")}.b`; |
| 191 | for (const key of ["sb_secret_do-not-send", serviceJwt]) { |
| 192 | expect((await resolveCloudFacts("stable", { ...env, SUPABASE_PUBLISHABLE_KEY: key }, { ...opts, fetchImpl })).kind).toBe("unavailable"); |
| 193 | } |
| 194 | expect(fetchImpl).not.toHaveBeenCalled(); |
| 195 | }); |
| 196 | |
| 197 | it("distinguishes no row and invalid channel and refuses a mismatched signed channel", async () => { |
| 198 | const fetchImpl = supabaseFetch([]); |
| 199 | expect((await resolveCloudFacts("Bad Slug", env, { ...opts, fetchImpl })).kind).toBe("none"); |
| 200 | expect(fetchImpl).not.toHaveBeenCalled(); |
| 201 | expect((await resolveCloudFacts("stable", env, { ...opts, fetchImpl })).kind).toBe("none"); |
| 202 | const beta = signed({ channel: "beta" }); |
| 203 | expect(await resolveCloudFacts("stable", env, { keys: [EPHEMERAL_KEY], now: () => NOW, fetchImpl: supabaseFetch([rowFrom(beta)]) })).toMatchObject({ kind: "unverifiable", reason: "wrong-channel" }); |
| 204 | }); |
| 205 | |
| 206 | it("never caches digest/signature failures and does not amplify them through a 304", async () => { |
| 207 | for (const change of [{ payload_sha256: "0".repeat(64) }, { sig_b64: `A${fixture.sig_b64.slice(1)}` }]) { |
| 208 | const kv = new MemKV(); |
| 209 | const result = await resolveCloudFacts("stable", { ...env, CURATED_KV: kv }, { ...opts, fetchImpl: supabaseFetch([rowFrom(fixture, change)]) }); |
| 210 | expect(["sha-mismatch", "unverifiable"]).toContain(result.kind); |
| 211 | expect(kv.store.size).toBe(0); |
| 212 | expect(responseFor(result, new Request("https://example.test", { headers: { "if-none-match": "*" } }), "stable", "GET").status).not.toBe(304); |
| 213 | } |
| 214 | }); |
| 215 | |
| 216 | it("revalidates cached digest, channel, expiry and current trust after an outage", async () => { |
| 217 | const kv = new MemKV(); |
| 218 | await kv.put("facts:cloud:stable", JSON.stringify(fixture)); |
| 219 | expect(await resolveCloudFacts("stable", { ...env, CURATED_KV: kv }, { ...opts, fetchImpl: failing })).toMatchObject({ kind: "ok", source: "kv-stale" }); |
| 220 | for (const envelope of [{ ...fixture, sha256: "0".repeat(64) }, signed({ channel: "beta" }), signed({ not_after: "2026-09-06T00:00:00Z" })]) { |
| 221 | await kv.put("facts:cloud:stable", JSON.stringify(envelope)); |
| 222 | expect((await resolveCloudFacts("stable", { ...env, CURATED_KV: kv }, { keys: [TEST_KEY, EPHEMERAL_KEY], now: () => NOW, fetchImpl: failing })).kind).toBe("unavailable"); |
| 223 | } |
| 224 | await kv.put("facts:cloud:stable", JSON.stringify(fixture)); |
| 225 | expect((await resolveCloudFacts("stable", { ...env, CURATED_KV: kv }, { keys: [EPHEMERAL_KEY], now: () => NOW, fetchImpl: failing })).kind).toBe("unavailable"); |
| 226 | }); |
| 227 | |
| 228 | it("caps and cancels PostgREST streaming bodies before JSON parsing", async () => { |
| 229 | const oversized = overflowingStream(); |
| 230 | const fetchImpl = vi.fn(async () => new Response(oversized.stream)); |
| 231 | expect((await resolveCloudFacts("stable", env, { ...opts, fetchImpl })).kind).toBe("unavailable"); |
| 232 | expect(oversized.cancel).toHaveBeenCalledOnce(); |
| 233 | expect(oversized.pulls()).toBeLessThanOrEqual(3); |
| 234 | const declared = overflowingStream(); |
| 235 | expect((await resolveCloudFacts("stable", env, { ...opts, fetchImpl: vi.fn(async () => new Response(declared.stream, { headers: { "content-length": String(MAX_ENVELOPE_BYTES + 1) } })) })).kind).toBe("unavailable"); |
| 236 | expect(declared.cancel).toHaveBeenCalledOnce(); |
| 237 | }); |
| 238 | |
| 239 | it("caps KV streams and isolates malformed cache objects", async () => { |
| 240 | const oversized = overflowingStream(); |
| 241 | const get = vi.fn(async () => oversized.stream); |
| 242 | expect((await resolveCloudFacts("stable", { ...env, CURATED_KV: { get, put: vi.fn() } }, { ...opts, fetchImpl: failing })).kind).toBe("unavailable"); |
| 243 | expect(get).toHaveBeenCalledWith("facts:cloud:stable", "stream"); |
| 244 | expect(oversized.cancel).toHaveBeenCalledOnce(); |
| 245 | const kv = new MemKV(); |
| 246 | for (const bad of ["not-json", "null", JSON.stringify({ sigs: null })]) { |
| 247 | await kv.put("facts:cloud:stable", bad); |
| 248 | expect((await resolveCloudFacts("stable", { ...env, CURATED_KV: kv }, { ...opts, fetchImpl: failing })).kind).toBe("unavailable"); |
| 249 | } |
| 250 | }); |
| 251 | }); |
| 252 | |
| 253 | describe("facts response protocol", () => { |
| 254 | const req = (headers: Record<string, string> = {}) => new Request("https://codewhale.net/api/facts/v1/stable", { headers }); |
| 255 | async function ok(): Promise<CloudFactsResult> { return { kind: "ok", envelope: fixture, body: JSON.stringify(fixture), etag: await etagFor(fixture), source: "supabase", verified: "verified", keyId: TEST_KEY.keyId }; } |
| 256 | |
| 257 | it("serves cacheable GET, conditional 304 and bodyless successful HEAD", async () => { |
| 258 | const result = await ok(); |
| 259 | if (result.kind !== "ok") throw new Error("fixture result"); |
| 260 | const get = responseFor(result, req(), "stable", "GET"); |
| 261 | expect(get.status).toBe(200); |
| 262 | expect(get.headers.get("cache-control")).toContain("s-maxage=300"); |
| 263 | expect(get.headers.get("set-cookie")).toBeNull(); |
| 264 | expect(get.headers.get("vary")).toBeNull(); |
| 265 | expect(await get.json()).toEqual(fixture); |
| 266 | for (const tag of [result.etag, `W/${result.etag}`, "*"]) { |
| 267 | const response = responseFor(result, req({ "if-none-match": tag }), "stable", "GET"); |
| 268 | expect(response.status).toBe(304); |
| 269 | expect(await response.text()).toBe(""); |
| 270 | } |
| 271 | const head = responseFor(result, req(), "stable", "HEAD"); |
| 272 | expect(head.headers.get("content-length")).toBe(String(Buffer.byteLength(result.body))); |
| 273 | expect(await head.text()).toBe(""); |
| 274 | }); |
| 275 | |
| 276 | it("returns bodyless HEAD for every error and the invalid-channel route", async () => { |
| 277 | const cases: CloudFactsResult[] = [{ kind: "none" }, { kind: "sha-mismatch", channel: "stable", factsVersion: 7 }, |
| 278 | { kind: "unverifiable", channel: "stable", factsVersion: 7, reason: "bad-signature" }, { kind: "unavailable", reason: "no-active-keys" }]; |
| 279 | for (const result of cases) { |
| 280 | const response = responseFor(result, req(), "stable", "HEAD"); |
| 281 | expect([404, 502, 503]).toContain(response.status); |
| 282 | expect(await response.text()).toBe(""); |
| 283 | } |
| 284 | expect(await (await HEAD(req(), { params: Promise.resolve({ channel: "Bad Slug" }) })).text()).toBe(""); |
| 285 | const get = await GET(req(), { params: Promise.resolve({ channel: "Bad Slug" }) }); |
| 286 | expect(get.status).toBe(404); |
| 287 | }); |
| 288 | |
| 289 | it("keeps CDN freshness inside a signed expiry without stale-serving extensions", async () => { |
| 290 | const envelope = signed({ not_after: new Date(Date.now() + 90_000).toISOString() }); |
| 291 | const result = await resolveCloudFacts("stable", env, { keys: [EPHEMERAL_KEY], fetchImpl: supabaseFetch([rowFrom(envelope)]) }); |
| 292 | expect(result.kind).toBe("ok"); |
| 293 | const response = responseFor(result, req(), "stable", "GET"); |
| 294 | const control = response.headers.get("cache-control")!; |
| 295 | expect(control).toContain("must-revalidate"); |
| 296 | expect(control).not.toContain("stale-"); |
| 297 | expect(Number(control.match(/s-maxage=(\d+)/)?.[1])).toBeLessThanOrEqual(90); |
| 298 | }); |
| 299 | }); |
| 300 | |
| 301 | describe("facts publisher boundaries", () => { |
| 302 | const script = fileURLToPath(new URL("../scripts/facts-publish.mjs", import.meta.url)); |
| 303 | |
| 304 | it("requires an active pinned primary key and cannot publish with an explicit fixture public key", () => { |
| 305 | expect(() => activePublishingKey(fixture, [])).toThrow("not pinned and active"); |
| 306 | expect(() => activePublishingKey(fixture, [{ ...TEST_KEY, status: "retired" }])).toThrow("not pinned and active"); |
| 307 | expect(activePublishingKey(fixture, [TEST_KEY], NOW).check.ok).toBe(true); |
| 308 | const result = spawnSync(process.execPath, [script, "publish", fileURLToPath(fixturePath), "--dry-run", "--public-key", TEST_KEY.publicKey], { encoding: "utf8" }); |
| 309 | expect(result.status).toBe(1); |
| 310 | expect(result.stderr).toContain("publication requires the active pinned table"); |
| 311 | }); |
| 312 | |
| 313 | it("refuses to publish authentically signed future or expired facts", () => { |
| 314 | expect(() => activePublishingKey(signed({ not_after: "2026-09-06T00:00:00Z" }), [EPHEMERAL_KEY], NOW)).toThrow("future or expired"); |
| 315 | expect(() => activePublishingKey(signed({ published_at: "2026-09-08T00:00:00Z" }), [EPHEMERAL_KEY], NOW)).toThrow("future or expired"); |
| 316 | }); |
| 317 | |
| 318 | it("refuses CI signing before reading a source or private credential path", () => { |
| 319 | const result = spawnSync(process.execPath, [script, "sign", "--source", "/nonexistent-source-must-not-be-read"], { |
| 320 | encoding: "utf8", env: { ...process.env, CI: "true", CODEWHALE_FACTS_SIGNING_KEY_FILE: "/nonexistent-key-must-not-be-read" }, |
| 321 | }); |
| 322 | expect(result.status).toBe(1); |
| 323 | expect(result.stderr).toContain("refusing to run with a secret under CI"); |
| 324 | expect(result.stderr).not.toContain("ENOENT"); |
| 325 | }); |
| 326 | |
| 327 | it("creates private keys exclusively without overwriting an existing file or symlink", () => { |
| 328 | const dir = mkdtempSync(join(tmpdir(), "facts-key-exclusion-")); |
| 329 | try { |
| 330 | const target = join(dir, "existing.key"); |
| 331 | writeFileSync(target, "preserve-existing-file"); |
| 332 | const cleanEnv = { ...process.env }; |
| 333 | for (const marker of ["CI", "GITHUB_ACTIONS", "GITLAB_CI", "BUILDKITE", "CIRCLECI", "JENKINS_URL", "TF_BUILD"]) delete cleanEnv[marker]; |
| 334 | const result = spawnSync(process.execPath, [script, "keygen", "--key-id", "cwf-test-exclusive", "--out", target], { encoding: "utf8", env: cleanEnv }); |
| 335 | expect(result.status).toBe(1); |
| 336 | expect(readFileSync(target, "utf8")).toBe("preserve-existing-file"); |
| 337 | if (process.platform !== "win32") { |
| 338 | const link = join(dir, "linked.key"); |
| 339 | symlinkSync(target, link); |
| 340 | expect(spawnSync(process.execPath, [script, "keygen", "--key-id", "cwf-test-exclusive", "--out", link], { encoding: "utf8", env: cleanEnv }).status).toBe(1); |
| 341 | expect(readFileSync(target, "utf8")).toBe("preserve-existing-file"); |
| 342 | } |
| 343 | } finally { rmSync(dir, { recursive: true, force: true }); } |
| 344 | }); |
| 345 | |
| 346 | it("rejects signed numeric SQL injection and emits escaped SQL only after verification", () => { |
| 347 | const malicious = signed({ facts_version: "7; DROP TABLE public.facts_key;" }); |
| 348 | expect(() => emitSql(malicious, { publicKeyB64: EPHEMERAL_KEY.publicKey })).toThrow("positive safe integer"); |
| 349 | const sql = emitSql(fixture, { publicKeyB64: TEST_KEY.publicKey, publishedBy: "O'Hara" }); |
| 350 | expect(sql).toContain("select c.id, 7, 1, 1"); |
| 351 | expect(sql).toContain("O''Hara"); |
| 352 | }); |
| 353 | |
| 354 | it("parses intentional empty key tables but fails closed on unknown syntax or duplicate/invalid anchors", () => { |
| 355 | expect(parseTsKeys("export const TRUSTED_KEYS: readonly TrustedKey[] = [];")).toEqual([]); |
| 356 | expect(parseRustKeys("pub const TRUSTED_KEYS: &[TrustedKey] = &[];")).toEqual([]); |
| 357 | for (const text of ["no table", "export const TRUSTED_KEYS: readonly TrustedKey[] = [makeKey()];"]) expect(() => parseTsKeys(text)).toThrow(); |
| 358 | expect(() => parseRustKeys("pub const TRUSTED_KEYS: &[TrustedKey] = &[make_key()];")).toThrow(); |
| 359 | expect(() => parseTsKeys("// export const TRUSTED_KEYS: readonly TrustedKey[] = [];\nexport const TRUSTED_KEYS = makeKeys();")).toThrow(); |
| 360 | expect(() => parseRustKeys("// pub const TRUSTED_KEYS: &[TrustedKey] = &[];\npub const TRUSTED_KEYS: &[TrustedKey] = &[make_key()];")).toThrow(); |
| 361 | const entry = `{ keyId: "${TEST_KEY.keyId}", publicKey: "${TEST_KEY.publicKey}", status: "active" }`; |
| 362 | expect(() => parseTsKeys(`export const TRUSTED_KEYS: readonly TrustedKey[] = [${entry}, ${entry}];`)).toThrow(); |
| 363 | expect(() => parseTsKeys(`export const TRUSTED_KEYS: readonly TrustedKey[] = [${entry.replace(TEST_KEY.publicKey, "bad")}];`)).toThrow(); |
| 364 | const rust = `pub const TRUSTED_KEYS: &[TrustedKey] = &[TrustedKey { key_id: "${TEST_KEY.keyId}", public_key: [${[...Buffer.from(TEST_KEY.publicKey, "base64")].join(",")}], status: KeyStatus::Active }];`; |
| 365 | expect(parseRustKeys(rust)).toEqual([TEST_KEY]); |
| 366 | }); |
| 367 | |
| 368 | it("bounds publisher file/response reads and rejects symlink and hardlink inputs", async () => { |
| 369 | const dir = mkdtempSync(join(tmpdir(), "facts-bounded-read-")); |
| 370 | try { |
| 371 | const target = join(dir, "source.json"); |
| 372 | writeFileSync(target, "12345"); |
| 373 | expect(() => readBoundedFile(target, 4)).toThrow(); |
| 374 | if (process.platform !== "win32") { |
| 375 | const link = join(dir, "symlink.json"); |
| 376 | symlinkSync(target, link); |
| 377 | expect(() => readBoundedFile(link)).toThrow(); |
| 378 | linkSync(target, join(dir, "hardlink.json")); |
| 379 | expect(() => readBoundedFile(target)).toThrow(); |
| 380 | } |
| 381 | const oversized = overflowingStream(); |
| 382 | await expect(readBoundedResponse(new Response(oversized.stream))).rejects.toThrow("size limit"); |
| 383 | expect(oversized.cancel).toHaveBeenCalledOnce(); |
| 384 | } finally { rmSync(dir, { recursive: true, force: true }); } |
| 385 | }); |
| 386 | |
| 387 | it("the local facts gate verifies both public fixtures and reports the pinned anchor count", () => { |
| 388 | const checker = fileURLToPath(new URL("../scripts/check-cloud-facts.mjs", import.meta.url)); |
| 389 | // Derived from the table rather than hardcoded, so rotating or adding an |
| 390 | // anchor does not require editing this assertion — only a gate that has |
| 391 | // drifted out of step with the table will fail it. |
| 392 | const active = TRUSTED_KEYS.filter((key) => key.status === "active").length; |
| 393 | expect(execFileSync(process.execPath, [checker], { encoding: "utf8" })).toContain( |
| 394 | `${active} active production keys`, |
| 395 | ); |
| 396 | }); |
| 397 | }); |
| 398 |