first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
# route-discovery
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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> | 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.