@prosopo/database 4.1.3 → 4.2.0

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 (29) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +5 -8
  2. package/.turbo/turbo-build$colon$tsc.log +16 -19
  3. package/.turbo/turbo-build.log +6 -12
  4. package/CHANGELOG.md +52 -0
  5. package/LICENSE +201 -0
  6. package/dist/cjs/databases/captcha.cjs +38 -22
  7. package/dist/cjs/databases/centralDbStreamer.cjs +26 -8
  8. package/dist/cjs/databases/provider.cjs +243 -103
  9. package/dist/databases/captcha.d.ts +8 -3
  10. package/dist/databases/captcha.d.ts.map +1 -1
  11. package/dist/databases/captcha.js +39 -23
  12. package/dist/databases/captcha.js.map +1 -1
  13. package/dist/databases/centralDbStreamer.d.ts +7 -1
  14. package/dist/databases/centralDbStreamer.d.ts.map +1 -1
  15. package/dist/databases/centralDbStreamer.js +26 -8
  16. package/dist/databases/centralDbStreamer.js.map +1 -1
  17. package/dist/databases/provider.d.ts +26 -2
  18. package/dist/databases/provider.d.ts.map +1 -1
  19. package/dist/databases/provider.js +244 -104
  20. package/dist/databases/provider.js.map +1 -1
  21. package/dist/tests/integration/countCommitmentsByNormalisedEmail.integration.test.js +67 -0
  22. package/dist/tests/integration/countCommitmentsByNormalisedEmail.integration.test.js.map +1 -1
  23. package/dist/tests/integration/projectionContracts.integration.test.js +54 -0
  24. package/dist/tests/integration/projectionContracts.integration.test.js.map +1 -1
  25. package/dist/tests/integration/serverCheckClaim.integration.test.js +52 -0
  26. package/dist/tests/integration/serverCheckClaim.integration.test.js.map +1 -1
  27. package/dist/tests/integration/sessionRecordProjection.integration.test.js +22 -0
  28. package/dist/tests/integration/sessionRecordProjection.integration.test.js.map +1 -1
  29. package/package.json +66 -67
@@ -1,7 +1,7 @@
1
1
  import { MongoDatabase } from "../base/mongo.js";
2
2
  import { ProsopoDBError } from "@prosopo/common";
3
3
  import { CaptchaStates, CaptchaStatus, CaptchaType, DatasetWithIdsAndTreeSchema, DecisionMachineKind, StoredStatusNames, UserCommitmentSchema, isBlockingCaptchaResult } from "@prosopo/types";
4
- import { CaptchaRecordSchema, ClientRecordSchema, DatasetRecordSchema, DecisionMachineArtifactRecordSchema, PoWCaptchaRecordSchema, PuzzleCaptchaRecordSchema, SESSION_PROJECTION, ScheduledTaskRecordSchema, ScheduledTaskSchema, SessionRecordSchema, SolutionRecordSchema, SpamEmailDomainRecordSchema, UserCommitmentRecordSchema, UserSolutionRecordSchema } from "@prosopo/types-database";
4
+ import { AudioCaptchaRecordSchema, CaptchaRecordSchema, ClientRecordSchema, DatasetRecordSchema, DecisionMachineArtifactRecordSchema, IconOrderCaptchaRecordSchema, PoWCaptchaRecordSchema, PuzzleCaptchaRecordSchema, SESSION_PROJECTION, ScheduledTaskRecordSchema, ScheduledTaskSchema, SessionRecordSchema, SolutionRecordSchema, SpamEmailDomainRecordSchema, UserCommitmentRecordSchema, UserSolutionRecordSchema } from "@prosopo/types-database";
5
5
  import { isHex } from "@polkadot/util/is";
6
6
  import { connectToRedis, setupRedisIndex } from "@prosopo/redis-client";
7
7
  import { accessRulesRedisIndex, createRedisAccessRulesStorage } from "@prosopo/user-access-policy/redis";
@@ -23,6 +23,16 @@ var PROVIDER_TABLES = [
23
23
  modelName: "PuzzleCaptcha",
24
24
  schema: PuzzleCaptchaRecordSchema
25
25
  },
26
+ {
27
+ collectionName: "audiocaptcha",
28
+ modelName: "AudioCaptcha",
29
+ schema: AudioCaptchaRecordSchema
30
+ },
31
+ {
32
+ collectionName: "iconordercaptcha",
33
+ modelName: "IconOrderCaptcha",
34
+ schema: IconOrderCaptchaRecordSchema
35
+ },
26
36
  {
27
37
  collectionName: "dataset",
28
38
  modelName: "Dataset",
@@ -640,46 +650,76 @@ var ProviderDatabase = class ProviderDatabase extends MongoDatabase {
640
650
  $unset: { pendingStage: 1 }
641
651
  }).then(() => {}));
642
652
  }
643
- async storePuzzleCaptchaRecord(challenge, components, targetX, targetY, originX, originY, tolerance, providerSignature, ipAddress, headers, ja4, sessionId, ipInfo) {
644
- const tables = this.getTables();
645
- const puzzleCaptchaRecord = {
653
+ interactiveCollection(captchaType) {
654
+ switch (captchaType) {
655
+ case CaptchaType.puzzle: return {
656
+ table: "puzzlecaptcha",
657
+ label: "PuzzleCaptcha",
658
+ captchaType,
659
+ answerProjection: {
660
+ targetX: 1,
661
+ targetY: 1,
662
+ originX: 1,
663
+ originY: 1,
664
+ tolerance: 1,
665
+ puzzleEvents: 1
666
+ },
667
+ streamRecord: (record, markStored) => this.centralStreamer?.streamPuzzleRecord(record, markStored),
668
+ streamUpdate: (challenge, markStored) => this.centralStreamer?.streamPuzzleUpdate(() => this.getPuzzleCaptchaRecordByChallenge(challenge), markStored)
669
+ };
670
+ case CaptchaType.iconOrder: return {
671
+ table: "iconordercaptcha",
672
+ label: "IconOrderCaptcha",
673
+ captchaType,
674
+ answerProjection: {
675
+ targets: 1,
676
+ tolerance: 1,
677
+ clicks: 1,
678
+ iconOrderEvents: 1
679
+ },
680
+ streamRecord: (record, markStored) => this.centralStreamer?.streamIconOrderRecord(record, markStored),
681
+ streamUpdate: (challenge, markStored) => this.centralStreamer?.streamIconOrderUpdate(() => this.getIconOrderCaptchaRecordByChallenge(challenge), markStored)
682
+ };
683
+ case CaptchaType.audio: return {
684
+ table: "audiocaptcha",
685
+ label: "AudioCaptcha",
686
+ captchaType,
687
+ answerProjection: {
688
+ answer: 1,
689
+ submittedAnswer: 1,
690
+ replays: 1,
691
+ audioEvents: 1
692
+ },
693
+ streamRecord: (record, markStored) => this.centralStreamer?.streamAudioRecord(record, markStored),
694
+ streamUpdate: (challenge, markStored) => this.centralStreamer?.streamAudioUpdate(() => this.getAudioCaptchaRecordByChallenge(challenge), markStored)
695
+ };
696
+ }
697
+ }
698
+ markInteractiveRecordStored(collection, challenge) {
699
+ return (ts) => this.tables[collection.table].updateOne({
646
700
  challenge,
647
- userAccount: components.userAccount,
648
- dappAccount: components.dappAccount,
649
- requestedAtTimestamp: new Date(components.requestedAtTimestamp),
650
- ipAddress,
651
- headers,
652
- ja4,
653
- result: { status: CaptchaStatus.pending },
654
- userSubmitted: false,
655
- serverChecked: false,
656
- targetX,
657
- targetY,
658
- originX,
659
- originY,
660
- tolerance,
661
- providerSignature,
662
- lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
663
- pendingStage: true,
664
- sessionId,
665
- ipInfo
666
- };
701
+ lastUpdatedTimestamp: { $lte: ts }
702
+ }, {
703
+ $set: { storedAtTimestamp: ts },
704
+ $unset: { pendingStage: 1 }
705
+ }).then(() => {});
706
+ }
707
+ streamInteractiveUpdate(collection, challenge) {
708
+ collection.streamUpdate(challenge, this.markInteractiveRecordStored(collection, challenge));
709
+ }
710
+ async storeInteractiveCaptchaRecord(collection, record) {
711
+ const tables = this.getTables();
712
+ const { challenge, ipInfo } = record;
667
713
  try {
668
- await tables.puzzlecaptcha.create(puzzleCaptchaRecord);
714
+ await tables[collection.table].create(record);
669
715
  this.logger.info(() => ({
670
716
  data: {
671
717
  challenge,
672
718
  countryCode: ipInfo?.isValid ? ipInfo.countryCode : void 0
673
719
  },
674
- msg: "PuzzleCaptcha record added successfully"
720
+ msg: `${collection.label} record added successfully`
675
721
  }));
676
- this.centralStreamer?.streamPuzzleRecord(puzzleCaptchaRecord, (ts) => this.tables.puzzlecaptcha.updateOne({
677
- challenge,
678
- lastUpdatedTimestamp: { $lte: ts }
679
- }, {
680
- $set: { storedAtTimestamp: ts },
681
- $unset: { pendingStage: 1 }
682
- }).then(() => {}));
722
+ collection.streamRecord(record, this.markInteractiveRecordStored(collection, challenge));
683
723
  } catch (error) {
684
724
  const err = new ProsopoDBError("DATABASE.CAPTCHA_UPDATE_FAILED", {
685
725
  context: {
@@ -691,24 +731,60 @@ var ProviderDatabase = class ProviderDatabase extends MongoDatabase {
691
731
  });
692
732
  this.logger.error(() => ({
693
733
  err: error,
694
- msg: "Failed to add PuzzleCaptcha record"
734
+ msg: `Failed to add ${collection.label} record`
695
735
  }));
696
736
  throw err;
697
737
  }
698
738
  }
699
- /**
700
- * @description Retrieves a Puzzle Captcha record by its challenge string.
701
- * @param {string} challenge The challenge string to search for.
702
- * @returns {Promise<PuzzleCaptchaRecord | null>} A promise that resolves with the found record or null if not found.
703
- */
704
- async getPuzzleCaptchaRecordByChallenge(challenge) {
739
+ pendingInteractiveRecord(challenge, components, providerSignature, ipAddress, headers, ja4, sessionId, ipInfo) {
740
+ return {
741
+ challenge,
742
+ userAccount: components.userAccount,
743
+ dappAccount: components.dappAccount,
744
+ requestedAtTimestamp: new Date(components.requestedAtTimestamp),
745
+ ipAddress,
746
+ headers,
747
+ ja4,
748
+ result: { status: CaptchaStatus.pending },
749
+ userSubmitted: false,
750
+ serverChecked: false,
751
+ providerSignature,
752
+ lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
753
+ pendingStage: true,
754
+ sessionId,
755
+ ipInfo
756
+ };
757
+ }
758
+ async storePuzzleCaptchaRecord(challenge, components, targetX, targetY, originX, originY, tolerance, providerSignature, ipAddress, headers, ja4, sessionId, ipInfo) {
759
+ await this.storeInteractiveCaptchaRecord(this.interactiveCollection(CaptchaType.puzzle), {
760
+ ...this.pendingInteractiveRecord(challenge, components, providerSignature, ipAddress, headers, ja4, sessionId, ipInfo),
761
+ tolerance,
762
+ targetX,
763
+ targetY,
764
+ originX,
765
+ originY
766
+ });
767
+ }
768
+ async storeAudioCaptchaRecord(challenge, components, answer, providerSignature, ipAddress, headers, ja4, sessionId, ipInfo) {
769
+ await this.storeInteractiveCaptchaRecord(this.interactiveCollection(CaptchaType.audio), {
770
+ ...this.pendingInteractiveRecord(challenge, components, providerSignature, ipAddress, headers, ja4, sessionId, ipInfo),
771
+ answer
772
+ });
773
+ }
774
+ async storeIconOrderCaptchaRecord(challenge, components, targets, tolerance, providerSignature, ipAddress, headers, ja4, sessionId, ipInfo) {
775
+ await this.storeInteractiveCaptchaRecord(this.interactiveCollection(CaptchaType.iconOrder), {
776
+ ...this.pendingInteractiveRecord(challenge, components, providerSignature, ipAddress, headers, ja4, sessionId, ipInfo),
777
+ targets,
778
+ tolerance
779
+ });
780
+ }
781
+ async getInteractiveCaptchaRecordByChallenge(collection, challenge, failedFuncName) {
705
782
  if (!this.tables) throw new ProsopoDBError("DATABASE.DATABASE_UNDEFINED", {
706
- context: { failedFuncName: this.getPuzzleCaptchaRecordByChallenge.name },
783
+ context: { failedFuncName },
707
784
  logger: this.logger
708
785
  });
709
786
  try {
710
- const filter = { challenge };
711
- const record = await this.tables.puzzlecaptcha.findOne(filter, {
787
+ const record = await this.tables[collection.table].findOne({ challenge }, {
712
788
  challenge: 1,
713
789
  userAccount: 1,
714
790
  dappAccount: 1,
@@ -718,12 +794,6 @@ var ProviderDatabase = class ProviderDatabase extends MongoDatabase {
718
794
  headers: 1,
719
795
  ja4: 1,
720
796
  result: 1,
721
- targetX: 1,
722
- targetY: 1,
723
- originX: 1,
724
- originY: 1,
725
- tolerance: 1,
726
- puzzleEvents: 1,
727
797
  sessionId: 1,
728
798
  ipInfo: 1,
729
799
  deviceCapability: 1,
@@ -731,18 +801,19 @@ var ProviderDatabase = class ProviderDatabase extends MongoDatabase {
731
801
  serverChecked: 1,
732
802
  userSubmitted: 1,
733
803
  coords: 1,
734
- clientMetaData: 1
804
+ clientMetaData: 1,
805
+ ...collection.answerProjection
735
806
  }).lean();
736
807
  if (record) {
737
808
  this.logger.info(() => ({
738
809
  data: { challenge },
739
- msg: "PuzzleCaptcha record retrieved successfully"
810
+ msg: `${collection.label} record retrieved successfully`
740
811
  }));
741
812
  return record;
742
813
  }
743
814
  this.logger.info(() => ({
744
815
  data: { challenge },
745
- msg: "No PuzzleCaptcha record found"
816
+ msg: `No ${collection.label} record found`
746
817
  }));
747
818
  return null;
748
819
  } catch (error) {
@@ -755,23 +826,71 @@ var ProviderDatabase = class ProviderDatabase extends MongoDatabase {
755
826
  });
756
827
  this.logger.error(() => ({
757
828
  err,
758
- msg: "Failed to retrieve PuzzleCaptcha record"
829
+ msg: `Failed to retrieve ${collection.label} record`
759
830
  }));
760
831
  throw err;
761
832
  }
762
833
  }
763
834
  /**
764
- * @description Updates a Puzzle Captcha record result in the database.
765
- * @param {string} challenge The challenge string of the captcha to be updated.
766
- * @param result
767
- * @param serverChecked
768
- * @param userSubmitted
769
- * @param userSignature
770
- * @param coords
771
- * @param lastUpdatedTimestamp
772
- * @returns {Promise<void>} A promise that resolves when the record is updated.
835
+ * @description Retrieves a Puzzle Captcha record by its challenge string.
836
+ * @param {string} challenge The challenge string to search for.
837
+ * @returns {Promise<PuzzleCaptchaRecord | null>} A promise that resolves with the found record or null if not found.
773
838
  */
774
- async updatePuzzleCaptchaRecordResult(challenge, result, serverChecked = false, userSubmitted = false, userSignature, coords, lastUpdatedTimestamp) {
839
+ async getPuzzleCaptchaRecordByChallenge(challenge) {
840
+ return this.getInteractiveCaptchaRecordByChallenge(this.interactiveCollection(CaptchaType.puzzle), challenge, this.getPuzzleCaptchaRecordByChallenge.name);
841
+ }
842
+ async getIconOrderCaptchaRecordByChallenge(challenge) {
843
+ return this.getInteractiveCaptchaRecordByChallenge(this.interactiveCollection(CaptchaType.iconOrder), challenge, this.getIconOrderCaptchaRecordByChallenge.name);
844
+ }
845
+ async getAudioCaptchaRecordByChallenge(challenge) {
846
+ return this.getInteractiveCaptchaRecordByChallenge(this.interactiveCollection(CaptchaType.audio), challenge, this.getAudioCaptchaRecordByChallenge.name);
847
+ }
848
+ /**
849
+ * The filter is the guard: of N concurrent submitters only one matches
850
+ * `userSubmitted: { $ne: true }`, where a read-then-check would let them
851
+ * all through to be graded. `submittedAtTimestamp` is stamped here so the
852
+ * record is consistent even if the caller dies before writing a result.
853
+ */
854
+ async claimInteractiveCaptchaSubmission(collection, challenge) {
855
+ const tables = this.getTables();
856
+ try {
857
+ const won = (await tables[collection.table].updateOne({
858
+ challenge,
859
+ userSubmitted: { $ne: true }
860
+ }, { $set: {
861
+ userSubmitted: true,
862
+ submittedAtTimestamp: /* @__PURE__ */ new Date()
863
+ } })).modifiedCount === 1;
864
+ this.logger.info(() => ({
865
+ data: {
866
+ challenge,
867
+ won
868
+ },
869
+ msg: won ? `${collection.label} submission claimed` : `${collection.label} submission already claimed`
870
+ }));
871
+ return won;
872
+ } catch (error) {
873
+ const err = new ProsopoDBError("DATABASE.CAPTCHA_UPDATE_FAILED", {
874
+ context: {
875
+ error,
876
+ challenge
877
+ },
878
+ logger: this.logger
879
+ });
880
+ this.logger.error(() => ({
881
+ err,
882
+ msg: `Failed to claim ${collection.label} submission`
883
+ }));
884
+ throw err;
885
+ }
886
+ }
887
+ async claimIconOrderCaptchaSubmission(challenge) {
888
+ return this.claimInteractiveCaptchaSubmission(this.interactiveCollection(CaptchaType.iconOrder), challenge);
889
+ }
890
+ async claimAudioCaptchaSubmission(challenge) {
891
+ return this.claimInteractiveCaptchaSubmission(this.interactiveCollection(CaptchaType.audio), challenge);
892
+ }
893
+ async updateInteractiveCaptchaRecordResult(collection, challenge, result, serverChecked, userSubmitted, userSignature, coords, lastUpdatedTimestamp) {
775
894
  const tables = this.getTables();
776
895
  const timestamp = lastUpdatedTimestamp ?? /* @__PURE__ */ new Date();
777
896
  const isDisapproved = result.status === CaptchaStatus.disapproved;
@@ -783,13 +902,13 @@ var ProviderDatabase = class ProviderDatabase extends MongoDatabase {
783
902
  userSignature,
784
903
  lastUpdatedTimestamp: timestamp,
785
904
  pendingStage: true,
786
- blocked: isBlockingCaptchaResult(CaptchaType.puzzle, result),
905
+ blocked: isBlockingCaptchaResult(collection.captchaType, result),
787
906
  ...coords && { coords },
788
907
  ...userSubmitted && { submittedAtTimestamp: timestamp },
789
908
  ...isDisapproved && { failedAtTimestamp: timestamp }
790
909
  };
791
910
  try {
792
- if ((await tables.puzzlecaptcha.updateOne({ challenge }, { $set: setStage })).matchedCount === 0) {
911
+ if ((await tables[collection.table].updateOne({ challenge }, { $set: setStage })).matchedCount === 0) {
793
912
  const err = new ProsopoDBError("DATABASE.CAPTCHA_GET_FAILED", {
794
913
  context: {
795
914
  challenge,
@@ -799,7 +918,7 @@ var ProviderDatabase = class ProviderDatabase extends MongoDatabase {
799
918
  });
800
919
  this.logger.info(() => ({
801
920
  err,
802
- msg: "No PuzzleCaptcha record found to update"
921
+ msg: `No ${collection.label} record found to update`
803
922
  }));
804
923
  throw err;
805
924
  }
@@ -808,15 +927,9 @@ var ProviderDatabase = class ProviderDatabase extends MongoDatabase {
808
927
  challenge,
809
928
  ...setStage
810
929
  },
811
- msg: "PuzzleCaptcha record updated successfully"
930
+ msg: `${collection.label} record updated successfully`
812
931
  }));
813
- this.centralStreamer?.streamPuzzleUpdate(() => this.getPuzzleCaptchaRecordByChallenge(challenge), (ts) => this.tables.puzzlecaptcha.updateOne({
814
- challenge,
815
- lastUpdatedTimestamp: { $lte: ts }
816
- }, {
817
- $set: { storedAtTimestamp: ts },
818
- $unset: { pendingStage: 1 }
819
- }).then(() => {}));
932
+ this.streamInteractiveUpdate(collection, challenge);
820
933
  } catch (error) {
821
934
  const err = new ProsopoDBError("DATABASE.CAPTCHA_UPDATE_FAILED", {
822
935
  context: {
@@ -828,12 +941,32 @@ var ProviderDatabase = class ProviderDatabase extends MongoDatabase {
828
941
  });
829
942
  this.logger.error(() => ({
830
943
  err,
831
- msg: "Failed to update PuzzleCaptcha record"
944
+ msg: `Failed to update ${collection.label} record`
832
945
  }));
833
946
  throw err;
834
947
  }
835
948
  }
836
- async updatePuzzleCaptchaRecord(challenge, updates) {
949
+ /**
950
+ * @description Updates a Puzzle Captcha record result in the database.
951
+ * @param {string} challenge The challenge string of the captcha to be updated.
952
+ * @param result
953
+ * @param serverChecked
954
+ * @param userSubmitted
955
+ * @param userSignature
956
+ * @param coords
957
+ * @param lastUpdatedTimestamp
958
+ * @returns {Promise<void>} A promise that resolves when the record is updated.
959
+ */
960
+ async updatePuzzleCaptchaRecordResult(challenge, result, serverChecked = false, userSubmitted = false, userSignature, coords, lastUpdatedTimestamp) {
961
+ await this.updateInteractiveCaptchaRecordResult(this.interactiveCollection(CaptchaType.puzzle), challenge, result, serverChecked, userSubmitted, userSignature, coords, lastUpdatedTimestamp);
962
+ }
963
+ async updateIconOrderCaptchaRecordResult(challenge, result, serverChecked = false, userSubmitted = false, userSignature, coords, lastUpdatedTimestamp) {
964
+ await this.updateInteractiveCaptchaRecordResult(this.interactiveCollection(CaptchaType.iconOrder), challenge, result, serverChecked, userSubmitted, userSignature, coords, lastUpdatedTimestamp);
965
+ }
966
+ async updateAudioCaptchaRecordResult(challenge, result, serverChecked = false, userSubmitted = false, userSignature, coords, lastUpdatedTimestamp) {
967
+ await this.updateInteractiveCaptchaRecordResult(this.interactiveCollection(CaptchaType.audio), challenge, result, serverChecked, userSubmitted, userSignature, coords, lastUpdatedTimestamp);
968
+ }
969
+ async updateInteractiveCaptchaRecord(collection, challenge, updates) {
837
970
  const tables = this.getTables();
838
971
  const timestamp = /* @__PURE__ */ new Date();
839
972
  const baseSet = {
@@ -843,19 +976,22 @@ var ProviderDatabase = class ProviderDatabase extends MongoDatabase {
843
976
  ...updates.userSubmitted === true && { submittedAtTimestamp: timestamp },
844
977
  ...updates.result?.status === CaptchaStatus.disapproved && { failedAtTimestamp: timestamp }
845
978
  };
846
- await tables.puzzlecaptcha.updateOne({ challenge }, { $set: baseSet });
847
- this.centralStreamer?.streamPuzzleUpdate(() => this.getPuzzleCaptchaRecordByChallenge(challenge), (ts) => this.tables.puzzlecaptcha.updateOne({
848
- challenge,
849
- lastUpdatedTimestamp: { $lte: ts }
850
- }, {
851
- $set: { storedAtTimestamp: ts },
852
- $unset: { pendingStage: 1 }
853
- }).then(() => {}));
979
+ await tables[collection.table].updateOne({ challenge }, { $set: baseSet });
980
+ this.streamInteractiveUpdate(collection, challenge);
854
981
  }
855
- async markPuzzleCaptchaRecordChecked(challenge) {
982
+ async updatePuzzleCaptchaRecord(challenge, updates) {
983
+ await this.updateInteractiveCaptchaRecord(this.interactiveCollection(CaptchaType.puzzle), challenge, updates);
984
+ }
985
+ async updateIconOrderCaptchaRecord(challenge, updates) {
986
+ await this.updateInteractiveCaptchaRecord(this.interactiveCollection(CaptchaType.iconOrder), challenge, updates);
987
+ }
988
+ async updateAudioCaptchaRecord(challenge, updates) {
989
+ await this.updateInteractiveCaptchaRecord(this.interactiveCollection(CaptchaType.audio), challenge, updates);
990
+ }
991
+ async markInteractiveCaptchaRecordChecked(collection, challenge) {
856
992
  const tables = this.getTables();
857
993
  const timestamp = /* @__PURE__ */ new Date();
858
- if ((await tables.puzzlecaptcha.updateOne({
994
+ if ((await tables[collection.table].updateOne({
859
995
  challenge,
860
996
  serverChecked: { $ne: true }
861
997
  }, { $set: {
@@ -864,15 +1000,18 @@ var ProviderDatabase = class ProviderDatabase extends MongoDatabase {
864
1000
  verifiedAtTimestamp: timestamp,
865
1001
  pendingStage: true
866
1002
  } })).modifiedCount === 0) return false;
867
- this.centralStreamer?.streamPuzzleUpdate(() => this.getPuzzleCaptchaRecordByChallenge(challenge), (ts) => this.tables.puzzlecaptcha.updateOne({
868
- challenge,
869
- lastUpdatedTimestamp: { $lte: ts }
870
- }, {
871
- $set: { storedAtTimestamp: ts },
872
- $unset: { pendingStage: 1 }
873
- }).then(() => {}));
1003
+ this.streamInteractiveUpdate(collection, challenge);
874
1004
  return true;
875
1005
  }
1006
+ async markIconOrderCaptchaRecordChecked(challenge) {
1007
+ return this.markInteractiveCaptchaRecordChecked(this.interactiveCollection(CaptchaType.iconOrder), challenge);
1008
+ }
1009
+ async markAudioCaptchaRecordChecked(challenge) {
1010
+ return this.markInteractiveCaptchaRecordChecked(this.interactiveCollection(CaptchaType.audio), challenge);
1011
+ }
1012
+ async markPuzzleCaptchaRecordChecked(challenge) {
1013
+ return this.markInteractiveCaptchaRecordChecked(this.interactiveCollection(CaptchaType.puzzle), challenge);
1014
+ }
876
1015
  /** @description Get serverChecked Dapp User image captcha commitments from the commitments table
877
1016
  */
878
1017
  async getCheckedDappUserCommitments() {
@@ -966,13 +1105,13 @@ var ProviderDatabase = class ProviderDatabase extends MongoDatabase {
966
1105
  } }], { updatePipeline: true });
967
1106
  }
968
1107
  /**
969
- * Counts server-checked captcha records across image (`commitment`),
970
- * PoW and puzzle collections whose `metadata.emailNormalised` matches.
971
- * Each collection carries the same partial index
972
- * (`spamEmailCount_partial`) so all three counts hit index-only scans.
973
- * The three counts are summed — one dapp can mix captcha types over
974
- * time, and per-email rate limits should apply across the whole
975
- * verified surface, not per-type.
1108
+ * Counts server-checked captcha records across the image (`commitment`),
1109
+ * PoW, puzzle and icon-order collections whose `metadata.emailNormalised`
1110
+ * matches. Each collection carries the same partial index
1111
+ * (`spamEmailCount_partial`) so every count hits an index-only scan. The
1112
+ * counts are summed — one dapp can mix captcha types over time, and
1113
+ * per-email rate limits should apply across the whole verified surface,
1114
+ * not per-type.
976
1115
  */
977
1116
  async countCommitmentsByNormalisedEmail(dappAccount, emailNormalised) {
978
1117
  if (!emailNormalised) return 0;
@@ -982,12 +1121,13 @@ var ProviderDatabase = class ProviderDatabase extends MongoDatabase {
982
1121
  serverChecked: true,
983
1122
  "metadata.emailNormalised": emailNormalised
984
1123
  };
985
- const [imgCount, powCount, puzzleCount] = await Promise.all([
1124
+ return (await Promise.all([
986
1125
  tables.commitment.countDocuments(filter),
987
1126
  tables.powcaptcha.countDocuments(filter),
988
- tables.puzzlecaptcha.countDocuments(filter)
989
- ]);
990
- return imgCount + powCount + puzzleCount;
1127
+ tables.puzzlecaptcha.countDocuments(filter),
1128
+ tables.iconordercaptcha.countDocuments(filter),
1129
+ tables.audiocaptcha.countDocuments(filter)
1130
+ ])).reduce((total, count) => total + count, 0);
991
1131
  }
992
1132
  /**
993
1133
  * @description Get Dapp User PoW captcha commitments that have not been counted towards the client's total