@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
@@ -23,6 +23,16 @@ var PROVIDER_TABLES = [
23
23
  modelName: "PuzzleCaptcha",
24
24
  schema: _prosopo_types_database.PuzzleCaptchaRecordSchema
25
25
  },
26
+ {
27
+ collectionName: "audiocaptcha",
28
+ modelName: "AudioCaptcha",
29
+ schema: _prosopo_types_database.AudioCaptchaRecordSchema
30
+ },
31
+ {
32
+ collectionName: "iconordercaptcha",
33
+ modelName: "IconOrderCaptcha",
34
+ schema: _prosopo_types_database.IconOrderCaptchaRecordSchema
35
+ },
26
36
  {
27
37
  collectionName: "dataset",
28
38
  modelName: "Dataset",
@@ -640,46 +650,76 @@ var ProviderDatabase = class ProviderDatabase extends require_mongo.MongoDatabas
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 _prosopo_types.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 _prosopo_types.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 _prosopo_types.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: _prosopo_types.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 _prosopo_common.ProsopoDBError("DATABASE.CAPTCHA_UPDATE_FAILED", {
685
725
  context: {
@@ -691,24 +731,60 @@ var ProviderDatabase = class ProviderDatabase extends require_mongo.MongoDatabas
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: _prosopo_types.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(_prosopo_types.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(_prosopo_types.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(_prosopo_types.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 _prosopo_common.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 require_mongo.MongoDatabas
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 require_mongo.MongoDatabas
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 require_mongo.MongoDatabas
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(_prosopo_types.CaptchaType.puzzle), challenge, this.getPuzzleCaptchaRecordByChallenge.name);
841
+ }
842
+ async getIconOrderCaptchaRecordByChallenge(challenge) {
843
+ return this.getInteractiveCaptchaRecordByChallenge(this.interactiveCollection(_prosopo_types.CaptchaType.iconOrder), challenge, this.getIconOrderCaptchaRecordByChallenge.name);
844
+ }
845
+ async getAudioCaptchaRecordByChallenge(challenge) {
846
+ return this.getInteractiveCaptchaRecordByChallenge(this.interactiveCollection(_prosopo_types.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 _prosopo_common.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(_prosopo_types.CaptchaType.iconOrder), challenge);
889
+ }
890
+ async claimAudioCaptchaSubmission(challenge) {
891
+ return this.claimInteractiveCaptchaSubmission(this.interactiveCollection(_prosopo_types.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 === _prosopo_types.CaptchaStatus.disapproved;
@@ -783,13 +902,13 @@ var ProviderDatabase = class ProviderDatabase extends require_mongo.MongoDatabas
783
902
  userSignature,
784
903
  lastUpdatedTimestamp: timestamp,
785
904
  pendingStage: true,
786
- blocked: (0, _prosopo_types.isBlockingCaptchaResult)(_prosopo_types.CaptchaType.puzzle, result),
905
+ blocked: (0, _prosopo_types.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 _prosopo_common.ProsopoDBError("DATABASE.CAPTCHA_GET_FAILED", {
794
913
  context: {
795
914
  challenge,
@@ -799,7 +918,7 @@ var ProviderDatabase = class ProviderDatabase extends require_mongo.MongoDatabas
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 require_mongo.MongoDatabas
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 _prosopo_common.ProsopoDBError("DATABASE.CAPTCHA_UPDATE_FAILED", {
822
935
  context: {
@@ -828,12 +941,32 @@ var ProviderDatabase = class ProviderDatabase extends require_mongo.MongoDatabas
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(_prosopo_types.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(_prosopo_types.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(_prosopo_types.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 require_mongo.MongoDatabas
843
976
  ...updates.userSubmitted === true && { submittedAtTimestamp: timestamp },
844
977
  ...updates.result?.status === _prosopo_types.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(_prosopo_types.CaptchaType.puzzle), challenge, updates);
984
+ }
985
+ async updateIconOrderCaptchaRecord(challenge, updates) {
986
+ await this.updateInteractiveCaptchaRecord(this.interactiveCollection(_prosopo_types.CaptchaType.iconOrder), challenge, updates);
987
+ }
988
+ async updateAudioCaptchaRecord(challenge, updates) {
989
+ await this.updateInteractiveCaptchaRecord(this.interactiveCollection(_prosopo_types.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 require_mongo.MongoDatabas
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(_prosopo_types.CaptchaType.iconOrder), challenge);
1008
+ }
1009
+ async markAudioCaptchaRecordChecked(challenge) {
1010
+ return this.markInteractiveCaptchaRecordChecked(this.interactiveCollection(_prosopo_types.CaptchaType.audio), challenge);
1011
+ }
1012
+ async markPuzzleCaptchaRecordChecked(challenge) {
1013
+ return this.markInteractiveCaptchaRecordChecked(this.interactiveCollection(_prosopo_types.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 require_mongo.MongoDatabas
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 require_mongo.MongoDatabas
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
@@ -1,5 +1,5 @@
1
1
  import { type Logger } from "@prosopo/logger";
2
- import { type CaptchaProperties, type ICaptchaDatabase, type PoWCaptchaRecord, type PuzzleCaptchaRecord, type StoredSession, type Tables, type UserCommitmentRecord } from "@prosopo/types-database";
2
+ import { type AudioCaptchaRecord, type CaptchaProperties, type ICaptchaDatabase, type IconOrderCaptchaRecord, type PoWCaptchaRecord, type PuzzleCaptchaRecord, type StoredSession, type Tables, type UserCommitmentRecord } from "@prosopo/types-database";
3
3
  import type { QueryFilter } from "mongoose";
4
4
  import { type MongoConnectionTimeouts, MongoDatabase } from "../base/index.js";
5
5
  declare enum TableNames {
@@ -7,7 +7,9 @@ declare enum TableNames {
7
7
  session = "session",
8
8
  commitment = "commitment",
9
9
  powcaptcha = "powcaptcha",
10
- puzzlecaptcha = "puzzlecaptcha"
10
+ puzzlecaptcha = "puzzlecaptcha",
11
+ audiocaptcha = "audiocaptcha",
12
+ iconordercaptcha = "iconordercaptcha"
11
13
  }
12
14
  export declare class CaptchaDatabase extends MongoDatabase implements ICaptchaDatabase {
13
15
  tables: Tables<TableNames>;
@@ -18,11 +20,14 @@ export declare class CaptchaDatabase extends MongoDatabase implements ICaptchaDa
18
20
  ensureIndexes(): Promise<void>;
19
21
  private static normaliseCompositeIp;
20
22
  private static normaliseDocCompositeIps;
21
- saveCaptchas(sessionEvents: StoredSession[], imageCaptchaEvents: UserCommitmentRecord[], powCaptchaEvents: PoWCaptchaRecord[], puzzleCaptchaEvents?: PuzzleCaptchaRecord[]): Promise<void>;
23
+ saveCaptchas(sessionEvents: StoredSession[], imageCaptchaEvents: UserCommitmentRecord[], powCaptchaEvents: PoWCaptchaRecord[], puzzleCaptchaEvents?: PuzzleCaptchaRecord[], iconOrderCaptchaEvents?: IconOrderCaptchaRecord[], audioCaptchaEvents?: AudioCaptchaRecord[]): Promise<void>;
24
+ private upsertChallengeRecords;
22
25
  getCaptchas(filter?: QueryFilter<CaptchaProperties>, limit?: number): Promise<{
23
26
  userCommitmentRecords: UserCommitmentRecord[];
24
27
  powCaptchaRecords: PoWCaptchaRecord[];
25
28
  puzzleCaptchaRecords: PuzzleCaptchaRecord[];
29
+ audioCaptchaRecords: AudioCaptchaRecord[];
30
+ iconOrderCaptchaRecords: IconOrderCaptchaRecord[];
26
31
  }>;
27
32
  }
28
33
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"captcha.d.ts","sourceRoot":"","sources":["../../src/databases/captcha.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,KAAK,MAAM,EAAa,MAAM,iBAAiB,CAAC;AACzD,OAAO,EACN,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EAGxB,KAAK,aAAa,EAGlB,KAAK,MAAM,EACX,KAAK,oBAAoB,EACzB,MAAM,yBAAyB,CAAC;AAajC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EAAE,KAAK,uBAAuB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAI/E,aAAK,UAAU;IACd,iBAAiB,sBAAsB;IACvC,OAAO,YAAY;IACnB,UAAU,eAAe;IACzB,UAAU,eAAe;IACzB,aAAa,kBAAkB;CAC/B;AAyBD,qBAAa,eAAgB,SAAQ,aAAc,YAAW,gBAAgB;IAC7E,MAAM,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;IAC3B,OAAO,CAAC,cAAc,CAAS;gBAG9B,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,MAAM,EACf,UAAU,CAAC,EAAE,MAAM,EACnB,MAAM,CAAC,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,uBAAuB;IAMpB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAUvC,SAAS,IAAI,MAAM,CAAC,UAAU,CAAC;IAUzB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAqDpC,OAAO,CAAC,MAAM,CAAC,oBAAoB;IAoBnC,OAAO,CAAC,MAAM,CAAC,wBAAwB;IAajC,YAAY,CACjB,aAAa,EAAE,aAAa,EAAE,EAC9B,kBAAkB,EAAE,oBAAoB,EAAE,EAC1C,gBAAgB,EAAE,gBAAgB,EAAE,EACpC,mBAAmB,GAAE,mBAAmB,EAAO;IAmI1C,WAAW,CAChB,MAAM,GAAE,WAAW,CAAC,iBAAiB,CAAM,EAC3C,KAAK,SAAM,GACT,OAAO,CAAC;QACV,qBAAqB,EAAE,oBAAoB,EAAE,CAAC;QAC9C,iBAAiB,EAAE,gBAAgB,EAAE,CAAC;QACtC,oBAAoB,EAAE,mBAAmB,EAAE,CAAC;KAC5C,CAAC;CAsCF"}
1
+ {"version":3,"file":"captcha.d.ts","sourceRoot":"","sources":["../../src/databases/captcha.ts"],"names":[],"mappings":"AAeA,OAAO,EAAE,KAAK,MAAM,EAAa,MAAM,iBAAiB,CAAC;AACzD,OAAO,EACN,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,gBAAgB,EACrB,KAAK,sBAAsB,EAC3B,KAAK,gBAAgB,EACrB,KAAK,mBAAmB,EAKxB,KAAK,aAAa,EAGlB,KAAK,MAAM,EACX,KAAK,oBAAoB,EACzB,MAAM,yBAAyB,CAAC;AAajC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EAAE,KAAK,uBAAuB,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAI/E,aAAK,UAAU;IACd,iBAAiB,sBAAsB;IACvC,OAAO,YAAY;IACnB,UAAU,eAAe;IACzB,UAAU,eAAe;IACzB,aAAa,kBAAkB;IAC/B,YAAY,iBAAiB;IAC7B,gBAAgB,qBAAqB;CACrC;AAmCD,qBAAa,eAAgB,SAAQ,aAAc,YAAW,gBAAgB;IAC7E,MAAM,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC;IAC3B,OAAO,CAAC,cAAc,CAAS;gBAG9B,GAAG,EAAE,MAAM,EACX,MAAM,CAAC,EAAE,MAAM,EACf,UAAU,CAAC,EAAE,MAAM,EACnB,MAAM,CAAC,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,uBAAuB;IAMpB,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAUvC,SAAS,IAAI,MAAM,CAAC,UAAU,CAAC;IAUzB,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAqDpC,OAAO,CAAC,MAAM,CAAC,oBAAoB;IAoBnC,OAAO,CAAC,MAAM,CAAC,wBAAwB;IAajC,YAAY,CACjB,aAAa,EAAE,aAAa,EAAE,EAC9B,kBAAkB,EAAE,oBAAoB,EAAE,EAC1C,gBAAgB,EAAE,gBAAgB,EAAE,EACpC,mBAAmB,GAAE,mBAAmB,EAAO,EAC/C,sBAAsB,GAAE,sBAAsB,EAAO,EACrD,kBAAkB,GAAE,kBAAkB,EAAO;YA0HhC,sBAAsB;IAuC9B,WAAW,CAChB,MAAM,GAAE,WAAW,CAAC,iBAAiB,CAAM,EAC3C,KAAK,SAAM,GACT,OAAO,CAAC;QACV,qBAAqB,EAAE,oBAAoB,EAAE,CAAC;QAC9C,iBAAiB,EAAE,gBAAgB,EAAE,CAAC;QACtC,oBAAoB,EAAE,mBAAmB,EAAE,CAAC;QAC5C,mBAAmB,EAAE,kBAAkB,EAAE,CAAC;QAC1C,uBAAuB,EAAE,sBAAsB,EAAE,CAAC;KAClD,CAAC;CAkDF"}
@@ -1,7 +1,7 @@
1
1
  import { MongoDatabase } from "../base/mongo.js";
2
2
  import { ProsopoDBError } from "@prosopo/common";
3
3
  import { getLogger } from "@prosopo/logger";
4
- import { StoredPoWCaptchaRecordSchema, StoredPuzzleCaptchaRecordSchema, StoredSessionRecordSchema, StoredUserCommitmentRecordSchema } from "@prosopo/types-database";
4
+ import { StoredAudioCaptchaRecordSchema, StoredIconOrderCaptchaRecordSchema, StoredPoWCaptchaRecordSchema, StoredPuzzleCaptchaRecordSchema, StoredSessionRecordSchema, StoredUserCommitmentRecordSchema } from "@prosopo/types-database";
5
5
  import { Decimal128, Long } from "bson";
6
6
  //#region src/databases/captcha.ts
7
7
  var isBsonLong = (value) => typeof value === "object" && value !== null && "_bsontype" in value && value._bsontype === "Long";
@@ -26,6 +26,16 @@ var CAPTCHA_TABLES = [
26
26
  collectionName: "puzzlecaptcha",
27
27
  modelName: "PuzzleCaptcha",
28
28
  schema: StoredPuzzleCaptchaRecordSchema
29
+ },
30
+ {
31
+ collectionName: "audiocaptcha",
32
+ modelName: "AudioCaptcha",
33
+ schema: StoredAudioCaptchaRecordSchema
34
+ },
35
+ {
36
+ collectionName: "iconordercaptcha",
37
+ modelName: "IconOrderCaptcha",
38
+ schema: StoredIconOrderCaptchaRecordSchema
29
39
  }
30
40
  ];
31
41
  var CaptchaDatabase = class CaptchaDatabase extends MongoDatabase {
@@ -111,7 +121,7 @@ var CaptchaDatabase = class CaptchaDatabase extends MongoDatabase {
111
121
  providedIp: CaptchaDatabase.normaliseCompositeIp(doc.providedIp)
112
122
  };
113
123
  }
114
- async saveCaptchas(sessionEvents, imageCaptchaEvents, powCaptchaEvents, puzzleCaptchaEvents = []) {
124
+ async saveCaptchas(sessionEvents, imageCaptchaEvents, powCaptchaEvents, puzzleCaptchaEvents = [], iconOrderCaptchaEvents = [], audioCaptchaEvents = []) {
115
125
  await this.connect();
116
126
  if (sessionEvents.length) {
117
127
  const result = await this.tables.session.bulkWrite(sessionEvents.map((document) => {
@@ -173,35 +183,41 @@ var CaptchaDatabase = class CaptchaDatabase extends MongoDatabase {
173
183
  msg: "Mongo Saved PoW Events"
174
184
  }));
175
185
  }
176
- if (puzzleCaptchaEvents.length) {
177
- const result = await this.tables.puzzlecaptcha.bulkWrite(puzzleCaptchaEvents.map((doc) => {
178
- const { _id, ...safeDoc } = doc;
179
- const normalised = CaptchaDatabase.normaliseDocCompositeIps(safeDoc);
180
- return { updateOne: {
181
- filter: { challenge: normalised.challenge },
182
- update: { $set: normalised },
183
- upsert: true
184
- } };
185
- }));
186
- logger.info(() => ({
187
- data: {
188
- upsertedCount: result.upsertedCount,
189
- matchedCount: result.matchedCount,
190
- modifiedCount: result.modifiedCount,
191
- totalProcessed: puzzleCaptchaEvents.length
192
- },
193
- msg: "Mongo Saved Puzzle Events"
194
- }));
195
- }
186
+ await this.upsertChallengeRecords("puzzlecaptcha", puzzleCaptchaEvents, "Mongo Saved Puzzle Events");
187
+ await this.upsertChallengeRecords("audiocaptcha", audioCaptchaEvents, "Mongo Saved Audio Events");
188
+ await this.upsertChallengeRecords("iconordercaptcha", iconOrderCaptchaEvents, "Mongo Saved Icon Order Events");
196
189
  await this.close();
197
190
  }
191
+ async upsertChallengeRecords(table, records, msg) {
192
+ if (!records.length) return;
193
+ const result = await this.tables[table].bulkWrite(records.map((doc) => {
194
+ const { _id, ...safeDoc } = doc;
195
+ const normalised = CaptchaDatabase.normaliseDocCompositeIps(safeDoc);
196
+ return { updateOne: {
197
+ filter: { challenge: normalised.challenge },
198
+ update: { $set: normalised },
199
+ upsert: true
200
+ } };
201
+ }));
202
+ logger.info(() => ({
203
+ data: {
204
+ upsertedCount: result.upsertedCount,
205
+ matchedCount: result.matchedCount,
206
+ modifiedCount: result.modifiedCount,
207
+ totalProcessed: records.length
208
+ },
209
+ msg
210
+ }));
211
+ }
198
212
  async getCaptchas(filter = {}, limit = 100) {
199
213
  await this.connect();
200
214
  try {
201
215
  return {
202
216
  userCommitmentRecords: await this.tables.commitment.find(filter).limit(limit).lean(),
203
217
  powCaptchaRecords: await this.tables.powcaptcha.find(filter).limit(limit).lean(),
204
- puzzleCaptchaRecords: await this.tables.puzzlecaptcha.find(filter).limit(limit).lean()
218
+ puzzleCaptchaRecords: await this.tables.puzzlecaptcha.find(filter).limit(limit).lean(),
219
+ audioCaptchaRecords: await this.tables.audiocaptcha.find(filter).limit(limit).lean(),
220
+ iconOrderCaptchaRecords: await this.tables.iconordercaptcha.find(filter).limit(limit).lean()
205
221
  };
206
222
  } catch (error) {
207
223
  throw new ProsopoDBError("DATABASE.QUERY_ERROR", {