4.0 KiB
bridge
(no submodules)
Purpose
This module is the package's flagship factory:
createSandboxedVmPluginModule({ sandbox, manifest, registry }) →
BridgedPlugin — a frozen object whose module is a
PluginModule-shaped lifecycle bundle and whose routes is a
materialised PipelineRoute[] ready to hand to
host.installPlugin({ module, manifest, routes, ... }).
The factory drives the underlying sandbox's start() through the
idempotent lifecycle forwarder, asserts the sandbox registered at
least one route via the bridge.registerRoute capability, and wraps
every registered handler in a marshal-invoke-unmarshal closure so the
host's request pipeline can dispatch into the sandboxed plugin as if
it were a regular trusted plugin.
Features
- Lifecycle delegation —
module.start(),module.stop(),module.uninstall()thread through the lifecycle forwarder so the host's lifecycle driver sees consistent idempotent semantics. - Route materialisation — every sandbox registration becomes a frozen
PipelineRoutewith the plugin id, method (already upper-cased), path, options (defaulted, overridable), wrapped handler, empty params, and forwarded status map. - Marshal closure per route — host
Request→SerializedRequest→ sandbox handler →SerializedResponse→ hostResponse. - Empty-registry guard — sandboxes that registered no routes raise
INVALID_PLUGIN_SURFACEso the host never mounts an empty plugin. - OTel attribution — each step (marshal, handler invocation, unmarshal) increments the relevant counter on the bridge's OTel bundle.
- Cause-chain preservation — sandbox throws surface as
HANDLER_REJECTEDwith the original error preserved ascause.
Usage: Basic
import { createSandboxedVmPluginModule, createBridgeRouteRegistry }
from "@suaveplan/api-sandbox-vm-host-bridge";
import { createVmSandbox } from "@suaveplan/api-sandbox-vm";
const registry = createBridgeRouteRegistry();
const sandbox = createVmSandbox({
pluginId: "demo",
pluginModule: { kind: "source", identifier: "demo.mjs", source },
checker,
proxies: { fs, fetch, capabilities: { bridge: registry.capability } },
logger,
});
const bridged = await createSandboxedVmPluginModule({
sandbox,
manifest,
registry,
});
await host.installPlugin({
module: bridged.module,
manifest,
manifestHash,
specifier,
routes: bridged.routes,
});
Usage: Advanced
Supply per-route option overrides keyed by "METHOD /path":
const bridged = await createSandboxedVmPluginModule({
sandbox,
manifest,
registry,
routeOptions: {
"POST /upload": { streaming: true, auth: "tenant.write" },
"GET /healthz": { auth: null, csrf: false },
},
statusMap: { 200: 201 },
});
API Reference
interface BridgedPluginModule<Caps>— frozen lifecycle bundle.interface BridgedPlugin<Caps>—module,routes,dispose,lifecycle.interface CreateSandboxedVmPluginModuleOptions—sandbox,manifest,registry,routeOptions?,statusMap?.createSandboxedVmPluginModule<Caps>(options): Promise<BridgedPlugin<Caps>>— async factory; runslifecycle.start()synchronously before returning.
Implementation Notes
The factory deliberately freezes both the returned BridgedPlugin and
its nested module / routes so the host's lifecycle driver cannot
patch a route's handler after install. The wrapped handler captures
the OTel duration histogram across the whole marshal-invoke-unmarshal
cycle — this surfaces back-pressure under load as a single metric the
operator can alert on without instrumenting each sub-step
individually.
The requires generic on BridgedPluginModule is allowed to be empty
because the bridge owns the only capability the sandbox needs (bridge);
plugins that declare other capability requirements (db, secrets) thread
those through the sandbox via the host's existing capability map — the
bridge itself is unaware of them.