Privacy egress gate
An empty allowlist blocks every outbound host by default, a request only egresses if a typed sink names the exact host and why.
What it is
local-privacy is the Local-first edition's runtime egress boundary: a closed-enum PrivacyPolicy (Zod .strict(), "local-only" the sole mode) declares zero-egress-by-default, and EgressGuard enforces it in front of the kernel's fetchWithTimeout chokepoint. A host must be allowlisted for one of exactly two sanctioned sink kinds (model-fetch or rented-backend) before a socket ever opens; an empty or omitted allowlist blocks everything.
What ships in the module
Closed-enum policy, not a config flag
privacyPolicySchema is a strictObject over privacyModeSchema (PRIVACY_MODES has exactly one member, "local-only") and a bounded allowlist (max 16 entries, default []). There is deliberately no "hosted" mode in the enum, introducing one takes an ADR and a schema change, not a config edit. ZERO_EGRESS_POLICY is the frozen air-gap default: local-only with an empty allowlist.
Two sanctioned sink kinds, exact-match hosts
SANCTIONED_SINK_KINDS closes the reachable-for-a-reason set to model-fetch (the first-run ONNX model download) and rented-backend (the opt-in metered hosted-inference host). egressSinkSchema validates each entry against HOSTNAME_RE and normalizes it (trim + lowercase), matching is exact-string against url.hostname, never a suffix or wildcard.
Blocked before a socket opens
EgressGuard.assertAllowed rejects a non-https scheme, a malformed URL, or a host absent from the allowlist, all as fail-closed AuthzError/ValidationError thrown before fetchWithTimeout is ever reached. The thrown error's details carry only host and scheme, never the full URL, so a blocked path or query holding a token or PII is never captured in the error.
Purpose-bound sinks, a rented-backend Bearer can't reach the model host
assertAllowedFor / fetchAs require a host to be allowlisted for a specific kind, not just present on the list. A host sanctioned only for model-fetch throws AuthzError if a rented-backend credentialed request targets it, and vice versa, each sink kind exists for exactly one credentialed surface.
guardedFetch, install as another runtime's outbound hook
The guard exposes itself as a bare (input, init) => Promise<Response>, the shape transformers.js's env.fetch accepts, so an on-device model loader can be handed the guard directly and cannot egress out of band. @caisson/local-inference's rented-backend transport calls guard.fetchAs("rented-backend", ...) the same way, composed directly, not just a manifest listing.
Defensive re-parse at construction
The EgressGuard constructor re-runs parsePrivacyPolicy on the policy it's given, so a hand-built or deserialized policy object that bypassed parsePrivacyPolicy at the boundary still fails closed on a bad host, unknown kind, or unknown mode before the guard's host map is even built.
assertAllowed(input: string | URL): URL {
let url: URL;
try {
url = input instanceof URL ? input : new URL(input);
} catch {
throw new ValidationError("egress blocked: malformed URL");
}
if (url.protocol !== "https:") {
// Non-https never egresses — blocks http:, and data:/file:/javascript: smuggling.
throw new AuthzError("egress blocked: non-https scheme", {
scheme: url.protocol,
});
}
const host = url.hostname.toLowerCase();
if (!this.#allow.has(host)) {
// Empty allowlist ⇒ this branch always fires ⇒ zero egress. No host is implicit.
throw new AuthzError(
"egress blocked: host not on the privacy allowlist (fail-closed-to-offline)",
{ host, privacy: this.#policy.privacy },
);
}
return url;
}- url.protocol !== "https:" runs before the allowlist lookup, http:, data:, file:, and javascript: schemes are blocked outright, not just non-allowlisted hosts.
- this.#allow.has(host) checks a Map built once at construction from the policy's allowlist, with an empty allowlist this is always false, so every call falls through to the AuthzError (zero egress by default).
- The thrown AuthzError's details carry only host and privacy, never the input URL's path or query, where a token or PII could otherwise leak into a caught error.