@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.
- package/CHANGELOG.md +26 -0
- package/dist/cjs/base/index.cjs +6 -0
- package/dist/cjs/base/mongo.cjs +102 -0
- package/dist/cjs/base/mongoMemory.cjs +28 -0
- package/dist/cjs/databases/captcha.cjs +125 -0
- package/dist/cjs/databases/client.cjs +63 -0
- package/dist/cjs/databases/index.cjs +19 -0
- package/dist/cjs/databases/provider.cjs +1230 -0
- package/dist/cjs/index.cjs +15 -0
- package/dist/databases/captcha.d.ts +9 -2
- package/dist/databases/captcha.d.ts.map +1 -1
- package/dist/databases/captcha.js +53 -4
- package/dist/databases/captcha.js.map +1 -1
- package/dist/databases/provider.d.ts +7 -3
- package/dist/databases/provider.d.ts.map +1 -1
- package/dist/databases/provider.js +70 -3
- package/dist/databases/provider.js.map +1 -1
- package/package.json +8 -7
|
@@ -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;
|