@prosopo/provider 4.15.13 → 4.15.15

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 (236) 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 +78 -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/powCaptcha/powTasks.unit.test.js +6 -0
  219. package/dist/tests/unit/tasks/powCaptcha/powTasks.unit.test.js.map +1 -1
  220. package/dist/tests/unit/tasks/puzzleCaptcha/puzzleTasks.unit.test.js +60 -1
  221. package/dist/tests/unit/tasks/puzzleCaptcha/puzzleTasks.unit.test.js.map +1 -1
  222. package/dist/tests/unit/tasks/spam/checkTrafficFilter.unit.test.js +140 -0
  223. package/dist/tests/unit/tasks/spam/checkTrafficFilter.unit.test.js.map +1 -1
  224. package/dist/tests/unit/testUtils/mockProviderEnv.d.ts +7 -7
  225. package/dist/util/usageCounters.js +220 -212
  226. package/dist/util.d.ts.map +1 -1
  227. package/dist/util.js +327 -364
  228. package/dist/util.js.map +1 -1
  229. package/dist/utils/devicePlatform.js +15 -9
  230. package/dist/utils/dns.js +64 -77
  231. package/dist/utils/hashUserAgent.js +14 -7
  232. package/dist/utils/hashUserIp.js +14 -6
  233. package/dist/utils/honeypot/encoders.js +74 -80
  234. package/dist/utils/honeypot/phraseBank.js +49 -44
  235. package/dist/utils/normalizeRequestIp.js +34 -26
  236. package/package.json +24 -24
@@ -1,807 +1,556 @@
1
- import { u8aToHex, stringToHex } from "@polkadot/util";
2
- import { ProsopoEnvError } from "@prosopo/common";
3
- import { computePendingRequestHash, compareCaptchaSolutions, parseAndSortCaptchaSolutions } from "@prosopo/datasets";
4
- import { DEFAULT_IMAGE_CAPTCHA_TIMEOUT, SimdReadingsStage, ApiParams, CaptchaStatus, ResultReason, CaptchaType, FrictionlessReason, DecisionMachineDecision } from "@prosopo/types";
5
- import { extractData, at } from "@prosopo/util";
6
- import { randomAsHex, signatureVerify } from "@prosopo/util-crypto";
7
- import { getCompositeIpAddress, getIpAddressFromComposite } from "../../compositeIpAddress.js";
8
- import { peelCheckboxPrefix, constructPairList, containsIdenticalPairs } from "../../pairs.js";
9
- import { checkLangRules } from "../../rules/lang.js";
10
- import { shuffleArray, deepValidateIpAddress } from "../../util.js";
11
1
  import { buildAllWindowIncrements } from "../../util/usageCounters.js";
12
- import { CaptchaManager } from "../captchaManager.js";
13
2
  import { DecisionMachineRunner } from "../decisionMachine/decisionMachineRunner.js";
14
- import { enrichDnsEvent, getIpInfoAsn, computeDnsAsymmetry } from "../dnsEvent/enrichDnsEvent.js";
15
- import "../frictionless/frictionlessTasks.js";
3
+ import { checkLangRules } from "../../rules/lang.js";
4
+ import { getCompositeIpAddress, getIpAddressFromComposite } from "../../compositeIpAddress.js";
5
+ import { computeDnsAsymmetry, enrichDnsEvent, getIpInfoAsn } from "../dnsEvent/enrichDnsEvent.js";
6
+ import { CaptchaManager } from "../captchaManager.js";
7
+ import { FrictionlessReason as FrictionlessReason$1 } from "../frictionless/frictionlessTasks.js";
8
+ import { constructPairList, containsIdenticalPairs, peelCheckboxPrefix } from "../../pairs.js";
9
+ import { deepValidateIpAddress, shuffleArray } from "../../util.js";
16
10
  import { computeFrictionlessScore } from "../frictionless/frictionlessTasksUtils.js";
17
11
  import { evaluateEmailSpamRules } from "../spam/evaluateEmailSpamRules.js";
18
12
  import { buildTreeAndGetCommitmentId } from "./imgCaptchaTasksUtils.js";
19
- class ImgCaptchaManager extends CaptchaManager {
20
- constructor(db, pair, config, logger, writeQueue, usageCounters) {
21
- super(db, pair, config, logger, writeQueue);
22
- this.config = config;
23
- this.decisionMachineRunner = new DecisionMachineRunner(db);
24
- this.usageCounters = usageCounters ?? null;
25
- }
26
- async getCaptchaWithProof(datasetId, solved, size) {
27
- const captchaDocs = await this.db.getRandomCaptcha(solved, datasetId, size);
28
- if (!captchaDocs) {
29
- throw new ProsopoEnvError("DATABASE.CAPTCHA_GET_FAILED", {
30
- context: {
31
- failedFuncName: this.getCaptchaWithProof.name,
32
- datasetId,
33
- solved,
34
- size
35
- }
36
- });
37
- }
38
- return captchaDocs;
39
- }
40
- async getRandomCaptchasAndRequestHash(datasetId, userAccount, ipAddress, captchaConfig, threshold, sessionId, ipInfo) {
41
- const dataset = await this.db.getDatasetDetails(datasetId);
42
- if (!dataset) {
43
- throw new ProsopoEnvError("DATABASE.DATASET_GET_FAILED", {
44
- context: {
45
- failedFuncName: this.getRandomCaptchasAndRequestHash.name,
46
- dataset,
47
- datasetId
48
- }
49
- });
50
- }
51
- const unsolvedCount = Math.abs(
52
- Math.trunc(captchaConfig.unsolved.count)
53
- );
54
- const solvedCount = Math.abs(
55
- Math.trunc(captchaConfig.solved.count)
56
- );
57
- if (!solvedCount) {
58
- throw new ProsopoEnvError("CONFIG.INVALID_CAPTCHA_NUMBER");
59
- }
60
- const solved = await this.getCaptchaWithProof(datasetId, true, solvedCount);
61
- let unsolved = [];
62
- if (unsolvedCount) {
63
- unsolved = await this.getCaptchaWithProof(
64
- datasetId,
65
- false,
66
- unsolvedCount
67
- );
68
- }
69
- const captchas = shuffleArray([...solved, ...unsolved]);
70
- const salt = randomAsHex();
71
- const requestHash = computePendingRequestHash(
72
- captchas.map((c) => c.captchaId),
73
- userAccount,
74
- salt
75
- );
76
- const currentTime = Date.now();
77
- const signedRequestHash = u8aToHex(
78
- this.pair.sign(stringToHex(requestHash))
79
- );
80
- const timeLimit = captchas.map((captcha) => captcha.timeLimitMs || DEFAULT_IMAGE_CAPTCHA_TIMEOUT).reduce((a, b) => a + b, 0);
81
- const deadlineDate = new Date(timeLimit + currentTime);
82
- await this.db.storePendingImageCommitment(
83
- userAccount,
84
- requestHash,
85
- salt,
86
- deadlineDate,
87
- new Date(currentTime),
88
- getCompositeIpAddress(ipAddress),
89
- threshold,
90
- sessionId,
91
- ipInfo
92
- );
93
- return {
94
- captchas,
95
- requestHash,
96
- timestamp: currentTime,
97
- signedRequestHash
98
- };
99
- }
100
- /**
101
- * Validate and store the text captcha solution(s) from the Dapp User in a web2 environment
102
- * @param {string} userAccount
103
- * @param {string} dappAccount
104
- * @param {string} requestHash
105
- * @param {JSON} captchas
106
- * @param userTimestampSignature
107
- * @param timestamp
108
- * @param providerRequestHashSignature
109
- * @param ipAddress
110
- * @param headers
111
- * @param ja4
112
- * @param behavioralData
113
- * @return {Promise<DappUserSolutionResult>} result containing the contract event
114
- */
115
- async dappUserSolution(userAccount, dappAccount, requestHash, captchas, userTimestampSignature, timestamp, providerRequestHashSignature, ipAddress, headers, ja4, behavioralData, ipInfo, simdReadings, clientMetaData) {
116
- const decodedSimdReadings = simdReadings ? await this.decryptSimdReadingsForAttach(simdReadings) : void 0;
117
- const pushSimdAttachIfAny = (sessionId, writes) => {
118
- if (decodedSimdReadings) {
119
- writes.push(
120
- this.recordSessionSimdReadingsIfAbsentWithCache(
121
- sessionId,
122
- decodedSimdReadings,
123
- SimdReadingsStage.submit
124
- )
125
- );
126
- }
127
- };
128
- const verification = signatureVerify(
129
- stringToHex(timestamp.toString()),
130
- userTimestampSignature,
131
- userAccount
132
- );
133
- if (!verification.isValid) {
134
- const err = new ProsopoEnvError("GENERAL.INVALID_SIGNATURE", {
135
- context: { failedFuncName: this.dappUserSolution.name, userAccount }
136
- });
137
- this.logger.info(() => ({
138
- err,
139
- msg: "Invalid user timestamp signature"
140
- }));
141
- throw err;
142
- }
143
- const providerRequestHashSignatureVerify = signatureVerify(
144
- stringToHex(requestHash.toString()),
145
- providerRequestHashSignature,
146
- this.pair.address
147
- );
148
- if (!providerRequestHashSignatureVerify.isValid) {
149
- const err = new ProsopoEnvError("GENERAL.INVALID_SIGNATURE", {
150
- context: {
151
- failedFuncName: this.dappUserSolution.name,
152
- userAccount,
153
- error: "requestHash signature is invalid"
154
- }
155
- });
156
- this.logger.info(() => ({
157
- err,
158
- msg: "Invalid provider requestHash signature"
159
- }));
160
- throw err;
161
- }
162
- let response = {
163
- captchas: [],
164
- verified: false
165
- };
166
- const pendingRecord = await this.db.getPendingImageCommitment(requestHash);
167
- const unverifiedCaptchaIds = captchas.map((captcha) => captcha.captchaId);
168
- const pendingRequest = await this.validateDappUserSolutionRequestIsPending(
169
- requestHash,
170
- pendingRecord,
171
- userAccount,
172
- unverifiedCaptchaIds
173
- );
174
- if (pendingRequest) {
175
- const { storedCaptchas, receivedCaptchas, captchaIds } = await this.validateReceivedCaptchasAgainstStoredCaptchas(captchas);
176
- const rawFlat = receivedCaptchas.map((c) => extractData(c.salt));
177
- const { checkbox: checkboxCoordPair, flat } = peelCheckboxPrefix(
178
- rawFlat,
179
- receivedCaptchas.map((c) => c.solution.length)
180
- );
181
- const shapePairs = flat.map((list) => constructPairList(list));
182
- const pairs = checkboxCoordPair ? [[checkboxCoordPair], ...shapePairs] : shapePairs;
183
- const { tree, commitmentId } = buildTreeAndGetCommitmentId(receivedCaptchas);
184
- const datasetId = at(storedCaptchas, 0).datasetId;
185
- if (!datasetId) {
186
- throw new ProsopoEnvError("CAPTCHA.ID_MISMATCH", {
187
- context: { failedFuncName: this.dappUserSolution.name }
188
- });
189
- }
190
- await this.db.updatePendingImageCommitmentStatus(requestHash);
191
- let behavioralDataPacked;
192
- let deviceCapability;
193
- if (behavioralData) {
194
- try {
195
- const decryptKeys = [
196
- // Process DB keys first, then env var key last as env key will likely be invalid
197
- ...await this.getDetectorKeys(),
198
- process.env.BOT_DECRYPTION_KEY
199
- ];
200
- const decryptedData = await this.decryptBehavioralData(
201
- behavioralData,
202
- decryptKeys
203
- );
204
- if (decryptedData) {
205
- this.logger?.info(() => ({
206
- msg: "Behavioral analysis completed",
207
- data: {
208
- userAccount,
209
- dappAccount,
210
- requestHash,
211
- mouseEventsCount: decryptedData.collector1?.length || 0,
212
- touchEventsCount: decryptedData.collector2?.length || 0,
213
- clickEventsCount: decryptedData.collector3?.length || 0,
214
- deviceCapability: decryptedData.deviceCapability
215
- }
216
- }));
217
- behavioralDataPacked = {
218
- c1: decryptedData.collector1 || [],
219
- c2: decryptedData.collector2 || [],
220
- c3: decryptedData.collector3 || [],
221
- d: decryptedData.deviceCapability
222
- };
223
- deviceCapability = decryptedData.deviceCapability;
224
- }
225
- } catch (error) {
226
- this.logger?.error(() => ({
227
- msg: "Failed to process behavioral data",
228
- err: error
229
- }));
230
- }
231
- }
232
- const commit = {
233
- id: commitmentId,
234
- userAccount,
235
- dappAccount,
236
- providerAccount: this.pair.address,
237
- datasetId,
238
- result: { status: CaptchaStatus.pending },
239
- userSignature: userTimestampSignature,
240
- userSubmitted: true,
241
- serverChecked: false,
242
- requestedAtTimestamp: new Date(timestamp),
243
- submittedAtTimestamp: /* @__PURE__ */ new Date(),
244
- ipAddress: getCompositeIpAddress(ipAddress),
245
- headers,
246
- sessionId: pendingRecord.sessionId,
247
- ja4,
248
- ipInfo,
249
- pending: false,
250
- salt: pendingRecord.salt,
251
- requestHash: pendingRecord[ApiParams.requestHash],
252
- threshold: pendingRecord.threshold,
253
- deadlineTimestamp: pendingRecord.deadlineTimestamp,
254
- ...behavioralDataPacked && { behavioralDataPacked },
255
- ...deviceCapability && { deviceCapability },
256
- ...clientMetaData?.hp && {
257
- clientMetaData: { hp: clientMetaData.hp }
258
- }
259
- };
260
- await this.db.storeUserImageCaptchaSolution(receivedCaptchas, commit);
261
- const solutionRecords = await Promise.all(
262
- storedCaptchas.map(async (captcha) => {
263
- const solutionRecord = await this.db.getSolutionByCaptchaId(
264
- captcha.captchaId
265
- );
266
- if (!solutionRecord) {
267
- throw new ProsopoEnvError("CAPTCHA.SOLUTION_NOT_FOUND", {
268
- context: { failedFuncName: this.dappUserSolution.name }
269
- });
270
- }
271
- return solutionRecord;
272
- })
273
- );
274
- const totalImages = storedCaptchas[0]?.items.length || 0;
275
- if (containsIdenticalPairs(pairs) && process.env.NODE_ENV !== "test") {
276
- const writePromises = [
277
- this.db.disapproveDappUserCommitment(
278
- commitmentId,
279
- "CAPTCHA.INVALID_SOLUTION",
280
- pairs
281
- )
282
- ];
283
- if (pendingRecord.sessionId) {
284
- writePromises.push(
285
- this.updateSessionRecordWithCache(pendingRecord.sessionId, {
286
- userSubmitted: true,
287
- result: {
288
- status: CaptchaStatus.disapproved,
289
- reason: ResultReason.CAPTCHA_INVALID_SOLUTION
290
- }
291
- })
292
- );
293
- pushSimdAttachIfAny(pendingRecord.sessionId, writePromises);
294
- }
295
- await Promise.all(writePromises);
296
- response = {
297
- captchas: captchaIds.map((id) => ({
298
- captchaId: id,
299
- proof: [[]]
300
- })),
301
- verified: false
302
- };
303
- return response;
304
- }
305
- if (compareCaptchaSolutions(
306
- receivedCaptchas,
307
- solutionRecords,
308
- totalImages,
309
- pendingRecord.threshold
310
- )) {
311
- response = {
312
- captchas: captchaIds.map((id) => ({
313
- captchaId: id,
314
- proof: tree.proof(id)
315
- })),
316
- verified: true
317
- };
318
- const writePromises = [
319
- this.db.approveDappUserCommitment(commitmentId, pairs)
320
- ];
321
- if (pendingRecord.sessionId) {
322
- writePromises.push(
323
- this.updateSessionRecordWithCache(pendingRecord.sessionId, {
324
- userSubmitted: true,
325
- result: { status: CaptchaStatus.approved }
326
- })
327
- );
328
- pushSimdAttachIfAny(pendingRecord.sessionId, writePromises);
329
- }
330
- await Promise.all(writePromises);
331
- } else {
332
- const writePromises = [
333
- this.db.disapproveDappUserCommitment(
334
- commitmentId,
335
- "CAPTCHA.INVALID_SOLUTION",
336
- pairs
337
- )
338
- ];
339
- if (pendingRecord.sessionId) {
340
- writePromises.push(
341
- this.updateSessionRecordWithCache(pendingRecord.sessionId, {
342
- userSubmitted: true,
343
- result: {
344
- status: CaptchaStatus.disapproved,
345
- reason: ResultReason.CAPTCHA_INVALID_SOLUTION
346
- }
347
- })
348
- );
349
- pushSimdAttachIfAny(pendingRecord.sessionId, writePromises);
350
- }
351
- await Promise.all(writePromises);
352
- response = {
353
- captchas: captchaIds.map((id) => ({
354
- captchaId: id,
355
- proof: [[]]
356
- })),
357
- verified: false
358
- };
359
- }
360
- } else {
361
- this.logger.info(() => ({
362
- msg: "Request hash not found"
363
- }));
364
- }
365
- return response;
366
- }
367
- /**
368
- * Validate length of received captchas array matches length of captchas found in database
369
- * Validate that the datasetId is the same for all captchas and is equal to the datasetId on the stored captchas
370
- */
371
- async validateReceivedCaptchasAgainstStoredCaptchas(captchas) {
372
- const receivedCaptchas = parseAndSortCaptchaSolutions(captchas);
373
- const captchaIds = receivedCaptchas.map((captcha) => captcha.captchaId);
374
- const storedCaptchas = await this.db.getCaptchaById(captchaIds);
375
- if (!storedCaptchas || receivedCaptchas.length !== storedCaptchas.length) {
376
- throw new ProsopoEnvError("CAPTCHA.INVALID_CAPTCHA_ID", {
377
- context: {
378
- failedFuncName: this.validateReceivedCaptchasAgainstStoredCaptchas.name,
379
- captchas
380
- }
381
- });
382
- }
383
- if (!storedCaptchas.every(
384
- (captcha) => captcha.datasetId === at(storedCaptchas, 0).datasetId
385
- )) {
386
- throw new ProsopoEnvError("CAPTCHA.DIFFERENT_DATASET_IDS", {
387
- context: {
388
- failedFuncName: this.validateReceivedCaptchasAgainstStoredCaptchas.name,
389
- captchas
390
- }
391
- });
392
- }
393
- return { storedCaptchas, receivedCaptchas, captchaIds };
394
- }
395
- /**
396
- * Validate that a Dapp User is responding to their own pending captcha request
397
- * @param {string} requestHash
398
- * @param {PendingImageCaptchaRequest} pendingRecord
399
- * @param {string} userAccount
400
- * @param {string[]} captchaIds
401
- */
402
- async validateDappUserSolutionRequestIsPending(requestHash, pendingRecord, userAccount, captchaIds) {
403
- const currentTime = /* @__PURE__ */ new Date();
404
- if (!pendingRecord) {
405
- this.logger.info(() => ({
406
- msg: "No pending record found"
407
- }));
408
- return false;
409
- }
410
- if (pendingRecord.deadlineTimestamp < currentTime) {
411
- this.logger.info(() => ({
412
- msg: "Deadline for responding to captcha has expired"
413
- }));
414
- return false;
415
- }
416
- if (pendingRecord) {
417
- const pendingHashComputed = computePendingRequestHash(
418
- captchaIds,
419
- userAccount,
420
- pendingRecord.salt
421
- );
422
- return requestHash === pendingHashComputed;
423
- }
424
- return false;
425
- }
426
- /*
427
- * Get dapp user solution from database
428
- */
429
- async getDappUserCommitmentById(commitmentId) {
430
- const dappUserSolution = await this.db.getDappUserCommitmentById(commitmentId);
431
- if (!dappUserSolution) {
432
- throw new ProsopoEnvError("CAPTCHA.DAPP_USER_SOLUTION_NOT_FOUND", {
433
- context: {
434
- failedFuncName: this.getDappUserCommitmentById.name,
435
- commitmentId
436
- }
437
- });
438
- }
439
- return dappUserSolution;
440
- }
441
- /* Check if dapp user has verified solution in cache */
442
- async getDappUserCommitmentByAccount(userAccount, dappAccount) {
443
- const dappUserSolutions = await this.db.getDappUserCommitmentByAccount(
444
- userAccount,
445
- dappAccount
446
- );
447
- if (dappUserSolutions.length > 0) {
448
- for (const dappUserSolution of dappUserSolutions) {
449
- if (dappUserSolution.result.status === CaptchaStatus.approved) {
450
- return dappUserSolution;
451
- }
452
- }
453
- }
454
- return void 0;
455
- }
456
- async verifyImageCaptchaSolution(user, dapp, commitmentId, env, maxVerifiedTime, ip, disallowWebView, contextAwareEnabled = false, userAccessRulesStorage, email, spamEmailDomainCheckingEnabled = false, spamFilter, trafficFilter, storeMetadata = false) {
457
- const logger = this.logger.with({ commitmentId, dapp });
458
- const solution = await (commitmentId ? this.getDappUserCommitmentById(commitmentId) : this.getDappUserCommitmentByAccount(user, dapp));
459
- if (!solution) {
460
- logger.debug(() => ({
461
- msg: "Not verified - no solution found"
462
- }));
463
- return {
464
- status: "API.USER_NOT_VERIFIED_NO_SOLUTION",
465
- verified: false
466
- };
467
- }
468
- if (solution.serverChecked) {
469
- return {
470
- status: "API.USER_ALREADY_VERIFIED",
471
- verified: false
472
- };
473
- }
474
- await this.db.markDappUserCommitmentsChecked([solution.id]);
475
- if (solution.result.status === CaptchaStatus.disapproved) {
476
- return {
477
- status: solution.result.reason || "API.USER_NOT_VERIFIED",
478
- verified: false
479
- };
480
- }
481
- maxVerifiedTime = maxVerifiedTime || 60 * 1e3;
482
- const currentTime = Date.now();
483
- const submittedAt = solution.submittedAtTimestamp;
484
- const timeSinceCompletion = submittedAt instanceof Date ? currentTime - submittedAt.getTime() : Number.POSITIVE_INFINITY;
485
- if (timeSinceCompletion > maxVerifiedTime) {
486
- logger.debug(() => ({
487
- msg: "Not verified - timed out"
488
- }));
489
- return {
490
- status: "API.USER_NOT_VERIFIED_TIME_EXPIRED",
491
- verified: false
492
- };
493
- }
494
- const commitmentUpdates = {};
495
- let failStatus;
496
- if (userAccessRulesStorage) {
497
- try {
498
- const blockPolicy = await this.checkForHardBlock(
499
- userAccessRulesStorage,
500
- solution,
501
- solution.userAccount,
502
- solution.headers,
503
- solution.coords,
504
- solution.ipInfo?.isValid ? solution.ipInfo.countryCode : void 0,
505
- solution.ipInfo?.isValid ? solution.ipInfo.asnNumber : void 0
506
- );
507
- if (blockPolicy) {
508
- logger.info(() => ({
509
- msg: "User blocked by access policy in server image verification",
510
- data: {
511
- userAccount: solution.userAccount,
512
- dappAccount: solution.dappAccount,
513
- policy: blockPolicy
514
- }
515
- }));
516
- commitmentUpdates.result = {
517
- status: CaptchaStatus.disapproved,
518
- reason: ResultReason.ACCESS_POLICY_BLOCK
519
- };
520
- failStatus = ResultReason.ACCESS_POLICY_BLOCK;
521
- }
522
- } catch (error) {
523
- logger.warn(() => ({
524
- msg: "Failed to check user access policies in server image verification",
525
- error
526
- }));
527
- }
528
- }
529
- if (!failStatus && email && spamEmailDomainCheckingEnabled) {
530
- try {
531
- const isSpam = await this.checkSpamEmail(email);
532
- if (isSpam) {
533
- const emailDomain = email.split("@")[1] || "unknown";
534
- logger.info(() => ({
535
- msg: "Spam email domain detected in server image verification",
536
- data: { emailDomain }
537
- }));
538
- commitmentUpdates.result = {
539
- status: CaptchaStatus.disapproved,
540
- reason: ResultReason.SPAM_EMAIL_DOMAIN
541
- };
542
- failStatus = ResultReason.SPAM_EMAIL_DOMAIN;
543
- }
544
- } catch (error) {
545
- logger.warn(() => ({
546
- msg: "Failed to check spam email domain in server image verification",
547
- error
548
- }));
549
- }
550
- }
551
- if (!failStatus && spamFilter?.enabled && spamFilter.emailRules?.enabled && email) {
552
- const result = evaluateEmailSpamRules(email, spamFilter.emailRules);
553
- if (result.isSpam) {
554
- logger.info(() => ({
555
- msg: "Spam filter rejected email in image verification",
556
- data: { reason: result.reason }
557
- }));
558
- commitmentUpdates.result = {
559
- status: CaptchaStatus.disapproved,
560
- reason: ResultReason.SPAM_EMAIL_RULE
561
- };
562
- failStatus = ResultReason.SPAM_EMAIL_RULE;
563
- }
564
- }
565
- const sessionRecord = solution.sessionId ? await this.db.getSessionRecordBySessionId(solution.sessionId) : void 0;
566
- const enrichedDnsEvent = await enrichDnsEvent(
567
- sessionRecord?.dnsEvent,
568
- env.ipInfoService,
569
- ip ?? solution.ipInfo?.ip
570
- );
571
- if (!failStatus) {
572
- const check = await this.resolveTrafficFilterCheck(
573
- env,
574
- solution.ipInfo,
575
- trafficFilter,
576
- ip,
577
- enrichedDnsEvent
578
- );
579
- if (check.isBlocked) {
580
- logger.info(() => ({
581
- msg: "Traffic filter rejected request",
582
- data: {
583
- ip,
584
- reason: check.reason,
585
- dnsPeerIp: enrichedDnsEvent?.peerIp,
586
- dnsResolverIp: enrichedDnsEvent?.resolverIp,
587
- dnsPeerAsn: getIpInfoAsn(enrichedDnsEvent?.peerIpInfo),
588
- dnsResolverAsn: getIpInfoAsn(enrichedDnsEvent?.resolverIpInfo),
589
- dnsPathValid: enrichedDnsEvent?.pathValid
590
- }
591
- }));
592
- commitmentUpdates.result = {
593
- status: CaptchaStatus.disapproved,
594
- reason: check.reason
595
- };
596
- failStatus = check.reason;
597
- }
598
- }
599
- if (storeMetadata && email) {
600
- commitmentUpdates.metadata = { email };
601
- }
602
- if (ip) {
603
- const solutionIpAddress = getIpAddressFromComposite(solution.ipAddress);
604
- const clientRecord = await this.db.getClientRecord(dapp);
605
- const ipValidationRules = clientRecord?.settings?.ipValidationRules;
606
- commitmentUpdates.providedIp = getCompositeIpAddress(ip);
607
- if (!failStatus && ipValidationRules?.enabled === true) {
608
- const ipValidation = await deepValidateIpAddress(
609
- ip,
610
- solutionIpAddress,
611
- logger,
612
- env.ipInfoService,
613
- ipValidationRules,
614
- enrichedDnsEvent?.peerIp
615
- );
616
- if (!ipValidation.isValid) {
617
- logger.error(() => ({
618
- msg: "IP validation failed for image captcha",
619
- data: {
620
- ip,
621
- solutionIp: solutionIpAddress.address,
622
- error: ipValidation.errorMessage,
623
- distanceKm: ipValidation.distanceKm
624
- }
625
- }));
626
- failStatus = ResultReason.FAILED_IP_VALIDATION;
627
- }
628
- }
629
- }
630
- let isApproved = !failStatus && solution.result.status === CaptchaStatus.approved;
631
- let failureStatus = failStatus || ResultReason.USER_NOT_VERIFIED;
632
- if (!failStatus && solution.result.status === CaptchaStatus.approved && this.usageCounters) {
633
- const ipValue = ip ?? "";
634
- this.usageCounters.incrManyAsync(
635
- solution.dappAccount,
636
- buildAllWindowIncrements(
637
- "solved",
638
- CaptchaType.image,
639
- ipValue,
640
- solution.userAccount,
641
- enrichedDnsEvent?.peerIp
642
- )
643
- );
644
- }
645
- let score;
646
- if (sessionRecord) {
647
- const dnsAsymmetry = computeDnsAsymmetry(
648
- enrichedDnsEvent,
649
- solution.ipInfo,
650
- trafficFilter
651
- );
652
- if (dnsAsymmetry > 0) {
653
- sessionRecord.scoreComponents = {
654
- ...sessionRecord.scoreComponents,
655
- dnsAsymmetry
656
- };
657
- }
658
- score = computeFrictionlessScore(sessionRecord?.scoreComponents);
659
- logger.info(() => ({
660
- data: {
661
- scoreComponents: sessionRecord?.scoreComponents,
662
- score,
663
- dnsPeerAsn: getIpInfoAsn(enrichedDnsEvent?.peerIpInfo),
664
- dnsResolverAsn: getIpInfoAsn(enrichedDnsEvent?.resolverIpInfo)
665
- }
666
- }));
667
- if (!failStatus && disallowWebView === true && (sessionRecord.webView === true || (sessionRecord.scoreComponents.webView || 0) > 0)) {
668
- logger.info(() => ({
669
- msg: "Disallowing webview access - user not verified"
670
- }));
671
- commitmentUpdates.result = {
672
- status: CaptchaStatus.disapproved,
673
- reason: ResultReason.DISALLOWED_WEBVIEW
674
- };
675
- failStatus = ResultReason.DISALLOWED_WEBVIEW;
676
- isApproved = false;
677
- failureStatus = ResultReason.DISALLOWED_WEBVIEW;
678
- }
679
- if (contextAwareEnabled && sessionRecord.reason === FrictionlessReason.CONTEXT_AWARE_VALIDATION_FAILED) {
680
- logger.info(() => ({
681
- msg: "Context aware validation failed"
682
- }));
683
- }
684
- }
685
- if (isApproved) {
686
- const decisionInput = {
687
- userAccount: solution.userAccount,
688
- dappAccount: solution.dappAccount,
689
- captchaResult: "passed",
690
- headers: solution.headers,
691
- captchaType: CaptchaType.image,
692
- behavioralDataPacked: solution.behavioralDataPacked,
693
- deviceCapability: solution.deviceCapability,
694
- countryCode: solution.ipInfo?.isValid ? solution.ipInfo.countryCode : void 0,
695
- ipInfo: solution.ipInfo,
696
- dnsEvent: enrichedDnsEvent,
697
- score,
698
- threshold: sessionRecord?.threshold,
699
- scoreComponents: sessionRecord?.scoreComponents,
700
- decryptedHeadHash: sessionRecord?.decryptedHeadHash,
701
- userSitekeyIpHash: sessionRecord?.userSitekeyIpHash,
702
- simdReadings: sessionRecord?.simdReadings,
703
- frictionlessReason: sessionRecord?.reason,
704
- ruleType: sessionRecord?.ruleType,
705
- webView: sessionRecord?.webView,
706
- iFrame: sessionRecord?.iFrame,
707
- coords: solution.coords
708
- };
709
- try {
710
- const decision = await this.decisionMachineRunner.decide(
711
- decisionInput,
712
- logger
713
- );
714
- if (decision.decision === DecisionMachineDecision.Deny) {
715
- const dmReason = decision.reason || ResultReason.CAPTCHA_DECISION_MACHINE_DENIED;
716
- logger.info(() => ({
717
- msg: "Decision machine denied user verification",
718
- data: {
719
- reason: decision.reason
720
- }
721
- }));
722
- commitmentUpdates.result = {
723
- status: CaptchaStatus.disapproved,
724
- reason: dmReason
725
- };
726
- isApproved = false;
727
- failureStatus = dmReason;
728
- }
729
- } catch (error) {
730
- logger.error(() => ({
731
- msg: "Failed to process decision machine",
732
- err: error
733
- }));
734
- }
735
- }
736
- const writePromises = [];
737
- const sideUpdates = {};
738
- if (commitmentUpdates.providedIp) {
739
- sideUpdates.providedIp = commitmentUpdates.providedIp;
740
- }
741
- if (commitmentUpdates.metadata) {
742
- sideUpdates.metadata = commitmentUpdates.metadata;
743
- }
744
- if (Object.keys(sideUpdates).length > 0) {
745
- writePromises.push(
746
- this.db.updateDappUserCommitment(solution.id, sideUpdates)
747
- );
748
- }
749
- if (commitmentId) {
750
- if (isApproved) {
751
- writePromises.push(this.db.approveDappUserCommitment(commitmentId));
752
- } else if (commitmentUpdates.result) {
753
- writePromises.push(
754
- this.db.disapproveDappUserCommitment(
755
- commitmentId,
756
- commitmentUpdates.result.reason
757
- )
758
- );
759
- }
760
- }
761
- const finalResult = isApproved ? { status: CaptchaStatus.approved } : commitmentUpdates.result || {
762
- status: CaptchaStatus.disapproved,
763
- reason: failureStatus
764
- };
765
- if (solution.sessionId) {
766
- writePromises.push(
767
- this.updateSessionRecordWithCache(
768
- solution.sessionId,
769
- {
770
- serverChecked: true,
771
- result: finalResult
772
- },
773
- true
774
- )
775
- );
776
- }
777
- if (writePromises.length > 0) {
778
- await Promise.all(writePromises);
779
- }
780
- return {
781
- status: isApproved ? ResultReason.USER_VERIFIED : failureStatus,
782
- verified: isApproved,
783
- commitmentId: solution.id.toString(),
784
- ...score && { score }
785
- };
786
- }
787
- checkLangRules(acceptLanguage) {
788
- return checkLangRules(this.config, acceptLanguage);
789
- }
790
- getVerificationResponse(verified, clientRecord, translateFn, score, commitmentId, reason) {
791
- return {
792
- ...super.getVerificationResponse(
793
- verified,
794
- clientRecord,
795
- translateFn,
796
- score,
797
- reason
798
- ),
799
- ...commitmentId && {
800
- [ApiParams.commitmentId]: commitmentId
801
- }
802
- };
803
- }
804
- }
805
- export {
806
- ImgCaptchaManager
13
+ import { ProsopoEnvError } from "@prosopo/common";
14
+ import { ApiParams, CaptchaStatus, CaptchaType, DEFAULT_IMAGE_CAPTCHA_TIMEOUT, DecisionMachineDecision, ResultReason, SimdReadingsStage } from "@prosopo/types";
15
+ import { at, extractData } from "@prosopo/util";
16
+ import { randomAsHex, signatureVerify } from "@prosopo/util-crypto";
17
+ import { compareCaptchaSolutions, computePendingRequestHash, parseAndSortCaptchaSolutions } from "@prosopo/datasets";
18
+ import { stringToHex, u8aToHex } from "@polkadot/util";
19
+ //#region src/tasks/imgCaptcha/imgCaptchaTasks.ts
20
+ var ImgCaptchaManager = class extends CaptchaManager {
21
+ constructor(db, pair, config, logger, writeQueue, usageCounters) {
22
+ super(db, pair, config, logger, writeQueue);
23
+ this.config = config;
24
+ this.decisionMachineRunner = new DecisionMachineRunner(db);
25
+ this.usageCounters = usageCounters ?? null;
26
+ }
27
+ async getCaptchaWithProof(datasetId, solved, size) {
28
+ const captchaDocs = await this.db.getRandomCaptcha(solved, datasetId, size);
29
+ if (!captchaDocs) throw new ProsopoEnvError("DATABASE.CAPTCHA_GET_FAILED", { context: {
30
+ failedFuncName: this.getCaptchaWithProof.name,
31
+ datasetId,
32
+ solved,
33
+ size
34
+ } });
35
+ return captchaDocs;
36
+ }
37
+ async getRandomCaptchasAndRequestHash(datasetId, userAccount, ipAddress, captchaConfig, threshold, sessionId, ipInfo) {
38
+ const dataset = await this.db.getDatasetDetails(datasetId);
39
+ if (!dataset) throw new ProsopoEnvError("DATABASE.DATASET_GET_FAILED", { context: {
40
+ failedFuncName: this.getRandomCaptchasAndRequestHash.name,
41
+ dataset,
42
+ datasetId
43
+ } });
44
+ const unsolvedCount = Math.abs(Math.trunc(captchaConfig.unsolved.count));
45
+ const solvedCount = Math.abs(Math.trunc(captchaConfig.solved.count));
46
+ if (!solvedCount) throw new ProsopoEnvError("CONFIG.INVALID_CAPTCHA_NUMBER");
47
+ const solved = await this.getCaptchaWithProof(datasetId, true, solvedCount);
48
+ let unsolved = [];
49
+ if (unsolvedCount) unsolved = await this.getCaptchaWithProof(datasetId, false, unsolvedCount);
50
+ const captchas = shuffleArray([...solved, ...unsolved]);
51
+ const salt = randomAsHex();
52
+ const requestHash = computePendingRequestHash(captchas.map((c) => c.captchaId), userAccount, salt);
53
+ const currentTime = Date.now();
54
+ const signedRequestHash = u8aToHex(this.pair.sign(stringToHex(requestHash)));
55
+ const timeLimit = captchas.map((captcha) => captcha.timeLimitMs || DEFAULT_IMAGE_CAPTCHA_TIMEOUT).reduce((a, b) => a + b, 0);
56
+ const deadlineDate = new Date(timeLimit + currentTime);
57
+ await this.db.storePendingImageCommitment(userAccount, requestHash, salt, deadlineDate, new Date(currentTime), getCompositeIpAddress(ipAddress), threshold, sessionId, ipInfo);
58
+ return {
59
+ captchas,
60
+ requestHash,
61
+ timestamp: currentTime,
62
+ signedRequestHash
63
+ };
64
+ }
65
+ /**
66
+ * Validate and store the text captcha solution(s) from the Dapp User in a web2 environment
67
+ * @param {string} userAccount
68
+ * @param {string} dappAccount
69
+ * @param {string} requestHash
70
+ * @param {JSON} captchas
71
+ * @param userTimestampSignature
72
+ * @param timestamp
73
+ * @param providerRequestHashSignature
74
+ * @param ipAddress
75
+ * @param headers
76
+ * @param ja4
77
+ * @param behavioralData
78
+ * @return {Promise<DappUserSolutionResult>} result containing the contract event
79
+ */
80
+ async dappUserSolution(userAccount, dappAccount, requestHash, captchas, userTimestampSignature, timestamp, providerRequestHashSignature, ipAddress, headers, ja4, behavioralData, ipInfo, simdReadings, clientMetaData) {
81
+ const decodedSimdReadings = simdReadings ? await this.decryptSimdReadingsForAttach(simdReadings) : void 0;
82
+ const pushSimdAttachIfAny = (sessionId, writes) => {
83
+ if (decodedSimdReadings) writes.push(this.recordSessionSimdReadingsIfAbsentWithCache(sessionId, decodedSimdReadings, SimdReadingsStage.submit));
84
+ };
85
+ if (!signatureVerify(stringToHex(timestamp.toString()), userTimestampSignature, userAccount).isValid) {
86
+ const err = new ProsopoEnvError("GENERAL.INVALID_SIGNATURE", { context: {
87
+ failedFuncName: this.dappUserSolution.name,
88
+ userAccount
89
+ } });
90
+ this.logger.info(() => ({
91
+ err,
92
+ msg: "Invalid user timestamp signature"
93
+ }));
94
+ throw err;
95
+ }
96
+ if (!signatureVerify(stringToHex(requestHash.toString()), providerRequestHashSignature, this.pair.address).isValid) {
97
+ const err = new ProsopoEnvError("GENERAL.INVALID_SIGNATURE", { context: {
98
+ failedFuncName: this.dappUserSolution.name,
99
+ userAccount,
100
+ error: "requestHash signature is invalid"
101
+ } });
102
+ this.logger.info(() => ({
103
+ err,
104
+ msg: "Invalid provider requestHash signature"
105
+ }));
106
+ throw err;
107
+ }
108
+ let response = {
109
+ captchas: [],
110
+ verified: false
111
+ };
112
+ const pendingRecord = await this.db.getPendingImageCommitment(requestHash);
113
+ const unverifiedCaptchaIds = captchas.map((captcha) => captcha.captchaId);
114
+ if (await this.validateDappUserSolutionRequestIsPending(requestHash, pendingRecord, userAccount, unverifiedCaptchaIds)) {
115
+ const { storedCaptchas, receivedCaptchas, captchaIds } = await this.validateReceivedCaptchasAgainstStoredCaptchas(captchas);
116
+ const { checkbox: checkboxCoordPair, flat } = peelCheckboxPrefix(receivedCaptchas.map((c) => extractData(c.salt)), receivedCaptchas.map((c) => c.solution.length));
117
+ const shapePairs = flat.map((list) => constructPairList(list));
118
+ const pairs = checkboxCoordPair ? [[checkboxCoordPair], ...shapePairs] : shapePairs;
119
+ const { tree, commitmentId } = buildTreeAndGetCommitmentId(receivedCaptchas);
120
+ const datasetId = at(storedCaptchas, 0).datasetId;
121
+ if (!datasetId) throw new ProsopoEnvError("CAPTCHA.ID_MISMATCH", { context: { failedFuncName: this.dappUserSolution.name } });
122
+ await this.db.updatePendingImageCommitmentStatus(requestHash);
123
+ let behavioralDataPacked;
124
+ let deviceCapability;
125
+ if (behavioralData) try {
126
+ const decryptKeys = [...await this.getDetectorKeys(), process.env.BOT_DECRYPTION_KEY];
127
+ const decryptedData = await this.decryptBehavioralData(behavioralData, decryptKeys);
128
+ if (decryptedData) {
129
+ this.logger?.info(() => ({
130
+ msg: "Behavioral analysis completed",
131
+ data: {
132
+ userAccount,
133
+ dappAccount,
134
+ requestHash,
135
+ mouseEventsCount: decryptedData.collector1?.length || 0,
136
+ touchEventsCount: decryptedData.collector2?.length || 0,
137
+ clickEventsCount: decryptedData.collector3?.length || 0,
138
+ deviceCapability: decryptedData.deviceCapability
139
+ }
140
+ }));
141
+ behavioralDataPacked = {
142
+ c1: decryptedData.collector1 || [],
143
+ c2: decryptedData.collector2 || [],
144
+ c3: decryptedData.collector3 || [],
145
+ d: decryptedData.deviceCapability
146
+ };
147
+ deviceCapability = decryptedData.deviceCapability;
148
+ }
149
+ } catch (error) {
150
+ this.logger?.error(() => ({
151
+ msg: "Failed to process behavioral data",
152
+ err: error
153
+ }));
154
+ }
155
+ const commit = {
156
+ id: commitmentId,
157
+ userAccount,
158
+ dappAccount,
159
+ providerAccount: this.pair.address,
160
+ datasetId,
161
+ result: { status: CaptchaStatus.pending },
162
+ userSignature: userTimestampSignature,
163
+ userSubmitted: true,
164
+ serverChecked: false,
165
+ requestedAtTimestamp: new Date(timestamp),
166
+ submittedAtTimestamp: /* @__PURE__ */ new Date(),
167
+ ipAddress: getCompositeIpAddress(ipAddress),
168
+ headers,
169
+ sessionId: pendingRecord.sessionId,
170
+ ja4,
171
+ ipInfo,
172
+ pending: false,
173
+ salt: pendingRecord.salt,
174
+ requestHash: pendingRecord[ApiParams.requestHash],
175
+ threshold: pendingRecord.threshold,
176
+ deadlineTimestamp: pendingRecord.deadlineTimestamp,
177
+ ...behavioralDataPacked && { behavioralDataPacked },
178
+ ...deviceCapability && { deviceCapability },
179
+ ...clientMetaData?.hp && { clientMetaData: { hp: clientMetaData.hp } }
180
+ };
181
+ await this.db.storeUserImageCaptchaSolution(receivedCaptchas, commit);
182
+ const solutionRecords = await Promise.all(storedCaptchas.map(async (captcha) => {
183
+ const solutionRecord = await this.db.getSolutionByCaptchaId(captcha.captchaId);
184
+ if (!solutionRecord) throw new ProsopoEnvError("CAPTCHA.SOLUTION_NOT_FOUND", { context: { failedFuncName: this.dappUserSolution.name } });
185
+ return solutionRecord;
186
+ }));
187
+ const totalImages = storedCaptchas[0]?.items.length || 0;
188
+ if (containsIdenticalPairs(pairs) && process.env.NODE_ENV !== "test") {
189
+ const writePromises = [this.db.disapproveDappUserCommitment(commitmentId, "CAPTCHA.INVALID_SOLUTION", pairs)];
190
+ if (pendingRecord.sessionId) {
191
+ writePromises.push(this.updateSessionRecordWithCache(pendingRecord.sessionId, {
192
+ userSubmitted: true,
193
+ result: {
194
+ status: CaptchaStatus.disapproved,
195
+ reason: ResultReason.CAPTCHA_INVALID_SOLUTION
196
+ }
197
+ }));
198
+ pushSimdAttachIfAny(pendingRecord.sessionId, writePromises);
199
+ }
200
+ await Promise.all(writePromises);
201
+ response = {
202
+ captchas: captchaIds.map((id) => ({
203
+ captchaId: id,
204
+ proof: [[]]
205
+ })),
206
+ verified: false
207
+ };
208
+ return response;
209
+ }
210
+ if (compareCaptchaSolutions(receivedCaptchas, solutionRecords, totalImages, pendingRecord.threshold)) {
211
+ response = {
212
+ captchas: captchaIds.map((id) => ({
213
+ captchaId: id,
214
+ proof: tree.proof(id)
215
+ })),
216
+ verified: true
217
+ };
218
+ const writePromises = [this.db.approveDappUserCommitment(commitmentId, pairs)];
219
+ if (pendingRecord.sessionId) {
220
+ writePromises.push(this.updateSessionRecordWithCache(pendingRecord.sessionId, {
221
+ userSubmitted: true,
222
+ result: { status: CaptchaStatus.approved }
223
+ }));
224
+ pushSimdAttachIfAny(pendingRecord.sessionId, writePromises);
225
+ }
226
+ await Promise.all(writePromises);
227
+ } else {
228
+ const writePromises = [this.db.disapproveDappUserCommitment(commitmentId, "CAPTCHA.INVALID_SOLUTION", pairs)];
229
+ if (pendingRecord.sessionId) {
230
+ writePromises.push(this.updateSessionRecordWithCache(pendingRecord.sessionId, {
231
+ userSubmitted: true,
232
+ result: {
233
+ status: CaptchaStatus.disapproved,
234
+ reason: ResultReason.CAPTCHA_INVALID_SOLUTION
235
+ }
236
+ }));
237
+ pushSimdAttachIfAny(pendingRecord.sessionId, writePromises);
238
+ }
239
+ await Promise.all(writePromises);
240
+ response = {
241
+ captchas: captchaIds.map((id) => ({
242
+ captchaId: id,
243
+ proof: [[]]
244
+ })),
245
+ verified: false
246
+ };
247
+ }
248
+ } else this.logger.info(() => ({ msg: "Request hash not found" }));
249
+ return response;
250
+ }
251
+ /**
252
+ * Validate length of received captchas array matches length of captchas found in database
253
+ * Validate that the datasetId is the same for all captchas and is equal to the datasetId on the stored captchas
254
+ */
255
+ async validateReceivedCaptchasAgainstStoredCaptchas(captchas) {
256
+ const receivedCaptchas = parseAndSortCaptchaSolutions(captchas);
257
+ const captchaIds = receivedCaptchas.map((captcha) => captcha.captchaId);
258
+ const storedCaptchas = await this.db.getCaptchaById(captchaIds);
259
+ if (!storedCaptchas || receivedCaptchas.length !== storedCaptchas.length) throw new ProsopoEnvError("CAPTCHA.INVALID_CAPTCHA_ID", { context: {
260
+ failedFuncName: this.validateReceivedCaptchasAgainstStoredCaptchas.name,
261
+ captchas
262
+ } });
263
+ if (!storedCaptchas.every((captcha) => captcha.datasetId === at(storedCaptchas, 0).datasetId)) throw new ProsopoEnvError("CAPTCHA.DIFFERENT_DATASET_IDS", { context: {
264
+ failedFuncName: this.validateReceivedCaptchasAgainstStoredCaptchas.name,
265
+ captchas
266
+ } });
267
+ return {
268
+ storedCaptchas,
269
+ receivedCaptchas,
270
+ captchaIds
271
+ };
272
+ }
273
+ /**
274
+ * Validate that a Dapp User is responding to their own pending captcha request
275
+ * @param {string} requestHash
276
+ * @param {PendingImageCaptchaRequest} pendingRecord
277
+ * @param {string} userAccount
278
+ * @param {string[]} captchaIds
279
+ */
280
+ async validateDappUserSolutionRequestIsPending(requestHash, pendingRecord, userAccount, captchaIds) {
281
+ const currentTime = /* @__PURE__ */ new Date();
282
+ if (!pendingRecord) {
283
+ this.logger.info(() => ({ msg: "No pending record found" }));
284
+ return false;
285
+ }
286
+ if (pendingRecord.deadlineTimestamp < currentTime) {
287
+ this.logger.info(() => ({ msg: "Deadline for responding to captcha has expired" }));
288
+ return false;
289
+ }
290
+ if (pendingRecord) return requestHash === computePendingRequestHash(captchaIds, userAccount, pendingRecord.salt);
291
+ return false;
292
+ }
293
+ async getDappUserCommitmentById(commitmentId) {
294
+ const dappUserSolution = await this.db.getDappUserCommitmentById(commitmentId);
295
+ if (!dappUserSolution) throw new ProsopoEnvError("CAPTCHA.DAPP_USER_SOLUTION_NOT_FOUND", { context: {
296
+ failedFuncName: this.getDappUserCommitmentById.name,
297
+ commitmentId
298
+ } });
299
+ return dappUserSolution;
300
+ }
301
+ async getDappUserCommitmentByAccount(userAccount, dappAccount) {
302
+ const dappUserSolutions = await this.db.getDappUserCommitmentByAccount(userAccount, dappAccount);
303
+ if (dappUserSolutions.length > 0) {
304
+ for (const dappUserSolution of dappUserSolutions) if (dappUserSolution.result.status === CaptchaStatus.approved) return dappUserSolution;
305
+ }
306
+ }
307
+ async verifyImageCaptchaSolution(user, dapp, commitmentId, env, maxVerifiedTime, ip, disallowWebView, contextAwareEnabled = false, userAccessRulesStorage, email, spamEmailDomainCheckingEnabled = false, spamFilter, trafficFilter, storeMetadata = false) {
308
+ const logger = this.logger.with({
309
+ commitmentId,
310
+ dapp
311
+ });
312
+ const solution = await (commitmentId ? this.getDappUserCommitmentById(commitmentId) : this.getDappUserCommitmentByAccount(user, dapp));
313
+ if (!solution) {
314
+ logger.debug(() => ({ msg: "Not verified - no solution found" }));
315
+ return {
316
+ status: "API.USER_NOT_VERIFIED_NO_SOLUTION",
317
+ verified: false
318
+ };
319
+ }
320
+ if (solution.serverChecked) return {
321
+ status: "API.USER_ALREADY_VERIFIED",
322
+ verified: false
323
+ };
324
+ await this.db.markDappUserCommitmentsChecked([solution.id]);
325
+ if (solution.result.status === CaptchaStatus.disapproved) return {
326
+ status: solution.result.reason || "API.USER_NOT_VERIFIED",
327
+ verified: false
328
+ };
329
+ maxVerifiedTime = maxVerifiedTime || 60 * 1e3;
330
+ const currentTime = Date.now();
331
+ const submittedAt = solution.submittedAtTimestamp;
332
+ if ((submittedAt instanceof Date ? currentTime - submittedAt.getTime() : Number.POSITIVE_INFINITY) > maxVerifiedTime) {
333
+ logger.debug(() => ({ msg: "Not verified - timed out" }));
334
+ return {
335
+ status: "API.USER_NOT_VERIFIED_TIME_EXPIRED",
336
+ verified: false
337
+ };
338
+ }
339
+ const commitmentUpdates = {};
340
+ let failStatus;
341
+ if (userAccessRulesStorage) try {
342
+ const blockPolicy = await this.checkForHardBlock(userAccessRulesStorage, solution, solution.userAccount, solution.headers, solution.coords, solution.ipInfo?.isValid ? solution.ipInfo.countryCode : void 0, solution.ipInfo?.isValid ? solution.ipInfo.asnNumber : void 0);
343
+ if (blockPolicy) {
344
+ logger.info(() => ({
345
+ msg: "User blocked by access policy in server image verification",
346
+ data: {
347
+ userAccount: solution.userAccount,
348
+ dappAccount: solution.dappAccount,
349
+ policy: blockPolicy
350
+ }
351
+ }));
352
+ commitmentUpdates.result = {
353
+ status: CaptchaStatus.disapproved,
354
+ reason: ResultReason.ACCESS_POLICY_BLOCK
355
+ };
356
+ failStatus = ResultReason.ACCESS_POLICY_BLOCK;
357
+ }
358
+ } catch (error) {
359
+ logger.warn(() => ({
360
+ msg: "Failed to check user access policies in server image verification",
361
+ error
362
+ }));
363
+ }
364
+ if (!failStatus && email && spamEmailDomainCheckingEnabled) try {
365
+ if (await this.checkSpamEmail(email)) {
366
+ const emailDomain = email.split("@")[1] || "unknown";
367
+ logger.info(() => ({
368
+ msg: "Spam email domain detected in server image verification",
369
+ data: { emailDomain }
370
+ }));
371
+ commitmentUpdates.result = {
372
+ status: CaptchaStatus.disapproved,
373
+ reason: ResultReason.SPAM_EMAIL_DOMAIN
374
+ };
375
+ failStatus = ResultReason.SPAM_EMAIL_DOMAIN;
376
+ }
377
+ } catch (error) {
378
+ logger.warn(() => ({
379
+ msg: "Failed to check spam email domain in server image verification",
380
+ error
381
+ }));
382
+ }
383
+ if (!failStatus && spamFilter?.enabled && spamFilter.emailRules?.enabled && email) {
384
+ const result = evaluateEmailSpamRules(email, spamFilter.emailRules);
385
+ if (result.isSpam) {
386
+ logger.info(() => ({
387
+ msg: "Spam filter rejected email in image verification",
388
+ data: { reason: result.reason }
389
+ }));
390
+ commitmentUpdates.result = {
391
+ status: CaptchaStatus.disapproved,
392
+ reason: ResultReason.SPAM_EMAIL_RULE
393
+ };
394
+ failStatus = ResultReason.SPAM_EMAIL_RULE;
395
+ }
396
+ }
397
+ const sessionRecord = solution.sessionId ? await this.db.getSessionRecordBySessionId(solution.sessionId) : void 0;
398
+ const enrichedDnsEvent = await enrichDnsEvent(sessionRecord?.dnsEvent, env.ipInfoService, ip ?? solution.ipInfo?.ip);
399
+ if (!failStatus) {
400
+ const check = await this.resolveTrafficFilterCheck(env, solution.ipInfo, trafficFilter, ip, enrichedDnsEvent);
401
+ if (check.isBlocked) {
402
+ logger.info(() => ({
403
+ msg: "Traffic filter rejected request",
404
+ data: {
405
+ ip,
406
+ reason: check.reason,
407
+ dnsPeerIp: enrichedDnsEvent?.peerIp,
408
+ dnsResolverIp: enrichedDnsEvent?.resolverIp,
409
+ dnsPeerAsn: getIpInfoAsn(enrichedDnsEvent?.peerIpInfo),
410
+ dnsResolverAsn: getIpInfoAsn(enrichedDnsEvent?.resolverIpInfo),
411
+ dnsPathValid: enrichedDnsEvent?.pathValid
412
+ }
413
+ }));
414
+ commitmentUpdates.result = {
415
+ status: CaptchaStatus.disapproved,
416
+ reason: check.reason
417
+ };
418
+ failStatus = check.reason;
419
+ }
420
+ }
421
+ if (storeMetadata && email) commitmentUpdates.metadata = { email };
422
+ if (ip) {
423
+ const solutionIpAddress = getIpAddressFromComposite(solution.ipAddress);
424
+ const ipValidationRules = (await this.db.getClientRecord(dapp))?.settings?.ipValidationRules;
425
+ commitmentUpdates.providedIp = getCompositeIpAddress(ip);
426
+ if (!failStatus && ipValidationRules?.enabled === true) {
427
+ const ipValidation = await deepValidateIpAddress(ip, solutionIpAddress, logger, env.ipInfoService, ipValidationRules, enrichedDnsEvent?.peerIp);
428
+ if (!ipValidation.isValid) {
429
+ logger.error(() => ({
430
+ msg: "IP validation failed for image captcha",
431
+ data: {
432
+ ip,
433
+ solutionIp: solutionIpAddress.address,
434
+ error: ipValidation.errorMessage,
435
+ distanceKm: ipValidation.distanceKm
436
+ }
437
+ }));
438
+ failStatus = ResultReason.FAILED_IP_VALIDATION;
439
+ }
440
+ }
441
+ }
442
+ let isApproved = !failStatus && solution.result.status === CaptchaStatus.approved;
443
+ let failureStatus = failStatus || ResultReason.USER_NOT_VERIFIED;
444
+ if (!failStatus && solution.result.status === CaptchaStatus.approved && this.usageCounters) {
445
+ const ipValue = ip ?? "";
446
+ this.usageCounters.incrManyAsync(solution.dappAccount, buildAllWindowIncrements("solved", CaptchaType.image, ipValue, solution.userAccount, enrichedDnsEvent?.peerIp));
447
+ }
448
+ let score;
449
+ if (sessionRecord) {
450
+ const dnsAsymmetry = computeDnsAsymmetry(enrichedDnsEvent, solution.ipInfo, trafficFilter);
451
+ if (dnsAsymmetry > 0) sessionRecord.scoreComponents = {
452
+ ...sessionRecord.scoreComponents,
453
+ dnsAsymmetry
454
+ };
455
+ score = computeFrictionlessScore(sessionRecord?.scoreComponents);
456
+ logger.info(() => ({ data: {
457
+ scoreComponents: sessionRecord?.scoreComponents,
458
+ score,
459
+ dnsPeerAsn: getIpInfoAsn(enrichedDnsEvent?.peerIpInfo),
460
+ dnsResolverAsn: getIpInfoAsn(enrichedDnsEvent?.resolverIpInfo)
461
+ } }));
462
+ if (!failStatus && disallowWebView === true && (sessionRecord.webView === true || (sessionRecord.scoreComponents.webView || 0) > 0)) {
463
+ logger.info(() => ({ msg: "Disallowing webview access - user not verified" }));
464
+ commitmentUpdates.result = {
465
+ status: CaptchaStatus.disapproved,
466
+ reason: ResultReason.DISALLOWED_WEBVIEW
467
+ };
468
+ failStatus = ResultReason.DISALLOWED_WEBVIEW;
469
+ isApproved = false;
470
+ failureStatus = ResultReason.DISALLOWED_WEBVIEW;
471
+ }
472
+ if (contextAwareEnabled && sessionRecord.reason === FrictionlessReason$1.CONTEXT_AWARE_VALIDATION_FAILED) logger.info(() => ({ msg: "Context aware validation failed" }));
473
+ }
474
+ if (isApproved) {
475
+ const decisionInput = {
476
+ userAccount: solution.userAccount,
477
+ dappAccount: solution.dappAccount,
478
+ captchaResult: "passed",
479
+ headers: solution.headers,
480
+ captchaType: CaptchaType.image,
481
+ behavioralDataPacked: solution.behavioralDataPacked,
482
+ deviceCapability: solution.deviceCapability,
483
+ countryCode: solution.ipInfo?.isValid ? solution.ipInfo.countryCode : void 0,
484
+ ipInfo: solution.ipInfo,
485
+ dnsEvent: enrichedDnsEvent,
486
+ score,
487
+ threshold: sessionRecord?.threshold,
488
+ scoreComponents: sessionRecord?.scoreComponents,
489
+ decryptedHeadHash: sessionRecord?.decryptedHeadHash,
490
+ userSitekeyIpHash: sessionRecord?.userSitekeyIpHash,
491
+ simdReadings: sessionRecord?.simdReadings,
492
+ frictionlessReason: sessionRecord?.reason,
493
+ ruleType: sessionRecord?.ruleType,
494
+ webView: sessionRecord?.webView,
495
+ iFrame: sessionRecord?.iFrame,
496
+ coords: solution.coords
497
+ };
498
+ try {
499
+ const decision = await this.decisionMachineRunner.decide(decisionInput, logger);
500
+ if (decision.decision === DecisionMachineDecision.Deny) {
501
+ const dmReason = decision.reason || ResultReason.CAPTCHA_DECISION_MACHINE_DENIED;
502
+ logger.info(() => ({
503
+ msg: "Decision machine denied user verification",
504
+ data: { reason: decision.reason }
505
+ }));
506
+ commitmentUpdates.result = {
507
+ status: CaptchaStatus.disapproved,
508
+ reason: dmReason
509
+ };
510
+ isApproved = false;
511
+ failureStatus = dmReason;
512
+ }
513
+ } catch (error) {
514
+ logger.error(() => ({
515
+ msg: "Failed to process decision machine",
516
+ err: error
517
+ }));
518
+ }
519
+ }
520
+ const writePromises = [];
521
+ const sideUpdates = {};
522
+ if (commitmentUpdates.providedIp) sideUpdates.providedIp = commitmentUpdates.providedIp;
523
+ if (commitmentUpdates.metadata) sideUpdates.metadata = commitmentUpdates.metadata;
524
+ if (Object.keys(sideUpdates).length > 0) writePromises.push(this.db.updateDappUserCommitment(solution.id, sideUpdates));
525
+ if (commitmentId) {
526
+ if (isApproved) writePromises.push(this.db.approveDappUserCommitment(commitmentId));
527
+ else if (commitmentUpdates.result) writePromises.push(this.db.disapproveDappUserCommitment(commitmentId, commitmentUpdates.result.reason));
528
+ }
529
+ const finalResult = isApproved ? { status: CaptchaStatus.approved } : commitmentUpdates.result || {
530
+ status: CaptchaStatus.disapproved,
531
+ reason: failureStatus
532
+ };
533
+ if (solution.sessionId) writePromises.push(this.updateSessionRecordWithCache(solution.sessionId, {
534
+ serverChecked: true,
535
+ result: finalResult
536
+ }, true));
537
+ if (writePromises.length > 0) await Promise.all(writePromises);
538
+ return {
539
+ status: isApproved ? ResultReason.USER_VERIFIED : failureStatus,
540
+ verified: isApproved,
541
+ commitmentId: solution.id.toString(),
542
+ ...score && { score }
543
+ };
544
+ }
545
+ checkLangRules(acceptLanguage) {
546
+ return checkLangRules(this.config, acceptLanguage);
547
+ }
548
+ getVerificationResponse(verified, clientRecord, translateFn, score, commitmentId, reason) {
549
+ return {
550
+ ...super.getVerificationResponse(verified, clientRecord, translateFn, score, reason),
551
+ ...commitmentId && { [ApiParams.commitmentId]: commitmentId }
552
+ };
553
+ }
807
554
  };
555
+ //#endregion
556
+ export { ImgCaptchaManager };