@prosopo/provider 4.15.14 → 4.15.16

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 (232) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +106 -106
  2. package/.turbo/turbo-build$colon$tsc.log +24 -24
  3. package/.turbo/turbo-build.log +107 -104
  4. package/CHANGELOG.md +63 -0
  5. package/dist/_virtual/_rolldown/runtime.js +3 -0
  6. package/dist/api/admin/apiAdminRoutesProvider.js +27 -46
  7. package/dist/api/admin/apiClearAllCountersEndpoint.js +62 -57
  8. package/dist/api/admin/apiDnsEventEndpoint.js +79 -92
  9. package/dist/api/admin/apiGetAllDecisionMachinesEndpoint.js +43 -45
  10. package/dist/api/admin/apiGetDecisionMachineEndpoint.js +40 -40
  11. package/dist/api/admin/apiRegisterSiteKeyEndpoint.js +23 -22
  12. package/dist/api/admin/apiRegisterSiteKeysEndpoint.js +26 -30
  13. package/dist/api/admin/apiRemoveAllDecisionMachinesEndpoint.js +35 -37
  14. package/dist/api/admin/apiRemoveDecisionMachineEndpoint.js +40 -40
  15. package/dist/api/admin/apiRemoveDetectorKeyEndpoint.d.ts.map +1 -1
  16. package/dist/api/admin/apiRemoveDetectorKeyEndpoint.js +29 -31
  17. package/dist/api/admin/apiRemoveSiteKeyEndpoint.js +33 -29
  18. package/dist/api/admin/apiRemoveSiteKeysEndpoint.js +32 -31
  19. package/dist/api/admin/apiToggleMaintenanceModeEndpoint.js +43 -35
  20. package/dist/api/admin/apiUpdateDecisionMachineEndpoint.js +43 -63
  21. package/dist/api/admin/apiUpdateDetectorKeyEndpoint.js +36 -35
  22. package/dist/api/admin/createApiAdminRoutesProvider.js +6 -8
  23. package/dist/api/blacklistRequestInspector.js +285 -377
  24. package/dist/api/block.js +17 -30
  25. package/dist/api/captcha/checkSpamEmail.js +88 -98
  26. package/dist/api/captcha/contextAwareValidation.js +21 -15
  27. package/dist/api/captcha/getFrictionlessCaptchaChallenge/accessPolicy.js +126 -140
  28. package/dist/api/captcha/getFrictionlessCaptchaChallenge/constants.js +11 -12
  29. package/dist/api/captcha/getFrictionlessCaptchaChallenge/decisionMachine.js +236 -299
  30. package/dist/api/captcha/getFrictionlessCaptchaChallenge/decryptSimdReadings.js +10 -13
  31. package/dist/api/captcha/getFrictionlessCaptchaChallenge/handler.js +336 -462
  32. package/dist/api/captcha/getFrictionlessCaptchaChallenge/honeypotResponse.js +17 -14
  33. package/dist/api/captcha/getFrictionlessCaptchaChallenge/sessionDedup.js +27 -27
  34. package/dist/api/captcha/getFrictionlessCaptchaChallenge/shortCircuit.js +42 -63
  35. package/dist/api/captcha/getFrictionlessCaptchaChallenge.js +2 -4
  36. package/dist/api/captcha/getImageCaptchaChallenge.js +136 -207
  37. package/dist/api/captcha/getPoWCaptchaChallenge.js +128 -198
  38. package/dist/api/captcha/getPuzzleCaptchaChallenge.js +132 -201
  39. package/dist/api/captcha/maintenanceModeResponses.js +38 -47
  40. package/dist/api/captcha/submitImageCaptchaSolution.js +75 -101
  41. package/dist/api/captcha/submitPoWCaptchaSolution.js +137 -207
  42. package/dist/api/captcha/submitPuzzleCaptchaSolution.js +73 -105
  43. package/dist/api/captcha.js +76 -58
  44. package/dist/api/dnsEventUrl.js +20 -22
  45. package/dist/api/domainMiddleware.js +104 -115
  46. package/dist/api/forwardVerify.js +62 -48
  47. package/dist/api/handshakeTimingMiddleware.js +43 -62
  48. package/dist/api/hardBlockVerdictCache.js +114 -117
  49. package/dist/api/headerCheckMiddleware.js +34 -34
  50. package/dist/api/ignoreMiddleware.js +13 -15
  51. package/dist/api/ipInfoMiddleware.js +17 -19
  52. package/dist/api/ja4.js +149 -188
  53. package/dist/api/ja4Middleware.js +39 -54
  54. package/dist/api/metrics.js +245 -257
  55. package/dist/api/public.js +52 -54
  56. package/dist/api/robotsMiddleware.js +10 -10
  57. package/dist/api/startProviderApi.js +205 -232
  58. package/dist/api/testSiteKey.js +16 -16
  59. package/dist/api/validateAddress.js +23 -21
  60. package/dist/api/verify.js +334 -377
  61. package/dist/cjs/_virtual/_rolldown/runtime.cjs +23 -0
  62. package/dist/cjs/api/admin/apiAdminRoutesProvider.cjs +40 -59
  63. package/dist/cjs/api/admin/apiClearAllCountersEndpoint.cjs +63 -58
  64. package/dist/cjs/api/admin/apiDnsEventEndpoint.cjs +82 -93
  65. package/dist/cjs/api/admin/apiGetAllDecisionMachinesEndpoint.cjs +46 -48
  66. package/dist/cjs/api/admin/apiGetDecisionMachineEndpoint.cjs +41 -41
  67. package/dist/cjs/api/admin/apiRegisterSiteKeyEndpoint.cjs +25 -24
  68. package/dist/cjs/api/admin/apiRegisterSiteKeysEndpoint.cjs +28 -32
  69. package/dist/cjs/api/admin/apiRemoveAllDecisionMachinesEndpoint.cjs +38 -40
  70. package/dist/cjs/api/admin/apiRemoveDecisionMachineEndpoint.cjs +41 -41
  71. package/dist/cjs/api/admin/apiRemoveDetectorKeyEndpoint.cjs +31 -33
  72. package/dist/cjs/api/admin/apiRemoveSiteKeyEndpoint.cjs +35 -31
  73. package/dist/cjs/api/admin/apiRemoveSiteKeysEndpoint.cjs +34 -33
  74. package/dist/cjs/api/admin/apiToggleMaintenanceModeEndpoint.cjs +45 -35
  75. package/dist/cjs/api/admin/apiUpdateDecisionMachineEndpoint.cjs +45 -65
  76. package/dist/cjs/api/admin/apiUpdateDetectorKeyEndpoint.cjs +38 -37
  77. package/dist/cjs/api/admin/createApiAdminRoutesProvider.cjs +7 -8
  78. package/dist/cjs/api/blacklistRequestInspector.cjs +288 -376
  79. package/dist/cjs/api/block.cjs +18 -30
  80. package/dist/cjs/api/captcha/checkSpamEmail.cjs +90 -100
  81. package/dist/cjs/api/captcha/contextAwareValidation.cjs +21 -14
  82. package/dist/cjs/api/captcha/getFrictionlessCaptchaChallenge/accessPolicy.cjs +125 -140
  83. package/dist/cjs/api/captcha/getFrictionlessCaptchaChallenge/constants.cjs +10 -10
  84. package/dist/cjs/api/captcha/getFrictionlessCaptchaChallenge/decisionMachine.cjs +238 -301
  85. package/dist/cjs/api/captcha/getFrictionlessCaptchaChallenge/decryptSimdReadings.cjs +9 -12
  86. package/dist/cjs/api/captcha/getFrictionlessCaptchaChallenge/handler.cjs +349 -473
  87. package/dist/cjs/api/captcha/getFrictionlessCaptchaChallenge/honeypotResponse.cjs +18 -14
  88. package/dist/cjs/api/captcha/getFrictionlessCaptchaChallenge/sessionDedup.cjs +26 -26
  89. package/dist/cjs/api/captcha/getFrictionlessCaptchaChallenge/shortCircuit.cjs +42 -63
  90. package/dist/cjs/api/captcha/getFrictionlessCaptchaChallenge.cjs +2 -3
  91. package/dist/cjs/api/captcha/getImageCaptchaChallenge.cjs +138 -208
  92. package/dist/cjs/api/captcha/getPoWCaptchaChallenge.cjs +131 -200
  93. package/dist/cjs/api/captcha/getPuzzleCaptchaChallenge.cjs +135 -203
  94. package/dist/cjs/api/captcha/maintenanceModeResponses.cjs +38 -44
  95. package/dist/cjs/api/captcha/submitImageCaptchaSolution.cjs +77 -102
  96. package/dist/cjs/api/captcha/submitPoWCaptchaSolution.cjs +140 -209
  97. package/dist/cjs/api/captcha/submitPuzzleCaptchaSolution.cjs +76 -107
  98. package/dist/cjs/api/captcha.cjs +79 -58
  99. package/dist/cjs/api/dnsEventUrl.cjs +20 -21
  100. package/dist/cjs/api/domainMiddleware.cjs +106 -117
  101. package/dist/cjs/api/forwardVerify.cjs +63 -49
  102. package/dist/cjs/api/handshakeTimingMiddleware.cjs +43 -61
  103. package/dist/cjs/api/hardBlockVerdictCache.cjs +114 -114
  104. package/dist/cjs/api/headerCheckMiddleware.cjs +34 -34
  105. package/dist/cjs/api/ignoreMiddleware.cjs +12 -14
  106. package/dist/cjs/api/ipInfoMiddleware.cjs +16 -18
  107. package/dist/cjs/api/ja4.cjs +149 -187
  108. package/dist/cjs/api/ja4Middleware.cjs +40 -53
  109. package/dist/cjs/api/metrics.cjs +229 -228
  110. package/dist/cjs/api/public.cjs +57 -57
  111. package/dist/cjs/api/robotsMiddleware.cjs +9 -9
  112. package/dist/cjs/api/startProviderApi.cjs +219 -237
  113. package/dist/cjs/api/testSiteKey.cjs +15 -14
  114. package/dist/cjs/api/validateAddress.cjs +24 -21
  115. package/dist/cjs/api/verify.cjs +338 -379
  116. package/dist/cjs/compositeIpAddress.cjs +40 -47
  117. package/dist/cjs/index.cjs +65 -71
  118. package/dist/cjs/pairs.cjs +31 -35
  119. package/dist/cjs/rules/lang.cjs +10 -14
  120. package/dist/cjs/schedulers/captchaScheduler.cjs +26 -32
  121. package/dist/cjs/schedulers/getClientList.cjs +35 -33
  122. package/dist/cjs/schedulers/setClientEntropy.cjs +35 -33
  123. package/dist/cjs/schedulers/updateSpamEmailDomains.cjs +39 -43
  124. package/dist/cjs/services/ipComparison.cjs +122 -119
  125. package/dist/cjs/tasks/captchaManager.cjs +407 -486
  126. package/dist/cjs/tasks/client/clientTasks.cjs +347 -470
  127. package/dist/cjs/tasks/dataset/datasetTasks.cjs +25 -29
  128. package/dist/cjs/tasks/dataset/datasetTasksUtils.cjs +16 -32
  129. package/dist/cjs/tasks/decisionMachine/decisionMachineRunner.cjs +220 -307
  130. package/dist/cjs/tasks/detection/decodeBehavior.cjs +408 -317
  131. package/dist/cjs/tasks/detection/decodePayload.cjs +686 -468
  132. package/dist/cjs/tasks/detection/decodeSimd.cjs +462 -304
  133. package/dist/cjs/tasks/detection/getBotScore.cjs +37 -43
  134. package/dist/cjs/tasks/dnsEvent/enrichDnsEvent.cjs +88 -71
  135. package/dist/cjs/tasks/frictionless/frictionlessTasks.cjs +348 -486
  136. package/dist/cjs/tasks/frictionless/frictionlessTasksUtils.cjs +13 -30
  137. package/dist/cjs/tasks/frictionless/routingMachine.cjs +71 -55
  138. package/dist/cjs/tasks/imgCaptcha/imgCaptchaTasks.cjs +555 -806
  139. package/dist/cjs/tasks/imgCaptcha/imgCaptchaTasksUtils.cjs +22 -23
  140. package/dist/cjs/tasks/index.cjs +2 -4
  141. package/dist/cjs/tasks/powCaptcha/powTasks.cjs +491 -687
  142. package/dist/cjs/tasks/powCaptcha/powTasksUtils.cjs +17 -29
  143. package/dist/cjs/tasks/puzzleCaptcha/puzzleTasks.cjs +422 -591
  144. package/dist/cjs/tasks/puzzleCaptcha/puzzleTasksUtils.cjs +8 -5
  145. package/dist/cjs/tasks/spam/checkSpamEmail.cjs +133 -141
  146. package/dist/cjs/tasks/spam/checkTrafficFilter.cjs +118 -76
  147. package/dist/cjs/tasks/spam/evaluateEmailSpamRules.cjs +75 -84
  148. package/dist/cjs/tasks/spam/updateSpamEmailDomains.cjs +40 -56
  149. package/dist/cjs/tasks/tasks.cjs +107 -158
  150. package/dist/cjs/util/usageCounters.cjs +220 -211
  151. package/dist/cjs/util.cjs +329 -359
  152. package/dist/cjs/utils/devicePlatform.cjs +14 -8
  153. package/dist/cjs/utils/dns.cjs +68 -93
  154. package/dist/cjs/utils/hashUserAgent.cjs +14 -7
  155. package/dist/cjs/utils/hashUserIp.cjs +14 -6
  156. package/dist/cjs/utils/honeypot/encoders.cjs +74 -80
  157. package/dist/cjs/utils/honeypot/phraseBank.cjs +51 -44
  158. package/dist/cjs/utils/normalizeRequestIp.cjs +33 -25
  159. package/dist/compositeIpAddress.js +38 -46
  160. package/dist/index.js +12 -56
  161. package/dist/pairs.js +31 -37
  162. package/dist/rules/lang.js +11 -15
  163. package/dist/schedulers/captchaScheduler.js +25 -31
  164. package/dist/schedulers/getClientList.js +34 -32
  165. package/dist/schedulers/setClientEntropy.js +34 -32
  166. package/dist/schedulers/updateSpamEmailDomains.js +38 -42
  167. package/dist/services/ipComparison.js +122 -119
  168. package/dist/tasks/captchaManager.js +403 -483
  169. package/dist/tasks/client/clientTasks.js +343 -467
  170. package/dist/tasks/dataset/datasetTasks.js +24 -28
  171. package/dist/tasks/dataset/datasetTasksUtils.js +15 -31
  172. package/dist/tasks/decisionMachine/decisionMachineRunner.js +217 -306
  173. package/dist/tasks/detection/getBotScore.js +37 -43
  174. package/dist/tasks/dnsEvent/enrichDnsEvent.d.ts.map +1 -1
  175. package/dist/tasks/dnsEvent/enrichDnsEvent.js +88 -74
  176. package/dist/tasks/dnsEvent/enrichDnsEvent.js.map +1 -1
  177. package/dist/tasks/frictionless/frictionlessTasks.js +342 -479
  178. package/dist/tasks/frictionless/frictionlessTasksUtils.js +14 -32
  179. package/dist/tasks/frictionless/routingMachine.js +72 -56
  180. package/dist/tasks/imgCaptcha/imgCaptchaTasks.js +550 -801
  181. package/dist/tasks/imgCaptcha/imgCaptchaTasksUtils.js +21 -22
  182. package/dist/tasks/index.js +1 -3
  183. package/dist/tasks/powCaptcha/powTasks.d.ts.map +1 -1
  184. package/dist/tasks/powCaptcha/powTasks.js +486 -682
  185. package/dist/tasks/powCaptcha/powTasks.js.map +1 -1
  186. package/dist/tasks/powCaptcha/powTasksUtils.js +15 -28
  187. package/dist/tasks/puzzleCaptcha/puzzleTasks.d.ts.map +1 -1
  188. package/dist/tasks/puzzleCaptcha/puzzleTasks.js +417 -586
  189. package/dist/tasks/puzzleCaptcha/puzzleTasks.js.map +1 -1
  190. package/dist/tasks/puzzleCaptcha/puzzleTasksUtils.js +9 -6
  191. package/dist/tasks/spam/checkSpamEmail.js +133 -141
  192. package/dist/tasks/spam/checkTrafficFilter.d.ts +1 -0
  193. package/dist/tasks/spam/checkTrafficFilter.d.ts.map +1 -1
  194. package/dist/tasks/spam/checkTrafficFilter.js +117 -77
  195. package/dist/tasks/spam/checkTrafficFilter.js.map +1 -1
  196. package/dist/tasks/spam/evaluateEmailSpamRules.js +75 -86
  197. package/dist/tasks/spam/updateSpamEmailDomains.js +36 -54
  198. package/dist/tasks/tasks.js +98 -150
  199. package/dist/tests/integration/api/blacklistRequestInspector.integration.test.js +4 -1
  200. package/dist/tests/integration/api/blacklistRequestInspector.integration.test.js.map +1 -1
  201. package/dist/tests/unit/api/block.unit.test.js +8 -4
  202. package/dist/tests/unit/api/block.unit.test.js.map +1 -1
  203. package/dist/tests/unit/api/captcha/maintenanceModeShortCircuit.unit.test.js +8 -6
  204. package/dist/tests/unit/api/captcha/maintenanceModeShortCircuit.unit.test.js.map +1 -1
  205. package/dist/tests/unit/api/forwardVerify.unit.test.js +3 -1
  206. package/dist/tests/unit/api/forwardVerify.unit.test.js.map +1 -1
  207. package/dist/tests/unit/api/getFrictionlessCaptchaChallenge.unit.test.js +15 -7
  208. package/dist/tests/unit/api/getFrictionlessCaptchaChallenge.unit.test.js.map +1 -1
  209. package/dist/tests/unit/schedulers/captchaScheduler.unit.test.js +24 -18
  210. package/dist/tests/unit/schedulers/captchaScheduler.unit.test.js.map +1 -1
  211. package/dist/tests/unit/schedulers/getClientList.unit.test.js +19 -13
  212. package/dist/tests/unit/schedulers/getClientList.unit.test.js.map +1 -1
  213. package/dist/tests/unit/schedulers/setClientEntropy.unit.test.js +19 -13
  214. package/dist/tests/unit/schedulers/setClientEntropy.unit.test.js.map +1 -1
  215. package/dist/tests/unit/tasks/client/clientTasks.unit.test.js.map +1 -1
  216. package/dist/tests/unit/tasks/imgCaptcha/imgCaptchaTasksUtils.unit.test.js +22 -16
  217. package/dist/tests/unit/tasks/imgCaptcha/imgCaptchaTasksUtils.unit.test.js.map +1 -1
  218. package/dist/tests/unit/tasks/spam/checkTrafficFilter.unit.test.js +140 -0
  219. package/dist/tests/unit/tasks/spam/checkTrafficFilter.unit.test.js.map +1 -1
  220. package/dist/tests/unit/testUtils/mockProviderEnv.d.ts +7 -7
  221. package/dist/util/usageCounters.js +220 -212
  222. package/dist/util.d.ts.map +1 -1
  223. package/dist/util.js +327 -364
  224. package/dist/util.js.map +1 -1
  225. package/dist/utils/devicePlatform.js +15 -9
  226. package/dist/utils/dns.js +64 -77
  227. package/dist/utils/hashUserAgent.js +14 -7
  228. package/dist/utils/hashUserIp.js +14 -6
  229. package/dist/utils/honeypot/encoders.js +74 -80
  230. package/dist/utils/honeypot/phraseBank.js +49 -44
  231. package/dist/utils/normalizeRequestIp.js +34 -26
  232. package/package.json +24 -24
@@ -1,488 +1,408 @@
1
- import { getLogger } from "@prosopo/logger";
2
- import { ResultReason, Tier, ApiParams } from "@prosopo/types";
3
- import { AccessPolicyType } from "@prosopo/user-access-policy";
4
- import { getPrioritisedAccessRule, getRequestUserScope } from "../api/blacklistRequestInspector.js";
5
1
  import { getIpAddressFromComposite } from "../compositeIpAddress.js";
2
+ import { getPrioritisedAccessRule, getRequestUserScope } from "../api/blacklistRequestInspector.js";
3
+ import { checkTrafficFilter } from "./spam/checkTrafficFilter.js";
6
4
  import { extraIpInfosFromEnrichedDnsEvent } from "./dnsEvent/enrichDnsEvent.js";
7
5
  import { checkSpamEmail } from "./spam/checkSpamEmail.js";
8
- import { checkTrafficFilter } from "./spam/checkTrafficFilter.js";
9
- const findHardBlockPolicy = (accessPolicies) => {
10
- return accessPolicies.find((policy) => {
11
- if (policy.deferToVerify === true) {
12
- return true;
13
- }
14
- return policy.type === AccessPolicyType.Block && !policy.captchaType;
15
- });
6
+ import { getLogger } from "@prosopo/logger";
7
+ import { ApiParams, ResultReason, Tier } from "@prosopo/types";
8
+ import { AccessPolicyType } from "@prosopo/user-access-policy";
9
+ //#region src/tasks/captchaManager.ts
10
+ /**
11
+ * Finds a hard block policy from access policies.
12
+ *
13
+ * A hard block is either:
14
+ * (a) A Block policy with no captchaType (the historical "block all
15
+ * challenge types" case).
16
+ * (b) A Block policy with `deferToVerify: true` — request-time
17
+ * middleware skips these, so verify-time is the only place they
18
+ * can reject the commitment.
19
+ * (c) A Restrict policy with `deferToVerify: true` — the frictionless
20
+ * flow serves the rule's captchaType/solvedImagesCount normally
21
+ * (imposing the compute burden of the challenge), then verify
22
+ * marks the commitment disapproved regardless of solve outcome.
23
+ * Used by PROXY_POOL_TLS_NARROW to force a solving farm to burn
24
+ * 8 image rounds per session while still failing verify — bots
25
+ * solve at 100% so we can't stop them by demanding correctness,
26
+ * only by wasting their compute.
27
+ *
28
+ * Policies with captchaType and without `deferToVerify` are pure routing
29
+ * rules — they pick which challenge type to serve, not whether to reject.
30
+ */
31
+ var findHardBlockPolicy = (accessPolicies) => {
32
+ return accessPolicies.find((policy) => {
33
+ if (policy.deferToVerify === true) return true;
34
+ return policy.type === AccessPolicyType.Block && !policy.captchaType;
35
+ });
16
36
  };
17
- class CaptchaManager {
18
- constructor(db, pair, config, logger, writeQueue) {
19
- this.pair = pair;
20
- this.db = db;
21
- this.config = config;
22
- this.logger = logger || getLogger("info", "provider:captcha-manager");
23
- this.writeQueue = writeQueue ?? null;
24
- }
25
- /**
26
- * Update a session record with cache-first / write-behind semantics:
27
- * the Redis cache patch is awaited (so the in-request view of the
28
- * session is up-to-date for any subsequent fast-path read), and the
29
- * Mongo write is fire-and-forget so the response isn't gated on a
30
- * round-trip to Mongo. Matches the `RedisWriteQueue` design where
31
- * Redis is the source of truth for the in-flight request and Mongo is
32
- * eventually consistent.
33
- *
34
- * Prefer this over `this.db.updateSessionRecord` in any in-request
35
- * handler.
36
- */
37
- async updateSessionRecordWithCache(sessionId, updates, streamToCentral) {
38
- if (this.writeQueue) {
39
- await this.writeQueue.patchCachedSession(
40
- sessionId,
41
- updates
42
- );
43
- }
44
- this.scheduleMongoSessionUpdate(sessionId, updates, streamToCentral);
45
- }
46
- /**
47
- * Decrypt with the active detector keys and strip the throwaway
48
- * `timestamp` field. Returns `undefined` when no key decrypts.
49
- */
50
- async decryptSimdReadingsForAttach(simdReadingsCiphertext) {
51
- const decryptKeys = [
52
- ...await this.getDetectorKeys(),
53
- process.env.BOT_DECRYPTION_KEY
54
- ];
55
- const decrypted = await this.decryptSimdReadings(
56
- simdReadingsCiphertext,
57
- decryptKeys
58
- );
59
- if (!decrypted) return void 0;
60
- const { timestamp: _ignored, ...readings } = decrypted;
61
- return readings;
62
- }
63
- /** Decrypt + first-hop-wins attach. No-op on decrypt failure. */
64
- async decryptAndAttachSimdReadingsIfAbsent(sessionId, simdReadingsCiphertext, stage) {
65
- const readings = await this.decryptSimdReadingsForAttach(
66
- simdReadingsCiphertext
67
- );
68
- if (!readings) return;
69
- await this.recordSessionSimdReadingsIfAbsentWithCache(
70
- sessionId,
71
- readings,
72
- stage
73
- );
74
- }
75
- /**
76
- * First-hop-wins SIMD attach with cache-first / Mongo-deferred
77
- * semantics — mirrors `updateSessionRecordWithCache`.
78
- */
79
- async recordSessionSimdReadingsIfAbsentWithCache(sessionId, readings, stage) {
80
- if (this.writeQueue) {
81
- await this.writeQueue.patchCachedSimdReadingsIfAbsent(
82
- sessionId,
83
- readings,
84
- stage
85
- );
86
- }
87
- this.scheduleMongoSimdReadingsUpdate(sessionId, readings, stage);
88
- }
89
- /**
90
- * Fire-and-forget Mongo session update. Public so non-Task handlers
91
- * (e.g. challenge GETs) can keep request response latency off the
92
- * Mongo critical path while still ensuring eventual persistence.
93
- */
94
- scheduleMongoSessionUpdate(sessionId, updates, streamToCentral) {
95
- Promise.resolve(
96
- streamToCentral === void 0 ? this.db.updateSessionRecord(sessionId, updates) : this.db.updateSessionRecord(sessionId, updates, streamToCentral)
97
- ).catch((err) => {
98
- this.logger.warn(() => ({
99
- err,
100
- msg: "Background Mongo session update failed",
101
- data: { sessionId }
102
- }));
103
- });
104
- }
105
- /**
106
- * Fire-and-forget Mongo SIMD-readings update. Public for the same
107
- * reason as `scheduleMongoSessionUpdate`.
108
- */
109
- scheduleMongoSimdReadingsUpdate(sessionId, readings, stage) {
110
- Promise.resolve(
111
- this.db.recordSessionSimdReadingsIfAbsent(sessionId, readings, stage)
112
- ).catch((err) => {
113
- this.logger.warn(() => ({
114
- err,
115
- msg: "Background Mongo SIMD-readings update failed",
116
- data: { sessionId, stage }
117
- }));
118
- });
119
- }
120
- async validateSessionIP(sessionRecord, currentIP, env) {
121
- return { valid: true };
122
- }
123
- async isValidRequest(clientSettings, requestedCaptchaType, env, sessionId, userAccessPolicy, currentIP) {
124
- this.logger.debug(() => ({
125
- msg: "Validating request",
126
- data: {
127
- captchaType: requestedCaptchaType,
128
- sessionId
129
- }
130
- }));
131
- if (userAccessPolicy?.captchaType !== void 0 && userAccessPolicy.captchaType !== requestedCaptchaType) {
132
- this.logger.warn(() => ({
133
- msg: "Invalid captcha type for user access policy",
134
- data: {
135
- account: clientSettings.account,
136
- captchaType: userAccessPolicy.captchaType
137
- }
138
- }));
139
- return {
140
- valid: false,
141
- reason: ResultReason.INCORRECT_CAPTCHA_TYPE,
142
- type: requestedCaptchaType
143
- };
144
- }
145
- if (sessionId) {
146
- const cachedBeforeRemove = this.writeQueue ? await this.writeQueue.getCachedSession(sessionId) : null;
147
- const cachedHash = typeof cachedBeforeRemove?.userSitekeyIpHash === "string" ? cachedBeforeRemove.userSitekeyIpHash : void 0;
148
- await Promise.all([
149
- this.writeQueue?.invalidateCachedSession(sessionId),
150
- cachedHash ? this.writeQueue?.invalidateCachedSessionByHash(cachedHash) : Promise.resolve()
151
- ]);
152
- let sessionRecord = await this.db.checkAndRemoveSession(sessionId);
153
- let resolvedSessionId = sessionId;
154
- if (!sessionRecord && this.writeQueue) {
155
- const escalationSessionId = await this.writeQueue.getCachedSessionEscalation(sessionId);
156
- if (escalationSessionId && escalationSessionId !== sessionId) {
157
- const peekedEscalationRecord = await this.db.getSessionRecordBySessionId(escalationSessionId);
158
- if (peekedEscalationRecord) {
159
- this.logger.info(() => ({
160
- msg: "Resolving origin sessionId to escalation",
161
- data: {
162
- account: clientSettings.account,
163
- originSessionId: sessionId,
164
- escalationSessionId,
165
- escalationCaptchaType: peekedEscalationRecord.captchaType,
166
- requestedCaptchaType
167
- }
168
- }));
169
- if (peekedEscalationRecord.captchaType === requestedCaptchaType) {
170
- const consumedEscalationRecord = await this.db.checkAndRemoveSession(escalationSessionId);
171
- if (consumedEscalationRecord) {
172
- sessionRecord = consumedEscalationRecord;
173
- resolvedSessionId = escalationSessionId;
174
- }
175
- } else {
176
- this.logger.warn(() => ({
177
- msg: "Escalation captcha type does not match requested type",
178
- data: {
179
- account: clientSettings.account,
180
- originSessionId: sessionId,
181
- escalationSessionId,
182
- escalationCaptchaType: peekedEscalationRecord.captchaType,
183
- requestedCaptchaType
184
- }
185
- }));
186
- await this.writeQueue.invalidateCachedSessionEscalation(
187
- sessionId
188
- );
189
- return {
190
- valid: false,
191
- reason: ResultReason.INCORRECT_CAPTCHA_TYPE,
192
- type: requestedCaptchaType
193
- };
194
- }
195
- }
196
- await this.writeQueue.invalidateCachedSessionEscalation(sessionId);
197
- }
198
- }
199
- if (!sessionRecord) {
200
- this.logger.warn(() => ({
201
- msg: "No session found",
202
- data: {
203
- account: clientSettings.account,
204
- sessionId
205
- }
206
- }));
207
- if (this.writeQueue) {
208
- const cachedHash2 = typeof cachedBeforeRemove?.userSitekeyIpHash === "string" ? cachedBeforeRemove.userSitekeyIpHash : void 0;
209
- await Promise.all([
210
- this.writeQueue.invalidateCachedSession(sessionId),
211
- cachedHash2 ? this.writeQueue.invalidateCachedSessionByHash(cachedHash2) : Promise.resolve()
212
- ]);
213
- }
214
- return {
215
- valid: false,
216
- reason: ResultReason.CAPTCHA_NO_SESSION_FOUND,
217
- type: requestedCaptchaType
218
- };
219
- }
220
- sessionId = resolvedSessionId;
221
- if (this.writeQueue) {
222
- await Promise.all([
223
- this.writeQueue.invalidateCachedSession(sessionId),
224
- sessionRecord.userSitekeyIpHash ? this.writeQueue.invalidateCachedSessionByHash(
225
- sessionRecord.userSitekeyIpHash
226
- ) : Promise.resolve()
227
- ]);
228
- }
229
- if (currentIP) {
230
- const ipValidation = await this.validateSessionIP(
231
- sessionRecord,
232
- currentIP,
233
- env
234
- );
235
- if (!ipValidation.valid) {
236
- return {
237
- valid: false,
238
- reason: ipValidation.reason,
239
- type: requestedCaptchaType
240
- };
241
- }
242
- }
243
- if (sessionRecord.captchaType !== requestedCaptchaType) {
244
- this.logger.warn(() => ({
245
- msg: "Session captcha type does not match requested type",
246
- data: {
247
- account: clientSettings.account,
248
- sessionId,
249
- sessionCaptchaType: sessionRecord.captchaType,
250
- requestedCaptchaType
251
- }
252
- }));
253
- return {
254
- valid: false,
255
- reason: ResultReason.INCORRECT_CAPTCHA_TYPE,
256
- type: requestedCaptchaType
257
- };
258
- }
259
- return {
260
- valid: true,
261
- sessionId: sessionRecord.sessionId,
262
- type: requestedCaptchaType,
263
- ...sessionRecord.powDifficulty && {
264
- powDifficulty: sessionRecord.powDifficulty
265
- },
266
- ...sessionRecord.solvedImagesCount && {
267
- solvedImagesCount: sessionRecord.solvedImagesCount
268
- },
269
- ipInfo: sessionRecord.ipInfo
270
- };
271
- }
272
- if (clientSettings?.settings?.captchaType !== requestedCaptchaType) {
273
- this.logger.warn(() => ({
274
- msg: `Invalid ${requestedCaptchaType} request`,
275
- data: {
276
- account: clientSettings.account,
277
- requestedCaptchaType,
278
- settingsCaptchaType: clientSettings?.settings?.captchaType
279
- }
280
- }));
281
- return {
282
- valid: false,
283
- reason: ResultReason.INCORRECT_CAPTCHA_TYPE,
284
- type: requestedCaptchaType
285
- };
286
- }
287
- return { valid: true, type: requestedCaptchaType };
288
- }
289
- getVerificationResponse(verified, clientRecord, translateFn, score, reason) {
290
- return {
291
- status: translateFn(
292
- verified ? "API.USER_VERIFIED" : "API.USER_NOT_VERIFIED"
293
- ),
294
- [ApiParams.verified]: verified,
295
- ...CaptchaManager.canClientSeeScore(clientRecord.tier, score) && {
296
- [ApiParams.score]: score
297
- },
298
- ...!verified && clientRecord.tier !== Tier.Free && reason && {
299
- [ApiParams.reason]: reason
300
- }
301
- };
302
- }
303
- async getPrioritisedAccessPolicies(userAccessRulesStorage, clientId, userScope, options) {
304
- return getPrioritisedAccessRule(
305
- userAccessRulesStorage,
306
- userScope,
307
- clientId,
308
- options
309
- );
310
- }
311
- async getDetectorKeys() {
312
- return await this.db.getDetectorKeys();
313
- }
314
- /**
315
- * Decrypt the catcher's WASM SIMD CPU fingerprint readings via the
316
- * obfuscated `decodeSimd.js` bundle (source lives in the private
317
- * @prosopo/catcher repo). Mirrors `decryptBehavioralData`: tries each
318
- * detector key in turn (loaded by the caller, with the env-var key as
319
- * fallback). Returns null when no key works so the caller can drop
320
- * the field rather than fail the whole request.
321
- */
322
- async decryptSimdReadings(encryptedData, decryptKeys) {
323
- const decryptSimdReadings = (await import("./detection/decodeSimd.js")).default;
324
- const validKeys = decryptKeys.filter((k) => k);
325
- if (validKeys.length === 0) {
326
- this.logger?.error(() => ({
327
- msg: "No decryption keys provided for SIMD readings"
328
- }));
329
- return null;
330
- }
331
- for (const [keyIndex, key] of validKeys.entries()) {
332
- try {
333
- return await decryptSimdReadings(encryptedData, key);
334
- } catch (err) {
335
- this.logger?.debug(() => ({
336
- msg: "Failed to decrypt SIMD readings with key, trying next",
337
- data: {
338
- keyIndex: keyIndex + 1,
339
- totalKeys: validKeys.length,
340
- err
341
- }
342
- }));
343
- }
344
- }
345
- this.logger?.warn(() => ({
346
- msg: "Failed to decrypt SIMD readings with all available keys",
347
- data: { totalKeysAttempted: validKeys.length }
348
- }));
349
- return null;
350
- }
351
- async decryptBehavioralData(encryptedData, decryptKeys) {
352
- const decryptBehavioralData = (await import("./detection/decodeBehavior.js")).default;
353
- const validKeys = decryptKeys.filter((k) => k);
354
- if (validKeys.length === 0) {
355
- this.logger?.error(() => ({
356
- msg: "No decryption keys provided for behavioral data"
357
- }));
358
- return null;
359
- }
360
- for (const [keyIndex, key] of validKeys.entries()) {
361
- try {
362
- this.logger?.debug(() => ({
363
- msg: "Attempting to decrypt behavioral data",
364
- data: {
365
- keyIndex: keyIndex + 1,
366
- totalKeys: validKeys.length
367
- }
368
- }));
369
- const result = await decryptBehavioralData(encryptedData, key);
370
- this.logger?.info(() => ({
371
- msg: "Behavioral data decrypted successfully",
372
- data: {
373
- keyIndex: keyIndex + 1,
374
- c1Length: result.collector1?.length || 0,
375
- c2Length: result.collector2?.length || 0,
376
- c3Length: result.collector3?.length || 0,
377
- deviceCapability: result.deviceCapability
378
- }
379
- }));
380
- return result;
381
- } catch (error) {
382
- this.logger?.debug(() => ({
383
- msg: "Failed to decrypt with key, trying next",
384
- data: {
385
- keyIndex: keyIndex + 1,
386
- totalKeys: validKeys.length,
387
- err: error
388
- }
389
- }));
390
- }
391
- }
392
- this.logger?.error(() => ({
393
- msg: "Failed to decrypt behavioral data with all available keys",
394
- data: {
395
- totalKeysAttempted: validKeys.length
396
- }
397
- }));
398
- return null;
399
- }
400
- /**
401
- * Checks if a user should be hard blocked based on access policies
402
- * Only checks for Block policies without captchaType
403
- *
404
- * @returns The blocking policy if user should be blocked, undefined otherwise
405
- */
406
- async checkForHardBlock(userAccessRulesStorage, challengeRecord, userAccount, headers, coords, countryCode, asn) {
407
- let headHash;
408
- if (challengeRecord.sessionId) {
409
- const sessionRecord = await this.db.getSessionRecordBySessionId(
410
- challengeRecord.sessionId
411
- );
412
- headHash = sessionRecord?.decryptedHeadHash;
413
- }
414
- const coordsString = coords ? JSON.stringify(coords) : void 0;
415
- const ipAddressRecord = getIpAddressFromComposite(
416
- challengeRecord.ipAddress
417
- );
418
- const userScope = getRequestUserScope(
419
- headers,
420
- challengeRecord.ja4,
421
- ipAddressRecord.address,
422
- userAccount,
423
- headHash,
424
- coordsString,
425
- countryCode,
426
- asn
427
- );
428
- const accessPolicies = await this.getPrioritisedAccessPolicies(
429
- userAccessRulesStorage,
430
- challengeRecord.dappAccount,
431
- userScope,
432
- // Hard-block lookup only — restrict the Redis-side candidate
433
- // pool to Block rules so the SERVER_SIDE_RANK_TOP_N cap can't
434
- // crowd a hard-block out of the top-N with Restrict or
435
- // routing-Block (captchaType-scoped) entries.
436
- { blockOnly: true }
437
- );
438
- return findHardBlockPolicy(accessPolicies);
439
- }
440
- /**
441
- * Checks if the provided email address has a domain in the spam email domain list.
442
- * Returns true if the email domain is spam (i.e. should be blocked), false otherwise.
443
- * Handles formats: user@domain.com, @domain.com, domain.com
444
- */
445
- async checkSpamEmail(email) {
446
- return checkSpamEmail(email, this.db, this.config, this.logger);
447
- }
448
- /**
449
- * Resolves the IP info to feed to `checkTrafficFilter` and runs the check.
450
- *
451
- * - The captcha record already carries the IPInfoResponse from request
452
- * time (ipInfoMiddleware → storeXxxRecord), so by default we reuse it
453
- * instead of hitting the sidecar again.
454
- * - If the dapp's server passed up the end user's current IP via the
455
- * verify call, look that up fresh — it's the "now" IP for filtering
456
- * and may differ from the IP that originally requested the captcha.
457
- * - When the session carries a `dnsEvent`, its `peerIp` and `resolverIp`
458
- * are enriched and passed alongside the primary IP.
459
- * - `blockAbuser` defaults to true so abusive networks are always
460
- * blocked even when the site hasn't configured a trafficFilter.
461
- * - Returns `{ isBlocked: false }` if every filter flag is off, without
462
- * consulting the payload at all.
463
- *
464
- * Callers handle the "blocked" branch themselves (each verify path
465
- * updates record / session state differently); this helper just returns
466
- * the verdict.
467
- */
468
- async resolveTrafficFilterCheck(env, recordIpInfo, trafficFilter, currentIp, enrichedDnsEvent) {
469
- const effective = { blockAbuser: true, ...trafficFilter };
470
- const hasAny = Object.values(effective).some((v) => v);
471
- if (!hasAny) {
472
- return { isBlocked: false };
473
- }
474
- const ipInfo = currentIp ? await env.ipInfoService.lookup(currentIp) : recordIpInfo;
475
- return checkTrafficFilter(
476
- ipInfo,
477
- effective,
478
- extraIpInfosFromEnrichedDnsEvent(enrichedDnsEvent),
479
- enrichedDnsEvent?.pathValid
480
- );
481
- }
482
- static canClientSeeScore(tier, score) {
483
- return score && tier && tier !== Tier.Free;
484
- }
485
- }
486
- export {
487
- CaptchaManager
37
+ var CaptchaManager = class CaptchaManager {
38
+ constructor(db, pair, config, logger, writeQueue) {
39
+ this.pair = pair;
40
+ this.db = db;
41
+ this.config = config;
42
+ this.logger = logger || getLogger("info", "provider:captcha-manager");
43
+ this.writeQueue = writeQueue ?? null;
44
+ }
45
+ /**
46
+ * Update a session record with cache-first / write-behind semantics:
47
+ * the Redis cache patch is awaited (so the in-request view of the
48
+ * session is up-to-date for any subsequent fast-path read), and the
49
+ * Mongo write is fire-and-forget so the response isn't gated on a
50
+ * round-trip to Mongo. Matches the `RedisWriteQueue` design where
51
+ * Redis is the source of truth for the in-flight request and Mongo is
52
+ * eventually consistent.
53
+ *
54
+ * Prefer this over `this.db.updateSessionRecord` in any in-request
55
+ * handler.
56
+ */
57
+ async updateSessionRecordWithCache(sessionId, updates, streamToCentral) {
58
+ if (this.writeQueue) await this.writeQueue.patchCachedSession(sessionId, updates);
59
+ this.scheduleMongoSessionUpdate(sessionId, updates, streamToCentral);
60
+ }
61
+ /**
62
+ * Decrypt with the active detector keys and strip the throwaway
63
+ * `timestamp` field. Returns `undefined` when no key decrypts.
64
+ */
65
+ async decryptSimdReadingsForAttach(simdReadingsCiphertext) {
66
+ const decryptKeys = [...await this.getDetectorKeys(), process.env.BOT_DECRYPTION_KEY];
67
+ const decrypted = await this.decryptSimdReadings(simdReadingsCiphertext, decryptKeys);
68
+ if (!decrypted) return void 0;
69
+ const { timestamp: _ignored, ...readings } = decrypted;
70
+ return readings;
71
+ }
72
+ /** Decrypt + first-hop-wins attach. No-op on decrypt failure. */
73
+ async decryptAndAttachSimdReadingsIfAbsent(sessionId, simdReadingsCiphertext, stage) {
74
+ const readings = await this.decryptSimdReadingsForAttach(simdReadingsCiphertext);
75
+ if (!readings) return;
76
+ await this.recordSessionSimdReadingsIfAbsentWithCache(sessionId, readings, stage);
77
+ }
78
+ /**
79
+ * First-hop-wins SIMD attach with cache-first / Mongo-deferred
80
+ * semantics — mirrors `updateSessionRecordWithCache`.
81
+ */
82
+ async recordSessionSimdReadingsIfAbsentWithCache(sessionId, readings, stage) {
83
+ if (this.writeQueue) await this.writeQueue.patchCachedSimdReadingsIfAbsent(sessionId, readings, stage);
84
+ this.scheduleMongoSimdReadingsUpdate(sessionId, readings, stage);
85
+ }
86
+ /**
87
+ * Fire-and-forget Mongo session update. Public so non-Task handlers
88
+ * (e.g. challenge GETs) can keep request response latency off the
89
+ * Mongo critical path while still ensuring eventual persistence.
90
+ */
91
+ scheduleMongoSessionUpdate(sessionId, updates, streamToCentral) {
92
+ Promise.resolve(streamToCentral === void 0 ? this.db.updateSessionRecord(sessionId, updates) : this.db.updateSessionRecord(sessionId, updates, streamToCentral)).catch((err) => {
93
+ this.logger.warn(() => ({
94
+ err,
95
+ msg: "Background Mongo session update failed",
96
+ data: { sessionId }
97
+ }));
98
+ });
99
+ }
100
+ /**
101
+ * Fire-and-forget Mongo SIMD-readings update. Public for the same
102
+ * reason as `scheduleMongoSessionUpdate`.
103
+ */
104
+ scheduleMongoSimdReadingsUpdate(sessionId, readings, stage) {
105
+ Promise.resolve(this.db.recordSessionSimdReadingsIfAbsent(sessionId, readings, stage)).catch((err) => {
106
+ this.logger.warn(() => ({
107
+ err,
108
+ msg: "Background Mongo SIMD-readings update failed",
109
+ data: {
110
+ sessionId,
111
+ stage
112
+ }
113
+ }));
114
+ });
115
+ }
116
+ async validateSessionIP(sessionRecord, currentIP, env) {
117
+ return { valid: true };
118
+ }
119
+ async isValidRequest(clientSettings, requestedCaptchaType, env, sessionId, userAccessPolicy, currentIP) {
120
+ this.logger.debug(() => ({
121
+ msg: "Validating request",
122
+ data: {
123
+ captchaType: requestedCaptchaType,
124
+ sessionId
125
+ }
126
+ }));
127
+ if (userAccessPolicy?.captchaType !== void 0 && userAccessPolicy.captchaType !== requestedCaptchaType) {
128
+ this.logger.warn(() => ({
129
+ msg: "Invalid captcha type for user access policy",
130
+ data: {
131
+ account: clientSettings.account,
132
+ captchaType: userAccessPolicy.captchaType
133
+ }
134
+ }));
135
+ return {
136
+ valid: false,
137
+ reason: ResultReason.INCORRECT_CAPTCHA_TYPE,
138
+ type: requestedCaptchaType
139
+ };
140
+ }
141
+ if (sessionId) {
142
+ const cachedBeforeRemove = this.writeQueue ? await this.writeQueue.getCachedSession(sessionId) : null;
143
+ const cachedHash = typeof cachedBeforeRemove?.userSitekeyIpHash === "string" ? cachedBeforeRemove.userSitekeyIpHash : void 0;
144
+ await Promise.all([this.writeQueue?.invalidateCachedSession(sessionId), cachedHash ? this.writeQueue?.invalidateCachedSessionByHash(cachedHash) : Promise.resolve()]);
145
+ let sessionRecord = await this.db.checkAndRemoveSession(sessionId);
146
+ let resolvedSessionId = sessionId;
147
+ if (!sessionRecord && this.writeQueue) {
148
+ const escalationSessionId = await this.writeQueue.getCachedSessionEscalation(sessionId);
149
+ if (escalationSessionId && escalationSessionId !== sessionId) {
150
+ const peekedEscalationRecord = await this.db.getSessionRecordBySessionId(escalationSessionId);
151
+ if (peekedEscalationRecord) {
152
+ this.logger.info(() => ({
153
+ msg: "Resolving origin sessionId to escalation",
154
+ data: {
155
+ account: clientSettings.account,
156
+ originSessionId: sessionId,
157
+ escalationSessionId,
158
+ escalationCaptchaType: peekedEscalationRecord.captchaType,
159
+ requestedCaptchaType
160
+ }
161
+ }));
162
+ if (peekedEscalationRecord.captchaType === requestedCaptchaType) {
163
+ const consumedEscalationRecord = await this.db.checkAndRemoveSession(escalationSessionId);
164
+ if (consumedEscalationRecord) {
165
+ sessionRecord = consumedEscalationRecord;
166
+ resolvedSessionId = escalationSessionId;
167
+ }
168
+ } else {
169
+ this.logger.warn(() => ({
170
+ msg: "Escalation captcha type does not match requested type",
171
+ data: {
172
+ account: clientSettings.account,
173
+ originSessionId: sessionId,
174
+ escalationSessionId,
175
+ escalationCaptchaType: peekedEscalationRecord.captchaType,
176
+ requestedCaptchaType
177
+ }
178
+ }));
179
+ await this.writeQueue.invalidateCachedSessionEscalation(sessionId);
180
+ return {
181
+ valid: false,
182
+ reason: ResultReason.INCORRECT_CAPTCHA_TYPE,
183
+ type: requestedCaptchaType
184
+ };
185
+ }
186
+ }
187
+ await this.writeQueue.invalidateCachedSessionEscalation(sessionId);
188
+ }
189
+ }
190
+ if (!sessionRecord) {
191
+ this.logger.warn(() => ({
192
+ msg: "No session found",
193
+ data: {
194
+ account: clientSettings.account,
195
+ sessionId
196
+ }
197
+ }));
198
+ if (this.writeQueue) {
199
+ const cachedHash = typeof cachedBeforeRemove?.userSitekeyIpHash === "string" ? cachedBeforeRemove.userSitekeyIpHash : void 0;
200
+ await Promise.all([this.writeQueue.invalidateCachedSession(sessionId), cachedHash ? this.writeQueue.invalidateCachedSessionByHash(cachedHash) : Promise.resolve()]);
201
+ }
202
+ return {
203
+ valid: false,
204
+ reason: ResultReason.CAPTCHA_NO_SESSION_FOUND,
205
+ type: requestedCaptchaType
206
+ };
207
+ }
208
+ sessionId = resolvedSessionId;
209
+ if (this.writeQueue) await Promise.all([this.writeQueue.invalidateCachedSession(sessionId), sessionRecord.userSitekeyIpHash ? this.writeQueue.invalidateCachedSessionByHash(sessionRecord.userSitekeyIpHash) : Promise.resolve()]);
210
+ if (currentIP) {
211
+ const ipValidation = await this.validateSessionIP(sessionRecord, currentIP, env);
212
+ if (!ipValidation.valid) return {
213
+ valid: false,
214
+ reason: ipValidation.reason,
215
+ type: requestedCaptchaType
216
+ };
217
+ }
218
+ if (sessionRecord.captchaType !== requestedCaptchaType) {
219
+ this.logger.warn(() => ({
220
+ msg: "Session captcha type does not match requested type",
221
+ data: {
222
+ account: clientSettings.account,
223
+ sessionId,
224
+ sessionCaptchaType: sessionRecord.captchaType,
225
+ requestedCaptchaType
226
+ }
227
+ }));
228
+ return {
229
+ valid: false,
230
+ reason: ResultReason.INCORRECT_CAPTCHA_TYPE,
231
+ type: requestedCaptchaType
232
+ };
233
+ }
234
+ return {
235
+ valid: true,
236
+ sessionId: sessionRecord.sessionId,
237
+ type: requestedCaptchaType,
238
+ ...sessionRecord.powDifficulty && { powDifficulty: sessionRecord.powDifficulty },
239
+ ...sessionRecord.solvedImagesCount && { solvedImagesCount: sessionRecord.solvedImagesCount },
240
+ ipInfo: sessionRecord.ipInfo
241
+ };
242
+ }
243
+ if (clientSettings?.settings?.captchaType !== requestedCaptchaType) {
244
+ this.logger.warn(() => ({
245
+ msg: `Invalid ${requestedCaptchaType} request`,
246
+ data: {
247
+ account: clientSettings.account,
248
+ requestedCaptchaType,
249
+ settingsCaptchaType: clientSettings?.settings?.captchaType
250
+ }
251
+ }));
252
+ return {
253
+ valid: false,
254
+ reason: ResultReason.INCORRECT_CAPTCHA_TYPE,
255
+ type: requestedCaptchaType
256
+ };
257
+ }
258
+ return {
259
+ valid: true,
260
+ type: requestedCaptchaType
261
+ };
262
+ }
263
+ getVerificationResponse(verified, clientRecord, translateFn, score, reason) {
264
+ return {
265
+ status: translateFn(verified ? "API.USER_VERIFIED" : "API.USER_NOT_VERIFIED"),
266
+ [ApiParams.verified]: verified,
267
+ ...CaptchaManager.canClientSeeScore(clientRecord.tier, score) && { [ApiParams.score]: score },
268
+ ...!verified && clientRecord.tier !== Tier.Free && reason && { [ApiParams.reason]: reason }
269
+ };
270
+ }
271
+ async getPrioritisedAccessPolicies(userAccessRulesStorage, clientId, userScope, options) {
272
+ return getPrioritisedAccessRule(userAccessRulesStorage, userScope, clientId, options);
273
+ }
274
+ async getDetectorKeys() {
275
+ return await this.db.getDetectorKeys();
276
+ }
277
+ /**
278
+ * Decrypt the catcher's WASM SIMD CPU fingerprint readings via the
279
+ * obfuscated `decodeSimd.js` bundle (source lives in the private
280
+ * @prosopo/catcher repo). Mirrors `decryptBehavioralData`: tries each
281
+ * detector key in turn (loaded by the caller, with the env-var key as
282
+ * fallback). Returns null when no key works so the caller can drop
283
+ * the field rather than fail the whole request.
284
+ */
285
+ async decryptSimdReadings(encryptedData, decryptKeys) {
286
+ const decryptSimdReadings = (await import("./detection/decodeSimd.js")).default;
287
+ const validKeys = decryptKeys.filter((k) => k);
288
+ if (validKeys.length === 0) {
289
+ this.logger?.error(() => ({ msg: "No decryption keys provided for SIMD readings" }));
290
+ return null;
291
+ }
292
+ for (const [keyIndex, key] of validKeys.entries()) try {
293
+ return await decryptSimdReadings(encryptedData, key);
294
+ } catch (err) {
295
+ this.logger?.debug(() => ({
296
+ msg: "Failed to decrypt SIMD readings with key, trying next",
297
+ data: {
298
+ keyIndex: keyIndex + 1,
299
+ totalKeys: validKeys.length,
300
+ err
301
+ }
302
+ }));
303
+ }
304
+ this.logger?.warn(() => ({
305
+ msg: "Failed to decrypt SIMD readings with all available keys",
306
+ data: { totalKeysAttempted: validKeys.length }
307
+ }));
308
+ return null;
309
+ }
310
+ async decryptBehavioralData(encryptedData, decryptKeys) {
311
+ const decryptBehavioralData = (await import("./detection/decodeBehavior.js")).default;
312
+ const validKeys = decryptKeys.filter((k) => k);
313
+ if (validKeys.length === 0) {
314
+ this.logger?.error(() => ({ msg: "No decryption keys provided for behavioral data" }));
315
+ return null;
316
+ }
317
+ for (const [keyIndex, key] of validKeys.entries()) try {
318
+ this.logger?.debug(() => ({
319
+ msg: "Attempting to decrypt behavioral data",
320
+ data: {
321
+ keyIndex: keyIndex + 1,
322
+ totalKeys: validKeys.length
323
+ }
324
+ }));
325
+ const result = await decryptBehavioralData(encryptedData, key);
326
+ this.logger?.info(() => ({
327
+ msg: "Behavioral data decrypted successfully",
328
+ data: {
329
+ keyIndex: keyIndex + 1,
330
+ c1Length: result.collector1?.length || 0,
331
+ c2Length: result.collector2?.length || 0,
332
+ c3Length: result.collector3?.length || 0,
333
+ deviceCapability: result.deviceCapability
334
+ }
335
+ }));
336
+ return result;
337
+ } catch (error) {
338
+ this.logger?.debug(() => ({
339
+ msg: "Failed to decrypt with key, trying next",
340
+ data: {
341
+ keyIndex: keyIndex + 1,
342
+ totalKeys: validKeys.length,
343
+ err: error
344
+ }
345
+ }));
346
+ }
347
+ this.logger?.error(() => ({
348
+ msg: "Failed to decrypt behavioral data with all available keys",
349
+ data: { totalKeysAttempted: validKeys.length }
350
+ }));
351
+ return null;
352
+ }
353
+ /**
354
+ * Checks if a user should be hard blocked based on access policies
355
+ * Only checks for Block policies without captchaType
356
+ *
357
+ * @returns The blocking policy if user should be blocked, undefined otherwise
358
+ */
359
+ async checkForHardBlock(userAccessRulesStorage, challengeRecord, userAccount, headers, coords, countryCode, asn) {
360
+ let headHash;
361
+ if (challengeRecord.sessionId) headHash = (await this.db.getSessionRecordBySessionId(challengeRecord.sessionId))?.decryptedHeadHash;
362
+ const coordsString = coords ? JSON.stringify(coords) : void 0;
363
+ const ipAddressRecord = getIpAddressFromComposite(challengeRecord.ipAddress);
364
+ const userScope = getRequestUserScope(headers, challengeRecord.ja4, ipAddressRecord.address, userAccount, headHash, coordsString, countryCode, asn);
365
+ return findHardBlockPolicy(await this.getPrioritisedAccessPolicies(userAccessRulesStorage, challengeRecord.dappAccount, userScope, { blockOnly: true }));
366
+ }
367
+ /**
368
+ * Checks if the provided email address has a domain in the spam email domain list.
369
+ * Returns true if the email domain is spam (i.e. should be blocked), false otherwise.
370
+ * Handles formats: user@domain.com, @domain.com, domain.com
371
+ */
372
+ async checkSpamEmail(email) {
373
+ return checkSpamEmail(email, this.db, this.config, this.logger);
374
+ }
375
+ /**
376
+ * Resolves the IP info to feed to `checkTrafficFilter` and runs the check.
377
+ *
378
+ * - The captcha record already carries the IPInfoResponse from request
379
+ * time (ipInfoMiddleware → storeXxxRecord), so by default we reuse it
380
+ * instead of hitting the sidecar again.
381
+ * - If the dapp's server passed up the end user's current IP via the
382
+ * verify call, look that up fresh — it's the "now" IP for filtering
383
+ * and may differ from the IP that originally requested the captcha.
384
+ * - When the session carries a `dnsEvent`, its `peerIp` and `resolverIp`
385
+ * are enriched and passed alongside the primary IP.
386
+ * - `blockAbuser` defaults to true so abusive networks are always
387
+ * blocked even when the site hasn't configured a trafficFilter.
388
+ * - Returns `{ isBlocked: false }` if every filter flag is off, without
389
+ * consulting the payload at all.
390
+ *
391
+ * Callers handle the "blocked" branch themselves (each verify path
392
+ * updates record / session state differently); this helper just returns
393
+ * the verdict.
394
+ */
395
+ async resolveTrafficFilterCheck(env, recordIpInfo, trafficFilter, currentIp, enrichedDnsEvent) {
396
+ const effective = {
397
+ blockAbuser: true,
398
+ ...trafficFilter
399
+ };
400
+ if (!Object.values(effective).some((v) => v)) return { isBlocked: false };
401
+ return checkTrafficFilter(currentIp ? await env.ipInfoService.lookup(currentIp) : recordIpInfo, effective, extraIpInfosFromEnrichedDnsEvent(enrichedDnsEvent), enrichedDnsEvent?.pathValid);
402
+ }
403
+ static canClientSeeScore(tier, score) {
404
+ return score && tier && tier !== Tier.Free;
405
+ }
488
406
  };
407
+ //#endregion
408
+ export { CaptchaManager };