返回 CodeWhale
cloud-facts.test.ts
根目录 / web / lib / cloud-facts.test.ts
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, publishRelease, readBoundedFile, readBoundedResponse, revokeRelease, 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("never resurrects a revoked head through the stale cache after an outage", async () => {
229 const kv = new MemKV();
230 const ttls: unknown[] = [];
231 const put = kv.put.bind(kv);
232 kv.put = async (key: string, value: string, options?: { expirationTtl?: number }) => { ttls.push(options?.expirationTtl); await put(key, value); };
233 expect((await resolveCloudFacts("stable", { ...env, CURATED_KV: kv }, { ...opts, fetchImpl: supabaseFetch([rowFrom(fixture)]) })).kind).toBe("ok");
234 // The operator revokes the head: facts_current returns no row.
235 expect((await resolveCloudFacts("stable", { ...env, CURATED_KV: kv }, { ...opts, fetchImpl: supabaseFetch([]) })).kind).toBe("none");
236 // Supabase then goes down. The revoked envelope must not come back.
237 expect(await resolveCloudFacts("stable", { ...env, CURATED_KV: kv }, { ...opts, fetchImpl: failing })).toMatchObject({ kind: "unavailable" });
238 // The bridge lasts no longer than a client's own 6 h staleness bound.
239 expect(ttls.length).toBeGreaterThan(0);
240 expect(ttls.every((ttl) => ttl === 6 * 60 * 60)).toBe(true);
241 });
242
243 it("caps and cancels PostgREST streaming bodies before JSON parsing", async () => {
244 const oversized = overflowingStream();
245 const fetchImpl = vi.fn(async () => new Response(oversized.stream));
246 expect((await resolveCloudFacts("stable", env, { ...opts, fetchImpl })).kind).toBe("unavailable");
247 expect(oversized.cancel).toHaveBeenCalledOnce();
248 expect(oversized.pulls()).toBeLessThanOrEqual(3);
249 const declared = overflowingStream();
250 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");
251 expect(declared.cancel).toHaveBeenCalledOnce();
252 });
253
254 it("caps KV streams and isolates malformed cache objects", async () => {
255 const oversized = overflowingStream();
256 const get = vi.fn(async () => oversized.stream);
257 expect((await resolveCloudFacts("stable", { ...env, CURATED_KV: { get, put: vi.fn() } }, { ...opts, fetchImpl: failing })).kind).toBe("unavailable");
258 expect(get).toHaveBeenCalledWith("facts:cloud:stable", "stream");
259 expect(oversized.cancel).toHaveBeenCalledOnce();
260 const kv = new MemKV();
261 for (const bad of ["not-json", "null", JSON.stringify({ sigs: null })]) {
262 await kv.put("facts:cloud:stable", bad);
263 expect((await resolveCloudFacts("stable", { ...env, CURATED_KV: kv }, { ...opts, fetchImpl: failing })).kind).toBe("unavailable");
264 }
265 });
266 });
267
268 describe("facts response protocol", () => {
269 const req = (headers: Record<string, string> = {}) => new Request("https://codewhale.net/api/facts/v1/stable", { headers });
270 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 }; }
271
272 it("serves cacheable GET, conditional 304 and bodyless successful HEAD", async () => {
273 const result = await ok();
274 if (result.kind !== "ok") throw new Error("fixture result");
275 const get = responseFor(result, req(), "stable", "GET");
276 expect(get.status).toBe(200);
277 expect(get.headers.get("cache-control")).toContain("s-maxage=300");
278 expect(get.headers.get("set-cookie")).toBeNull();
279 expect(get.headers.get("vary")).toBeNull();
280 expect(await get.json()).toEqual(fixture);
281 for (const tag of [result.etag, `W/${result.etag}`, "*"]) {
282 const response = responseFor(result, req({ "if-none-match": tag }), "stable", "GET");
283 expect(response.status).toBe(304);
284 expect(await response.text()).toBe("");
285 }
286 const head = responseFor(result, req(), "stable", "HEAD");
287 expect(head.headers.get("content-length")).toBe(String(Buffer.byteLength(result.body)));
288 expect(await head.text()).toBe("");
289 });
290
291 it("returns bodyless HEAD for every error and the invalid-channel route", async () => {
292 const cases: CloudFactsResult[] = [{ kind: "none" }, { kind: "sha-mismatch", channel: "stable", factsVersion: 7 },
293 { kind: "unverifiable", channel: "stable", factsVersion: 7, reason: "bad-signature" }, { kind: "unavailable", reason: "no-active-keys" }];
294 for (const result of cases) {
295 const response = responseFor(result, req(), "stable", "HEAD");
296 expect([404, 502, 503]).toContain(response.status);
297 expect(await response.text()).toBe("");
298 }
299 expect(await (await HEAD(req(), { params: Promise.resolve({ channel: "Bad Slug" }) })).text()).toBe("");
300 const get = await GET(req(), { params: Promise.resolve({ channel: "Bad Slug" }) });
301 expect(get.status).toBe(404);
302 });
303
304 it("keeps CDN freshness inside a signed expiry without stale-serving extensions", async () => {
305 const envelope = signed({ not_after: new Date(Date.now() + 90_000).toISOString() });
306 const result = await resolveCloudFacts("stable", env, { keys: [EPHEMERAL_KEY], fetchImpl: supabaseFetch([rowFrom(envelope)]) });
307 expect(result.kind).toBe("ok");
308 const response = responseFor(result, req(), "stable", "GET");
309 const control = response.headers.get("cache-control")!;
310 expect(control).toContain("must-revalidate");
311 expect(control).not.toContain("stale-");
312 expect(Number(control.match(/s-maxage=(\d+)/)?.[1])).toBeLessThanOrEqual(90);
313 });
314 });
315
316 describe("facts publisher boundaries", () => {
317 const script = fileURLToPath(new URL("../scripts/facts-publish.mjs", import.meta.url));
318
319 /** A PostgREST double over one channel and its releases. */
320 function fakePostgrest(head: number, releases: { facts_version: number; status: string; payload_sha256: string; revoked_at?: string; revoke_reason?: string }[]) {
321 const calls: string[] = [];
322 const request = async (path: string, init: { method?: string; body?: Record<string, unknown> } = {}) => {
323 calls.push(`${init.method ?? "GET"} ${path}`);
324 if (path.startsWith("facts_channel")) return [{ id: "c1", max_facts_version: head }];
325 if (path === "facts_key") return null;
326 if (path === "facts_release") {
327 if (Number(init.body?.facts_version) <= head) throw Object.assign(new Error("PostgREST request failed (HTTP 409)"), { status: 409 });
328 head = Number(init.body?.facts_version);
329 const stored = { id: "r-new", facts_version: head, status: "published", payload_sha256: fixture.sha256 };
330 releases.push(stored);
331 return [stored];
332 }
333 const version = Number(/facts_version=eq\.(\d+)/.exec(path)?.[1]);
334 const matches = releases.filter((r) => r.facts_version === version && (!path.includes("status=eq.published") || r.status === "published"));
335 if (init.method === "PATCH") for (const r of matches) Object.assign(r, init.body);
336 return matches.map((r) => ({ id: `r-${r.facts_version}`, ...r }));
337 };
338 return { request, calls, releases };
339 }
340
341 it("revokes once, keeps the first revocation on a re-run, and refuses an unknown version", async () => {
342 const pg = fakePostgrest(3, [{ facts_version: 3, status: "published", payload_sha256: "a" }]);
343 expect(await revokeRelease(pg.request, { channel: "stable", version: 3, reason: "bad price", at: "2026-09-30T00:00:00Z" }))
344 .toMatchObject({ revoked: 1, already_revoked: false, head: true });
345 const again = await revokeRelease(pg.request, { channel: "stable", version: 3, reason: "second run", at: "2026-10-01T00:00:00Z" });
346 expect(again).toMatchObject({ revoked: 0, already_revoked: true, revoked_at: "2026-09-30T00:00:00Z", revoke_reason: "bad price" });
347 expect(pg.releases[0]).toMatchObject({ revoked_at: "2026-09-30T00:00:00Z", revoke_reason: "bad price" });
348 await expect(revokeRelease(pg.request, { channel: "stable", version: 9, reason: "typo", at: "2026-10-01T00:00:00Z" })).rejects.toThrow("no published facts_version 9");
349 });
350
351 it("publishes exactly the envelope and treats a replay of the same bytes as already published", async () => {
352 const pg = fakePostgrest(fixture.facts_version - 1, []);
353 const row = { facts_version: fixture.facts_version };
354 expect(await publishRelease(pg.request, fixture, row, "pub")).toMatchObject({ published: true, payload_sha256: fixture.sha256 });
355 expect(await publishRelease(pg.request, fixture, row, "pub")).toMatchObject({ published: false, already_published: true, release_id: "r-new" });
356 const other = { ...fixture, sha256: "f".repeat(64) };
357 await expect(publishRelease(pg.request, other, row, "pub")).rejects.toThrow("high-water mark");
358 });
359
360 it("requires an active pinned primary key and cannot publish with an explicit fixture public key", () => {
361 expect(() => activePublishingKey(fixture, [])).toThrow("not pinned and active");
362 expect(() => activePublishingKey(fixture, [{ ...TEST_KEY, status: "retired" }])).toThrow("not pinned and active");
363 expect(activePublishingKey(fixture, [TEST_KEY], NOW).check.ok).toBe(true);
364 const result = spawnSync(process.execPath, [script, "publish", fileURLToPath(fixturePath), "--dry-run", "--public-key", TEST_KEY.publicKey], { encoding: "utf8" });
365 expect(result.status).toBe(1);
366 expect(result.stderr).toContain("publication requires the active pinned table");
367 });
368
369 it("refuses to publish authentically signed future or expired facts", () => {
370 expect(() => activePublishingKey(signed({ not_after: "2026-09-06T00:00:00Z" }), [EPHEMERAL_KEY], NOW)).toThrow("future or expired");
371 expect(() => activePublishingKey(signed({ published_at: "2026-09-08T00:00:00Z" }), [EPHEMERAL_KEY], NOW)).toThrow("future or expired");
372 });
373
374 it("refuses CI signing before reading a source or private credential path", () => {
375 const result = spawnSync(process.execPath, [script, "sign", "--source", "/nonexistent-source-must-not-be-read"], {
376 encoding: "utf8", env: { ...process.env, CI: "true", CODEWHALE_FACTS_SIGNING_KEY_FILE: "/nonexistent-key-must-not-be-read" },
377 });
378 expect(result.status).toBe(1);
379 expect(result.stderr).toContain("refusing to run with a secret under CI");
380 expect(result.stderr).not.toContain("ENOENT");
381 });
382
383 it("creates private keys exclusively without overwriting an existing file or symlink", () => {
384 const dir = mkdtempSync(join(tmpdir(), "facts-key-exclusion-"));
385 try {
386 const target = join(dir, "existing.key");
387 writeFileSync(target, "preserve-existing-file");
388 const cleanEnv = { ...process.env };
389 for (const marker of ["CI", "GITHUB_ACTIONS", "GITLAB_CI", "BUILDKITE", "CIRCLECI", "JENKINS_URL", "TF_BUILD"]) delete cleanEnv[marker];
390 const result = spawnSync(process.execPath, [script, "keygen", "--key-id", "cwf-test-exclusive", "--out", target], { encoding: "utf8", env: cleanEnv });
391 expect(result.status).toBe(1);
392 expect(readFileSync(target, "utf8")).toBe("preserve-existing-file");
393 if (process.platform !== "win32") {
394 const link = join(dir, "linked.key");
395 symlinkSync(target, link);
396 expect(spawnSync(process.execPath, [script, "keygen", "--key-id", "cwf-test-exclusive", "--out", link], { encoding: "utf8", env: cleanEnv }).status).toBe(1);
397 expect(readFileSync(target, "utf8")).toBe("preserve-existing-file");
398 }
399 } finally { rmSync(dir, { recursive: true, force: true }); }
400 });
401
402 it("rejects signed numeric SQL injection and emits escaped SQL only after verification", () => {
403 const malicious = signed({ facts_version: "7; DROP TABLE public.facts_key;" });
404 expect(() => emitSql(malicious, { publicKeyB64: EPHEMERAL_KEY.publicKey })).toThrow("positive safe integer");
405 const sql = emitSql(fixture, { publicKeyB64: TEST_KEY.publicKey, publishedBy: "O'Hara" });
406 expect(sql).toContain("select c.id, 7, 1, 1");
407 expect(sql).toContain("O''Hara");
408 });
409
410 it("parses intentional empty key tables but fails closed on unknown syntax or duplicate/invalid anchors", () => {
411 expect(parseTsKeys("export const TRUSTED_KEYS: readonly TrustedKey[] = [];")).toEqual([]);
412 expect(parseRustKeys("pub const TRUSTED_KEYS: &[TrustedKey] = &[];")).toEqual([]);
413 for (const text of ["no table", "export const TRUSTED_KEYS: readonly TrustedKey[] = [makeKey()];"]) expect(() => parseTsKeys(text)).toThrow();
414 expect(() => parseRustKeys("pub const TRUSTED_KEYS: &[TrustedKey] = &[make_key()];")).toThrow();
415 expect(() => parseTsKeys("// export const TRUSTED_KEYS: readonly TrustedKey[] = [];\nexport const TRUSTED_KEYS = makeKeys();")).toThrow();
416 expect(() => parseRustKeys("// pub const TRUSTED_KEYS: &[TrustedKey] = &[];\npub const TRUSTED_KEYS: &[TrustedKey] = &[make_key()];")).toThrow();
417 const entry = `{ keyId: "${TEST_KEY.keyId}", publicKey: "${TEST_KEY.publicKey}", status: "active" }`;
418 expect(() => parseTsKeys(`export const TRUSTED_KEYS: readonly TrustedKey[] = [${entry}, ${entry}];`)).toThrow();
419 expect(() => parseTsKeys(`export const TRUSTED_KEYS: readonly TrustedKey[] = [${entry.replace(TEST_KEY.publicKey, "bad")}];`)).toThrow();
420 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 }];`;
421 expect(parseRustKeys(rust)).toEqual([TEST_KEY]);
422 });
423
424 it("bounds publisher file/response reads and rejects symlink and hardlink inputs", async () => {
425 const dir = mkdtempSync(join(tmpdir(), "facts-bounded-read-"));
426 try {
427 const target = join(dir, "source.json");
428 writeFileSync(target, "12345");
429 expect(() => readBoundedFile(target, 4)).toThrow();
430 if (process.platform !== "win32") {
431 const link = join(dir, "symlink.json");
432 symlinkSync(target, link);
433 expect(() => readBoundedFile(link)).toThrow();
434 linkSync(target, join(dir, "hardlink.json"));
435 expect(() => readBoundedFile(target)).toThrow();
436 }
437 const oversized = overflowingStream();
438 await expect(readBoundedResponse(new Response(oversized.stream))).rejects.toThrow("size limit");
439 expect(oversized.cancel).toHaveBeenCalledOnce();
440 } finally { rmSync(dir, { recursive: true, force: true }); }
441 });
442
443 it("the local facts gate verifies both public fixtures and reports the pinned anchor count", () => {
444 const checker = fileURLToPath(new URL("../scripts/check-cloud-facts.mjs", import.meta.url));
445 // Derived from the table rather than hardcoded, so rotating or adding an
446 // anchor does not require editing this assertion — only a gate that has
447 // drifted out of step with the table will fail it.
448 const active = TRUSTED_KEYS.filter((key) => key.status === "active").length;
449 expect(execFileSync(process.execPath, [checker], { encoding: "utf8" })).toContain(
450 `${active} active production keys`,
451 );
452 });
453 });
454
454 lines TYPESCRIPT