3.8 KiB
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+TextEncoderround-trip, distinguishingJSON.stringifythrows from theundefined-return case caused by non-serialisable top-level values.unmarshalResponse/unmarshalRoutes— strict UTF-8 decoding (fatal: true) +JSON.parse+ Zod schema validation, withcontext.reasonset 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
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
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<typeof unmarshalResponse> }
| { readonly ok: false; readonly reason: string };
API Reference
marshalRequest(request: SerializedRequest, pluginName: string): Uint8Array— throwsSAND_WASM_BRIDGE.MARSHAL_FAILED.unmarshalResponse(bytes: Uint8Array, pluginName: string): SerializedResponse— throwsSAND_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.