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 @@
# fetch proxy
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`installFetchProxy` returns the outbound-HTTP surface visible inside
the sandbox. Every request gates through the supplied
`PermissionChecker` before reaching the host adapter, and denied calls
never touch the network — they raise `SAND_VM.PERMISSION_DENIED` and
increment `plugin.permission_denied_total{capability="fetch"}` without
opening a socket, performing a DNS lookup, or hitting the host's
connection pool.
The proxy intentionally exposes only a `request` method with a narrow
shape (`url`, optional `init`) so the sandboxed plugin cannot reach
ambient Node primitives like the connection pool, the DNS resolver,
the timeout default, or the raw streaming body. Anything richer is
the responsibility of an explicit capability (and arrives through
`installCapabilityProxies`).
## Features
- Synchronous permission check; the host adapter call is deferred
until allowance is confirmed.
- Closure-bound adapter — the sandbox cannot pry it back out of the
proxy receiver.
- Denial path records the `plugin` and `capability` attributes on the
shared `plugin.permission_denied_total` counter so dashboards see a
uniform denial-rate series across permission implementations.
- `URL`-parsed host match — the proxy receives the URL as a string and
the checker normalises it through `new URL(...).hostname`; an
unparseable URL is rejected by the checker before reaching the host.
## Usage: Basic
```ts
import { installFetchProxy } from "@suaveplan/api-sandbox-vm/proxies/fetch-proxy/fetch-proxy";
import { createPermissionChecker } from "@suaveplan/api-sandbox-vm/permissions/checker";
const checker = createPermissionChecker({
policy: { fetch: ["api.example.com"] },
});
const fetch = installFetchProxy({
checker,
pluginId: "demo",
adapter: { request: async (url) => realRequest(url) },
});
const res = await fetch.request("https://api.example.com/v1/users");
```
## Usage: Advanced
Wrap the adapter to enforce a per-tenant header before reaching the
upstream:
```ts
import { installFetchProxy } from "@suaveplan/api-sandbox-vm/proxies/fetch-proxy/fetch-proxy";
const adapter = {
request: async (
url: string,
init?: { readonly method?: string; readonly body?: string }
) => {
const merged = {
...init,
method: init?.method ?? "GET",
};
return realRequest(url, {
...merged,
// host-side adapter is free to attach additional headers
// that the sandbox cannot see or override.
});
},
};
const fetch = installFetchProxy({ checker, adapter, pluginId: "demo" });
```
## API Reference
- `installFetchProxy(options): FetchProxy` — build the proxy.
- `interface FetchProxy` — frozen receiver with `request(url, init?)`.
- `interface FetchHostAdapter` — the host-supplied counterpart that
performs the actual outbound HTTP request.
## Implementation Notes
The proxy emits the denial metric with a fixed two-attribute label set
(`plugin`, `capability`) so cardinality stays bounded. The
`reason` from the checker decision is folded into the error
`context` rather than into the metric attribute set so the metric
remains aggregatable. Adapter-side errors (network timeouts, host
unreachable, TLS failures) bubble up through the proxy unchanged; the
proxy only owns the policy boundary, not the transport-level error
mapping.