@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
@@ -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, StoredStatusNames, CaptchaStatus, ApiParams, CaptchaStates, ContextType } from "@prosopo/types";
5
- import { CaptchaRecordSchema, PoWCaptchaRecordSchema, DatasetRecordSchema, SolutionRecordSchema, UserCommitmentRecordSchema, UserSolutionRecordSchema, PendingRecordSchema, ScheduledTaskRecordSchema, ClientRecordSchema, SessionRecordSchema, DetectorRecordSchema, ClientContextEntropyRecordSchema, UserCommitmentSchema, ScheduledTaskSchema } from "@prosopo/types-database";
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).lean();
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, storedStatus = StoredStatusNames.notStored, userSignature) {
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
- sessionId
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
- storedStatus
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
- storedStatus
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).lean();
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 filterNoStoredTimestamp = { storedAtTimestamp: { $exists: false } };
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
- { id: { $in: commitmentIds } },
701
- { $set: updateDoc },
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, updates);
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 filterNoStoredTimestamp = { storedAtTimestamp: { $exists: false } };
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
- { challenge: { $in: challenges } },
778
- { $set: updateDoc },
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
- await this.tables.session.create(sessionRecord);
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).lean();
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
- const filterNoStoredTimestamp = { storedAtTimestamp: { $exists: false } };
885
- return this.tables?.session.aggregate([
886
- {
887
- $match: {
888
- $or: [
889
- filterNoStoredTimestamp,
890
- {
891
- $expr: {
892
- $lt: [
893
- {
894
- $convert: {
895
- input: "$storedAtTimestamp",
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
- $sort: { _id: 1 }
1245
+ sessionId: { $in: sessionIds },
1246
+ lastUpdatedTimestamp: { $lte: asOfTimestamp }
913
1247
  },
914
1248
  {
915
- $skip: skip
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
- accountId: userAccount,
1268
+ userAccount,
947
1269
  pending: true,
948
1270
  salt,
949
1271
  requestHash,
950
1272
  deadlineTimestamp,
951
- requestedAtTimestamp: new Date(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?.pending.updateOne(
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 Dapp user's pending record
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 filter = {
975
- [ApiParams.requestHash]: requestHash
976
- };
977
- const doc = await this.tables?.pending.findOne(filter).lean();
1319
+ const doc = await this.tables?.commitment.findOne({
1320
+ requestHash,
1321
+ pending: true
1322
+ }).lean();
978
1323
  if (doc) {
979
- return doc;
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
- const filter = {
1001
- [ApiParams.requestHash]: requestHash
1002
- };
1003
- await this.tables?.pending.updateOne(
1004
- filter,
1355
+ await this.tables?.commitment.updateOne(
1356
+ { requestHash },
1005
1357
  {
1006
1358
  $set: {
1007
- [CaptchaStatus.pending]: false
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 = { projection: { _id: 0 } };
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).lean();
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 = { _id: 0 };
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).lean();
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