{"_id":"guarded-fetch","_rev":"11-4ebae51b24a9074d484934ca9a4f8093","name":"guarded-fetch","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.1":{"name":"guarded-fetch","version":"0.1.1","author":{"name":"Vercel"},"license":"Apache-2.0","_id":"guarded-fetch@0.1.1","maintainers":[{"name":"caleb-vercel","email":"caleb.an@vercel.com"}],"homepage":"https://github.com/vercel-labs/guarded-fetch#readme","bugs":{"url":"https://github.com/vercel-labs/guarded-fetch/issues"},"dist":{"shasum":"980b2ad3ad1973f852381c91e97f3318085b747d","tarball":"https://registry.npmjs.org/guarded-fetch/-/guarded-fetch-0.1.1.tgz","fileCount":6,"integrity":"sha512-ma0GCiW6xyUbk1P9ryNlzlPYQG+OcMMG898mF7lIiy2iXdb2r2ktebeiSaAEEbHm8/kgaiadBkb+SR1bTz4aEg==","signatures":[{"sig":"MEUCIQDYmTRQ6II/RMbbEOTWNgU/CnHGgoDPT+7IixmVS69cyQIgQM23r5FiZD30IebBZy6yiKDapC45TF7ElxTMYZ0hQSI=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":169376},"main":"./dist/index.mjs","type":"module","types":"./dist/index.d.mts","engines":{"node":">=20.19"},"exports":{".":{"types":"./dist/index.d.mts","import":"./dist/index.mjs","default":"./dist/index.mjs"},"./package.json":"./package.json"},"gitHead":"fa51047d3d1f55f0d99f1c88a2aac27ff86441b2","scripts":{"lint":"oxlint .","test":"vitest run","build":"tsdown","format":"oxfmt --check","prepare":"lefthook install || true","lint:fix":"oxlint --fix .","validate":"pnpm run --parallel '/^(typecheck|lint|format|test)$/'","typecheck":"tsc --noEmit","format:fix":"oxfmt .","test:watch":"vitest","prepublishOnly":"pnpm build"},"_npmUser":{"name":"caleb-vercel","email":"caleb.an@vercel.com"},"repository":{"url":"git+https://github.com/vercel-labs/guarded-fetch.git"},"_npmVersion":"11.9.0","description":"SSRF-safe HTTP(S) fetch utilities backed by undici: DNS-rebinding protection, redirect validation, header sanitization, and bounded body reads","directories":{},"sideEffects":false,"_nodeVersion":"24.14.0","dependencies":{"undici":"7.28.0","ipaddr.js":"2.2.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@10.32.1","devDependencies":{"oxfmt":"0.44.0","oxlint":"1.59.0","tsdown":"0.21.7","vitest":"4.1.4","lefthook":"2.1.5","typescript":"6.0.2","@types/node":"25.6.0"},"_npmOperationalInternal":{"tmp":"tmp/guarded-fetch_0.1.1_1786580445980_0.9782798630899741","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"guarded-fetch","version":"0.1.2","author":{"name":"Vercel"},"license":"Apache-2.0","_id":"guarded-fetch@0.1.2","maintainers":[{"name":"caleb-vercel","email":"caleb.an@vercel.com"}],"homepage":"https://github.com/vercel-labs/guarded-fetch#readme","bugs":{"url":"https://github.com/vercel-labs/guarded-fetch/issues"},"dist":{"shasum":"10b6cb6b7fe43fc2c8b65cb489ff281153d5c75d","tarball":"https://registry.npmjs.org/guarded-fetch/-/guarded-fetch-0.1.2.tgz","fileCount":6,"integrity":"sha512-oHcFAgmIYWP9/lHLAaSFh7RVTWIAnSbFyFXzrKQEUA1RzBlC+m42y5PANMKZEb4eH1CHGQIy3nx3JAjIkg1UKg==","signatures":[{"sig":"MEUCIQDIc0WIYgt2lh9yifdtPnTeRQ8mckfnnrU4QEfJoaRcCwIgfz+4AVaztvO9GRUl22O5NA/qV8F7l1YpkbRwkcmwjSs=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":184980},"main":"./dist/index.mjs","type":"module","types":"./dist/index.d.mts","engines":{"node":">=20.19"},"exports":{".":{"types":"./dist/index.d.mts","import":"./dist/index.mjs","default":"./dist/index.mjs"},"./package.json":"./package.json"},"gitHead":"c041594cde70a7bee810eb76fb0419a57d0c4fec","scripts":{"lint":"oxlint .","test":"vitest run","build":"tsdown","format":"oxfmt --check","prepare":"lefthook install || true","lint:fix":"oxlint --fix .","validate":"pnpm run --parallel '/^(typecheck|lint|format|test)$/'","typecheck":"tsc --noEmit","format:fix":"oxfmt .","test:watch":"vitest","prepublishOnly":"pnpm build"},"_npmUser":{"name":"caleb-vercel","email":"caleb.an@vercel.com"},"repository":{"url":"git+https://github.com/vercel-labs/guarded-fetch.git"},"_npmVersion":"11.9.0","description":"SSRF-safe HTTP(S) fetch utilities backed by undici: DNS-rebinding protection, redirect validation, header sanitization, and bounded body reads","directories":{},"sideEffects":false,"_nodeVersion":"24.14.0","dependencies":{"undici":"7.28.0","ipaddr.js":"2.2.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@10.32.1","devDependencies":{"oxfmt":"0.44.0","oxlint":"1.59.0","tsdown":"0.21.7","vitest":"4.1.4","lefthook":"2.1.5","typescript":"6.0.2","@types/node":"25.6.0"},"_npmOperationalInternal":{"tmp":"tmp/guarded-fetch_0.1.2_1786750235648_0.47740512534990254","host":"s3://npm-registry-packages-npm-production"}},"0.1.3":{"name":"guarded-fetch","version":"0.1.3","author":{"name":"Vercel"},"license":"Apache-2.0","_id":"guarded-fetch@0.1.3","maintainers":[{"name":"caleb-vercel","email":"caleb.an@vercel.com"}],"homepage":"https://github.com/vercel-labs/guarded-fetch#readme","bugs":{"url":"https://github.com/vercel-labs/guarded-fetch/issues"},"dist":{"shasum":"c03cf7b22d95a8a2274b34c368d3ba762f75156a","tarball":"https://registry.npmjs.org/guarded-fetch/-/guarded-fetch-0.1.3.tgz","fileCount":6,"integrity":"sha512-PSaNyJk8VJlUVEhMHPXT/Z/WgNrLBh43WaYQJiu1qvyCuPQA+kgFMNmAOxI3GU3uXrCkO/xRW11XjcswrksYOA==","signatures":[{"sig":"MEQCIAMFOZUf+Pkb/Uvco/ek4T7Mb3u2pW8kMhbYCDHjF/OsAiB1M9aoL5izLL1Yd4LCybeP/1oVViw5wp5e5lVABk2jhQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":184980},"main":"./dist/index.mjs","type":"module","types":"./dist/index.d.mts","engines":{"node":">=20.19"},"exports":{".":{"types":"./dist/index.d.mts","import":"./dist/index.mjs","default":"./dist/index.mjs"},"./package.json":"./package.json"},"gitHead":"2375d8b264781e3c449e8781c9021282169677bc","scripts":{"lint":"oxlint .","test":"vitest run","build":"tsdown","format":"oxfmt --check","prepare":"lefthook install || true","lint:fix":"oxlint --fix .","validate":"pnpm run --parallel '/^(typecheck|lint|format|test)$/'","typecheck":"tsc --noEmit","format:fix":"oxfmt .","test:watch":"vitest","prepublishOnly":"pnpm build"},"_npmUser":{"name":"caleb-vercel","email":"caleb.an@vercel.com"},"repository":{"url":"git+https://github.com/vercel-labs/guarded-fetch.git"},"_npmVersion":"11.9.0","description":"SSRF-safe HTTP(S) fetch utilities backed by undici: DNS-rebinding protection, redirect validation, header sanitization, and bounded body reads","directories":{},"sideEffects":false,"_nodeVersion":"24.14.0","dependencies":{"undici":"7.29.0","ipaddr.js":"2.2.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@10.32.1","devDependencies":{"oxfmt":"0.44.0","oxlint":"1.59.0","tsdown":"0.21.7","vitest":"4.1.4","lefthook":"2.1.5","typescript":"6.0.2","@types/node":"25.6.0"},"_npmOperationalInternal":{"tmp":"tmp/guarded-fetch_0.1.3_1787355302096_0.6404913377887551","host":"s3://npm-registry-packages-npm-production"}},"0.1.4":{"name":"guarded-fetch","version":"0.1.4","author":{"name":"Vercel"},"license":"Apache-2.0","_id":"guarded-fetch@0.1.4","maintainers":[{"name":"caleb-vercel","email":"caleb.an@vercel.com"}],"homepage":"https://github.com/vercel-labs/guarded-fetch#readme","bugs":{"url":"https://github.com/vercel-labs/guarded-fetch/issues"},"dist":{"shasum":"87a346746e4aef4acbfaf100f98a9d7a475ec71b","tarball":"https://registry.npmjs.org/guarded-fetch/-/guarded-fetch-0.1.4.tgz","fileCount":6,"integrity":"sha512-QSd8MHls7AFgSsC0Gk3nuFnLf7uNBpDmje4j/VEU8wqG2bKZeGJgMbClqlV73/YFtGp/HDIWlA37Q4pmifAR/Q==","signatures":[{"sig":"MEQCIA9HGYMsXHy+s+GMq5T6kh7IT9vIT/0XaebTsFLfUba/AiBxDx49I6cByZVzHco8izC6DOoCtlnnZln48jSiCm4eTQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":193656},"main":"./dist/index.mjs","type":"module","types":"./dist/index.d.mts","engines":{"node":">=20.19"},"exports":{".":{"types":"./dist/index.d.mts","import":"./dist/index.mjs","default":"./dist/index.mjs"},"./package.json":"./package.json"},"gitHead":"25066b4d2f2f01d90f47fb4f54a13101fff2c5cb","scripts":{"lint":"oxlint .","test":"vitest run","build":"tsdown","format":"oxfmt --check","prepare":"lefthook install || true","lint:fix":"oxlint --fix .","validate":"pnpm run --parallel '/^(typecheck|lint|format|test)$/'","typecheck":"tsc --noEmit","format:fix":"oxfmt .","test:watch":"vitest","prepublishOnly":"pnpm build"},"_npmUser":{"name":"caleb-vercel","email":"caleb.an@vercel.com"},"repository":{"url":"git+https://github.com/vercel-labs/guarded-fetch.git"},"_npmVersion":"11.9.0","description":"SSRF-safe HTTP(S) fetch utilities backed by undici: DNS-rebinding protection, redirect validation, header sanitization, and bounded body reads","directories":{},"sideEffects":false,"_nodeVersion":"24.14.0","dependencies":{"undici":"7.29.0","ipaddr.js":"2.2.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@10.32.1","devDependencies":{"oxfmt":"0.44.0","oxlint":"1.59.0","tsdown":"0.21.7","vitest":"4.1.4","lefthook":"2.1.5","typescript":"6.0.2","@types/node":"25.6.0"},"_npmOperationalInternal":{"tmp":"tmp/guarded-fetch_0.1.4_1788199682442_0.36292654745565534","host":"s3://npm-registry-packages-npm-production"}},"0.1.5":{"name":"guarded-fetch","version":"0.1.5","author":{"name":"Vercel"},"license":"Apache-2.0","_id":"guarded-fetch@0.1.5","maintainers":[{"name":"caleb-vercel","email":"caleb.an@vercel.com"}],"homepage":"https://github.com/vercel-labs/guarded-fetch#readme","bugs":{"url":"https://github.com/vercel-labs/guarded-fetch/issues"},"dist":{"shasum":"12358811194028a6d05109568f3933a17673cb72","tarball":"https://registry.npmjs.org/guarded-fetch/-/guarded-fetch-0.1.5.tgz","fileCount":6,"integrity":"sha512-Bg7rC1VaYk6OhxT3/a6Uyy1lGsNVgMTVEttOaA3P0J81MQsRug4irc6MJGMmZ/nuZ0TTVfwIWjUaI6w80xrEmg==","signatures":[{"sig":"MEUCIQCHrOB5QlHUValjzNzUs/vKuhVj5RhfK6Te5Pg+Ckhi5gIgPxKfuZvCj0S5uHxgC84hfSRgN6Y/w7jdSWuVC36jXm8=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIGMYN3gaFVFWiiE2kQf6/5dCGN01L2gyrCk0DASMSHCBAiEAuT+OOKPwzpBrC+2JBD5FYrVPRyJdxkb5FCA/pQN+Udg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":193656},"main":"./dist/index.mjs","type":"module","types":"./dist/index.d.mts","engines":{"node":">=20.19"},"exports":{".":{"types":"./dist/index.d.mts","import":"./dist/index.mjs","default":"./dist/index.mjs"},"./package.json":"./package.json"},"gitHead":"bacdd4336d2b4b2474da02fb0e0c6212578964be","scripts":{"lint":"oxlint .","test":"vitest run","build":"tsdown","format":"oxfmt --check","prepare":"lefthook install || true","lint:fix":"oxlint --fix .","validate":"pnpm run --parallel '/^(typecheck|lint|format|test)$/'","typecheck":"tsc --noEmit","format:fix":"oxfmt .","test:watch":"vitest","prepublishOnly":"pnpm build"},"_npmUser":{"name":"caleb-vercel","email":"caleb.an@vercel.com"},"repository":{"url":"git+https://github.com/vercel-labs/guarded-fetch.git"},"_npmVersion":"11.9.0","description":"SSRF-safe HTTP(S) fetch utilities backed by undici: DNS-rebinding protection, redirect validation, header sanitization, and bounded body reads","directories":{},"sideEffects":false,"_nodeVersion":"24.14.0","dependencies":{"undici":"7.30.0","ipaddr.js":"2.2.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@10.32.1","devDependencies":{"oxfmt":"0.44.0","oxlint":"1.59.0","tsdown":"0.21.7","vitest":"4.1.4","lefthook":"2.1.5","typescript":"6.0.2","@types/node":"25.6.0"},"_npmOperationalInternal":{"tmp":"tmp/guarded-fetch_0.1.5_1790961460411_0.5854522066657608","host":"s3://npm-registry-packages-npm-production"}},"0.1.6":{"name":"guarded-fetch","version":"0.1.6","author":{"name":"Vercel"},"license":"Apache-2.0","_id":"guarded-fetch@0.1.6","maintainers":[{"name":"caleb-vercel","email":"caleb.an@vercel.com"}],"homepage":"https://github.com/vercel-labs/guarded-fetch#readme","bugs":{"url":"https://github.com/vercel-labs/guarded-fetch/issues"},"dist":{"shasum":"a250280913220d47745f011db5e55b191a67ef1d","tarball":"https://registry.npmjs.org/guarded-fetch/-/guarded-fetch-0.1.6.tgz","fileCount":6,"integrity":"sha512-l7cf1Rk7WOrZ8pEq2RtgO2zctc5gFmk9YUfw5lmjIVFh19pMWOxux5gUPVAZnqU4Y36gUfWmjN3upqjoWWeXdg==","signatures":[{"sig":"MEYCIQDE++BoC5qx9z5FMHQdbiwk7uawTUe8fYzTRZZ77yFAhQIhAOxp7mS5f6dmays/Vby1vhmBbw1abKzbOminGqYXr+8d","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEUCIDlIJg8H/aNcTXVpIGIZSdCPiiDYXveWrvpj/SZ/Fil5AiEAhiAew82yD+I8WCpC+5ZD6UwY57NU3ztzAhUWy1BBCO0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/guarded-fetch@0.1.6","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":193656},"main":"./dist/index.mjs","type":"module","types":"./dist/index.d.mts","engines":{"node":">=20.19"},"exports":{".":{"types":"./dist/index.d.mts","import":"./dist/index.mjs","default":"./dist/index.mjs"},"./package.json":"./package.json"},"gitHead":"25e89f08d988c1be0eef8ee0e3dbe69d725e60d3","scripts":{"lint":"oxlint .","test":"vitest run","build":"tsdown","format":"oxfmt --check","prepare":"lefthook install || true","lint:fix":"oxlint --fix .","validate":"pnpm run --parallel '/^(typecheck|lint|format|test)$/'","typecheck":"tsc --noEmit","format:fix":"oxfmt .","test:watch":"vitest","prepublishOnly":"pnpm build"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"6ce09c55-6fd2-49a4-8027-a7443bf5b431"}},"repository":{"url":"git+https://github.com/vercel-labs/guarded-fetch.git"},"_npmVersion":"11.19.0","description":"SSRF-safe HTTP(S) fetch utilities backed by undici: DNS-rebinding protection, redirect validation, header sanitization, and bounded body reads","directories":{},"sideEffects":false,"_nodeVersion":"24.21.0","dependencies":{"undici":"7.30.0","ipaddr.js":"2.2.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@10.32.1","devDependencies":{"oxfmt":"0.44.0","oxlint":"1.59.0","tsdown":"0.21.7","vitest":"4.1.4","lefthook":"2.1.5","typescript":"6.0.2","@types/node":"25.6.0"},"_npmOperationalInternal":{"tmp":"tmp/guarded-fetch_0.1.6_1792102089914_0.15925433399489886","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"_id":"guarded-fetch@0.2.0","bugs":{"url":"https://github.com/vercel-labs/guarded-fetch/issues"},"dist":{"shasum":"d12a384c52403fd9366e135e3fff82f9534ad118","tarball":"https://registry.npmjs.org/guarded-fetch/-/guarded-fetch-0.2.0.tgz","fileCount":6,"integrity":"sha512-5TEji0gMKD9colS4Gs3zCGtabdc2T23AvDgejBIyY53F/pOz64mKLGSTHqnXjJo3wqTiX3X0gxzh6KyqLZwg9A==","signatures":[{"sig":"MEQCIChH8bcsuxbdYZhLgpmSpV7y3vgKcMfdv7ZwScQk+lwIAiBCRzbI5vubOOE4wQSwwcSoTf9MDKrH6WEQIyDIiqmmsw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCo6EHPnFdvz+HdDYgqqR3F8VjeqXeaKP59DolXLlrKYAIgbqU23JceIBx21Vbujp4hHf5Wudr3xZsQ0/p3aF6s1a0="}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/guarded-fetch@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":203407},"main":"./dist/index.mjs","name":"guarded-fetch","type":"module","types":"./dist/index.d.mts","author":{"name":"Vercel"},"engines":{"node":">=20.19"},"exports":{".":{"types":"./dist/index.d.mts","import":"./dist/index.mjs","default":"./dist/index.mjs"},"./package.json":"./package.json"},"gitHead":"f9f5cad8720f4d59cc15f876413e698e84bdb87e","license":"Apache-2.0","scripts":{"lint":"oxlint .","test":"vitest run","build":"tsdown","format":"oxfmt --check","prepare":"lefthook install || true","lint:fix":"oxlint --fix .","validate":"pnpm run --parallel '/^(typecheck|lint|format|test)$/'","typecheck":"tsc --noEmit","format:fix":"oxfmt .","test:watch":"vitest","prepublishOnly":"pnpm build"},"version":"0.2.0","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"6ce09c55-6fd2-49a4-8027-a7443bf5b431"}},"homepage":"https://github.com/vercel-labs/guarded-fetch#readme","repository":{"url":"git+https://github.com/vercel-labs/guarded-fetch.git"},"_npmVersion":"11.19.1","description":"SSRF-safe HTTP(S) fetch utilities backed by undici: DNS-rebinding protection, redirect validation, header sanitization, and bounded body reads","directories":{},"maintainers":[{"name":"vercel-release-bot","email":"infra+release@vercel.com"},{"name":"caleb-vercel","email":"caleb.an@vercel.com"}],"sideEffects":false,"_nodeVersion":"24.22.1","dependencies":{"undici":"7.30.1","ipaddr.js":"2.2.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"packageManager":"pnpm@10.32.1","devDependencies":{"oxfmt":"0.44.0","oxlint":"1.59.0","tsdown":"0.21.7","vitest":"4.1.4","lefthook":"2.1.5","typescript":"6.0.2","@types/node":"25.6.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/guarded-fetch_0.2.0_1793133086297_0.5241248859729101"}}},"time":{"created":"2026-08-13T00:20:45.793Z","modified":"2026-10-27T20:31:26.770Z","0.1.0":"2026-08-11T23:49:00.613Z","0.1.1":"2026-08-13T00:20:46.136Z","0.1.2":"2026-08-14T23:30:35.825Z","0.1.3":"2026-08-21T23:35:02.254Z","0.1.4":"2026-08-31T18:08:02.617Z","0.1.5":"2026-10-02T17:17:40.504Z","0.1.6":"2026-10-15T22:08:10.017Z","0.2.0":"2026-10-27T20:31:26.425Z"},"bugs":{"url":"https://github.com/vercel-labs/guarded-fetch/issues"},"author":{"name":"Vercel"},"license":"Apache-2.0","homepage":"https://github.com/vercel-labs/guarded-fetch#readme","repository":{"url":"git+https://github.com/vercel-labs/guarded-fetch.git"},"description":"SSRF-safe HTTP(S) fetch utilities backed by undici: DNS-rebinding protection, redirect validation, header sanitization, and bounded body reads","maintainers":[{"name":"vercel-release-bot","email":"infra+release@vercel.com"},{"name":"caleb-vercel","email":"caleb.an@vercel.com"}],"readme":"# guarded-fetch\n\nDrop-in `fetch` on the server for URLs you don't trust!\n\nServer Side Request Forgery is when user modified urls let an attacker point your server at\n`http://169.254.169.254` and read your cloud credentials (Common cases are urls like webhooks,\nlog drains, connector\ncallbacks, etc.). `guardedFetch` is a\ndrop-in replacement that blocks that whole class of attack (SSRF), plus DNS\nrebinding, redirect tricks, header smuggling, and unbounded responses.\n\nBuilt on [undici](https://github.com/nodejs/undici). Two runtime\ndependencies (`undici`, `ipaddr.js`). Node ≥ 20.19.\n\n## Getting started\n\n```bash\nnpm install guarded-fetch\n```\n\n```ts\nimport {\n  guardedFetch,\n  guardedFetchJson,\n  GuardedFetchError,\n} from 'guarded-fetch';\n\n// Just like fetch — returns a standard Response:\nconst response = await guardedFetch('https://example.com/api/data');\nconst data = await response.json();\n\n// Or fetch + parse JSON with a size limit in one call:\nconst config = await guardedFetchJson<{ issuer: string }>(\n  'https://example.com/.well-known/openid-configuration',\n);\n```\n\nWhen a request is blocked or fails, a `GuardedFetchError` is thrown with a\nstable `code` you can branch on:\n\n```ts\ntry {\n  await guardedFetch(userSuppliedUrl, { method: 'POST', body: payload });\n} catch (err) {\n  if (err instanceof GuardedFetchError && err.code === 'hostname_unsafe') {\n    // The URL resolved to a private/internal address — likely an SSRF probe.\n  }\n  throw err;\n}\n```\n\n## What's on by default\n\n`guarded-fetch` has a high security bar by default. `guardedFetch(url)` call with no additional options prevents:\n\n| Protection              | Default behavior                                                                                                                                                                 |\n| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Protocol allowlist      | Only `http:` and `https:` URLs are accepted.                                                                                                                                     |\n| SSRF DNS check          | The hostname is DNS-resolved before connecting; private, loopback, link-local, CGNAT, and cloud-metadata addresses are rejected. `localhost` and `*.local` are rejected by name. |\n| Connect-time IP pinning | The resolved IP is re-validated _inside_ the socket connect, so a DNS server can't swap in a private IP between the check and the connection (DNS rebinding).                    |\n| Header sanitization     | `Host`, `Cookie`, `X-Forwarded-*`, cloud-metadata headers, and other dangerous headers are silently stripped from your request.                                                  |\n| Safe redirects          | Redirects are followed manually (max 5 hops) and every hop re-runs all of the checks above.                                                                                      |\n| Credential stripping    | `Authorization` and `Cookie` are dropped when a redirect crosses origins, so tokens never leak to a different host.                                                              |\n| Timeout attacks         | The whole request chain (including redirects) is capped at 10 seconds.                                                                                                           |\n\nTwo things that always need configuration:\n\n- **Response size limits** apply only to `guardedFetchJson` / `guardedFetchText`\n  (10 MB default). Bare `guardedFetch` returns the `Response` unread, so if you\n  call `response.json()` yourself there's no size cap — prefer the wrappers\n  for untrusted endpoints.\n- **A host allowlist** — any publicly-resolving host is allowed unless you\n  pass `allowedHosts`. Entries are explicit matches only; use a leading `*.`\n  wildcard to allow one subdomain level (`*.example.com`), or `**.` for any\n  subdomain depth (`**.example.com`).\n\n## Recipes\n\n### User-configured webhook / log drain\n\nStrict timeout, opaque errors, capped redirects — for URLs where the\nattacker can both choose the destination and read your error messages:\n\n```ts\nimport { guardedFetch, isGuardedFetchError } from 'guarded-fetch';\n\ntry {\n  await guardedFetch(webhook.url, {\n    method: 'POST',\n    headers: webhook.headers, // sanitized automatically\n    body: JSON.stringify(event),\n    timeoutMs: 4_000,\n    maxRedirects: 3,\n    opaqueErrors: true,\n  });\n} catch (err) {\n  if (isGuardedFetchError(err)) {\n    // err.code is intact for internal logging;\n    // err.message is safe to show the user.\n    logger.info({ code: err.code, hostname: err.hostname }, 'delivery failed');\n  }\n  throw err;\n}\n```\n\n### Locked-down fetch to a known vendor\n\n```ts\nconst data = await guardedFetchJson(vendorUrl, {\n  httpsOnly: true,\n  allowedHosts: ['api.vendor.com'],\n  maxResponseBytes: 1 * 1024 * 1024,\n  throwOnHttpError: true,\n});\n```\n\n### Blocking Node2Shell\n\nNode2Shell (CVE-2026-111497) is a remote code execution bug in the brotli\ndecoder that Node bundles. Your outbound requests can be exploited by\na malicious server even if you don't explicitly pick brotli, since the response\ncould be brotli compressed and Node will accept it.\n\nWhile we recommend patching to a safe Node version, on an unpatched Node\nyou can mitigate this attack by blocking `br` encoding:\n\n```ts\nawait guardedFetch(url, { blockedEncodings: ['br'] });\n```\n\nThe request sends `Accept-Encoding: gzip, deflate`. A server that still\nresponds with `Content-Encoding: br` is aborted before any byte reaches the\ndecoder, and the call throws `response_encoding_not_allowed`.\n\n_Note that using custom fetcher will not work with this_\n\n### Size-bounded reads on an existing Response\n\n```ts\nimport { guardedFetch, readBodyAsJson } from 'guarded-fetch';\n\nconst response = await guardedFetch(url);\nconst data = await readBodyAsJson(response, {\n  maxResponseBytes: 512 * 1024,\n});\n```\n\n## Options reference\n\nAll options are optional, and standard `RequestInit` fields (`method`,\n`headers`, `body`, ...) pass through as usual — except `redirect` and\n`signal`, which are managed internally. `maxResponseBytes` and\n`throwOnHttpError` exist only on the `guardedFetchJson` / `guardedFetchText`\nwrappers.\n\n| Option                         | Type                 | Default                | What it does                                                                                                                                                                                                                                                                                                                                                                                                                                               |\n| ------------------------------ | -------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `timeoutMs`                    | `number`             | `10_000`               | Deadline in ms for the entire chain: connection, redirects, and (in the JSON/text wrappers) the body read.                                                                                                                                                                                                                                                                                                                                                 |\n| `followRedirects`              | `boolean`            | `true`                 | Follow redirects, re-validating every hop. When `false`, the 3xx response is returned as-is.                                                                                                                                                                                                                                                                                                                                                               |\n| `maxRedirects`                 | `number`             | `5`                    | Redirect hop cap. Exceeding it throws `too_many_redirects`.                                                                                                                                                                                                                                                                                                                                                                                                |\n| `httpsOnly`                    | `boolean`            | `false`                | Restrict to `https:` URLs (`http:` and `https:` are otherwise both allowed).                                                                                                                                                                                                                                                                                                                                                                               |\n| `allowedHosts`                 | `string[]`           | _(none)_               | Hostname allowlist — explicit exact match only (`api.example.com` matches only `api.example.com`). Leading wildcards: `*.example.com` allows one subdomain level (`api.example.com`); `**.example.com` allows any depth (`a.b.example.com` too). Neither matches the base domain itself. Not public-suffix-aware — scope wildcards to domains you own (`**.com` would allow every `.com` host). Omit to allow any public host. **Empty array = deny all.** |\n| `skipSsrfCheckForAllowedHosts` | `boolean`            | `false`                | Skip the DNS/SSRF check for hosts that matched `allowedHosts`. Only safe for hosts you fully control.                                                                                                                                                                                                                                                                                                                                                      |\n| `sanitizeHeaders`              | `boolean`            | `true`                 | Strip [dangerous request headers](#headers-that-are-stripped) before sending. Set `false` to send headers verbatim (dangerous with user input).                                                                                                                                                                                                                                                                                                            |\n| `allowedCookie`                | `string`             | _(none)_               | Explicitly send a `Cookie` header (which sanitization otherwise strips). Dropped on cross-origin redirects, like `Authorization`.                                                                                                                                                                                                                                                                                                                          |\n| `blockedEncodings`             | `string[]`           | `[]`                   | Response content codings to reject, e.g. `['br']` on Node without the fix for CVE-2026-111497. `Accept-Encoding` omits them (unless you set it) and a response that uses one throws `response_encoding_not_allowed` before any byte is decoded.                                                                                                                                                                                                            |\n| `opaqueErrors`                 | `boolean`            | `false`                | Replace every error message with one generic string so failures are indistinguishable to attackers. Error `code` is preserved.                                                                                                                                                                                                                                                                                                                             |\n| `onUrlBlocked`                 | `function`           | _(none)_               | Per-call block-event handler; overrides the module-level `setUrlBlockedHandler` for this call.                                                                                                                                                                                                                                                                                                                                                             |\n| `signal`                       | `AbortSignal`        | _(none)_               | External abort signal, combined with the internal timeout.                                                                                                                                                                                                                                                                                                                                                                                                 |\n| `fetch`                        | `fetch` impl         | `undici.fetch`         | Injectable fetch for tests. Must be undici-compatible.                                                                                                                                                                                                                                                                                                                                                                                                     |\n| `dispatcher`                   | `Dispatcher \\| null` | shared safe dispatcher | undici dispatcher for the request. `null` disables connect-time IP pinning — dangerous; only for when a trusted proxy already controls the connection target.                                                                                                                                                                                                                                                                                              |\n| `maxResponseBytes`             | `number`             | `10 * 1024 * 1024`     | _(wrappers only)_ Max body size; exceeding it throws `response_too_large`.                                                                                                                                                                                                                                                                                                                                                                                 |\n| `throwOnHttpError`             | `boolean`            | `false`                | _(wrappers only)_ Throw on non-2xx responses instead of returning the body.                                                                                                                                                                                                                                                                                                                                                                                |\n\nAlways on, not configurable:\n\n- **Credential stripping on cross-origin redirects** — `Authorization` and\n  `Cookie` never follow a redirect to a different origin.\n- **RFC 7231 redirect normalization** — 301/302/303 responses convert the\n  method to `GET` and drop the body (307/308 preserve both).\n\n## What's prevented\n\nEvery outbound attempt — the original URL and every redirect hop — is\nprotected against:\n\n| Attack                                                                                      | How it's stopped                                                            |\n| ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- |\n| SSRF to private / loopback / link-local IPs, incl. AWS/GCP metadata (`169.254.169.254`)     | Hostname is DNS-resolved and every record classified before connecting.     |\n| SSRF via non-HTTP schemes (`file:`, `ftp:`, `gopher:`, ...)                                 | Protocol allowlist: `http:`/`https:` only.                                  |\n| DNS rebinding (TTL=0 flip between check and connect)                                        | IP re-validated inside the socket connect via a pinned undici lookup.       |\n| Multi-record DNS races (one public + one private record)                                    | Rejected if _any_ resolved A/AAAA record is unsafe.                         |\n| Private IPv4 hidden in IPv6 literals (`::ffff:`, 6to4 `2002::/16`, NAT64 `64:ff9b::/96`)    | Embedded IPv4 is decoded and classified with the same rules as native IPv4. |\n| NAT64 translation to private IPv4 via the local-use prefix (`64:ff9b:1::/48`)               | Whole range rejected — it addresses a local translator, never a real host.  |\n| Redirect to an internal host after an initial safe response                                 | Manual redirect following; every hop re-runs all checks.                    |\n| Infinite / abusive redirect chains                                                          | Hop cap (`maxRedirects`, default 5).                                        |\n| Cloud-credential theft via metadata headers (`Metadata-Flavor`, `X-aws-ec2-metadata-token`) | Header stripped.                                                            |\n| Origin-IP spoofing (`X-Forwarded-*`, `Forwarded`, `Via`, `X-Real-IP`)                       | Header stripped.                                                            |\n| Virtual-host routing to internal backends via a supplied `Host` header                      | Header stripped.                                                            |\n| Session leakage via `Cookie` to a user-controlled destination                               | Header stripped (opt back in with `allowedCookie`).                         |\n| Auth-token leak on cross-origin redirect                                                    | `Authorization` and `Cookie` dropped when the redirect changes origin.      |\n| Memory exhaustion from huge responses; slow-loris bodies                                    | `maxResponseBytes` + whole-chain `timeoutMs` (use the JSON/text wrappers).  |\n| Internal-network mapping via distinguishable error messages                                 | `opaqueErrors: true` normalizes every failure message.                      |\n| Brotli decoder RCE on unpatched Node (CVE-2026-111497)                                      | `blockedEncodings: ['br']` aborts `br` responses before decoding.           |\n\n### Blocked network destinations\n\nRequests are refused when the hostname resolves to (or literally is) any of:\n\n- Loopback (`127.0.0.0/8`, `::1`) and unspecified (`0.0.0.0`, `::`)\n- Private ranges (`10/8`, `172.16/12`, `192.168/16`, IPv6 ULA `fc00::/7`)\n- Link-local — where cloud metadata lives (`169.254.0.0/16`, `fe80::/10`)\n- CGNAT (`100.64.0.0/10`) — note this covers Tailscale-style addresses\n- IPv6 forms that embed any of the above (IPv4-mapped, 6to4, NAT64\n  `64:ff9b::/96`), plus the NAT64 local-use prefix `64:ff9b:1::/48` in full\n- `localhost`, `*.localhost`, and `*.local` — blocked by name, regardless\n  of what DNS says\n\n### Headers that are stripped\n\n`BLOCKED_REQUEST_HEADERS` is exported if you need the canonical list. It\ncovers:\n\n- **Hop-by-hop / transport**: `Connection`, `Keep-Alive`, `TE`, `Trailer`,\n  `Transfer-Encoding`, `Upgrade`\n- **Host routing**: `Host`\n- **Proxy / origin spoofing**: `Forwarded`, `Proxy-Authorization`, `Via`,\n  `X-Forwarded-For`, `X-Forwarded-Host`, `X-Forwarded-Proto`, `X-Real-IP`\n- **Cloud metadata**: `Metadata`, `Metadata-Flavor`,\n  `X-Aws-Ec2-Metadata-Token`, `X-Metadata-Token`\n- **Session**: `Cookie` (see `allowedCookie`), `Set-Cookie`\n\n### DNS rebinding protection\n\n`guardedFetch` defends against DNS rebinding with a **two-layer** approach:\n\n1. **Preflight**: `assertUrlIsSafeToFetch` DNS-resolves the hostname and\n   rejects if any A/AAAA record is private/loopback/link-local.\n2. **Connect-time pin**: the default undici dispatcher installs a custom\n   `connect.lookup` (`createGuardedLookup`) that re-validates the IP **inside\n   the same resolution call the socket connects with**. A separate\n   check-then-fetch sequence leaves a window where an attacker's DNS server\n   returns a public IP for the check and `169.254.169.254` for the\n   connection; this closes it.\n\nLayer 2 is on by default via the shared dispatcher; injected `fetch`\nimplementations must be undici-compatible for it to stay active. Pass\n`dispatcher: null` to opt out (dangerous — only behind a trusted proxy that\ncontrols the connection target).\n\n### Using `createGuardedLookup` on its own\n\nA `dns.lookup` function only runs when the host actually needs resolving.\nNode's socket APIs skip DNS entirely for IP literals, so **no `lookup`\nimplementation is called for `net.connect({ host: '127.0.0.1' })`** — this one\nincluded. On its own, `createGuardedLookup` is a DNS-rebinding guard, not a\ncomplete SSRF guard.\n\n`guardedFetch` is unaffected: it runs `assertUrlIsSafeToFetch` first, which\nrejects unsafe IP literals before any socket work. `getSharedGuardedDispatcher`\nand `createGuardedDispatcher` are also complete on their own — they pair the\nlookup with a pre-connect IP-literal check.\n\nIf you are wiring sockets by hand, validate the target first:\n\n```ts\nimport net from 'node:net';\nimport { assertUrlIsSafeToFetch, createGuardedLookup } from 'guarded-fetch';\n\nawait assertUrlIsSafeToFetch(target); // rejects IP literals and unsafe DNS\nnet.connect({ host, port, lookup: createGuardedLookup() });\n```\n\n## API surface\n\n| Export                                       | Purpose                                                                                                                  |\n| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |\n| `guardedFetch(url, opts?)`                   | SSRF-safe `fetch` — returns a `Response`.                                                                                |\n| `guardedFetchJson<T>(url, opts?)`            | Fetch + bounded JSON parse.                                                                                              |\n| `guardedFetchText(url, opts?)`               | Fetch + bounded UTF-8 read.                                                                                              |\n| `assertUrlIsSafeToFetch(url, opts?)`         | Pre-flight validator — no HTTP request, but does resolve DNS.                                                            |\n| `sanitizeRequestHeaders(headers)`            | Strip SSRF/proxy/cookie headers → `Headers`.                                                                             |\n| `readBodyAsJson(res, opts?)`                 | Size-bounded JSON reader for an existing `Response`.                                                                     |\n| `readBodyAsText(res, opts?)`                 | Size-bounded text reader for an existing `Response`.                                                                     |\n| `GuardedFetchError`, `GuardedFetchErrorCode` | Structured error type for every failure mode.                                                                            |\n| `isGuardedFetchError(v)`                     | Type guard resilient to duplicate module copies.                                                                         |\n| `isPermanentGuardedFetchError(v)`            | True when the failure cannot succeed on retry.                                                                           |\n| `BLOCKED_REQUEST_HEADERS`                    | The canonical header blocklist.                                                                                          |\n| `createGuardedLookup(opts?)`                 | `dns.lookup`-compatible function that validates and pins resolved IPs inline. Rebinding guard only — see the note below. |\n| `isSafeIpAddress(ip)`                        | Returns `true` for public IPv4/IPv6 addresses.                                                                           |\n| `getSharedGuardedDispatcher()`               | Lazy process-wide undici `Agent` used by default. Connection-pooled.                                                     |\n| `createGuardedDispatcher(opts?)`             | Fresh undici `Agent` with IP pinning. Caller must `.close()`.                                                            |\n| `setUrlBlockedHandler(handler)`              | Register a process-wide block-event handler.                                                                             |\n| `URL_BLOCKED_LOG_MESSAGE`                    | Suggested constant log message for block events.                                                                         |\n\n## Error codes\n\nEvery failure throws `GuardedFetchError` with a stable string `code`\n(`err.code`, also available as the `GuardedFetchErrorCode` enum):\n\n| Code                            | Meaning                                                                             | Permanent? |\n| ------------------------------- | ----------------------------------------------------------------------------------- | ---------- |\n| `invalid_url`                   | URL string failed to parse.                                                         | Yes        |\n| `protocol_not_allowed`          | Scheme outside the allowlist (`http:`/`https:`, or `https:` only with `httpsOnly`). | Yes        |\n| `host_not_allowed`              | Host not in `allowedHosts`.                                                         | Yes        |\n| `hostname_unsafe`               | Resolved to a private/loopback/link-local address — or DNS failed (fail-closed).    | Usually    |\n| `timeout`                       | Exceeded `timeoutMs`, or the external `signal` aborted.                             | No         |\n| `too_many_redirects`            | Redirect chain exceeded `maxRedirects`.                                             | No         |\n| `redirect_invalid`              | Redirect `Location` missing or malformed.                                           | No         |\n| `redirect_to_unsafe_host`       | A redirect target failed the SSRF/protocol/host checks.                             | No         |\n| `response_too_large`            | Body exceeded `maxResponseBytes`.                                                   | No         |\n| `response_encoding_not_allowed` | Response used a `Content-Encoding` listed in `blockedEncodings`.                    | Yes        |\n| `network_error`                 | DNS / connection / TLS / parse failure, or non-2xx with `throwOnHttpError`.         | No         |\n\nUse `isPermanentGuardedFetchError(err)` rather than hardcoding the\nretryability column.\n\n## Observability\n\nBlocked URLs dispatch a `{ reason, domain, subReason? }` event to an\noptional handler — nothing is collected until you register one, and handler\nerrors never reach your request path:\n\n```ts\nimport { setUrlBlockedHandler, URL_BLOCKED_LOG_MESSAGE } from 'guarded-fetch';\n\n// Once at startup — works with any logger or metrics stack:\nsetUrlBlockedHandler(({ reason, domain, subReason }) => {\n  logger.info({ reason, domain, subReason }, URL_BLOCKED_LOG_MESSAGE);\n});\n```\n\nFor per-request context, pass `onUrlBlocked` on the individual call — but\nconnect-time (DNS-rebinding) blocks only reach the module-level handler, so\nregister both for full coverage.\n\n## FAQ\n\n### Why use this instead of plain `fetch` + an IP check?\n\nA hand-rolled \"resolve the hostname, check the IP, then fetch\" sequence has\na race condition (DNS rebinding) and misses a long tail of bypasses:\nredirects to internal hosts, IPv6 literals that embed private IPv4\naddresses, multi-record DNS responses where only one record is private, and\nheader smuggling. See\n[DNS rebinding protection](#dns-rebinding-protection) for how the race is\nclosed.\n\n### Why is `localhost` blocked? I need it for local development.\n\nBlocking loopback is the point — in production, `http://127.0.0.1:8080`\nfrom user input is an attack. For dev and tests, inject a fetch\nimplementation instead of weakening the production configuration:\n\n```ts\nawait guardedFetch(url, { fetch: mockFetch }); // tests\n```\n\n### Why did my request go through but the destination returned 401/404?\n\nHeader sanitization is silent: blocked headers (see\n[the full list](#headers-that-are-stripped)) are dropped, not rejected, so\na destination that relied on one fails on its side. For cookies there's a\nnarrow opt-in: `allowedCookie`.\n\n### Why does `allowedHosts: []` block everything?\n\nAn empty array means \"allow these zero hosts\" — deny-all — not \"no\nallowlist configured\". If you build the list dynamically, guard against it\nending up empty; to disable allowlisting, omit the option entirely.\n\n### The URL is valid, the host is public — why `hostname_unsafe`?\n\nDNS is fail-closed: resolver errors and empty results block, and one\nprivate A/AAAA record rejects the whole host, even alongside public ones\n(split-horizon DNS). Use `isPermanentGuardedFetchError(err)` to decide\nwhether to retry.\n\n### Can I validate a URL without making a request?\n\nYes — use `assertUrlIsSafeToFetch` at write time, e.g. when a user saves a\nwebhook URL:\n\n```ts\nimport { assertUrlIsSafeToFetch } from 'guarded-fetch';\n\nawait assertUrlIsSafeToFetch(body.webhookUrl, { httpsOnly: true });\n// Throws GuardedFetchError if unsafe; safe to store otherwise.\n```\n\nNote the DNS answer can change between write time and request time — the\nrequest-time checks still run on every fetch.\n\n### What about error messages leaking information to attackers?\n\nIf the URL supplier can read your error messages (e.g. a webhook delivery\nlog), distinguishable failures let them map your internal network.\n`opaqueErrors: true` collapses every failure into one generic message while\npreserving `err.code` for your internal logs.\n\n### What are the limitations?\n\n- **Injected `fetch` implementations must be undici-compatible**, or\n  connect-time IP pinning silently doesn't apply to them.\n- **`node:http`-level `localAddress`/`family` hints** set outside this\n  package can reintroduce private-network reachability. Don't combine them\n  with user-controlled URLs.\n- **DNS is fail-closed** — resolver errors block as `hostname_unsafe`\n  rather than surfacing a retryable network error.\n- **One unsafe DNS record blocks the whole host**, even alongside public\n  records (split-horizon DNS, stray internal records).\n- **CGNAT and `*.local` are always unsafe**, which can reject legitimate\n  Tailscale-style or mDNS endpoints.\n- **Header stripping is silent** — see\n  [the FAQ entry](#why-did-my-request-go-through-but-the-destination-returned-401404).\n- **Credential stripping on cross-origin redirects is not configurable** —\n  auth-preserving redirect patterns (e.g. an API 302 to a presigned URL\n  that also expects the auth header) will lose the header.\n- **`allowedHosts: []` denies every host** — see\n  [the FAQ entry](#why-does-allowedhosts--block-everything).\n- **Redirect chains through link shorteners** can exceed the default 5-hop\n  cap; raise `maxRedirects` if you expect them.\n\n## License\n\nApache-2.0\n","readmeFilename":"README.md"}