@prosopo/ipinfo 0.2.14 → 0.2.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 +34 -0
- package/.turbo/turbo-build$colon$tsc.log +13 -11
- package/.turbo/turbo-build.log +38 -38
- package/CHANGELOG.md +7 -0
- package/dist/IpInfoService.js +79 -86
- package/dist/backends/ipapi.js +99 -95
- package/dist/backends/maxmind.js +137 -140
- package/dist/cjs/IpInfoService.cjs +89 -0
- package/dist/cjs/backends/ipapi.cjs +104 -0
- package/dist/cjs/backends/maxmind.cjs +169 -0
- package/dist/cjs/index.cjs +4 -0
- package/dist/index.js +4 -2
- package/package.json +2 -2
- package/src/IpInfoService.ts +147 -0
- package/src/backends/ipapi.ts +148 -0
- package/src/backends/maxmind.ts +222 -0
- package/src/index.ts +16 -0
- package/src/types.ts +30 -0
- package/tsconfig.cjs.json +24 -0
- package/tsconfig.json +25 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsconfig.types.json +9 -0
- package/.turbo/turbo-typecheck.log +0 -4
package/dist/backends/maxmind.js
CHANGED
|
@@ -1,150 +1,147 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
class MaxMindBackend {
|
|
2
|
+
constructor(config) {
|
|
3
|
+
this.cityReader = null;
|
|
4
|
+
this.asnReader = null;
|
|
5
|
+
this.config = config;
|
|
6
|
+
}
|
|
7
|
+
async initialize() {
|
|
8
|
+
const { Reader } = await import("@maxmind/geoip2-node");
|
|
9
|
+
if (this.config.cityDbPath) {
|
|
10
|
+
try {
|
|
11
|
+
this.cityReader = await Reader.open(this.config.cityDbPath);
|
|
12
|
+
this.config.logger?.info(() => ({
|
|
13
|
+
msg: "MaxMind City reader initialized",
|
|
14
|
+
data: { dbPath: this.config.cityDbPath }
|
|
15
|
+
}));
|
|
16
|
+
} catch (error) {
|
|
17
|
+
this.config.logger?.warn(() => ({
|
|
18
|
+
msg: "Failed to initialize MaxMind City reader",
|
|
19
|
+
err: error,
|
|
20
|
+
data: { dbPath: this.config.cityDbPath }
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
6
23
|
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
data: { dbPath: this.config.cityDbPath },
|
|
22
|
-
}));
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
if (this.config.asnDbPath) {
|
|
26
|
-
try {
|
|
27
|
-
this.asnReader = await Reader.open(this.config.asnDbPath);
|
|
28
|
-
this.config.logger?.info(() => ({
|
|
29
|
-
msg: "MaxMind ASN reader initialized",
|
|
30
|
-
data: { dbPath: this.config.asnDbPath },
|
|
31
|
-
}));
|
|
32
|
-
}
|
|
33
|
-
catch (error) {
|
|
34
|
-
this.config.logger?.warn(() => ({
|
|
35
|
-
msg: "Failed to initialize MaxMind ASN reader",
|
|
36
|
-
err: error,
|
|
37
|
-
data: { dbPath: this.config.asnDbPath },
|
|
38
|
-
}));
|
|
39
|
-
}
|
|
40
|
-
}
|
|
24
|
+
if (this.config.asnDbPath) {
|
|
25
|
+
try {
|
|
26
|
+
this.asnReader = await Reader.open(this.config.asnDbPath);
|
|
27
|
+
this.config.logger?.info(() => ({
|
|
28
|
+
msg: "MaxMind ASN reader initialized",
|
|
29
|
+
data: { dbPath: this.config.asnDbPath }
|
|
30
|
+
}));
|
|
31
|
+
} catch (error) {
|
|
32
|
+
this.config.logger?.warn(() => ({
|
|
33
|
+
msg: "Failed to initialize MaxMind ASN reader",
|
|
34
|
+
err: error,
|
|
35
|
+
data: { dbPath: this.config.asnDbPath }
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
41
38
|
}
|
|
42
|
-
|
|
43
|
-
|
|
39
|
+
}
|
|
40
|
+
isAvailable() {
|
|
41
|
+
return this.cityReader !== null || this.asnReader !== null;
|
|
42
|
+
}
|
|
43
|
+
async lookup(ip) {
|
|
44
|
+
if (!this.isAvailable()) {
|
|
45
|
+
return {
|
|
46
|
+
isValid: false,
|
|
47
|
+
error: "MaxMind readers not initialized",
|
|
48
|
+
ip
|
|
49
|
+
};
|
|
44
50
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
error: "MaxMind readers not initialized",
|
|
50
|
-
ip,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
51
|
+
try {
|
|
52
|
+
let cityData;
|
|
53
|
+
let asnData;
|
|
54
|
+
if (this.cityReader) {
|
|
53
55
|
try {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
this.config.logger?.debug(() => ({
|
|
62
|
-
msg: "MaxMind City lookup failed",
|
|
63
|
-
data: { ip },
|
|
64
|
-
err: error,
|
|
65
|
-
}));
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
if (this.asnReader) {
|
|
69
|
-
try {
|
|
70
|
-
asnData = this.asnReader.asn(ip);
|
|
71
|
-
}
|
|
72
|
-
catch (error) {
|
|
73
|
-
this.config.logger?.debug(() => ({
|
|
74
|
-
msg: "MaxMind ASN lookup failed",
|
|
75
|
-
data: { ip },
|
|
76
|
-
err: error,
|
|
77
|
-
}));
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
if (!cityData && !asnData) {
|
|
81
|
-
return {
|
|
82
|
-
isValid: false,
|
|
83
|
-
error: "No MaxMind data available for IP",
|
|
84
|
-
ip,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
const result = {
|
|
88
|
-
ip,
|
|
89
|
-
isValid: true,
|
|
90
|
-
isVPN: cityData?.traits?.isAnonymousVpn ?? false,
|
|
91
|
-
isTor: cityData?.traits?.isTorExitNode ?? false,
|
|
92
|
-
isProxy: (cityData?.traits?.isPublicProxy ?? false) ||
|
|
93
|
-
(cityData?.traits?.isResidentialProxy ?? false),
|
|
94
|
-
isDatacenter: cityData?.traits?.isHostingProvider ?? false,
|
|
95
|
-
isAbuser: false,
|
|
96
|
-
isMobile: false,
|
|
97
|
-
isSatellite: cityData?.traits?.isSatelliteProvider ?? false,
|
|
98
|
-
isCrawler: false,
|
|
99
|
-
country: cityData?.country?.names?.en,
|
|
100
|
-
countryCode: cityData?.country?.isoCode,
|
|
101
|
-
region: cityData?.subdivisions?.[0]?.names?.en,
|
|
102
|
-
city: cityData?.city?.names?.en,
|
|
103
|
-
latitude: cityData?.location?.latitude,
|
|
104
|
-
longitude: cityData?.location?.longitude,
|
|
105
|
-
timezone: cityData?.location?.timeZone,
|
|
106
|
-
asnNumber: cityData?.traits?.autonomousSystemNumber ??
|
|
107
|
-
asnData?.autonomousSystemNumber,
|
|
108
|
-
asnOrganization: cityData?.traits?.autonomousSystemOrganization ??
|
|
109
|
-
asnData?.autonomousSystemOrganization,
|
|
110
|
-
providerName: cityData?.traits?.autonomousSystemOrganization ??
|
|
111
|
-
asnData?.autonomousSystemOrganization,
|
|
112
|
-
providerType: mapUserType(cityData?.traits?.userType),
|
|
113
|
-
};
|
|
114
|
-
return result;
|
|
56
|
+
cityData = this.cityReader.city(ip);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
this.config.logger?.debug(() => ({
|
|
59
|
+
msg: "MaxMind City lookup failed",
|
|
60
|
+
data: { ip },
|
|
61
|
+
err: error
|
|
62
|
+
}));
|
|
115
63
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
64
|
+
}
|
|
65
|
+
if (this.asnReader) {
|
|
66
|
+
try {
|
|
67
|
+
asnData = this.asnReader.asn(ip);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
this.config.logger?.debug(() => ({
|
|
70
|
+
msg: "MaxMind ASN lookup failed",
|
|
71
|
+
data: { ip },
|
|
72
|
+
err: error
|
|
73
|
+
}));
|
|
122
74
|
}
|
|
75
|
+
}
|
|
76
|
+
if (!cityData && !asnData) {
|
|
77
|
+
return {
|
|
78
|
+
isValid: false,
|
|
79
|
+
error: "No MaxMind data available for IP",
|
|
80
|
+
ip
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const result = {
|
|
84
|
+
ip,
|
|
85
|
+
isValid: true,
|
|
86
|
+
// Threat indicators - GeoLite2 free DBs do not populate these
|
|
87
|
+
isVPN: cityData?.traits?.isAnonymousVpn ?? false,
|
|
88
|
+
isTor: cityData?.traits?.isTorExitNode ?? false,
|
|
89
|
+
isProxy: (cityData?.traits?.isPublicProxy ?? false) || (cityData?.traits?.isResidentialProxy ?? false),
|
|
90
|
+
isDatacenter: cityData?.traits?.isHostingProvider ?? false,
|
|
91
|
+
isAbuser: false,
|
|
92
|
+
isMobile: false,
|
|
93
|
+
isSatellite: cityData?.traits?.isSatelliteProvider ?? false,
|
|
94
|
+
isCrawler: false,
|
|
95
|
+
// Geolocation from City DB
|
|
96
|
+
country: cityData?.country?.names?.en,
|
|
97
|
+
countryCode: cityData?.country?.isoCode,
|
|
98
|
+
region: cityData?.subdivisions?.[0]?.names?.en,
|
|
99
|
+
city: cityData?.city?.names?.en,
|
|
100
|
+
latitude: cityData?.location?.latitude,
|
|
101
|
+
longitude: cityData?.location?.longitude,
|
|
102
|
+
timezone: cityData?.location?.timeZone,
|
|
103
|
+
// ASN info - prefer City DB traits, fall back to ASN DB
|
|
104
|
+
asnNumber: cityData?.traits?.autonomousSystemNumber ?? asnData?.autonomousSystemNumber,
|
|
105
|
+
asnOrganization: cityData?.traits?.autonomousSystemOrganization ?? asnData?.autonomousSystemOrganization,
|
|
106
|
+
// Provider info from ASN
|
|
107
|
+
providerName: cityData?.traits?.autonomousSystemOrganization ?? asnData?.autonomousSystemOrganization,
|
|
108
|
+
providerType: mapUserType(cityData?.traits?.userType)
|
|
109
|
+
};
|
|
110
|
+
return result;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
return {
|
|
113
|
+
isValid: false,
|
|
114
|
+
error: `MaxMind lookup error: ${error instanceof Error ? error.message : String(error)}`,
|
|
115
|
+
ip
|
|
116
|
+
};
|
|
123
117
|
}
|
|
118
|
+
}
|
|
124
119
|
}
|
|
125
120
|
function mapUserType(userType) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
121
|
+
switch (userType) {
|
|
122
|
+
case "hosting":
|
|
123
|
+
case "content_delivery_network":
|
|
124
|
+
return "hosting";
|
|
125
|
+
case "college":
|
|
126
|
+
case "school":
|
|
127
|
+
case "library":
|
|
128
|
+
return "education";
|
|
129
|
+
case "government":
|
|
130
|
+
case "military":
|
|
131
|
+
return "government";
|
|
132
|
+
case "business":
|
|
133
|
+
return "business";
|
|
134
|
+
case "residential":
|
|
135
|
+
case "cellular":
|
|
136
|
+
case "dialup":
|
|
137
|
+
case "cafe":
|
|
138
|
+
case "traveler":
|
|
139
|
+
case "router":
|
|
140
|
+
return "isp";
|
|
141
|
+
default:
|
|
142
|
+
return void 0;
|
|
143
|
+
}
|
|
149
144
|
}
|
|
150
|
-
|
|
145
|
+
export {
|
|
146
|
+
MaxMindBackend
|
|
147
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const ipapi = require("./backends/ipapi.cjs");
|
|
4
|
+
const maxmind = require("./backends/maxmind.cjs");
|
|
5
|
+
function isNonRoutable(ip) {
|
|
6
|
+
const normalized = ip.replace(/^::ffff:/i, "");
|
|
7
|
+
if (normalized.startsWith("127.") || normalized.startsWith("10.") || normalized.startsWith("192.168.") || normalized.startsWith("169.254.") || normalized === "0.0.0.0") {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
if (normalized.startsWith("172.")) {
|
|
11
|
+
const second = Number.parseInt(normalized.split(".")[1] ?? "", 10);
|
|
12
|
+
if (second >= 16 && second <= 31) return true;
|
|
13
|
+
}
|
|
14
|
+
if (normalized === "::1" || normalized === "::") return true;
|
|
15
|
+
if (/^f[cd]/i.test(normalized)) return true;
|
|
16
|
+
if (/^fe[89ab]/i.test(normalized)) return true;
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
class IpInfoService {
|
|
20
|
+
constructor(config) {
|
|
21
|
+
this.maxmindBackend = null;
|
|
22
|
+
this.ipapiBackend = null;
|
|
23
|
+
this.config = config;
|
|
24
|
+
if (config.maxmindCityDbPath || config.maxmindAsnDbPath) {
|
|
25
|
+
this.maxmindBackend = new maxmind.MaxMindBackend({
|
|
26
|
+
cityDbPath: config.maxmindCityDbPath,
|
|
27
|
+
asnDbPath: config.maxmindAsnDbPath,
|
|
28
|
+
logger: config.logger
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
if (config.ipapiUrl) {
|
|
32
|
+
this.ipapiBackend = new ipapi.IpapiBackend({
|
|
33
|
+
baseUrl: config.ipapiUrl,
|
|
34
|
+
apiKey: config.ipapiKey,
|
|
35
|
+
logger: config.logger
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async initialize() {
|
|
40
|
+
if (this.maxmindBackend) {
|
|
41
|
+
await this.maxmindBackend.initialize();
|
|
42
|
+
}
|
|
43
|
+
this.config.logger?.info(() => ({
|
|
44
|
+
msg: "IpInfoService initialized",
|
|
45
|
+
data: {
|
|
46
|
+
maxmindAvailable: this.maxmindBackend?.isAvailable() ?? false,
|
|
47
|
+
ipapiAvailable: this.ipapiBackend?.isAvailable() ?? false
|
|
48
|
+
}
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
isAvailable() {
|
|
52
|
+
return (this.maxmindBackend?.isAvailable() ?? false) || (this.ipapiBackend?.isAvailable() ?? false);
|
|
53
|
+
}
|
|
54
|
+
async lookup(ip) {
|
|
55
|
+
if (isNonRoutable(ip)) {
|
|
56
|
+
return {
|
|
57
|
+
isValid: false,
|
|
58
|
+
error: "Non-routable IP address",
|
|
59
|
+
ip
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (this.ipapiBackend?.isAvailable()) {
|
|
63
|
+
const result = await this.ipapiBackend.lookup(ip);
|
|
64
|
+
if (result.isValid) {
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
this.config.logger?.debug(() => ({
|
|
68
|
+
msg: "ipapi.is lookup failed, falling back to MaxMind",
|
|
69
|
+
data: {
|
|
70
|
+
ip,
|
|
71
|
+
error: "error" in result ? result.error : "unknown"
|
|
72
|
+
}
|
|
73
|
+
}));
|
|
74
|
+
if (this.maxmindBackend?.isAvailable()) {
|
|
75
|
+
return this.maxmindBackend.lookup(ip);
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
if (this.maxmindBackend?.isAvailable()) {
|
|
80
|
+
return this.maxmindBackend.lookup(ip);
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
isValid: false,
|
|
84
|
+
error: "No IP info backend available",
|
|
85
|
+
ip
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
exports.IpInfoService = IpInfoService;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const TIMEOUT_MS = 700;
|
|
4
|
+
class IpapiBackend {
|
|
5
|
+
constructor(config) {
|
|
6
|
+
this.config = config;
|
|
7
|
+
}
|
|
8
|
+
isAvailable() {
|
|
9
|
+
return Boolean(this.config.baseUrl);
|
|
10
|
+
}
|
|
11
|
+
async lookup(ip) {
|
|
12
|
+
try {
|
|
13
|
+
if (!ip || typeof ip !== "string") {
|
|
14
|
+
return {
|
|
15
|
+
isValid: false,
|
|
16
|
+
error: "Invalid IP address provided",
|
|
17
|
+
ip: ip || "undefined"
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const body = { q: ip };
|
|
21
|
+
if (this.config.apiKey) {
|
|
22
|
+
body.key = this.config.apiKey;
|
|
23
|
+
}
|
|
24
|
+
const controller = new AbortController();
|
|
25
|
+
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
26
|
+
try {
|
|
27
|
+
const response = await fetch(this.config.baseUrl, {
|
|
28
|
+
method: "POST",
|
|
29
|
+
headers: {
|
|
30
|
+
"Content-Type": "application/json",
|
|
31
|
+
Accept: "application/json"
|
|
32
|
+
},
|
|
33
|
+
body: JSON.stringify(body),
|
|
34
|
+
signal: controller.signal
|
|
35
|
+
});
|
|
36
|
+
clearTimeout(timeoutId);
|
|
37
|
+
if (!response.ok) {
|
|
38
|
+
return {
|
|
39
|
+
isValid: false,
|
|
40
|
+
error: `API request failed with status ${response.status}: ${response.statusText}`,
|
|
41
|
+
ip
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const data = await response.json();
|
|
45
|
+
if (data.is_bogon) {
|
|
46
|
+
return {
|
|
47
|
+
isValid: false,
|
|
48
|
+
error: "IP address is bogon (non-routable)",
|
|
49
|
+
ip
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const result = {
|
|
53
|
+
ip: data.ip,
|
|
54
|
+
isValid: true,
|
|
55
|
+
isVPN: data.is_vpn,
|
|
56
|
+
isTor: data.is_tor,
|
|
57
|
+
isProxy: data.is_proxy,
|
|
58
|
+
isDatacenter: data.is_datacenter,
|
|
59
|
+
isAbuser: data.is_abuser,
|
|
60
|
+
isMobile: data.is_mobile,
|
|
61
|
+
isSatellite: data.is_satellite,
|
|
62
|
+
isCrawler: data.is_crawler,
|
|
63
|
+
providerName: data.company?.name || data.datacenter?.datacenter,
|
|
64
|
+
providerType: data.company?.type || data.asn?.type,
|
|
65
|
+
asnNumber: data.asn?.asn,
|
|
66
|
+
asnOrganization: data.asn?.org,
|
|
67
|
+
country: data.location?.country,
|
|
68
|
+
countryCode: data.location?.country_code,
|
|
69
|
+
region: data.location?.state,
|
|
70
|
+
city: data.location?.city,
|
|
71
|
+
latitude: data.location?.latitude,
|
|
72
|
+
longitude: data.location?.longitude,
|
|
73
|
+
timezone: data.location?.timezone,
|
|
74
|
+
vpnService: data.vpn?.service,
|
|
75
|
+
vpnType: data.vpn?.type,
|
|
76
|
+
abuserScore: Number.parseFloat(
|
|
77
|
+
data.asn?.abuser_score.split(" ")[0] || "0"
|
|
78
|
+
),
|
|
79
|
+
companyAbuserScore: Number.parseFloat(
|
|
80
|
+
data.company?.abuser_score.split(" ")[0] || "0"
|
|
81
|
+
)
|
|
82
|
+
};
|
|
83
|
+
return result;
|
|
84
|
+
} catch (fetchError) {
|
|
85
|
+
clearTimeout(timeoutId);
|
|
86
|
+
if (fetchError instanceof Error && fetchError.name === "AbortError") {
|
|
87
|
+
return {
|
|
88
|
+
isValid: false,
|
|
89
|
+
error: `Request timed out after ${TIMEOUT_MS}ms`,
|
|
90
|
+
ip
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
throw fetchError;
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
return {
|
|
97
|
+
isValid: false,
|
|
98
|
+
error: `Network or parsing error: ${error instanceof Error ? error.message : String(error)}`,
|
|
99
|
+
ip
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
exports.IpapiBackend = IpapiBackend;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
25
|
+
class MaxMindBackend {
|
|
26
|
+
constructor(config) {
|
|
27
|
+
this.cityReader = null;
|
|
28
|
+
this.asnReader = null;
|
|
29
|
+
this.config = config;
|
|
30
|
+
}
|
|
31
|
+
async initialize() {
|
|
32
|
+
const { Reader } = await import("@maxmind/geoip2-node");
|
|
33
|
+
if (this.config.cityDbPath) {
|
|
34
|
+
try {
|
|
35
|
+
this.cityReader = await Reader.open(this.config.cityDbPath);
|
|
36
|
+
this.config.logger?.info(() => ({
|
|
37
|
+
msg: "MaxMind City reader initialized",
|
|
38
|
+
data: { dbPath: this.config.cityDbPath }
|
|
39
|
+
}));
|
|
40
|
+
} catch (error) {
|
|
41
|
+
this.config.logger?.warn(() => ({
|
|
42
|
+
msg: "Failed to initialize MaxMind City reader",
|
|
43
|
+
err: error,
|
|
44
|
+
data: { dbPath: this.config.cityDbPath }
|
|
45
|
+
}));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (this.config.asnDbPath) {
|
|
49
|
+
try {
|
|
50
|
+
this.asnReader = await Reader.open(this.config.asnDbPath);
|
|
51
|
+
this.config.logger?.info(() => ({
|
|
52
|
+
msg: "MaxMind ASN reader initialized",
|
|
53
|
+
data: { dbPath: this.config.asnDbPath }
|
|
54
|
+
}));
|
|
55
|
+
} catch (error) {
|
|
56
|
+
this.config.logger?.warn(() => ({
|
|
57
|
+
msg: "Failed to initialize MaxMind ASN reader",
|
|
58
|
+
err: error,
|
|
59
|
+
data: { dbPath: this.config.asnDbPath }
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
isAvailable() {
|
|
65
|
+
return this.cityReader !== null || this.asnReader !== null;
|
|
66
|
+
}
|
|
67
|
+
async lookup(ip) {
|
|
68
|
+
if (!this.isAvailable()) {
|
|
69
|
+
return {
|
|
70
|
+
isValid: false,
|
|
71
|
+
error: "MaxMind readers not initialized",
|
|
72
|
+
ip
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
let cityData;
|
|
77
|
+
let asnData;
|
|
78
|
+
if (this.cityReader) {
|
|
79
|
+
try {
|
|
80
|
+
cityData = this.cityReader.city(ip);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
this.config.logger?.debug(() => ({
|
|
83
|
+
msg: "MaxMind City lookup failed",
|
|
84
|
+
data: { ip },
|
|
85
|
+
err: error
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (this.asnReader) {
|
|
90
|
+
try {
|
|
91
|
+
asnData = this.asnReader.asn(ip);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
this.config.logger?.debug(() => ({
|
|
94
|
+
msg: "MaxMind ASN lookup failed",
|
|
95
|
+
data: { ip },
|
|
96
|
+
err: error
|
|
97
|
+
}));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (!cityData && !asnData) {
|
|
101
|
+
return {
|
|
102
|
+
isValid: false,
|
|
103
|
+
error: "No MaxMind data available for IP",
|
|
104
|
+
ip
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
const result = {
|
|
108
|
+
ip,
|
|
109
|
+
isValid: true,
|
|
110
|
+
// Threat indicators - GeoLite2 free DBs do not populate these
|
|
111
|
+
isVPN: cityData?.traits?.isAnonymousVpn ?? false,
|
|
112
|
+
isTor: cityData?.traits?.isTorExitNode ?? false,
|
|
113
|
+
isProxy: (cityData?.traits?.isPublicProxy ?? false) || (cityData?.traits?.isResidentialProxy ?? false),
|
|
114
|
+
isDatacenter: cityData?.traits?.isHostingProvider ?? false,
|
|
115
|
+
isAbuser: false,
|
|
116
|
+
isMobile: false,
|
|
117
|
+
isSatellite: cityData?.traits?.isSatelliteProvider ?? false,
|
|
118
|
+
isCrawler: false,
|
|
119
|
+
// Geolocation from City DB
|
|
120
|
+
country: cityData?.country?.names?.en,
|
|
121
|
+
countryCode: cityData?.country?.isoCode,
|
|
122
|
+
region: cityData?.subdivisions?.[0]?.names?.en,
|
|
123
|
+
city: cityData?.city?.names?.en,
|
|
124
|
+
latitude: cityData?.location?.latitude,
|
|
125
|
+
longitude: cityData?.location?.longitude,
|
|
126
|
+
timezone: cityData?.location?.timeZone,
|
|
127
|
+
// ASN info - prefer City DB traits, fall back to ASN DB
|
|
128
|
+
asnNumber: cityData?.traits?.autonomousSystemNumber ?? asnData?.autonomousSystemNumber,
|
|
129
|
+
asnOrganization: cityData?.traits?.autonomousSystemOrganization ?? asnData?.autonomousSystemOrganization,
|
|
130
|
+
// Provider info from ASN
|
|
131
|
+
providerName: cityData?.traits?.autonomousSystemOrganization ?? asnData?.autonomousSystemOrganization,
|
|
132
|
+
providerType: mapUserType(cityData?.traits?.userType)
|
|
133
|
+
};
|
|
134
|
+
return result;
|
|
135
|
+
} catch (error) {
|
|
136
|
+
return {
|
|
137
|
+
isValid: false,
|
|
138
|
+
error: `MaxMind lookup error: ${error instanceof Error ? error.message : String(error)}`,
|
|
139
|
+
ip
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function mapUserType(userType) {
|
|
145
|
+
switch (userType) {
|
|
146
|
+
case "hosting":
|
|
147
|
+
case "content_delivery_network":
|
|
148
|
+
return "hosting";
|
|
149
|
+
case "college":
|
|
150
|
+
case "school":
|
|
151
|
+
case "library":
|
|
152
|
+
return "education";
|
|
153
|
+
case "government":
|
|
154
|
+
case "military":
|
|
155
|
+
return "government";
|
|
156
|
+
case "business":
|
|
157
|
+
return "business";
|
|
158
|
+
case "residential":
|
|
159
|
+
case "cellular":
|
|
160
|
+
case "dialup":
|
|
161
|
+
case "cafe":
|
|
162
|
+
case "traveler":
|
|
163
|
+
case "router":
|
|
164
|
+
return "isp";
|
|
165
|
+
default:
|
|
166
|
+
return void 0;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
exports.MaxMindBackend = MaxMindBackend;
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prosopo/ipinfo",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.15",
|
|
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",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@maxmind/geoip2-node": "5.0.0",
|
|
29
29
|
"@prosopo/logger": "1.0.2",
|
|
30
|
-
"@prosopo/types": "4.3.
|
|
30
|
+
"@prosopo/types": "4.3.1"
|
|
31
31
|
},
|
|
32
32
|
"devDependencies": {
|
|
33
33
|
"@prosopo/config": "3.3.1",
|