import { SAND_PERM_CODES, SandboxPermissionError, } from "@suaveplan/api-sandbox-permissions"; import type { SerializedRequest, SerializedResponse, } from "@suaveplan/api-types/sandbox-bridge"; import { SAND_WASM_BRIDGE_CODES, SandboxWasmBridgeError, } from "../error-codes/error-codes.js"; /** * Shape the bridge factory threads through its `RequestContext` slot. * Defined locally to avoid leaking the host's `RequestContext` * generic into this utility — both variants need the same five fields * and nothing more. */ export interface RequestContextLike { readonly request: Request; readonly principal: { readonly id: string } | null; readonly tenant: { readonly id: string } | null; readonly capabilities: Readonly>; readonly requestId: string; } /** * Marshal a `RequestContextLike` into the wire `SerializedRequest` * shape both variants exchange with their guest plugins. */ export async function marshalContextRequest( ctx: RequestContextLike ): Promise { const request = ctx.request; const headers: Array = []; request.headers.forEach((value, name) => { headers.push([name.toLowerCase(), value] as const); }); let bodyBase64 = ""; if (request.body !== null) { const buffer = await request.arrayBuffer(); bodyBase64 = bytesToBase64UrlNoPad(new Uint8Array(buffer)); } return { url: request.url, method: request.method.toUpperCase(), headers, bodyBase64, principalId: (ctx.principal === null ? null : ctx.principal.id) as SerializedRequest["principalId"], tenantId: (ctx.tenant === null ? null : ctx.tenant.id) as SerializedRequest["tenantId"], capabilities: Object.keys(ctx.capabilities), requestId: ctx.requestId, timestamp: Date.now(), }; } /** * Convert the wire `SerializedResponse` into a host `Response`. When * the wire response carries an `errorCode`, surface the matching * sandbox error (preserving the canonical `SAND_PERM.DENIED` mapping). */ export function buildHostResponse( wire: SerializedResponse, pluginName: string ): Response { if (wire.errorCode !== undefined) { if (wire.errorCode === SAND_PERM_CODES.DENIED) { throw new SandboxPermissionError( SAND_PERM_CODES.DENIED, wire.errorMessage ?? "capability denied", { context: { plugin: pluginName } } ); } throw new SandboxWasmBridgeError( SAND_WASM_BRIDGE_CODES.CAPABILITY_DENIED, wire.errorCode, { context: { plugin: pluginName, sandboxErrorCode: wire.errorCode, sandboxMessage: wire.errorMessage ?? "", }, } ); } const headers = new Headers(); for (const [name, value] of wire.headers) headers.append(name, value); const body = base64UrlNoPadToBytes(wire.bodyBase64); return new Response( body.length > 0 ? (body as unknown as BodyInit) : null, { status: wire.status, headers, } ); } function bytesToBase64UrlNoPad(bytes: Uint8Array): string { let binary = ""; for (let i = 0; i < bytes.length; i++) { binary += String.fromCharCode(bytes[i] as number); } return btoa(binary) .replace(/\+/g, "-") .replace(/\//g, "_") .replace(/=+$/, ""); } function base64UrlNoPadToBytes(value: string): Uint8Array { if (value.length === 0) return new Uint8Array(0); const padded = value .replace(/-/g, "+") .replace(/_/g, "/") .padEnd(value.length + ((4 - (value.length % 4)) % 4), "="); const binary = atob(padded); const out = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { out[i] = binary.charCodeAt(i); } return out; }