// Browser-side mock API for dev-fixture mode. // // Wired into the page via `src/mocks/MockWorker.tsx`. Only initialised when // `BP_DEV_FIXTURE` is set on the server (the env value is forwarded to the // client via a window global). Production builds never start the worker. // // Today's surface is the small set of write paths the design shows. They // don't persist — every response is synthesised so the same click always // looks the same. When the real platform endpoints exist, drop the // matching handler from this file. import { http, HttpResponse, delay } from "msw"; type InvitePayload = { email?: string; role?: string; }; type TestRunPayload = { workflowId?: string; }; // ---- Frozen-tenant guard -------------------------------------------------- // In dev-fixture mode the tenant status is encoded in a cookie or a // window global; for now we read a hint from a custom header that the // caller sets, so the same mock handler can respond 402 or 201 depending // on which fixture is currently active. function isFrozen(req: Request): boolean { return req.headers.get("x-bp-tenant-status") === "frozen"; } function isArchived(req: Request): boolean { return req.headers.get("x-bp-tenant-status") === "archived"; } function archivedResponse() { return HttpResponse.json( { error: "tenant_archived", message: "Tenant retention window closed." }, { status: 410 }, ); } function frozenResponse() { return HttpResponse.json( { error: "tenant_frozen", message: "Tenant is read-only. Re-activate to resume writes.", }, { status: 402 }, ); } export const handlers = [ // ---- /api/team/invites ------------------------------------------------- http.post("/api/team/invites", async ({ request }) => { if (isArchived(request)) return archivedResponse(); if (isFrozen(request)) return frozenResponse(); await delay(280); const body = (await request.json().catch(() => ({}))) as InvitePayload; if (!body.email || !body.email.includes("@")) { return HttpResponse.json({ error: "invalid_email" }, { status: 400 }); } const role = body.role ?? "USER"; return HttpResponse.json( { id: "inv-" + Math.random().toString(36).slice(2, 9), email: body.email, role, status: "invited", created_at: new Date().toISOString(), }, { status: 201, headers: { "x-bp-status-code": "201 · invite.created" } }, ); }), // ---- /api/scans ------------------------------------------------------- http.post("/api/scans", async ({ request }) => { if (isArchived(request)) return archivedResponse(); if (isFrozen(request)) return frozenResponse(); await delay(420); return HttpResponse.json( { id: "scan-" + Math.random().toString(36).slice(2, 9), status: "queued", queued_at: new Date().toISOString(), }, { status: 202, headers: { "x-bp-status-code": "202 · scan.queued" } }, ); }), // ---- /api/workflows/:id/test ----------------------------------------- http.post("/api/workflows/:id/test", async ({ request, params }) => { if (isArchived(request)) return archivedResponse(); if (isFrozen(request)) return frozenResponse(); await delay(180); return HttpResponse.json( { workflow_id: params.id, run_id: "wfr-" + Math.random().toString(36).slice(2, 9), status: "started", } satisfies Record & { workflow_id: unknown }, { status: 202, headers: { "x-bp-status-code": "202 · workflow.test" } }, ); }), // ---- /api/billing/reactivate ---------------------------------------- http.post("/api/billing/reactivate", async ({ request }) => { if (isArchived(request)) return archivedResponse(); await delay(320); return HttpResponse.json( { status: "pending", contact: "billing@breakpilot.eu" }, { status: 202, headers: { "x-bp-status-code": "202 · reactivation.requested" }, }, ); }), ]; // Silence unused-type warnings for payloads we don't fully validate. export type { InvitePayload, TestRunPayload };