Files
skills/docs/api-stack/api/api-sandbox-wasm-host-bridge/src/abi-direct/marshal/marshal.md
2026-05-29 08:20:56 +01:00

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 + 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

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 — 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.