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<string, { data: any; etag: string }>();
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<typeof extractLocal>) =>
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<string, unknown> = {}, 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");
});