npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

fetch-untrusted

v1.0.0

Published

fetch() for URLs you did not write. Resolves, blocks private IPs, pins the connection, re-checks redirects.

Readme

fetch-untrusted

fetch() for URLs you didn't write.

Node will request whatever you hand it. If that string came from a user, a webhook, or a model, that includes http://169.254.169.254/ and http://127.0.0.1:6379/.

This resolves the host once, drops private and IANA special-use addresses, then connects to that IP. Host and SNI stay on the original name, so a later DNS change cannot retarget the socket. Redirects go through the same checks.

import { fetchUntrusted, UnsafeUrlError } from 'fetch-untrusted'

const res = await fetchUntrusted(url)
const body = await res.text()

Returns a normal Response. Node 18+, no dependencies. Copy src/ if you'd rather not install it.

npm i fetch-untrusted

Options

method, headers, body, signal, and redirect work like fetch. redirect defaults to follow (error / manual also work). Host is always taken from the URL.

| option | default | | | --- | --- | --- | | timeout | 10000 | ms, covers DNS, connect, redirects, and the body | | maxBytes | 5000000 | response body cap | | maxRedirects | 5 | | | allowHosts | | hostname must match if set. *.example.com matches subdomains, not the apex | | allowIps | | extra IPs, even if private. ::1 and 0:0:0:0:0:0:0:1 are the same address | | allowPrivate | false | skip the IP and name checks. lookup still has to return IPs | | lookup | dns.lookup | (hostname) => [{ address, family? }] or a string IP |

body can be a string, Buffer, Uint8Array, Node stream, or web ReadableStream. FormData / Blob / URLSearchParams are not accepted.

301/302 after POST, and 303 from anything but GET/HEAD, continue as GET with no body. 307/308 keep the method and body. A stream body can't be replayed on 307/308 (ERR_REDIRECT).

checkUrl

If you already have a client and just need the pin:

import { checkUrl } from 'fetch-untrusted'

const { url, address } = await checkUrl(input)
// connect to `address`, send Host: url.host

isBlockedIp and isBlockedHostname are the same lists without a DNS lookup.

What it blocks

Private, loopback, link-local, CGNAT, multicast, reserved. 192.0.0.0/24, TEST-NET, benchmarking, old 6to4 anycast. IPv6 unique-local, discard (100::/64), 100:0:0:1::/64, documentation (2001:db8::/32, 3fff::/20), SRv6 (5f00::/16).

Mapped and translated forms (::ffff:x, ::ffff:0:x, NAT64, 6to4) are judged by the embedded IPv4. Odd literals (2130706433, 0x7f000001, 127.1) are normalized by the URL parser first.

These names are rejected before DNS: localhost, *.localhost, *.local, *.internal, *.intranet, *.private, *.corp, *.home, *.lan, *.invalid, metadata, metadata.google.internal, *.svc.cluster.local.

If DNS returns both a public address and 127.0.0.1, the public one is used. A lookup function has to return IPs. Hostnames and other junk are ignored so they can't undo the pin.

Special-use ranges are blocked as published. A few addresses inside those prefixes are globally reachable (some 192.0.0.0/24 assignments, for example). Those get refused too.

allowPrivate is for tests.

Errors

UnsafeUrlError:

| code | | | --- | --- | | ERR_INVALID_URL | not a URL, no host, or port 0 | | ERR_BAD_PROTOCOL | not http: / https: | | ERR_CREDENTIALS | userinfo in the URL | | ERR_BLOCKED_HOST | name or allowHosts | | ERR_BLOCKED_IP | no public (or allowed) IP | | ERR_REDIRECT | redirect: 'error', bad Location, or stream body on 307/308 | | ERR_REDIRECT_LIMIT | more than maxRedirects | | ERR_RESPONSE_TOO_LARGE | body exceeded maxBytes | | ERR_TIMEOUT | timeout elapsed |

Socket errors, TLS failures, and a caller signal abort are thrown as-is.

try {
  await fetchUntrusted(url)
} catch (err) {
  if (err instanceof UnsafeUrlError) console.error(err.code, err.message)
  else throw err
}

This is not a fetch polyfill. No cookies, decompression, retries, or caching. It also won't save you if a public host itself redirects somewhere you can't see from here. It just won't be your process opening that socket.

License

MIT