@prosopo/database 3.6.6 → 3.13.8

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 (57) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +17 -13
  2. package/.turbo/turbo-build$colon$tsc.log +18 -15
  3. package/.turbo/turbo-build.log +18 -14
  4. package/CHANGELOG.md +612 -0
  5. package/dist/base/mongo.d.ts +1 -1
  6. package/dist/base/mongo.d.ts.map +1 -1
  7. package/dist/base/mongo.js +5 -2
  8. package/dist/base/mongo.js.map +1 -1
  9. package/dist/base/mongoMemory.d.ts +1 -1
  10. package/dist/cjs/base/mongo.cjs +6 -3
  11. package/dist/cjs/databases/captcha.cjs +2 -1
  12. package/dist/cjs/databases/centralDbStreamer.cjs +136 -0
  13. package/dist/cjs/databases/index.cjs +2 -0
  14. package/dist/cjs/databases/provider.cjs +687 -162
  15. package/dist/cjs/index.cjs +4 -0
  16. package/dist/cjs/redisCache.cjs +388 -0
  17. package/dist/databases/captcha.d.ts +1 -1
  18. package/dist/databases/captcha.d.ts.map +1 -1
  19. package/dist/databases/captcha.js +2 -1
  20. package/dist/databases/captcha.js.map +1 -1
  21. package/dist/databases/centralDbStreamer.d.ts +19 -0
  22. package/dist/databases/centralDbStreamer.d.ts.map +1 -0
  23. package/dist/databases/centralDbStreamer.js +136 -0
  24. package/dist/databases/centralDbStreamer.js.map +1 -0
  25. package/dist/databases/client.d.ts +1 -1
  26. package/dist/databases/client.d.ts.map +1 -1
  27. package/dist/databases/client.js.map +1 -1
  28. package/dist/databases/index.d.ts +1 -0
  29. package/dist/databases/index.d.ts.map +1 -1
  30. package/dist/databases/index.js +2 -0
  31. package/dist/databases/index.js.map +1 -1
  32. package/dist/databases/provider.d.ts +45 -14
  33. package/dist/databases/provider.d.ts.map +1 -1
  34. package/dist/databases/provider.js +688 -163
  35. package/dist/databases/provider.js.map +1 -1
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +5 -1
  39. package/dist/index.js.map +1 -1
  40. package/dist/redisCache.d.ts +31 -0
  41. package/dist/redisCache.d.ts.map +1 -0
  42. package/dist/redisCache.js +388 -0
  43. package/dist/redisCache.js.map +1 -0
  44. package/dist/tests/integration/ipInfoPersistence.integration.test.d.ts +2 -0
  45. package/dist/tests/integration/ipInfoPersistence.integration.test.d.ts.map +1 -0
  46. package/dist/tests/integration/ipInfoPersistence.integration.test.js +243 -0
  47. package/dist/tests/integration/ipInfoPersistence.integration.test.js.map +1 -0
  48. package/dist/tests/unit/captchaLabel.unit.test.d.ts +2 -0
  49. package/dist/tests/unit/captchaLabel.unit.test.d.ts.map +1 -0
  50. package/dist/tests/unit/captchaLabel.unit.test.js +41 -0
  51. package/dist/tests/unit/captchaLabel.unit.test.js.map +1 -0
  52. package/dist/tests/unit/databases/centralDbStreamer.unit.test.d.ts +2 -0
  53. package/dist/tests/unit/databases/centralDbStreamer.unit.test.d.ts.map +1 -0
  54. package/dist/tests/unit/databases/centralDbStreamer.unit.test.js +221 -0
  55. package/dist/tests/unit/databases/centralDbStreamer.unit.test.js.map +1 -0
  56. package/package.json +12 -9
  57. 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 },
@@ -601,6 +668,7 @@ class ProviderDatabase extends mongo.MongoDatabase {
601
668
  userSubmitted,
602
669
  userSignature,
603
670
  lastUpdatedTimestamp: timestamp,
671
+ pendingStage: true,
604
672
  ...coords && { coords }
605
673
  };
606
674
  try {
@@ -631,6 +699,17 @@ class ProviderDatabase extends mongo.MongoDatabase {
631
699
  },
632
700
  msg: "PowCaptcha record updated successfully"
633
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
+ );
634
713
  } catch (error) {
635
714
  const err = new common.ProsopoDBError("DATABASE.CAPTCHA_UPDATE_FAILED", {
636
715
  context: {
@@ -651,7 +730,205 @@ class ProviderDatabase extends mongo.MongoDatabase {
651
730
  const tables = this.getTables();
652
731
  await tables.powcaptcha.updateOne(
653
732
  { challenge },
654
- { $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 } },
655
932
  { upsert: false }
656
933
  );
657
934
  }
@@ -663,44 +940,38 @@ class ProviderDatabase extends mongo.MongoDatabase {
663
940
  return docs || [];
664
941
  }
665
942
  /** @description Get Dapp User captcha commitments from the commitments table that have not been counted towards the
666
- * 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.
667
952
  */
668
953
  async getUnstoredDappUserCommitments(limit = 1e3, skip = 0) {
669
- const filterNoStoredTimestamp = { storedAtTimestamp: { $exists: false } };
670
- const docs = await this.tables?.commitment.aggregate([
671
- {
672
- $match: {
673
- $or: [
674
- filterNoStoredTimestamp,
675
- {
676
- $expr: {
677
- $lt: ["$storedAtTimestamp", "$lastUpdatedTimestamp"]
678
- }
679
- }
680
- ]
681
- }
682
- },
683
- {
684
- $sort: { _id: 1 }
685
- },
686
- {
687
- $skip: skip
688
- },
689
- {
690
- $limit: limit
691
- }
692
- ]);
954
+ const docs = await this.tables?.commitment.find({ pendingStage: true }).sort({ _id: 1 }).skip(skip).limit(limit).lean();
693
955
  return docs || [];
694
956
  }
695
- /** @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.
696
964
  */
697
- async markDappUserCommitmentsStored(commitmentIds) {
698
- const updateDoc = {
699
- storedAtTimestamp: /* @__PURE__ */ new Date()
700
- };
965
+ async markDappUserCommitmentsStored(commitmentIds, asOfTimestamp = /* @__PURE__ */ new Date()) {
701
966
  await this.tables?.commitment.updateMany(
702
- { id: { $in: commitmentIds } },
703
- { $set: updateDoc },
967
+ {
968
+ id: { $in: commitmentIds },
969
+ lastUpdatedTimestamp: { $lte: asOfTimestamp }
970
+ },
971
+ {
972
+ $set: { storedAtTimestamp: /* @__PURE__ */ new Date() },
973
+ $unset: { pendingStage: 1 }
974
+ },
704
975
  { upsert: false }
705
976
  );
706
977
  }
@@ -709,7 +980,8 @@ class ProviderDatabase extends mongo.MongoDatabase {
709
980
  async markDappUserCommitmentsChecked(commitmentIds) {
710
981
  const updateDoc = {
711
982
  [types.StoredStatusNames.serverChecked]: true,
712
- lastUpdatedTimestamp: /* @__PURE__ */ new Date()
983
+ lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
984
+ pendingStage: true
713
985
  };
714
986
  await this.tables?.commitment.updateMany(
715
987
  { id: { $in: commitmentIds } },
@@ -721,7 +993,11 @@ class ProviderDatabase extends mongo.MongoDatabase {
721
993
  */
722
994
  async updateDappUserCommitment(commitmentId, updates) {
723
995
  const filter = { id: commitmentId };
724
- 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
+ });
725
1001
  }
726
1002
  /**
727
1003
  * @description Get Dapp User PoW captcha commitments that have not been counted towards the client's total
@@ -730,54 +1006,25 @@ class ProviderDatabase extends mongo.MongoDatabase {
730
1006
  * @returns {Promise<PoWCaptchaRecord[]>} Array of PoW captcha records
731
1007
  */
732
1008
  async getUnstoredDappUserPoWCommitments(limit = 1e3, skip = 0) {
733
- const filterNoStoredTimestamp = { storedAtTimestamp: { $exists: false } };
734
- const docs = await this.tables?.powcaptcha.aggregate([
735
- {
736
- $match: {
737
- $or: [
738
- filterNoStoredTimestamp,
739
- {
740
- $expr: {
741
- $lt: [
742
- {
743
- $convert: {
744
- input: "$storedAtTimestamp",
745
- to: "date"
746
- }
747
- },
748
- {
749
- $convert: {
750
- input: "$lastUpdatedTimestamp",
751
- to: "date"
752
- }
753
- }
754
- ]
755
- }
756
- }
757
- ]
758
- }
759
- },
760
- {
761
- $sort: { _id: 1 }
762
- },
763
- {
764
- $skip: skip
765
- },
766
- {
767
- $limit: limit
768
- }
769
- ]);
1009
+ const docs = await this.tables?.powcaptcha.find({ pendingStage: true }).sort({ _id: 1 }).skip(skip).limit(limit).lean();
770
1010
  return docs || [];
771
1011
  }
772
- /** @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.
773
1017
  */
774
- async markDappUserPoWCommitmentsStored(challenges) {
775
- const updateDoc = {
776
- storedAtTimestamp: /* @__PURE__ */ new Date()
777
- };
1018
+ async markDappUserPoWCommitmentsStored(challenges, asOfTimestamp = /* @__PURE__ */ new Date()) {
778
1019
  await this.tables?.powcaptcha.updateMany(
779
- { challenge: { $in: challenges } },
780
- { $set: updateDoc },
1020
+ {
1021
+ challenge: { $in: challenges },
1022
+ lastUpdatedTimestamp: { $lte: asOfTimestamp }
1023
+ },
1024
+ {
1025
+ $set: { storedAtTimestamp: /* @__PURE__ */ new Date() },
1026
+ $unset: { pendingStage: 1 }
1027
+ },
781
1028
  { upsert: false }
782
1029
  );
783
1030
  }
@@ -786,7 +1033,8 @@ class ProviderDatabase extends mongo.MongoDatabase {
786
1033
  async markDappUserPoWCommitmentsChecked(challenges) {
787
1034
  const updateDoc = {
788
1035
  [types.StoredStatusNames.serverChecked]: true,
789
- lastUpdatedTimestamp: /* @__PURE__ */ new Date()
1036
+ lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
1037
+ pendingStage: true
790
1038
  };
791
1039
  await this.tables?.powcaptcha.updateMany(
792
1040
  { challenge: { $in: challenges } },
@@ -804,7 +1052,26 @@ class ProviderDatabase extends mongo.MongoDatabase {
804
1052
  this.logger.debug(() => ({
805
1053
  data: { action: "storing", sessionRecord }
806
1054
  }));
807
- 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
+ );
808
1075
  } catch (err) {
809
1076
  throw new common.ProsopoDBError("DATABASE.SESSION_STORE_FAILED", {
810
1077
  context: { error: err, sessionId: sessionRecord.sessionId },
@@ -817,7 +1084,14 @@ class ProviderDatabase extends mongo.MongoDatabase {
817
1084
  */
818
1085
  async getSessionRecordBySessionId(sessionId) {
819
1086
  const filter = { sessionId };
820
- 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();
821
1095
  return doc || void 0;
822
1096
  }
823
1097
  /**
@@ -844,7 +1118,8 @@ class ProviderDatabase extends mongo.MongoDatabase {
844
1118
  try {
845
1119
  const session = await this.tables.session.findOneAndUpdate(filter, {
846
1120
  deleted: true,
847
- lastUpdatedTimestamp: /* @__PURE__ */ new Date()
1121
+ lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
1122
+ pendingStage: true
848
1123
  }).lean();
849
1124
  return session || void 0;
850
1125
  } catch (err) {
@@ -854,6 +1129,74 @@ class ProviderDatabase extends mongo.MongoDatabase {
854
1129
  });
855
1130
  }
856
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
+ }
857
1200
  /**
858
1201
  * Get an active session by user IP hash
859
1202
  * @param userSitekeyIpHash The hash of user, IP and sitekey combination
@@ -878,64 +1221,43 @@ class ProviderDatabase extends mongo.MongoDatabase {
878
1221
  }
879
1222
  }
880
1223
  /** Get unstored session records
881
- * @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.
882
1230
  * @param limit
883
1231
  * @param skip
884
1232
  */
885
1233
  getUnstoredSessionRecords(limit = 1e3, skip = 0) {
886
- const filterNoStoredTimestamp = { storedAtTimestamp: { $exists: false } };
887
- return this.tables?.session.aggregate([
888
- {
889
- $match: {
890
- $or: [
891
- filterNoStoredTimestamp,
892
- {
893
- $expr: {
894
- $lt: [
895
- {
896
- $convert: {
897
- input: "$storedAtTimestamp",
898
- to: "date"
899
- }
900
- },
901
- {
902
- $convert: {
903
- input: "$lastUpdatedTimestamp",
904
- to: "date"
905
- }
906
- }
907
- ]
908
- }
909
- }
910
- ]
911
- }
912
- },
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(
913
1246
  {
914
- $sort: { _id: 1 }
1247
+ sessionId: { $in: sessionIds },
1248
+ lastUpdatedTimestamp: { $lte: asOfTimestamp }
915
1249
  },
916
1250
  {
917
- $skip: skip
1251
+ $set: { storedAtTimestamp: /* @__PURE__ */ new Date() },
1252
+ $unset: { pendingStage: 1 }
918
1253
  },
919
- {
920
- $limit: limit
921
- }
922
- ]).then((docs) => docs || []);
923
- }
924
- /** Mark a list of session records as stored */
925
- async markSessionRecordsStored(sessionIds) {
926
- const updateDoc = {
927
- storedAtTimestamp: /* @__PURE__ */ new Date()
928
- };
929
- await this.tables?.session.updateMany(
930
- { sessionId: { $in: sessionIds } },
931
- { $set: updateDoc },
932
1254
  { upsert: false }
933
1255
  );
934
1256
  }
935
1257
  /**
936
1258
  * @description Store a Dapp User's pending record
937
1259
  */
938
- async storePendingImageCommitment(userAccount, requestHash, salt, deadlineTimestamp, requestedAtTimestamp, ipAddress, threshold, sessionId) {
1260
+ async storePendingImageCommitment(userAccount, requestHash, salt, deadlineTimestamp, requestedAtTimestamp, ipAddress, threshold, sessionId, ipInfo) {
939
1261
  if (!is.isHex(requestHash)) {
940
1262
  throw new common.ProsopoDBError("DATABASE.INVALID_HASH", {
941
1263
  context: {
@@ -945,24 +1267,47 @@ class ProviderDatabase extends mongo.MongoDatabase {
945
1267
  });
946
1268
  }
947
1269
  const pendingRecord = {
948
- accountId: userAccount,
1270
+ userAccount,
949
1271
  pending: true,
950
1272
  salt,
951
1273
  requestHash,
952
1274
  deadlineTimestamp,
953
- requestedAtTimestamp: new Date(requestedAtTimestamp),
1275
+ requestedAtTimestamp,
954
1276
  ipAddress,
955
1277
  sessionId,
956
- 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
957
1302
  };
958
- await this.tables?.pending.updateOne(
1303
+ await this.tables?.commitment.updateOne(
959
1304
  { requestHash },
960
1305
  { $set: pendingRecord },
961
1306
  { upsert: true }
962
1307
  );
963
1308
  }
964
1309
  /**
965
- * @description Get a Dapp user's pending record
1310
+ * @description Get a user's pending record
966
1311
  */
967
1312
  async getPendingImageCommitment(requestHash) {
968
1313
  if (!is.isHex(requestHash)) {
@@ -973,12 +1318,22 @@ class ProviderDatabase extends mongo.MongoDatabase {
973
1318
  }
974
1319
  });
975
1320
  }
976
- const filter = {
977
- [types.ApiParams.requestHash]: requestHash
978
- };
979
- const doc = await this.tables?.pending.findOne(filter).lean();
1321
+ const doc = await this.tables?.commitment.findOne({
1322
+ requestHash,
1323
+ pending: true
1324
+ }).lean();
980
1325
  if (doc) {
981
- 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
+ };
982
1337
  }
983
1338
  throw new common.ProsopoDBError("DATABASE.PENDING_RECORD_NOT_FOUND", {
984
1339
  context: {
@@ -999,17 +1354,13 @@ class ProviderDatabase extends mongo.MongoDatabase {
999
1354
  }
1000
1355
  });
1001
1356
  }
1002
- const filter = {
1003
- [types.ApiParams.requestHash]: requestHash
1004
- };
1005
- await this.tables?.pending.updateOne(
1006
- filter,
1357
+ await this.tables?.commitment.updateOne(
1358
+ { requestHash },
1007
1359
  {
1008
1360
  $set: {
1009
- [types.CaptchaStatus.pending]: false
1361
+ pending: false
1010
1362
  }
1011
- },
1012
- { upsert: true }
1363
+ }
1013
1364
  );
1014
1365
  }
1015
1366
  /**
@@ -1110,7 +1461,7 @@ class ProviderDatabase extends mongo.MongoDatabase {
1110
1461
  const filter = {
1111
1462
  commitmentId
1112
1463
  };
1113
- const project = { projection: { _id: 0 } };
1464
+ const project = { _id: 0 };
1114
1465
  const cursor = this.tables?.usersolution?.findOne(filter, project).lean();
1115
1466
  const doc = await cursor;
1116
1467
  if (doc) {
@@ -1126,7 +1477,18 @@ class ProviderDatabase extends mongo.MongoDatabase {
1126
1477
  */
1127
1478
  async getDappUserCommitmentById(commitmentId) {
1128
1479
  const filter = { id: commitmentId };
1129
- 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();
1130
1492
  const doc = await commitmentCursor;
1131
1493
  return doc ? doc : void 0;
1132
1494
  }
@@ -1140,7 +1502,10 @@ class ProviderDatabase extends mongo.MongoDatabase {
1140
1502
  userAccount,
1141
1503
  dappAccount
1142
1504
  };
1143
- const project = { _id: 0 };
1505
+ const project = {
1506
+ _id: 0,
1507
+ result: 1
1508
+ };
1144
1509
  const sort = { sort: { _id: -1 } };
1145
1510
  const docs = await this.tables?.commitment?.find(filter, project, sort).lean();
1146
1511
  return docs ? docs : [];
@@ -1156,10 +1521,22 @@ class ProviderDatabase extends mongo.MongoDatabase {
1156
1521
  const updateDoc = {
1157
1522
  result,
1158
1523
  lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
1524
+ pendingStage: true,
1159
1525
  ...coords ? { coords } : {}
1160
1526
  };
1161
1527
  const filter = { id: commitmentId };
1162
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
+ );
1163
1540
  } catch (err) {
1164
1541
  throw new common.ProsopoDBError("DATABASE.SOLUTION_APPROVE_FAILED", {
1165
1542
  context: { error: err, commitmentId }
@@ -1177,10 +1554,22 @@ class ProviderDatabase extends mongo.MongoDatabase {
1177
1554
  const updateDoc = {
1178
1555
  result: { status: types.CaptchaStatus.disapproved, reason },
1179
1556
  lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
1557
+ pendingStage: true,
1180
1558
  ...coords ? { coords } : {}
1181
1559
  };
1182
1560
  const filter = { id: commitmentId };
1183
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
+ );
1184
1573
  } catch (err) {
1185
1574
  throw new common.ProsopoDBError("DATABASE.SOLUTION_APPROVE_FAILED", {
1186
1575
  context: { error: err, commitmentId }
@@ -1316,6 +1705,17 @@ class ProviderDatabase extends mongo.MongoDatabase {
1316
1705
  }
1317
1706
  await this.tables?.client.bulkWrite(ops);
1318
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
+ }
1319
1719
  /**
1320
1720
  * @description Get all client records
1321
1721
  */
@@ -1328,7 +1728,11 @@ class ProviderDatabase extends mongo.MongoDatabase {
1328
1728
  */
1329
1729
  async getClientRecord(account) {
1330
1730
  const filter = { account };
1331
- 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();
1332
1736
  return doc ? doc : void 0;
1333
1737
  }
1334
1738
  /**
@@ -1366,6 +1770,97 @@ class ProviderDatabase extends mongo.MongoDatabase {
1366
1770
  ).sort({ createdAt: -1 }).lean();
1367
1771
  return (keyRecords || []).map((record) => record.detectorKey);
1368
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
+ }
1369
1864
  /**
1370
1865
  * @description set client context-specific entropy
1371
1866
  */
@@ -1461,5 +1956,35 @@ class ProviderDatabase extends mongo.MongoDatabase {
1461
1956
  })
1462
1957
  )).filter((headHash) => headHash !== void 0);
1463
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
+ }
1464
1989
  }
1465
1990
  exports.ProviderDatabase = ProviderDatabase;