# `checker/fetch` — outbound host allowlist _(no submodules)_ ## Purpose This module owns the `permissions.fetch[]` enforcement path. Every outbound HTTP call routed through the sandbox host MUST pass through `checkFetch` before the underlying `fetch(...)` runs. The matcher is host-only — request bodies, methods, headers, and paths are NOT part of this allowlist. The point is to bound which destinations a plugin can exfiltrate to, not to model an HTTP firewall. The scheme gate is part of the permission decision: by default only `https:` is permitted, and the rule must be authored as `http:` to allow plaintext requests against that host (typically used only for local development against `localhost`). ## Features - `checkFetch(policy, url)` — returns allow / deny. - Scheme-aware — `https:` always passes the protocol gate; `http:` requires an explicit `http:` rule prefix. - Wildcard suffix — a rule like `*.stripe.com` matches `eu.api.stripe.com` but NOT the bare apex `stripe.com`. - URL-parse failure returns `deny:invalid-url` rather than throwing. ## Usage: Basic ```ts import { PermissionPolicy } from "@suaveplan/api-sandbox-permissions/policy"; import { checkFetch } from "@suaveplan/api-sandbox-permissions/checker"; const policy = new PermissionPolicy({ permissions: { fetch: ["api.stripe.com", "*.example.com"] }, }); checkFetch(policy, "https://api.stripe.com/v1/charges"); // allow checkFetch(policy, "https://eu.example.com/api"); // allow checkFetch(policy, "https://example.com"); // deny: host-not-in-allowlist ``` ## Usage: Advanced ```ts import { PermissionPolicy } from "@suaveplan/api-sandbox-permissions/policy"; import { checkFetch } from "@suaveplan/api-sandbox-permissions/checker"; import { SAND_PERM_CODES, SandboxPermissionError, } from "@suaveplan/api-sandbox-permissions/error-codes"; const policy = new PermissionPolicy({ permissions: { fetch: ["http:localhost", "api.stripe.com"] }, }); function gatedFetch(plugin: string, url: string): Response | never { const result = checkFetch(policy, url); if (result.outcome === "deny") { throw new SandboxPermissionError( SAND_PERM_CODES.DENIED, "fetch capability denied", { context: { capability: "fetch", reason: result.reason } } ); } // …perform the actual fetch on the host side… return new Response(); } gatedFetch("billing", "http://localhost:8080/dev"); // allowed gatedFetch("billing", "https://api.stripe.com/v1/charges"); // allowed ``` ## API Reference - `checkFetch(policy: PermissionPolicy, url: string): CheckResult` - `CheckResult = { outcome: "allow" } | { outcome: "deny"; reason: string }` - `reason` is one of `"invalid-url"`, `"scheme-not-permitted"`, `"host-not-in-allowlist"`. ## Implementation Notes The host comparator lowercases both sides because the manifest is operator-authored and case may differ from the request URL. The wildcard form `*.suffix` matches only proper subdomains — the bare apex must be authored as its own exact-match rule. URL parse failures degrade to a deny rather than a throw so the host worker boundary doesn't need an extra try/catch around the check call. The deny reason `"invalid-url"` is observable on the `plugin.permission_denied_total` counter exactly like every other deny reason.