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