@prosopo/provider 3.2.5 → 3.12.3

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/CHANGELOG.md +578 -0
  2. package/dist/api/admin/apiRemoveDetectorKeyEndpoint.js +7 -4
  3. package/dist/api/blacklistRequestInspector.js +26 -20
  4. package/dist/api/captcha.js +121 -33
  5. package/dist/api/domainMiddleware.js +8 -8
  6. package/dist/api/headerCheckMiddleware.js +4 -0
  7. package/dist/api/ignoreMiddleware.js +4 -1
  8. package/dist/api/ja4Middleware.js +5 -23
  9. package/dist/api/public.js +26 -3
  10. package/dist/api/verify.js +4 -2
  11. package/dist/cjs/api/admin/apiRemoveDetectorKeyEndpoint.cjs +6 -3
  12. package/dist/cjs/api/blacklistRequestInspector.cjs +25 -19
  13. package/dist/cjs/api/captcha.cjs +121 -33
  14. package/dist/cjs/api/domainMiddleware.cjs +8 -8
  15. package/dist/cjs/api/headerCheckMiddleware.cjs +4 -0
  16. package/dist/cjs/api/ignoreMiddleware.cjs +3 -0
  17. package/dist/cjs/api/ja4Middleware.cjs +4 -22
  18. package/dist/cjs/api/public.cjs +26 -3
  19. package/dist/cjs/api/verify.cjs +4 -2
  20. package/dist/cjs/compositeIpAddress.cjs +53 -0
  21. package/dist/cjs/index.cjs +7 -0
  22. package/dist/cjs/pairs.cjs +27 -0
  23. package/dist/cjs/services/ipComparison.cjs +123 -0
  24. package/dist/cjs/services/ipInfo.cjs +87 -0
  25. package/dist/cjs/tasks/captchaManager.cjs +49 -2
  26. package/dist/cjs/tasks/client/clientTasks.cjs +33 -12
  27. package/dist/cjs/tasks/detection/decodePayload.cjs +712 -278
  28. package/dist/cjs/tasks/detection/getBotScore.cjs +14 -3
  29. package/dist/cjs/tasks/frictionless/frictionlessTasks.cjs +128 -24
  30. package/dist/cjs/tasks/frictionless/frictionlessTasksUtils.cjs +17 -0
  31. package/dist/cjs/tasks/imgCaptcha/imgCaptchaTasks.cjs +62 -20
  32. package/dist/cjs/tasks/powCaptcha/powTasks.cjs +43 -15
  33. package/dist/cjs/util.cjs +248 -16
  34. package/dist/cjs/utils/hashUserAgent.cjs +10 -0
  35. package/dist/compositeIpAddress.js +53 -0
  36. package/dist/index.js +8 -1
  37. package/dist/pairs.js +27 -0
  38. package/dist/services/ipComparison.js +123 -0
  39. package/dist/services/ipInfo.js +87 -0
  40. package/dist/tasks/captchaManager.js +49 -2
  41. package/dist/tasks/client/clientTasks.js +33 -12
  42. package/dist/tasks/detection/decodePayload.js +712 -278
  43. package/dist/tasks/detection/getBotScore.js +15 -4
  44. package/dist/tasks/frictionless/frictionlessTasks.js +128 -24
  45. package/dist/tasks/frictionless/frictionlessTasksUtils.js +18 -1
  46. package/dist/tasks/imgCaptcha/imgCaptchaTasks.js +64 -22
  47. package/dist/tasks/powCaptcha/powTasks.js +44 -16
  48. package/dist/util.js +249 -17
  49. package/dist/utils/hashUserAgent.js +10 -0
  50. package/package.json +28 -25
  51. package/vite.test.config.ts +3 -2
  52. package/vite.threads.test.config.ts +33 -0
package/dist/cjs/util.cjs CHANGED
@@ -7,6 +7,7 @@ const types = require("@prosopo/types");
7
7
  const util = require("@prosopo/util");
8
8
  const utilCrypto = require("@prosopo/util-crypto");
9
9
  const ipAddress = require("ip-address");
10
+ const ipComparison = require("./services/ipComparison.cjs");
10
11
  function encodeStringAddress(address) {
11
12
  try {
12
13
  return utilCrypto.encodeAddress(
@@ -63,7 +64,7 @@ const getIPAddressFromBigInt = (ipAddressBigInt) => {
63
64
  throw new common.ProsopoEnvError("API.INVALID_IP");
64
65
  }
65
66
  };
66
- const validateIpAddress = (ip, challengeRecordIpAddress, logger) => {
67
+ const validateIpAddress = (ip, challengeIpAddress, logger) => {
67
68
  if (!ip) {
68
69
  return { isValid: true };
69
70
  }
@@ -76,27 +77,19 @@ const validateIpAddress = (ip, challengeRecordIpAddress, logger) => {
76
77
  logger.info(() => ({ msg: errorMessage }));
77
78
  return { isValid: false, errorMessage };
78
79
  }
79
- let challengeIpV4orV6Address = getIPAddressFromBigInt(
80
- challengeRecordIpAddress
81
- );
82
80
  ipV4orV6Address = "address4" in ipV4orV6Address && ipV4orV6Address.address4 ? ipV4orV6Address.address4 : ipV4orV6Address;
83
- challengeIpV4orV6Address = "address4" in challengeIpV4orV6Address && challengeIpV4orV6Address.address4 ? challengeIpV4orV6Address.address4 : challengeIpV4orV6Address;
84
- if (ipV4orV6Address.v4 && !challengeIpV4orV6Address.v4) {
85
- challengeIpV4orV6Address = new ipAddress.Address4(
86
- challengeIpV4orV6Address.to4().correctForm()
87
- );
88
- }
89
- if (!ipV4orV6Address.v4 && challengeIpV4orV6Address.v4) {
90
- ipV4orV6Address = new ipAddress.Address6(
91
- ipV4orV6Address.to4().correctForm()
81
+ challengeIpAddress = "address4" in challengeIpAddress && challengeIpAddress.address4 ? challengeIpAddress.address4 : challengeIpAddress;
82
+ if (ipV4orV6Address.v4 && !challengeIpAddress.v4) {
83
+ challengeIpAddress = new ipAddress.Address4(
84
+ challengeIpAddress.to4().correctForm()
92
85
  );
93
86
  }
94
- if (challengeIpV4orV6Address.bigInt() - ipV4orV6Address.bigInt() !== 0n) {
95
- const errorMessage = `IP address mismatch: ${challengeIpV4orV6Address.address} !== ${ipV4orV6Address.address}`;
87
+ if (challengeIpAddress.bigInt() - ipV4orV6Address.bigInt() !== 0n) {
88
+ const errorMessage = `IP address mismatch: ${challengeIpAddress.address} !== ${ipV4orV6Address.address}`;
96
89
  logger.info(() => ({
97
90
  msg: errorMessage,
98
91
  data: {
99
- challengeIp: challengeIpV4orV6Address.address,
92
+ challengeIp: challengeIpAddress.address,
100
93
  providedIp: ipV4orV6Address.address
101
94
  }
102
95
  }));
@@ -104,8 +97,247 @@ const validateIpAddress = (ip, challengeRecordIpAddress, logger) => {
104
97
  }
105
98
  return { isValid: true };
106
99
  };
100
+ const evaluateIpValidationRules = (comparison, rules, logger) => {
101
+ if (!comparison.comparison) {
102
+ return { action: types.IPValidationAction.Allow };
103
+ }
104
+ const conditions = [];
105
+ const rejectActions = Object.values(rules.actions).filter(
106
+ (action) => action === types.IPValidationAction.Reject
107
+ );
108
+ const ip1Country = comparison.comparison.ip1Details?.country;
109
+ const ip2Country = comparison.comparison.ip2Details?.country;
110
+ const ip1CountryCode = comparison.comparison.ip1Details?.countryCode;
111
+ const ip2CountryCode = comparison.comparison.ip2Details?.countryCode;
112
+ let effectiveRules = rules;
113
+ let countryOverride = void 0;
114
+ if (ip1CountryCode && rules.countryOverrides?.[ip1CountryCode]) {
115
+ countryOverride = rules.countryOverrides[ip1CountryCode];
116
+ }
117
+ if (ip2CountryCode && rules.countryOverrides?.[ip2CountryCode]) {
118
+ countryOverride = rules.countryOverrides[ip2CountryCode];
119
+ }
120
+ if (countryOverride) {
121
+ effectiveRules = {
122
+ ...rules,
123
+ actions: {
124
+ ...rules.actions,
125
+ ...countryOverride.actions
126
+ },
127
+ distanceThresholdKm: countryOverride.distanceThresholdKm ?? rules.distanceThresholdKm,
128
+ abuseScoreThreshold: countryOverride.abuseScoreThreshold ?? rules.abuseScoreThreshold,
129
+ requireAllConditions: countryOverride.requireAllConditions ?? rules.requireAllConditions
130
+ };
131
+ }
132
+ if (ip1Country !== ip2Country) {
133
+ conditions.push({
134
+ met: true,
135
+ action: effectiveRules.actions.countryChangeAction,
136
+ message: `Country changed from ${ip1Country} to ${ip2Country}`
137
+ });
138
+ }
139
+ const ip1City = comparison.comparison.ip1Details?.city;
140
+ const ip2City = comparison.comparison.ip2Details?.city;
141
+ if (ip1City !== ip2City) {
142
+ conditions.push({
143
+ met: true,
144
+ action: effectiveRules.actions.cityChangeAction,
145
+ message: `City changed from ${ip1City} to ${ip2City}`
146
+ });
147
+ }
148
+ if (comparison.comparison.differentProviders) {
149
+ const ip1Provider = comparison.comparison.ip1Details?.provider;
150
+ const ip2Provider = comparison.comparison.ip2Details?.provider;
151
+ conditions.push({
152
+ met: true,
153
+ action: effectiveRules.actions.ispChangeAction,
154
+ message: `ISP changed from ${ip1Provider} to ${ip2Provider}`
155
+ });
156
+ }
157
+ const distanceKm = comparison.comparison.distanceKm;
158
+ if (distanceKm !== void 0 && distanceKm > effectiveRules.distanceThresholdKm) {
159
+ conditions.push({
160
+ met: true,
161
+ action: effectiveRules.actions.distanceExceedAction,
162
+ message: `IP addresses are ${distanceKm.toFixed(2)}km apart (>${effectiveRules.distanceThresholdKm}km limit)`
163
+ });
164
+ }
165
+ console.log(JSON.stringify(effectiveRules, null, 2));
166
+ const ip2AbuseScore = comparison.comparison.ip2Details?.abuserScore;
167
+ if (ip2AbuseScore !== void 0 && ip2AbuseScore > effectiveRules.abuseScoreThreshold) {
168
+ conditions.push({
169
+ met: true,
170
+ action: effectiveRules.actions.abuseScoreExceedAction,
171
+ message: `Abuse score ${ip2AbuseScore.toFixed(4)} exceeds threshold ${effectiveRules.abuseScoreThreshold}`
172
+ });
173
+ }
174
+ const ip1AbuseScore = comparison.comparison.ip1Details?.abuserScore;
175
+ if (ip1AbuseScore !== void 0 && ip1AbuseScore > effectiveRules.abuseScoreThreshold) {
176
+ conditions.push({
177
+ met: true,
178
+ action: effectiveRules.actions.abuseScoreExceedAction,
179
+ message: `Abuse score ${ip1AbuseScore.toFixed(4)} exceeds threshold ${effectiveRules.abuseScoreThreshold}`
180
+ });
181
+ }
182
+ if (conditions.length === 0) {
183
+ return { action: types.IPValidationAction.Allow };
184
+ }
185
+ const errorMessages = [];
186
+ let finalAction = types.IPValidationAction.Allow;
187
+ let shouldFlag = false;
188
+ if (effectiveRules.requireAllConditions) {
189
+ const rejectConditions = conditions.filter(
190
+ (c) => c.action === types.IPValidationAction.Reject
191
+ );
192
+ if (rejectConditions.length > 0 && rejectConditions.length >= rejectActions.length) {
193
+ finalAction = types.IPValidationAction.Reject;
194
+ }
195
+ for (const condition of conditions) {
196
+ if (condition.action === types.IPValidationAction.Reject || condition.action === types.IPValidationAction.Flag) {
197
+ errorMessages.push(condition.message);
198
+ }
199
+ }
200
+ } else {
201
+ for (const condition of conditions) {
202
+ if (condition.action === types.IPValidationAction.Reject) {
203
+ finalAction = types.IPValidationAction.Reject;
204
+ errorMessages.push(condition.message);
205
+ break;
206
+ }
207
+ if (condition.action === types.IPValidationAction.Flag) {
208
+ finalAction = types.IPValidationAction.Flag;
209
+ shouldFlag = true;
210
+ }
211
+ errorMessages.push(condition.message);
212
+ }
213
+ }
214
+ logger.info(() => ({
215
+ msg: `IP validation rules evaluated: ${finalAction}`,
216
+ data: {
217
+ conditions,
218
+ finalAction,
219
+ requireAllConditions: effectiveRules.requireAllConditions
220
+ }
221
+ }));
222
+ return {
223
+ action: finalAction,
224
+ errorMessage: errorMessages.length > 0 ? errorMessages.join("; ") : void 0,
225
+ shouldFlag
226
+ };
227
+ };
228
+ const deepValidateIpAddress = async (ip, challengeIpAddress, logger, apiKey, apiUrl, ipValidationRules) => {
229
+ const standardValidation = validateIpAddress(ip, challengeIpAddress, logger);
230
+ if (!standardValidation.isValid) {
231
+ if (standardValidation.errorMessage?.includes("Invalid IP address")) {
232
+ return standardValidation;
233
+ }
234
+ } else {
235
+ return { isValid: true };
236
+ }
237
+ try {
238
+ const challengeIpString = challengeIpAddress.address;
239
+ const comparison = await ipComparison.compareIPs(challengeIpString, ip, apiKey, apiUrl);
240
+ if ("error" in comparison) {
241
+ logger.error(() => ({
242
+ msg: "Failed to get IP distance comparison",
243
+ data: {
244
+ error: comparison.error,
245
+ challengeIp: challengeIpString,
246
+ providedIp: ip
247
+ }
248
+ }));
249
+ return {
250
+ isValid: false,
251
+ errorMessage: "Could not determine IP distance"
252
+ };
253
+ }
254
+ if (comparison.ipsMatch) {
255
+ return { isValid: true };
256
+ }
257
+ const distanceKm = comparison.comparison?.distanceKm;
258
+ if (!ipValidationRules) {
259
+ logger.info(() => ({
260
+ msg: "No IP validation rules provided, using legacy logic",
261
+ data: {
262
+ challengeIp: challengeIpString,
263
+ providedIp: ip,
264
+ distanceKm
265
+ }
266
+ }));
267
+ if (distanceKm !== void 0 && distanceKm > 1e3) {
268
+ const errorMessage = `IP addresses are too far apart: ${distanceKm.toFixed(2)}km (>1000km limit)`;
269
+ logger.info(() => ({
270
+ msg: "IP validation failed - distance too great",
271
+ data: {
272
+ challengeIp: challengeIpString,
273
+ providedIp: ip,
274
+ distanceKm,
275
+ comparison: comparison.comparison
276
+ }
277
+ }));
278
+ return {
279
+ isValid: false,
280
+ errorMessage,
281
+ distanceKm
282
+ };
283
+ }
284
+ logger.info(() => ({
285
+ msg: "IP addresses differ but within acceptable distance",
286
+ data: {
287
+ challengeIp: challengeIpString,
288
+ providedIp: ip,
289
+ distanceKm,
290
+ comparison: comparison.comparison
291
+ }
292
+ }));
293
+ return {
294
+ isValid: true,
295
+ distanceKm,
296
+ shouldFlag: true
297
+ };
298
+ }
299
+ const ruleEvaluation = evaluateIpValidationRules(
300
+ comparison,
301
+ ipValidationRules,
302
+ logger
303
+ );
304
+ switch (ruleEvaluation.action) {
305
+ case types.IPValidationAction.Reject:
306
+ return {
307
+ isValid: false,
308
+ errorMessage: ruleEvaluation.errorMessage,
309
+ distanceKm
310
+ };
311
+ case types.IPValidationAction.Flag:
312
+ return {
313
+ isValid: true,
314
+ distanceKm,
315
+ shouldFlag: true,
316
+ errorMessage: ruleEvaluation.errorMessage
317
+ };
318
+ default:
319
+ return {
320
+ isValid: true,
321
+ distanceKm
322
+ };
323
+ }
324
+ } catch (error) {
325
+ logger.error(() => ({
326
+ msg: "Error during IP distance validation",
327
+ err: error,
328
+ data: { challengeIp: challengeIpAddress.address, providedIp: ip }
329
+ }));
330
+ return {
331
+ isValid: true,
332
+ shouldFlag: true,
333
+ errorMessage: "IP distance validation error"
334
+ };
335
+ }
336
+ };
107
337
  exports.checkIfTaskIsRunning = checkIfTaskIsRunning;
338
+ exports.deepValidateIpAddress = deepValidateIpAddress;
108
339
  exports.encodeStringAddress = encodeStringAddress;
340
+ exports.evaluateIpValidationRules = evaluateIpValidationRules;
109
341
  exports.getIPAddress = getIPAddress;
110
342
  exports.getIPAddressFromBigInt = getIPAddressFromBigInt;
111
343
  exports.shuffleArray = shuffleArray;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const node_crypto = require("node:crypto");
4
+ function hashUserAgent(userAgent) {
5
+ const hash = node_crypto.createHash("sha256");
6
+ hash.update(userAgent, "utf8");
7
+ const hashHex = hash.digest("hex");
8
+ return hashHex.substring(0, 32);
9
+ }
10
+ exports.hashUserAgent = hashUserAgent;
@@ -0,0 +1,53 @@
1
+ import { IpAddressType } from "@prosopo/types-database";
2
+ import { getIPAddress } from "@prosopo/util";
3
+ import { Address4, Address6 } from "ip-address";
4
+ const V6_SHIFT = 64n;
5
+ const v6_LOWER_MASK = (1n << V6_SHIFT) - 1n;
6
+ const getCompositeIpAddress = (ip) => {
7
+ let ipAddress;
8
+ try {
9
+ ipAddress = "string" === typeof ip ? getIPAddress(ip) : ip;
10
+ } catch (e) {
11
+ return {
12
+ lower: 0n,
13
+ type: IpAddressType.v4
14
+ };
15
+ }
16
+ return getCompositeFromIpAddress(ipAddress);
17
+ };
18
+ const getCompositeFromIpAddress = (ipAddress) => {
19
+ const numericIp = ipAddress.bigInt();
20
+ if (ipAddress instanceof Address4) {
21
+ return {
22
+ lower: numericIp,
23
+ type: IpAddressType.v4
24
+ };
25
+ }
26
+ ipAddress;
27
+ return {
28
+ lower: numericIp & v6_LOWER_MASK,
29
+ upper: numericIp >> V6_SHIFT,
30
+ type: IpAddressType.v6
31
+ };
32
+ };
33
+ const getIpAddressFromComposite = (compositeIpAddress) => {
34
+ switch (compositeIpAddress.type) {
35
+ case IpAddressType.v4:
36
+ return Address4.fromBigInt(getBigInt(compositeIpAddress.lower));
37
+ case IpAddressType.v6:
38
+ return Address6.fromBigInt(
39
+ getBigInt(compositeIpAddress.upper) << V6_SHIFT | getBigInt(compositeIpAddress.lower) & v6_LOWER_MASK
40
+ );
41
+ default:
42
+ never();
43
+ return Address4.fromBigInt(0n);
44
+ }
45
+ };
46
+ const getBigInt = (number) => BigInt(number || 0n);
47
+ const never = () => {
48
+ throw new Error("Unhandled type");
49
+ };
50
+ export {
51
+ getCompositeIpAddress,
52
+ getIpAddressFromComposite
53
+ };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import "./tasks/index.js";
2
- import { checkIfTaskIsRunning, encodeStringAddress, getIPAddress, getIPAddressFromBigInt, shuffleArray, validateIpAddress } from "./util.js";
2
+ import { checkIfTaskIsRunning, deepValidateIpAddress, encodeStringAddress, evaluateIpValidationRules, getIPAddress, getIPAddressFromBigInt, shuffleArray, validateIpAddress } from "./util.js";
3
3
  import { blockMiddleware } from "./api/block.js";
4
4
  import { prosopoRouter } from "./api/captcha.js";
5
5
  import { prosopoVerifyRouter } from "./api/verify.js";
@@ -12,18 +12,25 @@ import { headerCheckMiddleware } from "./api/headerCheckMiddleware.js";
12
12
  import { createApiAdminRoutesProvider } from "./api/admin/createApiAdminRoutesProvider.js";
13
13
  import { ignoreMiddleware } from "./api/ignoreMiddleware.js";
14
14
  import { robotsMiddleware } from "./api/robotsMiddleware.js";
15
+ import { getCompositeIpAddress, getIpAddressFromComposite } from "./compositeIpAddress.js";
16
+ import { compareIPs } from "./services/ipComparison.js";
15
17
  import { Tasks } from "./tasks/tasks.js";
16
18
  export {
17
19
  DEFAULT_JA4,
18
20
  Tasks,
19
21
  blockMiddleware,
20
22
  checkIfTaskIsRunning,
23
+ compareIPs,
21
24
  createApiAdminRoutesProvider,
25
+ deepValidateIpAddress,
22
26
  domainMiddleware,
23
27
  encodeStringAddress,
28
+ evaluateIpValidationRules,
24
29
  getClientList,
30
+ getCompositeIpAddress,
25
31
  getIPAddress,
26
32
  getIPAddressFromBigInt,
33
+ getIpAddressFromComposite,
27
34
  getJA4,
28
35
  headerCheckMiddleware,
29
36
  ignoreMiddleware,
package/dist/pairs.js ADDED
@@ -0,0 +1,27 @@
1
+ import { at } from "@prosopo/util";
2
+ const constructPairList = (list) => {
3
+ if (list.length % 2 !== 0) {
4
+ throw new Error("Invalid pairs length");
5
+ }
6
+ const pairList = [];
7
+ for (let i = 0; i < list.length; i += 2) {
8
+ pairList.push([at(list, i), at(list, i + 1)]);
9
+ }
10
+ return pairList;
11
+ };
12
+ const containsIdenticalPairs = (pairsLists) => {
13
+ const set = /* @__PURE__ */ new Set();
14
+ for (const pairList of pairsLists) {
15
+ for (const pair of pairList) {
16
+ const x = at(pair, 0);
17
+ const y = at(pair, 1);
18
+ const coordString = `${x},${y}`;
19
+ set.add(coordString);
20
+ }
21
+ }
22
+ return set.size !== pairsLists.flat().flat().length / 2;
23
+ };
24
+ export {
25
+ constructPairList,
26
+ containsIdenticalPairs
27
+ };
@@ -0,0 +1,123 @@
1
+ import { getDistance } from "geolib";
2
+ import { getIPInfo } from "./ipInfo.js";
3
+ async function compareIPs(ip1, ip2, apiKey, apiUrl) {
4
+ try {
5
+ if (!ip1 || !ip2 || typeof ip1 !== "string" || typeof ip2 !== "string") {
6
+ return {
7
+ error: "Invalid IP addresses provided",
8
+ ip1: ip1 || "undefined",
9
+ ip2: ip2 || "undefined"
10
+ };
11
+ }
12
+ if (ip1 === ip2) {
13
+ return {
14
+ ipsMatch: true,
15
+ ip1,
16
+ ip2
17
+ };
18
+ }
19
+ const [ip1Info, ip2Info] = await Promise.all([
20
+ getIPInfo(ip1, apiUrl, apiKey),
21
+ getIPInfo(ip2, apiUrl, apiKey)
22
+ ]);
23
+ if (!ip1Info.isValid && !ip2Info.isValid) {
24
+ return {
25
+ error: "Failed to lookup both IP addresses",
26
+ ip1,
27
+ ip2,
28
+ ip1Error: ip1Info.error,
29
+ ip2Error: ip2Info.error
30
+ };
31
+ }
32
+ if (!ip1Info.isValid) {
33
+ return {
34
+ error: "Failed to lookup first IP address",
35
+ ip1,
36
+ ip2,
37
+ ip1Error: ip1Info.error
38
+ };
39
+ }
40
+ if (!ip2Info.isValid) {
41
+ return {
42
+ error: "Failed to lookup second IP address",
43
+ ip1,
44
+ ip2,
45
+ ip2Error: ip2Info.error
46
+ };
47
+ }
48
+ const determineConnectionType = (ipInfo) => {
49
+ if (ipInfo.isMobile) return "mobile";
50
+ if (ipInfo.isDatacenter) return "datacenter";
51
+ if (ipInfo.isSatellite) return "satellite";
52
+ if (ipInfo.providerType === "isp") return "residential";
53
+ switch (ipInfo.providerType) {
54
+ case "hosting":
55
+ return "datacenter";
56
+ case "business":
57
+ case "education":
58
+ case "government":
59
+ case "banking":
60
+ return "residential";
61
+ default:
62
+ return "unknown";
63
+ }
64
+ };
65
+ const ip1ConnectionType = determineConnectionType(ip1Info);
66
+ const ip2ConnectionType = determineConnectionType(ip2Info);
67
+ const differentConnectionTypes = ip1ConnectionType !== ip2ConnectionType;
68
+ const ip1Provider = ip1Info.providerName || ip1Info.asnOrganization || "Unknown";
69
+ const ip2Provider = ip2Info.providerName || ip2Info.asnOrganization || "Unknown";
70
+ const differentProviders = ip1Provider !== ip2Provider;
71
+ let distanceKm;
72
+ if (ip1Info.latitude !== void 0 && ip1Info.longitude !== void 0 && ip2Info.latitude !== void 0 && ip2Info.longitude !== void 0) {
73
+ const distanceMeters = getDistance(
74
+ { latitude: ip1Info.latitude, longitude: ip1Info.longitude },
75
+ { latitude: ip2Info.latitude, longitude: ip2Info.longitude }
76
+ );
77
+ distanceKm = distanceMeters / 1e3;
78
+ }
79
+ const ip1IsVpnOrProxy = ip1Info.isVPN || ip1Info.isProxy || ip1Info.isTor;
80
+ const ip2IsVpnOrProxy = ip2Info.isVPN || ip2Info.isProxy || ip2Info.isTor;
81
+ const anyVpnOrProxy = ip1IsVpnOrProxy || ip2IsVpnOrProxy;
82
+ const ip1Coordinates = ip1Info.latitude !== void 0 && ip1Info.longitude !== void 0 ? { latitude: ip1Info.latitude, longitude: ip1Info.longitude } : void 0;
83
+ const ip2Coordinates = ip2Info.latitude !== void 0 && ip2Info.longitude !== void 0 ? { latitude: ip2Info.latitude, longitude: ip2Info.longitude } : void 0;
84
+ return {
85
+ ipsMatch: false,
86
+ ip1,
87
+ ip2,
88
+ comparison: {
89
+ differentProviders,
90
+ differentConnectionTypes,
91
+ distanceKm,
92
+ anyVpnOrProxy,
93
+ ip1Details: {
94
+ provider: ip1Provider,
95
+ connectionType: ip1ConnectionType,
96
+ isVpnOrProxy: ip1IsVpnOrProxy,
97
+ country: ip1Info.country,
98
+ countryCode: ip1Info.countryCode,
99
+ city: ip1Info.city,
100
+ coordinates: ip1Coordinates
101
+ },
102
+ ip2Details: {
103
+ provider: ip2Provider,
104
+ connectionType: ip2ConnectionType,
105
+ isVpnOrProxy: ip2IsVpnOrProxy,
106
+ country: ip2Info.country,
107
+ countryCode: ip2Info.countryCode,
108
+ city: ip2Info.city,
109
+ coordinates: ip2Coordinates
110
+ }
111
+ }
112
+ };
113
+ } catch (error) {
114
+ return {
115
+ error: `Comparison failed: ${error instanceof Error ? error.message : String(error)}`,
116
+ ip1,
117
+ ip2
118
+ };
119
+ }
120
+ }
121
+ export {
122
+ compareIPs
123
+ };
@@ -0,0 +1,87 @@
1
+ async function getIPInfo(ip, apiUrl, apiKey, includeRawResponse = false) {
2
+ try {
3
+ if (!ip || typeof ip !== "string") {
4
+ return {
5
+ isValid: false,
6
+ error: "Invalid IP address provided",
7
+ ip: ip || "undefined"
8
+ };
9
+ }
10
+ const url = apiUrl;
11
+ const body = { q: ip };
12
+ if (apiKey) {
13
+ body.key = apiKey;
14
+ }
15
+ const response = await fetch(url, {
16
+ method: "POST",
17
+ headers: {
18
+ "Content-Type": "application/json",
19
+ Accept: "application/json"
20
+ },
21
+ body: JSON.stringify(body)
22
+ });
23
+ if (!response.ok) {
24
+ return {
25
+ isValid: false,
26
+ error: `API request failed with status ${response.status}: ${response.statusText}`,
27
+ ip
28
+ };
29
+ }
30
+ const data = await response.json();
31
+ if (data.is_bogon) {
32
+ return {
33
+ isValid: false,
34
+ error: "IP address is bogon (non-routable)",
35
+ ip
36
+ };
37
+ }
38
+ const result = {
39
+ ip: data.ip,
40
+ isValid: true,
41
+ // Threat indicators
42
+ isVPN: data.is_vpn,
43
+ isTor: data.is_tor,
44
+ isProxy: data.is_proxy,
45
+ isDatacenter: data.is_datacenter,
46
+ isAbuser: data.is_abuser,
47
+ isMobile: data.is_mobile,
48
+ isSatellite: data.is_satellite,
49
+ // Provider information
50
+ providerName: data.company?.name || data.datacenter?.datacenter,
51
+ providerType: data.company?.type || data.asn?.type,
52
+ asnNumber: data.asn?.asn,
53
+ asnOrganization: data.asn?.org,
54
+ // Geolocation
55
+ country: data.location?.country,
56
+ countryCode: data.location?.country_code,
57
+ region: data.location?.state,
58
+ city: data.location?.city,
59
+ latitude: data.location?.latitude,
60
+ longitude: data.location?.longitude,
61
+ timezone: data.location?.timezone,
62
+ // VPN specific details
63
+ vpnService: data.vpn?.service,
64
+ vpnType: data.vpn?.type,
65
+ // Risk scoring
66
+ abuserScore: Number.parseFloat(
67
+ data.asn?.abuser_score.split(" ")[0] || "0"
68
+ ),
69
+ companyAbuserScore: Number.parseFloat(
70
+ data.company?.abuser_score.split(" ")[0] || "0"
71
+ )
72
+ };
73
+ if (includeRawResponse) {
74
+ result.rawResponse = data;
75
+ }
76
+ return result;
77
+ } catch (error) {
78
+ return {
79
+ isValid: false,
80
+ error: `Network or parsing error: ${error instanceof Error ? error.message : String(error)}`,
81
+ ip
82
+ };
83
+ }
84
+ }
85
+ export {
86
+ getIPInfo
87
+ };