import { afterEach, describe, expect, it, vi } from "vitest"; import { createPrivateKey, generateKeyPairSync, sign } from "node:crypto"; import { DurableSessions, type BlobStore } from "../server/durable-sessions"; import { accessOwner } from "../server/access"; import { createNetlifyHandler } from "../server/netlify-handler"; import { CASES } from "../server/cases"; import { assess, extractLocal } from "../server/context"; class MemoryBlobs implements BlobStore { rows = new Map(); serial = 0; async get(key: string) { return structuredClone(this.rows.get(key)?.data ?? null); } async getWithMetadata(key: string) { return structuredClone(this.rows.get(key) ?? null); } async setJSON( key: string, value: unknown, options?: { onlyIfMatch?: string; onlyIfNew?: boolean } ) { const existing = this.rows.get(key); if ( (options?.onlyIfNew && existing) || (options?.onlyIfMatch && existing?.etag !== options.onlyIfMatch) ) return { modified: false }; this.rows.set(key, { data: structuredClone(value), etag: String(++this.serial), }); return { modified: true }; } async delete(key: string) { this.rows.delete(key); } async list(options?: { prefix?: string }) { return { blobs: [...this.rows.keys()] .filter((key) => key.startsWith(options?.prefix || "")) .map((key) => ({ key })), }; } } const sources = CASES[0].sources; const owner = "reviewer-one"; const reviewFor = (draft: ReturnType) => Object.fromEntries(draft.facts.map((f) => [f.id, "include" as const])); afterEach(() => { vi.unstubAllGlobals(); vi.unstubAllEnvs(); vi.useRealTimers(); }); describe("Netlify durable review", () => { it("survives a cold function instance and preserves report provenance", async () => { const store = new MemoryBlobs(); const first = new DurableSessions(store); const draft = await first.keepDraft(extractLocal(sources), sources, owner); const cold = new DurableSessions(store); expect(await cold.getDraft(draft.id, sources, owner)).toEqual(draft); const review = reviewFor(draft); const result = await cold.keepReport( draft.id, assess(sources, review, draft.fingerprint, true, draft), review, owner ); const report = await new DurableSessions(store).getReport( result.reportId!, owner ); expect(report.sources).toEqual(sources); expect(report.assessment.contextual.rulesReviewed).toBe(false); }); it("rejects cross-reviewer retrieval and altered original sources", async () => { const sessions = new DurableSessions(new MemoryBlobs()); const draft = await sessions.keepDraft( extractLocal(sources), sources, owner ); await expect( sessions.getDraft(draft.id, sources, "other") ).rejects.toThrow(); await expect( sessions.getDraft( draft.id, [ { ...sources[0], text: sources[0].text + "\nChanged" }, ...sources.slice(1), ], owner ) ).rejects.toThrow(/Sources changed/); const review = reviewFor(draft), result = await sessions.keepReport( draft.id, assess(sources, review, draft.fingerprint, true, draft), review, owner ); await expect( sessions.getReport(result.reportId!, "other") ).rejects.toThrow(); }); it("expires and cleans old server data while leaving fresh records", async () => { let now = 100; const store = new MemoryBlobs(); const sessions = new DurableSessions(store, () => now); const draft = await sessions.keepDraft( extractLocal(sources), sources, owner ); now += 7200001; await expect(sessions.getDraft(draft.id, sources, owner)).rejects.toThrow( /expired/ ); const fresh = await sessions.keepDraft( extractLocal(sources), sources, owner ); expect(await sessions.purge()).toBe(1); expect(await sessions.getDraft(fresh.id, sources, owner)).toBeTruthy(); }); it("retains clarification source links across cold instances", async () => { const clarified = [ ...sources, { id: "clarification-test", title: "Confirmed correction", kind: "Reviewer clarification", date: "2026-10-23", text: "Patient: Nora Demo", clarification: { sourceId: sources[0].id, line: 1, quote: sources[0].text.split("\n")[0], reason: "Confirm name", confirmedBy: "Synthetic fixture only", }, }, ]; const store = new MemoryBlobs(); const draft = await new DurableSessions(store).keepDraft( extractLocal(clarified), clarified, owner ); const next = await new DurableSessions(store).getDraft( draft.id, clarified, owner ); expect(next.fingerprint).toBe(draft.fingerprint); expect( (await store.get("draft/" + draft.id)).sources.at(-1).clarification ).toEqual(clarified.at(-1)!.clarification); }); it("makes previous report obsolete after a newer assessment", async () => { const sessions = new DurableSessions(new MemoryBlobs()); const draft = await sessions.keepDraft( extractLocal(sources), sources, owner ); const review = reviewFor(draft), assessment = assess(sources, review, draft.fingerprint, true, draft); const old = await sessions.keepReport(draft.id, assessment, review, owner); const current = await sessions.keepReport( draft.id, assessment, review, owner ); await expect(sessions.getReport(old.reportId!, owner)).rejects.toThrow( /no longer current/ ); expect(await sessions.getReport(current.reportId!, owner)).toBeTruthy(); }); it("does not oversubscribe reviewer quota under concurrent cold requests", async () => { const store = new MemoryBlobs(); const results = await Promise.allSettled( Array.from({ length: 20 }, () => new DurableSessions(store, () => 123).limit(owner) ) ); expect( results.filter((r) => r.status === "fulfilled").length ).toBeGreaterThan(0); expect( results.filter((r) => r.status === "fulfilled").length ).toBeLessThanOrEqual(6); }); it("preserves daily allowance across cold instances", async () => { let now = 0; const store = new MemoryBlobs(); for (let i = 0; i < 120; i++) { now = i * 60000; await new DurableSessions(store, () => now).limit(owner); } await expect( new DurableSessions(store, () => now + 60000).limit(owner) ).rejects.toThrow(/allowance/); }); }); describe("Netlify request boundary", () => { const make = () => { const store = new MemoryBlobs(); return { store, handler: createNetlifyHandler( () => store, async () => owner ), }; }; const req = (path: string, body?: unknown) => new Request("https://context.marmar.life/api/" + path, { method: body === undefined ? "GET" : "POST", headers: body === undefined ? {} : { "Content-Type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), }); it("runs extraction, gated review, assessment and export across handlers", async () => { const { store, handler } = make(); const draft = await ( await handler( req("extract", { sources, syntheticOnly: true, useModel: false }) ) ).json(); const review = reviewFor(draft); const payload = { sources, syntheticOnly: true, draftId: draft.id, review, fingerprint: draft.fingerprint, acknowledgeUnparsed: true, }; expect( (await handler(req("assess", { ...payload, review: {} }))).status ).toBe(400); const cold = createNetlifyHandler( () => store, async () => owner ); const response = await cold(req("assess", payload)); expect(response.status).toBe(200); const result = await response.json(); expect((await cold(req("reports/" + result.reportId))).status).toBe(200); }); it("blocks unrelated origins, real-data input and client model configuration", async () => { const { handler } = make(); expect( (await handler(new Request("https://evil.example/api/status"))).status ).toBe(403); expect( (await handler(req("extract", { sources, syntheticOnly: false }))).status ).toBe(400); expect((await handler(req("model", {}))).status).toBe(403); }); it("fails closed when authentication is absent", async () => { const handler = createNetlifyHandler(() => new MemoryBlobs()); vi.stubEnv( "CONTEXT_ACCESS_ISSUER", "https://marmar-test.cloudflareaccess.com" ); vi.stubEnv("CONTEXT_ACCESS_AUD", "test-audience"); expect((await handler(req("status"))).status).toBe(401); }); }); describe("Cloudflare Access signature validation", () => { const keys = generateKeyPairSync("rsa", { modulusLength: 2048 }); const issuer = "https://marmar-test.cloudflareaccess.com"; const jwk = { ...keys.publicKey.export({ format: "jwk" }), kid: "test-key", alg: "RS256", }; function token(overrides: Record = {}, alg = "RS256") { const head = Buffer.from(JSON.stringify({ alg, kid: "test-key" })).toString( "base64url" ); const body = Buffer.from( JSON.stringify({ iss: issuer, aud: ["test-audience"], sub: "synthetic-user", email: "synthetic@example.invalid", exp: Math.floor(Date.now() / 1000) + 100, ...overrides, }) ).toString("base64url"); return ( head + "." + body + "." + sign( "RSA-SHA256", Buffer.from(head + "." + body), keys.privateKey ).toString("base64url") ); } const env = { CONTEXT_ACCESS_ISSUER: issuer, CONTEXT_ACCESS_AUD: "test-audience", }; const request = (value: string) => new Request("https://context.marmar.life/api/status", { headers: { "cf-access-jwt-assertion": value }, }); it("accepts correctly signed intended audience only", async () => { vi.stubGlobal( "fetch", vi.fn(async () => Response.json({ keys: [jwk] })) ); expect(await accessOwner(request(token()), env)).toMatch(/^[a-f0-9]{64}$/); await expect( accessOwner(request(token({ aud: ["other"] })), env) ).rejects.toThrow(/sign-in/); await expect(accessOwner(request(token({ exp: 1 })), env)).rejects.toThrow( /sign-in/ ); await expect( accessOwner(request(token({ iss: "https://evil.example" })), env) ).rejects.toThrow(/sign-in/); }); it("rejects unsigned, tampered and wrong-algorithm tokens", async () => { vi.stubGlobal( "fetch", vi.fn(async () => Response.json({ keys: [jwk] })) ); await expect( accessOwner(request(token({}, "none")), env) ).rejects.toThrow(); const parts = token().split("."); parts[1] = Buffer.from(JSON.stringify({ sub: "attacker" })).toString( "base64url" ); await expect(accessOwner(request(parts.join(".")), env)).rejects.toThrow(); await expect(accessOwner(request(""), env)).rejects.toThrow(); }); }); it("fails closed when storage omits a concurrency version", async () => { const store = new MemoryBlobs(); const sessions = new DurableSessions(store); const draft = await sessions.keepDraft(extractLocal(sources), sources, owner); for (const row of store.rows.values()) row.etag = ""; await expect(sessions.getDraft(draft.id, sources, owner)).rejects.toThrow("verify"); await store.setJSON(`quota/minute/${Math.floor(Date.now()/60000)}/global`,{count:1}); for (const row of store.rows.values()) row.etag = ""; await expect(sessions.limit(owner)).rejects.toThrow("verify"); });