first commit
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
# `checker/fetch` — outbound host allowlist
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## 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 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.
|
||||
Reference in New Issue
Block a user