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

@portable/ip

v0.1.0

Published

Parse, format, compare, and classify IPv4 and IPv6 addresses and CIDR blocks, in any JavaScript runtime.

Readme

@portable/ip

Parses, formats, compares, and classifies IPv4 and IPv6 addresses and CIDR blocks. Runs in browsers, Cloudflare Workers, Node 24+, Bun, and Deno.

Replaces ip, ipaddr.js, is-ip, ip-cidr, cidr-tools, and ip-range-check for parsing, validation, CIDR matching, and classification.

npm install @portable/ip
import * as Ip from '@portable/ip';

const address = Ip.unmap(request.headers.get('cf-connecting-ip')!);
if (Ip.range(address) !== 'unicast') {
  throw new Error('Refusing to fetch an internal address.');
}

Ip.format('2001:0DB8:0:0:0:0:0:1'); // '2001:db8::1'
Ip.contains('10.0.0.0/8', '10.1.2.3'); // true
Ip.reverseName('192.0.2.1'); // '1.2.0.192.in-addr.arpa'

API

Every function that takes an address accepts an Address or a string, which it parses with parse.

type Address =
  | { version: 4; bytes: Uint8Array }
  | { version: 6; bytes: Uint8Array; zone?: string | undefined };
type Cidr = { address: Address; prefix: number };

bytes is the address in network order: 4 bytes for IPv4, 16 for IPv6. zone is the part after % in fe80::1%eth0.

| Function | Returns | | --- | --- | | parse(text) | An Address. Throws InvalidInputError for anything else. | | canParse(text, { version }) | Whether parse accepts text, optionally only for IPv4 or IPv6. Returns false for a non-string. | | format(address) | Text: dotted decimal for IPv4, RFC 5952 for IPv6, with %zone if present. | | fromBytes(bytes) | An Address built from a copy of 4 or 16 bytes. Throws InvalidInputError for any other length. | | compare(a, b) | A negative number, zero, or a positive number: IPv4 before IPv6, then by bytes, then by zone. Pass it to Array.prototype.sort. | | unmap(address) | The IPv4 address inside an IPv4-mapped address (::ffff:192.0.2.1), and any other address unchanged. | | toIpv6(address) | The IPv4-mapped form of an IPv4 address, and an IPv6 address unchanged. | | reverseName(address) | The PTR lookup name under in-addr.arpa or ip6.arpa. | | parseCidr(text) | A Cidr with the host bits of its address cleared. Throws InvalidInputError for malformed text, a zone, or a prefix out of range. | | formatCidr(cidr) | address/prefix text. | | contains(cidr, address) | Whether the block contains the address. Throws RangeError for a Cidr object whose prefix isn't an integer from 0 to 32 or 128. | | range(address) | The name of the special-purpose block the address is in (below), or 'unicast'. |

Strict parsing

parse accepts IPv4 only as four decimal numbers from 0 to 255 without leading zeros. inet_aton reads 0177.0.0.1 as octal (127.0.0.1), 0x7f.1 as hex, 2130706433 as an integer and 127.1 as two parts, while other parsers read 0177 as decimal 177 or reject it. CVE-2023-42282 and CVE-2024-29415 let 127.1 and 0x7f.1 pass ip's public-address check because the check read them one way and the network stack read them another. parse throws InvalidInputError for those spellings, so treat a parse error as a refusal.

The WHATWG URL parser converts those spellings to dotted decimal, so new URL('http://0x7f.1/').hostname is '127.0.0.1'. Parse the hostname of a URL, after removing the brackets around an IPv6 host, rather than the raw text.

IPv6 addresses follow RFC 4291: one to four hex digits per group, at most one ::, which replaces at least one group, an optional dotted IPv4 address in the last 32 bits (also without leading zeros), and an optional zone of letters, digits, ., _, ~, and -.

Ranges

range uses the block names of the IANA IPv4 and IPv6 Special-Purpose Address Registries. Check unmap(address) rather than address when the address may be IPv4-mapped, because range('::ffff:127.0.0.1') returns 'ipv4Mapped', not 'loopback'. To refuse internal destinations, allow only 'unicast'.

| Range | IPv4 | IPv6 | | --- | --- | --- | | thisNetwork | 0.0.0.0/8 | | | unspecified | | ::/128 | | loopback | 127.0.0.0/8 | ::1/128 | | private | 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 | | | uniqueLocal | | fc00::/7 | | sharedAddressSpace | 100.64.0.0/10 | | | linkLocal | 169.254.0.0/16 | fe80::/10 | | deprecatedSiteLocal | | fec0::/10 | | ietfProtocolAssignments | 192.0.0.0/24 | 2001::/23 | | documentation | 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24 | 2001:db8::/32, 3fff::/20 | | benchmarking | 198.18.0.0/15 | 2001:2::/48 | | as112 | 192.31.196.0/24, 192.175.48.0/24 | 2001:4:112::/48, 2620:4f:8000::/48 | | amt | 192.52.193.0/24 | 2001:3::/32 | | 6to4RelayAnycast | 192.88.99.0/24 | | | 6to4 | | 2002::/16 | | teredo | | 2001::/32 | | ipv4Mapped | | ::ffff:0:0/96 | | ipv4Translated | | ::ffff:0:0:0/96 | | ipv4Ipv6Translation | | 64:ff9b::/96, 64:ff9b:1::/48 | | discard | | 100::/64 | | dummy | | 100:0:0:1::/64 | | deprecatedOrchid | | 2001:10::/28 | | orchidV2 | | 2001:20::/28 | | droneRemoteIdProtocolEntityTags | | 2001:30::/28 | | segmentRouting | | 5f00::/16 | | multicast | 224.0.0.0/4 | ff00::/8 | | broadcast | 255.255.255.255/32 | | | reserved | 240.0.0.0/4 | everything outside 2000::/3 not named above | | unicast | every other address | every other address in 2000::/3 |

When blocks overlap, range returns the most specific one, so 2001:2::1 is benchmarking, not ietfProtocolAssignments.

IPv4-mapped addresses

A dual-stack socket reports an IPv4 client as ::ffff:192.0.2.1. contains treats the two forms as one address, as Node's BlockList does, so Ip.contains('127.0.0.0/8', '::ffff:7f00:1') is true. compare, range, and reverseName treat them as different addresses, so call unmap first. format writes mapped addresses in mixed notation, ::ffff:192.0.2.1, as RFC 5952 section 5 recommends.

::192.0.2.1 is an IPv4-compatible address, deprecated by RFC 4291, and is not treated as mapped. Its range is 'reserved'.

Types

| Type | Description | | --- | --- | | Ip.Address | { version, bytes, zone? }: an IPv4 or IPv6 address with its bytes in network order. | | Ip.Cidr | { address, prefix }: a network address and its prefix length. | | Ip.Range | The name of the special-purpose block an address belongs to, from the IANA registries. |

Why use this over ip or ipaddr.js

ip's isPublic and isPrivate accepted octal, hex, and short IPv4 spellings and misread them (CVE-2023-42282 and CVE-2024-29415). ip is unmaintained and needs Buffer and os. ipaddr.js parses those same spellings by default and its isValid accepts them.

  • Parsing eight mixed addresses took about 1.3 µs, 12 times faster than ipaddr.js and 2.3 times faster than Node's net.isIP, which only validates.
  • Parsing an address and checking it against a CIDR block was 11 times faster than ipaddr.js and 1.5 times faster than Node's BlockList.check.
  • contains returns false for a block of the other IP version. ipaddr.js's match throws.
  • range names come from the IANA registries. ipaddr.js reports 100:0:0:1::/64 and every IPv6 address outside 2000::/3 that it doesn't name, such as 4000::1, as unicast, and ip's isPrivate misses 198.18.0.0/15.
  • The IPv4 part of an IPv6 address can't have leading zeros. ipaddr.js reads ::ffff:0177.0.0.1 as 177.0.0.1 and 0177.0.0.1 as 127.0.0.1.
  • ip and ipaddr.js do subnet arithmetic, such as broadcast and last addresses, masks from prefix lengths, and merging or excluding blocks. This package doesn't.
  • ipaddr.js keeps a CIDR block's address as written, and parseCidr clears the host bits, so 192.168.1.134/26 formats as 192.168.1.128/26.
  • ipaddr.js accepts a zone in a CIDR block, such as fe80::1%eth0/64, and parseCidr throws for it.
  • ipaddr.js's toIPv4Address throws for an address that isn't IPv4-mapped, and unmap returns it unchanged.
  • ipaddr.js and ip write ::ffff:c000:201 where format writes ::ffff:192.0.2.1, and Node's inet_ntop writes ::192.0.2.1 where format writes ::c000:201.
  • Node's isIP accepts a zone that contains :, and parse rejects it.

Operations per second from pnpm bench, higher is better, in Node 24.22.0, on Intel(R) Xeon(R) Processor @ 2.10GHz (4 cores). Each benchmark lists only the libraries that can do the same work, fastest first. The relative speed compares each library with @portable/ip, and the bars are scaled to the fastest library.

| Library | Version | Minified and gzipped | Runtime dependencies | Node built-ins | TypeScript types | | --- | --- | ---: | ---: | --- | --- | | @portable/ip | – | 2.4 kB | 2 | none | included | | ipaddr.js | 2.5.0 | 4.1 kB | 0 | none | included | | ip | 2.0.1 | 2.6 kB | 0 | buffer, os | @types/ip | | node:net | – | built in | – | Node only | – |

Sizes cover the imports in the usage example, or the equivalent imports of each library, bundled for the browser by scripts/bundle-size.ts. Runtime dependencies count every package installed with the published package. A library that uses Node built-ins needs polyfills in a browser and the nodejs_compat flag in Cloudflare Workers.