// Local browser QA only: a fixture of /api/clinician with fictional people and records, the // preview's demo API (local reader only), and the built front end. Binds to 127.0.0.1. // Sign in as emeka.okafor@example.test; the codes are FIXTURE_EMAIL_CODE and FIXTURE_TOTP_CODE in data.ts. import { randomUUID } from "node:crypto"; import { readFile } from "node:fs/promises"; import { createServer, type IncomingMessage, type ServerResponse } from "node:http"; import { extname, resolve, sep } from "node:path"; import { z } from "zod"; import { CASES } from "../../server/cases"; import { assess } from "../../server/context"; import { runEvaluation } from "../../server/evaluation"; import { extract } from "../../server/model"; import { baseSchema, coverageReviewSchema } from "../../server/schemas"; import { getDraft, getReport, keepDraft, keepReport } from "../../server/sessions"; import { ENGINE_VERSION, RULES_VERSION } from "../../../../src/domain/checks/engine"; import { qrSvg } from "../../../../src/clinician/qr"; import { CHECK_SIGNED_OFF } from "../../../../src/domain/checks/signoff"; import { viewHours } from "../../src/api/adapt"; import type * as W from "../../src/api/wire"; import type { Variant } from "./calls"; import { FIXTURE_EMAIL, FIXTURE_EMAIL_CODE, FIXTURE_TOTP_SECRET } from "./data"; import { HttpError, invalid, route, text, type Ctx, type Route } from "./http"; import { checkInRoutes } from "./routes-checkins"; import { patientRoutes } from "./routes-patients"; import { createStore, totpValid, type Options, type Session, type Store } from "./store"; import { wireClinic, wireClinician, wireMe } from "./wire"; const COOKIE = "context_fixture"; const TWELVE_HOURS = 12 * 3_600_000; // The backend's patient routes: the step-up, then the terms (src/clinician/session.ts). const needsStepUp = (level: Route["auth"]) => level === "step" || level === "patients"; const me = (store: Store, s: Session) => wireMe(store.clinician, { createdAt: s.createdAt, stepUp: s.stepUp, locked: s.locked }, store.totpEnrolled); const recovery = () => Array.from({ length: 10 }, () => randomUUID().replace(/-/g, "").slice(0, 10)); // As the backend (src/clinician/profile.ts): numbers Context texts are stored in E.164, hours are HH:MM spans. function textable(value: unknown, path: string): string { let cleaned = text(value, 32).replace(/[\s().-]/g, ""); if (cleaned.startsWith("00")) cleaned = `+${cleaned.slice(2)}`; if (!/^\+[1-9]\d{6,14}$/.test(cleaned)) throw invalid("Check the highlighted fields.", [{ path, message: "Use the full number with its country code, for example +234 803 123 4567" }]); return cleaned; } function knownZone(value: unknown): string { try { new Intl.DateTimeFormat("en", { timeZone: String(value) }); return String(value); } catch { throw invalid("Check the highlighted fields.", [{ path: "timezone", message: "Not a known time zone" }]); } } const clock = (value: unknown) => typeof value === "string" && /^([01]\d|2[0-3]):[0-5]\d$/.test(value); function weekly(value: unknown, path: string): W.WeeklyHours { const spans = Array.isArray(value) ? (value as W.WeeklyHours) : []; const ok = Array.isArray(value) && spans.length <= 21 && spans.every((h) => Number.isInteger(h.day) && h.day >= 1 && h.day <= 7 && clock(h.open) && clock(h.close) && h.open < h.close); if (!ok) throw invalid("Check the highlighted fields.", [{ path, message: "Opening must come before closing" }]); return spans.map(({ day, open, close }) => ({ day, open, close })); } const accountRoutes: Route[] = [ route("POST", "/access-requests", "public", (ctx) => { const b = ctx.body ?? {}; const required = ["fullName", "profession", "licenceBody", "licenceNumber", "countryCode", "facilityName", "facilityPhone", "workEmail", "mobilePhone", "heardFrom", "termsVersion"]; const missing = required.filter((k) => !text(b[k])); if (missing.length || b.acceptTerms !== true) throw invalid("Check the highlighted fields.", missing.map((path) => ({ path, message: "Required" }))); if (!/^[A-Z]{2}$/.test(String(b.countryCode))) throw invalid("Check the highlighted fields.", [{ path: "countryCode", message: "Use a two-letter country code" }]); if (b.countryCode === "NG" && b.licenceBody !== (b.profession === "doctor" ? "MDCN" : "PCN")) throw invalid("Check the highlighted fields.", [{ path: "licenceBody", message: `In Nigeria, ${b.profession}s are registered with the ${b.profession === "doctor" ? "MDCN" : "PCN"}` }]); ctx.store.accessRequests.push(b); return { status: "received", message: "Thank you. We sent a confirmation to your work email and usually reply within 2 working days." } satisfies W.AccessRequestReceived; }, 202), route("GET", "/invite-preview/:token", "public", (ctx) => { const invite = [...ctx.store.invitations.values()].find((i) => i.token === decodeURIComponent(ctx.params[0])); if (!invite) throw new HttpError(404, "INVITE_NOT_FOUND", "This invitation link is not valid."); return { status: "pending", patientName: invite.patientName, withPerson: invite.withPerson, doctorName: "Dr Emeka Okafor", clinicName: "Surulere Family Clinic", doctorEmail: FIXTURE_EMAIL, expiresAt: invite.expiresAt, } satisfies W.InvitePreview; }), route("GET", "/me", "session", (ctx) => me(ctx.store, ctx.session!)), route("GET", "/step-up", "session", (ctx) => ({ totpEnrolled: ctx.store.totpEnrolled, recoveryCodesLeft: ctx.store.totpRecoveryCodes.length, stepUpDone: ctx.session!.stepUp && !ctx.session!.locked, locked: ctx.session!.locked, pausedUntil: null, }) satisfies W.StepUpState), route("POST", "/step-up/totp", "session", (ctx) => { if (ctx.store.totpEnrolled && !ctx.session!.stepUp) throw new HttpError(403, "STEP_UP_REQUIRED", "Enter a code from your current authenticator app before setting up a new one."); const otpauthUrl = `otpauth://totp/MARMAR%20Context:${encodeURIComponent(FIXTURE_EMAIL)}?secret=${FIXTURE_TOTP_SECRET}&issuer=MARMAR%20Context&algorithm=SHA1&digits=6&period=30`; // The backend's own QR drawing, as the real API answers. return { secret: FIXTURE_TOTP_SECRET, otpauthUrl, qrSvg: qrSvg(otpauthUrl) } satisfies W.TotpStart; }, 201), route("POST", "/step-up/totp/confirm", "session", (ctx) => { if (!totpValid(text(ctx.body?.code, 20))) throw new HttpError(400, "CODE_INCORRECT", "That code didn't match. Check the time on your phone and try the newest code."); ctx.store.totpEnrolled = true; ctx.store.totpRecoveryCodes = recovery(); Object.assign(ctx.session!, { stepUp: true, locked: false, lastActiveAt: Date.now() }); return { stepUpDone: true, recoveryCodes: ctx.store.totpRecoveryCodes } satisfies W.TotpConfirmed; }), route("POST", "/step-up/verify", "session", (ctx) => { if (!ctx.store.totpEnrolled) throw new HttpError(409, "NO_AUTHENTICATOR", "Set up an authenticator app first."); const code = text(ctx.body?.code, 20).replace(/\s/g, ""); const used = ctx.store.totpRecoveryCodes.indexOf(code); if (!/^\d{6}$/.test(code) ? used < 0 : !totpValid(code)) throw new HttpError(400, "CODE_INCORRECT", "That code didn't match. Try the newest code from your authenticator app."); if (used >= 0) ctx.store.totpRecoveryCodes.splice(used, 1); Object.assign(ctx.session!, { stepUp: true, locked: false, lastActiveAt: Date.now() }); return { stepUpDone: true, method: used >= 0 ? "recovery_code" : "totp" } satisfies W.StepUpVerified; }), route("POST", "/session/lock", "session", (ctx) => { Object.assign(ctx.session!, { stepUp: false, locked: true }); return { locked: true }; }), route("POST", "/sessions/sign-out-everywhere", "session", (ctx) => { const signedOut = ctx.store.sessions.size; ctx.store.sessions.clear(); ctx.res.setHeader("Set-Cookie", `${COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`); return { signedOut }; }), route("POST", "/me/terms", "session", (ctx) => { if (!text(ctx.body?.termsVersion, 32) || !text(ctx.body?.dpaVersion, 32)) throw invalid("Check the highlighted fields."); ctx.store.clinician.termsAcceptedAt = new Date().toISOString(); return { clinician: wireClinician(ctx.store.clinician) }; }), route("PATCH", "/me", "step", (ctx) => { const b = ctx.body ?? {}; const c = ctx.store.clinician; const allowed = ["title", "displayName", "pronoun", "mobilePhone", "idleLockMinutes"]; if (!Object.keys(b).length || Object.keys(b).some((k) => !allowed.includes(k))) throw invalid("Check the highlighted fields."); if (b.pronoun !== undefined && b.pronoun !== null && !["he", "she", "they"].includes(b.pronoun)) throw invalid("Check the highlighted fields.", [{ path: "pronoun", message: "Choose he, she or they" }]); if (b.idleLockMinutes !== undefined && ![15, 30, 60].includes(b.idleLockMinutes)) throw invalid("Check the highlighted fields.", [{ path: "idleLockMinutes", message: "Choose 15, 30 or 60" }]); if (b.displayName !== undefined) { if (text(b.displayName, 80).length < 2) throw invalid("Check the highlighted fields.", [{ path: "displayName", message: "Too short" }]); c.spokenName = text(b.displayName, 80); } if (b.title !== undefined) c.title = b.title ? text(b.title, 24) : ""; if (b.pronoun !== undefined) c.pronoun = b.pronoun; if (b.mobilePhone !== undefined) c.mobilePhone = b.mobilePhone === null ? null : textable(b.mobilePhone, "mobilePhone"); if (b.idleLockMinutes) c.idleLockMinutes = b.idleLockMinutes; return { clinician: wireClinician(c) }; }), route("GET", "/clinic", "session", ({ store }) => ({ clinic: wireClinic(store.clinic) })), route("PATCH", "/clinic", "step", (ctx) => { const b = ctx.body ?? {}; const c = ctx.store.clinic; if (b.expectedVersion !== undefined && b.expectedVersion !== c.version) throw new HttpError(409, "CLINIC_CHANGED", "Someone changed the clinic details since you opened them. Reload and try again."); if (b.phone && !/^\+?[0-9 ()-]{7,20}$/.test(String(b.phone))) throw invalid("Check the highlighted fields.", [{ path: "phone", message: "Use digits, spaces and an optional + only" }]); if (b.name !== undefined) c.name = text(b.name, 120); if (b.addressLine !== undefined) c.address = text(b.addressLine, 300); if (b.phone !== undefined) c.phone = text(b.phone, 20); if (b.timezone !== undefined) c.timeZone = knownZone(b.timezone); if (b.openingHours !== undefined) c.openingHours = viewHours(weekly(b.openingHours, "openingHours")); if (b.alertWatchHours !== undefined) c.alertWatchHours = viewHours(weekly(b.alertWatchHours, "alertWatchHours")); if (b.backupAlertName !== undefined || b.backupAlertPhone !== undefined || b.backupAlertEmail !== undefined) { const contact = { name: text(b.backupAlertName ?? "", 120), phone: b.backupAlertPhone ? textable(b.backupAlertPhone, "backupAlertPhone") : "", email: b.backupAlertEmail ? String(b.backupAlertEmail).trim().toLowerCase() : "" }; if (contact.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(contact.email)) throw invalid("Check the highlighted fields.", [{ path: "backupAlertEmail", message: "Invalid email address" }]); c.backupContact = contact.name || contact.phone || contact.email ? contact : null; } if (Array.isArray(b.emergencyNumbers)) c.emergencyNumbers = b.emergencyNumbers.map((n: { number: string; label: string; testDialledAt: string | null }) => ({ number: n.number, label: n.label, testedAt: n.testDialledAt, source: "clinic" as const })); c.version += 1; return { clinic: wireClinic(c) }; }), ]; const routes = [...accountRoutes, ...patientRoutes, ...checkInRoutes]; async function readBody(req: IncomingMessage, limit = 2_000_000): Promise { const chunks: Buffer[] = []; let size = 0; for await (const chunk of req) { size += chunk.length; if (size > limit) throw new HttpError(413, "TOO_LARGE", "The request is too large."); chunks.push(chunk); } return Buffer.concat(chunks); } // FIXTURE_RENAME="Mariam=Bola" renames a fictional person in every response (for a demo film). const renames = (process.env.FIXTURE_RENAME ?? "").split(",").filter((r) => r.includes("=")).map((r) => r.split("=") as [string, string]); const renamed = (text: string) => renames.reduce((t, [from, to]) => t.replaceAll(from, to), text); const json = (res: ServerResponse, status: number, body: unknown) => { if (res.headersSent) return; res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" }); res.end(renamed(JSON.stringify(body))); }; const cookie = (req: IncomingMessage) => Object.fromEntries((req.headers.cookie ?? "").split(";").map((p) => p.trim().split("=")).filter((p) => p.length === 2))[COOKIE] as string | undefined; function authenticate(store: Store, req: IncomingMessage, level: Route["auth"]): Session | null { const s = store.sessions.get(cookie(req) ?? ""); if (level === "public") return s ?? null; if (!s) throw new HttpError(401, "SIGN_IN_REQUIRED", "Sign in to continue."); const now = Date.now(); if (now - s.createdAt > TWELVE_HOURS) { store.sessions.delete(s.token); throw new HttpError(401, "SESSION_EXPIRED", "Your session ended after 12 hours. Sign in again."); } if (!s.locked && now - s.lastActiveAt > store.clinician.idleLockMinutes * 60_000) Object.assign(s, { stepUp: false, locked: true }); if (needsStepUp(level) && s.locked) throw new HttpError(403, "SESSION_LOCKED", "Your session locked while it wasn't in use. Enter a code from your authenticator app to continue."); if (needsStepUp(level) && (!store.totpEnrolled || !s.stepUp)) throw new HttpError(403, "STEP_UP_REQUIRED", "Enter a code from your authenticator app to see patient records."); if (level === "patients" && !store.clinician.termsAcceptedAt) throw new HttpError(403, "TERMS_REQUIRED", "Accept the clinician terms to work with real patients."); // Background polls and streams do not count as activity (ACC-7). if (req.headers["x-context-background"] !== "1") s.lastActiveAt = now; return s; } /** Better Auth's email one-time code routes, as the clinician instance exposes them. */ async function authRoute(store: Store, req: IncomingMessage, res: ServerResponse, path: string) { const body = JSON.parse((await readBody(req)).toString() || "{}"); if (path === "/email-otp/send-verification-otp") { if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(body.email ?? ""))) throw invalid("Enter your work email."); console.log(`[fixture] sign-in code requested (${body.email === FIXTURE_EMAIL ? "known clinician" : "unknown email, no code sent"})`); return json(res, 200, { success: true }); } if (path === "/sign-in/email-otp") { if (String(body.email).toLowerCase() !== FIXTURE_EMAIL || String(body.otp) !== FIXTURE_EMAIL_CODE) throw new HttpError(400, "INVALID_OTP", "That code is not right or has expired. Request a new code."); const token = randomUUID(); store.sessions.set(token, { token, email: FIXTURE_EMAIL, stepUp: false, locked: false, createdAt: Date.now(), lastActiveAt: Date.now() }); res.setHeader("Set-Cookie", `${COOKIE}=${token}; Path=/; HttpOnly; SameSite=Lax`); return json(res, 200, { token: null }); } if (path === "/sign-out") { store.sessions.delete(cookie(req) ?? ""); res.setHeader("Set-Cookie", `${COOKIE}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0`); return json(res, 200, { success: true }); } throw new HttpError(404, "NOT_FOUND", "Unknown sign-in route."); } function stream(ctx: Ctx) { const call = ctx.store.calls.get(decodeURIComponent(ctx.params[0])); if (!call || ctx.store.options.noSse) throw new HttpError(404, "NOT_FOUND", "No event stream here. Poll /updates instead."); const after = Number(ctx.req.headers["last-event-id"] ?? ctx.url.searchParams.get("lastEventId") ?? 0) || 0; ctx.res.writeHead(200, { "Content-Type": "text/event-stream", "Cache-Control": "no-cache, no-transform", "X-Accel-Buffering": "no", Connection: "keep-alive" }); const send = (e: W.CheckInEvent) => ctx.res.write(`id: ${e.id}\nevent: ${e.type}\ndata: ${renamed(JSON.stringify(e))}\n\n`); ctx.res.write("retry: 3000\n\n"); call.events.filter((e) => e.id > after).forEach(send); call.listeners.add(send); const ping = setInterval(() => ctx.res.write(": ping\n\n"), 25_000); ctx.req.on("close", () => { call.listeners.delete(send); clearInterval(ping); }); } /** The preview's demo API for Try a fictional patient: local reader only, no uploads. */ async function demoApi(req: IncomingMessage, res: ServerResponse, path: string) { if (req.method === "GET" && path === "/api/status") return json(res, 200, { engineVersion: ENGINE_VERSION, rulesVersion: RULES_VERSION, rulesReviewed: CHECK_SIGNED_OFF, modelConfigured: false, hosted: true, modelConnectionManaged: true, model: null, modelVerifiedAt: null }); if (req.method === "GET" && path === "/api/cases") return json(res, 200, CASES); if (req.method === "GET" && path.startsWith("/api/reports/")) return json(res, 200, getReport(path.slice("/api/reports/".length), "fixture")); if (req.method !== "POST") throw new HttpError(405, "METHOD", "Method not allowed."); const parsed = JSON.parse((await readBody(req, 120_000)).toString() || "{}"); if (path === "/api/evaluate") return json(res, 200, runEvaluation()); if (path === "/api/extract") { const data = baseSchema.extend({ useModel: z.boolean().default(false), freshModel: z.boolean().default(false) }).strict().parse(parsed); if (data.useModel) throw invalid("The demo uses the local reader only."); return json(res, 200, keepDraft(await extract(data.sources, false), data.sources, "fixture")); } 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); const draft = getDraft(data.draftId, data.sources, "fixture"); return json(res, 200, keepReport(data.draftId, assess(data.sources, data.review, data.fingerprint, data.acknowledgeUnparsed, draft, data.coverageReview), data.review, "fixture")); } throw new HttpError(404, "NOT_FOUND", "Unknown endpoint."); } const mime: Record = { ".html": "text/html", ".js": "text/javascript", ".css": "text/css", ".svg": "image/svg+xml", ".png": "image/png", ".ttf": "font/ttf", ".woff2": "font/woff2", ".json": "application/json" }; export function createFixtureServer(options: Options, distDir: string) { const store = createStore(options); const root = resolve(distDir); const server = createServer(async (req, res) => { const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "127.0.0.1"}`); const path = url.pathname; try { if (path === "/__fixture/variant" && req.method === "POST") { store.nextVariant = (JSON.parse((await readBody(req)).toString() || "{}").variant ?? null) as Variant | null; return json(res, 200, { nextVariant: store.nextVariant }); } if (path.startsWith("/__fixture/uploads/") && req.method === "PUT") { const up = store.uploads.get(path.split("/").pop()!); if (!up) throw new HttpError(404, "NOT_FOUND", "Unknown upload."); up.data = await readBody(req, 16 * 1024 * 1024); res.writeHead(200).end(); return; } if (path.startsWith("/api/auth/clinician/")) return await authRoute(store, req, res, path.slice("/api/auth/clinician".length)); if (path.startsWith("/api/clinician/")) { const sub = path.slice("/api/clinician".length); if (req.method === "GET" && /^\/check-ins\/[^/]+\/events$/.test(sub)) { const session = authenticate(store, req, "patients"); return stream({ req, res, url, params: [sub.split("/")[2]], body: null, session, store }); } for (const r of routes) { const match = r.method === req.method ? r.pattern.exec(sub) : null; if (!match) continue; const session = authenticate(store, req, r.auth); const raw = req.method === "GET" ? "" : (await readBody(req)).toString(); const body = raw ? JSON.parse(raw) : {}; const result = await r.handler({ req, res, url, params: match.slice(1), body, session, store }); if (result !== undefined) json(res, r.status, result); return; } throw new HttpError(404, "NOT_FOUND", "Unknown clinician endpoint."); } if (path.startsWith("/api/")) return await demoApi(req, res, path); const file = resolve(root, `.${decodeURIComponent(path)}`); if (!file.startsWith(root + sep) && file !== root) throw new HttpError(403, "FORBIDDEN", "Invalid path."); const target = extname(file) ? file : resolve(root, "index.html"); const data = await readFile(target).catch(() => null); if (!data) throw new HttpError(404, "NOT_FOUND", extname(file) ? "File not found." : "Run npm run build first."); res.writeHead(200, { "Content-Type": mime[extname(target)] ?? "application/octet-stream" }); res.end(data); } catch (error) { if (error instanceof HttpError) return json(res, error.status, { code: error.code, message: error.message, ...(error.details === undefined ? {} : Array.isArray(error.details) ? { details: error.details } : (error.details as object)) }); if (error instanceof z.ZodError || error instanceof SyntaxError) return json(res, 400, { code: "VALIDATION_ERROR", message: "The request is not valid." }); json(res, 400, { code: "REQUEST_FAILED", message: error instanceof Error ? error.message : "The request failed." }); } }); return { server, store }; }