first commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user