@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.
- package/.turbo/turbo-build$colon$cjs.log +5 -8
- package/.turbo/turbo-build$colon$tsc.log +16 -19
- package/.turbo/turbo-build.log +6 -12
- package/CHANGELOG.md +52 -0
- package/LICENSE +201 -0
- package/dist/cjs/databases/captcha.cjs +38 -22
- package/dist/cjs/databases/centralDbStreamer.cjs +26 -8
- package/dist/cjs/databases/provider.cjs +243 -103
- package/dist/databases/captcha.d.ts +8 -3
- package/dist/databases/captcha.d.ts.map +1 -1
- package/dist/databases/captcha.js +39 -23
- package/dist/databases/captcha.js.map +1 -1
- package/dist/databases/centralDbStreamer.d.ts +7 -1
- package/dist/databases/centralDbStreamer.d.ts.map +1 -1
- package/dist/databases/centralDbStreamer.js +26 -8
- package/dist/databases/centralDbStreamer.js.map +1 -1
- package/dist/databases/provider.d.ts +26 -2
- package/dist/databases/provider.d.ts.map +1 -1
- package/dist/databases/provider.js +244 -104
- package/dist/databases/provider.js.map +1 -1
- package/dist/tests/integration/countCommitmentsByNormalisedEmail.integration.test.js +67 -0
- package/dist/tests/integration/countCommitmentsByNormalisedEmail.integration.test.js.map +1 -1
- package/dist/tests/integration/projectionContracts.integration.test.js +54 -0
- package/dist/tests/integration/projectionContracts.integration.test.js.map +1 -1
- package/dist/tests/integration/serverCheckClaim.integration.test.js +52 -0
- package/dist/tests/integration/serverCheckClaim.integration.test.js.map +1 -1
- package/dist/tests/integration/sessionRecordProjection.integration.test.js +22 -0
- package/dist/tests/integration/sessionRecordProjection.integration.test.js.map +1 -1
- 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
|
-
|
|
644
|
-
|
|
645
|
-
|
|
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
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
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.
|
|
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:
|
|
720
|
+
msg: `${collection.label} record added successfully`
|
|
675
721
|
}));
|
|
676
|
-
|
|
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:
|
|
734
|
+
msg: `Failed to add ${collection.label} record`
|
|
695
735
|
}));
|
|
696
736
|
throw err;
|
|
697
737
|
}
|
|
698
738
|
}
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
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
|
|
783
|
+
context: { failedFuncName },
|
|
707
784
|
logger: this.logger
|
|
708
785
|
});
|
|
709
786
|
try {
|
|
710
|
-
const
|
|
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:
|
|
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:
|
|
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:
|
|
829
|
+
msg: `Failed to retrieve ${collection.label} record`
|
|
759
830
|
}));
|
|
760
831
|
throw err;
|
|
761
832
|
}
|
|
762
833
|
}
|
|
763
834
|
/**
|
|
764
|
-
* @description
|
|
765
|
-
* @param {string} challenge The challenge string
|
|
766
|
-
* @
|
|
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
|
|
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)(
|
|
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.
|
|
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:
|
|
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:
|
|
930
|
+
msg: `${collection.label} record updated successfully`
|
|
812
931
|
}));
|
|
813
|
-
this.
|
|
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:
|
|
944
|
+
msg: `Failed to update ${collection.label} record`
|
|
832
945
|
}));
|
|
833
946
|
throw err;
|
|
834
947
|
}
|
|
835
948
|
}
|
|
836
|
-
|
|
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.
|
|
847
|
-
this.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
|
971
|
-
* Each collection carries the same partial index
|
|
972
|
-
* (`spamEmailCount_partial`) so
|
|
973
|
-
*
|
|
974
|
-
*
|
|
975
|
-
*
|
|
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
|
-
|
|
1124
|
+
return (await Promise.all([
|
|
986
1125
|
tables.commitment.countDocuments(filter),
|
|
987
1126
|
tables.powcaptcha.countDocuments(filter),
|
|
988
|
-
tables.puzzlecaptcha.countDocuments(filter)
|
|
989
|
-
|
|
990
|
-
|
|
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,
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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", {
|