@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,688 +1,492 @@
1
- "use strict";
2
- Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
- const util = require("@polkadot/util");
4
- const common = require("@prosopo/common");
5
- const types = require("@prosopo/types");
6
- const util$1 = require("@prosopo/util");
7
- const compositeIpAddress = require("../../compositeIpAddress.cjs");
8
- const util$2 = require("../../util.cjs");
9
- const usageCounters = require("../../util/usageCounters.cjs");
10
- const captchaManager = require("../captchaManager.cjs");
11
- const decisionMachineRunner = require("../decisionMachine/decisionMachineRunner.cjs");
12
- const enrichDnsEvent = require("../dnsEvent/enrichDnsEvent.cjs");
13
- const frictionlessTasksUtils = require("../frictionless/frictionlessTasksUtils.cjs");
14
- const routingMachine = require("../frictionless/routingMachine.cjs");
15
- const evaluateEmailSpamRules = require("../spam/evaluateEmailSpamRules.cjs");
16
- const powTasksUtils = require("./powTasksUtils.cjs");
17
- const DEFAULT_POW_DIFFICULTY = 4;
18
- class PowCaptchaManager extends captchaManager.CaptchaManager {
19
- constructor(db, pair, config, logger, writeQueue, usageCounters2) {
20
- super(db, pair, config, logger, writeQueue);
21
- this.POW_SEPARATOR = types.POW_SEPARATOR;
22
- this.decisionMachineRunner = new decisionMachineRunner.DecisionMachineRunner(db);
23
- this.usageCounters = usageCounters2 ?? null;
24
- }
25
- /**
26
- * Provide the request-side data needed to run the post-pow routing pass
27
- * inside `verifyPowCaptchaSolution`. When unset, the routing pass is
28
- * skipped and verification behaves exactly as before.
29
- */
30
- setPostPowContext(ctx) {
31
- this.postPowContext = ctx;
32
- }
33
- /**
34
- * @description Generates a PoW Captcha for a given user and dapp
35
- *
36
- * @param {string} userAccount - user that is solving the captcha
37
- * @param {string} dappAccount - dapp that is requesting the captcha
38
- * @param origin - not currently used
39
- * @param powDifficulty
40
- */
41
- async getPowCaptchaChallenge(userAccount, dappAccount, origin, powDifficulty) {
42
- const difficulty = powDifficulty || DEFAULT_POW_DIFFICULTY;
43
- const requestedAtTimestamp = Date.now();
44
- const nonce = Math.floor(Math.random() * 1e6);
45
- const challenge = `${requestedAtTimestamp}___${userAccount}___${dappAccount}___${nonce}`;
46
- const challengeSignature = util.u8aToHex(this.pair.sign(util.stringToHex(challenge)));
47
- return {
48
- challenge,
49
- difficulty,
50
- providerSignature: challengeSignature,
51
- requestedAtTimestamp
52
- };
53
- }
54
- /**
55
- * @description Verifies a PoW Captcha for a given user and dapp
56
- *
57
- * @param {string} challenge - the starting string for the PoW challenge
58
- * @param {string} providerChallengeSignature - proof that the Provider provided the challenge
59
- * @param {string} nonce - the string that the user has found that satisfies the PoW challenge
60
- * @param {number} timeout - the time in milliseconds since the Provider was selected to provide the PoW captcha
61
- * @param {string} userTimestampSignature
62
- * @param ipAddress
63
- * @param headers
64
- * @param behavioralData
65
- * @param salt
66
- */
67
- async verifyPowCaptchaSolution(challenge, providerChallengeSignature, nonce, timeout, userTimestampSignature, ipAddress, headers, behavioralData, salt, simdReadings, clientMetaData) {
68
- powTasksUtils.checkPowSignature(
69
- challenge,
70
- providerChallengeSignature,
71
- this.pair.address,
72
- types.ApiParams.challenge
73
- );
74
- const challengeSplit = challenge.split(this.POW_SEPARATOR);
75
- const timestamp = Number.parseInt(util$1.at(challengeSplit, 0));
76
- const userAccount = util$1.at(challengeSplit, 1);
77
- powTasksUtils.checkPowSignature(
78
- timestamp.toString(),
79
- userTimestampSignature,
80
- userAccount,
81
- types.ApiParams.timestamp
82
- );
83
- const challengeRecord = await this.db.getPowCaptchaRecordByChallenge(challenge);
84
- if (!challengeRecord) {
85
- this.logger.debug(() => ({
86
- msg: `No record of this challenge: ${challenge}`
87
- }));
88
- return { verified: false };
89
- }
90
- const difficulty = challengeRecord.difficulty;
91
- let coords;
92
- let saltDecodeError;
93
- if (salt) {
94
- try {
95
- const extractedData = util$1.extractData(salt);
96
- if (extractedData.length >= 2) {
97
- const built = [
98
- [[extractedData[0], extractedData[1]]]
99
- ];
100
- util$1.assertCoordsSafe(built, "coords");
101
- coords = built;
102
- }
103
- } catch (error) {
104
- saltDecodeError = error;
105
- this.logger.warn(() => ({
106
- msg: "Failed to extract coordinates from salt",
107
- error,
108
- salt
109
- }));
110
- }
111
- }
112
- if (saltDecodeError) {
113
- const badSaltResult = {
114
- status: types.CaptchaStatus.disapproved,
115
- reason: types.ResultReason.CAPTCHA_INVALID_SALT
116
- };
117
- const writePromises2 = [
118
- this.db.updatePowCaptchaRecordResult(
119
- challenge,
120
- badSaltResult,
121
- false,
122
- // serverChecked
123
- true,
124
- // userSubmitted
125
- userTimestampSignature,
126
- void 0
127
- // never persist the bad coords
128
- )
129
- ];
130
- if (challengeRecord.sessionId) {
131
- writePromises2.push(
132
- this.updateSessionRecordWithCache(challengeRecord.sessionId, {
133
- userSubmitted: true,
134
- result: badSaltResult
135
- })
136
- );
137
- }
138
- await Promise.all(writePromises2);
139
- return { verified: false };
140
- }
141
- if (!util$1.verifyRecency(challenge, timeout)) {
142
- const timeoutResult = {
143
- status: types.CaptchaStatus.disapproved,
144
- reason: types.ResultReason.CAPTCHA_INVALID_TIMESTAMP
145
- };
146
- const writePromises2 = [
147
- this.db.updatePowCaptchaRecordResult(
148
- challenge,
149
- timeoutResult,
150
- false,
151
- //serverchecked
152
- true,
153
- // usersubmitted
154
- userTimestampSignature,
155
- coords
156
- )
157
- ];
158
- if (challengeRecord.sessionId) {
159
- writePromises2.push(
160
- this.updateSessionRecordWithCache(challengeRecord.sessionId, {
161
- userSubmitted: true,
162
- result: timeoutResult
163
- })
164
- );
165
- }
166
- await Promise.all(writePromises2);
167
- return { verified: false };
168
- }
169
- const correct = powTasksUtils.validateSolution(nonce, challenge, difficulty);
170
- let result = { status: types.CaptchaStatus.approved };
171
- if (!correct) {
172
- result = {
173
- status: types.CaptchaStatus.disapproved,
174
- reason: types.ResultReason.CAPTCHA_INVALID_SOLUTION
175
- };
176
- }
177
- if (correct && this.usageCounters) {
178
- const dappAccount = util$1.at(challengeSplit, 2);
179
- this.usageCounters.incrManyAsync(
180
- dappAccount,
181
- usageCounters.buildAllWindowIncrements(
182
- "solved",
183
- types.CaptchaType.pow,
184
- ipAddress.address,
185
- userAccount
186
- )
187
- );
188
- }
189
- let behavioralUpdates = {};
190
- let decryptedBehavioralDataPacked;
191
- if (behavioralData) {
192
- try {
193
- const decryptKeys = [
194
- // Process DB keys first, then env var key last as env key will likely be invalid
195
- ...await this.getDetectorKeys(),
196
- process.env.BOT_DECRYPTION_KEY
197
- ];
198
- const decryptedData = await this.decryptBehavioralData(
199
- behavioralData,
200
- decryptKeys
201
- );
202
- if (decryptedData) {
203
- const dappAccount = util$1.at(challengeSplit, 2);
204
- this.logger?.info(() => ({
205
- msg: "Behavioral analysis completed",
206
- data: {
207
- userAccount,
208
- dappAccount,
209
- challenge,
210
- mouseEventsCount: decryptedData.collector1?.length || 0,
211
- touchEventsCount: decryptedData.collector2?.length || 0,
212
- clickEventsCount: decryptedData.collector3?.length || 0,
213
- deviceCapability: decryptedData.deviceCapability,
214
- captchaResult: correct ? "passed" : "failed"
215
- }
216
- }));
217
- const packedData = {
218
- c1: decryptedData.collector1 || [],
219
- c2: decryptedData.collector2 || [],
220
- c3: decryptedData.collector3 || [],
221
- d: decryptedData.deviceCapability
222
- };
223
- behavioralUpdates = {
224
- behavioralDataPacked: packedData,
225
- deviceCapability: decryptedData.deviceCapability
226
- };
227
- decryptedBehavioralDataPacked = packedData;
228
- }
229
- } catch (error) {
230
- this.logger?.error(() => ({
231
- msg: "Failed to process behavioral data",
232
- err: error
233
- }));
234
- }
235
- }
236
- const recordUpdates = { ...behavioralUpdates };
237
- if (clientMetaData?.hp) {
238
- recordUpdates.clientMetaData = { hp: clientMetaData.hp };
239
- }
240
- if (Object.keys(recordUpdates).length > 0) {
241
- await this.db.updatePowCaptchaRecord(challenge, recordUpdates);
242
- }
243
- const writePromises = [
244
- this.db.updatePowCaptchaRecordResult(
245
- challenge,
246
- result,
247
- false,
248
- true,
249
- userTimestampSignature,
250
- coords
251
- )
252
- ];
253
- if (challengeRecord.sessionId) {
254
- const linkedSessionId = challengeRecord.sessionId;
255
- writePromises.push(
256
- this.updateSessionRecordWithCache(linkedSessionId, {
257
- userSubmitted: true,
258
- result
259
- })
260
- );
261
- if (simdReadings) {
262
- writePromises.push(
263
- this.decryptAndAttachSimdReadingsIfAbsent(
264
- linkedSessionId,
265
- simdReadings,
266
- types.SimdReadingsStage.submit
267
- )
268
- );
269
- }
270
- }
271
- await Promise.all(writePromises);
272
- if (!correct) {
273
- return { verified: correct };
274
- }
275
- const escalateForMissingCoords = !coords && Boolean(challengeRecord.sessionId);
276
- let routingOutput = this.postPowContext ? await this.runPostPowRouting({
277
- challengeRecord,
278
- challengeSplit,
279
- behavioralDataPacked: decryptedBehavioralDataPacked
280
- }) : void 0;
281
- if (escalateForMissingCoords && routingOutput?.captchaType !== types.CaptchaType.image && routingOutput?.captchaType !== types.CaptchaType.puzzle) {
282
- routingOutput = {
283
- captchaType: types.CaptchaType.image,
284
- reason: types.FrictionlessReason.MISSING_COORDINATES
285
- };
286
- }
287
- return { verified: correct, routingOutput };
288
- }
289
- /**
290
- * Resolve the originating session, build a full RoutingContext, and run the
291
- * post-pow phase of the routing machine. Returns the machine's output, or
292
- * undefined when prerequisites are missing (no session, no score) — in
293
- * which case the caller treats the result as "no escalation".
294
- */
295
- async runPostPowRouting(args) {
296
- if (!this.postPowContext) return void 0;
297
- const { challengeRecord, challengeSplit, behavioralDataPacked } = args;
298
- if (!challengeRecord.sessionId) {
299
- this.logger.debug(() => ({
300
- msg: "Skipping post-pow routing: PoW record has no linked session"
301
- }));
302
- return void 0;
303
- }
304
- const sessionRecord = await this.db.getSessionRecordBySessionId(
305
- challengeRecord.sessionId
306
- );
307
- if (!sessionRecord) {
308
- this.logger.debug(() => ({
309
- msg: "Skipping post-pow routing: session record not found",
310
- data: { sessionId: challengeRecord.sessionId }
311
- }));
312
- return void 0;
313
- }
314
- const score = frictionlessTasksUtils.computeFrictionlessScore(sessionRecord.scoreComponents);
315
- const dappAccount = util$1.at(challengeSplit, 2);
316
- const userAccount = util$1.at(challengeSplit, 1);
317
- const platform = {
318
- ...this.postPowContext.platform,
319
- isWebView: sessionRecord.webView
320
- };
321
- const ctx = {
322
- dappAccount,
323
- userAccount,
324
- ip: this.postPowContext.ip,
325
- countryCode: this.postPowContext.countryCode,
326
- score,
327
- platform,
328
- raw: {
329
- ...this.postPowContext.raw,
330
- ...behavioralDataPacked && { behavioralDataPacked },
331
- // SIMD readings are decrypted and attached to the session above
332
- // (decryptAndAttachSimdReadingsIfAbsent) before this re-fetch, so
333
- // they are available here in decoded form for the routing machine.
334
- ...sessionRecord.simdReadings && { simd: sessionRecord.simdReadings },
335
- // currentUrl / iframeUrl were captured at frictionless time
336
- // from the widget's payload — the submit request has no
337
- // equivalent signal (its Referer is the captcha iframe, not
338
- // the host page) so we surface them from the persisted
339
- // session. iframeUrl is only present when the widget was
340
- // embedded at frictionless time.
341
- ...sessionRecord.currentUrl && {
342
- currentUrl: sessionRecord.currentUrl
343
- },
344
- ...sessionRecord.iframeUrl && {
345
- iframeUrl: sessionRecord.iframeUrl
346
- }
347
- }
348
- };
349
- const baseline = {
350
- captchaType: types.CaptchaType.pow,
351
- powDifficulty: challengeRecord.difficulty
352
- };
353
- return routingMachine.applyRouter(
354
- this.decisionMachineRunner,
355
- this.usageCounters,
356
- baseline,
357
- ctx,
358
- this.logger,
359
- "postPow"
360
- );
361
- }
362
- /**
363
- * @description Verifies a PoW Captcha for a given user and dapp. This is called by the server to verify the user's solution
364
- * and update the record in the database to show that the user has solved the captcha
365
- *
366
- * @param {string} dappAccount - the dapp that is requesting the captcha
367
- * @param {string} challenge - the starting string for the PoW challenge
368
- * @param {number} timeout - the time in milliseconds since the Provider was selected to provide the PoW captcha
369
- * @param env - provider environment
370
- * @param ip - optional IP address for validation
371
- * @param userAccessRulesStorage - storage for querying user access policies
372
- * @param email
373
- * @param spamEmailDomainCheckingEnabled
374
- * @param spamFilter
375
- * @param trafficFilter
376
- * @param storeMetadata - when true, persists the dapp-server-provided
377
- * `email` (and any future metadata fields) on the captcha record so
378
- * it can be inspected for spam-rate analysis.
379
- */
380
- async serverVerifyPowCaptchaSolution(dappAccount, challenge, timeout, env, ip, userAccessRulesStorage, email, spamEmailDomainCheckingEnabled = false, spamFilter, trafficFilter, storeMetadata = false) {
381
- const notVerified = (reason) => ({
382
- verified: false,
383
- reason
384
- });
385
- const logger = this.logger.with({ challenge, dappAccount });
386
- const challengeRecord = await this.db.getPowCaptchaRecordByChallenge(challenge);
387
- if (!challengeRecord) {
388
- logger.debug(() => ({
389
- msg: `No record of this challenge: ${challenge}`
390
- }));
391
- return notVerified("CAPTCHA.DAPP_USER_SOLUTION_NOT_FOUND");
392
- }
393
- if (challengeRecord.result.status !== types.CaptchaStatus.approved) {
394
- throw new common.ProsopoApiError("CAPTCHA.INVALID_SOLUTION", {
395
- context: {
396
- failedFuncName: this.serverVerifyPowCaptchaSolution.name,
397
- challenge
398
- }
399
- });
400
- }
401
- if (challengeRecord.serverChecked)
402
- return notVerified("API.USER_ALREADY_VERIFIED");
403
- const challengeDappAccount = challengeRecord.dappAccount;
404
- if (dappAccount !== challengeDappAccount) {
405
- throw new common.ProsopoEnvError("CAPTCHA.DAPP_USER_SOLUTION_NOT_FOUND", {
406
- context: {
407
- failedFuncName: this.serverVerifyPowCaptchaSolution.name,
408
- dappAccount,
409
- challengeDappAccount
410
- }
411
- });
412
- }
413
- await this.db.markDappUserPoWCommitmentsChecked([
414
- challengeRecord.challenge
415
- ]);
416
- const powRecordUpdates = {};
417
- let failResult;
418
- let failReason;
419
- const submittedAt = challengeRecord.submittedAtTimestamp;
420
- const submitToVerifyMs = submittedAt instanceof Date ? Date.now() - submittedAt.getTime() : Number.POSITIVE_INFINITY;
421
- if (submitToVerifyMs > timeout) {
422
- failResult = {
423
- status: types.CaptchaStatus.disapproved,
424
- reason: types.ResultReason.TIMESTAMP_TOO_OLD
425
- };
426
- failReason = "API.TIMESTAMP_TOO_OLD";
427
- }
428
- if (!failResult && userAccessRulesStorage) {
429
- try {
430
- const blockPolicy = await this.checkForHardBlock(
431
- userAccessRulesStorage,
432
- challengeRecord,
433
- challengeRecord.userAccount,
434
- challengeRecord.headers,
435
- challengeRecord.coords,
436
- challengeRecord.ipInfo?.isValid ? challengeRecord.ipInfo.countryCode : void 0,
437
- challengeRecord.ipInfo?.isValid ? challengeRecord.ipInfo.asnNumber : void 0
438
- );
439
- if (blockPolicy) {
440
- logger.info(() => ({
441
- msg: "User blocked by access policy in server PoW verification",
442
- data: {
443
- userAccount: challengeRecord.userAccount,
444
- policy: blockPolicy
445
- }
446
- }));
447
- failResult = {
448
- status: types.CaptchaStatus.disapproved,
449
- reason: types.ResultReason.ACCESS_POLICY_BLOCK
450
- };
451
- failReason = "API.ACCESS_POLICY_BLOCK";
452
- }
453
- } catch (error) {
454
- logger.warn(() => ({
455
- msg: "Failed to check user access policies in server PoW verification",
456
- error
457
- }));
458
- }
459
- }
460
- if (!failResult && email && spamEmailDomainCheckingEnabled) {
461
- try {
462
- const isSpam = await this.checkSpamEmail(email);
463
- if (isSpam) {
464
- const emailDomain = email.split("@")[1] || "unknown";
465
- logger.info(() => ({
466
- msg: "Spam email domain detected in server PoW verification",
467
- data: { emailDomain }
468
- }));
469
- failResult = {
470
- status: types.CaptchaStatus.disapproved,
471
- reason: types.ResultReason.SPAM_EMAIL_DOMAIN
472
- };
473
- failReason = "API.SPAM_EMAIL_DOMAIN";
474
- }
475
- } catch (error) {
476
- logger.warn(() => ({
477
- msg: "Failed to check spam email domain in server PoW verification",
478
- error
479
- }));
480
- }
481
- }
482
- if (!failResult && spamFilter?.enabled && spamFilter.emailRules?.enabled && email) {
483
- const result = evaluateEmailSpamRules.evaluateEmailSpamRules(email, spamFilter.emailRules);
484
- if (result.isSpam) {
485
- logger.info(() => ({
486
- msg: "Spam filter rejected email in PoW verification",
487
- data: { reason: result.reason }
488
- }));
489
- failResult = {
490
- status: types.CaptchaStatus.disapproved,
491
- reason: types.ResultReason.SPAM_EMAIL_RULE
492
- };
493
- failReason = "API.SPAM_EMAIL_RULE";
494
- }
495
- }
496
- const sessionRecord = challengeRecord.sessionId ? await this.db.getSessionRecordBySessionId(challengeRecord.sessionId) : void 0;
497
- const enrichedDnsEvent = await enrichDnsEvent.enrichDnsEvent(
498
- sessionRecord?.dnsEvent,
499
- env.ipInfoService,
500
- ip ?? challengeRecord.ipInfo?.ip
501
- );
502
- if (!failResult) {
503
- const check = await this.resolveTrafficFilterCheck(
504
- env,
505
- challengeRecord.ipInfo,
506
- trafficFilter,
507
- ip,
508
- enrichedDnsEvent
509
- );
510
- if (check.isBlocked) {
511
- logger.info(() => ({
512
- msg: "Traffic filter rejected request in PoW verification",
513
- data: {
514
- ip,
515
- reason: check.reason,
516
- dnsPeerIp: enrichedDnsEvent?.peerIp,
517
- dnsResolverIp: enrichedDnsEvent?.resolverIp,
518
- dnsPeerAsn: enrichDnsEvent.getIpInfoAsn(enrichedDnsEvent?.peerIpInfo),
519
- dnsResolverAsn: enrichDnsEvent.getIpInfoAsn(enrichedDnsEvent?.resolverIpInfo),
520
- dnsPathValid: enrichedDnsEvent?.pathValid
521
- }
522
- }));
523
- failResult = {
524
- status: types.CaptchaStatus.disapproved,
525
- reason: check.reason
526
- };
527
- failReason = check.reason;
528
- }
529
- }
530
- if (storeMetadata && email) {
531
- powRecordUpdates.metadata = { email };
532
- }
533
- if (!failResult && ip) {
534
- const challengeIpAddress = compositeIpAddress.getIpAddressFromComposite(
535
- challengeRecord.ipAddress
536
- );
537
- const clientRecord = await this.db.getClientRecord(dappAccount);
538
- const ipValidationRules = clientRecord?.settings?.ipValidationRules;
539
- powRecordUpdates.providedIp = compositeIpAddress.getCompositeIpAddress(ip);
540
- if (ipValidationRules?.enabled === true) {
541
- const ipValidation = await util$2.deepValidateIpAddress(
542
- ip,
543
- challengeIpAddress,
544
- logger,
545
- env.ipInfoService,
546
- ipValidationRules,
547
- enrichedDnsEvent?.peerIp
548
- );
549
- if (!ipValidation.isValid) {
550
- logger.error(() => ({
551
- msg: "IP validation failed for PoW captcha",
552
- data: {
553
- ip,
554
- challengeIp: challengeIpAddress.address,
555
- error: ipValidation.errorMessage,
556
- distanceKm: ipValidation.distanceKm
557
- }
558
- }));
559
- failResult = {
560
- status: types.CaptchaStatus.disapproved,
561
- reason: types.ResultReason.FAILED_IP_VALIDATION
562
- };
563
- failReason = "API.FAILED_IP_VALIDATION";
564
- }
565
- }
566
- }
567
- let score;
568
- if (sessionRecord) {
569
- const dnsAsymmetry = enrichDnsEvent.computeDnsAsymmetry(
570
- enrichedDnsEvent,
571
- challengeRecord.ipInfo,
572
- trafficFilter
573
- );
574
- if (dnsAsymmetry > 0) {
575
- sessionRecord.scoreComponents = {
576
- ...sessionRecord.scoreComponents,
577
- dnsAsymmetry
578
- };
579
- }
580
- score = frictionlessTasksUtils.computeFrictionlessScore(sessionRecord?.scoreComponents);
581
- logger.info(() => ({
582
- data: {
583
- scoreComponents: { ...sessionRecord?.scoreComponents || {} },
584
- score,
585
- dnsPeerAsn: enrichDnsEvent.getIpInfoAsn(enrichedDnsEvent?.peerIpInfo),
586
- dnsResolverAsn: enrichDnsEvent.getIpInfoAsn(enrichedDnsEvent?.resolverIpInfo)
587
- }
588
- }));
589
- }
590
- if (!failResult) {
591
- try {
592
- const decisionInput = {
593
- userAccount: challengeRecord.userAccount,
594
- dappAccount: challengeRecord.dappAccount,
595
- captchaResult: "passed",
596
- headers: challengeRecord.headers,
597
- captchaType: types.CaptchaType.pow,
598
- behavioralDataPacked: challengeRecord.behavioralDataPacked,
599
- deviceCapability: challengeRecord.deviceCapability,
600
- countryCode: challengeRecord.ipInfo?.isValid ? challengeRecord.ipInfo.countryCode : void 0,
601
- ipInfo: challengeRecord.ipInfo,
602
- dnsEvent: enrichedDnsEvent,
603
- score,
604
- threshold: sessionRecord?.threshold,
605
- scoreComponents: sessionRecord?.scoreComponents,
606
- decryptedHeadHash: sessionRecord?.decryptedHeadHash,
607
- userSitekeyIpHash: sessionRecord?.userSitekeyIpHash,
608
- simdReadings: sessionRecord?.simdReadings,
609
- frictionlessReason: sessionRecord?.reason,
610
- ruleType: sessionRecord?.ruleType,
611
- webView: sessionRecord?.webView,
612
- iFrame: sessionRecord?.iFrame,
613
- coords: challengeRecord.coords
614
- };
615
- const decision = await this.decisionMachineRunner.decide(
616
- decisionInput,
617
- logger
618
- );
619
- if (decision.decision === types.DecisionMachineDecision.Deny) {
620
- logger.info(() => ({
621
- msg: "Decision machine denied PoW captcha in server verification",
622
- data: {
623
- userAccount: challengeRecord.userAccount,
624
- reason: decision.reason,
625
- score: decision.score,
626
- tags: decision.tags
627
- }
628
- }));
629
- const dmReason = decision.reason || types.ResultReason.CAPTCHA_DECISION_MACHINE_DENIED;
630
- failResult = {
631
- status: types.CaptchaStatus.disapproved,
632
- reason: dmReason
633
- };
634
- failReason = dmReason;
635
- } else {
636
- logger.debug(() => ({
637
- msg: "Decision machine allowed PoW captcha",
638
- data: {
639
- reason: decision.reason,
640
- score: decision.score,
641
- tags: decision.tags
642
- }
643
- }));
644
- }
645
- } catch (error) {
646
- logger.error(() => ({
647
- msg: "Failed to run decision machine in server PoW verification",
648
- err: error
649
- }));
650
- }
651
- }
652
- const finalResult = failResult || {
653
- status: types.CaptchaStatus.approved
654
- };
655
- if (failResult) {
656
- powRecordUpdates.result = failResult;
657
- }
658
- const writePromises = [];
659
- if (Object.keys(powRecordUpdates).length > 0) {
660
- writePromises.push(
661
- this.db.updatePowCaptchaRecord(
662
- challengeRecord.challenge,
663
- powRecordUpdates
664
- )
665
- );
666
- }
667
- if (challengeRecord.sessionId) {
668
- writePromises.push(
669
- this.updateSessionRecordWithCache(
670
- challengeRecord.sessionId,
671
- {
672
- serverChecked: true,
673
- result: finalResult
674
- },
675
- true
676
- )
677
- );
678
- }
679
- if (writePromises.length > 0) {
680
- await Promise.all(writePromises);
681
- }
682
- if (failReason) {
683
- return notVerified(failReason);
684
- }
685
- return { verified: true, ...score ? { score } : {} };
686
- }
687
- }
1
+ const require_usageCounters = require("../../util/usageCounters.cjs");
2
+ const require_decisionMachineRunner = require("../decisionMachine/decisionMachineRunner.cjs");
3
+ const require_compositeIpAddress = require("../../compositeIpAddress.cjs");
4
+ const require_enrichDnsEvent = require("../dnsEvent/enrichDnsEvent.cjs");
5
+ const require_captchaManager = require("../captchaManager.cjs");
6
+ const require_routingMachine = require("../frictionless/routingMachine.cjs");
7
+ const require_util = require("../../util.cjs");
8
+ const require_frictionlessTasksUtils = require("../frictionless/frictionlessTasksUtils.cjs");
9
+ const require_evaluateEmailSpamRules = require("../spam/evaluateEmailSpamRules.cjs");
10
+ const require_powTasksUtils = require("./powTasksUtils.cjs");
11
+ let _prosopo_common = require("@prosopo/common");
12
+ let _prosopo_types = require("@prosopo/types");
13
+ let _prosopo_util = require("@prosopo/util");
14
+ let _polkadot_util = require("@polkadot/util");
15
+ //#region src/tasks/powCaptcha/powTasks.ts
16
+ var DEFAULT_POW_DIFFICULTY = 4;
17
+ var PowCaptchaManager = class extends require_captchaManager.CaptchaManager {
18
+ constructor(db, pair, config, logger, writeQueue, usageCounters) {
19
+ super(db, pair, config, logger, writeQueue);
20
+ this.POW_SEPARATOR = _prosopo_types.POW_SEPARATOR;
21
+ this.decisionMachineRunner = new require_decisionMachineRunner.DecisionMachineRunner(db);
22
+ this.usageCounters = usageCounters ?? null;
23
+ }
24
+ /**
25
+ * Provide the request-side data needed to run the post-pow routing pass
26
+ * inside `verifyPowCaptchaSolution`. When unset, the routing pass is
27
+ * skipped and verification behaves exactly as before.
28
+ */
29
+ setPostPowContext(ctx) {
30
+ this.postPowContext = ctx;
31
+ }
32
+ /**
33
+ * @description Generates a PoW Captcha for a given user and dapp
34
+ *
35
+ * @param {string} userAccount - user that is solving the captcha
36
+ * @param {string} dappAccount - dapp that is requesting the captcha
37
+ * @param origin - not currently used
38
+ * @param powDifficulty
39
+ */
40
+ async getPowCaptchaChallenge(userAccount, dappAccount, origin, powDifficulty) {
41
+ const difficulty = powDifficulty || DEFAULT_POW_DIFFICULTY;
42
+ const requestedAtTimestamp = Date.now();
43
+ const challenge = `${requestedAtTimestamp}___${userAccount}___${dappAccount}___${Math.floor(Math.random() * 1e6)}`;
44
+ return {
45
+ challenge,
46
+ difficulty,
47
+ providerSignature: (0, _polkadot_util.u8aToHex)(this.pair.sign((0, _polkadot_util.stringToHex)(challenge))),
48
+ requestedAtTimestamp
49
+ };
50
+ }
51
+ /**
52
+ * @description Verifies a PoW Captcha for a given user and dapp
53
+ *
54
+ * @param {string} challenge - the starting string for the PoW challenge
55
+ * @param {string} providerChallengeSignature - proof that the Provider provided the challenge
56
+ * @param {string} nonce - the string that the user has found that satisfies the PoW challenge
57
+ * @param {number} timeout - the time in milliseconds since the Provider was selected to provide the PoW captcha
58
+ * @param {string} userTimestampSignature
59
+ * @param ipAddress
60
+ * @param headers
61
+ * @param behavioralData
62
+ * @param salt
63
+ */
64
+ async verifyPowCaptchaSolution(challenge, providerChallengeSignature, nonce, timeout, userTimestampSignature, ipAddress, headers, behavioralData, salt, simdReadings, clientMetaData) {
65
+ require_powTasksUtils.checkPowSignature(challenge, providerChallengeSignature, this.pair.address, _prosopo_types.ApiParams.challenge);
66
+ const challengeSplit = challenge.split(this.POW_SEPARATOR);
67
+ const timestamp = Number.parseInt((0, _prosopo_util.at)(challengeSplit, 0));
68
+ const userAccount = (0, _prosopo_util.at)(challengeSplit, 1);
69
+ require_powTasksUtils.checkPowSignature(timestamp.toString(), userTimestampSignature, userAccount, _prosopo_types.ApiParams.timestamp);
70
+ const challengeRecord = await this.db.getPowCaptchaRecordByChallenge(challenge);
71
+ if (!challengeRecord) {
72
+ this.logger.debug(() => ({ msg: `No record of this challenge: ${challenge}` }));
73
+ return { verified: false };
74
+ }
75
+ const difficulty = challengeRecord.difficulty;
76
+ let coords;
77
+ let saltDecodeError;
78
+ if (salt) try {
79
+ const extractedData = (0, _prosopo_util.extractData)(salt);
80
+ if (extractedData.length >= 2) {
81
+ const built = [[[extractedData[0], extractedData[1]]]];
82
+ (0, _prosopo_util.assertCoordsSafe)(built, "coords");
83
+ coords = built;
84
+ }
85
+ } catch (error) {
86
+ saltDecodeError = error;
87
+ this.logger.warn(() => ({
88
+ msg: "Failed to extract coordinates from salt",
89
+ error,
90
+ salt
91
+ }));
92
+ }
93
+ if (saltDecodeError) {
94
+ const badSaltResult = {
95
+ status: _prosopo_types.CaptchaStatus.disapproved,
96
+ reason: _prosopo_types.ResultReason.CAPTCHA_INVALID_SALT
97
+ };
98
+ const writePromises = [this.db.updatePowCaptchaRecordResult(challenge, badSaltResult, false, true, userTimestampSignature, void 0)];
99
+ if (challengeRecord.sessionId) writePromises.push(this.updateSessionRecordWithCache(challengeRecord.sessionId, {
100
+ userSubmitted: true,
101
+ result: badSaltResult
102
+ }));
103
+ await Promise.all(writePromises);
104
+ return { verified: false };
105
+ }
106
+ if (!(0, _prosopo_util.verifyRecency)(challenge, timeout)) {
107
+ const timeoutResult = {
108
+ status: _prosopo_types.CaptchaStatus.disapproved,
109
+ reason: _prosopo_types.ResultReason.CAPTCHA_INVALID_TIMESTAMP
110
+ };
111
+ const writePromises = [this.db.updatePowCaptchaRecordResult(challenge, timeoutResult, false, true, userTimestampSignature, coords)];
112
+ if (challengeRecord.sessionId) writePromises.push(this.updateSessionRecordWithCache(challengeRecord.sessionId, {
113
+ userSubmitted: true,
114
+ result: timeoutResult
115
+ }));
116
+ await Promise.all(writePromises);
117
+ return { verified: false };
118
+ }
119
+ const correct = require_powTasksUtils.validateSolution(nonce, challenge, difficulty);
120
+ let result = { status: _prosopo_types.CaptchaStatus.approved };
121
+ if (!correct) result = {
122
+ status: _prosopo_types.CaptchaStatus.disapproved,
123
+ reason: _prosopo_types.ResultReason.CAPTCHA_INVALID_SOLUTION
124
+ };
125
+ if (correct && this.usageCounters) {
126
+ const dappAccount = (0, _prosopo_util.at)(challengeSplit, 2);
127
+ this.usageCounters.incrManyAsync(dappAccount, require_usageCounters.buildAllWindowIncrements("solved", _prosopo_types.CaptchaType.pow, ipAddress.address, userAccount));
128
+ }
129
+ let behavioralUpdates = {};
130
+ let decryptedBehavioralDataPacked;
131
+ if (behavioralData) try {
132
+ const decryptKeys = [...await this.getDetectorKeys(), process.env.BOT_DECRYPTION_KEY];
133
+ const decryptedData = await this.decryptBehavioralData(behavioralData, decryptKeys);
134
+ if (decryptedData) {
135
+ const dappAccount = (0, _prosopo_util.at)(challengeSplit, 2);
136
+ this.logger?.info(() => ({
137
+ msg: "Behavioral analysis completed",
138
+ data: {
139
+ userAccount,
140
+ dappAccount,
141
+ challenge,
142
+ mouseEventsCount: decryptedData.collector1?.length || 0,
143
+ touchEventsCount: decryptedData.collector2?.length || 0,
144
+ clickEventsCount: decryptedData.collector3?.length || 0,
145
+ deviceCapability: decryptedData.deviceCapability,
146
+ captchaResult: correct ? "passed" : "failed"
147
+ }
148
+ }));
149
+ const packedData = {
150
+ c1: decryptedData.collector1 || [],
151
+ c2: decryptedData.collector2 || [],
152
+ c3: decryptedData.collector3 || [],
153
+ d: decryptedData.deviceCapability
154
+ };
155
+ behavioralUpdates = {
156
+ behavioralDataPacked: packedData,
157
+ deviceCapability: decryptedData.deviceCapability
158
+ };
159
+ decryptedBehavioralDataPacked = packedData;
160
+ }
161
+ } catch (error) {
162
+ this.logger?.error(() => ({
163
+ msg: "Failed to process behavioral data",
164
+ err: error
165
+ }));
166
+ }
167
+ const recordUpdates = { ...behavioralUpdates };
168
+ if (clientMetaData?.hp) recordUpdates.clientMetaData = { hp: clientMetaData.hp };
169
+ if (Object.keys(recordUpdates).length > 0) await this.db.updatePowCaptchaRecord(challenge, recordUpdates);
170
+ const writePromises = [this.db.updatePowCaptchaRecordResult(challenge, result, false, true, userTimestampSignature, coords)];
171
+ if (challengeRecord.sessionId) {
172
+ const linkedSessionId = challengeRecord.sessionId;
173
+ writePromises.push(this.updateSessionRecordWithCache(linkedSessionId, {
174
+ userSubmitted: true,
175
+ result
176
+ }));
177
+ if (simdReadings) writePromises.push(this.decryptAndAttachSimdReadingsIfAbsent(linkedSessionId, simdReadings, _prosopo_types.SimdReadingsStage.submit));
178
+ }
179
+ await Promise.all(writePromises);
180
+ if (!correct) return { verified: correct };
181
+ const escalateForMissingCoords = !coords && Boolean(challengeRecord.sessionId);
182
+ let routingOutput = this.postPowContext ? await this.runPostPowRouting({
183
+ challengeRecord,
184
+ challengeSplit,
185
+ behavioralDataPacked: decryptedBehavioralDataPacked
186
+ }) : void 0;
187
+ if (escalateForMissingCoords && routingOutput?.captchaType !== _prosopo_types.CaptchaType.image && routingOutput?.captchaType !== _prosopo_types.CaptchaType.puzzle) routingOutput = {
188
+ captchaType: _prosopo_types.CaptchaType.image,
189
+ reason: _prosopo_types.FrictionlessReason.MISSING_COORDINATES
190
+ };
191
+ return {
192
+ verified: correct,
193
+ routingOutput
194
+ };
195
+ }
196
+ /**
197
+ * Resolve the originating session, build a full RoutingContext, and run the
198
+ * post-pow phase of the routing machine. Returns the machine's output, or
199
+ * undefined when prerequisites are missing (no session, no score) — in
200
+ * which case the caller treats the result as "no escalation".
201
+ */
202
+ async runPostPowRouting(args) {
203
+ if (!this.postPowContext) return void 0;
204
+ const { challengeRecord, challengeSplit, behavioralDataPacked } = args;
205
+ if (!challengeRecord.sessionId) {
206
+ this.logger.debug(() => ({ msg: "Skipping post-pow routing: PoW record has no linked session" }));
207
+ return;
208
+ }
209
+ const sessionRecord = await this.db.getSessionRecordBySessionId(challengeRecord.sessionId);
210
+ if (!sessionRecord) {
211
+ this.logger.debug(() => ({
212
+ msg: "Skipping post-pow routing: session record not found",
213
+ data: { sessionId: challengeRecord.sessionId }
214
+ }));
215
+ return;
216
+ }
217
+ const score = require_frictionlessTasksUtils.computeFrictionlessScore(sessionRecord.scoreComponents);
218
+ const dappAccount = (0, _prosopo_util.at)(challengeSplit, 2);
219
+ const userAccount = (0, _prosopo_util.at)(challengeSplit, 1);
220
+ const platform = {
221
+ ...this.postPowContext.platform,
222
+ isWebView: sessionRecord.webView
223
+ };
224
+ const ctx = {
225
+ dappAccount,
226
+ userAccount,
227
+ ip: this.postPowContext.ip,
228
+ countryCode: this.postPowContext.countryCode,
229
+ score,
230
+ platform,
231
+ raw: {
232
+ ...this.postPowContext.raw,
233
+ ...behavioralDataPacked && { behavioralDataPacked },
234
+ ...sessionRecord.simdReadings && { simd: sessionRecord.simdReadings },
235
+ ...sessionRecord.currentUrl && { currentUrl: sessionRecord.currentUrl },
236
+ ...sessionRecord.iframeUrl && { iframeUrl: sessionRecord.iframeUrl }
237
+ }
238
+ };
239
+ const baseline = {
240
+ captchaType: _prosopo_types.CaptchaType.pow,
241
+ powDifficulty: challengeRecord.difficulty
242
+ };
243
+ return require_routingMachine.applyRouter(this.decisionMachineRunner, this.usageCounters, baseline, ctx, this.logger, "postPow");
244
+ }
245
+ /**
246
+ * @description Verifies a PoW Captcha for a given user and dapp. This is called by the server to verify the user's solution
247
+ * and update the record in the database to show that the user has solved the captcha
248
+ *
249
+ * @param {string} dappAccount - the dapp that is requesting the captcha
250
+ * @param {string} challenge - the starting string for the PoW challenge
251
+ * @param {number} timeout - the time in milliseconds since the Provider was selected to provide the PoW captcha
252
+ * @param env - provider environment
253
+ * @param ip - optional IP address for validation
254
+ * @param userAccessRulesStorage - storage for querying user access policies
255
+ * @param email
256
+ * @param spamEmailDomainCheckingEnabled
257
+ * @param spamFilter
258
+ * @param trafficFilter
259
+ * @param storeMetadata - when true, persists the dapp-server-provided
260
+ * `email` (and any future metadata fields) on the captcha record so
261
+ * it can be inspected for spam-rate analysis.
262
+ */
263
+ async serverVerifyPowCaptchaSolution(dappAccount, challenge, timeout, env, ip, userAccessRulesStorage, email, spamEmailDomainCheckingEnabled = false, spamFilter, trafficFilter, storeMetadata = false) {
264
+ const notVerified = (reason) => ({
265
+ verified: false,
266
+ reason
267
+ });
268
+ const logger = this.logger.with({
269
+ challenge,
270
+ dappAccount
271
+ });
272
+ const challengeRecord = await this.db.getPowCaptchaRecordByChallenge(challenge);
273
+ if (!challengeRecord) {
274
+ logger.debug(() => ({ msg: `No record of this challenge: ${challenge}` }));
275
+ return notVerified("CAPTCHA.DAPP_USER_SOLUTION_NOT_FOUND");
276
+ }
277
+ if (challengeRecord.result.status !== _prosopo_types.CaptchaStatus.approved) throw new _prosopo_common.ProsopoApiError("CAPTCHA.INVALID_SOLUTION", { context: {
278
+ code: 400,
279
+ failedFuncName: this.serverVerifyPowCaptchaSolution.name,
280
+ challenge
281
+ } });
282
+ if (challengeRecord.serverChecked) return notVerified("API.USER_ALREADY_VERIFIED");
283
+ const challengeDappAccount = challengeRecord.dappAccount;
284
+ if (dappAccount !== challengeDappAccount) throw new _prosopo_common.ProsopoEnvError("CAPTCHA.DAPP_USER_SOLUTION_NOT_FOUND", { context: {
285
+ failedFuncName: this.serverVerifyPowCaptchaSolution.name,
286
+ dappAccount,
287
+ challengeDappAccount
288
+ } });
289
+ await this.db.markDappUserPoWCommitmentsChecked([challengeRecord.challenge]);
290
+ const powRecordUpdates = {};
291
+ let failResult;
292
+ let failReason;
293
+ const submittedAt = challengeRecord.submittedAtTimestamp;
294
+ if ((submittedAt instanceof Date ? Date.now() - submittedAt.getTime() : Number.POSITIVE_INFINITY) > timeout) {
295
+ failResult = {
296
+ status: _prosopo_types.CaptchaStatus.disapproved,
297
+ reason: _prosopo_types.ResultReason.TIMESTAMP_TOO_OLD
298
+ };
299
+ failReason = "API.TIMESTAMP_TOO_OLD";
300
+ }
301
+ if (!failResult && userAccessRulesStorage) try {
302
+ 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);
303
+ if (blockPolicy) {
304
+ logger.info(() => ({
305
+ msg: "User blocked by access policy in server PoW verification",
306
+ data: {
307
+ userAccount: challengeRecord.userAccount,
308
+ policy: blockPolicy
309
+ }
310
+ }));
311
+ failResult = {
312
+ status: _prosopo_types.CaptchaStatus.disapproved,
313
+ reason: _prosopo_types.ResultReason.ACCESS_POLICY_BLOCK
314
+ };
315
+ failReason = "API.ACCESS_POLICY_BLOCK";
316
+ }
317
+ } catch (error) {
318
+ logger.warn(() => ({
319
+ msg: "Failed to check user access policies in server PoW verification",
320
+ error
321
+ }));
322
+ }
323
+ if (!failResult && email && spamEmailDomainCheckingEnabled) try {
324
+ if (await this.checkSpamEmail(email)) {
325
+ const emailDomain = email.split("@")[1] || "unknown";
326
+ logger.info(() => ({
327
+ msg: "Spam email domain detected in server PoW verification",
328
+ data: { emailDomain }
329
+ }));
330
+ failResult = {
331
+ status: _prosopo_types.CaptchaStatus.disapproved,
332
+ reason: _prosopo_types.ResultReason.SPAM_EMAIL_DOMAIN
333
+ };
334
+ failReason = "API.SPAM_EMAIL_DOMAIN";
335
+ }
336
+ } catch (error) {
337
+ logger.warn(() => ({
338
+ msg: "Failed to check spam email domain in server PoW verification",
339
+ error
340
+ }));
341
+ }
342
+ if (!failResult && spamFilter?.enabled && spamFilter.emailRules?.enabled && email) {
343
+ const result = require_evaluateEmailSpamRules.evaluateEmailSpamRules(email, spamFilter.emailRules);
344
+ if (result.isSpam) {
345
+ logger.info(() => ({
346
+ msg: "Spam filter rejected email in PoW verification",
347
+ data: { reason: result.reason }
348
+ }));
349
+ failResult = {
350
+ status: _prosopo_types.CaptchaStatus.disapproved,
351
+ reason: _prosopo_types.ResultReason.SPAM_EMAIL_RULE
352
+ };
353
+ failReason = "API.SPAM_EMAIL_RULE";
354
+ }
355
+ }
356
+ const sessionRecord = challengeRecord.sessionId ? await this.db.getSessionRecordBySessionId(challengeRecord.sessionId) : void 0;
357
+ const enrichedDnsEvent = await require_enrichDnsEvent.enrichDnsEvent(sessionRecord?.dnsEvent, env.ipInfoService, ip ?? challengeRecord.ipInfo?.ip);
358
+ if (!failResult) {
359
+ const check = await this.resolveTrafficFilterCheck(env, challengeRecord.ipInfo, trafficFilter, ip, enrichedDnsEvent);
360
+ if (check.isBlocked) {
361
+ logger.info(() => ({
362
+ msg: "Traffic filter rejected request in PoW verification",
363
+ data: {
364
+ ip,
365
+ reason: check.reason,
366
+ dnsPeerIp: enrichedDnsEvent?.peerIp,
367
+ dnsResolverIp: enrichedDnsEvent?.resolverIp,
368
+ dnsPeerAsn: require_enrichDnsEvent.getIpInfoAsn(enrichedDnsEvent?.peerIpInfo),
369
+ dnsResolverAsn: require_enrichDnsEvent.getIpInfoAsn(enrichedDnsEvent?.resolverIpInfo),
370
+ dnsPathValid: enrichedDnsEvent?.pathValid
371
+ }
372
+ }));
373
+ failResult = {
374
+ status: _prosopo_types.CaptchaStatus.disapproved,
375
+ reason: check.reason
376
+ };
377
+ failReason = check.reason;
378
+ }
379
+ }
380
+ if (storeMetadata && email) powRecordUpdates.metadata = { email };
381
+ if (!failResult && ip) {
382
+ const challengeIpAddress = require_compositeIpAddress.getIpAddressFromComposite(challengeRecord.ipAddress);
383
+ const ipValidationRules = (await this.db.getClientRecord(dappAccount))?.settings?.ipValidationRules;
384
+ powRecordUpdates.providedIp = require_compositeIpAddress.getCompositeIpAddress(ip);
385
+ if (ipValidationRules?.enabled === true) {
386
+ const ipValidation = await require_util.deepValidateIpAddress(ip, challengeIpAddress, logger, env.ipInfoService, ipValidationRules, enrichedDnsEvent?.peerIp);
387
+ if (!ipValidation.isValid) {
388
+ logger.error(() => ({
389
+ msg: "IP validation failed for PoW captcha",
390
+ data: {
391
+ ip,
392
+ challengeIp: challengeIpAddress.address,
393
+ error: ipValidation.errorMessage,
394
+ distanceKm: ipValidation.distanceKm
395
+ }
396
+ }));
397
+ failResult = {
398
+ status: _prosopo_types.CaptchaStatus.disapproved,
399
+ reason: _prosopo_types.ResultReason.FAILED_IP_VALIDATION
400
+ };
401
+ failReason = "API.FAILED_IP_VALIDATION";
402
+ }
403
+ }
404
+ }
405
+ let score;
406
+ if (sessionRecord) {
407
+ const dnsAsymmetry = require_enrichDnsEvent.computeDnsAsymmetry(enrichedDnsEvent, challengeRecord.ipInfo, trafficFilter);
408
+ if (dnsAsymmetry > 0) sessionRecord.scoreComponents = {
409
+ ...sessionRecord.scoreComponents,
410
+ dnsAsymmetry
411
+ };
412
+ score = require_frictionlessTasksUtils.computeFrictionlessScore(sessionRecord?.scoreComponents);
413
+ logger.info(() => ({ data: {
414
+ scoreComponents: { ...sessionRecord?.scoreComponents || {} },
415
+ score,
416
+ dnsPeerAsn: require_enrichDnsEvent.getIpInfoAsn(enrichedDnsEvent?.peerIpInfo),
417
+ dnsResolverAsn: require_enrichDnsEvent.getIpInfoAsn(enrichedDnsEvent?.resolverIpInfo)
418
+ } }));
419
+ }
420
+ if (!failResult) try {
421
+ const decisionInput = {
422
+ userAccount: challengeRecord.userAccount,
423
+ dappAccount: challengeRecord.dappAccount,
424
+ captchaResult: "passed",
425
+ headers: challengeRecord.headers,
426
+ captchaType: _prosopo_types.CaptchaType.pow,
427
+ behavioralDataPacked: challengeRecord.behavioralDataPacked,
428
+ deviceCapability: challengeRecord.deviceCapability,
429
+ countryCode: challengeRecord.ipInfo?.isValid ? challengeRecord.ipInfo.countryCode : void 0,
430
+ ipInfo: challengeRecord.ipInfo,
431
+ dnsEvent: enrichedDnsEvent,
432
+ score,
433
+ threshold: sessionRecord?.threshold,
434
+ scoreComponents: sessionRecord?.scoreComponents,
435
+ decryptedHeadHash: sessionRecord?.decryptedHeadHash,
436
+ userSitekeyIpHash: sessionRecord?.userSitekeyIpHash,
437
+ simdReadings: sessionRecord?.simdReadings,
438
+ frictionlessReason: sessionRecord?.reason,
439
+ ruleType: sessionRecord?.ruleType,
440
+ webView: sessionRecord?.webView,
441
+ iFrame: sessionRecord?.iFrame,
442
+ coords: challengeRecord.coords
443
+ };
444
+ const decision = await this.decisionMachineRunner.decide(decisionInput, logger);
445
+ if (decision.decision === _prosopo_types.DecisionMachineDecision.Deny) {
446
+ logger.info(() => ({
447
+ msg: "Decision machine denied PoW captcha in server verification",
448
+ data: {
449
+ userAccount: challengeRecord.userAccount,
450
+ reason: decision.reason,
451
+ score: decision.score,
452
+ tags: decision.tags
453
+ }
454
+ }));
455
+ const dmReason = decision.reason || _prosopo_types.ResultReason.CAPTCHA_DECISION_MACHINE_DENIED;
456
+ failResult = {
457
+ status: _prosopo_types.CaptchaStatus.disapproved,
458
+ reason: dmReason
459
+ };
460
+ failReason = dmReason;
461
+ } else logger.debug(() => ({
462
+ msg: "Decision machine allowed PoW captcha",
463
+ data: {
464
+ reason: decision.reason,
465
+ score: decision.score,
466
+ tags: decision.tags
467
+ }
468
+ }));
469
+ } catch (error) {
470
+ logger.error(() => ({
471
+ msg: "Failed to run decision machine in server PoW verification",
472
+ err: error
473
+ }));
474
+ }
475
+ const finalResult = failResult || { status: _prosopo_types.CaptchaStatus.approved };
476
+ if (failResult) powRecordUpdates.result = failResult;
477
+ const writePromises = [];
478
+ if (Object.keys(powRecordUpdates).length > 0) writePromises.push(this.db.updatePowCaptchaRecord(challengeRecord.challenge, powRecordUpdates));
479
+ if (challengeRecord.sessionId) writePromises.push(this.updateSessionRecordWithCache(challengeRecord.sessionId, {
480
+ serverChecked: true,
481
+ result: finalResult
482
+ }, true));
483
+ if (writePromises.length > 0) await Promise.all(writePromises);
484
+ if (failReason) return notVerified(failReason);
485
+ return {
486
+ verified: true,
487
+ ...score ? { score } : {}
488
+ };
489
+ }
490
+ };
491
+ //#endregion
688
492
  exports.PowCaptchaManager = PowCaptchaManager;