diff --git a/README.md b/README.md index a1c2ef7..551be0a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # IPData Node.js Library -[![](https://github.com/ipdata/node/workflows/CI/badge.svg)](https://github.com/ipdata/node/actions) +[![Lint and Test](https://github.com/ipdata/node/actions/workflows/default.yml/badge.svg)](https://github.com/ipdata/node/actions/workflows/default.yml) A Node.js library for the [ipdata](https://ipdata.co) API. Look up IP addresses for geolocation, threat intelligence, company data, and more. @@ -57,6 +57,8 @@ const ipdata = new IPData(''); The library caches up to 4096 responses for 24 hours using an LRU cache by default. You can configure the cache by passing an options object as the second parameter. +Entries are keyed by IP address and by what was requested: a lookup restricted with `selectField` or `fields` is cached separately from a full lookup of the same address, so a filtered result is never returned for a full lookup, or the other way round. Only identical selections share an entry (the order of `fields` does not matter). Full lookups are stored under the address itself, so `ipdata.cache.has('1.1.1.1')` tells you whether a complete record is cached. + ```js const ipdata = new IPData('', { max: 1000, // max number of entries @@ -166,14 +168,14 @@ const ipdata = new IPData('', undefined, undefined, { }); ``` -Use `dispatcher` to send requests through your own [undici dispatcher](https://undici.nodejs.org/#/docs/api/Dispatcher), for example to reach the API through a proxy or to intercept requests in tests. `http2` and `keepAliveTimeout` only apply to the pool the library creates itself. +Use `dispatcher` to send requests through your own [undici dispatcher](https://undici.nodejs.org/#/docs/api/Dispatcher), for example to reach the API through a proxy or to intercept requests in tests. `http2` and `keepAliveTimeout` only apply to the pool the library creates itself: a dispatcher you inject negotiates HTTP/2 only if it is configured to. For undici's `ProxyAgent` and `EnvHttpProxyAgent` that means `allowH2: true` together with `requestTls: { allowH2: true }`, because the protocols offered on the TLS connection tunnelled through the proxy come from `requestTls`. ```js import { EnvHttpProxyAgent, MockAgent, ProxyAgent } from 'undici'; -// Explicit proxy +// Explicit proxy, negotiating HTTP/2 with the API through the tunnel const ipdata = new IPData('', undefined, undefined, { - dispatcher: new ProxyAgent('http://proxy.example.com:8080'), + dispatcher: new ProxyAgent({ uri: 'http://proxy.example.com:8080', allowH2: true, requestTls: { allowH2: true } }), }); // HTTP_PROXY, HTTPS_PROXY and NO_PROXY from the environment @@ -191,6 +193,8 @@ const ipdata = new IPData('', undefined, undefined, { dispatcher: mockAg A request that fails because the server closed a pooled connection before any response byte arrived (connection reset or closed) is sent once more on a fresh connection; this also covers the `POST` made by `bulkLookup()`. HTTP error statuses are never retried. +Every request carries a `User-Agent` header of the form `ipdata-node/`, with the version of the installed package. + ## Response Fields The following fields are available for use with `selectField` and `fields`: @@ -207,7 +211,7 @@ The following fields are available for use with `selectField` and `fields`: **`is_anycast`** — `true` when the address is anycast. Also included only when `true`. -**`threat`** — `{ is_tor, is_icloud_relay, is_proxy, is_datacenter, is_anonymous, is_known_attacker, is_known_abuser, is_threat, is_bogon, blocklists }` +**`threat`** — `{ is_tor, is_vpn, is_icloud_relay, is_proxy, is_datacenter, is_anonymous, is_known_attacker, is_known_abuser, is_threat, is_bogon, blocklists, scores }` Each entry of `threat.blocklists` is `{ name, site, type, first_seen, last_seen, days_seen }`: @@ -217,6 +221,14 @@ Each entry of `threat.blocklists` is `{ name, site, type, first_seen, last_seen, Some feeds, for example ipdata's own `vpn` list, do not report these three, so an entry may contain only `name`, `site` and `type`. The dates are passed through as strings. +**`threat.scores`** — `{ vpn_score, proxy_score, threat_score, trust_score }`, each a number between 0 and 100. + +**`region_type`** — the type of administrative region named by `region`; `null` when the location is unknown. + +**`count`** — the request count for your API key, returned by the API as a string (for example `"0"`). + +`city`, `region`, `region_code`, `region_type`, `postal` and every value of `time_zone` are `null` when an address has no known location, as is the case for anycast addresses; the TypeScript types declare them as nullable. + ## TypeScript The library is written in TypeScript and exports the following types: diff --git a/package-lock.json b/package-lock.json index 5aa695b..715eb5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,13 +36,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -51,9 +51,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", - "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", "dev": true, "license": "MIT", "engines": { @@ -61,21 +61,21 @@ } }, "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -102,14 +102,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "version": "7.29.12", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.12.tgz", + "integrity": "sha512-2W76mhjrpQFO1lK5e8IvGYB2D0YZ7S2R1Z2BjO2ZUYuW+amcpK/z60U6ZZmzskwoa7sJ6XohP/Wg8H5KxXI/IA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.12", + "@babel/types": "^7.29.11", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -119,14 +119,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -146,9 +146,9 @@ } }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.2.tgz", + "integrity": "sha512-GYrCADwpu6yV5OAvchkQ27Z5wwMxWAEMg5NNWjyHh/5DboFFqbdbvYNIvZouQpxXyeMPoTOh0vdPYKlf0ZswdQ==", "dev": true, "license": "ISC", "bin": { @@ -156,9 +156,9 @@ } }, "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", "dev": true, "license": "MIT", "engines": { @@ -166,29 +166,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -208,9 +208,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.11.tgz", + "integrity": "sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==", "dev": true, "license": "MIT", "engines": { @@ -218,9 +218,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", "engines": { @@ -228,9 +228,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", "dev": true, "license": "MIT", "engines": { @@ -238,27 +238,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", - "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "version": "7.29.10", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.10.tgz", + "integrity": "sha512-EMCx3AGZ3WfAl6ht7AyQoPm2Lh+BGYEMdc4Q6Hf7uSCDOZFuLq44qNwL+iaim8VlyYhRAo4X43+weadK8ZB4Yg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", - "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "version": "7.29.12", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.12.tgz", + "integrity": "sha512-WJ7JBp/Q4+z/ImydtI7jS6NVco5++632ExBemSX+Ysb+G1lCwQ5BVo3SY+jGGOcZXI4ZccbZSUSJLNBW6eEpJg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.29.0" + "@babel/types": "^7.29.11" }, "bin": { "parser": "bin/babel-parser.js" @@ -507,33 +507,33 @@ } }, "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "version": "7.29.12", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.12.tgz", + "integrity": "sha512-MscXe/NAZcOkYEsRajHZFt3ynsb22PUi6CTrp5U7+RYycR9By84mn9mFN54YgfITiqK4hFx3Ju3tZSyzfpS5ew==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.12", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.12", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.11", "debug": "^4.3.1" }, "engines": { @@ -541,14 +541,14 @@ } }, "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.11", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz", + "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" + "@babel/helper-string-parser": "^7.29.11", + "@babel/helper-validator-identifier": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -648,17 +648,10 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -677,9 +670,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", - "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -715,17 +708,10 @@ "node": ">=10.10.0" } }, - "node_modules/@humanwhocodes/config-array/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -734,9 +720,9 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", - "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -810,9 +796,9 @@ } }, "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", + "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", "dev": true, "license": "MIT", "dependencies": { @@ -2443,19 +2429,16 @@ } }, "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } + "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", - "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "version": "2.11.35", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.35.tgz", + "integrity": "sha512-GNk8KX52CHqeNKGk8+7tZnQab/2k1t/BgrUHPKNdijzpEhF1saPQZHVKb6m5loSAxB39gyrTYw34Qf9CQ3u1qQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2466,16 +2449,13 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", - "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" + "balanced-match": "^1.0.0" } }, "node_modules/braces": { @@ -2492,9 +2472,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", - "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "version": "4.29.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.5.tgz", + "integrity": "sha512-khTmKu5NGYTotqcYo8Bew2k4RAE9PuXRSFRXG2lQBrCEQHyeQ/vX6gKMxfokwywBsLiXZMnHsmWMG1pnO8J7uQ==", "dev": true, "funding": [ { @@ -2512,11 +2492,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.9.0", - "caniuse-lite": "^1.0.30001759", - "electron-to-chromium": "^1.5.263", - "node-releases": "^2.0.27", - "update-browserslist-db": "^1.2.0" + "baseline-browser-mapping": "^2.11.33", + "caniuse-lite": "^1.0.30001819", + "electron-to-chromium": "^1.5.464", + "node-releases": "^2.0.60", + "update-browserslist-db": "^1.3.4" }, "bin": { "browserslist": "cli.js" @@ -2626,9 +2606,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001772", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001772.tgz", - "integrity": "sha512-mIwLZICj+ntVTw4BT2zfp+yu/AqV6GMKfJVJMx3MwPxs+uk/uj2GLl2dH8LQbjiLDX66amCga5nKFyDgRR43kg==", + "version": "1.0.30001820", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001820.tgz", + "integrity": "sha512-gfWD6MxqVCqAOwmODfZf7DchWvlrA+SRdNX7ceLfg3xRqe5ssjjpF7EgslC/0tiLiNTajdaNN/q8c/5u/7Gz8w==", "dev": true, "funding": [ { @@ -3040,9 +3020,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.302", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", - "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==", + "version": "1.5.473", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.473.tgz", + "integrity": "sha512-9njcRQK+6FOXNAWw5g+h2RPiyazqo5RFMVJ39ZMHjDHUNnnFlhnOGV7O0WmJetODDx19ES5p+FLi+KCVFQGugw==", "dev": true, "license": "ISC" }, @@ -3495,17 +3475,10 @@ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, - "node_modules/eslint-plugin-import/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, "node_modules/eslint-plugin-import/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -3537,9 +3510,9 @@ } }, "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", - "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -3646,17 +3619,10 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, "node_modules/eslint/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -3685,9 +3651,9 @@ } }, "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", - "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -3962,9 +3928,9 @@ } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", "dev": true, "license": "ISC" }, @@ -4205,17 +4171,10 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -4224,9 +4183,9 @@ } }, "node_modules/glob/node_modules/minimatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", - "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -4318,13 +4277,13 @@ "license": "MIT" }, "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "version": "4.7.10", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.10.tgz", + "integrity": "sha512-P5VJMVM7qgBn6vjXMw8WG9uVI+ncf2pi72j4de4yz5ZULLj2RGqLYaKOYGsgyrViQ0tePOVlN1tDCCXXtFqXKg==", "dev": true, "license": "MIT", "dependencies": { - "minimist": "^1.2.5", + "minimist": "^1.2.8", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" @@ -5735,10 +5694,20 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -5989,13 +5958,13 @@ } }, "node_modules/minimatch": { - "version": "9.0.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.6.tgz", - "integrity": "sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ==", + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", "dev": true, "license": "ISC", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^2.0.2" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -6059,11 +6028,14 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "version": "2.0.63", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.63.tgz", + "integrity": "sha512-6li2QanLsAXpIH2R6j77MzRyvOyrvdTqlKtO4CgBdHGNvN99ZZfyxszHc5o5+IZBWD5u6Hzwg83JoGXmqZNG9Q==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/normalize-path": { "version": "3.0.0", @@ -6408,9 +6380,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, "license": "MIT", "engines": { @@ -6894,9 +6866,9 @@ } }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.6", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.6.tgz", + "integrity": "sha512-9lpSSU4Mly3mrtruwLs1eLK6+gzbzqeBFwRNjIjE+9LuDpWdYctrQlQ3ej1Ilgi7gknpsTRnPoLP8DpCHqHIJw==", "dev": true, "license": "ISC", "bin": { @@ -7347,17 +7319,10 @@ "node": ">=8" } }, - "node_modules/test-exclude/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -7366,9 +7331,9 @@ } }, "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", - "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -7421,9 +7386,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", "engines": { @@ -7467,19 +7432,19 @@ } }, "node_modules/ts-jest": { - "version": "29.4.6", - "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", - "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", + "version": "29.4.15", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.15.tgz", + "integrity": "sha512-s2qmEUYFRqbjz1wv0EmccNPVOb45HOFpFuhzQJLKShYVisuTbCFPIIAIV2EuK5rIxb4BejLDbH7dnBZfYjybUQ==", "dev": true, "license": "MIT", "dependencies": { "bs-logger": "^0.2.6", "fast-json-stable-stringify": "^2.1.0", - "handlebars": "^4.7.8", + "handlebars": "^4.7.10", "json5": "^2.2.3", "lodash.memoize": "^4.1.2", "make-error": "^1.3.6", - "semver": "^7.7.3", + "semver": "^7.8.5", "type-fest": "^4.41.0", "yargs-parser": "^21.1.1" }, @@ -7490,13 +7455,13 @@ "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" }, "peerDependencies": { - "@babel/core": ">=7.0.0-beta.0 <8", + "@babel/core": ">=7.0.0-beta.0 <9", "@jest/transform": "^29.0.0 || ^30.0.0", "@jest/types": "^29.0.0 || ^30.0.0", "babel-jest": "^29.0.0 || ^30.0.0", "jest": "^29.0.0 || ^30.0.0", "jest-util": "^29.0.0 || ^30.0.0", - "typescript": ">=4.3 <6" + "typescript": ">=4.3 <7" }, "peerDependenciesMeta": { "@babel/core": { @@ -7800,9 +7765,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.4.tgz", + "integrity": "sha512-t8wonIOdQyOjtPd2VktlBu6QjTVTks6v8bSi7kd0r2Vll8TK1JYDZtRRdbmCHOAMphkZgrzK+L+vXK4pyZ8t4A==", "dev": true, "funding": [ { diff --git a/package.json b/package.json index 57e2fac..110b14d 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,8 @@ "scripts": { "prebuild": "del lib", "build": "tsc", - "lint": "eslint --fix \"src/**\"", + "lint": "eslint \"src/**\"", + "lint:fix": "eslint --fix \"src/**\"", "test": "jest" }, "repository": { diff --git a/src/ipdata.test.ts b/src/ipdata.test.ts index b737cea..7137842 100644 --- a/src/ipdata.test.ts +++ b/src/ipdata.test.ts @@ -4,6 +4,9 @@ import { once } from 'events'; import { Agent, MockAgent, errors } from 'undici'; import IPData, { EU_BASE_URL, LookupResponse } from './ipdata'; +// eslint-disable-next-line @typescript-eslint/no-require-imports +const { version: packageVersion }: { version: string } = require('../package.json'); + const TEST_API_KEY = 'test-api-key'; const TEST_IP = '1.1.1.1'; const DEFAULT_IP_KEY = 'DEFAULT_IP'; @@ -50,7 +53,7 @@ const MOCK_IP1_DATA = { is_datacenter: true, blocklists: [], }, - count: 0, + count: '0', }; const MOCK_IP2_DATA = { @@ -275,6 +278,13 @@ function keyBy(items: LookupResponse[]): Record { }, {}); } +// Waits long enough for an entry stored with ttl: 1 to expire. +function waitForTtlExpiry(): Promise { + return new Promise((resolve) => { + setTimeout(resolve, 5); + }); +} + describe('constructor()', () => { it('should throw an error if an apiKey is not provided', () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -564,8 +574,12 @@ describe('lookup()', () => { const customIPData = new IPData(TEST_API_KEY, { ttl: 1 }, undefined, { dispatcher: mockAgent }); mockLookup('/', MOCK_DEFAULT_IP_DATA); await customIPData.lookup(); - const info = ipdata.cache.get(DEFAULT_IP_KEY); - expect(info).toBeUndefined(); + await waitForTtlExpiry(); + expect(customIPData.cache.get(DEFAULT_IP_KEY)).toBeUndefined(); + // The entry has expired, so the next lookup is sent to the API again. + mockLookup('/', MOCK_DEFAULT_IP_DATA); + const info = await customIPData.lookup(); + expect(info).toHaveProperty('ip', MOCK_DEFAULT_IP_DATA.ip); }); }); @@ -594,8 +608,12 @@ describe('lookup()', () => { const customIPData = new IPData(TEST_API_KEY, { ttl: 1 }, undefined, { dispatcher: mockAgent }); mockLookup('/1.1.1.1', MOCK_IP1_DATA); await customIPData.lookup(TEST_IP); - const info = ipdata.cache.get(TEST_IP); - expect(info).toBeUndefined(); + await waitForTtlExpiry(); + expect(customIPData.cache.get(TEST_IP)).toBeUndefined(); + // The entry has expired, so the next lookup is sent to the API again. + mockLookup('/1.1.1.1', MOCK_IP1_DATA); + const info = await customIPData.lookup(TEST_IP); + expect(info).toHaveProperty('ip', TEST_IP); }); }); @@ -639,6 +657,84 @@ describe('lookup()', () => { }); }); + describe('cache keys', () => { + it('should not serve a selectField result to a full lookup of the same address', async () => { + mockLookup('/1.1.1.1/city', 'Los Angeles'); + const city = await ipdata.lookup(TEST_IP, 'city'); + expect(city).toEqual({ city: 'Los Angeles', status: 200 }); + expect(ipdata.cache.has(TEST_IP)).toBe(false); + + mockLookup('/1.1.1.1', MOCK_IP1_DATA); + const info = await ipdata.lookup(TEST_IP); + expect(info).toHaveProperty('country_name', 'United States'); + expect(ipdata.cache.get(TEST_IP)).toBe(info); + }); + + it('should not serve a fields result to a full lookup of the same address', async () => { + mockLookup('/1.1.1.1', { ip: TEST_IP, city: 'Los Angeles' }, { params: { fields: 'ip,city' } }); + const partial = await ipdata.lookup(TEST_IP, undefined, ['ip', 'city']); + expect(partial).toEqual({ ip: TEST_IP, city: 'Los Angeles', status: 200 }); + expect(ipdata.cache.has(TEST_IP)).toBe(false); + + mockLookup('/1.1.1.1', MOCK_IP1_DATA); + const info = await ipdata.lookup(TEST_IP); + expect(info).toHaveProperty('country_name', 'United States'); + }); + + it('should not serve a full result to a filtered lookup of the same address', async () => { + mockLookup('/1.1.1.1', MOCK_IP1_DATA); + await ipdata.lookup(TEST_IP); + + mockLookup('/1.1.1.1/city', 'Los Angeles'); + const city = await ipdata.lookup(TEST_IP, 'city'); + expect(city).toEqual({ city: 'Los Angeles', status: 200 }); + + mockLookup('/1.1.1.1', { ip: TEST_IP }, { params: { fields: 'ip' } }); + const partial = await ipdata.lookup(TEST_IP, undefined, ['ip']); + expect(partial).toEqual({ ip: TEST_IP, status: 200 }); + }); + + it('should serve repeated identical filtered lookups from the cache', async () => { + mockLookup('/1.1.1.1/city', 'Los Angeles'); + const first = await ipdata.lookup(TEST_IP, 'city'); + const second = await ipdata.lookup({ ip: TEST_IP, selectField: 'city' }); + expect(second).toBe(first); + + mockLookup('/1.1.1.1', { ip: TEST_IP, city: 'Los Angeles' }, { params: { fields: 'ip,city' } }); + const partial = await ipdata.lookup(TEST_IP, undefined, ['ip', 'city']); + // The same fields in another order select the same data, so the entry is shared. + const reordered = await ipdata.lookup(TEST_IP, undefined, ['city', 'ip']); + expect(reordered).toBe(partial); + }); + + it('should cache different selections of the same address separately', async () => { + mockLookup('/1.1.1.1/city', 'Los Angeles'); + mockLookup('/1.1.1.1/is_eu', false); + const city = await ipdata.lookup(TEST_IP, 'city'); + const isEu = await ipdata.lookup(TEST_IP, 'is_eu'); + expect(city).toEqual({ city: 'Los Angeles', status: 200 }); + expect(isEu).toEqual({ is_eu: false, status: 200 }); + }); + + it('should key lookups of the default address by selection as well', async () => { + mockLookup('/is_eu', false); + await ipdata.lookup(undefined, 'is_eu'); + expect(ipdata.cache.has(DEFAULT_IP_KEY)).toBe(false); + + mockLookup('/', MOCK_DEFAULT_IP_DATA); + const info = await ipdata.lookup(); + expect(info).toHaveProperty('ip', MOCK_DEFAULT_IP_DATA.ip); + expect(ipdata.cache.get(DEFAULT_IP_KEY)).toBe(info); + }); + + it('should validate the parameters before consulting the cache', async () => { + mockLookup('/1.1.1.1', MOCK_IP1_DATA); + await ipdata.lookup(TEST_IP); + await expect(ipdata.lookup(TEST_IP, 'field')).rejects.toThrow('field is not a valid field.'); + await expect(ipdata.lookup(TEST_IP, undefined, ['field'])).rejects.toThrow('field is not a valid field.'); + }); + }); + describe('company field', () => { it('should accept company as a valid select field', async () => { const companyData = { @@ -790,6 +886,35 @@ describe('lookup()', () => { expect(info.is_satellite).toBeUndefined(); }); }); + + describe('is_vpn, scores, region_type and nullable fields', () => { + it('should expose threat.is_vpn and threat.scores', async () => { + mockLookup('/187.13.250.1', SAMPLE_VPN_DATA); + const info = await ipdata.lookup('187.13.250.1'); + expect(info.threat.is_vpn).toBe(true); + expect(info.threat.scores).toEqual({ vpn_score: 100, proxy_score: 99, threat_score: 100, trust_score: 0 }); + expect(info.threat.scores?.trust_score).toBe(0); + }); + + it('should pass count through as the string the API sends', async () => { + mockLookup('/98.97.34.0', SAMPLE_SATELLITE_DATA); + const info = await ipdata.lookup('98.97.34.0'); + expect(info.count).toBe('0'); + expect(info.threat.is_vpn).toBe(false); + expect(info.threat.scores).toEqual({ vpn_score: 0, proxy_score: 1, threat_score: 8, trust_score: 97 }); + }); + + it('should expose null location fields for an address without a location', async () => { + mockLookup('/8.8.8.8', SAMPLE_ANYCAST_DATA); + const info = await ipdata.lookup('8.8.8.8'); + expect(info.city).toBeNull(); + expect(info.region).toBeNull(); + expect(info.region_code).toBeNull(); + expect(info.region_type).toBeNull(); + expect(info.postal).toBeNull(); + expect(info.time_zone).toEqual({ name: null, abbr: null, offset: null, is_dst: null, current_time: null }); + }); + }); }); describe('bulkLookup()', () => { @@ -879,6 +1004,64 @@ describe('bulkLookup()', () => { }); }); + describe('cache keys', () => { + it('should cache a fields result separately from a full lookup', async () => { + const fields = ['ip', 'is_eu']; + mockBulkLookup( + [IP1, IP2], + [ + { ip: IP1, is_eu: false }, + { ip: IP2, is_eu: true }, + ], + { params: { fields: fields.join(',') } }, + ); + await ipdata.bulkLookup([IP1, IP2], fields); + expect(ipdata.cache.has(IP1)).toBe(false); + expect(ipdata.cache.has(IP2)).toBe(false); + + // Neither a full lookup nor a full bulk lookup is served the filtered entries. + mockLookup('/1.1.1.1', MOCK_IP1_DATA); + const single = await ipdata.lookup(IP1); + expect(single).toHaveProperty('country_name', 'United States'); + + mockBulkLookup([IP2], [MOCK_IP2_DATA]); + const info = keyBy(await ipdata.bulkLookup([IP1, IP2])); + expect(info[IP1]).toBe(single); + expect(info[IP2]).toHaveProperty('country_name', 'United States'); + expect(ipdata.cache.get(IP2)).toBe(info[IP2]); + }); + + it('should reuse cached entries only for the same fields', async () => { + const fields = ['ip', 'is_eu']; + mockLookup('/1.1.1.1', { ip: IP1, is_eu: false }, { params: { fields: fields.join(',') } }); + await ipdata.lookup(IP1, undefined, fields); + mockLookup('/8.8.8.8', MOCK_IP2_DATA); + await ipdata.lookup(IP2); + + // IP1 is served from its entry for these fields; the full entry of IP2 does not match, so IP2 is requested. + mockBulkLookup([IP2], [{ ip: IP2, is_eu: true }], { params: { fields: fields.join(',') } }); + const info = keyBy(await ipdata.bulkLookup([IP1, IP2], fields)); + expect(info[IP1]).toEqual({ ip: IP1, is_eu: false, status: 200 }); + expect(info[IP2]).toEqual({ ip: IP2, is_eu: true, status: 200 }); + }); + + it('should not cache entries that do not include the ip field', async () => { + const fields = ['is_eu']; + mockBulkLookup([IP1, IP2], [{ is_eu: false }, { is_eu: true }], { params: { fields: fields.join(',') } }); + const result = await ipdata.bulkLookup([IP1, IP2], fields); + expect(result).toEqual([ + { is_eu: false, status: 200 }, + { is_eu: true, status: 200 }, + ]); + expect(ipdata.cache.size).toBe(0); + + // A lookup of the default address with the same fields is not served one of those entries. + mockLookup('/', { is_eu: false }, { params: { fields: fields.join(',') } }); + const info = await ipdata.lookup({ fields }); + expect(info).toEqual({ is_eu: false, status: 200 }); + }); + }); + describe('is_satellite, is_anycast and blocklist dates', () => { const SATELLITE_IP = '98.97.34.0'; const ANYCAST_IP = '8.8.8.8'; @@ -919,3 +1102,38 @@ describe('bulkLookup()', () => { }); }); }); + +describe('User-Agent', () => { + const ipdata = new IPData(TEST_API_KEY, undefined, undefined, { dispatcher: mockAgent }); + const userAgent = `ipdata-node/${packageVersion}`; + + afterEach(() => { + ipdata.cache.clear(); + }); + + it('should be derived from the package version', () => { + expect(packageVersion).toMatch(/^\d+\.\d+\.\d+/); + }); + + it('should identify the library on lookup()', async () => { + mockPool + .intercept({ path: apiPath('/1.1.1.1'), method: 'GET', headers: { 'user-agent': userAgent } }) + .reply(200, JSON.stringify(MOCK_IP1_DATA), { headers: { 'content-type': 'application/json' } }); + const info = await ipdata.lookup(TEST_IP); + expect(info).toHaveProperty('ip', TEST_IP); + }); + + it('should identify the library on bulkLookup()', async () => { + const ips = [TEST_IP, '8.8.8.8']; + mockPool + .intercept({ + path: apiPath('/bulk'), + method: 'POST', + body: JSON.stringify(ips), + headers: { 'user-agent': userAgent, 'content-type': 'application/json', accept: 'application/json' }, + }) + .reply(200, JSON.stringify([MOCK_IP1_DATA, MOCK_IP2_DATA]), { headers: { 'content-type': 'application/json' } }); + const result = await ipdata.bulkLookup(ips); + expect(result).toHaveLength(2); + }); +}); diff --git a/src/ipdata.ts b/src/ipdata.ts index dae0455..0a975b4 100644 --- a/src/ipdata.ts +++ b/src/ipdata.ts @@ -1,6 +1,10 @@ import { isIP as netIsIP } from 'net'; import { LRUCache } from 'lru-cache'; -import { fetch, Agent, type Dispatcher, type RequestInit, type Response } from 'undici'; +import { fetch, Agent, Headers, type Dispatcher, type RequestInit, type Response } from 'undici'; + +// package.json is part of every install of this package, so the version is available at runtime. +// eslint-disable-next-line @typescript-eslint/no-require-imports +const { version }: { version: string } = require('../package.json'); const CACHE_MAX = 4096; // max number of items const CACHE_TTL = 1000 * 60 * 60 * 24; // 24 hours @@ -8,6 +12,7 @@ const KEEP_ALIVE_TIMEOUT = 1000 * 60; // close a pooled connection after 60 seco const KEEP_ALIVE_MAX_TIMEOUT = 1000 * 60 * 10; // upper bound when the server sends a larger keep-alive hint const KEEP_ALIVE_PROBE_INTERVAL = 1000 * 30; // HTTP/2 PING and TCP keepalive probe interval const DEFAULT_IP = 'DEFAULT_IP'; +const USER_AGENT = `ipdata-node/${version}`; const VALID_FIELDS = [ 'ip', 'is_eu', @@ -45,29 +50,39 @@ function isValidIP(ip: string): boolean { return ip === DEFAULT_IP || netIsIP(ip) !== 0; } -function isValidSelectField(field: string): boolean { - const index = VALID_FIELDS.indexOf(field); - - if (index === -1) { +function validateSelectField(field: string): void { + if (VALID_FIELDS.indexOf(field) === -1) { throw new Error(`${field} is not a valid field.`); } - - return true; } -function isValidFields(fields: string[]): boolean { +function validateFields(fields: string[]): void { if (!Array.isArray(fields)) { throw new Error('Fields should be an array.'); } fields.forEach((field) => { - const index = VALID_FIELDS.indexOf(field); - if (index === -1) { + if (VALID_FIELDS.indexOf(field) === -1) { throw new Error(`${field} is not a valid field.`); } }); +} + +// Cache entries are keyed by address and by what was requested, so a lookup restricted to a single field or to a +// list of fields is never served to a full lookup of the same address, or the other way round. The order of +// `fields` does not change the response, so the key uses them sorted. +function cacheKey(ip: string | undefined, selectField?: string, fields?: string[]): string { + const address = ip || DEFAULT_IP; + + if (selectField) { + return `${address}/${selectField}`; + } - return true; + if (fields) { + return `${address}?fields=${[...fields].sort().join(',')}`; + } + + return address; } interface TransportError { @@ -143,16 +158,19 @@ export interface LookupResponse { is_satellite?: boolean; /** true for anycast addresses. The API includes the key only when it is true and omits it otherwise. */ is_anycast?: boolean; - city?: string; - region?: string; - region_code?: string; + /** Null, like region, region_code, region_type and postal, when the location is unknown (for example anycast). */ + city?: string | null; + region?: string | null; + region_code?: string | null; + /** Type of the administrative region named by `region`. */ + region_type?: string | null; country_name: string; country_code: string; continent_name: string; continent_code: string; latitude: number; longitude: number; - postal?: string; + postal?: string | null; calling_code: string; flag: string; emoji_flag: string; @@ -183,15 +201,18 @@ export interface LookupResponse { native: string; plural: string; }; + /** Every time_zone value is null when the location is unknown. */ time_zone: { - name: string; - abbr: string; - offset: string; - is_dst: boolean; - current_time: string; + name: string | null; + abbr: string | null; + offset: string | null; + is_dst: boolean | null; + current_time: string | null; }; threat: { is_tor: boolean; + /** true when the address belongs to a VPN provider. */ + is_vpn?: boolean; is_proxy: boolean; is_anonymous: boolean; is_known_attacker: boolean; @@ -211,8 +232,16 @@ export interface LookupResponse { /** Number of days the address has been seen on this list. Omitted together with the dates. */ days_seen?: number; }[]; + /** Risk scores between 0 and 100. */ + scores?: { + vpn_score: number; + proxy_score: number; + threat_score: number; + trust_score: number; + }; }; - count: number; + /** Request count for the API key, returned by the API as a string (for example "0"). */ + count: string; status: number; } @@ -260,7 +289,9 @@ export class IPData { } private async request(url: URL, init?: RequestInit): Promise { - const options: RequestInit = { ...init, dispatcher: this.dispatcher }; + const headers = new Headers(init?.headers); + headers.set('User-Agent', USER_AGENT); + const options: RequestInit = { ...init, headers, dispatcher: this.dispatcher }; try { return await fetch(url.toString(), options); @@ -310,23 +341,34 @@ export class IPData { throw new Error(`${ip} is an invalid IP address.`); } - if (this.cache.has(ip || DEFAULT_IP)) { - return this.cache.get(ip || DEFAULT_IP)!; - } - if (selectField && fields) { throw new Error('The selectField and fields parameters cannot be used at the same time.'); } + if (selectField) { + validateSelectField(selectField); + } + + if (fields) { + validateFields(fields); + } + + const key = cacheKey(ip, selectField, fields); + const cached = this.cache.get(key); + + if (cached) { + return cached; + } + let urlPath = ip || ''; - if (selectField && isValidSelectField(selectField)) { + if (selectField) { urlPath = urlPath ? `${urlPath}/${selectField}` : selectField; } const url = this.buildUrl(urlPath); - if (fields && isValidFields(fields)) { + if (fields) { url.searchParams.set('fields', fields.join(',')); } @@ -346,8 +388,8 @@ export class IPData { data = { ...(responseData as Record), status: response.status } as LookupResponse; } - this.cache.set(ip || DEFAULT_IP, data); - return this.cache.get(ip || DEFAULT_IP)!; + this.cache.set(key, data); + return data; } async bulkLookup(ips: string[], fields?: string[]): Promise { @@ -358,22 +400,24 @@ export class IPData { throw new Error('Bulk Lookup requires more than 1 IP Address in the payload.'); } + if (fields) { + validateFields(fields); + } + ips.forEach((ip) => { if (!isValidIP(ip)) { throw new Error(`${ip} is an invalid IP address.`); } - if (this.cache.has(ip)) { - responses.push(this.cache.get(ip)!); + const cached = this.cache.get(cacheKey(ip, undefined, fields)); + + if (cached) { + responses.push(cached); } else { bulk.push(ip); } }); - if (fields && isValidFields(fields)) { - // validation only — fields are passed as query params below - } - if (bulk.length > 0) { const url = this.buildUrl('bulk'); @@ -394,8 +438,14 @@ export class IPData { const responseData = (await response.json()) as LookupResponse[]; responseData.forEach((info) => { - this.cache.set(info.ip, { ...info, status: response.status }); - responses.push(this.cache.get(info.ip)!); + const data: LookupResponse = { ...info, status: response.status }; + + // Entries are cached by address, which a fields selection without "ip" leaves out of the response. + if (typeof info.ip === 'string') { + this.cache.set(cacheKey(info.ip, undefined, fields), data); + } + + responses.push(data); }); }