first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,91 @@
# Request / response marshaller
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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<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.