feat: guardedFetch + sameSitePolicy — runtime-agnostic guarded fetch#10
Merged
Conversation
Workers/browser/edge consumers had to hand-roll the manual-redirect loop the README described (AskLinq did exactly that). The pattern now ships as API: - guardedFetch(input, policy, init?) — URL-time validation plus the same redirect semantics as safeFetch (every hop re-passes the policy before it is followed, credentials stripped on cross-origin redirects, 303/301/302-POST downgrade to GET without body replay), minus DNS resolution and pinning. The policy allowlist is the primary control; the package's fail-closed default (empty allowlist allows nothing) does real work here. fetchImpl is injectable. - sameSitePolicy(url, overrides?) — derive the allowlist from a submitted URL: the whole fetch, redirects included, locks to that domain (www stripped so apex<->www redirects survive). Overrides merge additively so specific extra hosts can be allowlisted — the 'bypass' mechanism stays in the policy, auditable in one place. Refactor: the redirect-revalidation loop moved verbatim from safeFetch into redirect.ts (no Node imports) and both fetchers share it — safeFetch layers its DNS hooks (unpinned per-hop check, pinned undici connector, guard-error unwrap) on top. node:dns is now imported lazily, so loading the package no longer requires a functional dns module; calling safeFetch without one throws a typed SsrfGuardError pointing to guardedFetch. README (en+ko): Workers section now shows guardedFetch/sameSitePolicy instead of the DIY loop; support matrix gains the new row. Tests: 136/136 green — 12 new (allowlist fail-closed, redirect escape blocked without fetching, credential stripping, 303 downgrade, maxRedirects, sameSitePolicy derivation/merging/typed errors); the untouched safe-fetch suites (18 unpinned + 5 pinned) pass unchanged against the shared loop.
This was referenced Jul 12, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The Workers pattern the README told consumers to hand-roll (#9) now ships as API — AskLinq's URL ingestion was the first consumer to write that loop by hand, which was the signal it belongs in the package.
guardedFetch(input, policy, init?)URL-time validation + the same redirect semantics as
safeFetch— every hop re-passes the policy before it is followed, credentials stripped on cross-origin redirects,303/301/302-POST downgrade to GET without body replay — minus DNS resolution and pinning. Works on Cloudflare Workers, browsers, and edge runtimes.fetchImplis injectable for tests/instrumentation.The policy allowlist is the primary control: the fail-closed default (empty allowlist allows nothing) does real work here. Opening a specific host = putting it in the allowlist — the bypass mechanism stays in the policy, auditable in one place.
sameSitePolicy(url, overrides?)Derives the allowlist from a submitted URL: the whole fetch, redirects included, locks to that domain (
www.stripped so apex ↔ www redirects survive; subdomains reachable). Overrides merge additively, so extra hosts can be allowlisted alongside.Refactor
safeFetchintoredirect.ts(zero Node imports); both fetchers share it.safeFetchlayers its DNS hooks on top (unpinned per-hop check / pinned undici connector / guard-error unwrap).node:dnsis now lazily imported — loading the package no longer requires a functional dns module; callingsafeFetchwithout one throws a typedSsrfGuardErrorpointing toguardedFetch.Docs
README (en + ko): Workers section now shows
guardedFetch/sameSitePolicyinstead of the DIY loop; support matrix gains the new row. CHANGELOG gets an Unreleased section.Verification
tsc --noEmitclean, build cleansameSitePolicyderivation/merging/typed errors); the untouched safe-fetch suites (18 unpinned + 5 pinned) pass unchanged against the shared loopAfter release
AskLinq's
ingest/url.tsswaps its hand-rolledguardedFetchPageloop forguardedFetch+sameSitePolicy.