3.7 KiB
route-discovery
(no submodules)
Purpose
This module owns the route-discovery protocol that lets a sandboxed
plugin tell the host which HTTP routes it wants to serve. Because the
sandboxed plugin runs inside a vm.Context with no direct access to
the host's router, the bridge installs a bridge.registerRoute
capability that the plugin invokes during its module-evaluation phase;
the host captures every registration and replays them as
PipelineRoute entries on the bridge's PluginModule.
The pattern intentionally mirrors how __suaveplan.capabilities.*
already works in @suaveplan/api-sandbox-vm — a sandboxed plugin sees
bridge as just another capability, so the contract for declaring a
route is the same as the contract for any other host-resident API.
Features
createBridgeRouteRegistry()factory — produces a fresh capture registry + thebridgecapability bag in one call.- Zod-validated registrations —
registerRouterejects non-string methods, paths missing the leading/, or non-function handlers withSAND_VM_BRIDGE.INVALID_PLUGIN_SURFACE. - Method normalisation — methods are upper-cased before storage so
"get"and"GET"produce equivalent registrations. requireNonEmptyRegistry()— guard the bridge calls aftersandbox.start()resolves to ensure the plugin registered at least one route. Empty registries raiseINVALID_PLUGIN_SURFACE.- Frozen capability bag — the sandbox cannot patch
registerRouteto bypass the validator.
Usage: Basic
import { createBridgeRouteRegistry } from "@suaveplan/api-sandbox-vm-host-bridge/route-discovery/route-discovery";
const registry = createBridgeRouteRegistry();
const sandbox = createVmSandbox({
proxies: {
fs,
fetch,
capabilities: { bridge: registry.capability },
},
// ...
});
await sandbox.start();
const routes = registry.registrations();
Usage: Advanced
import {
createBridgeRouteRegistry,
requireNonEmptyRegistry,
} from "@suaveplan/api-sandbox-vm-host-bridge/route-discovery/route-discovery";
const registry = createBridgeRouteRegistry();
// pass registry.capability to the sandbox under
// `proxies.capabilities.bridge`, then start the sandbox.
await sandbox.start();
const routes = requireNonEmptyRegistry(registry, "demo-plugin");
// routes is `readonly BridgeRouteRegistration[]`
API Reference
type SandboxRouteHandler = (req: SerializedRequest) => Promise<SerializedResponse> | SerializedResponse.interface BridgeRouteRegistration—method,path,handler.interface BridgeCapability—registerRoute(registration: unknown): Promise<void>plus the open string-indexed mirror so the capability bag conforms to@suaveplan/api-sandbox-vm'sCapabilityBagstructural type.interface BridgeRouteRegistry—registrations()andcapability.createBridgeRouteRegistry(): BridgeRouteRegistry.requireNonEmptyRegistry(registry, pluginId): readonly BridgeRouteRegistration[].
Implementation Notes
registerRoute is implemented as an async function on the capability
bag because @suaveplan/api-sandbox-vm's installCapabilityProxies
contract requires every capability method to be async. The validator
runs synchronously inside the async body so failures surface on the
sandbox side via the proxy's permission-gate wrapper.
The Zod schema uses z.custom<SandboxRouteHandler> for the handler
field because the runtime check (typeof v === "function") is the
only invariant we can verify before invocation — the signature shape
is type-only and cannot be runtime-checked without invoking the
handler with a probe value, which would defeat the purpose.