@prosopo/provider 2.5.3 → 2.7.0

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 (79) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/api/captcha.d.ts.map +1 -1
  3. package/dist/api/captcha.js +1 -1
  4. package/dist/api/captcha.js.map +1 -1
  5. package/dist/api/ja4Middleware.d.ts +6 -0
  6. package/dist/api/ja4Middleware.d.ts.map +1 -1
  7. package/dist/api/ja4Middleware.js +7 -7
  8. package/dist/api/ja4Middleware.js.map +1 -1
  9. package/dist/cjs/api/admin/apiAdminRoutesProvider.cjs +32 -0
  10. package/dist/cjs/api/admin/apiRegisterSiteKeyEndpoint.cjs +25 -0
  11. package/dist/cjs/api/admin/apiRemoveDetectorKeyEndpoint.cjs +32 -0
  12. package/dist/cjs/api/admin/apiUpdateDetectorKeyEndpoint.cjs +32 -0
  13. package/dist/cjs/api/admin/createApiAdminRoutesProvider.cjs +10 -0
  14. package/dist/cjs/api/authMiddleware.cjs +81 -0
  15. package/dist/cjs/api/blacklistRequestInspector.cjs +73 -0
  16. package/dist/cjs/api/block.cjs +24 -0
  17. package/dist/cjs/api/captcha.cjs +482 -0
  18. package/dist/cjs/api/domainMiddleware.cjs +89 -0
  19. package/dist/cjs/api/headerCheckMiddleware.cjs +29 -0
  20. package/dist/cjs/api/ignoreMiddleware.cjs +14 -0
  21. package/dist/cjs/api/ja4Middleware.cjs +73 -0
  22. package/dist/cjs/api/public.cjs +27 -0
  23. package/dist/cjs/api/requestLoggerMiddleware.cjs +14 -0
  24. package/dist/cjs/api/robotsMiddleware.cjs +12 -0
  25. package/dist/cjs/api/validateAddress.cjs +19 -0
  26. package/dist/cjs/api/verify.cjs +138 -0
  27. package/dist/cjs/index.cjs +41 -0
  28. package/dist/cjs/rules/lang.cjs +16 -0
  29. package/dist/cjs/schedulers/captchaScheduler.cjs +31 -0
  30. package/dist/cjs/schedulers/getClientList.cjs +29 -0
  31. package/dist/cjs/tasks/captchaManager.cjs +90 -0
  32. package/dist/cjs/tasks/client/clientTasks.cjs +281 -0
  33. package/dist/cjs/tasks/dataset/datasetTasks.cjs +30 -0
  34. package/dist/cjs/tasks/dataset/datasetTasksUtils.cjs +34 -0
  35. package/dist/cjs/tasks/detection/decodePayload.cjs +475 -0
  36. package/dist/cjs/tasks/detection/getBotScore.cjs +13 -0
  37. package/dist/cjs/tasks/frictionless/frictionlessTasks.cjs +121 -0
  38. package/dist/cjs/tasks/frictionless/frictionlessTasksUtils.cjs +11 -0
  39. package/dist/cjs/tasks/imgCaptcha/imgCaptchaTasks.cjs +366 -0
  40. package/dist/cjs/tasks/imgCaptcha/imgCaptchaTasksUtils.cjs +25 -0
  41. package/dist/cjs/tasks/index.cjs +4 -0
  42. package/dist/cjs/tasks/powCaptcha/powTasks.cjs +155 -0
  43. package/dist/cjs/tasks/powCaptcha/powTasksUtils.cjs +26 -0
  44. package/dist/cjs/tasks/tasks.cjs +51 -0
  45. package/dist/cjs/util.cjs +58 -0
  46. package/dist/schedulers/captchaScheduler.d.ts +1 -1
  47. package/dist/schedulers/captchaScheduler.d.ts.map +1 -1
  48. package/dist/schedulers/captchaScheduler.js +1 -7
  49. package/dist/schedulers/captchaScheduler.js.map +1 -1
  50. package/dist/schedulers/getClientList.d.ts +1 -1
  51. package/dist/schedulers/getClientList.d.ts.map +1 -1
  52. package/dist/schedulers/getClientList.js +1 -7
  53. package/dist/schedulers/getClientList.js.map +1 -1
  54. package/dist/tasks/client/clientTasks.d.ts +5 -2
  55. package/dist/tasks/client/clientTasks.d.ts.map +1 -1
  56. package/dist/tasks/client/clientTasks.js +55 -9
  57. package/dist/tasks/client/clientTasks.js.map +1 -1
  58. package/dist/tasks/frictionless/frictionlessTasks.d.ts.map +1 -1
  59. package/dist/tasks/frictionless/frictionlessTasks.js +1 -0
  60. package/dist/tasks/frictionless/frictionlessTasks.js.map +1 -1
  61. package/dist/tasks/imgCaptcha/imgCaptchaTasks.d.ts +1 -1
  62. package/dist/tasks/imgCaptcha/imgCaptchaTasks.d.ts.map +1 -1
  63. package/dist/tasks/imgCaptcha/imgCaptchaTasks.js +13 -3
  64. package/dist/tasks/imgCaptcha/imgCaptchaTasks.js.map +1 -1
  65. package/dist/tests/unit/api/ja4Middleware.unit.test.d.ts +2 -0
  66. package/dist/tests/unit/api/ja4Middleware.unit.test.d.ts.map +1 -0
  67. package/dist/tests/unit/api/ja4Middleware.unit.test.js +57 -0
  68. package/dist/tests/unit/api/ja4Middleware.unit.test.js.map +1 -0
  69. package/dist/tests/unit/schedulers/captchaScheduler.unit.test.js +2 -2
  70. package/dist/tests/unit/schedulers/captchaScheduler.unit.test.js.map +1 -1
  71. package/dist/tests/unit/tasks/captchaManager.unit.test.js +1 -0
  72. package/dist/tests/unit/tasks/captchaManager.unit.test.js.map +1 -1
  73. package/dist/tests/unit/tasks/client/clientTasks.unit.test.js +11 -0
  74. package/dist/tests/unit/tasks/client/clientTasks.unit.test.js.map +1 -1
  75. package/dist/tests/unit/tasks/imgCaptcha/imgCaptchaTasks.unit.test.js +2 -2
  76. package/dist/tests/unit/tasks/imgCaptcha/imgCaptchaTasks.unit.test.js.map +1 -1
  77. package/dist/tests/unit/tasks/powCaptcha/powTasksUtils.unit.test.js +2 -1
  78. package/dist/tests/unit/tasks/powCaptcha/powTasksUtils.unit.test.js.map +1 -1
  79. package/package.json +19 -18
@@ -0,0 +1,366 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const util$1 = require("@polkadot/util");
4
+ const utilCrypto = require("@polkadot/util-crypto");
5
+ const common = require("@prosopo/common");
6
+ const datasets = require("@prosopo/datasets");
7
+ const types = require("@prosopo/types");
8
+ const util$2 = require("@prosopo/util");
9
+ const lang = require("../../rules/lang.cjs");
10
+ const util = require("../../util.cjs");
11
+ const captchaManager = require("../captchaManager.cjs");
12
+ const frictionlessTasksUtils = require("../frictionless/frictionlessTasksUtils.cjs");
13
+ const imgCaptchaTasksUtils = require("./imgCaptchaTasksUtils.cjs");
14
+ class ImgCaptchaManager extends captchaManager.CaptchaManager {
15
+ constructor(db, pair, config, logger) {
16
+ super(db, pair, logger);
17
+ this.config = config;
18
+ }
19
+ async getCaptchaWithProof(datasetId, solved, size) {
20
+ const captchaDocs = await this.db.getRandomCaptcha(solved, datasetId, size);
21
+ if (!captchaDocs) {
22
+ throw new common.ProsopoEnvError("DATABASE.CAPTCHA_GET_FAILED", {
23
+ context: {
24
+ failedFuncName: this.getCaptchaWithProof.name,
25
+ datasetId,
26
+ solved,
27
+ size
28
+ }
29
+ });
30
+ }
31
+ return captchaDocs;
32
+ }
33
+ async getRandomCaptchasAndRequestHash(datasetId, userAccount, ipAddress, captchaConfig, threshold, frictionlessTokenId) {
34
+ const dataset = await this.db.getDatasetDetails(datasetId);
35
+ if (!dataset) {
36
+ throw new common.ProsopoEnvError("DATABASE.DATASET_GET_FAILED", {
37
+ context: {
38
+ failedFuncName: this.getRandomCaptchasAndRequestHash.name,
39
+ dataset,
40
+ datasetId
41
+ }
42
+ });
43
+ }
44
+ const unsolvedCount = Math.abs(
45
+ Math.trunc(captchaConfig.unsolved.count)
46
+ );
47
+ const solvedCount = Math.abs(
48
+ Math.trunc(captchaConfig.solved.count)
49
+ );
50
+ if (!solvedCount) {
51
+ throw new common.ProsopoEnvError("CONFIG.INVALID_CAPTCHA_NUMBER");
52
+ }
53
+ const solved = await this.getCaptchaWithProof(datasetId, true, solvedCount);
54
+ let unsolved = [];
55
+ if (unsolvedCount) {
56
+ unsolved = await this.getCaptchaWithProof(
57
+ datasetId,
58
+ false,
59
+ unsolvedCount
60
+ );
61
+ }
62
+ const captchas = util.shuffleArray([...solved, ...unsolved]);
63
+ const salt = utilCrypto.randomAsHex();
64
+ const requestHash = datasets.computePendingRequestHash(
65
+ captchas.map((c) => c.captchaId),
66
+ userAccount,
67
+ salt
68
+ );
69
+ const currentTime = Date.now();
70
+ const signedRequestHash = util$1.u8aToHex(
71
+ this.pair.sign(util$1.stringToHex(requestHash))
72
+ );
73
+ const timeLimit = captchas.map((captcha) => captcha.timeLimitMs || types.DEFAULT_IMAGE_CAPTCHA_TIMEOUT).reduce((a, b) => a + b, 0);
74
+ const deadlineTs = timeLimit + currentTime;
75
+ await this.db.storePendingImageCommitment(
76
+ userAccount,
77
+ requestHash,
78
+ salt,
79
+ deadlineTs,
80
+ currentTime,
81
+ ipAddress.bigInt(),
82
+ threshold,
83
+ frictionlessTokenId
84
+ );
85
+ return {
86
+ captchas,
87
+ requestHash,
88
+ timestamp: currentTime,
89
+ signedRequestHash
90
+ };
91
+ }
92
+ /**
93
+ * Validate and store the text captcha solution(s) from the Dapp User in a web2 environment
94
+ * @param {string} userAccount
95
+ * @param {string} dappAccount
96
+ * @param {string} requestHash
97
+ * @param {JSON} captchas
98
+ * @param userTimestampSignature
99
+ * @param timestamp
100
+ * @param providerRequestHashSignature
101
+ * @param ipAddress
102
+ * @param headers
103
+ * @param threshold the percentage of captchas that must be correct to return true
104
+ * @return {Promise<DappUserSolutionResult>} result containing the contract event
105
+ */
106
+ async dappUserSolution(userAccount, dappAccount, requestHash, captchas, userTimestampSignature, timestamp, providerRequestHashSignature, ipAddress, headers, ja4) {
107
+ const verification = utilCrypto.signatureVerify(
108
+ util$1.stringToHex(timestamp.toString()),
109
+ userTimestampSignature,
110
+ userAccount
111
+ );
112
+ if (!verification.isValid) {
113
+ this.logger.info("Invalid user timestamp signature");
114
+ throw new common.ProsopoEnvError("GENERAL.INVALID_SIGNATURE", {
115
+ context: { failedFuncName: this.dappUserSolution.name, userAccount }
116
+ });
117
+ }
118
+ const providerRequestHashSignatureVerify = utilCrypto.signatureVerify(
119
+ util$1.stringToHex(requestHash.toString()),
120
+ providerRequestHashSignature,
121
+ this.pair.address
122
+ );
123
+ if (!providerRequestHashSignatureVerify.isValid) {
124
+ this.logger.info("Invalid provider requestHash signature");
125
+ throw new common.ProsopoEnvError("GENERAL.INVALID_SIGNATURE", {
126
+ context: {
127
+ failedFuncName: this.dappUserSolution.name,
128
+ userAccount,
129
+ error: "requestHash signature is invalid"
130
+ }
131
+ });
132
+ }
133
+ let response = {
134
+ captchas: [],
135
+ verified: false
136
+ };
137
+ const pendingRecord = await this.db.getPendingImageCommitment(requestHash);
138
+ const unverifiedCaptchaIds = captchas.map((captcha) => captcha.captchaId);
139
+ const pendingRequest = await this.validateDappUserSolutionRequestIsPending(
140
+ requestHash,
141
+ pendingRecord,
142
+ userAccount,
143
+ unverifiedCaptchaIds
144
+ );
145
+ if (pendingRequest) {
146
+ const { storedCaptchas, receivedCaptchas, captchaIds } = await this.validateReceivedCaptchasAgainstStoredCaptchas(captchas);
147
+ const { tree, commitmentId } = imgCaptchaTasksUtils.buildTreeAndGetCommitmentId(receivedCaptchas);
148
+ const datasetId = util$2.at(storedCaptchas, 0).datasetId;
149
+ if (!datasetId) {
150
+ throw new common.ProsopoEnvError("CAPTCHA.ID_MISMATCH", {
151
+ context: { failedFuncName: this.dappUserSolution.name }
152
+ });
153
+ }
154
+ await this.db.updatePendingImageCommitmentStatus(requestHash);
155
+ const commit = {
156
+ id: commitmentId,
157
+ userAccount,
158
+ dappAccount,
159
+ providerAccount: this.pair.address,
160
+ datasetId,
161
+ result: { status: types.CaptchaStatus.pending },
162
+ userSignature: userTimestampSignature,
163
+ userSubmitted: true,
164
+ serverChecked: false,
165
+ requestedAtTimestamp: timestamp,
166
+ ipAddress,
167
+ headers,
168
+ frictionlessTokenId: pendingRecord.frictionlessTokenId,
169
+ ja4
170
+ };
171
+ await this.db.storeUserImageCaptchaSolution(receivedCaptchas, commit);
172
+ const solutionRecords = await Promise.all(
173
+ storedCaptchas.map(async (captcha) => {
174
+ const solutionRecord = await this.db.getSolutionByCaptchaId(
175
+ captcha.captchaId
176
+ );
177
+ if (!solutionRecord) {
178
+ throw new common.ProsopoEnvError("CAPTCHA.SOLUTION_NOT_FOUND", {
179
+ context: { failedFuncName: this.dappUserSolution.name }
180
+ });
181
+ }
182
+ return solutionRecord;
183
+ })
184
+ );
185
+ const totalImages = storedCaptchas[0]?.items.length || 0;
186
+ if (datasets.compareCaptchaSolutions(
187
+ receivedCaptchas,
188
+ solutionRecords,
189
+ totalImages,
190
+ pendingRecord.threshold
191
+ )) {
192
+ response = {
193
+ captchas: captchaIds.map((id) => ({
194
+ captchaId: id,
195
+ proof: tree.proof(id)
196
+ })),
197
+ verified: true
198
+ };
199
+ await this.db.approveDappUserCommitment(commitmentId);
200
+ } else {
201
+ await this.db.disapproveDappUserCommitment(
202
+ commitmentId,
203
+ "CAPTCHA.INVALID_SOLUTION"
204
+ );
205
+ response = {
206
+ captchas: captchaIds.map((id) => ({
207
+ captchaId: id,
208
+ proof: [[]]
209
+ })),
210
+ verified: false
211
+ };
212
+ }
213
+ } else {
214
+ this.logger.info("Request hash not found");
215
+ }
216
+ return response;
217
+ }
218
+ /**
219
+ * Validate length of received captchas array matches length of captchas found in database
220
+ * Validate that the datasetId is the same for all captchas and is equal to the datasetId on the stored captchas
221
+ */
222
+ async validateReceivedCaptchasAgainstStoredCaptchas(captchas) {
223
+ const receivedCaptchas = datasets.parseAndSortCaptchaSolutions(captchas);
224
+ const captchaIds = receivedCaptchas.map((captcha) => captcha.captchaId);
225
+ const storedCaptchas = await this.db.getCaptchaById(captchaIds);
226
+ if (!storedCaptchas || receivedCaptchas.length !== storedCaptchas.length) {
227
+ throw new common.ProsopoEnvError("CAPTCHA.INVALID_CAPTCHA_ID", {
228
+ context: {
229
+ failedFuncName: this.validateReceivedCaptchasAgainstStoredCaptchas.name,
230
+ captchas
231
+ }
232
+ });
233
+ }
234
+ if (!storedCaptchas.every(
235
+ (captcha) => captcha.datasetId === util$2.at(storedCaptchas, 0).datasetId
236
+ )) {
237
+ throw new common.ProsopoEnvError("CAPTCHA.DIFFERENT_DATASET_IDS", {
238
+ context: {
239
+ failedFuncName: this.validateReceivedCaptchasAgainstStoredCaptchas.name,
240
+ captchas
241
+ }
242
+ });
243
+ }
244
+ return { storedCaptchas, receivedCaptchas, captchaIds };
245
+ }
246
+ /**
247
+ * Validate that a Dapp User is responding to their own pending captcha request
248
+ * @param {string} requestHash
249
+ * @param {PendingCaptchaRequest} pendingRecord
250
+ * @param {string} userAccount
251
+ * @param {string[]} captchaIds
252
+ */
253
+ async validateDappUserSolutionRequestIsPending(requestHash, pendingRecord, userAccount, captchaIds) {
254
+ const currentTime = Date.now();
255
+ if (!pendingRecord) {
256
+ this.logger.info("No pending record found");
257
+ return false;
258
+ }
259
+ if (pendingRecord.deadlineTimestamp < currentTime) {
260
+ this.logger.info("Deadline for responding to captcha has expired");
261
+ return false;
262
+ }
263
+ if (pendingRecord) {
264
+ const pendingHashComputed = datasets.computePendingRequestHash(
265
+ captchaIds,
266
+ userAccount,
267
+ pendingRecord.salt
268
+ );
269
+ return requestHash === pendingHashComputed;
270
+ }
271
+ return false;
272
+ }
273
+ /*
274
+ * Get dapp user solution from database
275
+ */
276
+ async getDappUserCommitmentById(commitmentId) {
277
+ const dappUserSolution = await this.db.getDappUserCommitmentById(commitmentId);
278
+ if (!dappUserSolution) {
279
+ throw new common.ProsopoEnvError("CAPTCHA.DAPP_USER_SOLUTION_NOT_FOUND", {
280
+ context: {
281
+ failedFuncName: this.getDappUserCommitmentById.name,
282
+ commitmentId
283
+ }
284
+ });
285
+ }
286
+ return dappUserSolution;
287
+ }
288
+ /* Check if dapp user has verified solution in cache */
289
+ async getDappUserCommitmentByAccount(userAccount, dappAccount) {
290
+ const dappUserSolutions = await this.db.getDappUserCommitmentByAccount(
291
+ userAccount,
292
+ dappAccount
293
+ );
294
+ if (dappUserSolutions.length > 0) {
295
+ for (const dappUserSolution of dappUserSolutions) {
296
+ if (dappUserSolution.result.status === types.CaptchaStatus.approved) {
297
+ return dappUserSolution;
298
+ }
299
+ }
300
+ }
301
+ return void 0;
302
+ }
303
+ async verifyImageCaptchaSolution(user, dapp, commitmentId, maxVerifiedTime) {
304
+ const solution = await (commitmentId ? this.getDappUserCommitmentById(commitmentId) : this.getDappUserCommitmentByAccount(user, dapp));
305
+ if (!solution) {
306
+ this.logger.debug("Not verified - no solution found");
307
+ return { status: "API.USER_NOT_VERIFIED_NO_SOLUTION", verified: false };
308
+ }
309
+ if (solution.serverChecked) {
310
+ return { status: "API.USER_ALREADY_VERIFIED", verified: false };
311
+ }
312
+ await this.db.markDappUserCommitmentsChecked([solution.id]);
313
+ if (solution.result.status === types.CaptchaStatus.disapproved) {
314
+ return { status: "API.USER_NOT_VERIFIED", verified: false };
315
+ }
316
+ maxVerifiedTime = maxVerifiedTime || 60 * 1e3;
317
+ if (maxVerifiedTime) {
318
+ const currentTime = Date.now();
319
+ const timeSinceCompletion = currentTime - solution.requestedAtTimestamp;
320
+ if (timeSinceCompletion > maxVerifiedTime) {
321
+ this.logger.debug("Not verified - timed out");
322
+ return {
323
+ status: "API.USER_NOT_VERIFIED_TIME_EXPIRED",
324
+ verified: false
325
+ };
326
+ }
327
+ }
328
+ const isApproved = solution.result.status === types.CaptchaStatus.approved;
329
+ let score;
330
+ if (solution.frictionlessTokenId) {
331
+ const tokenRecord = await this.db.getFrictionlessTokenRecordByTokenId(
332
+ solution.frictionlessTokenId
333
+ );
334
+ if (tokenRecord) {
335
+ score = frictionlessTasksUtils.computeFrictionlessScore(tokenRecord?.scoreComponents);
336
+ this.logger.info({
337
+ tscoreComponents: tokenRecord?.scoreComponents,
338
+ score
339
+ });
340
+ }
341
+ }
342
+ return {
343
+ status: isApproved ? "API.USER_VERIFIED" : "API.USER_NOT_VERIFIED",
344
+ verified: isApproved,
345
+ commitmentId: solution.id.toString(),
346
+ ...score && { score }
347
+ };
348
+ }
349
+ checkLangRules(acceptLanguage) {
350
+ return lang.checkLangRules(this.config, acceptLanguage);
351
+ }
352
+ getVerificationResponse(verified, clientRecord, translateFn, score, commitmentId) {
353
+ return {
354
+ ...super.getVerificationResponse(
355
+ verified,
356
+ clientRecord,
357
+ translateFn,
358
+ score
359
+ ),
360
+ ...commitmentId && {
361
+ [types.ApiParams.commitmentId]: commitmentId
362
+ }
363
+ };
364
+ }
365
+ }
366
+ exports.ImgCaptchaManager = ImgCaptchaManager;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const common = require("@prosopo/common");
4
+ const datasets = require("@prosopo/datasets");
5
+ const buildTreeAndGetCommitmentId = (captchaSolutions) => {
6
+ const tree = new datasets.CaptchaMerkleTree();
7
+ const solutionsHashed = captchaSolutions.map(
8
+ (captcha) => datasets.computeCaptchaSolutionHash(captcha)
9
+ );
10
+ tree.build(solutionsHashed);
11
+ const commitmentId = tree.root?.hash;
12
+ if (!commitmentId) {
13
+ throw new common.ProsopoEnvError(
14
+ "CONTRACT.CAPTCHA_SOLUTION_COMMITMENT_DOES_NOT_EXIST",
15
+ {
16
+ context: {
17
+ failedFuncName: buildTreeAndGetCommitmentId.name,
18
+ commitmentId
19
+ }
20
+ }
21
+ );
22
+ }
23
+ return { tree, commitmentId };
24
+ };
25
+ exports.buildTreeAndGetCommitmentId = buildTreeAndGetCommitmentId;
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const tasks = require("./tasks.cjs");
4
+ exports.Tasks = tasks.Tasks;
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const util = require("@polkadot/util");
4
+ const common = require("@prosopo/common");
5
+ const types = require("@prosopo/types");
6
+ const util$1 = require("@prosopo/util");
7
+ const captchaManager = require("../captchaManager.cjs");
8
+ const frictionlessTasksUtils = require("../frictionless/frictionlessTasksUtils.cjs");
9
+ const powTasksUtils = require("./powTasksUtils.cjs");
10
+ const DEFAULT_POW_DIFFICULTY = 4;
11
+ class PowCaptchaManager extends captchaManager.CaptchaManager {
12
+ constructor(db, pair, logger) {
13
+ super(db, pair, logger);
14
+ this.POW_SEPARATOR = types.POW_SEPARATOR;
15
+ }
16
+ /**
17
+ * @description Generates a PoW Captcha for a given user and dapp
18
+ *
19
+ * @param {string} userAccount - user that is solving the captcha
20
+ * @param {string} dappAccount - dapp that is requesting the captcha
21
+ * @param origin - not currently used
22
+ * @param powDifficulty
23
+ */
24
+ async getPowCaptchaChallenge(userAccount, dappAccount, origin, powDifficulty) {
25
+ const difficulty = powDifficulty || DEFAULT_POW_DIFFICULTY;
26
+ const requestedAtTimestamp = Date.now();
27
+ const nonce = Math.floor(Math.random() * 1e6);
28
+ const challenge = `${requestedAtTimestamp}___${userAccount}___${dappAccount}___${nonce}`;
29
+ const challengeSignature = util.u8aToHex(this.pair.sign(util.stringToHex(challenge)));
30
+ return {
31
+ challenge,
32
+ difficulty,
33
+ providerSignature: challengeSignature,
34
+ requestedAtTimestamp
35
+ };
36
+ }
37
+ /**
38
+ * @description Verifies a PoW Captcha for a given user and dapp
39
+ *
40
+ * @param {string} challenge - the starting string for the PoW challenge
41
+ * @param {string} difficulty - how many leading zeroes the solution must have
42
+ * @param {string} providerChallengeSignature - proof that the Provider provided the challenge
43
+ * @param {string} nonce - the string that the user has found that satisfies the PoW challenge
44
+ * @param {number} timeout - the time in milliseconds since the Provider was selected to provide the PoW captcha
45
+ * @param {string} userTimestampSignature
46
+ * @param ipAddress
47
+ * @param headers
48
+ */
49
+ async verifyPowCaptchaSolution(challenge, difficulty, providerChallengeSignature, nonce, timeout, userTimestampSignature, ipAddress, headers) {
50
+ powTasksUtils.checkPowSignature(
51
+ challenge,
52
+ providerChallengeSignature,
53
+ this.pair.address,
54
+ types.ApiParams.challenge
55
+ );
56
+ const challengeSplit = challenge.split(this.POW_SEPARATOR);
57
+ const timestamp = Number.parseInt(util$1.at(challengeSplit, 0));
58
+ const userAccount = util$1.at(challengeSplit, 1);
59
+ powTasksUtils.checkPowSignature(
60
+ timestamp.toString(),
61
+ userTimestampSignature,
62
+ userAccount,
63
+ types.ApiParams.timestamp
64
+ );
65
+ const challengeRecord = await this.db.getPowCaptchaRecordByChallenge(challenge);
66
+ if (!challengeRecord) {
67
+ this.logger.debug("No record of this challenge");
68
+ return false;
69
+ }
70
+ if (!util$1.verifyRecency(challenge, timeout)) {
71
+ await this.db.updatePowCaptchaRecord(
72
+ challenge,
73
+ {
74
+ status: types.CaptchaStatus.disapproved,
75
+ reason: "CAPTCHA.INVALID_TIMESTAMP"
76
+ },
77
+ false,
78
+ //serverchecked
79
+ true,
80
+ // usersubmitted
81
+ userTimestampSignature
82
+ );
83
+ return false;
84
+ }
85
+ const correct = powTasksUtils.validateSolution(nonce, challenge, difficulty);
86
+ let result = { status: types.CaptchaStatus.approved };
87
+ if (!correct) {
88
+ result = {
89
+ status: types.CaptchaStatus.disapproved,
90
+ reason: "CAPTCHA.INVALID_SOLUTION"
91
+ };
92
+ }
93
+ await this.db.updatePowCaptchaRecord(
94
+ challenge,
95
+ result,
96
+ false,
97
+ true,
98
+ userTimestampSignature
99
+ );
100
+ return correct;
101
+ }
102
+ /**
103
+ * @description Verifies a PoW Captcha for a given user and dapp. This is called by the server to verify the user's solution
104
+ * and update the record in the database to show that the user has solved the captcha
105
+ *
106
+ * @param {string} dappAccount - the dapp that is requesting the captcha
107
+ * @param {string} challenge - the starting string for the PoW challenge
108
+ * @param {number} timeout - the time in milliseconds since the Provider was selected to provide the PoW captcha
109
+ */
110
+ async serverVerifyPowCaptchaSolution(dappAccount, challenge, timeout) {
111
+ const challengeRecord = await this.db.getPowCaptchaRecordByChallenge(challenge);
112
+ if (!challengeRecord) {
113
+ this.logger.debug(`No record of this challenge: ${challenge}`);
114
+ return { verified: false };
115
+ }
116
+ if (challengeRecord.result.status !== types.CaptchaStatus.approved) {
117
+ throw new common.ProsopoApiError("CAPTCHA.INVALID_SOLUTION", {
118
+ context: {
119
+ failedFuncName: this.serverVerifyPowCaptchaSolution.name,
120
+ challenge
121
+ }
122
+ });
123
+ }
124
+ if (challengeRecord.serverChecked) return { verified: false };
125
+ const challengeDappAccount = challengeRecord.dappAccount;
126
+ if (dappAccount !== challengeDappAccount) {
127
+ throw new common.ProsopoEnvError("CAPTCHA.DAPP_USER_SOLUTION_NOT_FOUND", {
128
+ context: {
129
+ failedFuncName: this.serverVerifyPowCaptchaSolution.name,
130
+ dappAccount,
131
+ challengeDappAccount
132
+ }
133
+ });
134
+ }
135
+ util$1.verifyRecency(challenge, timeout);
136
+ await this.db.markDappUserPoWCommitmentsChecked([
137
+ challengeRecord.challenge
138
+ ]);
139
+ let score;
140
+ if (challengeRecord.frictionlessTokenId) {
141
+ const tokenRecord = await this.db.getFrictionlessTokenRecordByTokenId(
142
+ challengeRecord.frictionlessTokenId
143
+ );
144
+ if (tokenRecord) {
145
+ score = frictionlessTasksUtils.computeFrictionlessScore(tokenRecord?.scoreComponents);
146
+ this.logger.info({
147
+ tscoreComponents: tokenRecord?.scoreComponents,
148
+ score
149
+ });
150
+ }
151
+ }
152
+ return { verified: true, ...score ? { score } : {} };
153
+ }
154
+ }
155
+ exports.PowCaptchaManager = PowCaptchaManager;
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const sha256 = require("@noble/hashes/sha256");
4
+ const util = require("@polkadot/util");
5
+ const utilCrypto = require("@polkadot/util-crypto");
6
+ const common = require("@prosopo/common");
7
+ const validateSolution = (nonce, challenge, difficulty) => Array.from(sha256.sha256(new TextEncoder().encode(nonce + challenge))).map((byte) => byte.toString(16).padStart(2, "0")).join("").startsWith("0".repeat(difficulty));
8
+ const checkPowSignature = (challenge, signature, address, signatureType) => {
9
+ const signatureVerification = utilCrypto.signatureVerify(
10
+ util.stringToHex(challenge),
11
+ signature,
12
+ address
13
+ );
14
+ if (!signatureVerification.isValid) {
15
+ throw new common.ProsopoContractError("GENERAL.INVALID_SIGNATURE", {
16
+ context: {
17
+ ERROR: `Signature is invalid for this message: ${signatureType}`,
18
+ failedFuncName: checkPowSignature.name,
19
+ signature,
20
+ signatureType
21
+ }
22
+ });
23
+ }
24
+ };
25
+ exports.checkPowSignature = checkPowSignature;
26
+ exports.validateSolution = validateSolution;
@@ -0,0 +1,51 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const common = require("@prosopo/common");
4
+ const clientTasks = require("./client/clientTasks.cjs");
5
+ const datasetTasks = require("./dataset/datasetTasks.cjs");
6
+ const frictionlessTasks = require("./frictionless/frictionlessTasks.cjs");
7
+ const imgCaptchaTasks = require("./imgCaptcha/imgCaptchaTasks.cjs");
8
+ const powTasks = require("./powCaptcha/powTasks.cjs");
9
+ class Tasks {
10
+ constructor(env) {
11
+ this.config = env.config;
12
+ this.db = env.getDb();
13
+ this.captchaConfig = env.config.captchas;
14
+ this.logger = common.getLogger(env.config.logLevel, "Tasks");
15
+ if (!env.pair) {
16
+ throw new common.ProsopoEnvError("DEVELOPER.MISSING_PROVIDER_PAIR", {
17
+ context: { failedFuncName: "Tasks.constructor" }
18
+ });
19
+ }
20
+ this.pair = env.pair;
21
+ this.powCaptchaManager = new powTasks.PowCaptchaManager(
22
+ this.db,
23
+ this.pair,
24
+ this.logger
25
+ );
26
+ this.datasetManager = new datasetTasks.DatasetManager(
27
+ this.config,
28
+ this.logger,
29
+ this.captchaConfig,
30
+ this.db
31
+ );
32
+ this.imgCaptchaManager = new imgCaptchaTasks.ImgCaptchaManager(
33
+ this.db,
34
+ this.pair,
35
+ this.config,
36
+ this.logger
37
+ );
38
+ this.clientTaskManager = new clientTasks.ClientTaskManager(
39
+ this.config,
40
+ this.logger,
41
+ this.db
42
+ );
43
+ this.frictionlessManager = new frictionlessTasks.FrictionlessManager(
44
+ this.db,
45
+ this.pair,
46
+ this.config,
47
+ this.logger
48
+ );
49
+ }
50
+ }
51
+ exports.Tasks = Tasks;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const address = require("@polkadot/util-crypto/address");
4
+ const hex = require("@polkadot/util/hex");
5
+ const is = require("@polkadot/util/is");
6
+ const common = require("@prosopo/common");
7
+ const types = require("@prosopo/types");
8
+ const util = require("@prosopo/util");
9
+ const ipAddress = require("ip-address");
10
+ function encodeStringAddress(address$1) {
11
+ try {
12
+ return address.encodeAddress(
13
+ is.isHex(address$1) ? hex.hexToU8a(address$1) : address.decodeAddress(address$1)
14
+ );
15
+ } catch (err) {
16
+ throw new common.ProsopoContractError("CONTRACT.INVALID_ADDRESS", {
17
+ context: { address: address$1 }
18
+ });
19
+ }
20
+ }
21
+ function shuffleArray(array) {
22
+ for (let arrayIndex = array.length - 1; arrayIndex > 0; arrayIndex--) {
23
+ const randIndex = Math.floor(Math.random() * (arrayIndex + 1));
24
+ const tmp = util.at(array, randIndex);
25
+ array[randIndex] = util.at(array, arrayIndex);
26
+ array[arrayIndex] = tmp;
27
+ }
28
+ return array;
29
+ }
30
+ async function checkIfTaskIsRunning(taskName, db) {
31
+ const runningTask = await db.getLastScheduledTaskStatus(
32
+ taskName,
33
+ types.ScheduledTaskStatus.Running
34
+ );
35
+ const twoMinutesAgo = (/* @__PURE__ */ new Date()).getTime() - 1e3 * 60 * 2;
36
+ if (runningTask && runningTask.datetime > twoMinutesAgo) {
37
+ const completedTask = await db.getScheduledTaskStatus(
38
+ runningTask._id,
39
+ types.ScheduledTaskStatus.Completed
40
+ );
41
+ return !completedTask;
42
+ }
43
+ return false;
44
+ }
45
+ const getIPAddress = (ipAddressString) => {
46
+ try {
47
+ if (ipAddressString.match(/^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/)) {
48
+ return new ipAddress.Address4(ipAddressString);
49
+ }
50
+ return new ipAddress.Address6(ipAddressString);
51
+ } catch (e) {
52
+ throw new common.ProsopoEnvError("API.INVALID_IP");
53
+ }
54
+ };
55
+ exports.checkIfTaskIsRunning = checkIfTaskIsRunning;
56
+ exports.encodeStringAddress = encodeStringAddress;
57
+ exports.getIPAddress = getIPAddress;
58
+ exports.shuffleArray = shuffleArray;
@@ -1,4 +1,4 @@
1
1
  import type { KeyringPair } from "@polkadot/keyring/types";
2
2
  import { type ProsopoConfigOutput } from "@prosopo/types";
3
- export declare function storeCaptchasExternally(pair: KeyringPair, config: ProsopoConfigOutput): Promise<void>;
3
+ export declare function storeCaptchasExternally(pair: KeyringPair, cronSchedule: string, config: ProsopoConfigOutput): Promise<void>;
4
4
  //# sourceMappingURL=captchaScheduler.d.ts.map