Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: beaugunderson/ip-address
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v10.4.0
Choose a base ref
...
head repository: beaugunderson/ip-address
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v10.7.3
Choose a head ref
  • 20 commits
  • 21 files changed
  • 1 contributor

Commits on Aug 10, 2026

  1. Honor the fromURL graceful-failure contract for non-IPv6 hosts (#218)

    Address6.fromURL() is documented and typed to return
    `{ error, address: null, port: null }` when a URL can't be parsed, but
    that contract was only implemented on the regex-miss path. The URL host
    character class `[0-9a-f:.]` is a superset of valid IPv6, so any host
    made only of hex digits, colons and dots matched the regex and was
    handed straight to `new Address6(host)`, whose AddressError propagated
    out uncaught.
    
    IPv4 literals are the visible case — `http://127.0.0.1/`,
    `http://169.254.169.254/`, `http://0.0.0.0/` all threw — but so did
    `[:::]`, `[1:2:3:4:5:6:7:8:9]` and `[::ffff:999.1.1.1]`. A plain
    hostname like `example.com` avoided the throw only because letters
    outside a-f break the regex match first.
    
    Catch the constructor and return the same error object the regex-miss
    path returns, so the function never throws for any input.
    
    Reported by @zikk090 via GHSA-mxvh-v779-f36j. Declining that as a
    security advisory: an uncaught synchronous throw from a parser on the
    caller's own stack is a correctness bug, not a denial of service.
    beaugunderson authored Aug 10, 2026
    Configuration menu
    Copy the full SHA
    d478737 View commit details
    Browse the repository at this point in the history
  2. Correct the documentation where it disagreed with the library (#219)

    An audit of every documented claim against the built library. All 40
    worked examples (18 in the README block, 22 JSDoc @example) already
    passed; the inaccuracies were in prose and in generated signatures.
    
    Generated signatures (scripts/build-readme.ts):
    
    - Six signatures declared required parameters that carry defaults, two
      of them contradicting their own prose in the same bullet ("The default
      prefix is the well-known prefix 64:ff9b::/96" beside a signature
      saying you must pass it). TypeDoc sets isOptional inconsistently for
      defaulted parameters, so treat a default as optional too.
    - TypeScript `this` annotations rendered as arguments, so
      `isInSubnet(address)` read as taking two. Filter them out, and route
      function-valued properties through the same parameter rules.
    
    Prose:
    
    - Address4.reverseForm() was documented as returning ip6.arpa form. It
      returns in-addr.arpa, as the @PARAM two lines below already said.
    - 6to4 embeds a 32-bit IPv4 address in bits 16-47, not "the second 16
      bits".
    - "Parses all standard IPv4 and IPv6 notations" claimed more than the
      library does: the inet_aton forms are rejected by design, which
      SECURITY.md already says.
    - group() and groupForV6() return HTML and said nothing about it. These
      are the surfaces GHSA-v2v4-37r5-5v8g was about.
    - AddressError.parseMessage had no description at all, though
      GHSA-v2v4-37r5-5v8g describes rendering it as HTML as "its documented
      purpose".
    - Address4's byte-array methods documented neither their throw
      conditions nor that they reject the signed bytes Address6 folds. The
      11.0.0 tripwire in test/common-test.ts owns settling that split; this
      only writes down where it stands.
    - isHostInSubnet pointed at {@link common.isHostInSubnet}, a module the
      reference doesn't cover. Inlined instead.
    - Weekly download figures were stale (~66M against 85.7M actual).
    
    Badges and source links now use main, which is the default branch, so
    they no longer lean on GitHub's renamed-branch redirect.
    
    test/readme-test.ts guards the generator fix: no signature may expose a
    `this` parameter, defaulted parameters must render optional, and the
    HTML-returning methods must say so. All three fail against the previous
    README.
    beaugunderson authored Aug 10, 2026
    Configuration menu
    Copy the full SHA
    9fd1110 View commit details
    Browse the repository at this point in the history
  3. 10.5.0

    beaugunderson committed Aug 10, 2026
    Configuration menu
    Copy the full SHA
    ef98e0a View commit details
    Browse the repository at this point in the history

Commits on Aug 29, 2026

  1. Merge commit from fork

    isPrivate() covered ULAs and IPv4-mapped / NAT64 well-known addresses
    that embed an RFC 1918 address, but nothing in the RFC 8215 local-use
    range 64:ff9b:1::/48, so 64:ff9b:1:7f00:0:100:: (127.0.0.1 under a /48
    NAT64 prefix) read as non-internal to every classifier.
    
    The range cannot be handled by decoding an embedded IPv4 address the way
    the well-known prefix is: an operator picks a prefix of any RFC 6052
    length inside the /48, so the same bits decode differently under a /48
    and a /96 deployment. The whole range is not globally reachable per the
    IANA special-purpose registry (Python's ipaddress agrees, is_private is
    True), so isPrivate() reports it private as a whole.
    beaugunderson authored Aug 29, 2026
    Configuration menu
    Copy the full SHA
    ab3dc88 View commit details
    Browse the repository at this point in the history
  2. Merge commit from fork

    isLinkLocal() compared the first 64 bits against fe80:0:0:0, so it only
    recognized fe80::/64 and returned false for fe81::1, febf::1,
    fe80:0:0:1::1 and the rest of the /10 that RFC 4291 §2.4 assigns to
    link-local unicast. getType() and getScope() already matched fe80::/10
    via the TYPES table, so the library disagreed with itself on the same
    address. Use the same subnet test the rest of the library uses.
    
    Also add 2001::/32 to the TYPES table so getType() reports 'Teredo' for
    the addresses isTeredo() already returns true for.
    beaugunderson authored Aug 29, 2026
    Configuration menu
    Copy the full SHA
    d03e960 View commit details
    Browse the repository at this point in the history
  3. 10.5.1

    beaugunderson committed Aug 29, 2026
    Configuration menu
    Copy the full SHA
    38b02d7 View commit details
    Browse the repository at this point in the history
  4. Add isGlobal() and pin the classifiers to the IANA special-purpose re…

    …gistries (#224)
    
    * Add isGlobal() and pin the classifiers to the IANA special-purpose registries
    
    A guard written as an OR of named classifiers covers only the ranges it
    names, and the IANA registries hold more ranges than there are names. A
    differential sweep of every registry block boundary against Python's
    ipaddress found eight ranges no classifier caught: 0.0.0.0/8, the IETF
    protocol assignments in 192.0.0.0/24 and 2001::/23, the three IPv4
    documentation blocks, 198.18.0.0/15, 240.0.0.0/4, 100::/64 and
    100:0:0:1::/64, and 3fff::/20.
    
    isGlobal() on both classes answers from a table that mirrors the
    registry row for row, taking the most specific entry's Globally
    Reachable column, treating multicast as not global, and answering for
    the embedded IPv4 address of mapped and NAT64 well-known forms. SECURITY
    and the README point guards at it.
    
    For parity with the IPv6 side, Address4 gains isDocumentation(),
    isBenchmarking() and isReserved(); Address6.isDocumentation() covers
    3fff::/20 and Address6 gains isBenchmarking(); getType() learns
    Benchmarking, Discard-only and the second Documentation block.
    
    test/data/iana-corpus.json is generated by scripts/gen-iana-corpus.py
    from the registry CSVs checked in beside it: every block's first, last,
    neighboring and middle addresses plus their mapped and NAT64 forms, each
    carrying the registry's answer and Python's. The suite asserts isGlobal()
    matches the registry on all 592 and the named classifiers match Python,
    with the four rows where Python 3.14 and the registry disagree listed as
    tripwires.
    
    * Gate Address6.isGlobal() on the 2000::/3 global unicast allocation
    
    The special-purpose registry lists exceptions, but the IANA IPv6 Address
    Space Registry says only 2000::/3 is allocated for global unicast at all,
    so the deprecated site-local fec0::/10, the deprecated IPv4-compatible
    ::/96, and unallocated space such as 4000::/3 have nowhere to route.
    isGlobal() answered true for all of them; a survey of recent SSRF
    advisories found fec0::/10 named as a deny-list gap (GHSA-w98g-5w9p-p3rc).
    
    The address-space registry CSV joins the fixtures and its block
    boundaries join the corpus (730 probes). getType() names the two
    deprecated ranges.
    beaugunderson authored Aug 29, 2026
    Configuration menu
    Copy the full SHA
    fb12583 View commit details
    Browse the repository at this point in the history
  5. 10.6.0

    beaugunderson committed Aug 29, 2026
    Configuration menu
    Copy the full SHA
    42c1f8b View commit details
    Browse the repository at this point in the history
  6. Add offset() and nextNetwork(), accept prefix-length ip6.arpa names, …

    …correct the IPv6 end-address docs (#225)
    
    offset(n) returns the address n after (or before) this one with the
    same subnet mask; nextNetwork() returns the network after endAddress().
    Both take a number or bigint, reject non-integers, and throw when the
    result leaves the address space.
    
    Address6.fromArpa() accepts a name of 1 to 32 nibbles and returns the
    network it delegates with a mask of four bits per nibble, so
    fromArpa(x.reverseForm()) round-trips for any prefix; a full 32-nibble
    name still yields a /128. The root dot is optional.
    
    endAddress() and endAddressExclusive() on Address6 no longer describe a
    broadcast address: IPv6 has none, the last address is assignable, and
    the exclusive form drops exactly one address rather than the RFC 2526
    reserved subnet-anycast block.
    beaugunderson authored Aug 29, 2026
    Configuration menu
    Copy the full SHA
    87fae23 View commit details
    Browse the repository at this point in the history
  7. 10.7.0

    beaugunderson committed Aug 29, 2026
    Configuration menu
    Copy the full SHA
    2b7cab5 View commit details
    Browse the repository at this point in the history
  8. Bump js-yaml and brace-expansion in the lockfile (#226)

    Dev-only transitive dependencies flagged by Dependabot (#131) and npm
    audit: js-yaml 4.3.0 to 4.3.2 (quadratic CPU in !!omap resolution) and
    brace-expansion 1.1.16 / 2.1.2 / 5.0.8 to 1.1.18 / 2.1.4 / 5.0.9. Every
    dependent already accepted the patched ranges, so only the lockfile
    changes; every resolved URL stays on registry.npmjs.org with an
    integrity hash. npm audit reports 0 vulnerabilities.
    beaugunderson authored Aug 29, 2026
    Configuration menu
    Copy the full SHA
    4c2184a View commit details
    Browse the repository at this point in the history

Commits on Sep 15, 2026

  1. Report an address of the other family as not contained

    isHostInSubnet() compared masked binary strings with no family check.
    Address4 pads to 32 bits and Address6 to 128, so whenever the leading
    bits agreed the strings were equal: a00::1 read as inside 10.0.0.0/8
    and 32.0.0.1 as inside 2000::/3. isInSubnet() delegates to it and
    inherited the answer.
    
    An address of one family is never inside a network of the other, so
    both now return false when the widths differ. Callers that mean to
    compare across families convert first with Address6.fromAddress4(),
    to4(), or toAddress4Nat64().
    beaugunderson committed Sep 15, 2026
    Configuration menu
    Copy the full SHA
    1343629 View commit details
    Browse the repository at this point in the history
  2. Reject an address longer than the family allows before parsing it

    The bad-character diagnostic wraps every offending character in a
    <span class="parse-error">, so an N-byte string of punctuation cost
    about 106N bytes and a synchronous String.replace over all of it: 8 MiB
    of '!' took 529 ms and 895 MB inside Address6.isValid(), which built the
    diagnostic only to discard it. At 16 MiB the replace exceeds V8's
    maximum string length and throws a RangeError in place of the
    AddressError callers catch; at 32 MiB V8 aborts the process outright
    with "Fatal JavaScript invalid size error" (node 24.19.0).
    
    An IPv6 address minus its suffix and zone is at most 45 characters (six
    four-digit groups, six colons, a dotted quad), and CPython's ipaddress
    module draws the same line, so anything longer is rejected before
    parse() runs. Address4 gets the same treatment at 15 characters.
    beaugunderson committed Sep 15, 2026
    Configuration menu
    Copy the full SHA
    469ead1 View commit details
    Browse the repository at this point in the history
  3. Merge commit from fork

    isHostInSubnet() compared masked binary strings with no family check.
    Address4 pads to 32 bits and Address6 to 128, so whenever the leading
    bits agreed the strings were equal: a00::1 read as inside 10.0.0.0/8
    and 32.0.0.1 as inside 2000::/3. isInSubnet() delegates to it and
    inherited the answer.
    
    An address of one family is never inside a network of the other, so
    both now return false when the widths differ. Callers that mean to
    compare across families convert first with Address6.fromAddress4(),
    to4(), or toAddress4Nat64().
    beaugunderson committed Sep 15, 2026
    Configuration menu
    Copy the full SHA
    13b6155 View commit details
    Browse the repository at this point in the history
  4. Merge commit from fork

    The bad-character diagnostic wraps every offending character in a
    <span class="parse-error">, so an N-byte string of punctuation cost
    about 106N bytes and a synchronous String.replace over all of it: 8 MiB
    of '!' took 529 ms and 895 MB inside Address6.isValid(), which built the
    diagnostic only to discard it. At 16 MiB the replace exceeds V8's
    maximum string length and throws a RangeError in place of the
    AddressError callers catch; at 32 MiB V8 aborts the process outright
    with "Fatal JavaScript invalid size error" (node 24.19.0).
    
    An IPv6 address minus its suffix and zone is at most 45 characters (six
    four-digit groups, six colons, a dotted quad), and CPython's ipaddress
    module draws the same line, so anything longer is rejected before
    parse() runs. Address4 gets the same treatment at 15 characters.
    
    # Conflicts:
    #	README.md
    beaugunderson committed Sep 15, 2026
    Configuration menu
    Copy the full SHA
    8b34a21 View commit details
    Browse the repository at this point in the history
  5. 10.7.1

    beaugunderson committed Sep 15, 2026
    Configuration menu
    Copy the full SHA
    f0c25df View commit details
    Browse the repository at this point in the history
  6. Accept an arpa suffix in any case and without the root dot in fromArpa (

    #227)
    
    Address4.fromArpa() stripped `.in-addr.arpa` only when a root dot
    followed it, so `1.0.0.127.in-addr.arpa` threw while
    `1.0.0.127.in-addr.arpa.` parsed; Address6.fromArpa() already accepted
    both. Neither family accepted an uppercase suffix, though DNS names are
    case-insensitive.
    
    Both now strip the suffix in any case, with or without the root dot.
    Address4.fromArpa() also gets its own tests, including the reverseForm
    round trip for a host and a network.
    beaugunderson authored Sep 15, 2026
    Configuration menu
    Copy the full SHA
    4dfe8e5 View commit details
    Browse the repository at this point in the history
  7. 10.7.2

    beaugunderson committed Sep 15, 2026
    Configuration menu
    Copy the full SHA
    974b48d View commit details
    Browse the repository at this point in the history

Commits on Oct 1, 2026

  1. Reject an in-addr.arpa name longer than 32 characters before splittin…

    …g it (#228)
    
    Address4.fromArpa() split, reversed and joined its whole argument before
    handing the result to the constructor, so the constructor's 15-character
    limit ran only after work proportional to the input: about 8 ms per MB
    (node 24.19.0), against under 0.01 ms for the same string in
    Address6.fromArpa(), whose nibble grammar rejects it first.
    
    The longest name the constructor accepts is a 15-character address, a
    "/32" prefix length on its last octet (the RFC 2317 classless form) and
    ".in-addr.arpa.": 32 characters. Anything longer is rejected on entry.
    Every name that parsed before still parses; a longer one throws
    AddressError as it did, with a message naming the 32-character limit.
    beaugunderson authored Oct 1, 2026
    Configuration menu
    Copy the full SHA
    09b8072 View commit details
    Browse the repository at this point in the history
  2. 10.7.3

    beaugunderson committed Oct 1, 2026
    Configuration menu
    Copy the full SHA
    c7f838e View commit details
    Browse the repository at this point in the history
Loading