@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,592 +1,423 @@
1
- import { u8aToHex, stringToHex } from "@polkadot/util";
2
- import { ProsopoApiError, ProsopoEnvError } from "@prosopo/common";
3
- import { POW_SEPARATOR, puzzleToleranceDefault, ApiParams, ResultReason, CaptchaStatus, CaptchaType, SimdReadingsStage, DecisionMachineDecision } from "@prosopo/types";
4
- import { at, extractData, assertCoordsSafe, verifyRecency } from "@prosopo/util";
5
- import { getIpAddressFromComposite, getCompositeIpAddress } from "../../compositeIpAddress.js";
6
- import { deepValidateIpAddress } from "../../util.js";
7
1
  import { buildAllWindowIncrements } from "../../util/usageCounters.js";
8
- import { CaptchaManager } from "../captchaManager.js";
9
2
  import { DecisionMachineRunner } from "../decisionMachine/decisionMachineRunner.js";
10
- import { enrichDnsEvent, getIpInfoAsn, computeDnsAsymmetry } from "../dnsEvent/enrichDnsEvent.js";
3
+ import { getCompositeIpAddress, getIpAddressFromComposite } from "../../compositeIpAddress.js";
4
+ import { computeDnsAsymmetry, enrichDnsEvent, getIpInfoAsn } from "../dnsEvent/enrichDnsEvent.js";
5
+ import { CaptchaManager } from "../captchaManager.js";
6
+ import { deepValidateIpAddress } from "../../util.js";
11
7
  import { computeFrictionlessScore } from "../frictionless/frictionlessTasksUtils.js";
12
8
  import { checkPowSignature } from "../powCaptcha/powTasksUtils.js";
13
9
  import { validatePuzzleSolution } from "./puzzleTasksUtils.js";
14
- class PuzzleCaptchaManager extends CaptchaManager {
15
- constructor(db, pair, config, logger, usageCounters) {
16
- super(db, pair, config, logger);
17
- this.POW_SEPARATOR = POW_SEPARATOR;
18
- this.decisionMachineRunner = new DecisionMachineRunner(db);
19
- this.usageCounters = usageCounters ?? null;
20
- }
21
- /**
22
- * @description Generates a Puzzle Captcha challenge for a given user and dapp
23
- *
24
- * @param {string} userAccount - user that is solving the captcha
25
- * @param {string} dappAccount - dapp that is requesting the captcha
26
- * @param origin - not currently used
27
- * @param tolerance
28
- */
29
- async getPuzzleCaptchaChallenge(userAccount, dappAccount, origin, tolerance) {
30
- const resolvedTolerance = tolerance ?? puzzleToleranceDefault;
31
- const requestedAtTimestamp = Date.now();
32
- const nonce = Math.floor(Math.random() * 1e6);
33
- const challenge = `${requestedAtTimestamp}___${userAccount}___${dappAccount}___${nonce}`;
34
- const challengeSignature = u8aToHex(this.pair.sign(stringToHex(challenge)));
35
- const targetX = Math.floor(Math.random() * (280 - 150 + 1)) + 150;
36
- const targetY = Math.floor(Math.random() * (170 - 30 + 1)) + 30;
37
- const originX = Math.floor(Math.random() * (130 - 20 + 1)) + 20;
38
- const originY = Math.floor(Math.random() * (170 - 30 + 1)) + 30;
39
- return {
40
- challenge,
41
- targetX,
42
- targetY,
43
- originX,
44
- originY,
45
- tolerance: resolvedTolerance,
46
- providerSignature: challengeSignature,
47
- requestedAtTimestamp
48
- };
49
- }
50
- /**
51
- * @description Verifies a Puzzle Captcha solution for a given user and dapp
52
- *
53
- * @param {string} challenge - the challenge string
54
- * @param {string} providerChallengeSignature - proof that the Provider provided the challenge
55
- * @param {number} finalX - the final X coordinate of the puzzle
56
- * @param {number} finalY - the final Y coordinate of the puzzle
57
- * @param {PuzzleEvent[]} puzzleEvents - the puzzle event trail
58
- * @param {number} timeout - the time in milliseconds since the Provider was selected to provide the captcha
59
- * @param {string} userTimestampSignature
60
- * @param ipAddress
61
- * @param headers
62
- * @param behavioralData
63
- */
64
- async verifyPuzzleCaptchaSolution(challenge, providerChallengeSignature, finalX, finalY, puzzleEvents, timeout, userTimestampSignature, ipAddress, headers, behavioralData, salt, simdReadings, clientMetaData) {
65
- checkPowSignature(
66
- challenge,
67
- providerChallengeSignature,
68
- this.pair.address,
69
- ApiParams.challenge
70
- );
71
- const challengeSplit = challenge.split(this.POW_SEPARATOR);
72
- const timestamp = Number.parseInt(at(challengeSplit, 0));
73
- const userAccount = at(challengeSplit, 1);
74
- checkPowSignature(
75
- timestamp.toString(),
76
- userTimestampSignature,
77
- userAccount,
78
- ApiParams.timestamp
79
- );
80
- const challengeRecord = await this.db.getPuzzleCaptchaRecordByChallenge(challenge);
81
- if (!challengeRecord) {
82
- this.logger.debug(() => ({
83
- msg: `No record of this challenge: ${challenge}`
84
- }));
85
- return false;
86
- }
87
- let coords;
88
- let saltDecodeError;
89
- if (salt) {
90
- try {
91
- const extractedData = extractData(salt);
92
- if (extractedData.length >= 2) {
93
- const built = [
94
- [[extractedData[0], extractedData[1]]]
95
- ];
96
- assertCoordsSafe(built, "coords");
97
- coords = built;
98
- }
99
- } catch (error) {
100
- saltDecodeError = error;
101
- this.logger.warn(() => ({
102
- msg: "Failed to extract coordinates from salt",
103
- error,
104
- salt
105
- }));
106
- }
107
- }
108
- if (challengeRecord.userSubmitted) {
109
- this.logger.debug(() => ({
110
- msg: `Challenge already submitted: ${challenge}`
111
- }));
112
- return false;
113
- }
114
- if (saltDecodeError) {
115
- const badSaltResult = {
116
- status: CaptchaStatus.disapproved,
117
- reason: ResultReason.CAPTCHA_INVALID_SALT
118
- };
119
- await this.db.updatePuzzleCaptchaRecordResult(
120
- challenge,
121
- badSaltResult,
122
- false,
123
- // serverChecked
124
- true,
125
- // userSubmitted
126
- userTimestampSignature,
127
- void 0
128
- // never persist the bad coords
129
- );
130
- if (challengeRecord.sessionId) {
131
- await this.updateSessionRecordWithCache(challengeRecord.sessionId, {
132
- userSubmitted: true,
133
- result: badSaltResult
134
- });
135
- }
136
- return false;
137
- }
138
- if (!verifyRecency(challenge, timeout)) {
139
- const timeoutResult = {
140
- status: CaptchaStatus.disapproved,
141
- reason: ResultReason.CAPTCHA_INVALID_TIMESTAMP
142
- };
143
- await this.db.updatePuzzleCaptchaRecordResult(
144
- challenge,
145
- timeoutResult,
146
- false,
147
- //serverchecked
148
- true,
149
- // usersubmitted
150
- userTimestampSignature,
151
- coords
152
- );
153
- if (challengeRecord.sessionId) {
154
- await this.updateSessionRecordWithCache(challengeRecord.sessionId, {
155
- userSubmitted: true,
156
- result: timeoutResult
157
- });
158
- }
159
- return false;
160
- }
161
- const correct = validatePuzzleSolution(
162
- finalX,
163
- finalY,
164
- challengeRecord.targetX,
165
- challengeRecord.targetY,
166
- challengeRecord.tolerance
167
- );
168
- let result = { status: CaptchaStatus.approved };
169
- if (!correct) {
170
- result = {
171
- status: CaptchaStatus.disapproved,
172
- reason: ResultReason.CAPTCHA_INVALID_SOLUTION
173
- };
174
- }
175
- if (correct && this.usageCounters) {
176
- const dappAccount = at(challengeSplit, 2);
177
- this.usageCounters.incrManyAsync(
178
- dappAccount,
179
- buildAllWindowIncrements(
180
- "solved",
181
- CaptchaType.puzzle,
182
- ipAddress.address,
183
- userAccount
184
- )
185
- );
186
- }
187
- if (behavioralData) {
188
- try {
189
- const decryptKeys = [
190
- // Process DB keys first, then env var key last as env key will likely be invalid
191
- ...await this.getDetectorKeys(),
192
- process.env.BOT_DECRYPTION_KEY
193
- ];
194
- const decryptedData = await this.decryptBehavioralData(
195
- behavioralData,
196
- decryptKeys
197
- );
198
- if (decryptedData) {
199
- const dappAccount = at(challengeSplit, 2);
200
- this.logger?.info(() => ({
201
- msg: "Behavioral analysis completed",
202
- data: {
203
- userAccount,
204
- dappAccount,
205
- challenge,
206
- mouseEventsCount: decryptedData.collector1?.length || 0,
207
- touchEventsCount: decryptedData.collector2?.length || 0,
208
- clickEventsCount: decryptedData.collector3?.length || 0,
209
- deviceCapability: decryptedData.deviceCapability,
210
- captchaResult: correct ? "passed" : "failed"
211
- }
212
- }));
213
- const packedData = {
214
- c1: decryptedData.collector1 || [],
215
- c2: decryptedData.collector2 || [],
216
- c3: decryptedData.collector3 || [],
217
- d: decryptedData.deviceCapability
218
- };
219
- await this.db.updatePuzzleCaptchaRecord(challenge, {
220
- behavioralDataPacked: packedData,
221
- deviceCapability: decryptedData.deviceCapability,
222
- puzzleEvents
223
- });
224
- }
225
- } catch (error) {
226
- this.logger?.error(() => ({
227
- msg: "Failed to process behavioral data",
228
- err: error
229
- }));
230
- }
231
- } else {
232
- await this.db.updatePuzzleCaptchaRecord(challenge, {
233
- puzzleEvents
234
- });
235
- }
236
- if (clientMetaData?.hp) {
237
- await this.db.updatePuzzleCaptchaRecord(challenge, {
238
- clientMetaData: { hp: clientMetaData.hp }
239
- });
240
- }
241
- await this.db.updatePuzzleCaptchaRecordResult(
242
- challenge,
243
- result,
244
- false,
245
- true,
246
- userTimestampSignature,
247
- coords
248
- );
249
- if (challengeRecord.sessionId) {
250
- const linkedSessionId = challengeRecord.sessionId;
251
- await this.updateSessionRecordWithCache(linkedSessionId, {
252
- userSubmitted: true,
253
- result
254
- });
255
- if (simdReadings) {
256
- await this.decryptAndAttachSimdReadingsIfAbsent(
257
- linkedSessionId,
258
- simdReadings,
259
- SimdReadingsStage.submit
260
- );
261
- }
262
- }
263
- return correct;
264
- }
265
- /**
266
- * @description Verifies a Puzzle Captcha for a given user and dapp. This is called by the server to verify the user's solution
267
- * and update the record in the database to show that the user has solved the captcha
268
- *
269
- * @param {string} dappAccount - the dapp that is requesting the captcha
270
- * @param {string} challenge - the challenge string
271
- * @param {number} timeout - the time in milliseconds since the Provider was selected to provide the captcha
272
- * @param env - provider environment
273
- * @param ip - optional IP address for validation
274
- * @param userAccessRulesStorage - storage for querying user access policies
275
- * @param email
276
- * @param spamEmailDomainCheckingEnabled
277
- * @param trafficFilter
278
- * @param storeMetadata - when true, persists the dapp-server-provided
279
- * `email` on the captcha record for spam-rate analysis.
280
- */
281
- async serverVerifyPuzzleCaptchaSolution(dappAccount, challenge, timeout, env, ip, userAccessRulesStorage, email, spamEmailDomainCheckingEnabled = false, trafficFilter, storeMetadata = false) {
282
- const notVerifiedResponse = { verified: false };
283
- const logger = this.logger.with({ challenge, dappAccount });
284
- const challengeRecord = await this.db.getPuzzleCaptchaRecordByChallenge(challenge);
285
- if (!challengeRecord) {
286
- logger.debug(() => ({
287
- msg: `No record of this challenge: ${challenge}`
288
- }));
289
- return notVerifiedResponse;
290
- }
291
- if (challengeRecord.result.status !== CaptchaStatus.approved) {
292
- throw new ProsopoApiError("CAPTCHA.INVALID_SOLUTION", {
293
- context: {
294
- failedFuncName: this.serverVerifyPuzzleCaptchaSolution.name,
295
- challenge
296
- }
297
- });
298
- }
299
- if (challengeRecord.serverChecked) return notVerifiedResponse;
300
- const challengeDappAccount = challengeRecord.dappAccount;
301
- if (dappAccount !== challengeDappAccount) {
302
- throw new ProsopoEnvError("CAPTCHA.DAPP_USER_SOLUTION_NOT_FOUND", {
303
- context: {
304
- failedFuncName: this.serverVerifyPuzzleCaptchaSolution.name,
305
- dappAccount,
306
- challengeDappAccount
307
- }
308
- });
309
- }
310
- await this.db.updatePuzzleCaptchaRecord(challengeRecord.challenge, {
311
- serverChecked: true,
312
- lastUpdatedTimestamp: /* @__PURE__ */ new Date()
313
- });
314
- const submittedAt = challengeRecord.submittedAtTimestamp;
315
- const submitToVerifyMs = submittedAt instanceof Date ? Date.now() - submittedAt.getTime() : Number.POSITIVE_INFINITY;
316
- if (submitToVerifyMs > timeout) {
317
- const disapprovedResult = {
318
- status: CaptchaStatus.disapproved,
319
- reason: ResultReason.TIMESTAMP_TOO_OLD
320
- };
321
- await this.db.updatePuzzleCaptchaRecord(challengeRecord.challenge, {
322
- result: disapprovedResult
323
- });
324
- if (challengeRecord.sessionId) {
325
- await this.updateSessionRecordWithCache(challengeRecord.sessionId, {
326
- serverChecked: true,
327
- result: disapprovedResult
328
- });
329
- }
330
- return notVerifiedResponse;
331
- }
332
- if (userAccessRulesStorage) {
333
- try {
334
- const blockPolicy = await this.checkForHardBlock(
335
- userAccessRulesStorage,
336
- challengeRecord,
337
- challengeRecord.userAccount,
338
- challengeRecord.headers,
339
- challengeRecord.coords,
340
- challengeRecord.ipInfo?.isValid ? challengeRecord.ipInfo.countryCode : void 0,
341
- challengeRecord.ipInfo?.isValid ? challengeRecord.ipInfo.asnNumber : void 0
342
- );
343
- if (blockPolicy) {
344
- logger.info(() => ({
345
- msg: "User blocked by access policy in server puzzle verification",
346
- data: {
347
- userAccount: challengeRecord.userAccount,
348
- policy: blockPolicy
349
- }
350
- }));
351
- const blockedResult = {
352
- status: CaptchaStatus.disapproved,
353
- reason: ResultReason.ACCESS_POLICY_BLOCK
354
- };
355
- await this.db.updatePuzzleCaptchaRecord(challengeRecord.challenge, {
356
- result: blockedResult
357
- });
358
- if (challengeRecord.sessionId) {
359
- await this.updateSessionRecordWithCache(challengeRecord.sessionId, {
360
- serverChecked: true,
361
- result: blockedResult
362
- });
363
- }
364
- return notVerifiedResponse;
365
- }
366
- } catch (error) {
367
- logger.warn(() => ({
368
- msg: "Failed to check user access policies in server puzzle verification",
369
- error
370
- }));
371
- }
372
- }
373
- if (email && spamEmailDomainCheckingEnabled) {
374
- try {
375
- const isSpam = await this.checkSpamEmail(email);
376
- if (isSpam) {
377
- const emailDomain = email.split("@")[1] || "unknown";
378
- logger.info(() => ({
379
- msg: "Spam email domain detected in server puzzle verification",
380
- data: { emailDomain }
381
- }));
382
- await this.db.updatePuzzleCaptchaRecord(challengeRecord.challenge, {
383
- result: {
384
- status: CaptchaStatus.disapproved,
385
- reason: ResultReason.SPAM_EMAIL_DOMAIN
386
- }
387
- });
388
- return notVerifiedResponse;
389
- }
390
- } catch (error) {
391
- logger.warn(() => ({
392
- msg: "Failed to check spam email domain in server puzzle verification",
393
- error
394
- }));
395
- }
396
- }
397
- const sessionRecord = challengeRecord.sessionId ? await this.db.getSessionRecordBySessionId(challengeRecord.sessionId) : void 0;
398
- const enrichedDnsEvent = await enrichDnsEvent(
399
- sessionRecord?.dnsEvent,
400
- env.ipInfoService,
401
- ip ?? challengeRecord.ipInfo?.ip
402
- );
403
- {
404
- const check = await this.resolveTrafficFilterCheck(
405
- env,
406
- challengeRecord.ipInfo,
407
- trafficFilter,
408
- ip,
409
- enrichedDnsEvent
410
- );
411
- if (check.isBlocked) {
412
- logger.info(() => ({
413
- msg: "Traffic filter rejected request in puzzle verification",
414
- data: {
415
- ip,
416
- reason: check.reason,
417
- dnsPeerIp: enrichedDnsEvent?.peerIp,
418
- dnsResolverIp: enrichedDnsEvent?.resolverIp,
419
- dnsPeerAsn: getIpInfoAsn(enrichedDnsEvent?.peerIpInfo),
420
- dnsResolverAsn: getIpInfoAsn(enrichedDnsEvent?.resolverIpInfo),
421
- dnsPathValid: enrichedDnsEvent?.pathValid
422
- }
423
- }));
424
- const blockedResult = {
425
- status: CaptchaStatus.disapproved,
426
- reason: check.reason
427
- };
428
- await this.db.updatePuzzleCaptchaRecord(challengeRecord.challenge, {
429
- result: blockedResult
430
- });
431
- if (challengeRecord.sessionId) {
432
- await this.updateSessionRecordWithCache(challengeRecord.sessionId, {
433
- serverChecked: true,
434
- result: blockedResult
435
- });
436
- }
437
- return notVerifiedResponse;
438
- }
439
- }
440
- if (storeMetadata && email) {
441
- await this.db.updatePuzzleCaptchaRecord(challengeRecord.challenge, {
442
- metadata: { email }
443
- });
444
- }
445
- if (ip) {
446
- const challengeIpAddress = getIpAddressFromComposite(
447
- challengeRecord.ipAddress
448
- );
449
- const clientRecord = await this.db.getClientRecord(dappAccount);
450
- const ipValidationRules = clientRecord?.settings?.ipValidationRules;
451
- await this.db.updatePuzzleCaptchaRecord(challengeRecord.challenge, {
452
- providedIp: getCompositeIpAddress(ip)
453
- });
454
- if (ipValidationRules?.enabled === true) {
455
- const ipValidation = await deepValidateIpAddress(
456
- ip,
457
- challengeIpAddress,
458
- logger,
459
- env.ipInfoService,
460
- ipValidationRules,
461
- enrichedDnsEvent?.peerIp
462
- );
463
- if (!ipValidation.isValid) {
464
- logger.error(() => ({
465
- msg: "IP validation failed for puzzle captcha",
466
- data: {
467
- ip,
468
- challengeIp: challengeIpAddress.address,
469
- error: ipValidation.errorMessage,
470
- distanceKm: ipValidation.distanceKm
471
- }
472
- }));
473
- const ipFailResult = {
474
- status: CaptchaStatus.disapproved,
475
- reason: ResultReason.FAILED_IP_VALIDATION
476
- };
477
- await this.db.updatePuzzleCaptchaRecord(challengeRecord.challenge, {
478
- result: ipFailResult
479
- });
480
- if (challengeRecord.sessionId) {
481
- await this.updateSessionRecordWithCache(challengeRecord.sessionId, {
482
- serverChecked: true,
483
- result: ipFailResult
484
- });
485
- }
486
- return notVerifiedResponse;
487
- }
488
- }
489
- }
490
- let score;
491
- if (sessionRecord) {
492
- const dnsAsymmetry = computeDnsAsymmetry(
493
- enrichedDnsEvent,
494
- challengeRecord.ipInfo,
495
- trafficFilter
496
- );
497
- if (dnsAsymmetry > 0) {
498
- sessionRecord.scoreComponents = {
499
- ...sessionRecord.scoreComponents,
500
- dnsAsymmetry
501
- };
502
- }
503
- score = computeFrictionlessScore(sessionRecord?.scoreComponents);
504
- logger.info(() => ({
505
- data: {
506
- scoreComponents: { ...sessionRecord?.scoreComponents || {} },
507
- score,
508
- dnsPeerAsn: getIpInfoAsn(enrichedDnsEvent?.peerIpInfo),
509
- dnsResolverAsn: getIpInfoAsn(enrichedDnsEvent?.resolverIpInfo)
510
- }
511
- }));
512
- }
513
- try {
514
- const decisionInput = {
515
- userAccount: challengeRecord.userAccount,
516
- dappAccount: challengeRecord.dappAccount,
517
- captchaResult: "passed",
518
- headers: challengeRecord.headers,
519
- captchaType: CaptchaType.puzzle,
520
- behavioralDataPacked: challengeRecord.behavioralDataPacked,
521
- deviceCapability: challengeRecord.deviceCapability,
522
- countryCode: challengeRecord.ipInfo?.isValid ? challengeRecord.ipInfo.countryCode : void 0,
523
- ipInfo: challengeRecord.ipInfo,
524
- dnsEvent: enrichedDnsEvent,
525
- score,
526
- threshold: sessionRecord?.threshold,
527
- scoreComponents: sessionRecord?.scoreComponents,
528
- decryptedHeadHash: sessionRecord?.decryptedHeadHash,
529
- userSitekeyIpHash: sessionRecord?.userSitekeyIpHash,
530
- simdReadings: sessionRecord?.simdReadings,
531
- frictionlessReason: sessionRecord?.reason,
532
- ruleType: sessionRecord?.ruleType,
533
- webView: sessionRecord?.webView,
534
- iFrame: sessionRecord?.iFrame,
535
- coords: challengeRecord.coords,
536
- puzzleEvents: challengeRecord.puzzleEvents
537
- };
538
- const decision = await this.decisionMachineRunner.decide(
539
- decisionInput,
540
- logger
541
- );
542
- if (decision.decision === DecisionMachineDecision.Deny) {
543
- logger.info(() => ({
544
- msg: "Decision machine denied puzzle captcha in server verification",
545
- data: {
546
- userAccount: challengeRecord.userAccount,
547
- reason: decision.reason,
548
- score: decision.score,
549
- tags: decision.tags
550
- }
551
- }));
552
- const dmResult = {
553
- status: CaptchaStatus.disapproved,
554
- reason: decision.reason || ResultReason.CAPTCHA_DECISION_MACHINE_DENIED
555
- };
556
- await this.db.updatePuzzleCaptchaRecord(challengeRecord.challenge, {
557
- result: dmResult
558
- });
559
- if (challengeRecord.sessionId) {
560
- await this.updateSessionRecordWithCache(challengeRecord.sessionId, {
561
- serverChecked: true,
562
- result: dmResult
563
- });
564
- }
565
- return notVerifiedResponse;
566
- }
567
- logger.debug(() => ({
568
- msg: "Decision machine allowed puzzle captcha",
569
- data: {
570
- reason: decision.reason,
571
- score: decision.score,
572
- tags: decision.tags
573
- }
574
- }));
575
- } catch (error) {
576
- logger.error(() => ({
577
- msg: "Failed to run decision machine in server puzzle verification",
578
- err: error
579
- }));
580
- }
581
- if (challengeRecord.sessionId) {
582
- await this.updateSessionRecordWithCache(challengeRecord.sessionId, {
583
- serverChecked: true,
584
- result: { status: CaptchaStatus.approved }
585
- });
586
- }
587
- return { verified: true, ...score ? { score } : {} };
588
- }
589
- }
590
- export {
591
- PuzzleCaptchaManager
10
+ import { ProsopoApiError, ProsopoEnvError } from "@prosopo/common";
11
+ import { ApiParams, CaptchaStatus, CaptchaType, DecisionMachineDecision, POW_SEPARATOR, ResultReason, SimdReadingsStage, puzzleToleranceDefault } from "@prosopo/types";
12
+ import { assertCoordsSafe, at, extractData, verifyRecency } from "@prosopo/util";
13
+ import { stringToHex, u8aToHex } from "@polkadot/util";
14
+ //#region src/tasks/puzzleCaptcha/puzzleTasks.ts
15
+ var PuzzleCaptchaManager = class extends CaptchaManager {
16
+ constructor(db, pair, config, logger, usageCounters) {
17
+ super(db, pair, config, logger);
18
+ this.POW_SEPARATOR = POW_SEPARATOR;
19
+ this.decisionMachineRunner = new DecisionMachineRunner(db);
20
+ this.usageCounters = usageCounters ?? null;
21
+ }
22
+ /**
23
+ * @description Generates a Puzzle Captcha challenge for a given user and dapp
24
+ *
25
+ * @param {string} userAccount - user that is solving the captcha
26
+ * @param {string} dappAccount - dapp that is requesting the captcha
27
+ * @param origin - not currently used
28
+ * @param tolerance
29
+ */
30
+ async getPuzzleCaptchaChallenge(userAccount, dappAccount, origin, tolerance) {
31
+ const resolvedTolerance = tolerance ?? puzzleToleranceDefault;
32
+ const requestedAtTimestamp = Date.now();
33
+ const challenge = `${requestedAtTimestamp}___${userAccount}___${dappAccount}___${Math.floor(Math.random() * 1e6)}`;
34
+ const challengeSignature = u8aToHex(this.pair.sign(stringToHex(challenge)));
35
+ return {
36
+ challenge,
37
+ targetX: Math.floor(Math.random() * 131) + 150,
38
+ targetY: Math.floor(Math.random() * 141) + 30,
39
+ originX: Math.floor(Math.random() * 111) + 20,
40
+ originY: Math.floor(Math.random() * 141) + 30,
41
+ tolerance: resolvedTolerance,
42
+ providerSignature: challengeSignature,
43
+ requestedAtTimestamp
44
+ };
45
+ }
46
+ /**
47
+ * @description Verifies a Puzzle Captcha solution for a given user and dapp
48
+ *
49
+ * @param {string} challenge - the challenge string
50
+ * @param {string} providerChallengeSignature - proof that the Provider provided the challenge
51
+ * @param {number} finalX - the final X coordinate of the puzzle
52
+ * @param {number} finalY - the final Y coordinate of the puzzle
53
+ * @param {PuzzleEvent[]} puzzleEvents - the puzzle event trail
54
+ * @param {number} timeout - the time in milliseconds since the Provider was selected to provide the captcha
55
+ * @param {string} userTimestampSignature
56
+ * @param ipAddress
57
+ * @param headers
58
+ * @param behavioralData
59
+ */
60
+ async verifyPuzzleCaptchaSolution(challenge, providerChallengeSignature, finalX, finalY, puzzleEvents, timeout, userTimestampSignature, ipAddress, headers, behavioralData, salt, simdReadings, clientMetaData) {
61
+ checkPowSignature(challenge, providerChallengeSignature, this.pair.address, ApiParams.challenge);
62
+ const challengeSplit = challenge.split(this.POW_SEPARATOR);
63
+ const timestamp = Number.parseInt(at(challengeSplit, 0));
64
+ const userAccount = at(challengeSplit, 1);
65
+ checkPowSignature(timestamp.toString(), userTimestampSignature, userAccount, ApiParams.timestamp);
66
+ const challengeRecord = await this.db.getPuzzleCaptchaRecordByChallenge(challenge);
67
+ if (!challengeRecord) {
68
+ this.logger.debug(() => ({ msg: `No record of this challenge: ${challenge}` }));
69
+ return false;
70
+ }
71
+ let coords;
72
+ let saltDecodeError;
73
+ if (salt) try {
74
+ const extractedData = extractData(salt);
75
+ if (extractedData.length >= 2) {
76
+ const built = [[[extractedData[0], extractedData[1]]]];
77
+ assertCoordsSafe(built, "coords");
78
+ coords = built;
79
+ }
80
+ } catch (error) {
81
+ saltDecodeError = error;
82
+ this.logger.warn(() => ({
83
+ msg: "Failed to extract coordinates from salt",
84
+ error,
85
+ salt
86
+ }));
87
+ }
88
+ if (challengeRecord.userSubmitted) {
89
+ this.logger.debug(() => ({ msg: `Challenge already submitted: ${challenge}` }));
90
+ return false;
91
+ }
92
+ if (saltDecodeError) {
93
+ const badSaltResult = {
94
+ status: CaptchaStatus.disapproved,
95
+ reason: ResultReason.CAPTCHA_INVALID_SALT
96
+ };
97
+ await this.db.updatePuzzleCaptchaRecordResult(challenge, badSaltResult, false, true, userTimestampSignature, void 0);
98
+ if (challengeRecord.sessionId) await this.updateSessionRecordWithCache(challengeRecord.sessionId, {
99
+ userSubmitted: true,
100
+ result: badSaltResult
101
+ });
102
+ return false;
103
+ }
104
+ if (!verifyRecency(challenge, timeout)) {
105
+ const timeoutResult = {
106
+ status: CaptchaStatus.disapproved,
107
+ reason: ResultReason.CAPTCHA_INVALID_TIMESTAMP
108
+ };
109
+ await this.db.updatePuzzleCaptchaRecordResult(challenge, timeoutResult, false, true, userTimestampSignature, coords);
110
+ if (challengeRecord.sessionId) await this.updateSessionRecordWithCache(challengeRecord.sessionId, {
111
+ userSubmitted: true,
112
+ result: timeoutResult
113
+ });
114
+ return false;
115
+ }
116
+ const correct = validatePuzzleSolution(finalX, finalY, challengeRecord.targetX, challengeRecord.targetY, challengeRecord.tolerance);
117
+ let result = { status: CaptchaStatus.approved };
118
+ if (!correct) result = {
119
+ status: CaptchaStatus.disapproved,
120
+ reason: ResultReason.CAPTCHA_INVALID_SOLUTION
121
+ };
122
+ if (correct && this.usageCounters) {
123
+ const dappAccount = at(challengeSplit, 2);
124
+ this.usageCounters.incrManyAsync(dappAccount, buildAllWindowIncrements("solved", CaptchaType.puzzle, ipAddress.address, userAccount));
125
+ }
126
+ if (behavioralData) try {
127
+ const decryptKeys = [...await this.getDetectorKeys(), process.env.BOT_DECRYPTION_KEY];
128
+ const decryptedData = await this.decryptBehavioralData(behavioralData, decryptKeys);
129
+ if (decryptedData) {
130
+ const dappAccount = at(challengeSplit, 2);
131
+ this.logger?.info(() => ({
132
+ msg: "Behavioral analysis completed",
133
+ data: {
134
+ userAccount,
135
+ dappAccount,
136
+ challenge,
137
+ mouseEventsCount: decryptedData.collector1?.length || 0,
138
+ touchEventsCount: decryptedData.collector2?.length || 0,
139
+ clickEventsCount: decryptedData.collector3?.length || 0,
140
+ deviceCapability: decryptedData.deviceCapability,
141
+ captchaResult: correct ? "passed" : "failed"
142
+ }
143
+ }));
144
+ const packedData = {
145
+ c1: decryptedData.collector1 || [],
146
+ c2: decryptedData.collector2 || [],
147
+ c3: decryptedData.collector3 || [],
148
+ d: decryptedData.deviceCapability
149
+ };
150
+ await this.db.updatePuzzleCaptchaRecord(challenge, {
151
+ behavioralDataPacked: packedData,
152
+ deviceCapability: decryptedData.deviceCapability,
153
+ puzzleEvents
154
+ });
155
+ }
156
+ } catch (error) {
157
+ this.logger?.error(() => ({
158
+ msg: "Failed to process behavioral data",
159
+ err: error
160
+ }));
161
+ }
162
+ else await this.db.updatePuzzleCaptchaRecord(challenge, { puzzleEvents });
163
+ if (clientMetaData?.hp) await this.db.updatePuzzleCaptchaRecord(challenge, { clientMetaData: { hp: clientMetaData.hp } });
164
+ await this.db.updatePuzzleCaptchaRecordResult(challenge, result, false, true, userTimestampSignature, coords);
165
+ if (challengeRecord.sessionId) {
166
+ const linkedSessionId = challengeRecord.sessionId;
167
+ await this.updateSessionRecordWithCache(linkedSessionId, {
168
+ userSubmitted: true,
169
+ result
170
+ });
171
+ if (simdReadings) await this.decryptAndAttachSimdReadingsIfAbsent(linkedSessionId, simdReadings, SimdReadingsStage.submit);
172
+ }
173
+ return correct;
174
+ }
175
+ /**
176
+ * @description Verifies a Puzzle Captcha for a given user and dapp. This is called by the server to verify the user's solution
177
+ * and update the record in the database to show that the user has solved the captcha
178
+ *
179
+ * @param {string} dappAccount - the dapp that is requesting the captcha
180
+ * @param {string} challenge - the challenge string
181
+ * @param {number} timeout - the time in milliseconds since the Provider was selected to provide the captcha
182
+ * @param env - provider environment
183
+ * @param ip - optional IP address for validation
184
+ * @param userAccessRulesStorage - storage for querying user access policies
185
+ * @param email
186
+ * @param spamEmailDomainCheckingEnabled
187
+ * @param trafficFilter
188
+ * @param storeMetadata - when true, persists the dapp-server-provided
189
+ * `email` on the captcha record for spam-rate analysis.
190
+ */
191
+ async serverVerifyPuzzleCaptchaSolution(dappAccount, challenge, timeout, env, ip, userAccessRulesStorage, email, spamEmailDomainCheckingEnabled = false, trafficFilter, storeMetadata = false) {
192
+ const notVerifiedResponse = { verified: false };
193
+ const logger = this.logger.with({
194
+ challenge,
195
+ dappAccount
196
+ });
197
+ const challengeRecord = await this.db.getPuzzleCaptchaRecordByChallenge(challenge);
198
+ if (!challengeRecord) {
199
+ logger.debug(() => ({ msg: `No record of this challenge: ${challenge}` }));
200
+ return notVerifiedResponse;
201
+ }
202
+ if (challengeRecord.result.status !== CaptchaStatus.approved) throw new ProsopoApiError("CAPTCHA.INVALID_SOLUTION", { context: {
203
+ code: 400,
204
+ failedFuncName: this.serverVerifyPuzzleCaptchaSolution.name,
205
+ challenge
206
+ } });
207
+ if (challengeRecord.serverChecked) return notVerifiedResponse;
208
+ const challengeDappAccount = challengeRecord.dappAccount;
209
+ if (dappAccount !== challengeDappAccount) throw new ProsopoEnvError("CAPTCHA.DAPP_USER_SOLUTION_NOT_FOUND", { context: {
210
+ failedFuncName: this.serverVerifyPuzzleCaptchaSolution.name,
211
+ dappAccount,
212
+ challengeDappAccount
213
+ } });
214
+ await this.db.updatePuzzleCaptchaRecord(challengeRecord.challenge, {
215
+ serverChecked: true,
216
+ lastUpdatedTimestamp: /* @__PURE__ */ new Date()
217
+ });
218
+ const submittedAt = challengeRecord.submittedAtTimestamp;
219
+ if ((submittedAt instanceof Date ? Date.now() - submittedAt.getTime() : Number.POSITIVE_INFINITY) > timeout) {
220
+ const disapprovedResult = {
221
+ status: CaptchaStatus.disapproved,
222
+ reason: ResultReason.TIMESTAMP_TOO_OLD
223
+ };
224
+ await this.db.updatePuzzleCaptchaRecord(challengeRecord.challenge, { result: disapprovedResult });
225
+ if (challengeRecord.sessionId) await this.updateSessionRecordWithCache(challengeRecord.sessionId, {
226
+ serverChecked: true,
227
+ result: disapprovedResult
228
+ });
229
+ return notVerifiedResponse;
230
+ }
231
+ if (userAccessRulesStorage) try {
232
+ const blockPolicy = await this.checkForHardBlock(userAccessRulesStorage, challengeRecord, challengeRecord.userAccount, challengeRecord.headers, challengeRecord.coords, challengeRecord.ipInfo?.isValid ? challengeRecord.ipInfo.countryCode : void 0, challengeRecord.ipInfo?.isValid ? challengeRecord.ipInfo.asnNumber : void 0);
233
+ if (blockPolicy) {
234
+ logger.info(() => ({
235
+ msg: "User blocked by access policy in server puzzle verification",
236
+ data: {
237
+ userAccount: challengeRecord.userAccount,
238
+ policy: blockPolicy
239
+ }
240
+ }));
241
+ const blockedResult = {
242
+ status: CaptchaStatus.disapproved,
243
+ reason: ResultReason.ACCESS_POLICY_BLOCK
244
+ };
245
+ await this.db.updatePuzzleCaptchaRecord(challengeRecord.challenge, { result: blockedResult });
246
+ if (challengeRecord.sessionId) await this.updateSessionRecordWithCache(challengeRecord.sessionId, {
247
+ serverChecked: true,
248
+ result: blockedResult
249
+ });
250
+ return notVerifiedResponse;
251
+ }
252
+ } catch (error) {
253
+ logger.warn(() => ({
254
+ msg: "Failed to check user access policies in server puzzle verification",
255
+ error
256
+ }));
257
+ }
258
+ if (email && spamEmailDomainCheckingEnabled) try {
259
+ if (await this.checkSpamEmail(email)) {
260
+ const emailDomain = email.split("@")[1] || "unknown";
261
+ logger.info(() => ({
262
+ msg: "Spam email domain detected in server puzzle verification",
263
+ data: { emailDomain }
264
+ }));
265
+ await this.db.updatePuzzleCaptchaRecord(challengeRecord.challenge, { result: {
266
+ status: CaptchaStatus.disapproved,
267
+ reason: ResultReason.SPAM_EMAIL_DOMAIN
268
+ } });
269
+ return notVerifiedResponse;
270
+ }
271
+ } catch (error) {
272
+ logger.warn(() => ({
273
+ msg: "Failed to check spam email domain in server puzzle verification",
274
+ error
275
+ }));
276
+ }
277
+ const sessionRecord = challengeRecord.sessionId ? await this.db.getSessionRecordBySessionId(challengeRecord.sessionId) : void 0;
278
+ const enrichedDnsEvent = await enrichDnsEvent(sessionRecord?.dnsEvent, env.ipInfoService, ip ?? challengeRecord.ipInfo?.ip);
279
+ {
280
+ const check = await this.resolveTrafficFilterCheck(env, challengeRecord.ipInfo, trafficFilter, ip, enrichedDnsEvent);
281
+ if (check.isBlocked) {
282
+ logger.info(() => ({
283
+ msg: "Traffic filter rejected request in puzzle verification",
284
+ data: {
285
+ ip,
286
+ reason: check.reason,
287
+ dnsPeerIp: enrichedDnsEvent?.peerIp,
288
+ dnsResolverIp: enrichedDnsEvent?.resolverIp,
289
+ dnsPeerAsn: getIpInfoAsn(enrichedDnsEvent?.peerIpInfo),
290
+ dnsResolverAsn: getIpInfoAsn(enrichedDnsEvent?.resolverIpInfo),
291
+ dnsPathValid: enrichedDnsEvent?.pathValid
292
+ }
293
+ }));
294
+ const blockedResult = {
295
+ status: CaptchaStatus.disapproved,
296
+ reason: check.reason
297
+ };
298
+ await this.db.updatePuzzleCaptchaRecord(challengeRecord.challenge, { result: blockedResult });
299
+ if (challengeRecord.sessionId) await this.updateSessionRecordWithCache(challengeRecord.sessionId, {
300
+ serverChecked: true,
301
+ result: blockedResult
302
+ });
303
+ return notVerifiedResponse;
304
+ }
305
+ }
306
+ if (storeMetadata && email) await this.db.updatePuzzleCaptchaRecord(challengeRecord.challenge, { metadata: { email } });
307
+ if (ip) {
308
+ const challengeIpAddress = getIpAddressFromComposite(challengeRecord.ipAddress);
309
+ const ipValidationRules = (await this.db.getClientRecord(dappAccount))?.settings?.ipValidationRules;
310
+ await this.db.updatePuzzleCaptchaRecord(challengeRecord.challenge, { providedIp: getCompositeIpAddress(ip) });
311
+ if (ipValidationRules?.enabled === true) {
312
+ const ipValidation = await deepValidateIpAddress(ip, challengeIpAddress, logger, env.ipInfoService, ipValidationRules, enrichedDnsEvent?.peerIp);
313
+ if (!ipValidation.isValid) {
314
+ logger.error(() => ({
315
+ msg: "IP validation failed for puzzle captcha",
316
+ data: {
317
+ ip,
318
+ challengeIp: challengeIpAddress.address,
319
+ error: ipValidation.errorMessage,
320
+ distanceKm: ipValidation.distanceKm
321
+ }
322
+ }));
323
+ const ipFailResult = {
324
+ status: CaptchaStatus.disapproved,
325
+ reason: ResultReason.FAILED_IP_VALIDATION
326
+ };
327
+ await this.db.updatePuzzleCaptchaRecord(challengeRecord.challenge, { result: ipFailResult });
328
+ if (challengeRecord.sessionId) await this.updateSessionRecordWithCache(challengeRecord.sessionId, {
329
+ serverChecked: true,
330
+ result: ipFailResult
331
+ });
332
+ return notVerifiedResponse;
333
+ }
334
+ }
335
+ }
336
+ let score;
337
+ if (sessionRecord) {
338
+ const dnsAsymmetry = computeDnsAsymmetry(enrichedDnsEvent, challengeRecord.ipInfo, trafficFilter);
339
+ if (dnsAsymmetry > 0) sessionRecord.scoreComponents = {
340
+ ...sessionRecord.scoreComponents,
341
+ dnsAsymmetry
342
+ };
343
+ score = computeFrictionlessScore(sessionRecord?.scoreComponents);
344
+ logger.info(() => ({ data: {
345
+ scoreComponents: { ...sessionRecord?.scoreComponents || {} },
346
+ score,
347
+ dnsPeerAsn: getIpInfoAsn(enrichedDnsEvent?.peerIpInfo),
348
+ dnsResolverAsn: getIpInfoAsn(enrichedDnsEvent?.resolverIpInfo)
349
+ } }));
350
+ }
351
+ try {
352
+ const decisionInput = {
353
+ userAccount: challengeRecord.userAccount,
354
+ dappAccount: challengeRecord.dappAccount,
355
+ captchaResult: "passed",
356
+ headers: challengeRecord.headers,
357
+ captchaType: CaptchaType.puzzle,
358
+ behavioralDataPacked: challengeRecord.behavioralDataPacked,
359
+ deviceCapability: challengeRecord.deviceCapability,
360
+ countryCode: challengeRecord.ipInfo?.isValid ? challengeRecord.ipInfo.countryCode : void 0,
361
+ ipInfo: challengeRecord.ipInfo,
362
+ dnsEvent: enrichedDnsEvent,
363
+ score,
364
+ threshold: sessionRecord?.threshold,
365
+ scoreComponents: sessionRecord?.scoreComponents,
366
+ decryptedHeadHash: sessionRecord?.decryptedHeadHash,
367
+ userSitekeyIpHash: sessionRecord?.userSitekeyIpHash,
368
+ simdReadings: sessionRecord?.simdReadings,
369
+ frictionlessReason: sessionRecord?.reason,
370
+ ruleType: sessionRecord?.ruleType,
371
+ webView: sessionRecord?.webView,
372
+ iFrame: sessionRecord?.iFrame,
373
+ coords: challengeRecord.coords,
374
+ puzzleEvents: challengeRecord.puzzleEvents
375
+ };
376
+ const decision = await this.decisionMachineRunner.decide(decisionInput, logger);
377
+ if (decision.decision === DecisionMachineDecision.Deny) {
378
+ logger.info(() => ({
379
+ msg: "Decision machine denied puzzle captcha in server verification",
380
+ data: {
381
+ userAccount: challengeRecord.userAccount,
382
+ reason: decision.reason,
383
+ score: decision.score,
384
+ tags: decision.tags
385
+ }
386
+ }));
387
+ const dmResult = {
388
+ status: CaptchaStatus.disapproved,
389
+ reason: decision.reason || ResultReason.CAPTCHA_DECISION_MACHINE_DENIED
390
+ };
391
+ await this.db.updatePuzzleCaptchaRecord(challengeRecord.challenge, { result: dmResult });
392
+ if (challengeRecord.sessionId) await this.updateSessionRecordWithCache(challengeRecord.sessionId, {
393
+ serverChecked: true,
394
+ result: dmResult
395
+ });
396
+ return notVerifiedResponse;
397
+ }
398
+ logger.debug(() => ({
399
+ msg: "Decision machine allowed puzzle captcha",
400
+ data: {
401
+ reason: decision.reason,
402
+ score: decision.score,
403
+ tags: decision.tags
404
+ }
405
+ }));
406
+ } catch (error) {
407
+ logger.error(() => ({
408
+ msg: "Failed to run decision machine in server puzzle verification",
409
+ err: error
410
+ }));
411
+ }
412
+ if (challengeRecord.sessionId) await this.updateSessionRecordWithCache(challengeRecord.sessionId, {
413
+ serverChecked: true,
414
+ result: { status: CaptchaStatus.approved }
415
+ });
416
+ return {
417
+ verified: true,
418
+ ...score ? { score } : {}
419
+ };
420
+ }
592
421
  };
422
+ //#endregion
423
+ export { PuzzleCaptchaManager };