@prosopo/ipinfo 0.2.40 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +12 -10
  2. package/.turbo/turbo-build$colon$tsc.log +10 -10
  3. package/.turbo/turbo-build.log +14 -11
  4. package/CHANGELOG.md +19 -0
  5. package/dist/IpInfoService.d.ts +8 -1
  6. package/dist/IpInfoService.d.ts.map +1 -1
  7. package/dist/IpInfoService.js +77 -84
  8. package/dist/IpInfoService.js.map +1 -1
  9. package/dist/_virtual/_rolldown/runtime.js +3 -0
  10. package/dist/backends/ipapi.d.ts +6 -0
  11. package/dist/backends/ipapi.d.ts.map +1 -1
  12. package/dist/backends/ipapi.js +117 -104
  13. package/dist/backends/ipapi.js.map +1 -1
  14. package/dist/backends/maxmind.d.ts +4 -0
  15. package/dist/backends/maxmind.d.ts.map +1 -1
  16. package/dist/backends/maxmind.js +126 -145
  17. package/dist/backends/maxmind.js.map +1 -1
  18. package/dist/cjs/IpInfoService.cjs +80 -86
  19. package/dist/cjs/backends/ipapi.cjs +118 -104
  20. package/dist/cjs/backends/maxmind.cjs +124 -165
  21. package/dist/cjs/index.cjs +7 -3
  22. package/dist/index.d.ts +6 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +5 -4
  25. package/dist/index.js.map +1 -1
  26. package/dist/tests/ipInfoService.unit.test.d.ts +2 -0
  27. package/dist/tests/ipInfoService.unit.test.d.ts.map +1 -0
  28. package/dist/tests/ipInfoService.unit.test.js +244 -0
  29. package/dist/tests/ipInfoService.unit.test.js.map +1 -0
  30. package/dist/tests/ipapi.unit.test.d.ts +2 -0
  31. package/dist/tests/ipapi.unit.test.d.ts.map +1 -0
  32. package/dist/tests/ipapi.unit.test.js +337 -0
  33. package/dist/tests/ipapi.unit.test.js.map +1 -0
  34. package/dist/tests/ipinfo.test-d.d.ts +2 -0
  35. package/dist/tests/ipinfo.test-d.d.ts.map +1 -0
  36. package/dist/tests/ipinfo.test-d.js +97 -0
  37. package/dist/tests/ipinfo.test-d.js.map +1 -0
  38. package/dist/tests/maxmind.unit.test.d.ts +2 -0
  39. package/dist/tests/maxmind.unit.test.d.ts.map +1 -0
  40. package/dist/tests/maxmind.unit.test.js +391 -0
  41. package/dist/tests/maxmind.unit.test.js.map +1 -0
  42. package/package.json +10 -8
  43. package/src/IpInfoService.ts +19 -2
  44. package/src/backends/ipapi.ts +57 -10
  45. package/src/backends/maxmind.ts +22 -4
  46. package/src/index.ts +9 -1
  47. package/src/tests/ipInfoService.unit.test.ts +422 -0
  48. package/src/tests/ipapi.unit.test.ts +509 -0
  49. package/src/tests/ipinfo.test-d.ts +189 -0
  50. package/src/tests/maxmind.unit.test.ts +562 -0
  51. package/tsconfig.tsbuildinfo +1 -1
  52. package/vite.test.config.ts +18 -0
@@ -0,0 +1,97 @@
1
+ import { assertType, describe, expectTypeOf, it } from "vitest";
2
+ import { parseAbuserScore } from "../backends/ipapi.js";
3
+ import { IpInfoService, IpapiBackend, MaxMindBackend, isNonRoutable, } from "../index.js";
4
+ describe("IpInfoService", () => {
5
+ it("satisfies the interface it claims to implement", () => {
6
+ expectTypeOf().toMatchTypeOf();
7
+ });
8
+ it("takes a config and optional injected backends", () => {
9
+ expectTypeOf(IpInfoService).toBeConstructibleWith({});
10
+ expectTypeOf(IpInfoService).toBeConstructibleWith({}, {});
11
+ expectTypeOf(IpInfoService).toBeConstructibleWith({ ipapiUrl: "https://api.ipapi.is" }, { maxmind: null, ipapi: null });
12
+ });
13
+ it("resolves lookup to the shared response union, never a bare result", () => {
14
+ expectTypeOf().returns.toEqualTypeOf();
15
+ });
16
+ it("exposes initialize and isAvailable with no arguments", () => {
17
+ expectTypeOf().parameters.toEqualTypeOf();
18
+ expectTypeOf().returns.toEqualTypeOf();
19
+ expectTypeOf().returns.toEqualTypeOf();
20
+ });
21
+ it("keeps every config field optional", () => {
22
+ assertType({});
23
+ });
24
+ it("rejects an unknown config key", () => {
25
+ assertType({ maxmindDbPath: "/city.mmdb" });
26
+ });
27
+ it("rejects a config value of the wrong type", () => {
28
+ assertType({ ipapiUrl: new URL("https://x.test") });
29
+ });
30
+ });
31
+ describe("IpInfoBackends", () => {
32
+ it("accepts null for a backend that is deliberately absent", () => {
33
+ assertType({ maxmind: null, ipapi: null });
34
+ assertType({});
35
+ });
36
+ it("does not accept an arbitrary object as a backend", () => {
37
+ assertType({ maxmind: { lookup: () => undefined } });
38
+ });
39
+ });
40
+ describe("isNonRoutable", () => {
41
+ it("takes exactly one string and returns a boolean", () => {
42
+ expectTypeOf(isNonRoutable).parameters.toEqualTypeOf();
43
+ expectTypeOf(isNonRoutable).returns.toEqualTypeOf();
44
+ });
45
+ });
46
+ describe("parseAbuserScore", () => {
47
+ it("accepts the optional upstream field", () => {
48
+ expectTypeOf(parseAbuserScore).parameters.toEqualTypeOf();
49
+ });
50
+ it("returns an optional number, so callers must handle 'unknown'", () => {
51
+ expectTypeOf(parseAbuserScore).returns.toEqualTypeOf();
52
+ expectTypeOf(parseAbuserScore(undefined)).toEqualTypeOf();
53
+ });
54
+ });
55
+ describe("FetchFn", () => {
56
+ it("matches the global fetch closely enough to be its default", () => {
57
+ expectTypeOf().toMatchTypeOf();
58
+ });
59
+ it("returns the global Response, not a hand-rolled shape", () => {
60
+ expectTypeOf().returns.toEqualTypeOf();
61
+ });
62
+ it("takes a url and an init", () => {
63
+ expectTypeOf().parameters.toEqualTypeOf();
64
+ });
65
+ });
66
+ describe("OpenReader", () => {
67
+ it("takes a path and resolves to a reader", () => {
68
+ expectTypeOf().parameters.toEqualTypeOf();
69
+ expectTypeOf().returns.toMatchTypeOf();
70
+ });
71
+ });
72
+ describe("backend constructors", () => {
73
+ it("require a base url for ipapi and accept the injection seams", () => {
74
+ expectTypeOf(IpapiBackend).toBeConstructibleWith({
75
+ baseUrl: "https://x.test",
76
+ });
77
+ expectTypeOf(IpapiBackend).toBeConstructibleWith({
78
+ baseUrl: "https://x.test",
79
+ apiKey: "k",
80
+ timeoutMs: 100,
81
+ fetch: async () => new Response(),
82
+ });
83
+ });
84
+ it("leave every MaxMind option optional", () => {
85
+ expectTypeOf(MaxMindBackend).toBeConstructibleWith({});
86
+ expectTypeOf(MaxMindBackend).toBeConstructibleWith({
87
+ cityDbPath: "/city.mmdb",
88
+ asnDbPath: "/asn.mmdb",
89
+ });
90
+ });
91
+ it("give both backends the same lookup signature as the service", () => {
92
+ expectTypeOf().parameters.toEqualTypeOf();
93
+ expectTypeOf().parameters.toEqualTypeOf();
94
+ expectTypeOf().returns.toEqualTypeOf();
95
+ });
96
+ });
97
+ //# sourceMappingURL=ipinfo.test-d.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ipinfo.test-d.js","sourceRoot":"","sources":["../../src/tests/ipinfo.test-d.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=maxmind.unit.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"maxmind.unit.test.d.ts","sourceRoot":"","sources":["../../src/tests/maxmind.unit.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,391 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { MaxMindBackend, } from "../backends/maxmind.js";
3
+ const IP = "8.8.8.8";
4
+ const CITY_DB = "/dbs/GeoLite2-City.mmdb";
5
+ const ASN_DB = "/dbs/GeoLite2-ASN.mmdb";
6
+ const reader = (behaviour) => {
7
+ const notSupported = () => {
8
+ throw new Error("database does not support this lookup");
9
+ };
10
+ return {
11
+ city: behaviour.city ?? notSupported,
12
+ asn: behaviour.asn ?? notSupported,
13
+ };
14
+ };
15
+ const cityData = (overrides = {}) => ({
16
+ country: { isoCode: "US", names: { en: "United States" } },
17
+ city: { names: { en: "Mountain View" } },
18
+ subdivisions: [{ names: { en: "California" } }],
19
+ location: {
20
+ latitude: 37.4,
21
+ longitude: -122.1,
22
+ timeZone: "America/Los_Angeles",
23
+ },
24
+ traits: {},
25
+ ...overrides,
26
+ });
27
+ const asnData = (overrides = {}) => ({
28
+ autonomousSystemNumber: 15169,
29
+ autonomousSystemOrganization: "Google LLC",
30
+ ...overrides,
31
+ });
32
+ const opens = (readers) => {
33
+ return async (dbPath) => {
34
+ const model = dbPath === CITY_DB ? readers.city : readers.asn;
35
+ if (undefined === model) {
36
+ throw new Error(`no database at ${dbPath}`);
37
+ }
38
+ return model;
39
+ };
40
+ };
41
+ const expectValid = (response) => {
42
+ if (!response.isValid) {
43
+ throw new Error(`expected a valid result, got: ${response.error}`);
44
+ }
45
+ return response;
46
+ };
47
+ describe("MaxMindBackend.initialize", () => {
48
+ it("opens only the databases that are configured", async () => {
49
+ const openReader = vi.fn(opens({ city: reader({ city: () => cityData() }) }));
50
+ const backend = new MaxMindBackend({ cityDbPath: CITY_DB, openReader });
51
+ await backend.initialize();
52
+ expect(openReader).toHaveBeenCalledTimes(1);
53
+ expect(openReader).toHaveBeenCalledWith(CITY_DB);
54
+ });
55
+ it("opens neither database when no paths are configured", async () => {
56
+ const openReader = vi.fn(opens({}));
57
+ await new MaxMindBackend({ openReader }).initialize();
58
+ expect(openReader).not.toHaveBeenCalled();
59
+ });
60
+ it("logs the readers it did open", async () => {
61
+ const info = vi.fn();
62
+ await new MaxMindBackend({
63
+ cityDbPath: CITY_DB,
64
+ asnDbPath: ASN_DB,
65
+ openReader: opens({
66
+ city: reader({ city: () => cityData() }),
67
+ asn: reader({ asn: () => asnData() }),
68
+ }),
69
+ logger: { info, warn: vi.fn(), debug: vi.fn() },
70
+ }).initialize();
71
+ const messages = info.mock.calls.map((call) => call[0]().msg);
72
+ expect(messages).toEqual([
73
+ "MaxMind City reader initialized",
74
+ "MaxMind ASN reader initialized",
75
+ ]);
76
+ });
77
+ it("keeps the ASN database when the City database fails to open", async () => {
78
+ const backend = new MaxMindBackend({
79
+ cityDbPath: CITY_DB,
80
+ asnDbPath: ASN_DB,
81
+ openReader: opens({ asn: reader({ asn: () => asnData() }) }),
82
+ });
83
+ await backend.initialize();
84
+ expect(backend.isAvailable()).toBe(true);
85
+ });
86
+ it("does not throw when every database fails to open", async () => {
87
+ const backend = new MaxMindBackend({
88
+ cityDbPath: CITY_DB,
89
+ asnDbPath: ASN_DB,
90
+ openReader: opens({}),
91
+ });
92
+ await expect(backend.initialize()).resolves.toBeUndefined();
93
+ expect(backend.isAvailable()).toBe(false);
94
+ });
95
+ it("logs a warning for each database it could not open", async () => {
96
+ const warn = vi.fn();
97
+ const backend = new MaxMindBackend({
98
+ cityDbPath: CITY_DB,
99
+ asnDbPath: ASN_DB,
100
+ openReader: opens({}),
101
+ logger: { warn, info: vi.fn(), debug: vi.fn() },
102
+ });
103
+ await backend.initialize();
104
+ expect(warn).toHaveBeenCalledTimes(2);
105
+ const messages = warn.mock.calls.map((call) => call[0]().msg);
106
+ expect(messages).toEqual([
107
+ "Failed to initialize MaxMind City reader",
108
+ "Failed to initialize MaxMind ASN reader",
109
+ ]);
110
+ });
111
+ it("is safe to call twice", async () => {
112
+ const backend = new MaxMindBackend({
113
+ cityDbPath: CITY_DB,
114
+ openReader: opens({ city: reader({ city: () => cityData() }) }),
115
+ });
116
+ await backend.initialize();
117
+ await backend.initialize();
118
+ expect(backend.isAvailable()).toBe(true);
119
+ });
120
+ });
121
+ describe("MaxMindBackend.isAvailable", () => {
122
+ it("is unavailable before initialize, even when paths are configured", async () => {
123
+ const backend = new MaxMindBackend({
124
+ cityDbPath: CITY_DB,
125
+ openReader: opens({ city: reader({ city: () => cityData() }) }),
126
+ });
127
+ expect(backend.isAvailable()).toBe(false);
128
+ await backend.initialize();
129
+ expect(backend.isAvailable()).toBe(true);
130
+ });
131
+ });
132
+ describe("MaxMindBackend.lookup", () => {
133
+ const initialised = async (readers) => {
134
+ const backend = new MaxMindBackend({
135
+ cityDbPath: readers.city ? CITY_DB : undefined,
136
+ asnDbPath: readers.asn ? ASN_DB : undefined,
137
+ openReader: opens(readers),
138
+ });
139
+ await backend.initialize();
140
+ return backend;
141
+ };
142
+ it("refuses to look up before the databases are open", async () => {
143
+ const backend = new MaxMindBackend({ cityDbPath: CITY_DB });
144
+ await expect(backend.lookup(IP)).resolves.toEqual({
145
+ isValid: false,
146
+ error: "MaxMind readers not initialized",
147
+ ip: IP,
148
+ });
149
+ });
150
+ it("maps city data onto the shared result shape", async () => {
151
+ const backend = await initialised({
152
+ city: reader({ city: () => cityData() }),
153
+ });
154
+ const result = expectValid(await backend.lookup(IP));
155
+ expect(result).toMatchObject({
156
+ ip: IP,
157
+ isValid: true,
158
+ country: "United States",
159
+ countryCode: "US",
160
+ region: "California",
161
+ city: "Mountain View",
162
+ latitude: 37.4,
163
+ longitude: -122.1,
164
+ timezone: "America/Los_Angeles",
165
+ });
166
+ });
167
+ it("reports threat indicators the free databases never populate as false", async () => {
168
+ const backend = await initialised({
169
+ city: reader({ city: () => cityData() }),
170
+ });
171
+ const result = expectValid(await backend.lookup(IP));
172
+ expect(result.isVPN).toBe(false);
173
+ expect(result.isTor).toBe(false);
174
+ expect(result.isProxy).toBe(false);
175
+ expect(result.isAbuser).toBe(false);
176
+ expect(result.isMobile).toBe(false);
177
+ expect(result.isCrawler).toBe(false);
178
+ });
179
+ it("reads the anonymity traits when a paid database supplies them", async () => {
180
+ const backend = await initialised({
181
+ city: reader({
182
+ city: () => cityData({
183
+ traits: {
184
+ isAnonymousVpn: true,
185
+ isTorExitNode: true,
186
+ isHostingProvider: true,
187
+ isSatelliteProvider: true,
188
+ },
189
+ }),
190
+ }),
191
+ });
192
+ const result = expectValid(await backend.lookup(IP));
193
+ expect(result.isVPN).toBe(true);
194
+ expect(result.isTor).toBe(true);
195
+ expect(result.isDatacenter).toBe(true);
196
+ expect(result.isSatellite).toBe(true);
197
+ });
198
+ it("treats a residential proxy as a proxy", async () => {
199
+ const residential = await initialised({
200
+ city: reader({
201
+ city: () => cityData({ traits: { isResidentialProxy: true } }),
202
+ }),
203
+ });
204
+ const publicProxy = await initialised({
205
+ city: reader({
206
+ city: () => cityData({ traits: { isPublicProxy: true } }),
207
+ }),
208
+ });
209
+ expect(expectValid(await residential.lookup(IP)).isProxy).toBe(true);
210
+ expect(expectValid(await publicProxy.lookup(IP)).isProxy).toBe(true);
211
+ });
212
+ it("falls back to the ASN database for the AS number", async () => {
213
+ const backend = await initialised({
214
+ city: reader({ city: () => cityData() }),
215
+ asn: reader({ asn: () => asnData() }),
216
+ });
217
+ const result = expectValid(await backend.lookup(IP));
218
+ expect(result.asnNumber).toBe(15169);
219
+ expect(result.asnOrganization).toBe("Google LLC");
220
+ });
221
+ it("prefers the City database's ASN traits over the ASN database", async () => {
222
+ const backend = await initialised({
223
+ city: reader({
224
+ city: () => cityData({
225
+ traits: {
226
+ autonomousSystemNumber: 111,
227
+ autonomousSystemOrganization: "From City",
228
+ },
229
+ }),
230
+ }),
231
+ asn: reader({ asn: () => asnData({ autonomousSystemNumber: 222 }) }),
232
+ });
233
+ const result = expectValid(await backend.lookup(IP));
234
+ expect(result.asnNumber).toBe(111);
235
+ expect(result.asnOrganization).toBe("From City");
236
+ });
237
+ it("returns ASN-only data when no City database is configured", async () => {
238
+ const backend = await initialised({
239
+ asn: reader({ asn: () => asnData() }),
240
+ });
241
+ const result = expectValid(await backend.lookup(IP));
242
+ expect(result.asnNumber).toBe(15169);
243
+ expect(result.country).toBeUndefined();
244
+ });
245
+ it("reports no data when the IP is in neither database", async () => {
246
+ const missing = () => {
247
+ throw new Error("address not found");
248
+ };
249
+ const backend = await initialised({
250
+ city: reader({ city: missing }),
251
+ asn: reader({ asn: missing }),
252
+ });
253
+ await expect(backend.lookup(IP)).resolves.toEqual({
254
+ isValid: false,
255
+ error: "No MaxMind data available for IP",
256
+ ip: IP,
257
+ });
258
+ });
259
+ it("still answers from the ASN database when the City lookup throws", async () => {
260
+ const backend = await initialised({
261
+ city: reader({
262
+ city: () => {
263
+ throw new Error("address not found");
264
+ },
265
+ }),
266
+ asn: reader({ asn: () => asnData() }),
267
+ });
268
+ const result = expectValid(await backend.lookup(IP));
269
+ expect(result.asnNumber).toBe(15169);
270
+ expect(result.country).toBeUndefined();
271
+ });
272
+ it("maps a hosting user type onto the provider type", async () => {
273
+ const backend = await initialised({
274
+ city: reader({
275
+ city: () => cityData({ traits: { userType: "hosting" } }),
276
+ }),
277
+ });
278
+ expect(expectValid(await backend.lookup(IP)).providerType).toBe("hosting");
279
+ });
280
+ it("maps every user type MaxMind can emit", async () => {
281
+ const forUserType = async (userType) => {
282
+ const backend = await initialised({
283
+ city: reader({
284
+ city: () => cityData({ traits: { userType } }),
285
+ }),
286
+ });
287
+ return expectValid(await backend.lookup(IP)).providerType;
288
+ };
289
+ const expected = {
290
+ hosting: "hosting",
291
+ content_delivery_network: "hosting",
292
+ college: "education",
293
+ school: "education",
294
+ library: "education",
295
+ government: "government",
296
+ military: "government",
297
+ business: "business",
298
+ residential: "isp",
299
+ cellular: "isp",
300
+ dialup: "isp",
301
+ cafe: "isp",
302
+ traveler: "isp",
303
+ router: "isp",
304
+ consumer_privacy_network: undefined,
305
+ search_engine_spider: undefined,
306
+ };
307
+ for (const [userType, providerType] of Object.entries(expected)) {
308
+ expect(await forUserType(userType), userType).toBe(providerType);
309
+ }
310
+ });
311
+ it("logs each failed database lookup with the IP and the cause", async () => {
312
+ const debug = vi.fn();
313
+ const missing = () => {
314
+ throw new Error("address not found");
315
+ };
316
+ const backend = new MaxMindBackend({
317
+ cityDbPath: CITY_DB,
318
+ asnDbPath: ASN_DB,
319
+ openReader: opens({
320
+ city: reader({ city: missing }),
321
+ asn: reader({ asn: missing }),
322
+ }),
323
+ logger: { debug, warn: vi.fn(), info: vi.fn() },
324
+ });
325
+ await backend.initialize();
326
+ await backend.lookup(IP);
327
+ expect(debug).toHaveBeenCalledTimes(2);
328
+ const payloads = debug.mock.calls.map((call) => call[0]());
329
+ expect(payloads.map((p) => p.msg)).toEqual([
330
+ "MaxMind City lookup failed",
331
+ "MaxMind ASN lookup failed",
332
+ ]);
333
+ expect(payloads.every((p) => p.data.ip === IP)).toBe(true);
334
+ });
335
+ it("leaves the provider type unset for a user type it does not map", async () => {
336
+ const backend = await initialised({
337
+ city: reader({
338
+ city: () => cityData({
339
+ traits: { userType: "search_engine_spider" },
340
+ }),
341
+ }),
342
+ });
343
+ expect(expectValid(await backend.lookup(IP)).providerType).toBeUndefined();
344
+ });
345
+ it("leaves the provider type unset when there is no user type at all", async () => {
346
+ const backend = await initialised({
347
+ city: reader({ city: () => cityData() }),
348
+ });
349
+ expect(expectValid(await backend.lookup(IP)).providerType).toBeUndefined();
350
+ });
351
+ it("turns a failure during mapping into an error response", async () => {
352
+ const hostile = {
353
+ get traits() {
354
+ throw new Error("corrupt record");
355
+ },
356
+ };
357
+ const backend = await initialised({
358
+ city: reader({ city: () => hostile }),
359
+ });
360
+ await expect(backend.lookup(IP)).resolves.toEqual({
361
+ isValid: false,
362
+ error: "MaxMind lookup error: corrupt record",
363
+ ip: IP,
364
+ });
365
+ });
366
+ it("describes a non-Error failure without printing [object Object]", async () => {
367
+ const hostile = {
368
+ get traits() {
369
+ throw "just a string";
370
+ },
371
+ };
372
+ const backend = await initialised({
373
+ city: reader({ city: () => hostile }),
374
+ });
375
+ const response = await backend.lookup(IP);
376
+ expect(response).toMatchObject({
377
+ isValid: false,
378
+ error: "MaxMind lookup error: just a string",
379
+ });
380
+ });
381
+ });
382
+ describe("MaxMindBackend default reader", () => {
383
+ it("does not throw when the real database file is missing", async () => {
384
+ const backend = new MaxMindBackend({
385
+ cityDbPath: "/nonexistent/prosopo-test/GeoLite2-City.mmdb",
386
+ });
387
+ await expect(backend.initialize()).resolves.toBeUndefined();
388
+ expect(backend.isAvailable()).toBe(false);
389
+ });
390
+ });
391
+ //# sourceMappingURL=maxmind.unit.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"maxmind.unit.test.js","sourceRoot":"","sources":["../../src/tests/maxmind.unit.test.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prosopo/ipinfo",
3
- "version": "0.2.40",
3
+ "version": "0.3.0",
4
4
  "description": "IP information service with MaxMind and ipapi.is backends",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -22,22 +22,24 @@
22
22
  "build:cross-env": "vite build --config vite.esm.config.ts",
23
23
  "build:tsc": "tsc --build --verbose",
24
24
  "build:cjs": "NODE_ENV=${NODE_ENV:-development}; vite build --config vite.cjs.config.ts --mode $NODE_ENV",
25
- "typecheck": "tsc --project tsconfig.types.json"
25
+ "typecheck": "tsc --project tsconfig.types.json",
26
+ "test": "NODE_ENV=${NODE_ENV:-test}; npx vitest run --config ./vite.test.config.ts",
27
+ "test:watch": "NODE_ENV=${NODE_ENV:-test}; npx vitest watch --config ./vite.test.config.ts"
26
28
  },
27
29
  "dependencies": {
28
30
  "@maxmind/geoip2-node": "5.0.0",
29
- "@prosopo/logger": "2.0.3",
30
- "@prosopo/types": "4.9.12"
31
+ "@prosopo/logger": "2.0.4",
32
+ "@prosopo/types": "4.10.0"
31
33
  },
32
34
  "devDependencies": {
33
- "@prosopo/config": "3.3.4",
35
+ "@prosopo/config": "3.3.5",
34
36
  "@types/node": "22.10.2",
35
- "@vitest/coverage-v8": "3.2.4",
37
+ "@vitest/coverage-v8": "4.1.10",
36
38
  "del-cli": "6.0.0",
37
39
  "tslib": "2.7.0",
38
40
  "typescript": "5.6.2",
39
- "vite": "6.4.1",
40
- "vitest": "3.2.4"
41
+ "vite": "8.1.5",
42
+ "vitest": "4.1.10"
41
43
  },
42
44
  "license": "Apache-2.0",
43
45
  "repository": {
@@ -21,7 +21,7 @@ import type { IIpInfoService, IpInfoServiceConfig } from "./types.js";
21
21
  * Returns true for loopback, link-local, and private-range IPs that will
22
22
  * never yield useful geolocation or threat data from any backend.
23
23
  */
24
- function isNonRoutable(ip: string): boolean {
24
+ export function isNonRoutable(ip: string): boolean {
25
25
  // Strip IPv4-mapped IPv6 prefix (::ffff:127.0.0.1 -> 127.0.0.1)
26
26
  const normalized = ip.replace(/^::ffff:/i, "");
27
27
 
@@ -54,14 +54,31 @@ function isNonRoutable(ip: string): boolean {
54
54
  return false;
55
55
  }
56
56
 
57
+ /**
58
+ * The two backends, injected so the routing and fallback logic above can be
59
+ * tested without a MaxMind database on disk or a network call. Both are
60
+ * optional: absent means "not configured", which is distinct from "configured
61
+ * but unavailable".
62
+ */
63
+ export interface IpInfoBackends {
64
+ maxmind?: MaxMindBackend | null;
65
+ ipapi?: IpapiBackend | null;
66
+ }
67
+
57
68
  export class IpInfoService implements IIpInfoService {
58
69
  private maxmindBackend: MaxMindBackend | null = null;
59
70
  private ipapiBackend: IpapiBackend | null = null;
60
71
  private config: IpInfoServiceConfig;
61
72
 
62
- constructor(config: IpInfoServiceConfig) {
73
+ constructor(config: IpInfoServiceConfig, backends?: IpInfoBackends) {
63
74
  this.config = config;
64
75
 
76
+ if (backends) {
77
+ this.maxmindBackend = backends.maxmind ?? null;
78
+ this.ipapiBackend = backends.ipapi ?? null;
79
+ return;
80
+ }
81
+
65
82
  if (config.maxmindCityDbPath || config.maxmindAsnDbPath) {
66
83
  this.maxmindBackend = new MaxMindBackend({
67
84
  cityDbPath: config.maxmindCityDbPath,