import type { SerializedRequest, SerializedResponse, } from "@suaveplan/api-types/sandbox-bridge"; import { buildFailResponsePayload, createInvoice, finaliseInvoice, getInvoiceOrThrow, type InMemoryStore, listInvoices, parseCreateInvoiceInput, raiseSubscriptionInactive, } from "../domain/index.js"; import { getStore } from "./wasm-state.js"; /** * Routing + handler entry point for the wasm-tier surface. Receives a * `SerializedRequest` (the WASI-stdio envelope payload), matches against * the static route table by method + URL pathname, and dispatches to a * per-route handler that delegates every business decision to * `src/domain/invoice-rules.ts`. * * `SuaveplanError`-shaped throws are NOT caught here — the ABI shim * in `wasm-compiler-javy` wraps `handleRequest` and emits an * `{ kind: "error" }` envelope when a thrown `SuaveplanError` reaches * the boundary. */ export async function handleRequest( req: SerializedRequest ): Promise { const url = new URL(req.url); const pathname = url.pathname; const store = getStore(); const result = await dispatch({ method: req.method.toUpperCase(), pathname, search: url.searchParams, body: decodeBody(req.bodyBase64), capabilities: req.capabilities, store, }); return result; } interface DispatchCtx { readonly method: string; readonly pathname: string; readonly search: URLSearchParams; readonly body: Uint8Array; readonly capabilities: readonly string[]; readonly store: InMemoryStore; } async function dispatch(ctx: DispatchCtx): Promise { if (ctx.method === "GET" && ctx.pathname === "/example/public/ping") { return jsonResponse(200, { ok: true, pluginName: "@suaveplan/api-plugin-example", }); } if (ctx.method === "GET" && ctx.pathname === "/example/health") { return jsonResponse(200, { ok: true }); } if (ctx.method === "GET" && ctx.pathname === "/example/invoices") { const deny = rbacCheck(ctx.capabilities, "example:read"); if (deny !== null) return deny; return jsonResponse(200, { invoices: listInvoices(ctx.store) }); } if (ctx.method === "GET") { const id = matchInvoiceId(ctx.pathname); if (id !== null) { const deny = rbacCheck(ctx.capabilities, "example:read"); if (deny !== null) return deny; return jsonResponse(200, getInvoiceOrThrow(ctx.store, id)); } } if (ctx.method === "POST" && ctx.pathname === "/example/invoices") { const deny = rbacCheck(ctx.capabilities, "example:write"); if (deny !== null) return deny; const parsedBody = parseRequestBody(ctx.body); const parsed = parseCreateInvoiceInput(parsedBody); if (!parsed.success) { return jsonResponse(400, { code: "VALIDATION_FAILED", issues: parsed.issues, }); } const created = createInvoice(ctx.store, parsed.data); return jsonResponse(200, created); } if (ctx.method === "POST") { const finaliseId = matchFinaliseId(ctx.pathname); if (finaliseId !== null) { const deny = rbacCheck(ctx.capabilities, "example:write"); if (deny !== null) return deny; // The trusted tier awaits a Promise here to keep its // Stripe-call histogram observation > 0 ms; the wasm tier // has no histogram to record so the await is a no-op. await Promise.resolve(); const updated = finaliseInvoice(ctx.store, finaliseId); return jsonResponse(200, updated); } } if (ctx.method === "GET" && ctx.pathname === "/example/rate-limited") { return jsonResponse(200, { ok: true }); } if ( ctx.method === "GET" && ctx.pathname === "/example/subscription-required" ) { return raiseSubscriptionInactive(); } if (ctx.method === "GET" && ctx.pathname === "/_/test/fail-response") { const fail = ctx.search.get("failResponse") === "1"; return jsonResponse(200, buildFailResponsePayload(fail)); } return jsonResponse(404, { code: "ROUTE_NOT_FOUND", path: ctx.pathname }); } function matchInvoiceId(pathname: string): string | null { const prefix = "/example/invoices/"; if (!pathname.startsWith(prefix)) return null; const tail = pathname.slice(prefix.length); if (tail.length === 0 || tail.includes("/")) return null; return tail; } function matchFinaliseId(pathname: string): string | null { const prefix = "/example/invoices/"; const suffix = "/finalise"; if (!pathname.startsWith(prefix) || !pathname.endsWith(suffix)) return null; const id = pathname.slice(prefix.length, pathname.length - suffix.length); if (id.length === 0 || id.includes("/")) return null; return id; } function rbacCheck( capabilities: readonly string[], scope: string ): SerializedResponse | null { if (capabilities.includes(scope)) return null; return jsonResponse(403, { code: "RBAC_MISSING_SCOPE", requiredScope: scope, actualScopes: capabilities, }); } function jsonResponse(status: number, payload: unknown): SerializedResponse { const body = JSON.stringify(payload); return { status, headers: [["content-type", "application/json"]], bodyBase64: encodeBase64Url(textToBytes(body)), }; } /** Parse the request body bytes as JSON (or `undefined` when empty/invalid). */ function parseRequestBody(body: Uint8Array): unknown { if (body.length === 0) return undefined; const text = bytesToText(body); try { return JSON.parse(text); } catch { return undefined; } } /** Decode base64-url-no-pad to raw bytes. */ function decodeBody(base64Url: string): Uint8Array { if (base64Url.length === 0) return new Uint8Array(0); const padLen = (4 - (base64Url.length % 4)) % 4; const padded = base64Url.replace(/-/g, "+").replace(/_/g, "/") + "=".repeat(padLen); const binary = atob(padded); const out = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i += 1) { out[i] = binary.charCodeAt(i); } return out; } /** Encode raw bytes as base64-url-no-pad. */ function encodeBase64Url(bytes: Uint8Array): string { const codes: number[] = []; for (const byte of bytes) { codes.push(byte); } const binary = String.fromCharCode(...codes); const b64 = btoa(binary); return b64.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, ""); } function textToBytes(text: string): Uint8Array { return new TextEncoder().encode(text); } function bytesToText(bytes: Uint8Array): string { return new TextDecoder().decode(bytes); }