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