first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -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.