3.4 KiB
fetch proxy
(no submodules)
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
pluginandcapabilityattributes on the sharedplugin.permission_denied_totalcounter 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 throughnew URL(...).hostname; an unparseable URL is rejected by the checker before reaching the host.
Usage: Basic
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:
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 withrequest(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.