@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
@@ -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 },
@@ -590,7 +657,7 @@ class ProviderDatabase extends MongoDatabase {
590
657
  * @param userSignature
591
658
  * @returns {Promise<void>} A promise that resolves when the record is updated.
592
659
  */
593
- async updatePowCaptchaRecordResult(challenge, result, serverChecked = false, userSubmitted = false, userSignature) {
660
+ async updatePowCaptchaRecordResult(challenge, result, serverChecked = false, userSubmitted = false, userSignature, coords) {
594
661
  const tables = this.getTables();
595
662
  const timestamp = /* @__PURE__ */ new Date();
596
663
  const update = {
@@ -598,7 +665,9 @@ class ProviderDatabase extends MongoDatabase {
598
665
  serverChecked,
599
666
  userSubmitted,
600
667
  userSignature,
601
- lastUpdatedTimestamp: timestamp
668
+ lastUpdatedTimestamp: timestamp,
669
+ pendingStage: true,
670
+ ...coords && { coords }
602
671
  };
603
672
  try {
604
673
  const updateResult = await tables.powcaptcha.updateOne(
@@ -628,6 +697,17 @@ class ProviderDatabase extends MongoDatabase {
628
697
  },
629
698
  msg: "PowCaptcha record updated successfully"
630
699
  }));
700
+ this.centralStreamer?.streamPowUpdate(
701
+ () => this.getPowCaptchaRecordByChallenge(challenge),
702
+ (ts) => this.tables.powcaptcha.updateOne(
703
+ { challenge, lastUpdatedTimestamp: { $lte: ts } },
704
+ {
705
+ $set: { storedAtTimestamp: ts },
706
+ $unset: { pendingStage: 1 }
707
+ }
708
+ ).then(() => {
709
+ })
710
+ );
631
711
  } catch (error) {
632
712
  const err = new ProsopoDBError("DATABASE.CAPTCHA_UPDATE_FAILED", {
633
713
  context: {
@@ -648,7 +728,205 @@ class ProviderDatabase extends MongoDatabase {
648
728
  const tables = this.getTables();
649
729
  await tables.powcaptcha.updateOne(
650
730
  { challenge },
651
- { $set: updates },
731
+ { $set: { ...updates, pendingStage: true } },
732
+ { upsert: false }
733
+ );
734
+ this.centralStreamer?.streamPowUpdate(
735
+ () => this.getPowCaptchaRecordByChallenge(challenge),
736
+ (ts) => this.tables.powcaptcha.updateOne(
737
+ { challenge, lastUpdatedTimestamp: { $lte: ts } },
738
+ {
739
+ $set: { storedAtTimestamp: ts },
740
+ $unset: { pendingStage: 1 }
741
+ }
742
+ ).then(() => {
743
+ })
744
+ );
745
+ }
746
+ async storePuzzleCaptchaRecord(challenge, components, targetX, targetY, originX, originY, tolerance, providerSignature, ipAddress, headers, ja4, sessionId, ipInfo) {
747
+ const tables = this.getTables();
748
+ const puzzleCaptchaRecord = {
749
+ challenge,
750
+ userAccount: components.userAccount,
751
+ dappAccount: components.dappAccount,
752
+ requestedAtTimestamp: new Date(components.requestedAtTimestamp),
753
+ ipAddress,
754
+ headers,
755
+ ja4,
756
+ result: { status: CaptchaStatus.pending },
757
+ userSubmitted: false,
758
+ serverChecked: false,
759
+ targetX,
760
+ targetY,
761
+ originX,
762
+ originY,
763
+ tolerance,
764
+ providerSignature,
765
+ lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
766
+ pendingStage: true,
767
+ sessionId,
768
+ ipInfo
769
+ };
770
+ try {
771
+ await tables.puzzlecaptcha.create(puzzleCaptchaRecord);
772
+ this.logger.info(() => ({
773
+ data: {
774
+ challenge,
775
+ countryCode: ipInfo?.isValid ? ipInfo.countryCode : void 0
776
+ },
777
+ msg: "PuzzleCaptcha record added successfully"
778
+ }));
779
+ } catch (error) {
780
+ const err = new ProsopoDBError("DATABASE.CAPTCHA_UPDATE_FAILED", {
781
+ context: {
782
+ error,
783
+ challenge,
784
+ ipInfo
785
+ },
786
+ logger: this.logger
787
+ });
788
+ this.logger.error(() => ({
789
+ err: error,
790
+ msg: "Failed to add PuzzleCaptcha record"
791
+ }));
792
+ throw err;
793
+ }
794
+ }
795
+ /**
796
+ * @description Retrieves a Puzzle Captcha record by its challenge string.
797
+ * @param {string} challenge The challenge string to search for.
798
+ * @returns {Promise<PuzzleCaptchaRecord | null>} A promise that resolves with the found record or null if not found.
799
+ */
800
+ async getPuzzleCaptchaRecordByChallenge(challenge) {
801
+ if (!this.tables) {
802
+ throw new ProsopoDBError("DATABASE.DATABASE_UNDEFINED", {
803
+ context: {
804
+ failedFuncName: this.getPuzzleCaptchaRecordByChallenge.name
805
+ },
806
+ logger: this.logger
807
+ });
808
+ }
809
+ try {
810
+ const filter = { challenge };
811
+ const record = await this.tables.puzzlecaptcha.findOne(filter, {
812
+ challenge: 1,
813
+ userAccount: 1,
814
+ dappAccount: 1,
815
+ requestedAtTimestamp: 1,
816
+ ipAddress: 1,
817
+ headers: 1,
818
+ ja4: 1,
819
+ result: 1,
820
+ targetX: 1,
821
+ targetY: 1,
822
+ originX: 1,
823
+ originY: 1,
824
+ tolerance: 1,
825
+ puzzleEvents: 1,
826
+ sessionId: 1,
827
+ ipInfo: 1,
828
+ deviceCapability: 1,
829
+ behavioralDataPacked: 1,
830
+ serverChecked: 1,
831
+ userSubmitted: 1,
832
+ coords: 1
833
+ }).lean();
834
+ if (record) {
835
+ this.logger.info(() => ({
836
+ data: { challenge },
837
+ msg: "PuzzleCaptcha record retrieved successfully"
838
+ }));
839
+ return record;
840
+ }
841
+ this.logger.info(() => ({
842
+ data: { challenge },
843
+ msg: "No PuzzleCaptcha record found"
844
+ }));
845
+ return null;
846
+ } catch (error) {
847
+ const err = new ProsopoDBError("DATABASE.CAPTCHA_GET_FAILED", {
848
+ context: { error, challenge },
849
+ logger: this.logger
850
+ });
851
+ this.logger.error(() => ({
852
+ err,
853
+ msg: "Failed to retrieve PuzzleCaptcha record"
854
+ }));
855
+ throw err;
856
+ }
857
+ }
858
+ /**
859
+ * @description Updates a Puzzle Captcha record result in the database.
860
+ * @param {string} challenge The challenge string of the captcha to be updated.
861
+ * @param result
862
+ * @param serverChecked
863
+ * @param userSubmitted
864
+ * @param userSignature
865
+ * @param coords
866
+ * @param lastUpdatedTimestamp
867
+ * @returns {Promise<void>} A promise that resolves when the record is updated.
868
+ */
869
+ async updatePuzzleCaptchaRecordResult(challenge, result, serverChecked = false, userSubmitted = false, userSignature, coords, lastUpdatedTimestamp) {
870
+ const tables = this.getTables();
871
+ const timestamp = lastUpdatedTimestamp ?? /* @__PURE__ */ new Date();
872
+ const update = {
873
+ result,
874
+ serverChecked,
875
+ userSubmitted,
876
+ userSignature,
877
+ lastUpdatedTimestamp: timestamp,
878
+ pendingStage: true,
879
+ ...coords && { coords }
880
+ };
881
+ try {
882
+ const updateResult = await tables.puzzlecaptcha.updateOne(
883
+ { challenge },
884
+ {
885
+ $set: update
886
+ }
887
+ );
888
+ if (updateResult.matchedCount === 0) {
889
+ const err = new ProsopoDBError("DATABASE.CAPTCHA_GET_FAILED", {
890
+ context: {
891
+ challenge,
892
+ ...update
893
+ },
894
+ logger: this.logger
895
+ });
896
+ this.logger.info(() => ({
897
+ err,
898
+ msg: "No PuzzleCaptcha record found to update"
899
+ }));
900
+ throw err;
901
+ }
902
+ this.logger.info(() => ({
903
+ data: {
904
+ challenge,
905
+ ...update
906
+ },
907
+ msg: "PuzzleCaptcha record updated successfully"
908
+ }));
909
+ } catch (error) {
910
+ const err = new ProsopoDBError("DATABASE.CAPTCHA_UPDATE_FAILED", {
911
+ context: {
912
+ error,
913
+ challenge,
914
+ ...update
915
+ },
916
+ logger: this.logger
917
+ });
918
+ this.logger.error(() => ({
919
+ err,
920
+ msg: "Failed to update PuzzleCaptcha record"
921
+ }));
922
+ throw err;
923
+ }
924
+ }
925
+ async updatePuzzleCaptchaRecord(challenge, updates) {
926
+ const tables = this.getTables();
927
+ await tables.puzzlecaptcha.updateOne(
928
+ { challenge },
929
+ { $set: { ...updates, pendingStage: true } },
652
930
  { upsert: false }
653
931
  );
654
932
  }
@@ -660,44 +938,38 @@ class ProviderDatabase extends MongoDatabase {
660
938
  return docs || [];
661
939
  }
662
940
  /** @description Get Dapp User captcha commitments from the commitments table that have not been counted towards the
663
- * client's total
941
+ * client's total.
942
+ *
943
+ * Served by the `pendingStage_partial` index. Records have
944
+ * `pendingStage: true` set on insert and on every mutation (see
945
+ * `updateDappUserCommitment`, `markDappUserCommitmentsChecked`,
946
+ * `approveDappUserCommitment`, `disapproveDappUserCommitment`,
947
+ * `storePendingImageCommitment`). `markDappUserCommitmentsStored` clears
948
+ * the flag after a successful stage, guarded by `lastUpdatedTimestamp`
949
+ * so an in-flight update isn't lost.
664
950
  */
665
951
  async getUnstoredDappUserCommitments(limit = 1e3, skip = 0) {
666
- const filterNoStoredTimestamp = { storedAtTimestamp: { $exists: false } };
667
- const docs = await this.tables?.commitment.aggregate([
668
- {
669
- $match: {
670
- $or: [
671
- filterNoStoredTimestamp,
672
- {
673
- $expr: {
674
- $lt: ["$storedAtTimestamp", "$lastUpdatedTimestamp"]
675
- }
676
- }
677
- ]
678
- }
679
- },
680
- {
681
- $sort: { _id: 1 }
682
- },
683
- {
684
- $skip: skip
685
- },
686
- {
687
- $limit: limit
688
- }
689
- ]);
952
+ const docs = await this.tables?.commitment.find({ pendingStage: true }).sort({ _id: 1 }).skip(skip).limit(limit).lean();
690
953
  return docs || [];
691
954
  }
692
- /** @description Mark a list of captcha commits as stored
955
+ /** @description Mark a list of captcha commits as stored.
956
+ *
957
+ * `asOfTimestamp` defaults to "now" but the sweep should pass the time
958
+ * at which it fetched the batch. The lastUpdatedTimestamp guard prevents
959
+ * us from clearing pendingStage on a record that was mutated between
960
+ * fetch and mark-stored — those records will leave pendingStage set so
961
+ * the next sweep picks them up.
693
962
  */
694
- async markDappUserCommitmentsStored(commitmentIds) {
695
- const updateDoc = {
696
- storedAtTimestamp: /* @__PURE__ */ new Date()
697
- };
963
+ async markDappUserCommitmentsStored(commitmentIds, asOfTimestamp = /* @__PURE__ */ new Date()) {
698
964
  await this.tables?.commitment.updateMany(
699
- { id: { $in: commitmentIds } },
700
- { $set: updateDoc },
965
+ {
966
+ id: { $in: commitmentIds },
967
+ lastUpdatedTimestamp: { $lte: asOfTimestamp }
968
+ },
969
+ {
970
+ $set: { storedAtTimestamp: /* @__PURE__ */ new Date() },
971
+ $unset: { pendingStage: 1 }
972
+ },
701
973
  { upsert: false }
702
974
  );
703
975
  }
@@ -706,7 +978,8 @@ class ProviderDatabase extends MongoDatabase {
706
978
  async markDappUserCommitmentsChecked(commitmentIds) {
707
979
  const updateDoc = {
708
980
  [StoredStatusNames.serverChecked]: true,
709
- lastUpdatedTimestamp: /* @__PURE__ */ new Date()
981
+ lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
982
+ pendingStage: true
710
983
  };
711
984
  await this.tables?.commitment.updateMany(
712
985
  { id: { $in: commitmentIds } },
@@ -718,7 +991,11 @@ class ProviderDatabase extends MongoDatabase {
718
991
  */
719
992
  async updateDappUserCommitment(commitmentId, updates) {
720
993
  const filter = { id: commitmentId };
721
- await this.tables?.commitment.updateOne(filter, updates);
994
+ await this.tables?.commitment.updateOne(filter, {
995
+ ...updates,
996
+ lastUpdatedAtTimestamp: /* @__PURE__ */ new Date(),
997
+ pendingStage: true
998
+ });
722
999
  }
723
1000
  /**
724
1001
  * @description Get Dapp User PoW captcha commitments that have not been counted towards the client's total
@@ -727,54 +1004,25 @@ class ProviderDatabase extends MongoDatabase {
727
1004
  * @returns {Promise<PoWCaptchaRecord[]>} Array of PoW captcha records
728
1005
  */
729
1006
  async getUnstoredDappUserPoWCommitments(limit = 1e3, skip = 0) {
730
- const filterNoStoredTimestamp = { storedAtTimestamp: { $exists: false } };
731
- const docs = await this.tables?.powcaptcha.aggregate([
732
- {
733
- $match: {
734
- $or: [
735
- filterNoStoredTimestamp,
736
- {
737
- $expr: {
738
- $lt: [
739
- {
740
- $convert: {
741
- input: "$storedAtTimestamp",
742
- to: "date"
743
- }
744
- },
745
- {
746
- $convert: {
747
- input: "$lastUpdatedTimestamp",
748
- to: "date"
749
- }
750
- }
751
- ]
752
- }
753
- }
754
- ]
755
- }
756
- },
757
- {
758
- $sort: { _id: 1 }
759
- },
760
- {
761
- $skip: skip
762
- },
763
- {
764
- $limit: limit
765
- }
766
- ]);
1007
+ const docs = await this.tables?.powcaptcha.find({ pendingStage: true }).sort({ _id: 1 }).skip(skip).limit(limit).lean();
767
1008
  return docs || [];
768
1009
  }
769
- /** @description Mark a list of PoW captcha commits as stored
1010
+ /** @description Mark a list of PoW captcha commits as stored.
1011
+ *
1012
+ * `asOfTimestamp` defaults to "now" but the sweep should pass the time
1013
+ * at which it fetched the batch. See markDappUserCommitmentsStored for
1014
+ * the guard rationale.
770
1015
  */
771
- async markDappUserPoWCommitmentsStored(challenges) {
772
- const updateDoc = {
773
- storedAtTimestamp: /* @__PURE__ */ new Date()
774
- };
1016
+ async markDappUserPoWCommitmentsStored(challenges, asOfTimestamp = /* @__PURE__ */ new Date()) {
775
1017
  await this.tables?.powcaptcha.updateMany(
776
- { challenge: { $in: challenges } },
777
- { $set: updateDoc },
1018
+ {
1019
+ challenge: { $in: challenges },
1020
+ lastUpdatedTimestamp: { $lte: asOfTimestamp }
1021
+ },
1022
+ {
1023
+ $set: { storedAtTimestamp: /* @__PURE__ */ new Date() },
1024
+ $unset: { pendingStage: 1 }
1025
+ },
778
1026
  { upsert: false }
779
1027
  );
780
1028
  }
@@ -783,7 +1031,8 @@ class ProviderDatabase extends MongoDatabase {
783
1031
  async markDappUserPoWCommitmentsChecked(challenges) {
784
1032
  const updateDoc = {
785
1033
  [StoredStatusNames.serverChecked]: true,
786
- lastUpdatedTimestamp: /* @__PURE__ */ new Date()
1034
+ lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
1035
+ pendingStage: true
787
1036
  };
788
1037
  await this.tables?.powcaptcha.updateMany(
789
1038
  { challenge: { $in: challenges } },
@@ -801,7 +1050,26 @@ class ProviderDatabase extends MongoDatabase {
801
1050
  this.logger.debug(() => ({
802
1051
  data: { action: "storing", sessionRecord }
803
1052
  }));
804
- 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
+ );
805
1073
  } catch (err) {
806
1074
  throw new ProsopoDBError("DATABASE.SESSION_STORE_FAILED", {
807
1075
  context: { error: err, sessionId: sessionRecord.sessionId },
@@ -814,7 +1082,14 @@ class ProviderDatabase extends MongoDatabase {
814
1082
  */
815
1083
  async getSessionRecordBySessionId(sessionId) {
816
1084
  const filter = { sessionId };
817
- const doc = await this.tables.session.findOne(filter).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();
818
1093
  return doc || void 0;
819
1094
  }
820
1095
  /**
@@ -841,7 +1116,8 @@ class ProviderDatabase extends MongoDatabase {
841
1116
  try {
842
1117
  const session = await this.tables.session.findOneAndUpdate(filter, {
843
1118
  deleted: true,
844
- lastUpdatedTimestamp: /* @__PURE__ */ new Date()
1119
+ lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
1120
+ pendingStage: true
845
1121
  }).lean();
846
1122
  return session || void 0;
847
1123
  } catch (err) {
@@ -851,6 +1127,74 @@ class ProviderDatabase extends MongoDatabase {
851
1127
  });
852
1128
  }
853
1129
  }
1130
+ /**
1131
+ * Update a session record by sessionId. Pure Mongo write — callers in
1132
+ * the provider package are responsible for refreshing the Redis cache
1133
+ * via `RedisWriteQueue.patchCachedSession`, matching the existing
1134
+ * caller-side caching pattern (see `frictionlessTasks.createSession`).
1135
+ */
1136
+ async updateSessionRecord(sessionId, updates, streamToCentral) {
1137
+ try {
1138
+ await this.tables.session.updateOne(
1139
+ { sessionId },
1140
+ {
1141
+ $set: {
1142
+ ...updates,
1143
+ lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
1144
+ pendingStage: true
1145
+ }
1146
+ }
1147
+ );
1148
+ if (streamToCentral && this.centralStreamer) {
1149
+ const streamer = this.centralStreamer;
1150
+ const markStored = (ts) => this.tables.session.updateOne(
1151
+ { sessionId, lastUpdatedTimestamp: { $lte: ts } },
1152
+ {
1153
+ $set: { storedAtTimestamp: ts },
1154
+ $unset: { pendingStage: 1 }
1155
+ }
1156
+ ).then(() => {
1157
+ });
1158
+ this.tables.session.findOne({ sessionId }).lean().then((record) => {
1159
+ if (record) {
1160
+ streamer.streamSessionRecord(record, markStored);
1161
+ }
1162
+ }).catch(() => {
1163
+ });
1164
+ }
1165
+ } catch (err) {
1166
+ throw new ProsopoDBError("DATABASE.SESSION_GET_FAILED", {
1167
+ context: { error: err, sessionId },
1168
+ logger: this.logger
1169
+ });
1170
+ }
1171
+ }
1172
+ /**
1173
+ * Atomically record SIMD CPU fingerprint readings on the session — only
1174
+ * if they aren't already present. Uses a Mongo aggregation-pipeline
1175
+ * update so `simdReadings` and `simdReadingsStage` are set together,
1176
+ * first-hop-wins. Pure Mongo write — cache refresh is the caller's
1177
+ * responsibility via `RedisWriteQueue.patchCachedSimdReadingsIfAbsent`.
1178
+ */
1179
+ async recordSessionSimdReadingsIfAbsent(sessionId, readings, stage) {
1180
+ try {
1181
+ await this.tables.session.updateOne({ sessionId }, [
1182
+ {
1183
+ $set: {
1184
+ simdReadings: { $ifNull: ["$simdReadings", readings] },
1185
+ simdReadingsStage: { $ifNull: ["$simdReadingsStage", stage] },
1186
+ lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
1187
+ pendingStage: true
1188
+ }
1189
+ }
1190
+ ]);
1191
+ } catch (err) {
1192
+ throw new ProsopoDBError("DATABASE.SESSION_GET_FAILED", {
1193
+ context: { error: err, sessionId, stage },
1194
+ logger: this.logger
1195
+ });
1196
+ }
1197
+ }
854
1198
  /**
855
1199
  * Get an active session by user IP hash
856
1200
  * @param userSitekeyIpHash The hash of user, IP and sitekey combination
@@ -875,64 +1219,43 @@ class ProviderDatabase extends MongoDatabase {
875
1219
  }
876
1220
  }
877
1221
  /** Get unstored session records
878
- * @description Get session records that have not been stored yet
1222
+ * @description Get session records that have not been stored yet.
1223
+ *
1224
+ * Served by the `pendingStage_partial` index — see
1225
+ * `getUnstoredDappUserCommitments` for the lifecycle of the flag.
1226
+ * `checkAndRemoveSession` also flips the flag so consumed sessions
1227
+ * propagate to the central DB via the next sweep.
879
1228
  * @param limit
880
1229
  * @param skip
881
1230
  */
882
1231
  getUnstoredSessionRecords(limit = 1e3, skip = 0) {
883
- const filterNoStoredTimestamp = { storedAtTimestamp: { $exists: false } };
884
- return this.tables?.session.aggregate([
885
- {
886
- $match: {
887
- $or: [
888
- filterNoStoredTimestamp,
889
- {
890
- $expr: {
891
- $lt: [
892
- {
893
- $convert: {
894
- input: "$storedAtTimestamp",
895
- to: "date"
896
- }
897
- },
898
- {
899
- $convert: {
900
- input: "$lastUpdatedTimestamp",
901
- to: "date"
902
- }
903
- }
904
- ]
905
- }
906
- }
907
- ]
908
- }
909
- },
1232
+ return Promise.resolve(this.tables?.session).then(
1233
+ (tbl) => tbl?.find({ pendingStage: true }).sort({ _id: 1 }).skip(skip).limit(limit).lean()
1234
+ ).then((docs) => docs || []);
1235
+ }
1236
+ /** Mark a list of session records as stored.
1237
+ *
1238
+ * `asOfTimestamp` defaults to "now" but the sweep should pass the time
1239
+ * at which it fetched the batch. See markDappUserCommitmentsStored for
1240
+ * the guard rationale.
1241
+ */
1242
+ async markSessionRecordsStored(sessionIds, asOfTimestamp = /* @__PURE__ */ new Date()) {
1243
+ await this.tables?.session.updateMany(
910
1244
  {
911
- $sort: { _id: 1 }
1245
+ sessionId: { $in: sessionIds },
1246
+ lastUpdatedTimestamp: { $lte: asOfTimestamp }
912
1247
  },
913
1248
  {
914
- $skip: skip
1249
+ $set: { storedAtTimestamp: /* @__PURE__ */ new Date() },
1250
+ $unset: { pendingStage: 1 }
915
1251
  },
916
- {
917
- $limit: limit
918
- }
919
- ]).then((docs) => docs || []);
920
- }
921
- /** Mark a list of session records as stored */
922
- async markSessionRecordsStored(sessionIds) {
923
- const updateDoc = {
924
- storedAtTimestamp: /* @__PURE__ */ new Date()
925
- };
926
- await this.tables?.session.updateMany(
927
- { sessionId: { $in: sessionIds } },
928
- { $set: updateDoc },
929
1252
  { upsert: false }
930
1253
  );
931
1254
  }
932
1255
  /**
933
1256
  * @description Store a Dapp User's pending record
934
1257
  */
935
- async storePendingImageCommitment(userAccount, requestHash, salt, deadlineTimestamp, requestedAtTimestamp, ipAddress, threshold, sessionId) {
1258
+ async storePendingImageCommitment(userAccount, requestHash, salt, deadlineTimestamp, requestedAtTimestamp, ipAddress, threshold, sessionId, ipInfo) {
936
1259
  if (!isHex(requestHash)) {
937
1260
  throw new ProsopoDBError("DATABASE.INVALID_HASH", {
938
1261
  context: {
@@ -942,24 +1265,47 @@ class ProviderDatabase extends MongoDatabase {
942
1265
  });
943
1266
  }
944
1267
  const pendingRecord = {
945
- accountId: userAccount,
1268
+ userAccount,
946
1269
  pending: true,
947
1270
  salt,
948
1271
  requestHash,
949
1272
  deadlineTimestamp,
950
- requestedAtTimestamp: new Date(requestedAtTimestamp),
1273
+ requestedAtTimestamp,
951
1274
  ipAddress,
952
1275
  sessionId,
953
- threshold
1276
+ threshold,
1277
+ ipInfo,
1278
+ // Deliberately NOT setting pendingStage here. Placeholder
1279
+ // records have id: "" until the user submits a solution; if we
1280
+ // flag them, the sweep picks them up via the partial index but
1281
+ // then `markDappUserCommitmentsStored` runs
1282
+ // `{ id: { $in: ["", ...] } }` which collapses to a single ""
1283
+ // bound and scans every empty-id row via the `id_-1` index —
1284
+ // turning a cheap sweep into a fresh cache evictor. The real
1285
+ // commitment only gets `pendingStage: true` once `id` is
1286
+ // populated by approve/disapprove, at which point staging it is
1287
+ // meaningful.
1288
+ // Placeholder fields required by schema but not needed for pending state
1289
+ dappAccount: "",
1290
+ providerAccount: "",
1291
+ datasetId: "",
1292
+ id: "",
1293
+ // id is populated by the user's solution record when the user submits a solution, so we can leave it blank here
1294
+ result: { status: CaptchaStatus.pending },
1295
+ headers: {},
1296
+ ja4: "",
1297
+ userSignature: "",
1298
+ userSubmitted: false,
1299
+ serverChecked: false
954
1300
  };
955
- await this.tables?.pending.updateOne(
1301
+ await this.tables?.commitment.updateOne(
956
1302
  { requestHash },
957
1303
  { $set: pendingRecord },
958
1304
  { upsert: true }
959
1305
  );
960
1306
  }
961
1307
  /**
962
- * @description Get a Dapp user's pending record
1308
+ * @description Get a user's pending record
963
1309
  */
964
1310
  async getPendingImageCommitment(requestHash) {
965
1311
  if (!isHex(requestHash)) {
@@ -970,12 +1316,22 @@ class ProviderDatabase extends MongoDatabase {
970
1316
  }
971
1317
  });
972
1318
  }
973
- const filter = {
974
- [ApiParams.requestHash]: requestHash
975
- };
976
- const doc = await this.tables?.pending.findOne(filter).lean();
1319
+ const doc = await this.tables?.commitment.findOne({
1320
+ requestHash,
1321
+ pending: true
1322
+ }).lean();
977
1323
  if (doc) {
978
- 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
+ };
979
1335
  }
980
1336
  throw new ProsopoDBError("DATABASE.PENDING_RECORD_NOT_FOUND", {
981
1337
  context: {
@@ -996,17 +1352,13 @@ class ProviderDatabase extends MongoDatabase {
996
1352
  }
997
1353
  });
998
1354
  }
999
- const filter = {
1000
- [ApiParams.requestHash]: requestHash
1001
- };
1002
- await this.tables?.pending.updateOne(
1003
- filter,
1355
+ await this.tables?.commitment.updateOne(
1356
+ { requestHash },
1004
1357
  {
1005
1358
  $set: {
1006
- [CaptchaStatus.pending]: false
1359
+ pending: false
1007
1360
  }
1008
- },
1009
- { upsert: true }
1361
+ }
1010
1362
  );
1011
1363
  }
1012
1364
  /**
@@ -1107,7 +1459,7 @@ class ProviderDatabase extends MongoDatabase {
1107
1459
  const filter = {
1108
1460
  commitmentId
1109
1461
  };
1110
- const project = { projection: { _id: 0 } };
1462
+ const project = { _id: 0 };
1111
1463
  const cursor = this.tables?.usersolution?.findOne(filter, project).lean();
1112
1464
  const doc = await cursor;
1113
1465
  if (doc) {
@@ -1123,7 +1475,18 @@ class ProviderDatabase extends MongoDatabase {
1123
1475
  */
1124
1476
  async getDappUserCommitmentById(commitmentId) {
1125
1477
  const filter = { id: commitmentId };
1126
- const commitmentCursor = this.tables?.commitment?.findOne(filter).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();
1127
1490
  const doc = await commitmentCursor;
1128
1491
  return doc ? doc : void 0;
1129
1492
  }
@@ -1137,7 +1500,10 @@ class ProviderDatabase extends MongoDatabase {
1137
1500
  userAccount,
1138
1501
  dappAccount
1139
1502
  };
1140
- const project = { _id: 0 };
1503
+ const project = {
1504
+ _id: 0,
1505
+ result: 1
1506
+ };
1141
1507
  const sort = { sort: { _id: -1 } };
1142
1508
  const docs = await this.tables?.commitment?.find(filter, project, sort).lean();
1143
1509
  return docs ? docs : [];
@@ -1153,10 +1519,22 @@ class ProviderDatabase extends MongoDatabase {
1153
1519
  const updateDoc = {
1154
1520
  result,
1155
1521
  lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
1522
+ pendingStage: true,
1156
1523
  ...coords ? { coords } : {}
1157
1524
  };
1158
1525
  const filter = { id: commitmentId };
1159
1526
  await this.tables?.commitment?.findOneAndUpdate(filter, { $set: updateDoc }, { upsert: false }).lean();
1527
+ this.centralStreamer?.streamImageUpdate(
1528
+ () => this.tables.commitment.findOne({ id: commitmentId }).lean(),
1529
+ (ts) => this.tables.commitment.updateOne(
1530
+ { id: commitmentId, lastUpdatedTimestamp: { $lte: ts } },
1531
+ {
1532
+ $set: { storedAtTimestamp: ts },
1533
+ $unset: { pendingStage: 1 }
1534
+ }
1535
+ ).then(() => {
1536
+ })
1537
+ );
1160
1538
  } catch (err) {
1161
1539
  throw new ProsopoDBError("DATABASE.SOLUTION_APPROVE_FAILED", {
1162
1540
  context: { error: err, commitmentId }
@@ -1174,10 +1552,22 @@ class ProviderDatabase extends MongoDatabase {
1174
1552
  const updateDoc = {
1175
1553
  result: { status: CaptchaStatus.disapproved, reason },
1176
1554
  lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
1555
+ pendingStage: true,
1177
1556
  ...coords ? { coords } : {}
1178
1557
  };
1179
1558
  const filter = { id: commitmentId };
1180
1559
  await this.tables?.commitment?.findOneAndUpdate(filter, { $set: updateDoc }, { upsert: false }).lean();
1560
+ this.centralStreamer?.streamImageUpdate(
1561
+ () => this.tables.commitment.findOne({ id: commitmentId }).lean(),
1562
+ (ts) => this.tables.commitment.updateOne(
1563
+ { id: commitmentId, lastUpdatedTimestamp: { $lte: ts } },
1564
+ {
1565
+ $set: { storedAtTimestamp: ts },
1566
+ $unset: { pendingStage: 1 }
1567
+ }
1568
+ ).then(() => {
1569
+ })
1570
+ );
1181
1571
  } catch (err) {
1182
1572
  throw new ProsopoDBError("DATABASE.SOLUTION_APPROVE_FAILED", {
1183
1573
  context: { error: err, commitmentId }
@@ -1313,6 +1703,17 @@ class ProviderDatabase extends MongoDatabase {
1313
1703
  }
1314
1704
  await this.tables?.client.bulkWrite(ops);
1315
1705
  }
1706
+ /**
1707
+ * @description Remove client records by account
1708
+ */
1709
+ async removeClientRecords(accounts) {
1710
+ if (!accounts || accounts.length === 0) {
1711
+ return;
1712
+ }
1713
+ await this.tables?.client.deleteMany({
1714
+ account: { $in: accounts }
1715
+ });
1716
+ }
1316
1717
  /**
1317
1718
  * @description Get all client records
1318
1719
  */
@@ -1325,7 +1726,11 @@ class ProviderDatabase extends MongoDatabase {
1325
1726
  */
1326
1727
  async getClientRecord(account) {
1327
1728
  const filter = { account };
1328
- const doc = await this.tables?.client.findOne(filter).lean();
1729
+ const doc = await this.tables?.client.findOne(filter, {
1730
+ account: 1,
1731
+ settings: 1,
1732
+ tier: 1
1733
+ }).lean();
1329
1734
  return doc ? doc : void 0;
1330
1735
  }
1331
1736
  /**
@@ -1363,6 +1768,97 @@ class ProviderDatabase extends MongoDatabase {
1363
1768
  ).sort({ createdAt: -1 }).lean();
1364
1769
  return (keyRecords || []).map((record) => record.detectorKey);
1365
1770
  }
1771
+ /**
1772
+ * Stores a decision machine artifact with a unique scope identifier.
1773
+ *
1774
+ * The combination of scope + dappAccount uniquely identifies a single artifact:
1775
+ * - Global scope: One artifact per provider (dappAccount is null)
1776
+ * - Dapp scope: One artifact per dapp account (dappAccount is specified)
1777
+ *
1778
+ * @param artifact - The decision machine artifact to store
1779
+ */
1780
+ async upsertDecisionMachineArtifact(artifact) {
1781
+ const now = /* @__PURE__ */ new Date();
1782
+ const dappAccount = artifact.dappAccount ?? null;
1783
+ const filter = {
1784
+ scope: artifact.scope,
1785
+ dappAccount
1786
+ };
1787
+ await this.tables?.decisionMachine.updateOne(
1788
+ filter,
1789
+ {
1790
+ $set: {
1791
+ scope: artifact.scope,
1792
+ dappAccount,
1793
+ runtime: artifact.runtime,
1794
+ language: artifact.language,
1795
+ source: artifact.source,
1796
+ name: artifact.name,
1797
+ version: artifact.version,
1798
+ updatedAt: now
1799
+ },
1800
+ $setOnInsert: {
1801
+ createdAt: now
1802
+ }
1803
+ },
1804
+ { upsert: true }
1805
+ );
1806
+ }
1807
+ /**
1808
+ * Retrieves a single decision machine artifact by scope and optional dapp account.
1809
+ *
1810
+ * @param scope - The scope level (Global or Dapp)
1811
+ * @param dappAccount - Required for Dapp scope, unused for Global scope
1812
+ * @returns The matching artifact, or undefined if not found
1813
+ */
1814
+ async getDecisionMachineArtifact(scope, dappAccount) {
1815
+ const filter = {
1816
+ scope,
1817
+ dappAccount: dappAccount ?? null
1818
+ };
1819
+ const doc = await this.tables?.decisionMachine.findOne(filter).lean();
1820
+ return doc ?? void 0;
1821
+ }
1822
+ /**
1823
+ * Retrieves all decision machine artifacts.
1824
+ *
1825
+ * @returns Array of all decision machine artifacts
1826
+ */
1827
+ async getAllDecisionMachineArtifacts() {
1828
+ const docs = await this.tables?.decisionMachine.find({}).lean();
1829
+ return docs ?? [];
1830
+ }
1831
+ /**
1832
+ * Retrieves a single decision machine artifact by MongoDB ObjectId.
1833
+ *
1834
+ * @param id - The MongoDB ObjectId as a string
1835
+ * @returns The matching artifact, or undefined if not found
1836
+ */
1837
+ async getDecisionMachineArtifactById(id) {
1838
+ const doc = await this.tables?.decisionMachine.findById(id).lean();
1839
+ return doc ?? void 0;
1840
+ }
1841
+ /**
1842
+ * Removes a decision machine artifact by MongoDB ObjectId.
1843
+ *
1844
+ * @param id - The MongoDB ObjectId as a string
1845
+ * @returns true if deleted, false if not found
1846
+ */
1847
+ async removeDecisionMachineArtifact(id) {
1848
+ const result = await this.tables?.decisionMachine.deleteOne({
1849
+ _id: id
1850
+ });
1851
+ return (result?.deletedCount ?? 0) > 0;
1852
+ }
1853
+ /**
1854
+ * Removes all decision machine artifacts.
1855
+ *
1856
+ * @returns The number of artifacts deleted
1857
+ */
1858
+ async removeAllDecisionMachineArtifacts() {
1859
+ const result = await this.tables?.decisionMachine.deleteMany({});
1860
+ return result?.deletedCount ?? 0;
1861
+ }
1366
1862
  /**
1367
1863
  * @description set client context-specific entropy
1368
1864
  */
@@ -1405,7 +1901,7 @@ class ProviderDatabase extends MongoDatabase {
1405
1901
  },
1406
1902
  {
1407
1903
  $lookup: {
1408
- from: "session",
1904
+ from: "sessions",
1409
1905
  localField: "sessionId",
1410
1906
  foreignField: "sessionId",
1411
1907
  as: "sessionData"
@@ -1458,6 +1954,36 @@ class ProviderDatabase extends MongoDatabase {
1458
1954
  })
1459
1955
  )).filter((headHash) => headHash !== void 0);
1460
1956
  }
1957
+ async getSpamEmailDomain(domain) {
1958
+ if (!this.tables?.spamEmailDomain) {
1959
+ throw new ProsopoDBError("DATABASE.DATABASE_IMPORT_ERROR", {
1960
+ context: { failedFuncName: this.getSpamEmailDomain.name }
1961
+ });
1962
+ }
1963
+ const suffixCandidates = buildDomainSuffixCandidates(domain).slice(
1964
+ 0,
1965
+ MAX_DOMAIN_SUFFIX_CANDIDATES
1966
+ );
1967
+ const query = suffixCandidates.length > 0 ? { domain: { $in: suffixCandidates } } : { domain };
1968
+ return await this.tables.spamEmailDomain.findOne(query).exec();
1969
+ }
1970
+ async bulkUpdateSpamEmailDomains(domains, upsert) {
1971
+ if (!this.tables?.spamEmailDomain) {
1972
+ throw new ProsopoDBError("DATABASE.DATABASE_IMPORT_ERROR", {
1973
+ context: { failedFuncName: this.bulkUpdateSpamEmailDomains.name }
1974
+ });
1975
+ }
1976
+ const bulkOps = domains.map((op) => ({
1977
+ updateOne: {
1978
+ filter: op.filter,
1979
+ update: { $set: op.update },
1980
+ upsert
1981
+ }
1982
+ }));
1983
+ if (bulkOps.length > 0) {
1984
+ await this.tables.spamEmailDomain.bulkWrite(bulkOps);
1985
+ }
1986
+ }
1461
1987
  }
1462
1988
  export {
1463
1989
  ProviderDatabase