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