marmar-context / web / context / server / netlify-handler.ts
netlify-handler.ts
Raw
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> | string
) {
  return async (request: Request): Promise<Response> => {
    const headers: Record<string, string> = {
      "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();
}