# Request / response marshaller _(no submodules)_ ## Purpose Encodes a `SerializedRequest` to UTF-8 JSON bytes for transport into the guest's linear memory and decodes the matching `SerializedResponse` (or the JSON-encoded `SerializedRoute[]` produced by `__suaveplan_routes`) back into typed records. Wraps every failure mode behind the two stable codes `SAND_WASM_BRIDGE.MARSHAL_FAILED` and `SAND_WASM_BRIDGE.UNMARSHAL_FAILED` so call sites have a single catch-and-translate path. ## Features - `marshalRequest` — `JSON.stringify` + `TextEncoder` round-trip, distinguishing `JSON.stringify` throws from the `undefined`-return case caused by non-serialisable top-level values. - `unmarshalResponse` / `unmarshalRoutes` — strict UTF-8 decoding (`fatal: true`) + `JSON.parse` + Zod schema validation, with `context.reason` set to one of `"invalid_utf8" | "invalid_json" | "shape_mismatch"` so operators can tell apart guest-corruption from guest-bug failures. - Pure functions — no shared state, safe to call concurrently from multiple route handlers. - Strict-mode Zod schemas reject extra keys at the response boundary; the bridge therefore never silently honours an unspecified field a future guest might emit. ## Usage: Basic ```ts import { marshalRequest, unmarshalResponse, } from "@suaveplan/api-sandbox-wasm-host-bridge/marshal"; const reqBytes = marshalRequest(serialized, "example"); const respBytes = await invokeGuest(reqBytes); const wireResponse = unmarshalResponse(respBytes, "example"); ``` ## Usage: Advanced ```ts import { marshalRequest, unmarshalResponse, unmarshalRoutes, } from "@suaveplan/api-sandbox-wasm-host-bridge/marshal"; import type { SerializedRequest } from "@suaveplan/api-types/sandbox-bridge"; import { SAND_WASM_BRIDGE_CODES, SandboxWasmBridgeError, } from "@suaveplan/api-sandbox-wasm-host-bridge/error-codes"; function safeUnmarshal( bytes: Uint8Array, plugin: string, ): Result { try { return { ok: true, value: unmarshalResponse(bytes, plugin) }; } catch (err) { if ( err instanceof SandboxWasmBridgeError && err.code === SAND_WASM_BRIDGE_CODES.UNMARSHAL_FAILED ) { return { ok: false, reason: err.context?.["reason"] as string }; } throw err; } } const routes = unmarshalRoutes(routeBytes, "example"); const requestBytes: Uint8Array = marshalRequest( { url: "https://app.example.com/", method: "GET", headers: [], bodyBase64: "", principalId: null, tenantId: null, capabilities: [], requestId: "r1", timestamp: 0, } satisfies SerializedRequest, "example", ); type Result = | { readonly ok: true; readonly value: ReturnType } | { readonly ok: false; readonly reason: string }; ``` ## API Reference - `marshalRequest(request: SerializedRequest, pluginName: string): Uint8Array` — throws `SAND_WASM_BRIDGE.MARSHAL_FAILED`. - `unmarshalResponse(bytes: Uint8Array, pluginName: string): SerializedResponse` — throws `SAND_WASM_BRIDGE.UNMARSHAL_FAILED`. - `unmarshalRoutes(bytes: Uint8Array, pluginName: string): readonly SerializedRoute[]` — same failure mode. ## Implementation Notes The `TextDecoder` is constructed with `fatal: true` so malformed UTF-8 surfaces as a typed `TypeError` (the bridge's catch wraps it). The Zod schemas use `.strict()` because the wasm-side guest is expected to emit byte-for-byte the agreed shape; tolerating extra keys would let a buggy guest leak metadata into operator logs unnoticed. `JSON.stringify` returning `undefined` (top-level non-encodable, e.g. a `BigInt` reachable through a `toJSON` hook) is treated as a `"non_serialisable"` reason rather than a thrown error so the bridge can attribute the failure even when no `cause` is available.