@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
@@ -13,7 +13,20 @@ class CaptchaManager {
13
13
  );
14
14
  return tokenRecord ? tokenRecord._id : void 0;
15
15
  }
16
- async isValidRequest(clientSettings, requestedCaptchaType, sessionId, userAccessPolicy) {
16
+ async validateFrictionlessTokenIP(sessionRecord, currentIP, env) {
17
+ const tokenRecord = await this.db.getFrictionlessTokenRecordByTokenId(
18
+ sessionRecord.tokenId
19
+ );
20
+ if (!tokenRecord) {
21
+ this.logger.info(() => ({
22
+ msg: "No frictionless token found for session",
23
+ data: { sessionId: sessionRecord.sessionId }
24
+ }));
25
+ return { valid: false, reason: "CAPTCHA.NO_SESSION_FOUND" };
26
+ }
27
+ return { valid: true };
28
+ }
29
+ async isValidRequest(clientSettings, requestedCaptchaType, env, sessionId, userAccessPolicy, currentIP) {
17
30
  this.logger.debug(() => ({
18
31
  msg: "Validating request",
19
32
  data: {
@@ -52,11 +65,45 @@ class CaptchaManager {
52
65
  type: requestedCaptchaType
53
66
  };
54
67
  }
68
+ if (currentIP) {
69
+ const ipValidation = await this.validateFrictionlessTokenIP(
70
+ sessionRecord,
71
+ currentIP,
72
+ env
73
+ );
74
+ if (!ipValidation.valid) {
75
+ return {
76
+ valid: false,
77
+ reason: ipValidation.reason,
78
+ type: requestedCaptchaType
79
+ };
80
+ }
81
+ }
55
82
  const frictionlessTokenId = await this.getFrictionlessTokenIdFromSession(sessionRecord);
83
+ if (sessionRecord.captchaType !== requestedCaptchaType) {
84
+ this.logger.warn(() => ({
85
+ msg: "Invalid frictionless request",
86
+ data: {
87
+ account: clientSettings.account,
88
+ sessionId
89
+ }
90
+ }));
91
+ return {
92
+ valid: false,
93
+ reason: "CAPTCHA.NO_SESSION_FOUND",
94
+ type: requestedCaptchaType
95
+ };
96
+ }
56
97
  return {
57
98
  valid: true,
58
99
  frictionlessTokenId,
59
- type: requestedCaptchaType
100
+ type: requestedCaptchaType,
101
+ ...sessionRecord.powDifficulty && {
102
+ powDifficulty: sessionRecord.powDifficulty
103
+ },
104
+ ...sessionRecord.solvedImagesCount && {
105
+ solvedImagesCount: sessionRecord.solvedImagesCount
106
+ }
60
107
  };
61
108
  }
62
109
  this.logger.warn(() => ({
@@ -124,11 +124,10 @@ class ClientTaskManager {
124
124
  threshold: 0
125
125
  };
126
126
  }
127
+ const { _id, token, ...tokenRecordWithoutId } = tokenRecord;
127
128
  return {
128
129
  ...record,
129
- score: tokenRecord?.score || 0,
130
- scoreComponents: tokenRecord?.scoreComponents,
131
- threshold: tokenRecord?.threshold || 0
130
+ ...tokenRecordWithoutId
132
131
  };
133
132
  });
134
133
  if (filteredBatch.length > 0) {
@@ -136,6 +135,9 @@ class ClientTaskManager {
136
135
  await this.providerDB.markSessionRecordsStored(
137
136
  filteredBatch.map((record) => record.sessionId)
138
137
  );
138
+ await this.providerDB.markFrictionlessTokenRecordsStored(
139
+ filteredBatch.map((record) => record.tokenId).filter((id) => !!id)
140
+ );
139
141
  }
140
142
  processedSessionRecords += filteredBatch.length;
141
143
  }
@@ -246,25 +248,44 @@ class ClientTaskManager {
246
248
  const activeDetectorKeys = await this.providerDB.getDetectorKeys();
247
249
  return activeDetectorKeys;
248
250
  }
249
- async removeDetectorKey(detectorKey) {
251
+ async removeDetectorKey(detectorKey, expirationInSeconds) {
250
252
  if (!isValidPrivateKey(detectorKey)) {
251
253
  throw new ProsopoApiError("INVALID_DETECTOR_KEY", {
252
254
  context: { detectorKey },
253
255
  logger: this.logger
254
256
  });
255
257
  }
256
- await this.providerDB.removeDetectorKey(detectorKey);
258
+ await this.providerDB.removeDetectorKey(detectorKey, expirationInSeconds);
257
259
  }
258
- isSubdomainOrExactMatch(referrer, clientDomain) {
260
+ /**
261
+ * Matches a request referrer against an allowed domain pattern.
262
+ * Supports global '*', subdomain '*.example.com', glob '*example*',
263
+ * plain domains (exact or subdomain), and 'localhost'.
264
+ */
265
+ domainPatternMatcher(referrer, clientDomain) {
259
266
  if (!referrer || !clientDomain) return false;
260
- if (clientDomain === "*") return true;
261
267
  try {
262
- const referrerDomain = parseUrl(referrer).hostname.replace(/\.$/, "");
263
- const allowedDomain = parseUrl(clientDomain).hostname.replace(/\.$/, "");
264
- return referrerDomain === allowedDomain || referrerDomain.endsWith(`.${allowedDomain}`);
265
- } catch {
268
+ const referrerHost = parseUrl(referrer).hostname.replace(/\.$/, "");
269
+ const pattern = clientDomain.trim().toLowerCase();
270
+ if (pattern === "*") return true;
271
+ if (pattern === "localhost") {
272
+ return referrerHost === "localhost" || referrerHost.startsWith("localhost:");
273
+ }
274
+ if (pattern.startsWith("*.")) {
275
+ const suffix = pattern.slice(2);
276
+ const allowed = parseUrl(suffix).hostname.replace(/\.$/, "");
277
+ return referrerHost.endsWith(`.${allowed}`) || referrerHost === allowed;
278
+ }
279
+ if (pattern.includes("*")) {
280
+ const escaped = pattern.replace(/[.+?^${}()|\[\]\\]/g, "\\$&").replace(/\*/g, ".*");
281
+ const regex = new RegExp(`^${escaped}$`, "i");
282
+ return regex.test(referrerHost);
283
+ }
284
+ const allowedHost = parseUrl(pattern).hostname.replace(/\.$/, "");
285
+ return referrerHost === allowedHost || referrerHost.endsWith(`.${allowedHost}`);
286
+ } catch (e) {
266
287
  this.logger.error(() => ({
267
- msg: "Error in isSubdomainOrExactMatch",
288
+ msg: "Error in domainPatternMatcher",
268
289
  data: { referrer, clientDomain }
269
290
  }));
270
291
  return false;