# 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 `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.