@prosopo/ipinfo 0.4.13 → 0.4.15
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.
- package/.turbo/turbo-build$colon$cjs.log +2 -5
- package/.turbo/turbo-build$colon$tsc.log +10 -13
- package/.turbo/turbo-build.log +3 -9
- package/CHANGELOG.md +20 -0
- package/LICENSE +201 -0
- package/package.json +48 -49
- package/src/IpInfoService.ts +0 -185
- package/src/backends/ipapi.ts +0 -196
- package/src/backends/maxmind.ts +0 -336
- package/src/index.ts +0 -24
- package/src/tests/ipInfoService.unit.test.ts +0 -481
- package/src/tests/ipapi.unit.test.ts +0 -509
- package/src/tests/ipinfo.test-d.ts +0 -189
- package/src/tests/maxmind.unit.test.ts +0 -801
- package/src/types.ts +0 -36
- package/tsconfig.cjs.json +0 -24
- package/tsconfig.json +0 -25
- package/tsconfig.tsbuildinfo +0 -1
- package/tsconfig.types.json +0 -9
package/src/backends/ipapi.ts
DELETED
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
|
|
15
|
-
import type { Logger } from "@prosopo/logger";
|
|
16
|
-
import type {
|
|
17
|
-
IPApiResponse,
|
|
18
|
-
IPInfoResponse,
|
|
19
|
-
IPInfoResult,
|
|
20
|
-
} from "@prosopo/types";
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Default per-lookup budget. Short on purpose: an IP lookup sits in the request
|
|
24
|
-
* path, so a slow backend must not hold up the decision that depends on it.
|
|
25
|
-
*/
|
|
26
|
-
export const DEFAULT_TIMEOUT_MS = 700;
|
|
27
|
-
|
|
28
|
-
/** The subset of global fetch this backend uses. Injected so tests need no network. */
|
|
29
|
-
export type FetchFn = (
|
|
30
|
-
url: string,
|
|
31
|
-
init: RequestInit,
|
|
32
|
-
) => Promise<globalThis.Response>;
|
|
33
|
-
|
|
34
|
-
export interface IpapiBackendConfig {
|
|
35
|
-
baseUrl: string;
|
|
36
|
-
apiKey?: string;
|
|
37
|
-
logger?: Logger;
|
|
38
|
-
/** Overridden in tests; defaults to the global fetch. */
|
|
39
|
-
fetch?: FetchFn;
|
|
40
|
-
/** Overridden in tests and tunable in deployment; defaults to DEFAULT_TIMEOUT_MS. */
|
|
41
|
-
timeoutMs?: number;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Parse an upstream abuser score, which arrives as a string like "0.0012 (Low)".
|
|
46
|
-
*
|
|
47
|
-
* The field is declared required by the response type but is not guaranteed by
|
|
48
|
-
* the wire: it comes from `response.json()`, which is cast, not validated. A
|
|
49
|
-
* missing value used to throw, and the throw was caught far above as a generic
|
|
50
|
-
* "Network or parsing error" — discarding an otherwise complete and successful
|
|
51
|
-
* lookup over one absent score.
|
|
52
|
-
*
|
|
53
|
-
* Returns `undefined` — not 0 — when the field is absent or unparseable. The
|
|
54
|
-
* scale is 0..1 with 0 meaning "clean" (see `abuserScoreThreshold`, declared
|
|
55
|
-
* `{min: 0, max: 1}`), so a literal 0 would assert cleanliness we have not
|
|
56
|
-
* established. `undefined` says "unknown" instead, and the one consumer
|
|
57
|
-
* (checkTrafficFilter) already resolves it with `?? 0`, so the effective
|
|
58
|
-
* blocking behaviour is unchanged while the distinction stays available.
|
|
59
|
-
*
|
|
60
|
-
* Fail-closed alternatives were rejected: 1 turns any upstream formatting
|
|
61
|
-
* change into a silent max-severity block indistinguishable from a genuine
|
|
62
|
-
* 1.0, and throwing reintroduces exactly the bug above — a cosmetic sub-field
|
|
63
|
-
* collapsing a good lookup into a total failure.
|
|
64
|
-
*/
|
|
65
|
-
export const parseAbuserScore = (
|
|
66
|
-
score: string | undefined,
|
|
67
|
-
): number | undefined => {
|
|
68
|
-
const head = score?.split(" ")[0];
|
|
69
|
-
if (!head) {
|
|
70
|
-
return undefined;
|
|
71
|
-
}
|
|
72
|
-
const parsed = Number.parseFloat(head);
|
|
73
|
-
return Number.isNaN(parsed) ? undefined : parsed;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
export class IpapiBackend {
|
|
77
|
-
private config: IpapiBackendConfig;
|
|
78
|
-
|
|
79
|
-
constructor(config: IpapiBackendConfig) {
|
|
80
|
-
this.config = config;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
isAvailable(): boolean {
|
|
84
|
-
return Boolean(this.config.baseUrl);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
private get timeoutMs(): number {
|
|
88
|
-
return this.config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async lookup(ip: string): Promise<IPInfoResponse> {
|
|
92
|
-
try {
|
|
93
|
-
if (!ip || typeof ip !== "string") {
|
|
94
|
-
return {
|
|
95
|
-
isValid: false,
|
|
96
|
-
error: "Invalid IP address provided",
|
|
97
|
-
ip: ip || "undefined",
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const body: { q: string; key?: string } = { q: ip };
|
|
102
|
-
if (this.config.apiKey) {
|
|
103
|
-
body.key = this.config.apiKey;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const controller = new AbortController();
|
|
107
|
-
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
108
|
-
|
|
109
|
-
try {
|
|
110
|
-
const doFetch: FetchFn = this.config.fetch ?? globalThis.fetch;
|
|
111
|
-
const response = await doFetch(this.config.baseUrl, {
|
|
112
|
-
method: "POST",
|
|
113
|
-
headers: {
|
|
114
|
-
"Content-Type": "application/json",
|
|
115
|
-
Accept: "application/json",
|
|
116
|
-
},
|
|
117
|
-
body: JSON.stringify(body),
|
|
118
|
-
signal: controller.signal,
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
clearTimeout(timeoutId);
|
|
122
|
-
|
|
123
|
-
if (!response.ok) {
|
|
124
|
-
return {
|
|
125
|
-
isValid: false,
|
|
126
|
-
error: `API request failed with status ${response.status}: ${response.statusText}`,
|
|
127
|
-
ip,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
const data: IPApiResponse = (await response.json()) as IPApiResponse;
|
|
132
|
-
|
|
133
|
-
if (data.is_bogon) {
|
|
134
|
-
return {
|
|
135
|
-
isValid: false,
|
|
136
|
-
error: "IP address is bogon (non-routable)",
|
|
137
|
-
ip,
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const result: IPInfoResult = {
|
|
142
|
-
ip: data.ip,
|
|
143
|
-
isValid: true,
|
|
144
|
-
|
|
145
|
-
isVPN: data.is_vpn,
|
|
146
|
-
isTor: data.is_tor,
|
|
147
|
-
isProxy: data.is_proxy,
|
|
148
|
-
isDatacenter: data.is_datacenter,
|
|
149
|
-
isAbuser: data.is_abuser,
|
|
150
|
-
isMobile: data.is_mobile,
|
|
151
|
-
isSatellite: data.is_satellite,
|
|
152
|
-
isCrawler: data.is_crawler,
|
|
153
|
-
providerName: data.company?.name || data.datacenter?.datacenter,
|
|
154
|
-
providerType: data.company?.type || data.asn?.type,
|
|
155
|
-
asnNumber: data.asn?.asn,
|
|
156
|
-
asnOrganization: data.asn?.org,
|
|
157
|
-
datacenterName: data.datacenter?.datacenter,
|
|
158
|
-
|
|
159
|
-
country: data.location?.country,
|
|
160
|
-
countryCode: data.location?.country_code,
|
|
161
|
-
region: data.location?.state,
|
|
162
|
-
city: data.location?.city,
|
|
163
|
-
latitude: data.location?.latitude,
|
|
164
|
-
longitude: data.location?.longitude,
|
|
165
|
-
timezone: data.location?.timezone,
|
|
166
|
-
|
|
167
|
-
vpnService: data.vpn?.service,
|
|
168
|
-
vpnType: data.vpn?.type,
|
|
169
|
-
|
|
170
|
-
abuserScore: parseAbuserScore(data.asn?.abuser_score),
|
|
171
|
-
companyAbuserScore: parseAbuserScore(data.company?.abuser_score),
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
return result;
|
|
175
|
-
} catch (fetchError) {
|
|
176
|
-
clearTimeout(timeoutId);
|
|
177
|
-
|
|
178
|
-
if (fetchError instanceof Error && fetchError.name === "AbortError") {
|
|
179
|
-
return {
|
|
180
|
-
isValid: false,
|
|
181
|
-
error: `Request timed out after ${this.timeoutMs}ms`,
|
|
182
|
-
ip,
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
throw fetchError;
|
|
187
|
-
}
|
|
188
|
-
} catch (error) {
|
|
189
|
-
return {
|
|
190
|
-
isValid: false,
|
|
191
|
-
error: `Network or parsing error: ${error instanceof Error ? error.message : String(error)}`,
|
|
192
|
-
ip,
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
package/src/backends/maxmind.ts
DELETED
|
@@ -1,336 +0,0 @@
|
|
|
1
|
-
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
|
|
15
|
-
import type { Asn, City, Country, ReaderModel } from "@maxmind/geoip2-node";
|
|
16
|
-
import type { Logger } from "@prosopo/logger";
|
|
17
|
-
import type { IPInfoResponse, IPInfoResult } from "@prosopo/types";
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Opens a MaxMind database file. Injected so tests can exercise initialisation
|
|
21
|
-
* and lookup without shipping a .mmdb fixture, and so a failure to open one can
|
|
22
|
-
* be simulated at all — the real Reader only fails on a genuinely bad file.
|
|
23
|
-
*/
|
|
24
|
-
export type OpenReader = (dbPath: string) => Promise<ReaderModel>;
|
|
25
|
-
|
|
26
|
-
const openReaderFromFile: OpenReader = async (
|
|
27
|
-
dbPath: string,
|
|
28
|
-
): Promise<ReaderModel> => {
|
|
29
|
-
// Imported lazily: the module pulls in native-ish decoding machinery that a
|
|
30
|
-
// deployment without MaxMind databases should never pay for.
|
|
31
|
-
const { Reader } = await import("@maxmind/geoip2-node");
|
|
32
|
-
return Reader.open(dbPath);
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export interface MaxMindBackendConfig {
|
|
36
|
-
cityDbPath?: string;
|
|
37
|
-
asnDbPath?: string;
|
|
38
|
-
logger?: Logger;
|
|
39
|
-
/** Overridden in tests; defaults to opening the file from disk. */
|
|
40
|
-
openReader?: OpenReader;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Which accessor the geo reader actually supports.
|
|
45
|
-
*
|
|
46
|
-
* `cityDbPath` is a path, not a promise of a City database — production points
|
|
47
|
-
* it at GeoLite2-Country.mmdb. `Reader.open()` accepts any valid .mmdb, so the
|
|
48
|
-
* reader opens and `isAvailable()` reports true, but `city()` checks
|
|
49
|
-
* `metadata.databaseType` and throws `BadMethodCallError` on every call. The
|
|
50
|
-
* result was a backend that claimed to be up and failed 100% of lookups —
|
|
51
|
-
* invisible until the ipapi.is sidecar it was meant to back up went down.
|
|
52
|
-
*
|
|
53
|
-
* `metadata` is not on `ReaderModel`'s public type, so the kind is latched
|
|
54
|
-
* lazily on the first `BadMethodCallError` rather than probed at open time.
|
|
55
|
-
* "unknown" only ever costs one extra throw for the process lifetime.
|
|
56
|
-
*/
|
|
57
|
-
type GeoReaderKind = "unknown" | "city" | "country";
|
|
58
|
-
|
|
59
|
-
/** Thrown by geoip2-node when an accessor doesn't match the database type. */
|
|
60
|
-
const BAD_METHOD_CALL_ERROR = "BadMethodCallError";
|
|
61
|
-
|
|
62
|
-
export class MaxMindBackend {
|
|
63
|
-
private cityReader: ReaderModel | null = null;
|
|
64
|
-
private asnReader: ReaderModel | null = null;
|
|
65
|
-
private geoReaderKind: GeoReaderKind = "unknown";
|
|
66
|
-
private config: MaxMindBackendConfig;
|
|
67
|
-
|
|
68
|
-
constructor(config: MaxMindBackendConfig) {
|
|
69
|
-
this.config = config;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async initialize(): Promise<void> {
|
|
73
|
-
const openReader: OpenReader = this.config.openReader ?? openReaderFromFile;
|
|
74
|
-
|
|
75
|
-
if (this.config.cityDbPath) {
|
|
76
|
-
try {
|
|
77
|
-
this.cityReader = await openReader(this.config.cityDbPath);
|
|
78
|
-
this.config.logger?.info(() => ({
|
|
79
|
-
msg: "MaxMind City reader initialized",
|
|
80
|
-
data: { dbPath: this.config.cityDbPath },
|
|
81
|
-
}));
|
|
82
|
-
} catch (error) {
|
|
83
|
-
this.config.logger?.warn(() => ({
|
|
84
|
-
msg: "Failed to initialize MaxMind City reader",
|
|
85
|
-
err: error,
|
|
86
|
-
data: { dbPath: this.config.cityDbPath },
|
|
87
|
-
}));
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (this.config.asnDbPath) {
|
|
92
|
-
try {
|
|
93
|
-
this.asnReader = await openReader(this.config.asnDbPath);
|
|
94
|
-
this.config.logger?.info(() => ({
|
|
95
|
-
msg: "MaxMind ASN reader initialized",
|
|
96
|
-
data: { dbPath: this.config.asnDbPath },
|
|
97
|
-
}));
|
|
98
|
-
} catch (error) {
|
|
99
|
-
this.config.logger?.warn(() => ({
|
|
100
|
-
msg: "Failed to initialize MaxMind ASN reader",
|
|
101
|
-
err: error,
|
|
102
|
-
data: { dbPath: this.config.asnDbPath },
|
|
103
|
-
}));
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
isAvailable(): boolean {
|
|
109
|
-
return this.cityReader !== null || this.asnReader !== null;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* ISO 3166-1 alpha-2 country code, or undefined when the reader has no
|
|
114
|
-
* answer. Synchronous and allocation-light: the .mmdb is memory-mapped, so
|
|
115
|
-
* this is a tree walk with no I/O and no network.
|
|
116
|
-
*
|
|
117
|
-
* Separate from `lookup()` because callers that only need the country
|
|
118
|
-
* should not pay for the ASN read, the threat-field assembly, or the
|
|
119
|
-
* `IPInfoResponse` object. The reader-kind latch is shared with `lookup()`
|
|
120
|
-
* — `country()` rejects a City database and `city()` rejects a Country one,
|
|
121
|
-
* so the accessor has to be chosen from the database's own metadata.
|
|
122
|
-
*/
|
|
123
|
-
countryCode(ip: string): string | undefined {
|
|
124
|
-
if (!this.cityReader) return undefined;
|
|
125
|
-
|
|
126
|
-
if (this.geoReaderKind !== "country") {
|
|
127
|
-
try {
|
|
128
|
-
const isoCode = this.cityReader.city(ip).country?.isoCode;
|
|
129
|
-
this.geoReaderKind = "city";
|
|
130
|
-
return isoCode;
|
|
131
|
-
} catch (error) {
|
|
132
|
-
if (!isBadMethodCall(error)) {
|
|
133
|
-
// Address not in the database, or an invalid address: no
|
|
134
|
-
// country, and nothing to latch.
|
|
135
|
-
return undefined;
|
|
136
|
-
}
|
|
137
|
-
this.geoReaderKind = "country";
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
try {
|
|
142
|
-
return this.cityReader.country(ip).country?.isoCode;
|
|
143
|
-
} catch {
|
|
144
|
-
return undefined;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async lookup(ip: string): Promise<IPInfoResponse> {
|
|
149
|
-
if (!this.isAvailable()) {
|
|
150
|
-
return {
|
|
151
|
-
isValid: false,
|
|
152
|
-
error: "MaxMind readers not initialized",
|
|
153
|
-
ip,
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
try {
|
|
158
|
-
let cityData: City | undefined;
|
|
159
|
-
let countryData: Country | undefined;
|
|
160
|
-
let asnData: Asn | undefined;
|
|
161
|
-
|
|
162
|
-
if (this.cityReader) {
|
|
163
|
-
if (this.geoReaderKind !== "country") {
|
|
164
|
-
try {
|
|
165
|
-
cityData = this.cityReader.city(ip);
|
|
166
|
-
this.geoReaderKind = "city";
|
|
167
|
-
} catch (error) {
|
|
168
|
-
if (isBadMethodCall(error)) {
|
|
169
|
-
// Not a City database. Latch so subsequent lookups go
|
|
170
|
-
// straight to country() instead of paying the throw.
|
|
171
|
-
this.geoReaderKind = "country";
|
|
172
|
-
this.config.logger?.warn(() => ({
|
|
173
|
-
msg: "MaxMind geo database is not a City database; falling back to country-level lookups",
|
|
174
|
-
data: { dbPath: this.config.cityDbPath },
|
|
175
|
-
err: error,
|
|
176
|
-
}));
|
|
177
|
-
} else {
|
|
178
|
-
this.config.logger?.debug(() => ({
|
|
179
|
-
msg: "MaxMind City lookup failed",
|
|
180
|
-
data: { ip },
|
|
181
|
-
err: error,
|
|
182
|
-
}));
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (this.geoReaderKind === "country") {
|
|
188
|
-
try {
|
|
189
|
-
countryData = this.cityReader.country(ip);
|
|
190
|
-
} catch (error) {
|
|
191
|
-
this.config.logger?.debug(() => ({
|
|
192
|
-
msg: "MaxMind Country lookup failed",
|
|
193
|
-
data: { ip },
|
|
194
|
-
err: error,
|
|
195
|
-
}));
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if (this.asnReader) {
|
|
201
|
-
try {
|
|
202
|
-
asnData = this.asnReader.asn(ip);
|
|
203
|
-
} catch (error) {
|
|
204
|
-
this.config.logger?.debug(() => ({
|
|
205
|
-
msg: "MaxMind ASN lookup failed",
|
|
206
|
-
data: { ip },
|
|
207
|
-
err: error,
|
|
208
|
-
}));
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// `City extends Country`, so everything below the city/subdivision/
|
|
213
|
-
// location fields reads off whichever one the database gave us.
|
|
214
|
-
const geoData: City | Country | undefined = cityData ?? countryData;
|
|
215
|
-
|
|
216
|
-
if (!geoData && !asnData) {
|
|
217
|
-
return {
|
|
218
|
-
isValid: false,
|
|
219
|
-
error: "No MaxMind data available for IP",
|
|
220
|
-
ip,
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const result: IPInfoResult = {
|
|
225
|
-
ip,
|
|
226
|
-
isValid: true,
|
|
227
|
-
|
|
228
|
-
// Threat indicators - GeoLite2 free DBs do not populate these
|
|
229
|
-
isVPN: geoData?.traits?.isAnonymousVpn ?? false,
|
|
230
|
-
isTor: geoData?.traits?.isTorExitNode ?? false,
|
|
231
|
-
isProxy:
|
|
232
|
-
(geoData?.traits?.isPublicProxy ?? false) ||
|
|
233
|
-
(geoData?.traits?.isResidentialProxy ?? false),
|
|
234
|
-
isDatacenter: geoData?.traits?.isHostingProvider ?? false,
|
|
235
|
-
isAbuser: false,
|
|
236
|
-
isMobile: false,
|
|
237
|
-
isSatellite: geoData?.traits?.isSatelliteProvider ?? false,
|
|
238
|
-
isCrawler: false,
|
|
239
|
-
|
|
240
|
-
// Country is available from both database types; the rest needs a
|
|
241
|
-
// City database and stays undefined on a Country-only one.
|
|
242
|
-
country: geoData?.country?.names?.en,
|
|
243
|
-
countryCode: geoData?.country?.isoCode,
|
|
244
|
-
region: cityData?.subdivisions?.[0]?.names?.en,
|
|
245
|
-
city: cityData?.city?.names?.en,
|
|
246
|
-
latitude: cityData?.location?.latitude,
|
|
247
|
-
longitude: cityData?.location?.longitude,
|
|
248
|
-
timezone: cityData?.location?.timeZone,
|
|
249
|
-
|
|
250
|
-
// ASN info - prefer geo DB traits, fall back to ASN DB
|
|
251
|
-
asnNumber:
|
|
252
|
-
geoData?.traits?.autonomousSystemNumber ??
|
|
253
|
-
asnData?.autonomousSystemNumber,
|
|
254
|
-
asnOrganization:
|
|
255
|
-
geoData?.traits?.autonomousSystemOrganization ??
|
|
256
|
-
asnData?.autonomousSystemOrganization,
|
|
257
|
-
|
|
258
|
-
// Provider info from ASN
|
|
259
|
-
providerName:
|
|
260
|
-
geoData?.traits?.autonomousSystemOrganization ??
|
|
261
|
-
asnData?.autonomousSystemOrganization,
|
|
262
|
-
providerType: mapUserType(geoData?.traits?.userType),
|
|
263
|
-
};
|
|
264
|
-
|
|
265
|
-
return result;
|
|
266
|
-
} catch (error) {
|
|
267
|
-
return {
|
|
268
|
-
isValid: false,
|
|
269
|
-
error: `MaxMind lookup error: ${error instanceof Error ? error.message : String(error)}`,
|
|
270
|
-
ip,
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/**
|
|
277
|
-
* geoip2-node sets `name` on its error classes, so this survives the class
|
|
278
|
-
* identity being lost across the lazy `import()` in `openReaderFromFile`.
|
|
279
|
-
*/
|
|
280
|
-
function isBadMethodCall(error: unknown): boolean {
|
|
281
|
-
return error instanceof Error && error.name === BAD_METHOD_CALL_ERROR;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
export type MaxMindUserType =
|
|
285
|
-
| "business"
|
|
286
|
-
| "cafe"
|
|
287
|
-
| "cellular"
|
|
288
|
-
| "college"
|
|
289
|
-
| "consumer_privacy_network"
|
|
290
|
-
| "content_delivery_network"
|
|
291
|
-
| "dialup"
|
|
292
|
-
| "government"
|
|
293
|
-
| "hosting"
|
|
294
|
-
| "library"
|
|
295
|
-
| "military"
|
|
296
|
-
| "residential"
|
|
297
|
-
| "router"
|
|
298
|
-
| "school"
|
|
299
|
-
| "search_engine_spider"
|
|
300
|
-
| "traveler";
|
|
301
|
-
|
|
302
|
-
type ProviderType =
|
|
303
|
-
| "hosting"
|
|
304
|
-
| "education"
|
|
305
|
-
| "government"
|
|
306
|
-
| "banking"
|
|
307
|
-
| "business"
|
|
308
|
-
| "isp";
|
|
309
|
-
|
|
310
|
-
function mapUserType(
|
|
311
|
-
userType: MaxMindUserType | undefined,
|
|
312
|
-
): ProviderType | undefined {
|
|
313
|
-
switch (userType) {
|
|
314
|
-
case "hosting":
|
|
315
|
-
case "content_delivery_network":
|
|
316
|
-
return "hosting";
|
|
317
|
-
case "college":
|
|
318
|
-
case "school":
|
|
319
|
-
case "library":
|
|
320
|
-
return "education";
|
|
321
|
-
case "government":
|
|
322
|
-
case "military":
|
|
323
|
-
return "government";
|
|
324
|
-
case "business":
|
|
325
|
-
return "business";
|
|
326
|
-
case "residential":
|
|
327
|
-
case "cellular":
|
|
328
|
-
case "dialup":
|
|
329
|
-
case "cafe":
|
|
330
|
-
case "traveler":
|
|
331
|
-
case "router":
|
|
332
|
-
return "isp";
|
|
333
|
-
default:
|
|
334
|
-
return undefined;
|
|
335
|
-
}
|
|
336
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
|
|
15
|
-
export { IpInfoService, isNonRoutable } from "./IpInfoService.js";
|
|
16
|
-
export type { IpInfoBackends } from "./IpInfoService.js";
|
|
17
|
-
// The backends and their injection seams are part of the surface: without them
|
|
18
|
-
// a consumer cannot substitute either backend, and callers that already know an
|
|
19
|
-
// address is private cannot skip the lookup.
|
|
20
|
-
export { IpapiBackend } from "./backends/ipapi.js";
|
|
21
|
-
export type { FetchFn, IpapiBackendConfig } from "./backends/ipapi.js";
|
|
22
|
-
export { MaxMindBackend } from "./backends/maxmind.js";
|
|
23
|
-
export type { MaxMindBackendConfig, OpenReader } from "./backends/maxmind.js";
|
|
24
|
-
export type { IIpInfoService, IpInfoServiceConfig } from "./types.js";
|