@prosopo/database 3.5.6 → 3.13.7
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/.turbo/turbo-build$colon$cjs.log +17 -13
- package/.turbo/turbo-build$colon$tsc.log +47 -0
- package/.turbo/turbo-build.log +22 -14
- package/CHANGELOG.md +690 -0
- package/dist/base/index.d.ts +3 -0
- package/dist/base/index.d.ts.map +1 -0
- package/dist/base/index.js.map +1 -0
- package/dist/base/mongo.d.ts +18 -0
- package/dist/base/mongo.d.ts.map +1 -0
- package/dist/base/mongo.js +5 -2
- package/dist/base/mongo.js.map +1 -0
- package/dist/base/mongoMemory.d.ts +10 -0
- package/dist/base/mongoMemory.d.ts.map +1 -0
- package/dist/base/mongoMemory.js.map +1 -0
- package/dist/cjs/base/mongo.cjs +6 -3
- package/dist/cjs/databases/captcha.cjs +2 -1
- package/dist/cjs/databases/centralDbStreamer.cjs +136 -0
- package/dist/cjs/databases/index.cjs +2 -0
- package/dist/cjs/databases/provider.cjs +691 -165
- package/dist/cjs/index.cjs +4 -0
- package/dist/cjs/redisCache.cjs +388 -0
- package/dist/databases/captcha.d.ts +25 -0
- package/dist/databases/captcha.d.ts.map +1 -0
- package/dist/databases/captcha.js +2 -1
- package/dist/databases/captcha.js.map +1 -0
- package/dist/databases/centralDbStreamer.d.ts +19 -0
- package/dist/databases/centralDbStreamer.d.ts.map +1 -0
- package/dist/databases/centralDbStreamer.js +136 -0
- package/dist/databases/centralDbStreamer.js.map +1 -0
- package/dist/databases/client.d.ts +12 -0
- package/dist/databases/client.d.ts.map +1 -0
- package/dist/databases/client.js.map +1 -0
- package/dist/databases/index.d.ts +17 -0
- package/dist/databases/index.d.ts.map +1 -0
- package/dist/databases/index.js +2 -0
- package/dist/databases/index.js.map +1 -0
- package/dist/databases/provider.d.ts +143 -0
- package/dist/databases/provider.d.ts.map +1 -0
- package/dist/databases/provider.js +692 -166
- package/dist/databases/provider.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -0
- package/dist/redisCache.d.ts +31 -0
- package/dist/redisCache.d.ts.map +1 -0
- package/dist/redisCache.js +388 -0
- package/dist/redisCache.js.map +1 -0
- package/dist/tests/integration/ipInfoPersistence.integration.test.d.ts +2 -0
- package/dist/tests/integration/ipInfoPersistence.integration.test.d.ts.map +1 -0
- package/dist/tests/integration/ipInfoPersistence.integration.test.js +243 -0
- package/dist/tests/integration/ipInfoPersistence.integration.test.js.map +1 -0
- package/dist/tests/unit/captchaLabel.unit.test.d.ts +2 -0
- package/dist/tests/unit/captchaLabel.unit.test.d.ts.map +1 -0
- package/dist/tests/unit/captchaLabel.unit.test.js +41 -0
- package/dist/tests/unit/captchaLabel.unit.test.js.map +1 -0
- package/dist/tests/unit/databases/centralDbStreamer.unit.test.d.ts +2 -0
- package/dist/tests/unit/databases/centralDbStreamer.unit.test.d.ts.map +1 -0
- package/dist/tests/unit/databases/centralDbStreamer.unit.test.js +221 -0
- package/dist/tests/unit/databases/centralDbStreamer.unit.test.js.map +1 -0
- package/package.json +14 -10
- package/vite.cjs.config.ts +1 -1
- package/vite.esm.config.ts +1 -1
- package/vite.test.config.ts +18 -0
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
import { isHex } from "@polkadot/util/is";
|
|
2
2
|
import { ProsopoDBError } from "@prosopo/common";
|
|
3
3
|
import { connectToRedis, setupRedisIndex } from "@prosopo/redis-client";
|
|
4
|
-
import { DatasetWithIdsAndTreeSchema,
|
|
5
|
-
import { CaptchaRecordSchema, PoWCaptchaRecordSchema, DatasetRecordSchema, SolutionRecordSchema, UserCommitmentRecordSchema, UserSolutionRecordSchema,
|
|
4
|
+
import { DatasetWithIdsAndTreeSchema, UserCommitmentSchema, CaptchaStatus, StoredStatusNames, CaptchaStates, ContextType } from "@prosopo/types";
|
|
5
|
+
import { CaptchaRecordSchema, PoWCaptchaRecordSchema, PuzzleCaptchaRecordSchema, DatasetRecordSchema, SolutionRecordSchema, UserCommitmentRecordSchema, UserSolutionRecordSchema, ScheduledTaskRecordSchema, ClientRecordSchema, SessionRecordSchema, DetectorRecordSchema, DecisionMachineArtifactRecordSchema, ClientContextEntropyRecordSchema, SpamEmailDomainRecordSchema, ScheduledTaskSchema } from "@prosopo/types-database";
|
|
6
6
|
import { accessRulesRedisIndex, createRedisAccessRulesStorage } from "@prosopo/user-access-policy/redis";
|
|
7
|
+
import { buildDomainSuffixCandidates } from "@prosopo/util";
|
|
7
8
|
import { MongoDatabase } from "../base/mongo.js";
|
|
8
9
|
const TWENTY_FOUR_HOURS_IN_MS = 24 * 60 * 60 * 1e3;
|
|
10
|
+
const MAX_DOMAIN_SUFFIX_CANDIDATES = 5;
|
|
9
11
|
var TableNames = /* @__PURE__ */ ((TableNames2) => {
|
|
10
12
|
TableNames2["captcha"] = "captcha";
|
|
11
13
|
TableNames2["dataset"] = "dataset";
|
|
12
14
|
TableNames2["solution"] = "solution";
|
|
13
15
|
TableNames2["commitment"] = "commitment";
|
|
14
16
|
TableNames2["usersolution"] = "usersolution";
|
|
15
|
-
TableNames2["pending"] = "pending";
|
|
16
17
|
TableNames2["scheduler"] = "scheduler";
|
|
17
18
|
TableNames2["powcaptcha"] = "powcaptcha";
|
|
19
|
+
TableNames2["puzzlecaptcha"] = "puzzlecaptcha";
|
|
18
20
|
TableNames2["client"] = "client";
|
|
19
21
|
TableNames2["session"] = "session";
|
|
20
22
|
TableNames2["detector"] = "detector";
|
|
23
|
+
TableNames2["decisionMachine"] = "decisionMachine";
|
|
21
24
|
TableNames2["clientContextEntropy"] = "clientContextEntropy";
|
|
25
|
+
TableNames2["spamEmailDomain"] = "spamEmailDomain";
|
|
22
26
|
return TableNames2;
|
|
23
27
|
})(TableNames || {});
|
|
24
28
|
const PROVIDER_TABLES = [
|
|
@@ -32,6 +36,11 @@ const PROVIDER_TABLES = [
|
|
|
32
36
|
modelName: "PowCaptcha",
|
|
33
37
|
schema: PoWCaptchaRecordSchema
|
|
34
38
|
},
|
|
39
|
+
{
|
|
40
|
+
collectionName: "puzzlecaptcha",
|
|
41
|
+
modelName: "PuzzleCaptcha",
|
|
42
|
+
schema: PuzzleCaptchaRecordSchema
|
|
43
|
+
},
|
|
35
44
|
{
|
|
36
45
|
collectionName: "dataset",
|
|
37
46
|
modelName: "Dataset",
|
|
@@ -52,11 +61,6 @@ const PROVIDER_TABLES = [
|
|
|
52
61
|
modelName: "UserSolution",
|
|
53
62
|
schema: UserSolutionRecordSchema
|
|
54
63
|
},
|
|
55
|
-
{
|
|
56
|
-
collectionName: "pending",
|
|
57
|
-
modelName: "Pending",
|
|
58
|
-
schema: PendingRecordSchema
|
|
59
|
-
},
|
|
60
64
|
{
|
|
61
65
|
collectionName: "scheduler",
|
|
62
66
|
modelName: "Scheduler",
|
|
@@ -77,10 +81,20 @@ const PROVIDER_TABLES = [
|
|
|
77
81
|
modelName: "Detector",
|
|
78
82
|
schema: DetectorRecordSchema
|
|
79
83
|
},
|
|
84
|
+
{
|
|
85
|
+
collectionName: "decisionMachine",
|
|
86
|
+
modelName: "DecisionMachine",
|
|
87
|
+
schema: DecisionMachineArtifactRecordSchema
|
|
88
|
+
},
|
|
80
89
|
{
|
|
81
90
|
collectionName: "clientContextEntropy",
|
|
82
91
|
modelName: "ClientContextEntropy",
|
|
83
92
|
schema: ClientContextEntropyRecordSchema
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
collectionName: "spamEmailDomain",
|
|
96
|
+
modelName: "SpamEmailDomain",
|
|
97
|
+
schema: SpamEmailDomainRecordSchema
|
|
84
98
|
}
|
|
85
99
|
];
|
|
86
100
|
class ProviderDatabase extends MongoDatabase {
|
|
@@ -99,6 +113,12 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
99
113
|
this.redisConnection = null;
|
|
100
114
|
this.userAccessRulesStorage = null;
|
|
101
115
|
}
|
|
116
|
+
setCentralDbStreamer(streamer) {
|
|
117
|
+
this.centralStreamer = streamer;
|
|
118
|
+
}
|
|
119
|
+
hasCentralDbStreamer() {
|
|
120
|
+
return this.centralStreamer !== void 0;
|
|
121
|
+
}
|
|
102
122
|
async connect() {
|
|
103
123
|
await super.connect();
|
|
104
124
|
this.loadTables();
|
|
@@ -380,7 +400,12 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
380
400
|
*/
|
|
381
401
|
async getCaptchaById(captchaId) {
|
|
382
402
|
const filter = { captchaId: { $in: captchaId } };
|
|
383
|
-
const cursor = this.tables?.captcha.find(filter
|
|
403
|
+
const cursor = this.tables?.captcha.find(filter, {
|
|
404
|
+
_id: 0,
|
|
405
|
+
captchaId: 1,
|
|
406
|
+
datasetId: 1,
|
|
407
|
+
items: 1
|
|
408
|
+
}).lean();
|
|
384
409
|
const docs = await cursor;
|
|
385
410
|
if (docs?.length) {
|
|
386
411
|
return docs.map(({ _id, ...keepAttrs }) => keepAttrs);
|
|
@@ -476,6 +501,11 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
476
501
|
}
|
|
477
502
|
}));
|
|
478
503
|
await this.tables?.usersolution.bulkWrite(ops);
|
|
504
|
+
this.centralStreamer?.streamImageRecord(
|
|
505
|
+
commitmentRecord,
|
|
506
|
+
(ts) => this.tables.commitment.updateOne({ id: commit.id }, { $set: { storedAtTimestamp: ts } }).then(() => {
|
|
507
|
+
})
|
|
508
|
+
);
|
|
479
509
|
}
|
|
480
510
|
}
|
|
481
511
|
/**
|
|
@@ -492,9 +522,10 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
492
522
|
* @param userSubmitted
|
|
493
523
|
* @param storedStatus
|
|
494
524
|
* @param userSignature
|
|
525
|
+
* @param ipInfo full ipinfo payload from the ipInfoMiddleware
|
|
495
526
|
* @returns {Promise<void>} A promise that resolves when the record is added.
|
|
496
527
|
*/
|
|
497
|
-
async storePowCaptchaRecord(challenge, components, difficulty, providerSignature, ipAddress, headers, ja4, sessionId, serverChecked = false, userSubmitted = false,
|
|
528
|
+
async storePowCaptchaRecord(challenge, components, difficulty, providerSignature, ipAddress, headers, ja4, sessionId, serverChecked = false, userSubmitted = false, userSignature, ipInfo) {
|
|
498
529
|
const tables = this.getTables();
|
|
499
530
|
const powCaptchaRecord = {
|
|
500
531
|
challenge,
|
|
@@ -511,7 +542,9 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
511
542
|
providerSignature,
|
|
512
543
|
userSignature,
|
|
513
544
|
lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
|
|
514
|
-
|
|
545
|
+
pendingStage: true,
|
|
546
|
+
sessionId,
|
|
547
|
+
ipInfo
|
|
515
548
|
};
|
|
516
549
|
try {
|
|
517
550
|
await tables.powcaptcha.create(powCaptchaRecord);
|
|
@@ -520,10 +553,27 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
520
553
|
challenge,
|
|
521
554
|
userSubmitted,
|
|
522
555
|
serverChecked,
|
|
523
|
-
|
|
556
|
+
countryCode: ipInfo?.isValid ? ipInfo.countryCode : void 0
|
|
524
557
|
},
|
|
525
558
|
msg: "PowCaptcha record added successfully"
|
|
526
559
|
}));
|
|
560
|
+
this.centralStreamer?.streamPowRecord(
|
|
561
|
+
powCaptchaRecord,
|
|
562
|
+
(ts) => (
|
|
563
|
+
// Guard with `lastUpdatedTimestamp: { $lte: ts }` so a concurrent
|
|
564
|
+
// update (newer lastUpdatedTimestamp) doesn't get its pendingStage
|
|
565
|
+
// flag cleared by this older stage completion. Mismatched docs
|
|
566
|
+
// leave pendingStage set so the next sweep picks them up.
|
|
567
|
+
this.tables.powcaptcha.updateOne(
|
|
568
|
+
{ challenge, lastUpdatedTimestamp: { $lte: ts } },
|
|
569
|
+
{
|
|
570
|
+
$set: { storedAtTimestamp: ts },
|
|
571
|
+
$unset: { pendingStage: 1 }
|
|
572
|
+
}
|
|
573
|
+
).then(() => {
|
|
574
|
+
})
|
|
575
|
+
)
|
|
576
|
+
);
|
|
527
577
|
} catch (error) {
|
|
528
578
|
const err = new ProsopoDBError("DATABASE.CAPTCHA_UPDATE_FAILED", {
|
|
529
579
|
context: {
|
|
@@ -531,7 +581,7 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
531
581
|
challenge,
|
|
532
582
|
userSubmitted,
|
|
533
583
|
serverChecked,
|
|
534
|
-
|
|
584
|
+
ipInfo
|
|
535
585
|
},
|
|
536
586
|
logger: this.logger
|
|
537
587
|
});
|
|
@@ -556,7 +606,24 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
556
606
|
}
|
|
557
607
|
try {
|
|
558
608
|
const filter = { challenge };
|
|
559
|
-
const record = await this.tables.powcaptcha.findOne(filter
|
|
609
|
+
const record = await this.tables.powcaptcha.findOne(filter, {
|
|
610
|
+
challenge: 1,
|
|
611
|
+
userAccount: 1,
|
|
612
|
+
dappAccount: 1,
|
|
613
|
+
requestedAtTimestamp: 1,
|
|
614
|
+
ipAddress: 1,
|
|
615
|
+
headers: 1,
|
|
616
|
+
ja4: 1,
|
|
617
|
+
result: 1,
|
|
618
|
+
difficulty: 1,
|
|
619
|
+
sessionId: 1,
|
|
620
|
+
ipInfo: 1,
|
|
621
|
+
deviceCapability: 1,
|
|
622
|
+
behavioralDataPacked: 1,
|
|
623
|
+
serverChecked: 1,
|
|
624
|
+
userSubmitted: 1,
|
|
625
|
+
coords: 1
|
|
626
|
+
}).lean();
|
|
560
627
|
if (record) {
|
|
561
628
|
this.logger.info(() => ({
|
|
562
629
|
data: { challenge },
|
|
@@ -590,7 +657,7 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
590
657
|
* @param userSignature
|
|
591
658
|
* @returns {Promise<void>} A promise that resolves when the record is updated.
|
|
592
659
|
*/
|
|
593
|
-
async updatePowCaptchaRecordResult(challenge, result, serverChecked = false, userSubmitted = false, userSignature) {
|
|
660
|
+
async updatePowCaptchaRecordResult(challenge, result, serverChecked = false, userSubmitted = false, userSignature, coords) {
|
|
594
661
|
const tables = this.getTables();
|
|
595
662
|
const timestamp = /* @__PURE__ */ new Date();
|
|
596
663
|
const update = {
|
|
@@ -598,7 +665,9 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
598
665
|
serverChecked,
|
|
599
666
|
userSubmitted,
|
|
600
667
|
userSignature,
|
|
601
|
-
lastUpdatedTimestamp: timestamp
|
|
668
|
+
lastUpdatedTimestamp: timestamp,
|
|
669
|
+
pendingStage: true,
|
|
670
|
+
...coords && { coords }
|
|
602
671
|
};
|
|
603
672
|
try {
|
|
604
673
|
const updateResult = await tables.powcaptcha.updateOne(
|
|
@@ -628,6 +697,17 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
628
697
|
},
|
|
629
698
|
msg: "PowCaptcha record updated successfully"
|
|
630
699
|
}));
|
|
700
|
+
this.centralStreamer?.streamPowUpdate(
|
|
701
|
+
() => this.getPowCaptchaRecordByChallenge(challenge),
|
|
702
|
+
(ts) => this.tables.powcaptcha.updateOne(
|
|
703
|
+
{ challenge, lastUpdatedTimestamp: { $lte: ts } },
|
|
704
|
+
{
|
|
705
|
+
$set: { storedAtTimestamp: ts },
|
|
706
|
+
$unset: { pendingStage: 1 }
|
|
707
|
+
}
|
|
708
|
+
).then(() => {
|
|
709
|
+
})
|
|
710
|
+
);
|
|
631
711
|
} catch (error) {
|
|
632
712
|
const err = new ProsopoDBError("DATABASE.CAPTCHA_UPDATE_FAILED", {
|
|
633
713
|
context: {
|
|
@@ -648,7 +728,205 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
648
728
|
const tables = this.getTables();
|
|
649
729
|
await tables.powcaptcha.updateOne(
|
|
650
730
|
{ challenge },
|
|
651
|
-
{ $set: updates },
|
|
731
|
+
{ $set: { ...updates, pendingStage: true } },
|
|
732
|
+
{ upsert: false }
|
|
733
|
+
);
|
|
734
|
+
this.centralStreamer?.streamPowUpdate(
|
|
735
|
+
() => this.getPowCaptchaRecordByChallenge(challenge),
|
|
736
|
+
(ts) => this.tables.powcaptcha.updateOne(
|
|
737
|
+
{ challenge, lastUpdatedTimestamp: { $lte: ts } },
|
|
738
|
+
{
|
|
739
|
+
$set: { storedAtTimestamp: ts },
|
|
740
|
+
$unset: { pendingStage: 1 }
|
|
741
|
+
}
|
|
742
|
+
).then(() => {
|
|
743
|
+
})
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
async storePuzzleCaptchaRecord(challenge, components, targetX, targetY, originX, originY, tolerance, providerSignature, ipAddress, headers, ja4, sessionId, ipInfo) {
|
|
747
|
+
const tables = this.getTables();
|
|
748
|
+
const puzzleCaptchaRecord = {
|
|
749
|
+
challenge,
|
|
750
|
+
userAccount: components.userAccount,
|
|
751
|
+
dappAccount: components.dappAccount,
|
|
752
|
+
requestedAtTimestamp: new Date(components.requestedAtTimestamp),
|
|
753
|
+
ipAddress,
|
|
754
|
+
headers,
|
|
755
|
+
ja4,
|
|
756
|
+
result: { status: CaptchaStatus.pending },
|
|
757
|
+
userSubmitted: false,
|
|
758
|
+
serverChecked: false,
|
|
759
|
+
targetX,
|
|
760
|
+
targetY,
|
|
761
|
+
originX,
|
|
762
|
+
originY,
|
|
763
|
+
tolerance,
|
|
764
|
+
providerSignature,
|
|
765
|
+
lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
|
|
766
|
+
pendingStage: true,
|
|
767
|
+
sessionId,
|
|
768
|
+
ipInfo
|
|
769
|
+
};
|
|
770
|
+
try {
|
|
771
|
+
await tables.puzzlecaptcha.create(puzzleCaptchaRecord);
|
|
772
|
+
this.logger.info(() => ({
|
|
773
|
+
data: {
|
|
774
|
+
challenge,
|
|
775
|
+
countryCode: ipInfo?.isValid ? ipInfo.countryCode : void 0
|
|
776
|
+
},
|
|
777
|
+
msg: "PuzzleCaptcha record added successfully"
|
|
778
|
+
}));
|
|
779
|
+
} catch (error) {
|
|
780
|
+
const err = new ProsopoDBError("DATABASE.CAPTCHA_UPDATE_FAILED", {
|
|
781
|
+
context: {
|
|
782
|
+
error,
|
|
783
|
+
challenge,
|
|
784
|
+
ipInfo
|
|
785
|
+
},
|
|
786
|
+
logger: this.logger
|
|
787
|
+
});
|
|
788
|
+
this.logger.error(() => ({
|
|
789
|
+
err: error,
|
|
790
|
+
msg: "Failed to add PuzzleCaptcha record"
|
|
791
|
+
}));
|
|
792
|
+
throw err;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
/**
|
|
796
|
+
* @description Retrieves a Puzzle Captcha record by its challenge string.
|
|
797
|
+
* @param {string} challenge The challenge string to search for.
|
|
798
|
+
* @returns {Promise<PuzzleCaptchaRecord | null>} A promise that resolves with the found record or null if not found.
|
|
799
|
+
*/
|
|
800
|
+
async getPuzzleCaptchaRecordByChallenge(challenge) {
|
|
801
|
+
if (!this.tables) {
|
|
802
|
+
throw new ProsopoDBError("DATABASE.DATABASE_UNDEFINED", {
|
|
803
|
+
context: {
|
|
804
|
+
failedFuncName: this.getPuzzleCaptchaRecordByChallenge.name
|
|
805
|
+
},
|
|
806
|
+
logger: this.logger
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
try {
|
|
810
|
+
const filter = { challenge };
|
|
811
|
+
const record = await this.tables.puzzlecaptcha.findOne(filter, {
|
|
812
|
+
challenge: 1,
|
|
813
|
+
userAccount: 1,
|
|
814
|
+
dappAccount: 1,
|
|
815
|
+
requestedAtTimestamp: 1,
|
|
816
|
+
ipAddress: 1,
|
|
817
|
+
headers: 1,
|
|
818
|
+
ja4: 1,
|
|
819
|
+
result: 1,
|
|
820
|
+
targetX: 1,
|
|
821
|
+
targetY: 1,
|
|
822
|
+
originX: 1,
|
|
823
|
+
originY: 1,
|
|
824
|
+
tolerance: 1,
|
|
825
|
+
puzzleEvents: 1,
|
|
826
|
+
sessionId: 1,
|
|
827
|
+
ipInfo: 1,
|
|
828
|
+
deviceCapability: 1,
|
|
829
|
+
behavioralDataPacked: 1,
|
|
830
|
+
serverChecked: 1,
|
|
831
|
+
userSubmitted: 1,
|
|
832
|
+
coords: 1
|
|
833
|
+
}).lean();
|
|
834
|
+
if (record) {
|
|
835
|
+
this.logger.info(() => ({
|
|
836
|
+
data: { challenge },
|
|
837
|
+
msg: "PuzzleCaptcha record retrieved successfully"
|
|
838
|
+
}));
|
|
839
|
+
return record;
|
|
840
|
+
}
|
|
841
|
+
this.logger.info(() => ({
|
|
842
|
+
data: { challenge },
|
|
843
|
+
msg: "No PuzzleCaptcha record found"
|
|
844
|
+
}));
|
|
845
|
+
return null;
|
|
846
|
+
} catch (error) {
|
|
847
|
+
const err = new ProsopoDBError("DATABASE.CAPTCHA_GET_FAILED", {
|
|
848
|
+
context: { error, challenge },
|
|
849
|
+
logger: this.logger
|
|
850
|
+
});
|
|
851
|
+
this.logger.error(() => ({
|
|
852
|
+
err,
|
|
853
|
+
msg: "Failed to retrieve PuzzleCaptcha record"
|
|
854
|
+
}));
|
|
855
|
+
throw err;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* @description Updates a Puzzle Captcha record result in the database.
|
|
860
|
+
* @param {string} challenge The challenge string of the captcha to be updated.
|
|
861
|
+
* @param result
|
|
862
|
+
* @param serverChecked
|
|
863
|
+
* @param userSubmitted
|
|
864
|
+
* @param userSignature
|
|
865
|
+
* @param coords
|
|
866
|
+
* @param lastUpdatedTimestamp
|
|
867
|
+
* @returns {Promise<void>} A promise that resolves when the record is updated.
|
|
868
|
+
*/
|
|
869
|
+
async updatePuzzleCaptchaRecordResult(challenge, result, serverChecked = false, userSubmitted = false, userSignature, coords, lastUpdatedTimestamp) {
|
|
870
|
+
const tables = this.getTables();
|
|
871
|
+
const timestamp = lastUpdatedTimestamp ?? /* @__PURE__ */ new Date();
|
|
872
|
+
const update = {
|
|
873
|
+
result,
|
|
874
|
+
serverChecked,
|
|
875
|
+
userSubmitted,
|
|
876
|
+
userSignature,
|
|
877
|
+
lastUpdatedTimestamp: timestamp,
|
|
878
|
+
pendingStage: true,
|
|
879
|
+
...coords && { coords }
|
|
880
|
+
};
|
|
881
|
+
try {
|
|
882
|
+
const updateResult = await tables.puzzlecaptcha.updateOne(
|
|
883
|
+
{ challenge },
|
|
884
|
+
{
|
|
885
|
+
$set: update
|
|
886
|
+
}
|
|
887
|
+
);
|
|
888
|
+
if (updateResult.matchedCount === 0) {
|
|
889
|
+
const err = new ProsopoDBError("DATABASE.CAPTCHA_GET_FAILED", {
|
|
890
|
+
context: {
|
|
891
|
+
challenge,
|
|
892
|
+
...update
|
|
893
|
+
},
|
|
894
|
+
logger: this.logger
|
|
895
|
+
});
|
|
896
|
+
this.logger.info(() => ({
|
|
897
|
+
err,
|
|
898
|
+
msg: "No PuzzleCaptcha record found to update"
|
|
899
|
+
}));
|
|
900
|
+
throw err;
|
|
901
|
+
}
|
|
902
|
+
this.logger.info(() => ({
|
|
903
|
+
data: {
|
|
904
|
+
challenge,
|
|
905
|
+
...update
|
|
906
|
+
},
|
|
907
|
+
msg: "PuzzleCaptcha record updated successfully"
|
|
908
|
+
}));
|
|
909
|
+
} catch (error) {
|
|
910
|
+
const err = new ProsopoDBError("DATABASE.CAPTCHA_UPDATE_FAILED", {
|
|
911
|
+
context: {
|
|
912
|
+
error,
|
|
913
|
+
challenge,
|
|
914
|
+
...update
|
|
915
|
+
},
|
|
916
|
+
logger: this.logger
|
|
917
|
+
});
|
|
918
|
+
this.logger.error(() => ({
|
|
919
|
+
err,
|
|
920
|
+
msg: "Failed to update PuzzleCaptcha record"
|
|
921
|
+
}));
|
|
922
|
+
throw err;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
async updatePuzzleCaptchaRecord(challenge, updates) {
|
|
926
|
+
const tables = this.getTables();
|
|
927
|
+
await tables.puzzlecaptcha.updateOne(
|
|
928
|
+
{ challenge },
|
|
929
|
+
{ $set: { ...updates, pendingStage: true } },
|
|
652
930
|
{ upsert: false }
|
|
653
931
|
);
|
|
654
932
|
}
|
|
@@ -660,44 +938,38 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
660
938
|
return docs || [];
|
|
661
939
|
}
|
|
662
940
|
/** @description Get Dapp User captcha commitments from the commitments table that have not been counted towards the
|
|
663
|
-
* client's total
|
|
941
|
+
* client's total.
|
|
942
|
+
*
|
|
943
|
+
* Served by the `pendingStage_partial` index. Records have
|
|
944
|
+
* `pendingStage: true` set on insert and on every mutation (see
|
|
945
|
+
* `updateDappUserCommitment`, `markDappUserCommitmentsChecked`,
|
|
946
|
+
* `approveDappUserCommitment`, `disapproveDappUserCommitment`,
|
|
947
|
+
* `storePendingImageCommitment`). `markDappUserCommitmentsStored` clears
|
|
948
|
+
* the flag after a successful stage, guarded by `lastUpdatedTimestamp`
|
|
949
|
+
* so an in-flight update isn't lost.
|
|
664
950
|
*/
|
|
665
951
|
async getUnstoredDappUserCommitments(limit = 1e3, skip = 0) {
|
|
666
|
-
const
|
|
667
|
-
const docs = await this.tables?.commitment.aggregate([
|
|
668
|
-
{
|
|
669
|
-
$match: {
|
|
670
|
-
$or: [
|
|
671
|
-
filterNoStoredTimestamp,
|
|
672
|
-
{
|
|
673
|
-
$expr: {
|
|
674
|
-
$lt: ["$storedAtTimestamp", "$lastUpdatedTimestamp"]
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
]
|
|
678
|
-
}
|
|
679
|
-
},
|
|
680
|
-
{
|
|
681
|
-
$sort: { _id: 1 }
|
|
682
|
-
},
|
|
683
|
-
{
|
|
684
|
-
$skip: skip
|
|
685
|
-
},
|
|
686
|
-
{
|
|
687
|
-
$limit: limit
|
|
688
|
-
}
|
|
689
|
-
]);
|
|
952
|
+
const docs = await this.tables?.commitment.find({ pendingStage: true }).sort({ _id: 1 }).skip(skip).limit(limit).lean();
|
|
690
953
|
return docs || [];
|
|
691
954
|
}
|
|
692
|
-
/** @description Mark a list of captcha commits as stored
|
|
955
|
+
/** @description Mark a list of captcha commits as stored.
|
|
956
|
+
*
|
|
957
|
+
* `asOfTimestamp` defaults to "now" but the sweep should pass the time
|
|
958
|
+
* at which it fetched the batch. The lastUpdatedTimestamp guard prevents
|
|
959
|
+
* us from clearing pendingStage on a record that was mutated between
|
|
960
|
+
* fetch and mark-stored — those records will leave pendingStage set so
|
|
961
|
+
* the next sweep picks them up.
|
|
693
962
|
*/
|
|
694
|
-
async markDappUserCommitmentsStored(commitmentIds) {
|
|
695
|
-
const updateDoc = {
|
|
696
|
-
storedAtTimestamp: /* @__PURE__ */ new Date()
|
|
697
|
-
};
|
|
963
|
+
async markDappUserCommitmentsStored(commitmentIds, asOfTimestamp = /* @__PURE__ */ new Date()) {
|
|
698
964
|
await this.tables?.commitment.updateMany(
|
|
699
|
-
{
|
|
700
|
-
|
|
965
|
+
{
|
|
966
|
+
id: { $in: commitmentIds },
|
|
967
|
+
lastUpdatedTimestamp: { $lte: asOfTimestamp }
|
|
968
|
+
},
|
|
969
|
+
{
|
|
970
|
+
$set: { storedAtTimestamp: /* @__PURE__ */ new Date() },
|
|
971
|
+
$unset: { pendingStage: 1 }
|
|
972
|
+
},
|
|
701
973
|
{ upsert: false }
|
|
702
974
|
);
|
|
703
975
|
}
|
|
@@ -706,7 +978,8 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
706
978
|
async markDappUserCommitmentsChecked(commitmentIds) {
|
|
707
979
|
const updateDoc = {
|
|
708
980
|
[StoredStatusNames.serverChecked]: true,
|
|
709
|
-
lastUpdatedTimestamp: /* @__PURE__ */ new Date()
|
|
981
|
+
lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
|
|
982
|
+
pendingStage: true
|
|
710
983
|
};
|
|
711
984
|
await this.tables?.commitment.updateMany(
|
|
712
985
|
{ id: { $in: commitmentIds } },
|
|
@@ -718,7 +991,11 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
718
991
|
*/
|
|
719
992
|
async updateDappUserCommitment(commitmentId, updates) {
|
|
720
993
|
const filter = { id: commitmentId };
|
|
721
|
-
await this.tables?.commitment.updateOne(filter,
|
|
994
|
+
await this.tables?.commitment.updateOne(filter, {
|
|
995
|
+
...updates,
|
|
996
|
+
lastUpdatedAtTimestamp: /* @__PURE__ */ new Date(),
|
|
997
|
+
pendingStage: true
|
|
998
|
+
});
|
|
722
999
|
}
|
|
723
1000
|
/**
|
|
724
1001
|
* @description Get Dapp User PoW captcha commitments that have not been counted towards the client's total
|
|
@@ -727,54 +1004,25 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
727
1004
|
* @returns {Promise<PoWCaptchaRecord[]>} Array of PoW captcha records
|
|
728
1005
|
*/
|
|
729
1006
|
async getUnstoredDappUserPoWCommitments(limit = 1e3, skip = 0) {
|
|
730
|
-
const
|
|
731
|
-
const docs = await this.tables?.powcaptcha.aggregate([
|
|
732
|
-
{
|
|
733
|
-
$match: {
|
|
734
|
-
$or: [
|
|
735
|
-
filterNoStoredTimestamp,
|
|
736
|
-
{
|
|
737
|
-
$expr: {
|
|
738
|
-
$lt: [
|
|
739
|
-
{
|
|
740
|
-
$convert: {
|
|
741
|
-
input: "$storedAtTimestamp",
|
|
742
|
-
to: "date"
|
|
743
|
-
}
|
|
744
|
-
},
|
|
745
|
-
{
|
|
746
|
-
$convert: {
|
|
747
|
-
input: "$lastUpdatedTimestamp",
|
|
748
|
-
to: "date"
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
]
|
|
752
|
-
}
|
|
753
|
-
}
|
|
754
|
-
]
|
|
755
|
-
}
|
|
756
|
-
},
|
|
757
|
-
{
|
|
758
|
-
$sort: { _id: 1 }
|
|
759
|
-
},
|
|
760
|
-
{
|
|
761
|
-
$skip: skip
|
|
762
|
-
},
|
|
763
|
-
{
|
|
764
|
-
$limit: limit
|
|
765
|
-
}
|
|
766
|
-
]);
|
|
1007
|
+
const docs = await this.tables?.powcaptcha.find({ pendingStage: true }).sort({ _id: 1 }).skip(skip).limit(limit).lean();
|
|
767
1008
|
return docs || [];
|
|
768
1009
|
}
|
|
769
|
-
/** @description Mark a list of PoW captcha commits as stored
|
|
1010
|
+
/** @description Mark a list of PoW captcha commits as stored.
|
|
1011
|
+
*
|
|
1012
|
+
* `asOfTimestamp` defaults to "now" but the sweep should pass the time
|
|
1013
|
+
* at which it fetched the batch. See markDappUserCommitmentsStored for
|
|
1014
|
+
* the guard rationale.
|
|
770
1015
|
*/
|
|
771
|
-
async markDappUserPoWCommitmentsStored(challenges) {
|
|
772
|
-
const updateDoc = {
|
|
773
|
-
storedAtTimestamp: /* @__PURE__ */ new Date()
|
|
774
|
-
};
|
|
1016
|
+
async markDappUserPoWCommitmentsStored(challenges, asOfTimestamp = /* @__PURE__ */ new Date()) {
|
|
775
1017
|
await this.tables?.powcaptcha.updateMany(
|
|
776
|
-
{
|
|
777
|
-
|
|
1018
|
+
{
|
|
1019
|
+
challenge: { $in: challenges },
|
|
1020
|
+
lastUpdatedTimestamp: { $lte: asOfTimestamp }
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
$set: { storedAtTimestamp: /* @__PURE__ */ new Date() },
|
|
1024
|
+
$unset: { pendingStage: 1 }
|
|
1025
|
+
},
|
|
778
1026
|
{ upsert: false }
|
|
779
1027
|
);
|
|
780
1028
|
}
|
|
@@ -783,7 +1031,8 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
783
1031
|
async markDappUserPoWCommitmentsChecked(challenges) {
|
|
784
1032
|
const updateDoc = {
|
|
785
1033
|
[StoredStatusNames.serverChecked]: true,
|
|
786
|
-
lastUpdatedTimestamp: /* @__PURE__ */ new Date()
|
|
1034
|
+
lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
|
|
1035
|
+
pendingStage: true
|
|
787
1036
|
};
|
|
788
1037
|
await this.tables?.powcaptcha.updateMany(
|
|
789
1038
|
{ challenge: { $in: challenges } },
|
|
@@ -801,7 +1050,26 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
801
1050
|
this.logger.debug(() => ({
|
|
802
1051
|
data: { action: "storing", sessionRecord }
|
|
803
1052
|
}));
|
|
804
|
-
|
|
1053
|
+
const recordWithFlag = {
|
|
1054
|
+
...sessionRecord,
|
|
1055
|
+
lastUpdatedTimestamp: sessionRecord.lastUpdatedTimestamp ?? sessionRecord.createdAt,
|
|
1056
|
+
pendingStage: true
|
|
1057
|
+
};
|
|
1058
|
+
await this.tables.session.create(recordWithFlag);
|
|
1059
|
+
this.centralStreamer?.streamSessionRecord(
|
|
1060
|
+
recordWithFlag,
|
|
1061
|
+
(ts) => this.tables.session.updateOne(
|
|
1062
|
+
{
|
|
1063
|
+
sessionId: sessionRecord.sessionId,
|
|
1064
|
+
lastUpdatedTimestamp: { $lte: ts }
|
|
1065
|
+
},
|
|
1066
|
+
{
|
|
1067
|
+
$set: { storedAtTimestamp: ts },
|
|
1068
|
+
$unset: { pendingStage: 1 }
|
|
1069
|
+
}
|
|
1070
|
+
).then(() => {
|
|
1071
|
+
})
|
|
1072
|
+
);
|
|
805
1073
|
} catch (err) {
|
|
806
1074
|
throw new ProsopoDBError("DATABASE.SESSION_STORE_FAILED", {
|
|
807
1075
|
context: { error: err, sessionId: sessionRecord.sessionId },
|
|
@@ -814,7 +1082,14 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
814
1082
|
*/
|
|
815
1083
|
async getSessionRecordBySessionId(sessionId) {
|
|
816
1084
|
const filter = { sessionId };
|
|
817
|
-
const doc = await this.tables.session.findOne(filter
|
|
1085
|
+
const doc = await this.tables.session.findOne(filter, {
|
|
1086
|
+
sessionId: 1,
|
|
1087
|
+
ipInfo: 1,
|
|
1088
|
+
scoreComponents: 1,
|
|
1089
|
+
webView: 1,
|
|
1090
|
+
reason: 1,
|
|
1091
|
+
decryptedHeadHash: 1
|
|
1092
|
+
}).lean();
|
|
818
1093
|
return doc || void 0;
|
|
819
1094
|
}
|
|
820
1095
|
/**
|
|
@@ -841,7 +1116,8 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
841
1116
|
try {
|
|
842
1117
|
const session = await this.tables.session.findOneAndUpdate(filter, {
|
|
843
1118
|
deleted: true,
|
|
844
|
-
lastUpdatedTimestamp: /* @__PURE__ */ new Date()
|
|
1119
|
+
lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
|
|
1120
|
+
pendingStage: true
|
|
845
1121
|
}).lean();
|
|
846
1122
|
return session || void 0;
|
|
847
1123
|
} catch (err) {
|
|
@@ -851,6 +1127,74 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
851
1127
|
});
|
|
852
1128
|
}
|
|
853
1129
|
}
|
|
1130
|
+
/**
|
|
1131
|
+
* Update a session record by sessionId. Pure Mongo write — callers in
|
|
1132
|
+
* the provider package are responsible for refreshing the Redis cache
|
|
1133
|
+
* via `RedisWriteQueue.patchCachedSession`, matching the existing
|
|
1134
|
+
* caller-side caching pattern (see `frictionlessTasks.createSession`).
|
|
1135
|
+
*/
|
|
1136
|
+
async updateSessionRecord(sessionId, updates, streamToCentral) {
|
|
1137
|
+
try {
|
|
1138
|
+
await this.tables.session.updateOne(
|
|
1139
|
+
{ sessionId },
|
|
1140
|
+
{
|
|
1141
|
+
$set: {
|
|
1142
|
+
...updates,
|
|
1143
|
+
lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
|
|
1144
|
+
pendingStage: true
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
);
|
|
1148
|
+
if (streamToCentral && this.centralStreamer) {
|
|
1149
|
+
const streamer = this.centralStreamer;
|
|
1150
|
+
const markStored = (ts) => this.tables.session.updateOne(
|
|
1151
|
+
{ sessionId, lastUpdatedTimestamp: { $lte: ts } },
|
|
1152
|
+
{
|
|
1153
|
+
$set: { storedAtTimestamp: ts },
|
|
1154
|
+
$unset: { pendingStage: 1 }
|
|
1155
|
+
}
|
|
1156
|
+
).then(() => {
|
|
1157
|
+
});
|
|
1158
|
+
this.tables.session.findOne({ sessionId }).lean().then((record) => {
|
|
1159
|
+
if (record) {
|
|
1160
|
+
streamer.streamSessionRecord(record, markStored);
|
|
1161
|
+
}
|
|
1162
|
+
}).catch(() => {
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
} catch (err) {
|
|
1166
|
+
throw new ProsopoDBError("DATABASE.SESSION_GET_FAILED", {
|
|
1167
|
+
context: { error: err, sessionId },
|
|
1168
|
+
logger: this.logger
|
|
1169
|
+
});
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Atomically record SIMD CPU fingerprint readings on the session — only
|
|
1174
|
+
* if they aren't already present. Uses a Mongo aggregation-pipeline
|
|
1175
|
+
* update so `simdReadings` and `simdReadingsStage` are set together,
|
|
1176
|
+
* first-hop-wins. Pure Mongo write — cache refresh is the caller's
|
|
1177
|
+
* responsibility via `RedisWriteQueue.patchCachedSimdReadingsIfAbsent`.
|
|
1178
|
+
*/
|
|
1179
|
+
async recordSessionSimdReadingsIfAbsent(sessionId, readings, stage) {
|
|
1180
|
+
try {
|
|
1181
|
+
await this.tables.session.updateOne({ sessionId }, [
|
|
1182
|
+
{
|
|
1183
|
+
$set: {
|
|
1184
|
+
simdReadings: { $ifNull: ["$simdReadings", readings] },
|
|
1185
|
+
simdReadingsStage: { $ifNull: ["$simdReadingsStage", stage] },
|
|
1186
|
+
lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
|
|
1187
|
+
pendingStage: true
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
]);
|
|
1191
|
+
} catch (err) {
|
|
1192
|
+
throw new ProsopoDBError("DATABASE.SESSION_GET_FAILED", {
|
|
1193
|
+
context: { error: err, sessionId, stage },
|
|
1194
|
+
logger: this.logger
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
854
1198
|
/**
|
|
855
1199
|
* Get an active session by user IP hash
|
|
856
1200
|
* @param userSitekeyIpHash The hash of user, IP and sitekey combination
|
|
@@ -875,64 +1219,43 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
875
1219
|
}
|
|
876
1220
|
}
|
|
877
1221
|
/** Get unstored session records
|
|
878
|
-
* @description Get session records that have not been stored yet
|
|
1222
|
+
* @description Get session records that have not been stored yet.
|
|
1223
|
+
*
|
|
1224
|
+
* Served by the `pendingStage_partial` index — see
|
|
1225
|
+
* `getUnstoredDappUserCommitments` for the lifecycle of the flag.
|
|
1226
|
+
* `checkAndRemoveSession` also flips the flag so consumed sessions
|
|
1227
|
+
* propagate to the central DB via the next sweep.
|
|
879
1228
|
* @param limit
|
|
880
1229
|
* @param skip
|
|
881
1230
|
*/
|
|
882
1231
|
getUnstoredSessionRecords(limit = 1e3, skip = 0) {
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
to: "date"
|
|
896
|
-
}
|
|
897
|
-
},
|
|
898
|
-
{
|
|
899
|
-
$convert: {
|
|
900
|
-
input: "$lastUpdatedTimestamp",
|
|
901
|
-
to: "date"
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
]
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
]
|
|
908
|
-
}
|
|
909
|
-
},
|
|
1232
|
+
return Promise.resolve(this.tables?.session).then(
|
|
1233
|
+
(tbl) => tbl?.find({ pendingStage: true }).sort({ _id: 1 }).skip(skip).limit(limit).lean()
|
|
1234
|
+
).then((docs) => docs || []);
|
|
1235
|
+
}
|
|
1236
|
+
/** Mark a list of session records as stored.
|
|
1237
|
+
*
|
|
1238
|
+
* `asOfTimestamp` defaults to "now" but the sweep should pass the time
|
|
1239
|
+
* at which it fetched the batch. See markDappUserCommitmentsStored for
|
|
1240
|
+
* the guard rationale.
|
|
1241
|
+
*/
|
|
1242
|
+
async markSessionRecordsStored(sessionIds, asOfTimestamp = /* @__PURE__ */ new Date()) {
|
|
1243
|
+
await this.tables?.session.updateMany(
|
|
910
1244
|
{
|
|
911
|
-
|
|
1245
|
+
sessionId: { $in: sessionIds },
|
|
1246
|
+
lastUpdatedTimestamp: { $lte: asOfTimestamp }
|
|
912
1247
|
},
|
|
913
1248
|
{
|
|
914
|
-
$
|
|
1249
|
+
$set: { storedAtTimestamp: /* @__PURE__ */ new Date() },
|
|
1250
|
+
$unset: { pendingStage: 1 }
|
|
915
1251
|
},
|
|
916
|
-
{
|
|
917
|
-
$limit: limit
|
|
918
|
-
}
|
|
919
|
-
]).then((docs) => docs || []);
|
|
920
|
-
}
|
|
921
|
-
/** Mark a list of session records as stored */
|
|
922
|
-
async markSessionRecordsStored(sessionIds) {
|
|
923
|
-
const updateDoc = {
|
|
924
|
-
storedAtTimestamp: /* @__PURE__ */ new Date()
|
|
925
|
-
};
|
|
926
|
-
await this.tables?.session.updateMany(
|
|
927
|
-
{ sessionId: { $in: sessionIds } },
|
|
928
|
-
{ $set: updateDoc },
|
|
929
1252
|
{ upsert: false }
|
|
930
1253
|
);
|
|
931
1254
|
}
|
|
932
1255
|
/**
|
|
933
1256
|
* @description Store a Dapp User's pending record
|
|
934
1257
|
*/
|
|
935
|
-
async storePendingImageCommitment(userAccount, requestHash, salt, deadlineTimestamp, requestedAtTimestamp, ipAddress, threshold, sessionId) {
|
|
1258
|
+
async storePendingImageCommitment(userAccount, requestHash, salt, deadlineTimestamp, requestedAtTimestamp, ipAddress, threshold, sessionId, ipInfo) {
|
|
936
1259
|
if (!isHex(requestHash)) {
|
|
937
1260
|
throw new ProsopoDBError("DATABASE.INVALID_HASH", {
|
|
938
1261
|
context: {
|
|
@@ -942,24 +1265,47 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
942
1265
|
});
|
|
943
1266
|
}
|
|
944
1267
|
const pendingRecord = {
|
|
945
|
-
|
|
1268
|
+
userAccount,
|
|
946
1269
|
pending: true,
|
|
947
1270
|
salt,
|
|
948
1271
|
requestHash,
|
|
949
1272
|
deadlineTimestamp,
|
|
950
|
-
requestedAtTimestamp
|
|
1273
|
+
requestedAtTimestamp,
|
|
951
1274
|
ipAddress,
|
|
952
1275
|
sessionId,
|
|
953
|
-
threshold
|
|
1276
|
+
threshold,
|
|
1277
|
+
ipInfo,
|
|
1278
|
+
// Deliberately NOT setting pendingStage here. Placeholder
|
|
1279
|
+
// records have id: "" until the user submits a solution; if we
|
|
1280
|
+
// flag them, the sweep picks them up via the partial index but
|
|
1281
|
+
// then `markDappUserCommitmentsStored` runs
|
|
1282
|
+
// `{ id: { $in: ["", ...] } }` which collapses to a single ""
|
|
1283
|
+
// bound and scans every empty-id row via the `id_-1` index —
|
|
1284
|
+
// turning a cheap sweep into a fresh cache evictor. The real
|
|
1285
|
+
// commitment only gets `pendingStage: true` once `id` is
|
|
1286
|
+
// populated by approve/disapprove, at which point staging it is
|
|
1287
|
+
// meaningful.
|
|
1288
|
+
// Placeholder fields required by schema but not needed for pending state
|
|
1289
|
+
dappAccount: "",
|
|
1290
|
+
providerAccount: "",
|
|
1291
|
+
datasetId: "",
|
|
1292
|
+
id: "",
|
|
1293
|
+
// id is populated by the user's solution record when the user submits a solution, so we can leave it blank here
|
|
1294
|
+
result: { status: CaptchaStatus.pending },
|
|
1295
|
+
headers: {},
|
|
1296
|
+
ja4: "",
|
|
1297
|
+
userSignature: "",
|
|
1298
|
+
userSubmitted: false,
|
|
1299
|
+
serverChecked: false
|
|
954
1300
|
};
|
|
955
|
-
await this.tables?.
|
|
1301
|
+
await this.tables?.commitment.updateOne(
|
|
956
1302
|
{ requestHash },
|
|
957
1303
|
{ $set: pendingRecord },
|
|
958
1304
|
{ upsert: true }
|
|
959
1305
|
);
|
|
960
1306
|
}
|
|
961
1307
|
/**
|
|
962
|
-
* @description Get a
|
|
1308
|
+
* @description Get a user's pending record
|
|
963
1309
|
*/
|
|
964
1310
|
async getPendingImageCommitment(requestHash) {
|
|
965
1311
|
if (!isHex(requestHash)) {
|
|
@@ -970,12 +1316,22 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
970
1316
|
}
|
|
971
1317
|
});
|
|
972
1318
|
}
|
|
973
|
-
const
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1319
|
+
const doc = await this.tables?.commitment.findOne({
|
|
1320
|
+
requestHash,
|
|
1321
|
+
pending: true
|
|
1322
|
+
}).lean();
|
|
977
1323
|
if (doc) {
|
|
978
|
-
return
|
|
1324
|
+
return {
|
|
1325
|
+
dappAccount: doc.dappAccount,
|
|
1326
|
+
pending: doc.pending,
|
|
1327
|
+
salt: doc.salt,
|
|
1328
|
+
requestHash: doc.requestHash,
|
|
1329
|
+
deadlineTimestamp: doc.deadlineTimestamp,
|
|
1330
|
+
requestedAtTimestamp: doc.requestedAtTimestamp,
|
|
1331
|
+
ipAddress: doc.ipAddress,
|
|
1332
|
+
sessionId: doc.sessionId,
|
|
1333
|
+
threshold: doc.threshold
|
|
1334
|
+
};
|
|
979
1335
|
}
|
|
980
1336
|
throw new ProsopoDBError("DATABASE.PENDING_RECORD_NOT_FOUND", {
|
|
981
1337
|
context: {
|
|
@@ -996,17 +1352,13 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
996
1352
|
}
|
|
997
1353
|
});
|
|
998
1354
|
}
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
};
|
|
1002
|
-
await this.tables?.pending.updateOne(
|
|
1003
|
-
filter,
|
|
1355
|
+
await this.tables?.commitment.updateOne(
|
|
1356
|
+
{ requestHash },
|
|
1004
1357
|
{
|
|
1005
1358
|
$set: {
|
|
1006
|
-
|
|
1359
|
+
pending: false
|
|
1007
1360
|
}
|
|
1008
|
-
}
|
|
1009
|
-
{ upsert: true }
|
|
1361
|
+
}
|
|
1010
1362
|
);
|
|
1011
1363
|
}
|
|
1012
1364
|
/**
|
|
@@ -1107,7 +1459,7 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
1107
1459
|
const filter = {
|
|
1108
1460
|
commitmentId
|
|
1109
1461
|
};
|
|
1110
|
-
const project = {
|
|
1462
|
+
const project = { _id: 0 };
|
|
1111
1463
|
const cursor = this.tables?.usersolution?.findOne(filter, project).lean();
|
|
1112
1464
|
const doc = await cursor;
|
|
1113
1465
|
if (doc) {
|
|
@@ -1123,7 +1475,18 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
1123
1475
|
*/
|
|
1124
1476
|
async getDappUserCommitmentById(commitmentId) {
|
|
1125
1477
|
const filter = { id: commitmentId };
|
|
1126
|
-
const commitmentCursor = this.tables?.commitment?.findOne(filter
|
|
1478
|
+
const commitmentCursor = this.tables?.commitment?.findOne(filter, {
|
|
1479
|
+
id: 1,
|
|
1480
|
+
result: 1,
|
|
1481
|
+
serverChecked: 1,
|
|
1482
|
+
requestedAtTimestamp: 1,
|
|
1483
|
+
ipAddress: 1,
|
|
1484
|
+
sessionId: 1,
|
|
1485
|
+
userAccount: 1,
|
|
1486
|
+
dappAccount: 1,
|
|
1487
|
+
headers: 1,
|
|
1488
|
+
ipInfo: 1
|
|
1489
|
+
}).lean();
|
|
1127
1490
|
const doc = await commitmentCursor;
|
|
1128
1491
|
return doc ? doc : void 0;
|
|
1129
1492
|
}
|
|
@@ -1137,7 +1500,10 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
1137
1500
|
userAccount,
|
|
1138
1501
|
dappAccount
|
|
1139
1502
|
};
|
|
1140
|
-
const project = {
|
|
1503
|
+
const project = {
|
|
1504
|
+
_id: 0,
|
|
1505
|
+
result: 1
|
|
1506
|
+
};
|
|
1141
1507
|
const sort = { sort: { _id: -1 } };
|
|
1142
1508
|
const docs = await this.tables?.commitment?.find(filter, project, sort).lean();
|
|
1143
1509
|
return docs ? docs : [];
|
|
@@ -1153,10 +1519,22 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
1153
1519
|
const updateDoc = {
|
|
1154
1520
|
result,
|
|
1155
1521
|
lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
|
|
1522
|
+
pendingStage: true,
|
|
1156
1523
|
...coords ? { coords } : {}
|
|
1157
1524
|
};
|
|
1158
1525
|
const filter = { id: commitmentId };
|
|
1159
1526
|
await this.tables?.commitment?.findOneAndUpdate(filter, { $set: updateDoc }, { upsert: false }).lean();
|
|
1527
|
+
this.centralStreamer?.streamImageUpdate(
|
|
1528
|
+
() => this.tables.commitment.findOne({ id: commitmentId }).lean(),
|
|
1529
|
+
(ts) => this.tables.commitment.updateOne(
|
|
1530
|
+
{ id: commitmentId, lastUpdatedTimestamp: { $lte: ts } },
|
|
1531
|
+
{
|
|
1532
|
+
$set: { storedAtTimestamp: ts },
|
|
1533
|
+
$unset: { pendingStage: 1 }
|
|
1534
|
+
}
|
|
1535
|
+
).then(() => {
|
|
1536
|
+
})
|
|
1537
|
+
);
|
|
1160
1538
|
} catch (err) {
|
|
1161
1539
|
throw new ProsopoDBError("DATABASE.SOLUTION_APPROVE_FAILED", {
|
|
1162
1540
|
context: { error: err, commitmentId }
|
|
@@ -1174,10 +1552,22 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
1174
1552
|
const updateDoc = {
|
|
1175
1553
|
result: { status: CaptchaStatus.disapproved, reason },
|
|
1176
1554
|
lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
|
|
1555
|
+
pendingStage: true,
|
|
1177
1556
|
...coords ? { coords } : {}
|
|
1178
1557
|
};
|
|
1179
1558
|
const filter = { id: commitmentId };
|
|
1180
1559
|
await this.tables?.commitment?.findOneAndUpdate(filter, { $set: updateDoc }, { upsert: false }).lean();
|
|
1560
|
+
this.centralStreamer?.streamImageUpdate(
|
|
1561
|
+
() => this.tables.commitment.findOne({ id: commitmentId }).lean(),
|
|
1562
|
+
(ts) => this.tables.commitment.updateOne(
|
|
1563
|
+
{ id: commitmentId, lastUpdatedTimestamp: { $lte: ts } },
|
|
1564
|
+
{
|
|
1565
|
+
$set: { storedAtTimestamp: ts },
|
|
1566
|
+
$unset: { pendingStage: 1 }
|
|
1567
|
+
}
|
|
1568
|
+
).then(() => {
|
|
1569
|
+
})
|
|
1570
|
+
);
|
|
1181
1571
|
} catch (err) {
|
|
1182
1572
|
throw new ProsopoDBError("DATABASE.SOLUTION_APPROVE_FAILED", {
|
|
1183
1573
|
context: { error: err, commitmentId }
|
|
@@ -1313,6 +1703,17 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
1313
1703
|
}
|
|
1314
1704
|
await this.tables?.client.bulkWrite(ops);
|
|
1315
1705
|
}
|
|
1706
|
+
/**
|
|
1707
|
+
* @description Remove client records by account
|
|
1708
|
+
*/
|
|
1709
|
+
async removeClientRecords(accounts) {
|
|
1710
|
+
if (!accounts || accounts.length === 0) {
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1713
|
+
await this.tables?.client.deleteMany({
|
|
1714
|
+
account: { $in: accounts }
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1316
1717
|
/**
|
|
1317
1718
|
* @description Get all client records
|
|
1318
1719
|
*/
|
|
@@ -1325,7 +1726,11 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
1325
1726
|
*/
|
|
1326
1727
|
async getClientRecord(account) {
|
|
1327
1728
|
const filter = { account };
|
|
1328
|
-
const doc = await this.tables?.client.findOne(filter
|
|
1729
|
+
const doc = await this.tables?.client.findOne(filter, {
|
|
1730
|
+
account: 1,
|
|
1731
|
+
settings: 1,
|
|
1732
|
+
tier: 1
|
|
1733
|
+
}).lean();
|
|
1329
1734
|
return doc ? doc : void 0;
|
|
1330
1735
|
}
|
|
1331
1736
|
/**
|
|
@@ -1363,6 +1768,97 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
1363
1768
|
).sort({ createdAt: -1 }).lean();
|
|
1364
1769
|
return (keyRecords || []).map((record) => record.detectorKey);
|
|
1365
1770
|
}
|
|
1771
|
+
/**
|
|
1772
|
+
* Stores a decision machine artifact with a unique scope identifier.
|
|
1773
|
+
*
|
|
1774
|
+
* The combination of scope + dappAccount uniquely identifies a single artifact:
|
|
1775
|
+
* - Global scope: One artifact per provider (dappAccount is null)
|
|
1776
|
+
* - Dapp scope: One artifact per dapp account (dappAccount is specified)
|
|
1777
|
+
*
|
|
1778
|
+
* @param artifact - The decision machine artifact to store
|
|
1779
|
+
*/
|
|
1780
|
+
async upsertDecisionMachineArtifact(artifact) {
|
|
1781
|
+
const now = /* @__PURE__ */ new Date();
|
|
1782
|
+
const dappAccount = artifact.dappAccount ?? null;
|
|
1783
|
+
const filter = {
|
|
1784
|
+
scope: artifact.scope,
|
|
1785
|
+
dappAccount
|
|
1786
|
+
};
|
|
1787
|
+
await this.tables?.decisionMachine.updateOne(
|
|
1788
|
+
filter,
|
|
1789
|
+
{
|
|
1790
|
+
$set: {
|
|
1791
|
+
scope: artifact.scope,
|
|
1792
|
+
dappAccount,
|
|
1793
|
+
runtime: artifact.runtime,
|
|
1794
|
+
language: artifact.language,
|
|
1795
|
+
source: artifact.source,
|
|
1796
|
+
name: artifact.name,
|
|
1797
|
+
version: artifact.version,
|
|
1798
|
+
updatedAt: now
|
|
1799
|
+
},
|
|
1800
|
+
$setOnInsert: {
|
|
1801
|
+
createdAt: now
|
|
1802
|
+
}
|
|
1803
|
+
},
|
|
1804
|
+
{ upsert: true }
|
|
1805
|
+
);
|
|
1806
|
+
}
|
|
1807
|
+
/**
|
|
1808
|
+
* Retrieves a single decision machine artifact by scope and optional dapp account.
|
|
1809
|
+
*
|
|
1810
|
+
* @param scope - The scope level (Global or Dapp)
|
|
1811
|
+
* @param dappAccount - Required for Dapp scope, unused for Global scope
|
|
1812
|
+
* @returns The matching artifact, or undefined if not found
|
|
1813
|
+
*/
|
|
1814
|
+
async getDecisionMachineArtifact(scope, dappAccount) {
|
|
1815
|
+
const filter = {
|
|
1816
|
+
scope,
|
|
1817
|
+
dappAccount: dappAccount ?? null
|
|
1818
|
+
};
|
|
1819
|
+
const doc = await this.tables?.decisionMachine.findOne(filter).lean();
|
|
1820
|
+
return doc ?? void 0;
|
|
1821
|
+
}
|
|
1822
|
+
/**
|
|
1823
|
+
* Retrieves all decision machine artifacts.
|
|
1824
|
+
*
|
|
1825
|
+
* @returns Array of all decision machine artifacts
|
|
1826
|
+
*/
|
|
1827
|
+
async getAllDecisionMachineArtifacts() {
|
|
1828
|
+
const docs = await this.tables?.decisionMachine.find({}).lean();
|
|
1829
|
+
return docs ?? [];
|
|
1830
|
+
}
|
|
1831
|
+
/**
|
|
1832
|
+
* Retrieves a single decision machine artifact by MongoDB ObjectId.
|
|
1833
|
+
*
|
|
1834
|
+
* @param id - The MongoDB ObjectId as a string
|
|
1835
|
+
* @returns The matching artifact, or undefined if not found
|
|
1836
|
+
*/
|
|
1837
|
+
async getDecisionMachineArtifactById(id) {
|
|
1838
|
+
const doc = await this.tables?.decisionMachine.findById(id).lean();
|
|
1839
|
+
return doc ?? void 0;
|
|
1840
|
+
}
|
|
1841
|
+
/**
|
|
1842
|
+
* Removes a decision machine artifact by MongoDB ObjectId.
|
|
1843
|
+
*
|
|
1844
|
+
* @param id - The MongoDB ObjectId as a string
|
|
1845
|
+
* @returns true if deleted, false if not found
|
|
1846
|
+
*/
|
|
1847
|
+
async removeDecisionMachineArtifact(id) {
|
|
1848
|
+
const result = await this.tables?.decisionMachine.deleteOne({
|
|
1849
|
+
_id: id
|
|
1850
|
+
});
|
|
1851
|
+
return (result?.deletedCount ?? 0) > 0;
|
|
1852
|
+
}
|
|
1853
|
+
/**
|
|
1854
|
+
* Removes all decision machine artifacts.
|
|
1855
|
+
*
|
|
1856
|
+
* @returns The number of artifacts deleted
|
|
1857
|
+
*/
|
|
1858
|
+
async removeAllDecisionMachineArtifacts() {
|
|
1859
|
+
const result = await this.tables?.decisionMachine.deleteMany({});
|
|
1860
|
+
return result?.deletedCount ?? 0;
|
|
1861
|
+
}
|
|
1366
1862
|
/**
|
|
1367
1863
|
* @description set client context-specific entropy
|
|
1368
1864
|
*/
|
|
@@ -1405,7 +1901,7 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
1405
1901
|
},
|
|
1406
1902
|
{
|
|
1407
1903
|
$lookup: {
|
|
1408
|
-
from: "
|
|
1904
|
+
from: "sessions",
|
|
1409
1905
|
localField: "sessionId",
|
|
1410
1906
|
foreignField: "sessionId",
|
|
1411
1907
|
as: "sessionData"
|
|
@@ -1458,6 +1954,36 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
1458
1954
|
})
|
|
1459
1955
|
)).filter((headHash) => headHash !== void 0);
|
|
1460
1956
|
}
|
|
1957
|
+
async getSpamEmailDomain(domain) {
|
|
1958
|
+
if (!this.tables?.spamEmailDomain) {
|
|
1959
|
+
throw new ProsopoDBError("DATABASE.DATABASE_IMPORT_ERROR", {
|
|
1960
|
+
context: { failedFuncName: this.getSpamEmailDomain.name }
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
1963
|
+
const suffixCandidates = buildDomainSuffixCandidates(domain).slice(
|
|
1964
|
+
0,
|
|
1965
|
+
MAX_DOMAIN_SUFFIX_CANDIDATES
|
|
1966
|
+
);
|
|
1967
|
+
const query = suffixCandidates.length > 0 ? { domain: { $in: suffixCandidates } } : { domain };
|
|
1968
|
+
return await this.tables.spamEmailDomain.findOne(query).exec();
|
|
1969
|
+
}
|
|
1970
|
+
async bulkUpdateSpamEmailDomains(domains, upsert) {
|
|
1971
|
+
if (!this.tables?.spamEmailDomain) {
|
|
1972
|
+
throw new ProsopoDBError("DATABASE.DATABASE_IMPORT_ERROR", {
|
|
1973
|
+
context: { failedFuncName: this.bulkUpdateSpamEmailDomains.name }
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
const bulkOps = domains.map((op) => ({
|
|
1977
|
+
updateOne: {
|
|
1978
|
+
filter: op.filter,
|
|
1979
|
+
update: { $set: op.update },
|
|
1980
|
+
upsert
|
|
1981
|
+
}
|
|
1982
|
+
}));
|
|
1983
|
+
if (bulkOps.length > 0) {
|
|
1984
|
+
await this.tables.spamEmailDomain.bulkWrite(bulkOps);
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1461
1987
|
}
|
|
1462
1988
|
export {
|
|
1463
1989
|
ProviderDatabase
|