first commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user