import { coverageReviewSchema } from "./schemas"; import { z } from "zod"; import { ENGINE_VERSION, RULES_VERSION, } from "../../../src/domain/checks/engine"; import { CHECK_SIGNED_OFF } from "../../../src/domain/checks/signoff"; import { baseSchema } from "./schemas"; import { CASES } from "./cases"; import { assess } from "./context"; import { extract, modelReady, modelLabel, modelVerification } from "./model"; import { ModelError } from "./model-runtime"; import { runEvaluation } from "./evaluation"; import { accessOwner } from "./access"; import { demoOnly, demoOwner } from "./demo-auth"; import { DurableSessions, type BlobStore } from "./durable-sessions"; /** * CONTEXT_AUTH_MODE=demo serves Try a fictional patient to anyone (ACC-2): the fictional records * only, the local reader only. Otherwise reviewers sign in through Cloudflare Access. There is no * shared-password mode (ACC-11). */ export function createNetlifyHandler( getStore: () => BlobStore, authenticate?: (request: Request) => Promise | string ) { return async (request: Request): Promise => { const headers: Record = { "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff", "Referrer-Policy": "no-referrer", }; const json = (status: number, body: unknown) => Response.json(body, { status, headers }); try { const url = new URL(request.url); const allowed = ( process.env.CONTEXT_ALLOWED_ORIGINS || "https://context.marmar.life" ) .split(",") .map((x) => x.trim()); if ( !allowed.includes(url.origin) || (request.headers.has("Origin") && !allowed.includes(request.headers.get("Origin")!)) ) throw new ModelError("Origin is not allowed.", 403); const demo = !authenticate && process.env.CONTEXT_AUTH_MODE === "demo"; let owner: string; if (authenticate) owner = await authenticate(request); else if (demo) { const visitor = demoOwner(request); if (visitor.setCookie) headers["Set-Cookie"] = visitor.setCookie; owner = visitor.owner; } else owner = await accessOwner(request); const sessions = new DurableSessions(getStore()); const path = url.pathname; if (request.method === "GET" && path === "/api/status") return json(200, { engineVersion: ENGINE_VERSION, rulesVersion: RULES_VERSION, rulesReviewed: CHECK_SIGNED_OFF, modelConfigured: !demo && modelReady(), model: !demo ? modelLabel() : null, modelVerifiedAt: demo ? null : modelVerification(), hosted: true, modelConnectionManaged: true, sessionStorage: "durable", demo, }); if (request.method === "GET" && path === "/api/cases") return json(200, CASES); if (request.method === "GET" && path.startsWith("/api/reports/")) { headers["Content-Disposition"] = 'attachment; filename="marmar-context-evidence.json"'; return json( 200, await sessions.getReport(path.slice("/api/reports/".length), owner) ); } if (path === "/api/model") throw new ModelError( "Model connection is managed by the deployment administrator.", 403 ); if (request.method !== "POST") throw new ModelError("Method not allowed.", 405); if (!request.headers.get("Content-Type")?.startsWith("application/json")) throw new ModelError("Use application/json.", 415); if (Number(request.headers.get("Content-Length")) > 120000) throw new ModelError("Request is too large.", 413); const text = await request.text(); if (Buffer.byteLength(text) > 120000) throw new ModelError("Request is too large.", 413); let parsed: unknown; try { parsed = JSON.parse(text); } catch { throw new ModelError("Invalid JSON.", 400); } if (path === "/api/evaluate") return json(200, runEvaluation()); if (path === "/api/extract") { const data = baseSchema .extend({ useModel: z.boolean().default(false), freshModel: z.boolean().default(false), }) .strict() .parse(parsed); if (demo) demoOnly(data.sources, data.useModel); await sessions.limit(owner); const draft = await extract( data.sources, data.useModel, data.freshModel, request.signal ); return json(200, await sessions.keepDraft(draft, data.sources, owner)); } if (path === "/api/assess") { const data = baseSchema .extend({ draftId: z.string().uuid(), review: z.record(z.string(), z.enum(["include", "exclude"])), coverageReview: coverageReviewSchema, fingerprint: z.string().length(64), acknowledgeUnparsed: z.boolean(), }) .strict() .parse(parsed); if (demo) demoOnly(data.sources); const draft = await sessions.getDraft( data.draftId, data.sources, owner ); return json( 200, await sessions.keepReport( data.draftId, assess( data.sources, data.review, data.fingerprint, data.acknowledgeUnparsed, draft, data.coverageReview ), data.review, owner ) ); } return json(404, { error: "Unknown endpoint." }); } catch (error) { if (error instanceof ModelError) { if (error.retryAfter) headers["Retry-After"] = String(error.retryAfter); return json(error.status, { error: error.message }); } if (error instanceof z.ZodError) return json(400, { error: "Invalid request. Check field types, lengths and synthetic-data acknowledgement.", }); // Domain/model errors are user-facing; storage transport details must never leak credentials. if ( error instanceof Error && !/https?:|token|authorization|bearer|api.?key/i.test(error.message) ) return json(400, { error: error.message }); return json(503, { error: "The preview could not complete this request. Your browser-saved work is unchanged. Retry shortly.", }); } }; } export async function cleanup(getStore: () => BlobStore) { return new DurableSessions(getStore()).purge(); }