UNPKG

ip-sub

Version:

A set of utilities for IP/prefix validation and subnet matching.

855 lines (694 loc) 28.2 kB
const cache = { v4: {}, v6: {} }; const spaceConfig = { v4: { blockSize: 8, ipSizeCheck: 32, splitChar: ".", blockMax: 255, bits: 32, regex: new RegExp("^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)[.](25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)[.](25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)[.](25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$") }, v6: { blockSize: 16, ipSizeCheck: 64, splitChar: ":", blockMax: "ffff", bits: 128, regex: new RegExp("^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$") } }; const fromBinaryRegex = { v4: new RegExp(".{1,8}", "g"), v6: new RegExp(".{1,16}", "g") }; const v6FillStrings = []; for (let i = 0; i <= 7; i++) { v6FillStrings[i] = ":" + new Array(8 - i).fill(0).join(":") + ":"; } const ip = { IPV4_PRIVATE: ["10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "127.0.0.0/8"], IPV4_CGNAT: ["100.64.0.0/10"], IPV4_LOOPBACK: ["127.0.0.0/8"], IPV4_LINK_LOCAL: ["169.254.0.0/16"], IPV4_UNSPECIFIED: ["0.0.0.0/8"], IPV4_DOCUMENTATION: ["192.0.2.0/24", "198.51.100.0/24", "203.0.113.0/24"], IPV4_BENCHMARK: ["198.18.0.0/15"], IPV4_MULTICAST: ["224.0.0.0/4"], IPV4_RESERVED_FUTURE: ["240.0.0.0/4"], IPV4_BROADCAST: ["255.255.255.255/32"], IPV6_PRIVATE: ["fc00::/7"], IPV6_LOOPBACK: ["::1/128"], IPV6_UNSPECIFIED: ["::/128"], IPV6_LINK_LOCAL: ["fe80::/10"], IPV6_DOCUMENTATION: ["2001:db8::/32"], IPV6_MULTICAST: ["ff00::/8"], IPV6_IPV4_MAPPED: ["::ffff:0:0/96"], getIpAndCidr: function (prefix) { let bits, ip; for (let n = prefix.length - 1; n >= 0; n--) { if (prefix[n] == "/") { ip = prefix.slice(0, n); bits = prefix.slice(n + 1); if (bits !== "" && !isNaN(bits)) { return [ip, parseInt(bits)]; } } } throw new Error("Not valid prefix"); }, isValidPrefix: function (prefix) { try { return this._isValidPrefix(prefix, this.getAddressFamily(prefix)); } catch (error) { return false; } }, isValidNetwork: function (prefix) { try { return this._isValidPrefix(prefix, this.getAddressFamily(prefix)) && ip.toBinary(ip.getNetwork(prefix)) === ip.toBinary(prefix); } catch (error) { return false; } }, _isValidPrefix: function (prefix, af) { let bits, ip; if (prefix !== prefix.trim()) { return false; } try { const components = this.getIpAndCidr(prefix); ip = components[0]; bits = components[1]; if (af === 4) { return this._isValidIP(ip, af) && (bits >= 0 && bits <= spaceConfig.v4.bits); } else { return this._isValidIP(ip, af) && (bits >= 0 && bits <= spaceConfig.v6.bits); } } catch (e) { return false; } }, isValidIP: function (ip) { try { return this._isValidIP(ip, this.getAddressFamily(ip)); } catch (error) { return false; } }, _isValidCIDR: function (cidr) { const [ip, bits] = this.getIpAndCidr(cidr); const ntBin = this.toBinary(ip).slice(bits); return /^[0]*$/.test(ntBin); }, isValidCIDR: function (cidr) { try { return this.isValidPrefix(cidr) && this._isValidCIDR(cidr); } catch (error) { return false; } }, _isValidIP: function (ip, af) { try { if (!ip.includes("/")) { if (af === 4) { return spaceConfig.v4.regex.test(ip); } else { return spaceConfig.v6.regex.test(ip); } } return false; } catch (e) { return false; } }, sortByPrefixLength: function (a, b) { const netA = this.getIpAndCidr(a)[1]; const netB = this.getIpAndCidr(b)[1]; return parseInt(netA) - parseInt(netB); }, sortByPrefix: function (a, b) { const prefixAndNetA = this.getIpAndCidr(a); const prefixAndNetB = this.getIpAndCidr(b); const prefixA = prefixAndNetA[0]; const prefixB = prefixAndNetB[0]; const netA = prefixAndNetA[1]; const netB = prefixAndNetB[1]; const sortIp = this.sortByIp(prefixA, prefixB); if (sortIp === 0) { return parseInt(netA) - parseInt(netB); } else { return sortIp; } }, getPieces: function (ip) { const af = this.getAddressFamily(ip); if (af === 4) { return ip.split(spaceConfig.v4.splitChar).map(i => parseInt(i)); } else { return ip.split(spaceConfig.v6.splitChar).map(i => parseInt(i, 16)); } }, sortByIp: function (ipA, ipB) { const piecesA = this.getPieces(ipA); const piecesB = this.getPieces(ipB); for (let n = 0; n < piecesA.length; n++) { if (piecesA[n] < piecesB[n]) { return -1; } else if (piecesA[n] > piecesB[n]) { return 1; } } return 0; }, _v6Pad: function (ip) { return parseInt(ip, 16).toString(2).padStart(spaceConfig.v6.blockSize, "0"); }, _v4Pad: function (ip) { return parseInt(ip).toString(2).padStart(spaceConfig.v4.blockSize, "0"); }, _expandIP: function (ip, af, shortenedIpv6) { af = af || this.getAddressFamily(ip); return (af === 4) ? this._expandIPv4(ip) : this._expandIPv6(ip, shortenedIpv6); }, expandIP: function (ip, shortenedIpv6) { const af = this.getAddressFamily(ip); if (this._isValidIP(ip, af)) { return this._expandIP(ip, af, shortenedIpv6); } else { throw new Error("Not valid IP"); } }, expandPrefix: function (prefix, shortenedIpv6) { const [ip, bits] = this.getIpAndCidr(prefix); return [this.expandIP(ip, shortenedIpv6), bits].join("/"); }, _expandIPv4: function (ip) { let count = 0; for (let i = 0; i < ip.length; i++) { if (ip[i] === ".") { count++; } } for (let n = count; n < 3; n++) { ip += ".0"; } return ip; }, _expandIPv6: function (ip, shortenedIpv6) { ip = ip.toLowerCase(); const segments = ip.split(spaceConfig.v6.splitChar); const count = segments.length - 1; ip = segments .map(segment => { if (segment.length && segment == 0) { // Weak equality return "0"; } else { return segment || ""; } }) .join(spaceConfig.v6.splitChar); if (count !== 7) { ip = ip.replace("::", v6FillStrings[count]); if (ip[0] === ":") { ip = "0" + ip; } if (ip[ip.length - 1] === ":") { ip += "0"; } } return shortenedIpv6 ? ip : ip.split(":").map(i => ("0000" + i).slice(-4)).join(":"); }, isEqualIP: function (ip1, ip2) { const af1 = this.getAddressFamily(ip1); const af2 = this.getAddressFamily(ip2); if (this._isValidIP(ip1, af1) && this._isValidIP(ip2, af2)) { return af1 === af2 && this._isEqualIP(ip1, ip2, af1); } else { throw new Error("Not valid IP"); } }, isEqualPrefix: function (prefix1, prefix2) { const af1 = this.getAddressFamily(prefix1); const af2 = this.getAddressFamily(prefix2); if (this._isValidPrefix(prefix1, af1) && this._isValidPrefix(prefix2, af2)) { return af1 === af2 && this._isEqualPrefix(prefix1, prefix2, af1); } else { throw new Error("Not valid prefix"); } }, _isEqualIP: function (ip1, ip2, af) { return this._expandIP(ip1, af) === this._expandIP(ip2, af); }, _isEqualPrefix: function (prefix1, prefix2, af) { const components1 = this.getIpAndCidr(prefix1); const components2 = this.getIpAndCidr(prefix2); const ip1 = this._applyNetmask(this._expandIP(components1[0], af), components1[1], af); const ip2 = this._applyNetmask(this._expandIP(components2[0], af), components2[1], af); return components1[1] === components2[1] && ip1 === ip2; }, getAddressFamily: function (ip) { try { return (ip.indexOf(spaceConfig.v6.splitChar) === -1) ? 4 : 6; } catch (error) { throw new Error("Not valid IP or Prefix"); } }, toBinary: function (ip) { return this._toBinary(ip, this.getAddressFamily(ip)); }, _toBinary: function (ip, af) { let bytes = ""; let pad, splitter; let point = 0; let internalCache; if (af === 4) { pad = this._v4Pad; splitter = spaceConfig.v4.splitChar; ip = this._expandIPv4(ip); internalCache = cache.v4; } else { pad = this._v6Pad; splitter = spaceConfig.v6.splitChar; ip = this._expandIPv6(ip); internalCache = cache.v6; } for (let n = 1; n < ip.length; n++) { if (ip[n] == splitter) { const segment = ip.slice(point, n); if (internalCache[segment] == undefined) { internalCache[segment] = pad(segment); } bytes += internalCache[segment]; point = n + 1; } } const segment = ip.slice(point); if (internalCache[segment] == undefined) { internalCache[segment] = pad(segment); } bytes += internalCache[segment]; return bytes; }, applyNetmask: function (prefix, af) { const components = this.getIpAndCidr(prefix); const ip = components[0]; const bits = components[1]; af = af || this.getAddressFamily(ip); return this._applyNetmask(ip, bits, af); }, _applyNetmask: function (ip, bits, af) { if (af === 4) { return this._toBinary(ip, af).padEnd(spaceConfig.v4.bits, "0").slice(0, bits); } else { return this._toBinary(ip, af).padEnd(spaceConfig.v6.bits, "0").slice(0, bits); } }, _getPaddedNetmask: function (binary, af) { if (af === 4) { return binary.padEnd(spaceConfig.v4.bits, "0"); } else { return binary.padEnd(spaceConfig.v6.bits, "0"); } }, isSubnetBinary: (prefixContainer, prefixContained) => { return prefixContained !== prefixContainer && prefixContained.startsWith(prefixContainer); }, isSubnet: function (prefixContainer, prefixContained) { const p1af = this.getAddressFamily(prefixContainer); const p2af = this.getAddressFamily(prefixContained); return p1af === p2af && this._isSubnet(prefixContainer, prefixContained, p1af, p2af); }, _isSubnet: function (prefixContainer, prefixContained, p1af, p2af) { const components1 = this.getIpAndCidr(prefixContainer); const components2 = this.getIpAndCidr(prefixContained); return this.isSubnetBinary(this._applyNetmask(components1[0], components1[1], p1af), this._applyNetmask(components2[0], components2[1], p2af)); }, cidrToRange: function (cidr) { // Not optimized! const af = this.getAddressFamily(cidr); return this._cidrToRange(cidr, af); }, _cidrToRange: function (prefix, af) { const components = this.getIpAndCidr(prefix); const ip = components[0]; const bits = components[1]; const totalBits = af === 4 ? spaceConfig.v4.bits : spaceConfig.v6.bits; const bin = this._toBinary(ip, af).slice(0, bits); let first = this.fromBinary(bin.padEnd(totalBits, "0"), af); let last = this.fromBinary(bin.padEnd(totalBits, "1"), af); return [first, last]; }, getComponents: function (ip) { const af = this.getAddressFamily(ip); ip = this._expandIP(ip, af); return this._getComponents(ip, af); }, _getComponents: function (ip, af) { return ip.split((af === 4) ? spaceConfig.v4.splitChar : spaceConfig.v6.splitChar); }, fromBinary: function (ip, af) { let splitter, mapper; if (af === 4) { splitter = spaceConfig.v4.splitChar; mapper = function (bin) { return parseInt(bin, 2); }; } else { splitter = spaceConfig.v6.splitChar; mapper = function (bin) { return parseInt(bin, 2).toString(16); }; } const components = ip.match(af === 4 ? fromBinaryRegex.v4 : fromBinaryRegex.v6).map(mapper); return components.join(splitter); }, getSubPrefixes: function (prefix, recursive, netMaskIndex, af) { const [ip, bits] = this.getIpAndCidr(prefix); let out = []; netMaskIndex = netMaskIndex || {}; const newbits = bits + 1; const position = Math.floor(bits / spaceConfig[`v${af}`].blockSize); if (af === 4) { let components = this._getComponents(ip, af); if (bits >= spaceConfig.v4.ipSizeCheck) { return []; } while (components[position] < spaceConfig.v4.blockMax) { const ipTmp = components.join(spaceConfig.v4.splitChar); const msk = this._applyNetmask(ipTmp, newbits, af); if (!netMaskIndex[msk]) { out.push(ipTmp + "/" + newbits); netMaskIndex[msk] = true; } components[position]++; } } else { let components = this._getComponents(this._expandIP(ip, af), af); if (bits >= spaceConfig.v6.ipSizeCheck) { return []; } let item = parseInt(components[position], spaceConfig.v6.blockSize); const max = parseInt(spaceConfig.v6.blockMax, spaceConfig.v6.blockSize); while (item < max) { const ipTmp = components.join(spaceConfig.v6.splitChar); const prefixTmp = `${ipTmp}/${newbits}`; const msk = this._applyNetmask(ipTmp, newbits, af); if (!netMaskIndex[msk]) { out.push(prefixTmp); netMaskIndex[msk] = true; } item++; components[position] = item.toString(16); } } if (recursive) { return [].concat.apply([], out.concat(out.map(prefix => this.getSubPrefixes(prefix, true, netMaskIndex, af)))); } else { return out; } }, getAllSiblings: function (prefix) { const [ip, bits] = this.getIpAndCidr(prefix); const af = this.getAddressFamily(ip); this._getAllSiblings(ip, bits, af, bits); }, _getAllSiblings: function (ip, bits, af, parentBits) { let next = this._getNextSiblingPrefix(ip, bits, af, parentBits); const out = []; while (next) { out.push(next); next = this._getNextSiblingPrefix(...next, parentBits); } return out.map(i => i[0] + "/" + i[1]); }, _getNextSiblingPrefix: function (ip, bits, af, parentBits) { parentBits = parentBits || bits - 1; ip = this._expandIP(ip, af); let msk = this._applyNetmask(ip, bits, af); if (af === 4) { let value = parseInt(msk, 2) + 1; const result = value.toString(2).padStart(msk.length, "0"); if (msk.slice(0, parentBits) === result.slice(0, parentBits)) { return [this.fromBinary(this._getPaddedNetmask(result, 4), 4), bits, af]; } else { return null; } } else { // TODO // let value = parseInt(msk, 16) + "1".toString(16); // return [this.fromBinary(this._getPaddedNetmask(value.toString(2), 6), 6), bits, af]; } }, ipRangeToCidr: function (ip1, ip2) { // New implementation without external libraries. const af = this.getAddressFamily(ip1); if (af !== this.getAddressFamily(ip2)) { throw new Error("Address families do not match"); } const bits = spaceConfig[`v${af}`].bits; let startBin = this._toBinary(ip1, af).padStart(bits, "0"); let endBin = this._toBinary(ip2, af).padStart(bits, "0"); let start = BigInt("0b" + startBin); const end = BigInt("0b" + endBin); if (start > end) { throw new Error("Start IP is greater than end IP"); } const out = []; while (start <= end) { // count trailing zero bits of start to find max aligned block let maxTz = 0n; // find number of trailing zeros while (((start >> maxTz) & 1n) === 0n && maxTz < BigInt(bits)) { maxTz++; } // tzCandidate is the exponent for block size (2^tzCandidate) let tzCandidate = maxTz; // reduce block size until block end is within range while (tzCandidate >= 0n) { const blockSize = 1n << tzCandidate; const blockEnd = start + blockSize - 1n; if (blockEnd <= end) { break; } tzCandidate--; } // prefix length = bits - tzCandidate const prefixLen = bits - Number(tzCandidate); // convert start BigInt back to binary string and to IP string const binStr = start.toString(2).padStart(bits, "0"); const ipStr = this.fromBinary(binStr, af); out.push(`${ipStr}/${prefixLen}`); // advance start to next block start = start + (1n << tzCandidate); } return out; }, _compositeRange: function (ip1, ip2, bits, af, ipSizeCheck) { let mPrefixes = []; for (let b = bits; b <= ipSizeCheck; b++) { const tested = `${ip1}/${b}`; mPrefixes.push(tested); mPrefixes = mPrefixes.concat(this._getAllSiblings(ip1, b, af, bits)); const range = this._cidrToRange(tested, af); if (range[0] === ip1 && range[1] === ip2) { return [tested]; } } const out = []; const start = this._toBinary(ip1, af); const stop = this._toBinary(ip2, af); const sorted = mPrefixes .filter(prefix => { const range = this._cidrToRange(prefix, af).map(ip => this._toBinary(ip, af)); return range[0] >= start && range[1] <= stop; }) .sort((a, b) => this.sortByPrefixLength(a, b)); let pTmp = sorted.pop(); while (pTmp) { if (!sorted.some(prefix => this._isSubnet(prefix, pTmp, af, af))) { out.push(pTmp); } pTmp = sorted.pop(); } return out; }, toPrefix: function (ipOrPrefix) { const af = this.getAddressFamily(ipOrPrefix); if (this.isValidPrefix(ipOrPrefix)) { return this.shortenPrefix(ipOrPrefix, af); } else if (this.isValidIP(ipOrPrefix)) { if (af === 4) { return this.toPrefix(`${ipOrPrefix}/${spaceConfig.v4.bits}`); } else { return this.toPrefix(`${ipOrPrefix}/${spaceConfig.v6.bits}`); } } }, shortenIP: function (ip) { const af = this.getAddressFamily(ip); if (af === 4) { // IPv4: return as-is (no shortening needed) return ip; } // IPv6: expand to full 8 hextets padded, then apply RFC-style shortening: // - remove leading zeros from each hextet // - find the longest run of consecutive '0' hextets (length >= 2) and replace with :: const expanded = this._expandIP(ip, 6, false); // fully padded, lowercase const parts = expanded.split(":").map(p => p.toLowerCase()); // Strip leading zeros in each hextet const stripped = parts.map(p => { const s = p.replace(/^0+/, ""); return s === "" ? "0" : s; }); // Find longest sequence of '0' hextets let maxStart = -1, maxLen = 0; let curStart = -1, curLen = 0; for (let i = 0; i < stripped.length; i++) { if (stripped[i] === "0") { if (curStart === -1) { curStart = i; curLen = 1; } else { curLen++; } } else { if (curLen > maxLen) { maxLen = curLen; maxStart = curStart; } curStart = -1; curLen = 0; } } // tail check if (curLen > maxLen) { maxLen = curLen; maxStart = curStart; } // Only compress runs of length >= 2 if (maxLen < 2) { return stripped.join(":"); } const head = stripped.slice(0, maxStart).join(":"); const tail = stripped.slice(maxStart + maxLen).join(":"); if (head === "" && tail === "") { return "::"; } else if (head === "") { return `::${tail}`; } else if (tail === "") { return `${head}::`; } else { return `${head}::${tail}`; } }, shortenPrefix: function (prefix, af) { const [ip, cidr] = this.getIpAndCidr(prefix); af = af || this.getAddressFamily(ip); const binaryIp = this._getPaddedNetmask(this.applyNetmask(prefix, af), af); return `${this.shortenIP(this.fromBinary(binaryIp, af))}/${cidr}`; }, getAllLessSpecificBinaries: function (prefix, includeMoreSpecific = false) { const binary = this.applyNetmask(prefix); const out = includeMoreSpecific ? [binary] : []; for (let i = 1; i < binary.length; i++) { out.push(binary.slice(0, i)); } return out; }, getNextIp: function (ip) { const af = this.getAddressFamily(ip); const binaryIp = this._toBinary(ip, af); let incremented = (BigInt("0b" + binaryIp) + BigInt(1)).toString(2).padStart(binaryIp.length, "0"); return this.fromBinary(incremented, af); }, getPrevIp: function (ip) { const af = this.getAddressFamily(ip); const binaryIp = this._toBinary(ip, af); let decremented = (BigInt("0b" + binaryIp) - BigInt(1)).toString(2).padStart(binaryIp.length, "0"); return this.fromBinary(decremented, af); }, getUsableAddressRangeByNetwork: function (networkInCidrNotation, maintainCidr = false) { const [_, cidr] = this.getIpAndCidr(networkInCidrNotation); return [...new Set(this._getUsableAddressRangeByNetwork(networkInCidrNotation, cidr).map(i => maintainCidr ? `${i}/${cidr}` : i))]; }, _getUsableAddressRangeByNetwork: function (networkInCidrNotation, cidr) { const af = this.getAddressFamily(networkInCidrNotation); const range = this._cidrToRange(networkInCidrNotation, af); if ((cidr >= 31 && af === 4) || cidr >= 127 && af === 6) { return range; } return [this.getNextIp(range[0]), this.getPrevIp(range[1])]; }, getNetwork: function (prefix) { return this.shortenPrefix(prefix); }, isReservedRange: function (prefix) { if (this.isValidPrefix(prefix)) { const af = this.getAddressFamily(prefix); const reservedRanges = af === 4 ? [ ...this.IPV4_PRIVATE, ...this.IPV4_CGNAT, ...this.IPV4_LOOPBACK, ...this.IPV4_LINK_LOCAL, ...this.IPV4_UNSPECIFIED, ...this.IPV4_DOCUMENTATION, ...this.IPV4_BENCHMARK, ...this.IPV4_MULTICAST, ...this.IPV4_RESERVED_FUTURE, ...this.IPV4_BROADCAST ] : [ ...this.IPV6_PRIVATE, ...this.IPV6_LOOPBACK, ...this.IPV6_UNSPECIFIED, ...this.IPV6_LINK_LOCAL, ...this.IPV6_DOCUMENTATION, ...this.IPV6_MULTICAST, ...this.IPV6_IPV4_MAPPED ]; return reservedRanges.some(range => this.isSubnet(range, prefix)); } else { throw new Error("Invalid prefix"); } }, isReservedIP: function (ip) { if (this.isValidIP(ip)) { return this.isReservedRange(this.toPrefix(ip)); } else { throw new Error("Invalid IP"); } }, getAllMorespecificPrefixes: function (prefix) { // Validate prefix if (!this.isValidPrefix(prefix)) { throw new Error("Invalid prefix"); } const [ip, bits] = this.getIpAndCidr(prefix); const af = this.getAddressFamily(ip); // Delegate to getSubPrefixes which already supports recursive listing of more-specific prefixes. // We pass an empty netMaskIndex to ensure uniqueness across generated masks. return this.getSubPrefixes(prefix, true, {}, af); }, isEqual: function (a, b) { return this.isEqualPrefix(this.toPrefix(a), this.toPrefix(b)); }, unique: function (array) { const seen = new Set(); const result = []; for (const item of array) { const tmp = this.toPrefix(item); const af = this.getAddressFamily(tmp); const [ip, bits] = this.getIpAndCidr(tmp); const canonical = `${af}-${this._applyNetmask(ip, bits, af)}`; if (!seen.has(canonical)) { seen.add(canonical); result.push(item); } } return result; } }; module.exports = ip;