3.3 KiB
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:<host>
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 explicithttp:rule prefix. - Wildcard suffix — a rule like
*.stripe.commatcheseu.api.stripe.combut NOT the bare apexstripe.com. - URL-parse failure returns
deny:invalid-urlrather than throwing.
Usage: Basic
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
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): CheckResultCheckResult = { outcome: "allow" } | { outcome: "deny"; reason: string }reasonis 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.