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

3.5 KiB

bridge-marshal

Purpose

Shared RequestContext → SerializedRequest and SerializedResponse → Response helpers used by BOTH bridge variants. Splitting them out keeps bridge-direct.ts and bridge-wasi-stdio.ts focused on the guest-call orchestration that differs between variants — the wire marshalling is identical because both variants exchange the same SerializedRequest / SerializedResponse shapes (the difference is the transport: linear-memory copy vs. WASI-stdio frame).

The error-code mapping for sandbox-originated errorCode values is also shared: SAND_PERM.DENIED surfaces as SandboxPermissionError, everything else as SAND_WASM_BRIDGE.CAPABILITY_DENIED carrying the plugin's own code.

Features

  • marshalContextRequest(ctx) — convert a structural RequestContext into the wire SerializedRequest. Body bytes are base64-url-no-pad encoded; headers are lowercased.
  • buildHostResponse(wire, pluginName) — convert the wire SerializedResponse into a host Response. When wire.errorCode is set, raise the matching sandbox error instead of returning a response.
  • RequestContextLike — minimal structural interface for the five fields the bridge reads. Defined here so neither variant has to import the host's generic RequestContext<T>.

Usage: Basic

import {
    buildHostResponse,
    marshalContextRequest,
    type RequestContextLike,
} from "../bridge/bridge-marshal.js";

async function handle(ctx: RequestContextLike): Promise<Response> {
    const wire = await marshalContextRequest(ctx);
    void wire;
    return buildHostResponse(
        { status: 200, headers: [], bodyBase64: "" },
        "plugin",
    );
}

Usage: Advanced

import { buildHostResponse } from "../bridge/bridge-marshal.js";
import {
    SAND_WASM_BRIDGE_CODES,
    SandboxWasmBridgeError,
} from "../error-codes/error-codes.js";

try {
    const res = buildHostResponse(
        {
            status: 500,
            headers: [],
            bodyBase64: "",
            errorCode: "PLUGIN.RATE_LIMITED",
            errorMessage: "too many",
        },
        "billing",
    );
    void res;
} catch (err) {
    if (
        err instanceof SandboxWasmBridgeError &&
        err.code === SAND_WASM_BRIDGE_CODES.CAPABILITY_DENIED
    ) {
        // The plugin's own code is on `err.context.sandboxErrorCode`.
    }
    throw err;
}

API Reference

marshalContextRequest(ctx: RequestContextLike): Promise<SerializedRequest>

Reads the request body via arrayBuffer() once and encodes the bytes as base64-url-no-pad. Headers are lowercased.

buildHostResponse(wire: SerializedResponse, pluginName: string): Response

Throws SandboxPermissionError when wire.errorCode === "SAND_PERM.DENIED"; otherwise throws SandboxWasmBridgeError with code CAPABILITY_DENIED for any other errorCode. Returns a Response when no error.

Implementation Notes

  • Base64-url-no-pad matches the encoding used by api-types/sandbox-bridge's documented body field; both bridges and Javy guests use the same encoding so the wire shape stays consistent.
  • The request's body is materialised eagerly via arrayBuffer() because the wasi-stdio guest cannot read from a ReadableStream — the entire payload must be in the envelope before it crosses the fd 0 boundary.
  • Capability denials route through SAND_PERM.DENIED regardless of variant; the bridge-local CAPABILITY_DENIED is reserved for plugin-originated codes outside the SAND_PERM namespace.