@prosopo/provider 3.3.0 → 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.
- package/CHANGELOG.md +545 -0
- package/dist/api/admin/apiRemoveDetectorKeyEndpoint.js +7 -4
- package/dist/api/blacklistRequestInspector.js +11 -9
- package/dist/api/captcha.js +121 -33
- package/dist/api/domainMiddleware.js +8 -8
- package/dist/api/headerCheckMiddleware.js +4 -0
- package/dist/api/ignoreMiddleware.js +4 -1
- package/dist/api/ja4Middleware.js +5 -23
- package/dist/api/public.js +26 -3
- package/dist/api/verify.js +4 -2
- package/dist/cjs/api/admin/apiRemoveDetectorKeyEndpoint.cjs +6 -3
- package/dist/cjs/api/blacklistRequestInspector.cjs +11 -9
- package/dist/cjs/api/captcha.cjs +121 -33
- package/dist/cjs/api/domainMiddleware.cjs +8 -8
- package/dist/cjs/api/headerCheckMiddleware.cjs +4 -0
- package/dist/cjs/api/ignoreMiddleware.cjs +3 -0
- package/dist/cjs/api/ja4Middleware.cjs +4 -22
- package/dist/cjs/api/public.cjs +26 -3
- package/dist/cjs/api/verify.cjs +4 -2
- package/dist/cjs/compositeIpAddress.cjs +53 -0
- package/dist/cjs/index.cjs +7 -0
- package/dist/cjs/pairs.cjs +27 -0
- package/dist/cjs/services/ipComparison.cjs +123 -0
- package/dist/cjs/services/ipInfo.cjs +87 -0
- package/dist/cjs/tasks/captchaManager.cjs +49 -2
- package/dist/cjs/tasks/client/clientTasks.cjs +33 -12
- package/dist/cjs/tasks/detection/decodePayload.cjs +712 -278
- package/dist/cjs/tasks/detection/getBotScore.cjs +14 -3
- package/dist/cjs/tasks/frictionless/frictionlessTasks.cjs +128 -24
- package/dist/cjs/tasks/frictionless/frictionlessTasksUtils.cjs +17 -0
- package/dist/cjs/tasks/imgCaptcha/imgCaptchaTasks.cjs +62 -20
- package/dist/cjs/tasks/powCaptcha/powTasks.cjs +43 -15
- package/dist/cjs/util.cjs +248 -16
- package/dist/cjs/utils/hashUserAgent.cjs +10 -0
- package/dist/compositeIpAddress.js +53 -0
- package/dist/index.js +8 -1
- package/dist/pairs.js +27 -0
- package/dist/services/ipComparison.js +123 -0
- package/dist/services/ipInfo.js +87 -0
- package/dist/tasks/captchaManager.js +49 -2
- package/dist/tasks/client/clientTasks.js +33 -12
- package/dist/tasks/detection/decodePayload.js +712 -278
- package/dist/tasks/detection/getBotScore.js +15 -4
- package/dist/tasks/frictionless/frictionlessTasks.js +128 -24
- package/dist/tasks/frictionless/frictionlessTasksUtils.js +18 -1
- package/dist/tasks/imgCaptcha/imgCaptchaTasks.js +64 -22
- package/dist/tasks/powCaptcha/powTasks.js +44 -16
- package/dist/util.js +249 -17
- package/dist/utils/hashUserAgent.js +10 -0
- package/package.json +26 -23
- package/vite.test.config.ts +3 -2
- 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,
|
|
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
|
-
|
|
84
|
-
if (ipV4orV6Address.v4 && !
|
|
85
|
-
|
|
86
|
-
|
|
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 (
|
|
95
|
-
const errorMessage = `IP address mismatch: ${
|
|
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:
|
|
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
|
+
};
|