@prosopo/database 2.5.5 → 2.6.1

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.
@@ -0,0 +1,1230 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const is = require("@polkadot/util/is");
4
+ const common = require("@prosopo/common");
5
+ const types = require("@prosopo/types");
6
+ const typesDatabase = require("@prosopo/types-database");
7
+ const userAccessPolicy = require("@prosopo/user-access-policy");
8
+ const mongo = require("../base/mongo.cjs");
9
+ var TableNames = /* @__PURE__ */ ((TableNames2) => {
10
+ TableNames2["captcha"] = "captcha";
11
+ TableNames2["dataset"] = "dataset";
12
+ TableNames2["solution"] = "solution";
13
+ TableNames2["commitment"] = "commitment";
14
+ TableNames2["usersolution"] = "usersolution";
15
+ TableNames2["pending"] = "pending";
16
+ TableNames2["scheduler"] = "scheduler";
17
+ TableNames2["powcaptcha"] = "powcaptcha";
18
+ TableNames2["client"] = "client";
19
+ TableNames2["frictionlessToken"] = "frictionlessToken";
20
+ TableNames2["session"] = "session";
21
+ TableNames2["userAccessRules"] = "userAccessRules";
22
+ TableNames2["detector"] = "detector";
23
+ return TableNames2;
24
+ })(TableNames || {});
25
+ const PROVIDER_TABLES = [
26
+ {
27
+ collectionName: "captcha",
28
+ modelName: "Captcha",
29
+ schema: typesDatabase.CaptchaRecordSchema
30
+ },
31
+ {
32
+ collectionName: "powcaptcha",
33
+ modelName: "PowCaptcha",
34
+ schema: typesDatabase.PoWCaptchaRecordSchema
35
+ },
36
+ {
37
+ collectionName: "dataset",
38
+ modelName: "Dataset",
39
+ schema: typesDatabase.DatasetRecordSchema
40
+ },
41
+ {
42
+ collectionName: "solution",
43
+ modelName: "Solution",
44
+ schema: typesDatabase.SolutionRecordSchema
45
+ },
46
+ {
47
+ collectionName: "commitment",
48
+ modelName: "UserCommitment",
49
+ schema: typesDatabase.UserCommitmentRecordSchema
50
+ },
51
+ {
52
+ collectionName: "usersolution",
53
+ modelName: "UserSolution",
54
+ schema: typesDatabase.UserSolutionRecordSchema
55
+ },
56
+ {
57
+ collectionName: "pending",
58
+ modelName: "Pending",
59
+ schema: typesDatabase.PendingRecordSchema
60
+ },
61
+ {
62
+ collectionName: "scheduler",
63
+ modelName: "Scheduler",
64
+ schema: typesDatabase.ScheduledTaskRecordSchema
65
+ },
66
+ {
67
+ collectionName: "client",
68
+ modelName: "Client",
69
+ schema: typesDatabase.ClientRecordSchema
70
+ },
71
+ {
72
+ collectionName: "frictionlessToken",
73
+ modelName: "FrictionlessToken",
74
+ schema: typesDatabase.FrictionlessTokenRecordSchema
75
+ },
76
+ {
77
+ collectionName: "session",
78
+ modelName: "Session",
79
+ schema: typesDatabase.SessionRecordSchema
80
+ },
81
+ {
82
+ collectionName: "userAccessRules",
83
+ modelName: "UserAccessRules",
84
+ schema: userAccessPolicy.getRuleMongooseSchema()
85
+ },
86
+ {
87
+ collectionName: "detector",
88
+ modelName: "Detector",
89
+ schema: typesDatabase.DetectorRecordSchema
90
+ }
91
+ ];
92
+ class ProviderDatabase extends mongo.MongoDatabase {
93
+ constructor(url, dbname, authSource, logger) {
94
+ super(url, dbname, authSource, logger);
95
+ this.tables = {};
96
+ this.tables = {};
97
+ this.userAccessRulesDbStorage = null;
98
+ }
99
+ async connect() {
100
+ await super.connect();
101
+ this.loadTables();
102
+ this.userAccessRulesDbStorage = userAccessPolicy.createMongooseRulesStorage(
103
+ this.logger,
104
+ this.tables.userAccessRules
105
+ );
106
+ }
107
+ loadTables() {
108
+ const tables = {};
109
+ PROVIDER_TABLES.map(({ collectionName, modelName, schema }) => {
110
+ if (this.connection) {
111
+ tables[collectionName] = this.connection.model(modelName, schema);
112
+ }
113
+ });
114
+ this.tables = tables;
115
+ }
116
+ getTables() {
117
+ if (!this.tables) {
118
+ throw new common.ProsopoDBError("DATABASE.TABLES_UNDEFINED", {
119
+ context: { failedFuncName: this.getTables.name },
120
+ logger: this.logger
121
+ });
122
+ }
123
+ return this.tables;
124
+ }
125
+ getUserAccessRulesStorage() {
126
+ if (null === this.userAccessRulesDbStorage) {
127
+ throw new common.ProsopoDBError("DATABASE.USER_ACCESS_RULES_UNDEFINED");
128
+ }
129
+ return this.userAccessRulesDbStorage;
130
+ }
131
+ /**
132
+ * @description Load a dataset to the database
133
+ * @param {Dataset} dataset
134
+ */
135
+ async storeDataset(dataset) {
136
+ try {
137
+ this.logger.debug("Storing dataset in database");
138
+ const parsedDataset = types.DatasetWithIdsAndTreeSchema.parse(dataset);
139
+ const datasetDoc = {
140
+ datasetId: parsedDataset.datasetId,
141
+ datasetContentId: parsedDataset.datasetContentId,
142
+ format: parsedDataset.format,
143
+ contentTree: parsedDataset.contentTree,
144
+ solutionTree: parsedDataset.solutionTree
145
+ };
146
+ const filter = {
147
+ datasetId: parsedDataset.datasetId
148
+ };
149
+ await this.tables.dataset?.updateOne(
150
+ filter,
151
+ { $set: datasetDoc },
152
+ { upsert: true }
153
+ );
154
+ const captchaDocs = parsedDataset.captchas.map(
155
+ ({ solution, ...captcha }, index) => ({
156
+ ...captcha,
157
+ datasetId: parsedDataset.datasetId,
158
+ datasetContentId: parsedDataset.datasetContentId,
159
+ index,
160
+ solved: !!solution?.length
161
+ })
162
+ );
163
+ this.logger.debug("Inserting captcha records");
164
+ if (captchaDocs.length) {
165
+ await this.tables?.captcha.bulkWrite(
166
+ captchaDocs.map((captchaDoc) => ({
167
+ updateOne: {
168
+ filter: { captchaId: captchaDoc.captchaId },
169
+ update: { $set: captchaDoc },
170
+ upsert: true
171
+ }
172
+ }))
173
+ );
174
+ }
175
+ const captchaSolutionDocs = parsedDataset.captchas.filter(({ solution }) => solution?.length).map((captcha) => ({
176
+ captchaId: captcha.captchaId,
177
+ captchaContentId: captcha.captchaContentId,
178
+ solution: captcha.solution,
179
+ salt: captcha.salt,
180
+ datasetId: parsedDataset.datasetId,
181
+ datasetContentId: parsedDataset.datasetContentId
182
+ }));
183
+ this.logger.debug("Inserting solution records");
184
+ if (captchaSolutionDocs.length) {
185
+ await this.tables?.solution.bulkWrite(
186
+ captchaSolutionDocs.map((captchaSolutionDoc) => ({
187
+ updateOne: {
188
+ filter: { captchaId: captchaSolutionDoc.captchaId },
189
+ update: { $set: captchaSolutionDoc },
190
+ upsert: true
191
+ }
192
+ }))
193
+ );
194
+ }
195
+ this.logger.debug("Dataset stored in database");
196
+ } catch (err) {
197
+ throw new common.ProsopoDBError("DATABASE.DATASET_LOAD_FAILED", {
198
+ context: { failedFuncName: this.storeDataset.name, error: err },
199
+ logger: this.logger
200
+ });
201
+ }
202
+ }
203
+ /** @description Get solutions for a dataset
204
+ * @param {string} datasetId
205
+ */
206
+ async getSolutions(datasetId) {
207
+ const filter = { datasetId };
208
+ const docs = await this.tables?.solution.find(filter).lean();
209
+ return docs ? docs : [];
210
+ }
211
+ /** @description Get a solution by captcha id
212
+ * @param {string} captchaId
213
+ */
214
+ async getSolutionByCaptchaId(captchaId) {
215
+ const filter = { captchaId };
216
+ const doc = await this.tables?.solution.findOne(filter).lean();
217
+ return doc || null;
218
+ }
219
+ /** @description Get a dataset from the database
220
+ * @param {string} datasetId
221
+ */
222
+ async getDataset(datasetId) {
223
+ const filter = { datasetId };
224
+ const datasetDoc = await this.tables?.dataset.findOne(filter).lean();
225
+ if (datasetDoc) {
226
+ const { datasetContentId, format, contentTree, solutionTree } = datasetDoc;
227
+ const captchas = await this.tables?.captcha.find(filter).lean() || [];
228
+ const solutions = await this.tables?.solution.find(filter).lean() || [];
229
+ const solutionsKeyed = {};
230
+ for (const solution of solutions) {
231
+ solutionsKeyed[solution.captchaId] = solution;
232
+ }
233
+ return {
234
+ datasetId,
235
+ datasetContentId,
236
+ format,
237
+ contentTree: contentTree || [],
238
+ solutionTree: solutionTree || [],
239
+ captchas: captchas.map((captchaDoc) => {
240
+ const { captchaId, captchaContentId, items, target, salt, solved } = captchaDoc;
241
+ const solution = solutionsKeyed[captchaId];
242
+ return {
243
+ captchaId,
244
+ captchaContentId,
245
+ solved: !!solved,
246
+ salt,
247
+ items,
248
+ target,
249
+ solution: solved && solution ? solution.solution : []
250
+ };
251
+ })
252
+ };
253
+ }
254
+ throw new common.ProsopoDBError("DATABASE.DATASET_GET_FAILED", {
255
+ context: { failedFuncName: this.getDataset.name, datasetId }
256
+ });
257
+ }
258
+ /**
259
+ * @description Get random captchas that are solved or not solved
260
+ * @param {boolean} solved `true` when captcha is solved
261
+ * @param {string} datasetId the id of the data set
262
+ * @param {number} size the number of records to be returned
263
+ */
264
+ async getRandomCaptcha(solved, datasetId, size) {
265
+ if (!is.isHex(datasetId)) {
266
+ throw new common.ProsopoDBError("DATABASE.INVALID_HASH", {
267
+ context: { failedFuncName: this.getRandomCaptcha.name, datasetId }
268
+ });
269
+ }
270
+ const sampleSize = size ? Math.abs(Math.trunc(size)) : 1;
271
+ const filter = { datasetId, solved };
272
+ const cursor = this.tables?.captcha.aggregate([
273
+ { $match: filter },
274
+ { $sample: { size: sampleSize } },
275
+ {
276
+ $project: {
277
+ datasetId: 1,
278
+ datasetContentId: 1,
279
+ captchaId: 1,
280
+ captchaContentId: 1,
281
+ items: 1,
282
+ target: 1
283
+ }
284
+ }
285
+ ]);
286
+ const docs = await cursor;
287
+ if (docs?.length) {
288
+ return docs.map(({ _id, ...keepAttrs }) => keepAttrs);
289
+ }
290
+ throw new common.ProsopoDBError("DATABASE.CAPTCHA_GET_FAILED", {
291
+ context: {
292
+ failedFuncName: this.getRandomCaptcha.name,
293
+ solved,
294
+ datasetId,
295
+ size
296
+ }
297
+ });
298
+ }
299
+ /**
300
+ * @description Get captchas by id
301
+ * @param {string[]} captchaId
302
+ */
303
+ async getCaptchaById(captchaId) {
304
+ const filter = { captchaId: { $in: captchaId } };
305
+ const cursor = this.tables?.captcha.find(filter).lean();
306
+ const docs = await cursor;
307
+ if (docs?.length) {
308
+ return docs.map(({ _id, ...keepAttrs }) => keepAttrs);
309
+ }
310
+ throw new common.ProsopoDBError("DATABASE.CAPTCHA_GET_FAILED", {
311
+ context: { failedFuncName: this.getCaptchaById.name, captchaId }
312
+ });
313
+ }
314
+ /**
315
+ * @description Update a captcha
316
+ * @param {Captcha} captcha
317
+ * @param {string} datasetId the id of the data set
318
+ */
319
+ async updateCaptcha(captcha, datasetId) {
320
+ if (!is.isHex(datasetId)) {
321
+ throw new common.ProsopoDBError("DATABASE.INVALID_HASH", {
322
+ context: { failedFuncName: this.updateCaptcha.name, datasetId }
323
+ });
324
+ }
325
+ try {
326
+ const filter = { datasetId };
327
+ await this.tables?.captcha.updateOne(
328
+ filter,
329
+ { $set: captcha },
330
+ { upsert: false }
331
+ );
332
+ } catch (err) {
333
+ throw new common.ProsopoDBError("DATABASE.CAPTCHA_UPDATE_FAILED", {
334
+ context: { failedFuncName: this.getDatasetDetails.name, error: err }
335
+ });
336
+ }
337
+ }
338
+ /**
339
+ * @description Remove captchas
340
+ */
341
+ async removeCaptchas(captchaIds) {
342
+ const filter = { captchaId: { $in: captchaIds } };
343
+ await this.tables?.captcha.deleteMany(filter);
344
+ }
345
+ /**
346
+ * @description Get a dataset by Id
347
+ */
348
+ async getDatasetDetails(datasetId) {
349
+ if (!is.isHex(datasetId)) {
350
+ throw new common.ProsopoDBError("DATABASE.INVALID_HASH", {
351
+ context: { failedFuncName: this.getDatasetDetails.name, datasetId }
352
+ });
353
+ }
354
+ const filter = { datasetId };
355
+ const doc = await this.tables?.dataset.findOne(filter).lean();
356
+ if (doc) {
357
+ return doc;
358
+ }
359
+ throw new common.ProsopoDBError("DATABASE.DATASET_GET_FAILED", {
360
+ context: {
361
+ failedFuncName: this.getDatasetDetails.name,
362
+ datasetId
363
+ }
364
+ });
365
+ }
366
+ /**
367
+ * @description Store a Dapp User's captcha solution commitment
368
+ */
369
+ async storeUserImageCaptchaSolution(captchas, commit) {
370
+ const commitmentRecord = typesDatabase.UserCommitmentSchema.parse({
371
+ ...commit,
372
+ lastUpdatedTimestamp: Date.now()
373
+ });
374
+ if (captchas.length) {
375
+ const filter = {
376
+ id: commit.id
377
+ };
378
+ await this.tables?.commitment.updateOne(filter, commitmentRecord, {
379
+ upsert: true
380
+ });
381
+ const ops = captchas.map((captcha) => ({
382
+ updateOne: {
383
+ filter: {
384
+ commitmentId: commit.id,
385
+ captchaId: captcha.captchaId
386
+ },
387
+ update: {
388
+ $set: {
389
+ captchaId: captcha.captchaId,
390
+ captchaContentId: captcha.captchaContentId,
391
+ salt: captcha.salt,
392
+ solution: captcha.solution,
393
+ commitmentId: commit.id,
394
+ processed: false
395
+ }
396
+ },
397
+ upsert: true
398
+ }
399
+ }));
400
+ await this.tables?.usersolution.bulkWrite(ops);
401
+ }
402
+ }
403
+ /**
404
+ * @description Adds a new PoW Captcha record to the database.
405
+ * @param {string} challenge The challenge string for the captcha.
406
+ * @param components The components of the PoW challenge.
407
+ * @param difficulty
408
+ * @param providerSignature
409
+ * @param ipAddress
410
+ * @param headers
411
+ * @param ja4
412
+ * @param frictionlessTokenId
413
+ * @param serverChecked
414
+ * @param userSubmitted
415
+ * @param storedStatus
416
+ * @param userSignature
417
+ * @returns {Promise<void>} A promise that resolves when the record is added.
418
+ */
419
+ async storePowCaptchaRecord(challenge, components, difficulty, providerSignature, ipAddress, headers, ja4, frictionlessTokenId, serverChecked = false, userSubmitted = false, storedStatus = typesDatabase.StoredStatusNames.notStored, userSignature) {
420
+ const tables = this.getTables();
421
+ const powCaptchaRecord = {
422
+ challenge,
423
+ ...components,
424
+ ipAddress,
425
+ headers,
426
+ ja4,
427
+ result: { status: types.CaptchaStatus.pending },
428
+ userSubmitted,
429
+ serverChecked,
430
+ difficulty,
431
+ providerSignature,
432
+ userSignature,
433
+ lastUpdatedTimestamp: Date.now(),
434
+ frictionlessTokenId
435
+ };
436
+ try {
437
+ await tables.powcaptcha.create(powCaptchaRecord);
438
+ this.logger.info("PowCaptcha record added successfully", {
439
+ challenge,
440
+ userSubmitted,
441
+ serverChecked,
442
+ storedStatus
443
+ });
444
+ } catch (error) {
445
+ this.logger.error("Failed to add PowCaptcha record", {
446
+ error,
447
+ challenge,
448
+ userSubmitted,
449
+ serverChecked,
450
+ storedStatus
451
+ });
452
+ throw new common.ProsopoDBError("DATABASE.CAPTCHA_UPDATE_FAILED", {
453
+ context: {
454
+ error,
455
+ challenge,
456
+ userSubmitted,
457
+ serverChecked,
458
+ storedStatus
459
+ },
460
+ logger: this.logger
461
+ });
462
+ }
463
+ }
464
+ /**
465
+ * @description Retrieves a PoW Captcha record by its challenge string.
466
+ * @param {string} challenge The challenge string to search for.
467
+ * @returns {Promise<PoWCaptchaRecord | null>} A promise that resolves with the found record or null if not found.
468
+ */
469
+ async getPowCaptchaRecordByChallenge(challenge) {
470
+ if (!this.tables) {
471
+ throw new common.ProsopoDBError("DATABASE.DATABASE_UNDEFINED", {
472
+ context: { failedFuncName: this.getPowCaptchaRecordByChallenge.name },
473
+ logger: this.logger
474
+ });
475
+ }
476
+ try {
477
+ const filter = { challenge };
478
+ const record = await this.tables.powcaptcha.findOne(filter).lean();
479
+ if (record) {
480
+ this.logger.info("PowCaptcha record retrieved successfully", {
481
+ challenge
482
+ });
483
+ return record;
484
+ }
485
+ this.logger.info("No PowCaptcha record found", { challenge });
486
+ return null;
487
+ } catch (error) {
488
+ this.logger.error("Failed to retrieve PowCaptcha record", {
489
+ error,
490
+ challenge
491
+ });
492
+ throw new common.ProsopoDBError("DATABASE.CAPTCHA_GET_FAILED", {
493
+ context: { error, challenge },
494
+ logger: this.logger
495
+ });
496
+ }
497
+ }
498
+ /**
499
+ * @description Updates a PoW Captcha record in the database.
500
+ * @param {string} challenge The challenge string of the captcha to be updated.
501
+ * @param result
502
+ * @param serverChecked
503
+ * @param userSubmitted
504
+ * @param userSignature
505
+ * @returns {Promise<void>} A promise that resolves when the record is updated.
506
+ */
507
+ async updatePowCaptchaRecord(challenge, result, serverChecked = false, userSubmitted = false, userSignature) {
508
+ const tables = this.getTables();
509
+ const timestamp = Date.now();
510
+ const update = {
511
+ result,
512
+ serverChecked,
513
+ userSubmitted,
514
+ userSignature,
515
+ lastUpdatedTimestamp: timestamp
516
+ };
517
+ try {
518
+ const updateResult = await tables.powcaptcha.updateOne(
519
+ { challenge },
520
+ {
521
+ $set: update
522
+ }
523
+ );
524
+ if (updateResult.matchedCount === 0) {
525
+ this.logger.info("No PowCaptcha record found to update", {
526
+ challenge,
527
+ ...update
528
+ });
529
+ throw new common.ProsopoDBError("DATABASE.CAPTCHA_GET_FAILED", {
530
+ context: {
531
+ challenge,
532
+ ...update
533
+ },
534
+ logger: this.logger
535
+ });
536
+ }
537
+ this.logger.info("PowCaptcha record updated successfully", {
538
+ challenge,
539
+ ...update
540
+ });
541
+ } catch (error) {
542
+ this.logger.error("Failed to update PowCaptcha record", {
543
+ error,
544
+ challenge,
545
+ ...update
546
+ });
547
+ throw new common.ProsopoDBError("DATABASE.CAPTCHA_UPDATE_FAILED", {
548
+ context: {
549
+ error,
550
+ challenge,
551
+ ...update
552
+ },
553
+ logger: this.logger
554
+ });
555
+ }
556
+ }
557
+ /** @description Get serverChecked Dapp User image captcha commitments from the commitments table
558
+ */
559
+ async getCheckedDappUserCommitments() {
560
+ const filter = { [typesDatabase.StoredStatusNames.serverChecked]: true };
561
+ const docs = await this.tables?.commitment.find(filter).lean();
562
+ return docs || [];
563
+ }
564
+ /** @description Get Dapp User captcha commitments from the commitments table that have not been counted towards the
565
+ * client's total
566
+ */
567
+ async getUnstoredDappUserCommitments(limit = 1e3, skip = 0) {
568
+ const filterNoStoredTimestamp = { storedAtTimestamp: { $exists: false } };
569
+ const docs = await this.tables?.commitment.aggregate([
570
+ {
571
+ $match: {
572
+ $or: [
573
+ filterNoStoredTimestamp,
574
+ {
575
+ $expr: {
576
+ $lt: ["$storedAtTimestamp", "$lastUpdatedTimestamp"]
577
+ }
578
+ }
579
+ ]
580
+ }
581
+ },
582
+ {
583
+ $sort: { _id: 1 }
584
+ },
585
+ {
586
+ $skip: skip
587
+ },
588
+ {
589
+ $limit: limit
590
+ }
591
+ ]);
592
+ return docs || [];
593
+ }
594
+ /** @description Mark a list of captcha commits as stored
595
+ */
596
+ async markDappUserCommitmentsStored(commitmentIds) {
597
+ const updateDoc = {
598
+ storedAtTimestamp: Date.now()
599
+ };
600
+ await this.tables?.commitment.updateMany(
601
+ { id: { $in: commitmentIds } },
602
+ { $set: updateDoc },
603
+ { upsert: false }
604
+ );
605
+ }
606
+ /** @description Mark a list of captcha commits as checked
607
+ */
608
+ async markDappUserCommitmentsChecked(commitmentIds) {
609
+ const updateDoc = {
610
+ [typesDatabase.StoredStatusNames.serverChecked]: true,
611
+ lastUpdatedTimestamp: Date.now()
612
+ };
613
+ await this.tables?.commitment.updateMany(
614
+ { id: { $in: commitmentIds } },
615
+ { $set: updateDoc },
616
+ { upsert: false }
617
+ );
618
+ }
619
+ /**
620
+ * @description Get Dapp User PoW captcha commitments that have not been counted towards the client's total
621
+ * @param {number} limit Maximum number of records to return
622
+ * @param {number} skip Number of records to skip (for pagination)
623
+ * @returns {Promise<PoWCaptchaRecord[]>} Array of PoW captcha records
624
+ */
625
+ async getUnstoredDappUserPoWCommitments(limit = 1e3, skip = 0) {
626
+ const filterNoStoredTimestamp = { storedAtTimestamp: { $exists: false } };
627
+ const docs = await this.tables?.powcaptcha.aggregate([
628
+ {
629
+ $match: {
630
+ $or: [
631
+ filterNoStoredTimestamp,
632
+ {
633
+ $expr: {
634
+ $lt: [
635
+ {
636
+ $convert: {
637
+ input: "$storedAtTimestamp",
638
+ to: "date"
639
+ }
640
+ },
641
+ {
642
+ $convert: {
643
+ input: "$lastUpdatedTimestamp",
644
+ to: "date"
645
+ }
646
+ }
647
+ ]
648
+ }
649
+ }
650
+ ]
651
+ }
652
+ },
653
+ {
654
+ $sort: { _id: 1 }
655
+ },
656
+ {
657
+ $skip: skip
658
+ },
659
+ {
660
+ $limit: limit
661
+ }
662
+ ]);
663
+ return docs || [];
664
+ }
665
+ /** @description Mark a list of PoW captcha commits as stored
666
+ */
667
+ async markDappUserPoWCommitmentsStored(challenges) {
668
+ const updateDoc = {
669
+ storedAtTimestamp: Date.now()
670
+ };
671
+ await this.tables?.powcaptcha.updateMany(
672
+ { challenge: { $in: challenges } },
673
+ { $set: updateDoc },
674
+ { upsert: false }
675
+ );
676
+ }
677
+ /** @description Mark a list of PoW captcha commits as checked by the server
678
+ */
679
+ async markDappUserPoWCommitmentsChecked(challenges) {
680
+ const updateDoc = {
681
+ [typesDatabase.StoredStatusNames.serverChecked]: true,
682
+ lastUpdatedTimestamp: Date.now()
683
+ };
684
+ await this.tables?.powcaptcha.updateMany(
685
+ { challenge: { $in: challenges } },
686
+ {
687
+ $set: updateDoc
688
+ },
689
+ { upsert: false }
690
+ );
691
+ }
692
+ /**
693
+ * Store a new frictionless token record
694
+ */
695
+ async storeFrictionlessTokenRecord(tokenRecord) {
696
+ const doc = await this.tables.frictionlessToken.create(
697
+ tokenRecord
698
+ );
699
+ return doc._id;
700
+ }
701
+ /** Update a frictionless token record */
702
+ async updateFrictionlessTokenRecord(tokenId, updates) {
703
+ const filter = { _id: tokenId };
704
+ await this.tables.frictionlessToken.updateOne(filter, updates);
705
+ }
706
+ /** Get a frictionless token record */
707
+ async getFrictionlessTokenRecordByTokenId(tokenId) {
708
+ const filter = { _id: tokenId };
709
+ const doc = await this.tables.frictionlessToken.findOne(
710
+ filter
711
+ );
712
+ return doc ? doc : void 0;
713
+ }
714
+ /** Get many frictionless token records */
715
+ async getFrictionlessTokenRecordsByTokenIds(tokenId) {
716
+ const filter = {
717
+ _id: { $in: tokenId }
718
+ };
719
+ return this.tables.frictionlessToken.find(filter);
720
+ }
721
+ /**
722
+ * Check if a frictionless token record exists.
723
+ * Used to ensure that a token is not used more than once.
724
+ */
725
+ async getFrictionlessTokenRecordByToken(token) {
726
+ const filter = { token };
727
+ const record = await this.tables.frictionlessToken.findOne(
728
+ filter
729
+ );
730
+ return record || void 0;
731
+ }
732
+ /**
733
+ * Store a new session record
734
+ */
735
+ async storeSessionRecord(sessionRecord) {
736
+ try {
737
+ this.logger.debug({ action: "storing", sessionRecord });
738
+ await this.tables.session.create(sessionRecord);
739
+ } catch (err) {
740
+ throw new common.ProsopoDBError("DATABASE.SESSION_STORE_FAILED", {
741
+ context: { error: err, sessionId: sessionRecord.sessionId },
742
+ logger: this.logger
743
+ });
744
+ }
745
+ }
746
+ /**
747
+ * Check if a session exists and mark it as removed
748
+ * @returns The session record if it existed, undefined otherwise
749
+ */
750
+ async checkAndRemoveSession(sessionId) {
751
+ this.logger.debug({ action: "checking and removing", sessionId });
752
+ const filter = {
753
+ sessionId,
754
+ deleted: { $exists: false }
755
+ };
756
+ try {
757
+ const session = await this.tables.session.findOneAndUpdate(filter, {
758
+ deleted: true,
759
+ lastUpdatedTimestamp: Date.now()
760
+ }).lean();
761
+ return session || void 0;
762
+ } catch (err) {
763
+ throw new common.ProsopoDBError("DATABASE.SESSION_CHECK_REMOVE_FAILED", {
764
+ context: { error: err, sessionId },
765
+ logger: this.logger
766
+ });
767
+ }
768
+ }
769
+ /** Get unstored session records
770
+ * @description Get session records that have not been stored yet
771
+ * @param limit
772
+ * @param skip
773
+ */
774
+ getUnstoredSessionRecords(limit = 1e3, skip = 0) {
775
+ const filterNoStoredTimestamp = { storedAtTimestamp: { $exists: false } };
776
+ return this.tables?.session.aggregate([
777
+ {
778
+ $match: {
779
+ $or: [
780
+ filterNoStoredTimestamp,
781
+ {
782
+ $expr: {
783
+ $lt: [
784
+ {
785
+ $convert: {
786
+ input: "$storedAtTimestamp",
787
+ to: "date"
788
+ }
789
+ },
790
+ {
791
+ $convert: {
792
+ input: "$lastUpdatedTimestamp",
793
+ to: "date"
794
+ }
795
+ }
796
+ ]
797
+ }
798
+ }
799
+ ]
800
+ }
801
+ },
802
+ {
803
+ $sort: { _id: 1 }
804
+ },
805
+ {
806
+ $skip: skip
807
+ },
808
+ {
809
+ $limit: limit
810
+ }
811
+ ]).then((docs) => docs || []);
812
+ }
813
+ /** Mark a list of session records as stored */
814
+ async markSessionRecordsStored(sessionIds) {
815
+ const updateDoc = {
816
+ storedAtTimestamp: Date.now()
817
+ };
818
+ await this.tables?.session.updateMany(
819
+ { sessionId: { $in: sessionIds } },
820
+ { $set: updateDoc },
821
+ { upsert: false }
822
+ );
823
+ }
824
+ /**
825
+ * @description Store a Dapp User's pending record
826
+ */
827
+ async storePendingImageCommitment(userAccount, requestHash, salt, deadlineTimestamp, requestedAtTimestamp, ipAddress, threshold, frictionlessTokenId) {
828
+ if (!is.isHex(requestHash)) {
829
+ throw new common.ProsopoDBError("DATABASE.INVALID_HASH", {
830
+ context: {
831
+ failedFuncName: this.storePendingImageCommitment.name,
832
+ requestHash
833
+ }
834
+ });
835
+ }
836
+ const pendingRecord = {
837
+ accountId: userAccount,
838
+ pending: true,
839
+ salt,
840
+ requestHash,
841
+ deadlineTimestamp,
842
+ requestedAtTimestamp: new Date(requestedAtTimestamp),
843
+ ipAddress,
844
+ frictionlessTokenId,
845
+ threshold
846
+ };
847
+ await this.tables?.pending.updateOne(
848
+ { requestHash },
849
+ { $set: pendingRecord },
850
+ { upsert: true }
851
+ );
852
+ }
853
+ /**
854
+ * @description Get a Dapp user's pending record
855
+ */
856
+ async getPendingImageCommitment(requestHash) {
857
+ if (!is.isHex(requestHash)) {
858
+ throw new common.ProsopoDBError("DATABASE.INVALID_HASH", {
859
+ context: {
860
+ failedFuncName: this.getPendingImageCommitment.name,
861
+ requestHash
862
+ }
863
+ });
864
+ }
865
+ const filter = {
866
+ [types.ApiParams.requestHash]: requestHash
867
+ };
868
+ const doc = await this.tables?.pending.findOne(filter).lean();
869
+ if (doc) {
870
+ return doc;
871
+ }
872
+ throw new common.ProsopoDBError("DATABASE.PENDING_RECORD_NOT_FOUND", {
873
+ context: {
874
+ failedFuncName: this.getPendingImageCommitment.name,
875
+ requestHash
876
+ }
877
+ });
878
+ }
879
+ /**
880
+ * @description Mark a pending request as used
881
+ */
882
+ async updatePendingImageCommitmentStatus(requestHash) {
883
+ if (!is.isHex(requestHash)) {
884
+ throw new common.ProsopoDBError("DATABASE.INVALID_HASH", {
885
+ context: {
886
+ failedFuncName: this.updatePendingImageCommitmentStatus.name,
887
+ requestHash
888
+ }
889
+ });
890
+ }
891
+ const filter = {
892
+ [types.ApiParams.requestHash]: requestHash
893
+ };
894
+ await this.tables?.pending.updateOne(
895
+ filter,
896
+ {
897
+ $set: {
898
+ [types.CaptchaStatus.pending]: false
899
+ }
900
+ },
901
+ { upsert: true }
902
+ );
903
+ }
904
+ /**
905
+ * @description Get all unsolved captchas
906
+ */
907
+ async getAllCaptchasByDatasetId(datasetId, state) {
908
+ const filter = {
909
+ datasetId,
910
+ solved: state === types.CaptchaStates.Solved
911
+ };
912
+ const cursor = this.tables?.captcha.find(filter).lean();
913
+ const docs = await cursor;
914
+ if (docs) {
915
+ return docs.map(({ _id, ...keepAttrs }) => keepAttrs);
916
+ }
917
+ throw new common.ProsopoDBError("DATABASE.CAPTCHA_GET_FAILED");
918
+ }
919
+ /**
920
+ * @description Get all dapp user solutions by captchaIds
921
+ */
922
+ async getAllDappUserSolutions(captchaId) {
923
+ const filter = {
924
+ captchaId: { $in: captchaId }
925
+ };
926
+ const cursor = this.tables?.usersolution?.find(filter).lean();
927
+ const docs = await cursor;
928
+ if (docs) {
929
+ return docs.map(
930
+ ({ _id, ...keepAttrs }) => keepAttrs
931
+ );
932
+ }
933
+ throw new common.ProsopoDBError("DATABASE.SOLUTION_GET_FAILED");
934
+ }
935
+ async getDatasetIdWithSolvedCaptchasOfSizeN(solvedCaptchaCount) {
936
+ const cursor = this.tables?.solution.aggregate([
937
+ {
938
+ $match: {}
939
+ },
940
+ {
941
+ $group: {
942
+ _id: "$datasetId",
943
+ count: { $sum: 1 }
944
+ }
945
+ },
946
+ {
947
+ $match: {
948
+ count: { $gte: solvedCaptchaCount }
949
+ }
950
+ },
951
+ {
952
+ $sample: { size: 1 }
953
+ }
954
+ ]);
955
+ const docs = await cursor;
956
+ if (docs?.length) {
957
+ return docs[0]._id;
958
+ }
959
+ throw new common.ProsopoDBError("DATABASE.DATASET_WITH_SOLUTIONS_GET_FAILED");
960
+ }
961
+ async getRandomSolvedCaptchasFromSingleDataset(datasetId, size) {
962
+ if (!is.isHex(datasetId)) {
963
+ throw new common.ProsopoDBError("DATABASE.INVALID_HASH", {
964
+ context: {
965
+ failedFuncName: this.getRandomSolvedCaptchasFromSingleDataset.name,
966
+ datasetId
967
+ }
968
+ });
969
+ }
970
+ const sampleSize = size ? Math.abs(Math.trunc(size)) : 1;
971
+ const cursor = this.tables?.solution.aggregate([
972
+ { $match: { datasetId } },
973
+ { $sample: { size: sampleSize } },
974
+ {
975
+ $project: {
976
+ captchaId: 1,
977
+ captchaContentId: 1,
978
+ solution: 1
979
+ }
980
+ }
981
+ ]);
982
+ const docs = await cursor;
983
+ if (docs?.length) {
984
+ return docs;
985
+ }
986
+ throw new common.ProsopoDBError("DATABASE.SOLUTION_GET_FAILED", {
987
+ context: {
988
+ failedFuncName: this.getRandomSolvedCaptchasFromSingleDataset.name,
989
+ datasetId,
990
+ size
991
+ }
992
+ });
993
+ }
994
+ /**
995
+ * @description Get dapp user solution by ID
996
+ * @param {string[]} commitmentId
997
+ */
998
+ async getDappUserSolutionById(commitmentId) {
999
+ const filter = {
1000
+ commitmentId
1001
+ };
1002
+ const project = { projection: { _id: 0 } };
1003
+ const cursor = this.tables?.usersolution?.findOne(filter, project).lean();
1004
+ const doc = await cursor;
1005
+ if (doc) {
1006
+ return doc;
1007
+ }
1008
+ throw new common.ProsopoDBError("DATABASE.SOLUTION_GET_FAILED", {
1009
+ context: { failedFuncName: this.getCaptchaById.name, commitmentId }
1010
+ });
1011
+ }
1012
+ /**
1013
+ * @description Get dapp user commitment by user account
1014
+ * @param commitmentId
1015
+ */
1016
+ async getDappUserCommitmentById(commitmentId) {
1017
+ const filter = { id: commitmentId };
1018
+ const commitmentCursor = this.tables?.commitment?.findOne(filter).lean();
1019
+ const doc = await commitmentCursor;
1020
+ return doc ? doc : void 0;
1021
+ }
1022
+ /**
1023
+ * @description Get dapp user commitment by user account
1024
+ * @param {string} userAccount
1025
+ * @param {string} dappAccount
1026
+ */
1027
+ async getDappUserCommitmentByAccount(userAccount, dappAccount) {
1028
+ const filter = {
1029
+ userAccount,
1030
+ dappAccount
1031
+ };
1032
+ const project = { _id: 0 };
1033
+ const sort = { sort: { _id: -1 } };
1034
+ const docs = await this.tables?.commitment?.find(filter, project, sort).lean();
1035
+ return docs ? docs : [];
1036
+ }
1037
+ /**
1038
+ * @description Approve a dapp user's solution
1039
+ * @param {string[]} commitmentId
1040
+ */
1041
+ async approveDappUserCommitment(commitmentId) {
1042
+ try {
1043
+ const result = { status: types.CaptchaStatus.approved };
1044
+ const updateDoc = {
1045
+ result,
1046
+ lastUpdatedTimestamp: Date.now()
1047
+ };
1048
+ const filter = { id: commitmentId };
1049
+ await this.tables?.commitment?.findOneAndUpdate(filter, { $set: updateDoc }, { upsert: false }).lean();
1050
+ } catch (err) {
1051
+ throw new common.ProsopoDBError("DATABASE.SOLUTION_APPROVE_FAILED", {
1052
+ context: { error: err, commitmentId }
1053
+ });
1054
+ }
1055
+ }
1056
+ /**
1057
+ * @description Disapprove a dapp user's solution
1058
+ * @param {string} commitmentId
1059
+ * @param reason
1060
+ */
1061
+ async disapproveDappUserCommitment(commitmentId, reason) {
1062
+ try {
1063
+ const updateDoc = {
1064
+ result: { status: types.CaptchaStatus.disapproved, reason },
1065
+ lastUpdatedTimestamp: Date.now()
1066
+ };
1067
+ const filter = { id: commitmentId };
1068
+ await this.tables?.commitment?.findOneAndUpdate(filter, { $set: updateDoc }, { upsert: false }).lean();
1069
+ } catch (err) {
1070
+ throw new common.ProsopoDBError("DATABASE.SOLUTION_APPROVE_FAILED", {
1071
+ context: { error: err, commitmentId }
1072
+ });
1073
+ }
1074
+ }
1075
+ /**
1076
+ * @description Flag a dapp user's solutions as used by calculated solution
1077
+ * @param {string[]} captchaIds
1078
+ */
1079
+ async flagProcessedDappUserSolutions(captchaIds) {
1080
+ try {
1081
+ await this.tables?.usersolution?.updateMany(
1082
+ { captchaId: { $in: captchaIds } },
1083
+ { $set: { processed: true } },
1084
+ { upsert: false }
1085
+ ).lean();
1086
+ } catch (err) {
1087
+ throw new common.ProsopoDBError("DATABASE.SOLUTION_FLAG_FAILED", {
1088
+ context: { error: err, captchaIds }
1089
+ });
1090
+ }
1091
+ }
1092
+ /**
1093
+ * @description Flag dapp users' commitments as used by calculated solution
1094
+ * @param {string[]} commitmentIds
1095
+ */
1096
+ async flagProcessedDappUserCommitments(commitmentIds) {
1097
+ try {
1098
+ const distinctCommitmentIds = [...new Set(commitmentIds)];
1099
+ await this.tables?.commitment?.updateMany(
1100
+ { id: { $in: distinctCommitmentIds } },
1101
+ { $set: { processed: true } },
1102
+ { upsert: false }
1103
+ ).lean();
1104
+ } catch (err) {
1105
+ throw new common.ProsopoDBError("DATABASE.COMMITMENT_FLAG_FAILED", {
1106
+ context: { error: err, commitmentIds }
1107
+ });
1108
+ }
1109
+ }
1110
+ /**
1111
+ * @description Get a scheduled task status record by task ID and status
1112
+ */
1113
+ async getScheduledTaskStatus(taskId, status) {
1114
+ const filter = {
1115
+ _id: taskId,
1116
+ status
1117
+ };
1118
+ const cursor = await this.tables?.scheduler?.findOne(filter).lean();
1119
+ return cursor ? cursor : void 0;
1120
+ }
1121
+ /**
1122
+ * @description Get the most recent scheduled task status record for a given task
1123
+ */
1124
+ async getLastScheduledTaskStatus(task, status) {
1125
+ const filter = { processName: task };
1126
+ if (status) {
1127
+ filter.status = status;
1128
+ }
1129
+ const sort = {
1130
+ datetime: -1
1131
+ };
1132
+ const cursor = await this.tables?.scheduler?.findOne(filter).sort(sort).limit(1).lean();
1133
+ return cursor ? cursor : void 0;
1134
+ }
1135
+ /**
1136
+ * @description Create the status of a scheduled task
1137
+ */
1138
+ async createScheduledTaskStatus(taskName, status) {
1139
+ const now = (/* @__PURE__ */ new Date()).getTime();
1140
+ const doc = typesDatabase.ScheduledTaskSchema.parse({
1141
+ processName: taskName,
1142
+ datetime: now,
1143
+ status
1144
+ });
1145
+ const taskRecord = await this.tables?.scheduler.create(doc);
1146
+ return taskRecord._id;
1147
+ }
1148
+ /**
1149
+ * @description Update the status of a scheduled task and an optional result
1150
+ */
1151
+ async updateScheduledTaskStatus(taskId, status, result) {
1152
+ const update = {
1153
+ status,
1154
+ updated: (/* @__PURE__ */ new Date()).getTime(),
1155
+ ...result && { result }
1156
+ };
1157
+ const filter = { _id: taskId };
1158
+ await this.tables?.scheduler.updateOne(
1159
+ filter,
1160
+ { $set: update },
1161
+ {
1162
+ upsert: false
1163
+ }
1164
+ );
1165
+ }
1166
+ /**
1167
+ * @description Clean up the scheduled task status records
1168
+ */
1169
+ async cleanupScheduledTaskStatus(status) {
1170
+ const filter = {
1171
+ status
1172
+ };
1173
+ await this.tables?.scheduler.deleteMany(filter);
1174
+ }
1175
+ /**
1176
+ * @description Update the client records
1177
+ */
1178
+ async updateClientRecords(clientRecords) {
1179
+ const ops = clientRecords.map((record) => {
1180
+ const clientRecord = {
1181
+ account: record.account,
1182
+ settings: record.settings,
1183
+ tier: record.tier
1184
+ };
1185
+ const filter = {
1186
+ account: record.account
1187
+ };
1188
+ return {
1189
+ updateOne: {
1190
+ filter,
1191
+ update: {
1192
+ $set: clientRecord
1193
+ },
1194
+ upsert: true
1195
+ }
1196
+ };
1197
+ });
1198
+ await this.tables?.client.bulkWrite(ops);
1199
+ }
1200
+ /**
1201
+ * @description Get a client record
1202
+ */
1203
+ async getClientRecord(account) {
1204
+ const filter = { account };
1205
+ const doc = await this.tables?.client.findOne(filter).lean();
1206
+ return doc ? doc : void 0;
1207
+ }
1208
+ /**
1209
+ * @description Store a detector key
1210
+ */
1211
+ async storeDetectorKey(detectorKey) {
1212
+ return this.tables?.detector.create({
1213
+ detectorKey,
1214
+ createdAt: /* @__PURE__ */ new Date()
1215
+ });
1216
+ }
1217
+ /** @description Remove a detector key */
1218
+ async removeDetectorKey(detectorKey) {
1219
+ const filter = { detectorKey };
1220
+ await this.tables?.detector.deleteOne(filter);
1221
+ }
1222
+ /**
1223
+ * @description Get valid detector keys
1224
+ */
1225
+ async getDetectorKeys() {
1226
+ const keyRecords = await this.tables?.detector.find({}, { detectorKey: 1 }).sort({ createdAt: -1 }).lean();
1227
+ return (keyRecords || []).map((record) => record.detectorKey);
1228
+ }
1229
+ }
1230
+ exports.ProviderDatabase = ProviderDatabase;