# 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 ```ts 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 ```ts 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`. - `interface BridgeRouteRegistration` — `method`, `path`, `handler`. - `interface BridgeCapability` — `registerRoute(registration: unknown): Promise` 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` 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.