From c376970be63db96bbaa3a131e0a96ecafd023778 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 12:57:02 +0000 Subject: [PATCH 1/8] Key cache entries by address and requested fields The cache was keyed by IP address alone, so the result of lookup(ip, selectField) or lookup(ip, undefined, fields) was stored under the bare address and returned by a later full lookup of the same address, and a full result was returned for a later filtered lookup. bulkLookup() with fields stored filtered results the same way. Keys now include the selection: a full lookup keeps the address as its key, a single field is keyed as address/field and a field list as address?fields=a,b with the fields sorted, so the same selection in another order shares one entry. Bulk entries whose response lacks the ip field are returned but not cached, because they cannot be attributed to an address. Parameters are validated before the cache is consulted, and the empty if block around the fields validation in bulkLookup() is replaced by a plain call. Tests cover both directions for lookup() and bulkLookup(), and the README describes the keying. Co-Authored-By: Claude Fable 5.1 --- README.md | 2 + src/ipdata.test.ts | 136 +++++++++++++++++++++++++++++++++++++++++++++ src/ipdata.ts | 81 ++++++++++++++++++--------- 3 files changed, 193 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index a1c2ef7..ea5ebfb 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/ipdata.test.ts b/src/ipdata.test.ts index b737cea..79442e5 100644 --- a/src/ipdata.test.ts +++ b/src/ipdata.test.ts @@ -639,6 +639,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 = { @@ -879,6 +957,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'; diff --git a/src/ipdata.ts b/src/ipdata.ts index dae0455..88100af 100644 --- a/src/ipdata.ts +++ b/src/ipdata.ts @@ -45,29 +45,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; - return true; + if (selectField) { + return `${address}/${selectField}`; + } + + if (fields) { + return `${address}?fields=${[...fields].sort().join(',')}`; + } + + return address; } interface TransportError { @@ -310,23 +320,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 +367,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 +379,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 +417,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); }); } From 8d6364b5bf8b3941a27a2c00f6c718569ec341d1 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 12:57:03 +0000 Subject: [PATCH 2/8] Make the custom ttl cache tests inspect their own instance The two "should not cache the information" tests performed the lookup on an instance created with ttl: 1 but then read the cache of the shared instance, which was empty regardless, so they passed without checking anything. They now wait for the entry to expire, check the cache of the instance they used and verify that the next lookup is sent to the API again. Co-Authored-By: Claude Fable 5.1 --- src/ipdata.test.ts | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/ipdata.test.ts b/src/ipdata.test.ts index 79442e5..0db5532 100644 --- a/src/ipdata.test.ts +++ b/src/ipdata.test.ts @@ -275,6 +275,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 +571,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 +605,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); }); }); From 64996330b118657b14141794bdf6cc6c17aff9fc Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 12:57:03 +0000 Subject: [PATCH 3/8] Add is_vpn, scores, region_type and nullable fields to LookupResponse The API returns threat.is_vpn, threat.scores (vpn_score, proxy_score, threat_score and trust_score), a top-level region_type, count as a string, and null for city, region, region_code, region_type, postal and every time_zone value when an address has no known location. None of this was declared. LookupResponse now carries these fields: the new keys are optional, the location fields are nullable and count is typed as the string the API has always sent. The README documents them and the tests check them against the sample responses, including the compile-time types. Co-Authored-By: Claude Fable 5.1 --- README.md | 10 +++++++++- src/ipdata.test.ts | 31 ++++++++++++++++++++++++++++++- src/ipdata.ts | 34 ++++++++++++++++++++++++---------- 3 files changed, 63 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index ea5ebfb..071b34b 100644 --- a/README.md +++ b/README.md @@ -209,7 +209,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 }`: @@ -219,6 +219,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/src/ipdata.test.ts b/src/ipdata.test.ts index 0db5532..2d08563 100644 --- a/src/ipdata.test.ts +++ b/src/ipdata.test.ts @@ -50,7 +50,7 @@ const MOCK_IP1_DATA = { is_datacenter: true, blocklists: [], }, - count: 0, + count: '0', }; const MOCK_IP2_DATA = { @@ -883,6 +883,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()', () => { diff --git a/src/ipdata.ts b/src/ipdata.ts index 88100af..af5afaf 100644 --- a/src/ipdata.ts +++ b/src/ipdata.ts @@ -153,16 +153,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; @@ -193,15 +196,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; @@ -221,8 +227,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; } From 36eed14878ad5a734a002f65b5669d455494bc41 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 12:57:04 +0000 Subject: [PATCH 4/8] Send an ipdata-node/ User-Agent header Requests went out with undici's default User-Agent. Every request now identifies the library as ipdata-node/, with the version read from package.json at runtime; the file ships with every install, so no build step or additional dependency is needed. Tests match the header on the GET of lookup() and the POST of bulkLookup(). Co-Authored-By: Claude Fable 5.1 --- README.md | 2 ++ src/ipdata.test.ts | 38 ++++++++++++++++++++++++++++++++++++++ src/ipdata.ts | 11 +++++++++-- 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 071b34b..0f2474f 100644 --- a/README.md +++ b/README.md @@ -193,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`: diff --git a/src/ipdata.test.ts b/src/ipdata.test.ts index 2d08563..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'; @@ -1099,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 af5afaf..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', @@ -284,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); From 2f9278c7ef3381d5708adf99225af85c5e071c1d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 12:57:04 +0000 Subject: [PATCH 5/8] Point the README badge at the Lint and Test workflow The badge referenced a workflow named CI, which does not exist, so it rendered as broken. It now uses the badge of .github/workflows/default.yml and links to that workflow's runs. Co-Authored-By: Claude Fable 5.1 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0f2474f..2cef9a1 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. From f98de26c29a3deeaef449930c085f32d504a46d0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 12:57:04 +0000 Subject: [PATCH 6/8] Document HTTP/2 through a proxy dispatcher undici's ProxyAgent and EnvHttpProxyAgent take the ALPN protocols of the connection tunnelled through the proxy from requestTls, so negotiating HTTP/2 with the API needs requestTls: { allowH2: true } next to allowH2: true. The README dispatcher section says so and the proxy example shows it. Co-Authored-By: Claude Fable 5.1 --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2cef9a1..551be0a 100644 --- a/README.md +++ b/README.md @@ -168,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 From f8e8e69da633511c0549d3fc3985984c035a7888 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 12:57:04 +0000 Subject: [PATCH 7/8] Make npm run lint check only The lint script ran eslint --fix, so linting rewrote files in place, also in CI where the result is discarded. npm run lint now reports problems without touching files, which is what both workflows need, and npm run lint:fix applies the automatic fixes locally. Co-Authored-By: Claude Fable 5.1 --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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": { From e9d77666b74d354d6aa57005a51e5b53e15dd7c3 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 3 Nov 2026 13:01:27 +0000 Subject: [PATCH 8/8] Update development dependencies flagged by npm audit npm audit fix refreshes the transitive development dependencies that have fixed releases within the ranges package.json already allows: @babel/core and its helpers, baseline-browser-mapping, brace-expansion, browserslist, flatted, handlebars, js-yaml, minimatch 3.1.5 and 9.0.9, picomatch and ts-jest, among others. This clears every high and critical advisory; package.json is unchanged. Two advisories have no fix compatible with jest 29 and eslint 8: deepmerge (through jest-config) and the remaining minimatch report, which is only fixed in minimatch 11. npm lists every package that depends on them, which is why the report now shows more, lower severity entries than before. Co-Authored-By: Claude Fable 5.1 --- package-lock.json | 431 +++++++++++++++++++++------------------------- 1 file changed, 198 insertions(+), 233 deletions(-) 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": [ {