Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,201 @@
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<SerializedResponse> {
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<SerializedResponse> {
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);
}