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