Files
skills/docs/api-stack/api/api-sandbox-vm-host-bridge/src/route-discovery/route-discovery.md
2026-05-29 08:20:56 +01:00

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 + the bridge capability bag in one call.
  • Zod-validated registrations — registerRoute rejects non-string methods, paths missing the leading /, or non-function handlers with SAND_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 after sandbox.start() resolves to ensure the plugin registered at least one route. Empty registries raise INVALID_PLUGIN_SURFACE.
  • Frozen capability bag — the sandbox cannot patch registerRoute to 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's CapabilityBag structural type.
  • interface BridgeRouteRegistry — registrations() and capability.
  • 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.