@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.
- package/CHANGELOG.md +578 -0
- package/dist/api/admin/apiRemoveDetectorKeyEndpoint.js +7 -4
- package/dist/api/blacklistRequestInspector.js +26 -20
- 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 +25 -19
- 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 +28 -25
- package/vite.test.config.ts +3 -2
- package/vite.threads.test.config.ts +33 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
const geolib = require("geolib");
|
|
4
|
+
const ipInfo = require("./ipInfo.cjs");
|
|
5
|
+
async function compareIPs(ip1, ip2, apiKey, apiUrl) {
|
|
6
|
+
try {
|
|
7
|
+
if (!ip1 || !ip2 || typeof ip1 !== "string" || typeof ip2 !== "string") {
|
|
8
|
+
return {
|
|
9
|
+
error: "Invalid IP addresses provided",
|
|
10
|
+
ip1: ip1 || "undefined",
|
|
11
|
+
ip2: ip2 || "undefined"
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
if (ip1 === ip2) {
|
|
15
|
+
return {
|
|
16
|
+
ipsMatch: true,
|
|
17
|
+
ip1,
|
|
18
|
+
ip2
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const [ip1Info, ip2Info] = await Promise.all([
|
|
22
|
+
ipInfo.getIPInfo(ip1, apiUrl, apiKey),
|
|
23
|
+
ipInfo.getIPInfo(ip2, apiUrl, apiKey)
|
|
24
|
+
]);
|
|
25
|
+
if (!ip1Info.isValid && !ip2Info.isValid) {
|
|
26
|
+
return {
|
|
27
|
+
error: "Failed to lookup both IP addresses",
|
|
28
|
+
ip1,
|
|
29
|
+
ip2,
|
|
30
|
+
ip1Error: ip1Info.error,
|
|
31
|
+
ip2Error: ip2Info.error
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (!ip1Info.isValid) {
|
|
35
|
+
return {
|
|
36
|
+
error: "Failed to lookup first IP address",
|
|
37
|
+
ip1,
|
|
38
|
+
ip2,
|
|
39
|
+
ip1Error: ip1Info.error
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
if (!ip2Info.isValid) {
|
|
43
|
+
return {
|
|
44
|
+
error: "Failed to lookup second IP address",
|
|
45
|
+
ip1,
|
|
46
|
+
ip2,
|
|
47
|
+
ip2Error: ip2Info.error
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
const determineConnectionType = (ipInfo2) => {
|
|
51
|
+
if (ipInfo2.isMobile) return "mobile";
|
|
52
|
+
if (ipInfo2.isDatacenter) return "datacenter";
|
|
53
|
+
if (ipInfo2.isSatellite) return "satellite";
|
|
54
|
+
if (ipInfo2.providerType === "isp") return "residential";
|
|
55
|
+
switch (ipInfo2.providerType) {
|
|
56
|
+
case "hosting":
|
|
57
|
+
return "datacenter";
|
|
58
|
+
case "business":
|
|
59
|
+
case "education":
|
|
60
|
+
case "government":
|
|
61
|
+
case "banking":
|
|
62
|
+
return "residential";
|
|
63
|
+
default:
|
|
64
|
+
return "unknown";
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const ip1ConnectionType = determineConnectionType(ip1Info);
|
|
68
|
+
const ip2ConnectionType = determineConnectionType(ip2Info);
|
|
69
|
+
const differentConnectionTypes = ip1ConnectionType !== ip2ConnectionType;
|
|
70
|
+
const ip1Provider = ip1Info.providerName || ip1Info.asnOrganization || "Unknown";
|
|
71
|
+
const ip2Provider = ip2Info.providerName || ip2Info.asnOrganization || "Unknown";
|
|
72
|
+
const differentProviders = ip1Provider !== ip2Provider;
|
|
73
|
+
let distanceKm;
|
|
74
|
+
if (ip1Info.latitude !== void 0 && ip1Info.longitude !== void 0 && ip2Info.latitude !== void 0 && ip2Info.longitude !== void 0) {
|
|
75
|
+
const distanceMeters = geolib.getDistance(
|
|
76
|
+
{ latitude: ip1Info.latitude, longitude: ip1Info.longitude },
|
|
77
|
+
{ latitude: ip2Info.latitude, longitude: ip2Info.longitude }
|
|
78
|
+
);
|
|
79
|
+
distanceKm = distanceMeters / 1e3;
|
|
80
|
+
}
|
|
81
|
+
const ip1IsVpnOrProxy = ip1Info.isVPN || ip1Info.isProxy || ip1Info.isTor;
|
|
82
|
+
const ip2IsVpnOrProxy = ip2Info.isVPN || ip2Info.isProxy || ip2Info.isTor;
|
|
83
|
+
const anyVpnOrProxy = ip1IsVpnOrProxy || ip2IsVpnOrProxy;
|
|
84
|
+
const ip1Coordinates = ip1Info.latitude !== void 0 && ip1Info.longitude !== void 0 ? { latitude: ip1Info.latitude, longitude: ip1Info.longitude } : void 0;
|
|
85
|
+
const ip2Coordinates = ip2Info.latitude !== void 0 && ip2Info.longitude !== void 0 ? { latitude: ip2Info.latitude, longitude: ip2Info.longitude } : void 0;
|
|
86
|
+
return {
|
|
87
|
+
ipsMatch: false,
|
|
88
|
+
ip1,
|
|
89
|
+
ip2,
|
|
90
|
+
comparison: {
|
|
91
|
+
differentProviders,
|
|
92
|
+
differentConnectionTypes,
|
|
93
|
+
distanceKm,
|
|
94
|
+
anyVpnOrProxy,
|
|
95
|
+
ip1Details: {
|
|
96
|
+
provider: ip1Provider,
|
|
97
|
+
connectionType: ip1ConnectionType,
|
|
98
|
+
isVpnOrProxy: ip1IsVpnOrProxy,
|
|
99
|
+
country: ip1Info.country,
|
|
100
|
+
countryCode: ip1Info.countryCode,
|
|
101
|
+
city: ip1Info.city,
|
|
102
|
+
coordinates: ip1Coordinates
|
|
103
|
+
},
|
|
104
|
+
ip2Details: {
|
|
105
|
+
provider: ip2Provider,
|
|
106
|
+
connectionType: ip2ConnectionType,
|
|
107
|
+
isVpnOrProxy: ip2IsVpnOrProxy,
|
|
108
|
+
country: ip2Info.country,
|
|
109
|
+
countryCode: ip2Info.countryCode,
|
|
110
|
+
city: ip2Info.city,
|
|
111
|
+
coordinates: ip2Coordinates
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
} catch (error) {
|
|
116
|
+
return {
|
|
117
|
+
error: `Comparison failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
118
|
+
ip1,
|
|
119
|
+
ip2
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
exports.compareIPs = compareIPs;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
async function getIPInfo(ip, apiUrl, apiKey, includeRawResponse = false) {
|
|
4
|
+
try {
|
|
5
|
+
if (!ip || typeof ip !== "string") {
|
|
6
|
+
return {
|
|
7
|
+
isValid: false,
|
|
8
|
+
error: "Invalid IP address provided",
|
|
9
|
+
ip: ip || "undefined"
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
const url = apiUrl;
|
|
13
|
+
const body = { q: ip };
|
|
14
|
+
if (apiKey) {
|
|
15
|
+
body.key = apiKey;
|
|
16
|
+
}
|
|
17
|
+
const response = await fetch(url, {
|
|
18
|
+
method: "POST",
|
|
19
|
+
headers: {
|
|
20
|
+
"Content-Type": "application/json",
|
|
21
|
+
Accept: "application/json"
|
|
22
|
+
},
|
|
23
|
+
body: JSON.stringify(body)
|
|
24
|
+
});
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
return {
|
|
27
|
+
isValid: false,
|
|
28
|
+
error: `API request failed with status ${response.status}: ${response.statusText}`,
|
|
29
|
+
ip
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const data = await response.json();
|
|
33
|
+
if (data.is_bogon) {
|
|
34
|
+
return {
|
|
35
|
+
isValid: false,
|
|
36
|
+
error: "IP address is bogon (non-routable)",
|
|
37
|
+
ip
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const result = {
|
|
41
|
+
ip: data.ip,
|
|
42
|
+
isValid: true,
|
|
43
|
+
// Threat indicators
|
|
44
|
+
isVPN: data.is_vpn,
|
|
45
|
+
isTor: data.is_tor,
|
|
46
|
+
isProxy: data.is_proxy,
|
|
47
|
+
isDatacenter: data.is_datacenter,
|
|
48
|
+
isAbuser: data.is_abuser,
|
|
49
|
+
isMobile: data.is_mobile,
|
|
50
|
+
isSatellite: data.is_satellite,
|
|
51
|
+
// Provider information
|
|
52
|
+
providerName: data.company?.name || data.datacenter?.datacenter,
|
|
53
|
+
providerType: data.company?.type || data.asn?.type,
|
|
54
|
+
asnNumber: data.asn?.asn,
|
|
55
|
+
asnOrganization: data.asn?.org,
|
|
56
|
+
// Geolocation
|
|
57
|
+
country: data.location?.country,
|
|
58
|
+
countryCode: data.location?.country_code,
|
|
59
|
+
region: data.location?.state,
|
|
60
|
+
city: data.location?.city,
|
|
61
|
+
latitude: data.location?.latitude,
|
|
62
|
+
longitude: data.location?.longitude,
|
|
63
|
+
timezone: data.location?.timezone,
|
|
64
|
+
// VPN specific details
|
|
65
|
+
vpnService: data.vpn?.service,
|
|
66
|
+
vpnType: data.vpn?.type,
|
|
67
|
+
// Risk scoring
|
|
68
|
+
abuserScore: Number.parseFloat(
|
|
69
|
+
data.asn?.abuser_score.split(" ")[0] || "0"
|
|
70
|
+
),
|
|
71
|
+
companyAbuserScore: Number.parseFloat(
|
|
72
|
+
data.company?.abuser_score.split(" ")[0] || "0"
|
|
73
|
+
)
|
|
74
|
+
};
|
|
75
|
+
if (includeRawResponse) {
|
|
76
|
+
result.rawResponse = data;
|
|
77
|
+
}
|
|
78
|
+
return result;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return {
|
|
81
|
+
isValid: false,
|
|
82
|
+
error: `Network or parsing error: ${error instanceof Error ? error.message : String(error)}`,
|
|
83
|
+
ip
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
exports.getIPInfo = getIPInfo;
|
|
@@ -15,7 +15,20 @@ class CaptchaManager {
|
|
|
15
15
|
);
|
|
16
16
|
return tokenRecord ? tokenRecord._id : void 0;
|
|
17
17
|
}
|
|
18
|
-
async
|
|
18
|
+
async validateFrictionlessTokenIP(sessionRecord, currentIP, env) {
|
|
19
|
+
const tokenRecord = await this.db.getFrictionlessTokenRecordByTokenId(
|
|
20
|
+
sessionRecord.tokenId
|
|
21
|
+
);
|
|
22
|
+
if (!tokenRecord) {
|
|
23
|
+
this.logger.info(() => ({
|
|
24
|
+
msg: "No frictionless token found for session",
|
|
25
|
+
data: { sessionId: sessionRecord.sessionId }
|
|
26
|
+
}));
|
|
27
|
+
return { valid: false, reason: "CAPTCHA.NO_SESSION_FOUND" };
|
|
28
|
+
}
|
|
29
|
+
return { valid: true };
|
|
30
|
+
}
|
|
31
|
+
async isValidRequest(clientSettings, requestedCaptchaType, env, sessionId, userAccessPolicy, currentIP) {
|
|
19
32
|
this.logger.debug(() => ({
|
|
20
33
|
msg: "Validating request",
|
|
21
34
|
data: {
|
|
@@ -54,11 +67,45 @@ class CaptchaManager {
|
|
|
54
67
|
type: requestedCaptchaType
|
|
55
68
|
};
|
|
56
69
|
}
|
|
70
|
+
if (currentIP) {
|
|
71
|
+
const ipValidation = await this.validateFrictionlessTokenIP(
|
|
72
|
+
sessionRecord,
|
|
73
|
+
currentIP,
|
|
74
|
+
env
|
|
75
|
+
);
|
|
76
|
+
if (!ipValidation.valid) {
|
|
77
|
+
return {
|
|
78
|
+
valid: false,
|
|
79
|
+
reason: ipValidation.reason,
|
|
80
|
+
type: requestedCaptchaType
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
57
84
|
const frictionlessTokenId = await this.getFrictionlessTokenIdFromSession(sessionRecord);
|
|
85
|
+
if (sessionRecord.captchaType !== requestedCaptchaType) {
|
|
86
|
+
this.logger.warn(() => ({
|
|
87
|
+
msg: "Invalid frictionless request",
|
|
88
|
+
data: {
|
|
89
|
+
account: clientSettings.account,
|
|
90
|
+
sessionId
|
|
91
|
+
}
|
|
92
|
+
}));
|
|
93
|
+
return {
|
|
94
|
+
valid: false,
|
|
95
|
+
reason: "CAPTCHA.NO_SESSION_FOUND",
|
|
96
|
+
type: requestedCaptchaType
|
|
97
|
+
};
|
|
98
|
+
}
|
|
58
99
|
return {
|
|
59
100
|
valid: true,
|
|
60
101
|
frictionlessTokenId,
|
|
61
|
-
type: requestedCaptchaType
|
|
102
|
+
type: requestedCaptchaType,
|
|
103
|
+
...sessionRecord.powDifficulty && {
|
|
104
|
+
powDifficulty: sessionRecord.powDifficulty
|
|
105
|
+
},
|
|
106
|
+
...sessionRecord.solvedImagesCount && {
|
|
107
|
+
solvedImagesCount: sessionRecord.solvedImagesCount
|
|
108
|
+
}
|
|
62
109
|
};
|
|
63
110
|
}
|
|
64
111
|
this.logger.warn(() => ({
|
|
@@ -126,11 +126,10 @@ class ClientTaskManager {
|
|
|
126
126
|
threshold: 0
|
|
127
127
|
};
|
|
128
128
|
}
|
|
129
|
+
const { _id, token, ...tokenRecordWithoutId } = tokenRecord;
|
|
129
130
|
return {
|
|
130
131
|
...record,
|
|
131
|
-
|
|
132
|
-
scoreComponents: tokenRecord?.scoreComponents,
|
|
133
|
-
threshold: tokenRecord?.threshold || 0
|
|
132
|
+
...tokenRecordWithoutId
|
|
134
133
|
};
|
|
135
134
|
});
|
|
136
135
|
if (filteredBatch.length > 0) {
|
|
@@ -138,6 +137,9 @@ class ClientTaskManager {
|
|
|
138
137
|
await this.providerDB.markSessionRecordsStored(
|
|
139
138
|
filteredBatch.map((record) => record.sessionId)
|
|
140
139
|
);
|
|
140
|
+
await this.providerDB.markFrictionlessTokenRecordsStored(
|
|
141
|
+
filteredBatch.map((record) => record.tokenId).filter((id) => !!id)
|
|
142
|
+
);
|
|
141
143
|
}
|
|
142
144
|
processedSessionRecords += filteredBatch.length;
|
|
143
145
|
}
|
|
@@ -248,25 +250,44 @@ class ClientTaskManager {
|
|
|
248
250
|
const activeDetectorKeys = await this.providerDB.getDetectorKeys();
|
|
249
251
|
return activeDetectorKeys;
|
|
250
252
|
}
|
|
251
|
-
async removeDetectorKey(detectorKey) {
|
|
253
|
+
async removeDetectorKey(detectorKey, expirationInSeconds) {
|
|
252
254
|
if (!isValidPrivateKey(detectorKey)) {
|
|
253
255
|
throw new common.ProsopoApiError("INVALID_DETECTOR_KEY", {
|
|
254
256
|
context: { detectorKey },
|
|
255
257
|
logger: this.logger
|
|
256
258
|
});
|
|
257
259
|
}
|
|
258
|
-
await this.providerDB.removeDetectorKey(detectorKey);
|
|
260
|
+
await this.providerDB.removeDetectorKey(detectorKey, expirationInSeconds);
|
|
259
261
|
}
|
|
260
|
-
|
|
262
|
+
/**
|
|
263
|
+
* Matches a request referrer against an allowed domain pattern.
|
|
264
|
+
* Supports global '*', subdomain '*.example.com', glob '*example*',
|
|
265
|
+
* plain domains (exact or subdomain), and 'localhost'.
|
|
266
|
+
*/
|
|
267
|
+
domainPatternMatcher(referrer, clientDomain) {
|
|
261
268
|
if (!referrer || !clientDomain) return false;
|
|
262
|
-
if (clientDomain === "*") return true;
|
|
263
269
|
try {
|
|
264
|
-
const
|
|
265
|
-
const
|
|
266
|
-
|
|
267
|
-
|
|
270
|
+
const referrerHost = util.parseUrl(referrer).hostname.replace(/\.$/, "");
|
|
271
|
+
const pattern = clientDomain.trim().toLowerCase();
|
|
272
|
+
if (pattern === "*") return true;
|
|
273
|
+
if (pattern === "localhost") {
|
|
274
|
+
return referrerHost === "localhost" || referrerHost.startsWith("localhost:");
|
|
275
|
+
}
|
|
276
|
+
if (pattern.startsWith("*.")) {
|
|
277
|
+
const suffix = pattern.slice(2);
|
|
278
|
+
const allowed = util.parseUrl(suffix).hostname.replace(/\.$/, "");
|
|
279
|
+
return referrerHost.endsWith(`.${allowed}`) || referrerHost === allowed;
|
|
280
|
+
}
|
|
281
|
+
if (pattern.includes("*")) {
|
|
282
|
+
const escaped = pattern.replace(/[.+?^${}()|\[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
283
|
+
const regex = new RegExp(`^${escaped}$`, "i");
|
|
284
|
+
return regex.test(referrerHost);
|
|
285
|
+
}
|
|
286
|
+
const allowedHost = util.parseUrl(pattern).hostname.replace(/\.$/, "");
|
|
287
|
+
return referrerHost === allowedHost || referrerHost.endsWith(`.${allowedHost}`);
|
|
288
|
+
} catch (e) {
|
|
268
289
|
this.logger.error(() => ({
|
|
269
|
-
msg: "Error in
|
|
290
|
+
msg: "Error in domainPatternMatcher",
|
|
270
291
|
data: { referrer, clientDomain }
|
|
271
292
|
}));
|
|
272
293
|
return false;
|