Files
suaveplan/packages/api-stack/api/api-sandbox-vm-host-bridge/src/bridge/bridge.md
2026-05-27 23:29:53 +01:00

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 PipelineRoute with 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 → host Response.
  • Empty-registry guard — sandboxes that registered no routes raise INVALID_PLUGIN_SURFACE so 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_REJECTED with the original error preserved as cause.

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; runs lifecycle.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.