79 lines
4.4 KiB
Markdown
79 lines
4.4 KiB
Markdown
# Bridge factory
|
|
|
|
<!-- begin:auto-docs -->
|
|
_(no submodules)_
|
|
<!-- end:auto-docs -->
|
|
|
|
## Purpose
|
|
|
|
`createSandboxedWasmPluginModule(options)` is the public entry point of `@suaveplan/api-sandbox-wasm-host-bridge`. It dispatches on the supplied `abiVariant`: the default (`"direct-i32-i64"`) routes to `buildDirectInstall` in `bridge-direct.ts`, which validates the six `__suaveplan_*` ABI exports + `memory` and bridges requests through linear memory; the additive variant (`"wasi-stdio"`) routes to `buildWasiStdioInstall` in `bridge-wasi-stdio.ts`, which validates the WASI snapshot-preview1 import surface + `_start` + `memory` and bridges requests through length-prefixed JSON envelopes on fd 0 / fd 1. Both paths return the same `SandboxedWasmPluginInstall` shape so the host's `installPlugin` call is variant-agnostic. The returned bundle carries the `module`, the bridged `PipelineRoute[]`, the idempotent lifecycle, and the underlying sandbox handle.
|
|
|
|
## Features
|
|
|
|
- Single bridge-init validation: the ABI is checked, routes are discovered, and instruments are wired up in one synchronous block; failures surface BEFORE the host sees the install bundle.
|
|
- Per-route closures own the full linear-memory round-trip: alloc → write → handle_request → unpack → read → free → JSON-decode → host `Response`.
|
|
- Trap classification: `__suaveplan_handle_request` faults surface as `SAND_WASM_BRIDGE.HANDLER_TRAPPED` with `context.trapKind` derived from the runtime error message.
|
|
- Empty-response detection: a `(0, 0)` packed return value from the guest is treated as a marshal failure (`reason: "empty_response"`) rather than as a silent success.
|
|
- Capability denial passthrough: a `SerializedResponse.errorCode === "SAND_PERM.DENIED"` is re-thrown as the canonical `SandboxPermissionError`; other error codes surface as `SAND_WASM_BRIDGE.CAPABILITY_DENIED` carrying the sandbox's original code in `context.sandboxErrorCode`.
|
|
- Lifecycle stop chains through to `WasmSandboxHandle.dispose()` so a `host.uninstallPlugin` always tears down the underlying sandbox.
|
|
|
|
## Usage: Basic
|
|
|
|
```ts
|
|
import { createWasmSandbox } from "@suaveplan/api-sandbox-wasm";
|
|
import { createSandboxedWasmPluginModule } from "@suaveplan/api-sandbox-wasm-host-bridge";
|
|
|
|
const sandbox = await createWasmSandbox({
|
|
pluginName: "example",
|
|
policy,
|
|
capabilities,
|
|
checker,
|
|
wasmBytes,
|
|
});
|
|
|
|
const install = createSandboxedWasmPluginModule({ sandbox, manifest });
|
|
await host.installPlugin({
|
|
module: install.module,
|
|
manifest: install.manifest,
|
|
manifestHash,
|
|
specifier: "example@0.1.0",
|
|
routes: install.routes,
|
|
});
|
|
```
|
|
|
|
## Usage: Advanced
|
|
|
|
```ts
|
|
import { createSandboxWasmBridgeInstruments } from "@suaveplan/api-sandbox-wasm-host-bridge/instruments";
|
|
import { createSandboxedWasmPluginModule } from "@suaveplan/api-sandbox-wasm-host-bridge/bridge";
|
|
|
|
const instruments = createSandboxWasmBridgeInstruments({
|
|
pluginName: manifest.name,
|
|
meterProvider: customProvider,
|
|
});
|
|
|
|
const install = createSandboxedWasmPluginModule({
|
|
sandbox,
|
|
manifest,
|
|
instruments,
|
|
});
|
|
|
|
install.module.start();
|
|
try {
|
|
await runRequests(install.routes);
|
|
} finally {
|
|
install.module.stop();
|
|
}
|
|
```
|
|
|
|
## API Reference
|
|
|
|
- `createSandboxedWasmPluginModule(options): SandboxedWasmPluginInstall` — throws `SAND_WASM_BRIDGE.INVALID_PLUGIN_SURFACE` on ABI mismatch.
|
|
- `CreateSandboxedWasmPluginModuleOptions` — `{ sandbox, manifest, instruments? }`.
|
|
- `SandboxedWasmPluginInstall` — `{ module, manifest, routes, lifecycle, sandbox }`.
|
|
- `SandboxedWasmPluginModule` — plugin-module facade with `name`, `version`, `requires`, `isolation: "sandboxed"`, `trustLevel`, `start()`, `stop()`.
|
|
|
|
## Implementation Notes
|
|
|
|
The per-route closure is async because reading the host's `Request.body` requires awaiting the `arrayBuffer()` promise; the guest call itself is synchronous (WASM exports are synchronous unless the module uses the JS Promise Integration proposal, which this bridge does not target). Base64-url-no-pad encoding/decoding is inlined rather than delegated to a foundation utility because the bridge must boot before any optional foundation polyfill has a chance to load — keeping the codec self-contained avoids a circular load order with `@suaveplan/api-host`. `Object.freeze` is applied to the `routes` array AND each route's `options` / `params` / `statusMap` records so the host's downstream code cannot mutate the bridge's view by accident.
|