@prosopo/provider 3.2.5 → 3.12.3

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 (52) hide show
  1. package/CHANGELOG.md +578 -0
  2. package/dist/api/admin/apiRemoveDetectorKeyEndpoint.js +7 -4
  3. package/dist/api/blacklistRequestInspector.js +26 -20
  4. package/dist/api/captcha.js +121 -33
  5. package/dist/api/domainMiddleware.js +8 -8
  6. package/dist/api/headerCheckMiddleware.js +4 -0
  7. package/dist/api/ignoreMiddleware.js +4 -1
  8. package/dist/api/ja4Middleware.js +5 -23
  9. package/dist/api/public.js +26 -3
  10. package/dist/api/verify.js +4 -2
  11. package/dist/cjs/api/admin/apiRemoveDetectorKeyEndpoint.cjs +6 -3
  12. package/dist/cjs/api/blacklistRequestInspector.cjs +25 -19
  13. package/dist/cjs/api/captcha.cjs +121 -33
  14. package/dist/cjs/api/domainMiddleware.cjs +8 -8
  15. package/dist/cjs/api/headerCheckMiddleware.cjs +4 -0
  16. package/dist/cjs/api/ignoreMiddleware.cjs +3 -0
  17. package/dist/cjs/api/ja4Middleware.cjs +4 -22
  18. package/dist/cjs/api/public.cjs +26 -3
  19. package/dist/cjs/api/verify.cjs +4 -2
  20. package/dist/cjs/compositeIpAddress.cjs +53 -0
  21. package/dist/cjs/index.cjs +7 -0
  22. package/dist/cjs/pairs.cjs +27 -0
  23. package/dist/cjs/services/ipComparison.cjs +123 -0
  24. package/dist/cjs/services/ipInfo.cjs +87 -0
  25. package/dist/cjs/tasks/captchaManager.cjs +49 -2
  26. package/dist/cjs/tasks/client/clientTasks.cjs +33 -12
  27. package/dist/cjs/tasks/detection/decodePayload.cjs +712 -278
  28. package/dist/cjs/tasks/detection/getBotScore.cjs +14 -3
  29. package/dist/cjs/tasks/frictionless/frictionlessTasks.cjs +128 -24
  30. package/dist/cjs/tasks/frictionless/frictionlessTasksUtils.cjs +17 -0
  31. package/dist/cjs/tasks/imgCaptcha/imgCaptchaTasks.cjs +62 -20
  32. package/dist/cjs/tasks/powCaptcha/powTasks.cjs +43 -15
  33. package/dist/cjs/util.cjs +248 -16
  34. package/dist/cjs/utils/hashUserAgent.cjs +10 -0
  35. package/dist/compositeIpAddress.js +53 -0
  36. package/dist/index.js +8 -1
  37. package/dist/pairs.js +27 -0
  38. package/dist/services/ipComparison.js +123 -0
  39. package/dist/services/ipInfo.js +87 -0
  40. package/dist/tasks/captchaManager.js +49 -2
  41. package/dist/tasks/client/clientTasks.js +33 -12
  42. package/dist/tasks/detection/decodePayload.js +712 -278
  43. package/dist/tasks/detection/getBotScore.js +15 -4
  44. package/dist/tasks/frictionless/frictionlessTasks.js +128 -24
  45. package/dist/tasks/frictionless/frictionlessTasksUtils.js +18 -1
  46. package/dist/tasks/imgCaptcha/imgCaptchaTasks.js +64 -22
  47. package/dist/tasks/powCaptcha/powTasks.js +44 -16
  48. package/dist/util.js +249 -17
  49. package/dist/utils/hashUserAgent.js +10 -0
  50. package/package.json +28 -25
  51. package/vite.test.config.ts +3 -2
  52. package/vite.threads.test.config.ts +33 -0
@@ -1,12 +1,23 @@
1
- import uTahDN from "./decodePayload.js";
1
+ import GGaYiU from "./decodePayload.js";
2
+ const DEFAULT_ENTROPY = 13837;
2
3
  const getBotScore = async (payload, privateKeyString) => {
3
- const result = await uTahDN(payload, privateKeyString);
4
+ const result = await GGaYiU(
5
+ payload,
6
+ privateKeyString
7
+ );
4
8
  const baseBotScore = result.score;
5
9
  const timestamp = result.timestamp;
10
+ const providerSelectEntropy = result.providerSelectEntropy;
11
+ const userId = result.userId;
12
+ const userAgent = result.userAgent;
6
13
  if (baseBotScore === void 0) {
7
- return { baseBotScore: 1, timestamp: 0 };
14
+ return {
15
+ baseBotScore: 1,
16
+ timestamp: 0,
17
+ providerSelectEntropy: DEFAULT_ENTROPY
18
+ };
8
19
  }
9
- return { baseBotScore, timestamp };
20
+ return { baseBotScore, timestamp, providerSelectEntropy, userId, userAgent };
10
21
  };
11
22
  export {
12
23
  getBotScore
@@ -1,9 +1,20 @@
1
+ import { getRandomActiveProvider } from "@prosopo/load-balancer";
1
2
  import { CaptchaType, ApiParams } from "@prosopo/types";
2
3
  import { v4 } from "uuid";
3
4
  import { checkLangRules } from "../../rules/lang.js";
4
5
  import { CaptchaManager } from "../captchaManager.js";
5
6
  import { getBotScore } from "../detection/getBotScore.js";
7
+ const getDefaultEntropy = () => {
8
+ if (process.env.PROSOPO_ENTROPY) {
9
+ const parsed = Number.parseInt(process.env.PROSOPO_ENTROPY);
10
+ if (!Number.isNaN(parsed)) {
11
+ return parsed;
12
+ }
13
+ }
14
+ return 13337;
15
+ };
6
16
  const DEFAULT_MAX_TIMESTAMP_AGE = 60 * 10 * 1e3;
17
+ const DEFAULT_ENTROPY = getDefaultEntropy();
7
18
  class FrictionlessManager extends CaptchaManager {
8
19
  constructor(db, pair, config, logger) {
9
20
  super(db, pair, logger);
@@ -12,26 +23,54 @@ class FrictionlessManager extends CaptchaManager {
12
23
  checkLangRules(acceptLanguage) {
13
24
  return checkLangRules(this.config, acceptLanguage);
14
25
  }
15
- async createSession(tokenId, captchaType) {
26
+ async createSession(tokenId, captchaType, solvedImagesCount, powDifficulty) {
16
27
  const sessionRecord = {
17
28
  sessionId: v4(),
18
29
  createdAt: /* @__PURE__ */ new Date(),
19
30
  tokenId,
20
- captchaType
31
+ captchaType,
32
+ solvedImagesCount,
33
+ powDifficulty
21
34
  };
22
35
  await this.db.storeSessionRecord(sessionRecord);
23
36
  return sessionRecord;
24
37
  }
25
- async sendImageCaptcha(tokenId) {
26
- const sessionRecord = await this.createSession(tokenId, CaptchaType.image);
38
+ async hostVerified(entropy) {
39
+ const chosen = await getRandomActiveProvider(
40
+ this.config.defaultEnvironment,
41
+ entropy
42
+ );
43
+ const domain = new URL(chosen.provider.url).hostname;
44
+ this.logger.info(() => ({
45
+ data: { entropy, host: this.config.host, domain }
46
+ }));
47
+ if (domain !== this.config.host) {
48
+ this.logger.info(() => ({
49
+ msg: "Host mismatch",
50
+ data: { expected: this.config.host, got: domain, entropy }
51
+ }));
52
+ return { verified: false, domain };
53
+ }
54
+ return { verified: true, domain };
55
+ }
56
+ async sendImageCaptcha(tokenId, solvedImagesCount) {
57
+ const sessionRecord = await this.createSession(
58
+ tokenId,
59
+ CaptchaType.image,
60
+ solvedImagesCount
61
+ );
27
62
  return {
28
63
  [ApiParams.captchaType]: CaptchaType.image,
29
64
  [ApiParams.sessionId]: sessionRecord.sessionId,
30
65
  [ApiParams.status]: "ok"
31
66
  };
32
67
  }
33
- async sendPowCaptcha(tokenId) {
34
- const sessionRecord = await this.createSession(tokenId, CaptchaType.pow);
68
+ async sendPowCaptcha(tokenId, powDifficulty) {
69
+ const sessionRecord = await this.createSession(
70
+ tokenId,
71
+ CaptchaType.pow,
72
+ powDifficulty
73
+ );
35
74
  return {
36
75
  [ApiParams.captchaType]: CaptchaType.pow,
37
76
  [ApiParams.sessionId]: sessionRecord.sessionId,
@@ -50,6 +89,21 @@ class FrictionlessManager extends CaptchaManager {
50
89
  });
51
90
  return botScore;
52
91
  }
92
+ async scoreIncreaseUnverifiedHost(host, baseBotScore, botScore, tokenId) {
93
+ this.logger.info(() => ({
94
+ msg: "Host not verified",
95
+ data: { requested: this.config.host, selected: host }
96
+ }));
97
+ botScore += this.config.penalties.PENALTY_UNVERIFIED_HOST;
98
+ await this.db.updateFrictionlessTokenRecord(tokenId, {
99
+ score: botScore,
100
+ scoreComponents: {
101
+ baseScore: baseBotScore,
102
+ unverifiedHost: this.config.penalties.PENALTY_UNVERIFIED_HOST
103
+ }
104
+ });
105
+ return botScore;
106
+ }
53
107
  async scoreIncreaseTimestamp(timestamp, baseBotScore, botScore, tokenId) {
54
108
  this.logger.info(() => ({
55
109
  msg: "Timestamp is older than 10 minutes",
@@ -70,22 +124,31 @@ class FrictionlessManager extends CaptchaManager {
70
124
  const diff = now - timestamp;
71
125
  return diff > DEFAULT_MAX_TIMESTAMP_AGE;
72
126
  }
127
+ /**
128
+ * Redacts a key for logging purposes by showing only the first 5, middle 10, and last 5 characters
129
+ * @param key - The key to redact
130
+ * @returns Redacted key string or empty string if key is falsy
131
+ */
132
+ redactKeyForLogging(key) {
133
+ if (!key) return "";
134
+ const start = key.slice(0, 5);
135
+ const middle = key.slice(
136
+ Math.floor(key.length / 2) - 5,
137
+ Math.floor(key.length / 2) + 5
138
+ );
139
+ const end = key.slice(-5);
140
+ return `${start}...${middle}...${end}`;
141
+ }
73
142
  async decryptPayload(token) {
74
143
  const decryptKeys = [
75
- process.env.BOT_DECRYPTION_KEY,
76
- ...await this.getDetectorKeys()
144
+ // Process DB keys first, then env var key last as env key will likely be invalid
145
+ ...await this.getDetectorKeys(),
146
+ process.env.BOT_DECRYPTION_KEY
77
147
  ].filter((k) => k);
78
148
  this.logger.debug(() => {
79
- const loggedKeys = decryptKeys.map((key) => {
80
- if (!key) return "";
81
- const start = key.slice(0, 5);
82
- const middle = key.slice(
83
- Math.floor(key.length / 2) - 5,
84
- Math.floor(key.length / 2) + 5
85
- );
86
- const end = key.slice(-5);
87
- return `${start}...${middle}...${end}`;
88
- });
149
+ const loggedKeys = decryptKeys.map(
150
+ (key) => this.redactKeyForLogging(key)
151
+ );
89
152
  return {
90
153
  msg: "Decrypting score",
91
154
  data: {
@@ -96,19 +159,39 @@ class FrictionlessManager extends CaptchaManager {
96
159
  });
97
160
  let baseBotScore;
98
161
  let timestamp;
162
+ let providerSelectEntropy;
163
+ let userId;
164
+ let userAgent;
99
165
  for (const [keyIndex, key] of decryptKeys.entries()) {
100
166
  try {
101
- const { baseBotScore: s, timestamp: t } = await getBotScore(token, key);
102
167
  this.logger.info(() => ({
168
+ msg: "Attempting to decrypt score",
169
+ data: {
170
+ key: this.redactKeyForLogging(key)
171
+ }
172
+ }));
173
+ const decrypted = await getBotScore(token, key);
174
+ const s = decrypted.baseBotScore;
175
+ const t = decrypted.timestamp;
176
+ const p = decrypted.providerSelectEntropy;
177
+ const a = decrypted.userId;
178
+ const u = decrypted.userAgent;
179
+ this.logger.debug(() => ({
103
180
  msg: "Successfully decrypted score",
104
181
  data: {
105
- key: key ? `${key.slice(0, 5)}...${key.slice(-5)}` : "",
182
+ key: this.redactKeyForLogging(key),
106
183
  baseBotScore: s,
107
- timestamp: t
184
+ timestamp: t,
185
+ entropy: p,
186
+ userId: a,
187
+ userAgent: u
108
188
  }
109
189
  }));
110
190
  baseBotScore = s;
111
191
  timestamp = t;
192
+ providerSelectEntropy = p;
193
+ userId = a;
194
+ userAgent = u;
112
195
  break;
113
196
  } catch (err) {
114
197
  if (keyIndex === decryptKeys.length - 1) {
@@ -117,19 +200,40 @@ class FrictionlessManager extends CaptchaManager {
117
200
  }));
118
201
  baseBotScore = 1;
119
202
  timestamp = 0;
203
+ providerSelectEntropy = DEFAULT_ENTROPY + 1;
120
204
  }
121
205
  }
122
206
  }
123
- if (baseBotScore === void 0 || timestamp === void 0) {
207
+ const baseBotScoreUndefined = baseBotScore === void 0;
208
+ const timestampUndefined = timestamp === void 0;
209
+ const providerSelectEntropyUndefined = providerSelectEntropy === void 0;
210
+ const undefinedCount = Number(baseBotScoreUndefined) + Number(timestampUndefined) + Number(providerSelectEntropyUndefined);
211
+ if (undefinedCount > 0) {
124
212
  this.logger.error(() => ({
125
- msg: "Error decrypting score: baseBotScore or timestamp is undefined"
213
+ msg: "Error decrypting score: baseBotScore or timestamp or providerSelectEntropy is undefined"
126
214
  }));
127
215
  baseBotScore = 1;
128
216
  timestamp = 0;
217
+ providerSelectEntropy = DEFAULT_ENTROPY - undefinedCount;
129
218
  }
130
- return { baseBotScore, timestamp };
219
+ this.logger.info(() => ({
220
+ msg: "decryptPayload result",
221
+ data: {
222
+ baseBotScore,
223
+ timestamp,
224
+ entropy: providerSelectEntropy
225
+ }
226
+ }));
227
+ return {
228
+ baseBotScore: Number(baseBotScore),
229
+ timestamp: Number(timestamp),
230
+ providerSelectEntropy: Number(providerSelectEntropy),
231
+ userId,
232
+ userAgent
233
+ };
131
234
  }
132
235
  }
133
236
  export {
237
+ DEFAULT_ENTROPY,
134
238
  FrictionlessManager
135
239
  };
@@ -6,6 +6,23 @@ const computeFrictionlessScore = (scoreComponents) => {
6
6
  ).toFixed(2)
7
7
  );
8
8
  };
9
+ const timestampDecayFunction = (timestamp) => {
10
+ const max = (/* @__PURE__ */ new Date()).getTime();
11
+ if (max - timestamp > 36e5) {
12
+ return 12;
13
+ }
14
+ const min = 1e3;
15
+ const age = max - timestamp;
16
+ const decay = Math.log10(2e3) / max;
17
+ const bigScore = max * (1 - (1 - Math.exp(decay * age) ** 24));
18
+ return Math.max(
19
+ 2,
20
+ Math.round(
21
+ (Math.log(bigScore) - Math.log(min)) / (Math.log(max) - Math.log(min)) * 2.5
22
+ )
23
+ );
24
+ };
9
25
  export {
10
- computeFrictionlessScore
26
+ computeFrictionlessScore,
27
+ timestampDecayFunction
11
28
  };
@@ -2,10 +2,12 @@ import { u8aToHex, stringToHex } from "@polkadot/util";
2
2
  import { ProsopoEnvError } from "@prosopo/common";
3
3
  import { computePendingRequestHash, compareCaptchaSolutions, parseAndSortCaptchaSolutions } from "@prosopo/datasets";
4
4
  import { DEFAULT_IMAGE_CAPTCHA_TIMEOUT, CaptchaStatus, ApiParams } from "@prosopo/types";
5
- import { at } from "@prosopo/util";
5
+ import { extractData, at } from "@prosopo/util";
6
6
  import { randomAsHex, signatureVerify } from "@prosopo/util-crypto";
7
+ import { getCompositeIpAddress, getIpAddressFromComposite } from "../../compositeIpAddress.js";
8
+ import { constructPairList, containsIdenticalPairs } from "../../pairs.js";
7
9
  import { checkLangRules } from "../../rules/lang.js";
8
- import { shuffleArray, validateIpAddress } from "../../util.js";
10
+ import { shuffleArray, deepValidateIpAddress } from "../../util.js";
9
11
  import { CaptchaManager } from "../captchaManager.js";
10
12
  import { computeFrictionlessScore } from "../frictionless/frictionlessTasksUtils.js";
11
13
  import { buildTreeAndGetCommitmentId } from "./imgCaptchaTasksUtils.js";
@@ -76,7 +78,7 @@ class ImgCaptchaManager extends CaptchaManager {
76
78
  salt,
77
79
  deadlineTs,
78
80
  currentTime,
79
- ipAddress.bigInt(),
81
+ getCompositeIpAddress(ipAddress),
80
82
  threshold,
81
83
  frictionlessTokenId
82
84
  );
@@ -98,7 +100,7 @@ class ImgCaptchaManager extends CaptchaManager {
98
100
  * @param providerRequestHashSignature
99
101
  * @param ipAddress
100
102
  * @param headers
101
- * @param threshold the percentage of captchas that must be correct to return true
103
+ * @param ja4
102
104
  * @return {Promise<DappUserSolutionResult>} result containing the contract event
103
105
  */
104
106
  async dappUserSolution(userAccount, dappAccount, requestHash, captchas, userTimestampSignature, timestamp, providerRequestHashSignature, ipAddress, headers, ja4) {
@@ -150,6 +152,8 @@ class ImgCaptchaManager extends CaptchaManager {
150
152
  );
151
153
  if (pendingRequest) {
152
154
  const { storedCaptchas, receivedCaptchas, captchaIds } = await this.validateReceivedCaptchasAgainstStoredCaptchas(captchas);
155
+ const flat = receivedCaptchas.map((c) => extractData(c.salt));
156
+ const pairs = flat.map((list) => constructPairList(list));
153
157
  const { tree, commitmentId } = buildTreeAndGetCommitmentId(receivedCaptchas);
154
158
  const datasetId = at(storedCaptchas, 0).datasetId;
155
159
  if (!datasetId) {
@@ -169,7 +173,7 @@ class ImgCaptchaManager extends CaptchaManager {
169
173
  userSubmitted: true,
170
174
  serverChecked: false,
171
175
  requestedAtTimestamp: timestamp,
172
- ipAddress,
176
+ ipAddress: getCompositeIpAddress(ipAddress),
173
177
  headers,
174
178
  frictionlessTokenId: pendingRecord.frictionlessTokenId,
175
179
  ja4
@@ -189,6 +193,21 @@ class ImgCaptchaManager extends CaptchaManager {
189
193
  })
190
194
  );
191
195
  const totalImages = storedCaptchas[0]?.items.length || 0;
196
+ if (containsIdenticalPairs(pairs)) {
197
+ await this.db.disapproveDappUserCommitment(
198
+ commitmentId,
199
+ "CAPTCHA.INVALID_SOLUTION",
200
+ pairs
201
+ );
202
+ response = {
203
+ captchas: captchaIds.map((id) => ({
204
+ captchaId: id,
205
+ proof: [[]]
206
+ })),
207
+ verified: false
208
+ };
209
+ return response;
210
+ }
192
211
  if (compareCaptchaSolutions(
193
212
  receivedCaptchas,
194
213
  solutionRecords,
@@ -202,11 +221,12 @@ class ImgCaptchaManager extends CaptchaManager {
202
221
  })),
203
222
  verified: true
204
223
  };
205
- await this.db.approveDappUserCommitment(commitmentId);
224
+ await this.db.approveDappUserCommitment(commitmentId, pairs);
206
225
  } else {
207
226
  await this.db.disapproveDappUserCommitment(
208
227
  commitmentId,
209
- "CAPTCHA.INVALID_SOLUTION"
228
+ "CAPTCHA.INVALID_SOLUTION",
229
+ pairs
210
230
  );
211
231
  response = {
212
232
  captchas: captchaIds.map((id) => ({
@@ -312,7 +332,7 @@ class ImgCaptchaManager extends CaptchaManager {
312
332
  }
313
333
  return void 0;
314
334
  }
315
- async verifyImageCaptchaSolution(user, dapp, commitmentId, maxVerifiedTime, ip) {
335
+ async verifyImageCaptchaSolution(user, dapp, commitmentId, env, maxVerifiedTime, ip) {
316
336
  const solution = await (commitmentId ? this.getDappUserCommitmentById(commitmentId) : this.getDappUserCommitmentByAccount(user, dapp));
317
337
  if (!solution) {
318
338
  this.logger.debug(() => ({
@@ -320,10 +340,6 @@ class ImgCaptchaManager extends CaptchaManager {
320
340
  }));
321
341
  return { status: "API.USER_NOT_VERIFIED_NO_SOLUTION", verified: false };
322
342
  }
323
- const ipValidation = validateIpAddress(ip, solution.ipAddress, this.logger);
324
- if (!ipValidation.isValid) {
325
- return { status: "API.USER_NOT_VERIFIED", verified: false };
326
- }
327
343
  if (solution.serverChecked) {
328
344
  return { status: "API.USER_ALREADY_VERIFIED", verified: false };
329
345
  }
@@ -332,17 +348,43 @@ class ImgCaptchaManager extends CaptchaManager {
332
348
  return { status: "API.USER_NOT_VERIFIED", verified: false };
333
349
  }
334
350
  maxVerifiedTime = maxVerifiedTime || 60 * 1e3;
335
- if (maxVerifiedTime) {
336
- const currentTime = Date.now();
337
- const timeSinceCompletion = currentTime - solution.requestedAtTimestamp;
338
- if (timeSinceCompletion > maxVerifiedTime) {
339
- this.logger.debug(() => ({
340
- msg: "Not verified - timed out"
351
+ const currentTime = Date.now();
352
+ const timeSinceCompletion = currentTime - solution.requestedAtTimestamp;
353
+ if (timeSinceCompletion > maxVerifiedTime) {
354
+ this.logger.debug(() => ({
355
+ msg: "Not verified - timed out"
356
+ }));
357
+ return {
358
+ status: "API.USER_NOT_VERIFIED_TIME_EXPIRED",
359
+ verified: false
360
+ };
361
+ }
362
+ if (ip) {
363
+ const solutionIpAddress = getIpAddressFromComposite(solution.ipAddress);
364
+ const clientRecord = await this.db.getClientRecord(dapp);
365
+ const ipValidationRules = clientRecord?.settings?.ipValidationRules;
366
+ await this.db.updateDappUserCommitment(solution.id, {
367
+ providedIp: getCompositeIpAddress(ip)
368
+ });
369
+ const ipValidation = await deepValidateIpAddress(
370
+ ip,
371
+ solutionIpAddress,
372
+ this.logger,
373
+ env.config.ipApi.apiKey,
374
+ env.config.ipApi.baseUrl,
375
+ ipValidationRules
376
+ );
377
+ if (!ipValidation.isValid) {
378
+ this.logger.error(() => ({
379
+ msg: "IP validation failed for image captcha",
380
+ data: {
381
+ ip,
382
+ solutionIp: solutionIpAddress.address,
383
+ error: ipValidation.errorMessage,
384
+ distanceKm: ipValidation.distanceKm
385
+ }
341
386
  }));
342
- return {
343
- status: "API.USER_NOT_VERIFIED_TIME_EXPIRED",
344
- verified: false
345
- };
387
+ return { status: "API.USER_NOT_VERIFIED", verified: false };
346
388
  }
347
389
  }
348
390
  const isApproved = solution.result.status === CaptchaStatus.approved;
@@ -2,7 +2,8 @@ import { u8aToHex, stringToHex } from "@polkadot/util";
2
2
  import { ProsopoApiError, ProsopoEnvError } from "@prosopo/common";
3
3
  import { POW_SEPARATOR, ApiParams, CaptchaStatus } from "@prosopo/types";
4
4
  import { at, verifyRecency } from "@prosopo/util";
5
- import { validateIpAddress } from "../../util.js";
5
+ import { getIpAddressFromComposite, getCompositeIpAddress } from "../../compositeIpAddress.js";
6
+ import { deepValidateIpAddress } from "../../util.js";
6
7
  import { CaptchaManager } from "../captchaManager.js";
7
8
  import { computeFrictionlessScore } from "../frictionless/frictionlessTasksUtils.js";
8
9
  import { checkPowSignature, validateSolution } from "./powTasksUtils.js";
@@ -45,7 +46,7 @@ class PowCaptchaManager extends CaptchaManager {
45
46
  * @param ipAddress
46
47
  * @param headers
47
48
  */
48
- async verifyPowCaptchaSolution(challenge, difficulty, providerChallengeSignature, nonce, timeout, userTimestampSignature, ipAddress, headers) {
49
+ async verifyPowCaptchaSolution(challenge, providerChallengeSignature, nonce, timeout, userTimestampSignature, ipAddress, headers) {
49
50
  checkPowSignature(
50
51
  challenge,
51
52
  providerChallengeSignature,
@@ -68,8 +69,9 @@ class PowCaptchaManager extends CaptchaManager {
68
69
  }));
69
70
  return false;
70
71
  }
72
+ const difficulty = challengeRecord.difficulty;
71
73
  if (!verifyRecency(challenge, timeout)) {
72
- await this.db.updatePowCaptchaRecord(
74
+ await this.db.updatePowCaptchaRecordResult(
73
75
  challenge,
74
76
  {
75
77
  status: CaptchaStatus.disapproved,
@@ -91,7 +93,7 @@ class PowCaptchaManager extends CaptchaManager {
91
93
  reason: "CAPTCHA.INVALID_SOLUTION"
92
94
  };
93
95
  }
94
- await this.db.updatePowCaptchaRecord(
96
+ await this.db.updatePowCaptchaRecordResult(
95
97
  challenge,
96
98
  result,
97
99
  false,
@@ -109,21 +111,14 @@ class PowCaptchaManager extends CaptchaManager {
109
111
  * @param {number} timeout - the time in milliseconds since the Provider was selected to provide the PoW captcha
110
112
  * @param ip
111
113
  */
112
- async serverVerifyPowCaptchaSolution(dappAccount, challenge, timeout, ip) {
114
+ async serverVerifyPowCaptchaSolution(dappAccount, challenge, timeout, env, ip) {
115
+ const notVerifiedResponse = { verified: false };
113
116
  const challengeRecord = await this.db.getPowCaptchaRecordByChallenge(challenge);
114
117
  if (!challengeRecord) {
115
118
  this.logger.debug(() => ({
116
119
  msg: `No record of this challenge: ${challenge}`
117
120
  }));
118
- return { verified: false };
119
- }
120
- const ipValidation = validateIpAddress(
121
- ip,
122
- challengeRecord.ipAddress,
123
- this.logger
124
- );
125
- if (!ipValidation.isValid) {
126
- return { verified: false };
121
+ return notVerifiedResponse;
127
122
  }
128
123
  if (challengeRecord.result.status !== CaptchaStatus.approved) {
129
124
  throw new ProsopoApiError("CAPTCHA.INVALID_SOLUTION", {
@@ -133,7 +128,7 @@ class PowCaptchaManager extends CaptchaManager {
133
128
  }
134
129
  });
135
130
  }
136
- if (challengeRecord.serverChecked) return { verified: false };
131
+ if (challengeRecord.serverChecked) return notVerifiedResponse;
137
132
  const challengeDappAccount = challengeRecord.dappAccount;
138
133
  if (dappAccount !== challengeDappAccount) {
139
134
  throw new ProsopoEnvError("CAPTCHA.DAPP_USER_SOLUTION_NOT_FOUND", {
@@ -144,10 +139,43 @@ class PowCaptchaManager extends CaptchaManager {
144
139
  }
145
140
  });
146
141
  }
147
- verifyRecency(challenge, timeout);
148
142
  await this.db.markDappUserPoWCommitmentsChecked([
149
143
  challengeRecord.challenge
150
144
  ]);
145
+ const recent = verifyRecency(challenge, timeout);
146
+ if (!recent) {
147
+ return notVerifiedResponse;
148
+ }
149
+ if (ip) {
150
+ const challengeIpAddress = getIpAddressFromComposite(
151
+ challengeRecord.ipAddress
152
+ );
153
+ const clientRecord = await this.db.getClientRecord(dappAccount);
154
+ const ipValidationRules = clientRecord?.settings?.ipValidationRules;
155
+ await this.db.updatePowCaptchaRecord(challengeRecord.challenge, {
156
+ providedIp: getCompositeIpAddress(ip)
157
+ });
158
+ const ipValidation = await deepValidateIpAddress(
159
+ ip,
160
+ challengeIpAddress,
161
+ this.logger,
162
+ env.config.ipApi.apiKey,
163
+ env.config.ipApi.baseUrl,
164
+ ipValidationRules
165
+ );
166
+ if (!ipValidation.isValid) {
167
+ this.logger.error(() => ({
168
+ msg: "IP validation failed for PoW captcha",
169
+ data: {
170
+ ip,
171
+ challengeIp: challengeIpAddress.address,
172
+ error: ipValidation.errorMessage,
173
+ distanceKm: ipValidation.distanceKm
174
+ }
175
+ }));
176
+ return notVerifiedResponse;
177
+ }
178
+ }
151
179
  let score;
152
180
  if (challengeRecord.frictionlessTokenId) {
153
181
  const tokenRecord = await this.db.getFrictionlessTokenRecordByTokenId(