@prosopo/database 3.6.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 +18 -15
- package/.turbo/turbo-build.log +18 -14
- package/CHANGELOG.md +604 -0
- package/dist/base/mongo.d.ts +1 -1
- package/dist/base/mongo.d.ts.map +1 -1
- package/dist/base/mongo.js +5 -2
- package/dist/base/mongo.js.map +1 -1
- package/dist/base/mongoMemory.d.ts +1 -1
- 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 +687 -162
- package/dist/cjs/index.cjs +4 -0
- package/dist/cjs/redisCache.cjs +388 -0
- package/dist/databases/captcha.d.ts +1 -1
- package/dist/databases/captcha.d.ts.map +1 -1
- package/dist/databases/captcha.js +2 -1
- package/dist/databases/captcha.js.map +1 -1
- 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 +1 -1
- package/dist/databases/client.d.ts.map +1 -1
- package/dist/databases/client.js.map +1 -1
- package/dist/databases/index.d.ts +1 -0
- package/dist/databases/index.d.ts.map +1 -1
- package/dist/databases/index.js +2 -0
- package/dist/databases/index.js.map +1 -1
- package/dist/databases/provider.d.ts +45 -14
- package/dist/databases/provider.d.ts.map +1 -1
- package/dist/databases/provider.js +688 -163
- package/dist/databases/provider.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- 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 +12 -9
- 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 },
|
|
@@ -599,6 +666,7 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
599
666
|
userSubmitted,
|
|
600
667
|
userSignature,
|
|
601
668
|
lastUpdatedTimestamp: timestamp,
|
|
669
|
+
pendingStage: true,
|
|
602
670
|
...coords && { coords }
|
|
603
671
|
};
|
|
604
672
|
try {
|
|
@@ -629,6 +697,17 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
629
697
|
},
|
|
630
698
|
msg: "PowCaptcha record updated successfully"
|
|
631
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
|
+
);
|
|
632
711
|
} catch (error) {
|
|
633
712
|
const err = new ProsopoDBError("DATABASE.CAPTCHA_UPDATE_FAILED", {
|
|
634
713
|
context: {
|
|
@@ -649,7 +728,205 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
649
728
|
const tables = this.getTables();
|
|
650
729
|
await tables.powcaptcha.updateOne(
|
|
651
730
|
{ challenge },
|
|
652
|
-
{ $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 } },
|
|
653
930
|
{ upsert: false }
|
|
654
931
|
);
|
|
655
932
|
}
|
|
@@ -661,44 +938,38 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
661
938
|
return docs || [];
|
|
662
939
|
}
|
|
663
940
|
/** @description Get Dapp User captcha commitments from the commitments table that have not been counted towards the
|
|
664
|
-
* 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.
|
|
665
950
|
*/
|
|
666
951
|
async getUnstoredDappUserCommitments(limit = 1e3, skip = 0) {
|
|
667
|
-
const
|
|
668
|
-
const docs = await this.tables?.commitment.aggregate([
|
|
669
|
-
{
|
|
670
|
-
$match: {
|
|
671
|
-
$or: [
|
|
672
|
-
filterNoStoredTimestamp,
|
|
673
|
-
{
|
|
674
|
-
$expr: {
|
|
675
|
-
$lt: ["$storedAtTimestamp", "$lastUpdatedTimestamp"]
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
]
|
|
679
|
-
}
|
|
680
|
-
},
|
|
681
|
-
{
|
|
682
|
-
$sort: { _id: 1 }
|
|
683
|
-
},
|
|
684
|
-
{
|
|
685
|
-
$skip: skip
|
|
686
|
-
},
|
|
687
|
-
{
|
|
688
|
-
$limit: limit
|
|
689
|
-
}
|
|
690
|
-
]);
|
|
952
|
+
const docs = await this.tables?.commitment.find({ pendingStage: true }).sort({ _id: 1 }).skip(skip).limit(limit).lean();
|
|
691
953
|
return docs || [];
|
|
692
954
|
}
|
|
693
|
-
/** @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.
|
|
694
962
|
*/
|
|
695
|
-
async markDappUserCommitmentsStored(commitmentIds) {
|
|
696
|
-
const updateDoc = {
|
|
697
|
-
storedAtTimestamp: /* @__PURE__ */ new Date()
|
|
698
|
-
};
|
|
963
|
+
async markDappUserCommitmentsStored(commitmentIds, asOfTimestamp = /* @__PURE__ */ new Date()) {
|
|
699
964
|
await this.tables?.commitment.updateMany(
|
|
700
|
-
{
|
|
701
|
-
|
|
965
|
+
{
|
|
966
|
+
id: { $in: commitmentIds },
|
|
967
|
+
lastUpdatedTimestamp: { $lte: asOfTimestamp }
|
|
968
|
+
},
|
|
969
|
+
{
|
|
970
|
+
$set: { storedAtTimestamp: /* @__PURE__ */ new Date() },
|
|
971
|
+
$unset: { pendingStage: 1 }
|
|
972
|
+
},
|
|
702
973
|
{ upsert: false }
|
|
703
974
|
);
|
|
704
975
|
}
|
|
@@ -707,7 +978,8 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
707
978
|
async markDappUserCommitmentsChecked(commitmentIds) {
|
|
708
979
|
const updateDoc = {
|
|
709
980
|
[StoredStatusNames.serverChecked]: true,
|
|
710
|
-
lastUpdatedTimestamp: /* @__PURE__ */ new Date()
|
|
981
|
+
lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
|
|
982
|
+
pendingStage: true
|
|
711
983
|
};
|
|
712
984
|
await this.tables?.commitment.updateMany(
|
|
713
985
|
{ id: { $in: commitmentIds } },
|
|
@@ -719,7 +991,11 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
719
991
|
*/
|
|
720
992
|
async updateDappUserCommitment(commitmentId, updates) {
|
|
721
993
|
const filter = { id: commitmentId };
|
|
722
|
-
await this.tables?.commitment.updateOne(filter,
|
|
994
|
+
await this.tables?.commitment.updateOne(filter, {
|
|
995
|
+
...updates,
|
|
996
|
+
lastUpdatedAtTimestamp: /* @__PURE__ */ new Date(),
|
|
997
|
+
pendingStage: true
|
|
998
|
+
});
|
|
723
999
|
}
|
|
724
1000
|
/**
|
|
725
1001
|
* @description Get Dapp User PoW captcha commitments that have not been counted towards the client's total
|
|
@@ -728,54 +1004,25 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
728
1004
|
* @returns {Promise<PoWCaptchaRecord[]>} Array of PoW captcha records
|
|
729
1005
|
*/
|
|
730
1006
|
async getUnstoredDappUserPoWCommitments(limit = 1e3, skip = 0) {
|
|
731
|
-
const
|
|
732
|
-
const docs = await this.tables?.powcaptcha.aggregate([
|
|
733
|
-
{
|
|
734
|
-
$match: {
|
|
735
|
-
$or: [
|
|
736
|
-
filterNoStoredTimestamp,
|
|
737
|
-
{
|
|
738
|
-
$expr: {
|
|
739
|
-
$lt: [
|
|
740
|
-
{
|
|
741
|
-
$convert: {
|
|
742
|
-
input: "$storedAtTimestamp",
|
|
743
|
-
to: "date"
|
|
744
|
-
}
|
|
745
|
-
},
|
|
746
|
-
{
|
|
747
|
-
$convert: {
|
|
748
|
-
input: "$lastUpdatedTimestamp",
|
|
749
|
-
to: "date"
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
]
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
]
|
|
756
|
-
}
|
|
757
|
-
},
|
|
758
|
-
{
|
|
759
|
-
$sort: { _id: 1 }
|
|
760
|
-
},
|
|
761
|
-
{
|
|
762
|
-
$skip: skip
|
|
763
|
-
},
|
|
764
|
-
{
|
|
765
|
-
$limit: limit
|
|
766
|
-
}
|
|
767
|
-
]);
|
|
1007
|
+
const docs = await this.tables?.powcaptcha.find({ pendingStage: true }).sort({ _id: 1 }).skip(skip).limit(limit).lean();
|
|
768
1008
|
return docs || [];
|
|
769
1009
|
}
|
|
770
|
-
/** @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.
|
|
771
1015
|
*/
|
|
772
|
-
async markDappUserPoWCommitmentsStored(challenges) {
|
|
773
|
-
const updateDoc = {
|
|
774
|
-
storedAtTimestamp: /* @__PURE__ */ new Date()
|
|
775
|
-
};
|
|
1016
|
+
async markDappUserPoWCommitmentsStored(challenges, asOfTimestamp = /* @__PURE__ */ new Date()) {
|
|
776
1017
|
await this.tables?.powcaptcha.updateMany(
|
|
777
|
-
{
|
|
778
|
-
|
|
1018
|
+
{
|
|
1019
|
+
challenge: { $in: challenges },
|
|
1020
|
+
lastUpdatedTimestamp: { $lte: asOfTimestamp }
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
$set: { storedAtTimestamp: /* @__PURE__ */ new Date() },
|
|
1024
|
+
$unset: { pendingStage: 1 }
|
|
1025
|
+
},
|
|
779
1026
|
{ upsert: false }
|
|
780
1027
|
);
|
|
781
1028
|
}
|
|
@@ -784,7 +1031,8 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
784
1031
|
async markDappUserPoWCommitmentsChecked(challenges) {
|
|
785
1032
|
const updateDoc = {
|
|
786
1033
|
[StoredStatusNames.serverChecked]: true,
|
|
787
|
-
lastUpdatedTimestamp: /* @__PURE__ */ new Date()
|
|
1034
|
+
lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
|
|
1035
|
+
pendingStage: true
|
|
788
1036
|
};
|
|
789
1037
|
await this.tables?.powcaptcha.updateMany(
|
|
790
1038
|
{ challenge: { $in: challenges } },
|
|
@@ -802,7 +1050,26 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
802
1050
|
this.logger.debug(() => ({
|
|
803
1051
|
data: { action: "storing", sessionRecord }
|
|
804
1052
|
}));
|
|
805
|
-
|
|
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
|
+
);
|
|
806
1073
|
} catch (err) {
|
|
807
1074
|
throw new ProsopoDBError("DATABASE.SESSION_STORE_FAILED", {
|
|
808
1075
|
context: { error: err, sessionId: sessionRecord.sessionId },
|
|
@@ -815,7 +1082,14 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
815
1082
|
*/
|
|
816
1083
|
async getSessionRecordBySessionId(sessionId) {
|
|
817
1084
|
const filter = { sessionId };
|
|
818
|
-
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();
|
|
819
1093
|
return doc || void 0;
|
|
820
1094
|
}
|
|
821
1095
|
/**
|
|
@@ -842,7 +1116,8 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
842
1116
|
try {
|
|
843
1117
|
const session = await this.tables.session.findOneAndUpdate(filter, {
|
|
844
1118
|
deleted: true,
|
|
845
|
-
lastUpdatedTimestamp: /* @__PURE__ */ new Date()
|
|
1119
|
+
lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
|
|
1120
|
+
pendingStage: true
|
|
846
1121
|
}).lean();
|
|
847
1122
|
return session || void 0;
|
|
848
1123
|
} catch (err) {
|
|
@@ -852,6 +1127,74 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
852
1127
|
});
|
|
853
1128
|
}
|
|
854
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
|
+
}
|
|
855
1198
|
/**
|
|
856
1199
|
* Get an active session by user IP hash
|
|
857
1200
|
* @param userSitekeyIpHash The hash of user, IP and sitekey combination
|
|
@@ -876,64 +1219,43 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
876
1219
|
}
|
|
877
1220
|
}
|
|
878
1221
|
/** Get unstored session records
|
|
879
|
-
* @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.
|
|
880
1228
|
* @param limit
|
|
881
1229
|
* @param skip
|
|
882
1230
|
*/
|
|
883
1231
|
getUnstoredSessionRecords(limit = 1e3, skip = 0) {
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
to: "date"
|
|
897
|
-
}
|
|
898
|
-
},
|
|
899
|
-
{
|
|
900
|
-
$convert: {
|
|
901
|
-
input: "$lastUpdatedTimestamp",
|
|
902
|
-
to: "date"
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
]
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
]
|
|
909
|
-
}
|
|
910
|
-
},
|
|
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(
|
|
911
1244
|
{
|
|
912
|
-
|
|
1245
|
+
sessionId: { $in: sessionIds },
|
|
1246
|
+
lastUpdatedTimestamp: { $lte: asOfTimestamp }
|
|
913
1247
|
},
|
|
914
1248
|
{
|
|
915
|
-
$
|
|
1249
|
+
$set: { storedAtTimestamp: /* @__PURE__ */ new Date() },
|
|
1250
|
+
$unset: { pendingStage: 1 }
|
|
916
1251
|
},
|
|
917
|
-
{
|
|
918
|
-
$limit: limit
|
|
919
|
-
}
|
|
920
|
-
]).then((docs) => docs || []);
|
|
921
|
-
}
|
|
922
|
-
/** Mark a list of session records as stored */
|
|
923
|
-
async markSessionRecordsStored(sessionIds) {
|
|
924
|
-
const updateDoc = {
|
|
925
|
-
storedAtTimestamp: /* @__PURE__ */ new Date()
|
|
926
|
-
};
|
|
927
|
-
await this.tables?.session.updateMany(
|
|
928
|
-
{ sessionId: { $in: sessionIds } },
|
|
929
|
-
{ $set: updateDoc },
|
|
930
1252
|
{ upsert: false }
|
|
931
1253
|
);
|
|
932
1254
|
}
|
|
933
1255
|
/**
|
|
934
1256
|
* @description Store a Dapp User's pending record
|
|
935
1257
|
*/
|
|
936
|
-
async storePendingImageCommitment(userAccount, requestHash, salt, deadlineTimestamp, requestedAtTimestamp, ipAddress, threshold, sessionId) {
|
|
1258
|
+
async storePendingImageCommitment(userAccount, requestHash, salt, deadlineTimestamp, requestedAtTimestamp, ipAddress, threshold, sessionId, ipInfo) {
|
|
937
1259
|
if (!isHex(requestHash)) {
|
|
938
1260
|
throw new ProsopoDBError("DATABASE.INVALID_HASH", {
|
|
939
1261
|
context: {
|
|
@@ -943,24 +1265,47 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
943
1265
|
});
|
|
944
1266
|
}
|
|
945
1267
|
const pendingRecord = {
|
|
946
|
-
|
|
1268
|
+
userAccount,
|
|
947
1269
|
pending: true,
|
|
948
1270
|
salt,
|
|
949
1271
|
requestHash,
|
|
950
1272
|
deadlineTimestamp,
|
|
951
|
-
requestedAtTimestamp
|
|
1273
|
+
requestedAtTimestamp,
|
|
952
1274
|
ipAddress,
|
|
953
1275
|
sessionId,
|
|
954
|
-
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
|
|
955
1300
|
};
|
|
956
|
-
await this.tables?.
|
|
1301
|
+
await this.tables?.commitment.updateOne(
|
|
957
1302
|
{ requestHash },
|
|
958
1303
|
{ $set: pendingRecord },
|
|
959
1304
|
{ upsert: true }
|
|
960
1305
|
);
|
|
961
1306
|
}
|
|
962
1307
|
/**
|
|
963
|
-
* @description Get a
|
|
1308
|
+
* @description Get a user's pending record
|
|
964
1309
|
*/
|
|
965
1310
|
async getPendingImageCommitment(requestHash) {
|
|
966
1311
|
if (!isHex(requestHash)) {
|
|
@@ -971,12 +1316,22 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
971
1316
|
}
|
|
972
1317
|
});
|
|
973
1318
|
}
|
|
974
|
-
const
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1319
|
+
const doc = await this.tables?.commitment.findOne({
|
|
1320
|
+
requestHash,
|
|
1321
|
+
pending: true
|
|
1322
|
+
}).lean();
|
|
978
1323
|
if (doc) {
|
|
979
|
-
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
|
+
};
|
|
980
1335
|
}
|
|
981
1336
|
throw new ProsopoDBError("DATABASE.PENDING_RECORD_NOT_FOUND", {
|
|
982
1337
|
context: {
|
|
@@ -997,17 +1352,13 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
997
1352
|
}
|
|
998
1353
|
});
|
|
999
1354
|
}
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
};
|
|
1003
|
-
await this.tables?.pending.updateOne(
|
|
1004
|
-
filter,
|
|
1355
|
+
await this.tables?.commitment.updateOne(
|
|
1356
|
+
{ requestHash },
|
|
1005
1357
|
{
|
|
1006
1358
|
$set: {
|
|
1007
|
-
|
|
1359
|
+
pending: false
|
|
1008
1360
|
}
|
|
1009
|
-
}
|
|
1010
|
-
{ upsert: true }
|
|
1361
|
+
}
|
|
1011
1362
|
);
|
|
1012
1363
|
}
|
|
1013
1364
|
/**
|
|
@@ -1108,7 +1459,7 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
1108
1459
|
const filter = {
|
|
1109
1460
|
commitmentId
|
|
1110
1461
|
};
|
|
1111
|
-
const project = {
|
|
1462
|
+
const project = { _id: 0 };
|
|
1112
1463
|
const cursor = this.tables?.usersolution?.findOne(filter, project).lean();
|
|
1113
1464
|
const doc = await cursor;
|
|
1114
1465
|
if (doc) {
|
|
@@ -1124,7 +1475,18 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
1124
1475
|
*/
|
|
1125
1476
|
async getDappUserCommitmentById(commitmentId) {
|
|
1126
1477
|
const filter = { id: commitmentId };
|
|
1127
|
-
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();
|
|
1128
1490
|
const doc = await commitmentCursor;
|
|
1129
1491
|
return doc ? doc : void 0;
|
|
1130
1492
|
}
|
|
@@ -1138,7 +1500,10 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
1138
1500
|
userAccount,
|
|
1139
1501
|
dappAccount
|
|
1140
1502
|
};
|
|
1141
|
-
const project = {
|
|
1503
|
+
const project = {
|
|
1504
|
+
_id: 0,
|
|
1505
|
+
result: 1
|
|
1506
|
+
};
|
|
1142
1507
|
const sort = { sort: { _id: -1 } };
|
|
1143
1508
|
const docs = await this.tables?.commitment?.find(filter, project, sort).lean();
|
|
1144
1509
|
return docs ? docs : [];
|
|
@@ -1154,10 +1519,22 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
1154
1519
|
const updateDoc = {
|
|
1155
1520
|
result,
|
|
1156
1521
|
lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
|
|
1522
|
+
pendingStage: true,
|
|
1157
1523
|
...coords ? { coords } : {}
|
|
1158
1524
|
};
|
|
1159
1525
|
const filter = { id: commitmentId };
|
|
1160
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
|
+
);
|
|
1161
1538
|
} catch (err) {
|
|
1162
1539
|
throw new ProsopoDBError("DATABASE.SOLUTION_APPROVE_FAILED", {
|
|
1163
1540
|
context: { error: err, commitmentId }
|
|
@@ -1175,10 +1552,22 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
1175
1552
|
const updateDoc = {
|
|
1176
1553
|
result: { status: CaptchaStatus.disapproved, reason },
|
|
1177
1554
|
lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
|
|
1555
|
+
pendingStage: true,
|
|
1178
1556
|
...coords ? { coords } : {}
|
|
1179
1557
|
};
|
|
1180
1558
|
const filter = { id: commitmentId };
|
|
1181
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
|
+
);
|
|
1182
1571
|
} catch (err) {
|
|
1183
1572
|
throw new ProsopoDBError("DATABASE.SOLUTION_APPROVE_FAILED", {
|
|
1184
1573
|
context: { error: err, commitmentId }
|
|
@@ -1314,6 +1703,17 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
1314
1703
|
}
|
|
1315
1704
|
await this.tables?.client.bulkWrite(ops);
|
|
1316
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
|
+
}
|
|
1317
1717
|
/**
|
|
1318
1718
|
* @description Get all client records
|
|
1319
1719
|
*/
|
|
@@ -1326,7 +1726,11 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
1326
1726
|
*/
|
|
1327
1727
|
async getClientRecord(account) {
|
|
1328
1728
|
const filter = { account };
|
|
1329
|
-
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();
|
|
1330
1734
|
return doc ? doc : void 0;
|
|
1331
1735
|
}
|
|
1332
1736
|
/**
|
|
@@ -1364,6 +1768,97 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
1364
1768
|
).sort({ createdAt: -1 }).lean();
|
|
1365
1769
|
return (keyRecords || []).map((record) => record.detectorKey);
|
|
1366
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
|
+
}
|
|
1367
1862
|
/**
|
|
1368
1863
|
* @description set client context-specific entropy
|
|
1369
1864
|
*/
|
|
@@ -1459,6 +1954,36 @@ class ProviderDatabase extends MongoDatabase {
|
|
|
1459
1954
|
})
|
|
1460
1955
|
)).filter((headHash) => headHash !== void 0);
|
|
1461
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
|
+
}
|
|
1462
1987
|
}
|
|
1463
1988
|
export {
|
|
1464
1989
|
ProviderDatabase
|