@prosopo/database 3.4.5 → 3.5.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/CHANGELOG.md CHANGED
@@ -1,5 +1,113 @@
1
1
  # @prosopo/database
2
2
 
3
+ ## 3.5.0
4
+ ### Minor Changes
5
+
6
+ - bb5f41c: Context awareness
7
+
8
+ ### Patch Changes
9
+
10
+ - 55a64c6: stop refresh image to pow
11
+ - 8ce9205: Change engine requirements
12
+ - b6e98b2: Run npm audit
13
+ - 55a64c6: Persist sessions for user ip combinations
14
+ - Updated dependencies [8ce9205]
15
+ - Updated dependencies [15ae7cf]
16
+ - Updated dependencies [bb5f41c]
17
+ - Updated dependencies [55a64c6]
18
+ - Updated dependencies [8ce9205]
19
+ - Updated dependencies [df79c03]
20
+ - Updated dependencies [8f22479]
21
+ - Updated dependencies [b6e98b2]
22
+ - Updated dependencies [55a64c6]
23
+ - @prosopo/user-access-policy@3.5.28
24
+ - @prosopo/types@3.6.0
25
+ - @prosopo/types-database@4.0.0
26
+ - @prosopo/redis-client@1.0.7
27
+ - @prosopo/common@3.1.22
28
+ - @prosopo/locale@3.1.22
29
+ - @prosopo/config@3.1.22
30
+
31
+ ## 3.4.13
32
+ ### Patch Changes
33
+
34
+ - Updated dependencies [8f1773a]
35
+ - @prosopo/types@3.5.11
36
+ - @prosopo/types-database@3.3.13
37
+ - @prosopo/user-access-policy@3.5.27
38
+
39
+ ## 3.4.12
40
+ ### Patch Changes
41
+
42
+ - Updated dependencies [cb8ab85]
43
+ - @prosopo/types-database@3.3.12
44
+ - @prosopo/types@3.5.10
45
+ - @prosopo/user-access-policy@3.5.26
46
+
47
+ ## 3.4.11
48
+ ### Patch Changes
49
+
50
+ - 43907e8: Convert timestamp fields from numbers to Date objects throughout codebase
51
+ - b4639ec: Merge frictionless tokens into sessions
52
+ - Updated dependencies [43907e8]
53
+ - Updated dependencies [b4639ec]
54
+ - Updated dependencies [005ce66]
55
+ - Updated dependencies [7101036]
56
+ - @prosopo/types-database@3.3.11
57
+ - @prosopo/types@3.5.9
58
+ - @prosopo/user-access-policy@3.5.25
59
+
60
+ ## 3.4.10
61
+ ### Patch Changes
62
+
63
+ - Updated dependencies [b10a65f]
64
+ - Updated dependencies [e5c259d]
65
+ - Updated dependencies [6420187]
66
+ - @prosopo/types-database@3.3.10
67
+ - @prosopo/types@3.5.8
68
+ - @prosopo/user-access-policy@3.5.24
69
+
70
+ ## 3.4.9
71
+ ### Patch Changes
72
+
73
+ - b8185a4: feat/uap-rules-syncer
74
+ - Updated dependencies [c9d8fdf]
75
+ - Updated dependencies [b8185a4]
76
+ - Updated dependencies [3a027ef]
77
+ - Updated dependencies [3a027ef]
78
+ - @prosopo/user-access-policy@3.5.23
79
+ - @prosopo/common@3.1.21
80
+ - @prosopo/config@3.1.21
81
+ - @prosopo/types-database@3.3.9
82
+ - @prosopo/redis-client@1.0.6
83
+ - @prosopo/locale@3.1.21
84
+ - @prosopo/types@3.5.7
85
+
86
+ ## 3.4.8
87
+ ### Patch Changes
88
+
89
+ - Updated dependencies [5d11a81]
90
+ - @prosopo/types@3.5.6
91
+ - @prosopo/types-database@3.3.8
92
+ - @prosopo/user-access-policy@3.5.22
93
+
94
+ ## 3.4.7
95
+ ### Patch Changes
96
+
97
+ - Updated dependencies [494c5a8]
98
+ - @prosopo/types-database@3.3.7
99
+ - @prosopo/types@3.5.5
100
+ - @prosopo/user-access-policy@3.5.21
101
+
102
+ ## 3.4.6
103
+ ### Patch Changes
104
+
105
+ - Updated dependencies [08ff50f]
106
+ - Updated dependencies [08ff50f]
107
+ - @prosopo/types-database@3.3.6
108
+ - @prosopo/types@3.5.4
109
+ - @prosopo/user-access-policy@3.5.20
110
+
3
111
  ## 3.4.5
4
112
  ### Patch Changes
5
113
 
@@ -3,6 +3,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const common = require("@prosopo/common");
4
4
  const mongodb = require("mongodb");
5
5
  const mongoose = require("mongoose");
6
+ var _documentCurrentScript = typeof document !== "undefined" ? document.currentScript : null;
6
7
  mongoose.set("strictQuery", false);
7
8
  const DEFAULT_ENDPOINT = "mongodb://127.0.0.1:27017";
8
9
  class MongoDatabase {
@@ -19,7 +20,7 @@ class MongoDatabase {
19
20
  this._url = parsedUrl.toString();
20
21
  this.safeURL = this.url.replace(/\w+:\w+/, "<Credentials>");
21
22
  this.dbname = dbname || parsedUrl.pathname.replace("/", "");
22
- this.logger = logger || common.getLogger("info", module);
23
+ this.logger = logger || common.getLogger("info", typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("base/mongo.cjs", document.baseURI).href);
23
24
  }
24
25
  get url() {
25
26
  return this._url;
@@ -4,7 +4,8 @@ const common = require("@prosopo/common");
4
4
  const typesDatabase = require("@prosopo/types-database");
5
5
  require("../base/index.cjs");
6
6
  const mongo = require("../base/mongo.cjs");
7
- const logger = common.getLogger("info", module);
7
+ var _documentCurrentScript = typeof document !== "undefined" ? document.currentScript : null;
8
+ const logger = common.getLogger("info", typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("databases/captcha.cjs", document.baseURI).href);
8
9
  var TableNames = /* @__PURE__ */ ((TableNames2) => {
9
10
  TableNames2["frictionlessToken"] = "frictionlessToken";
10
11
  TableNames2["session"] = "session";
@@ -87,8 +88,8 @@ class CaptchaDatabase extends mongo.MongoDatabase {
87
88
  await this.connect();
88
89
  if (sessionEvents.length) {
89
90
  const result = await this.tables.session.bulkWrite(
90
- sessionEvents.map((document) => {
91
- const { _id, ...safeDoc } = document;
91
+ sessionEvents.map((document2) => {
92
+ const { _id, ...safeDoc } = document2;
92
93
  return {
93
94
  insertOne: {
94
95
  document: safeDoc
@@ -5,8 +5,9 @@ const common = require("@prosopo/common");
5
5
  const redisClient = require("@prosopo/redis-client");
6
6
  const types = require("@prosopo/types");
7
7
  const typesDatabase = require("@prosopo/types-database");
8
- const userAccessPolicy = require("@prosopo/user-access-policy");
8
+ const redis = require("@prosopo/user-access-policy/redis");
9
9
  const mongo = require("../base/mongo.cjs");
10
+ const TWENTY_FOUR_HOURS_IN_MS = 24 * 60 * 60 * 1e3;
10
11
  var TableNames = /* @__PURE__ */ ((TableNames2) => {
11
12
  TableNames2["captcha"] = "captcha";
12
13
  TableNames2["dataset"] = "dataset";
@@ -17,9 +18,9 @@ var TableNames = /* @__PURE__ */ ((TableNames2) => {
17
18
  TableNames2["scheduler"] = "scheduler";
18
19
  TableNames2["powcaptcha"] = "powcaptcha";
19
20
  TableNames2["client"] = "client";
20
- TableNames2["frictionlessToken"] = "frictionlessToken";
21
21
  TableNames2["session"] = "session";
22
22
  TableNames2["detector"] = "detector";
23
+ TableNames2["clientEntropy"] = "clientEntropy";
23
24
  return TableNames2;
24
25
  })(TableNames || {});
25
26
  const PROVIDER_TABLES = [
@@ -68,11 +69,6 @@ const PROVIDER_TABLES = [
68
69
  modelName: "Client",
69
70
  schema: typesDatabase.ClientRecordSchema
70
71
  },
71
- {
72
- collectionName: "frictionlessToken",
73
- modelName: "FrictionlessToken",
74
- schema: typesDatabase.FrictionlessTokenRecordSchema
75
- },
76
72
  {
77
73
  collectionName: "session",
78
74
  modelName: "Session",
@@ -82,6 +78,11 @@ const PROVIDER_TABLES = [
82
78
  collectionName: "detector",
83
79
  modelName: "Detector",
84
80
  schema: typesDatabase.DetectorRecordSchema
81
+ },
82
+ {
83
+ collectionName: "clientEntropy",
84
+ modelName: "ClientEntropy",
85
+ schema: typesDatabase.ClientEntropyRecordSchema
85
86
  }
86
87
  ];
87
88
  class ProviderDatabase extends mongo.MongoDatabase {
@@ -114,12 +115,12 @@ class ProviderDatabase extends mongo.MongoDatabase {
114
115
  this.redisAccessRulesConnection = redisClient.setupRedisIndex(
115
116
  this.redisConnection,
116
117
  {
117
- ...userAccessPolicy.redisAccessRulesIndex,
118
- name: this.options.redis?.indexName || userAccessPolicy.redisAccessRulesIndex.name
118
+ ...redis.accessRulesRedisIndex,
119
+ name: this.options.redis?.indexName || redis.accessRulesRedisIndex.name
119
120
  },
120
121
  this.logger
121
122
  );
122
- this.userAccessRulesStorage = userAccessPolicy.createRedisAccessRulesStorage(
123
+ this.userAccessRulesStorage = redis.createRedisAccessRulesStorage(
123
124
  this.redisAccessRulesConnection,
124
125
  this.logger
125
126
  );
@@ -448,7 +449,7 @@ class ProviderDatabase extends mongo.MongoDatabase {
448
449
  async storeUserImageCaptchaSolution(captchas, commit) {
449
450
  const commitmentRecord = typesDatabase.UserCommitmentSchema.parse({
450
451
  ...commit,
451
- lastUpdatedTimestamp: Date.now()
452
+ lastUpdatedTimestamp: /* @__PURE__ */ new Date()
452
453
  });
453
454
  if (captchas.length) {
454
455
  const filter = {
@@ -488,18 +489,20 @@ class ProviderDatabase extends mongo.MongoDatabase {
488
489
  * @param ipAddress
489
490
  * @param headers
490
491
  * @param ja4
491
- * @param frictionlessTokenId
492
+ * @param sessionId
492
493
  * @param serverChecked
493
494
  * @param userSubmitted
494
495
  * @param storedStatus
495
496
  * @param userSignature
496
497
  * @returns {Promise<void>} A promise that resolves when the record is added.
497
498
  */
498
- async storePowCaptchaRecord(challenge, components, difficulty, providerSignature, ipAddress, headers, ja4, frictionlessTokenId, serverChecked = false, userSubmitted = false, storedStatus = types.StoredStatusNames.notStored, userSignature) {
499
+ async storePowCaptchaRecord(challenge, components, difficulty, providerSignature, ipAddress, headers, ja4, sessionId, serverChecked = false, userSubmitted = false, storedStatus = types.StoredStatusNames.notStored, userSignature) {
499
500
  const tables = this.getTables();
500
501
  const powCaptchaRecord = {
501
502
  challenge,
502
- ...components,
503
+ userAccount: components.userAccount,
504
+ dappAccount: components.dappAccount,
505
+ requestedAtTimestamp: new Date(components.requestedAtTimestamp),
503
506
  ipAddress,
504
507
  headers,
505
508
  ja4,
@@ -509,8 +512,8 @@ class ProviderDatabase extends mongo.MongoDatabase {
509
512
  difficulty,
510
513
  providerSignature,
511
514
  userSignature,
512
- lastUpdatedTimestamp: Date.now(),
513
- frictionlessTokenId
515
+ lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
516
+ sessionId
514
517
  };
515
518
  try {
516
519
  await tables.powcaptcha.create(powCaptchaRecord);
@@ -591,7 +594,7 @@ class ProviderDatabase extends mongo.MongoDatabase {
591
594
  */
592
595
  async updatePowCaptchaRecordResult(challenge, result, serverChecked = false, userSubmitted = false, userSignature) {
593
596
  const tables = this.getTables();
594
- const timestamp = Date.now();
597
+ const timestamp = /* @__PURE__ */ new Date();
595
598
  const update = {
596
599
  result,
597
600
  serverChecked,
@@ -692,7 +695,7 @@ class ProviderDatabase extends mongo.MongoDatabase {
692
695
  */
693
696
  async markDappUserCommitmentsStored(commitmentIds) {
694
697
  const updateDoc = {
695
- storedAtTimestamp: Date.now()
698
+ storedAtTimestamp: /* @__PURE__ */ new Date()
696
699
  };
697
700
  await this.tables?.commitment.updateMany(
698
701
  { id: { $in: commitmentIds } },
@@ -705,7 +708,7 @@ class ProviderDatabase extends mongo.MongoDatabase {
705
708
  async markDappUserCommitmentsChecked(commitmentIds) {
706
709
  const updateDoc = {
707
710
  [types.StoredStatusNames.serverChecked]: true,
708
- lastUpdatedTimestamp: Date.now()
711
+ lastUpdatedTimestamp: /* @__PURE__ */ new Date()
709
712
  };
710
713
  await this.tables?.commitment.updateMany(
711
714
  { id: { $in: commitmentIds } },
@@ -769,7 +772,7 @@ class ProviderDatabase extends mongo.MongoDatabase {
769
772
  */
770
773
  async markDappUserPoWCommitmentsStored(challenges) {
771
774
  const updateDoc = {
772
- storedAtTimestamp: Date.now()
775
+ storedAtTimestamp: /* @__PURE__ */ new Date()
773
776
  };
774
777
  await this.tables?.powcaptcha.updateMany(
775
778
  { challenge: { $in: challenges } },
@@ -782,7 +785,7 @@ class ProviderDatabase extends mongo.MongoDatabase {
782
785
  async markDappUserPoWCommitmentsChecked(challenges) {
783
786
  const updateDoc = {
784
787
  [types.StoredStatusNames.serverChecked]: true,
785
- lastUpdatedTimestamp: Date.now()
788
+ lastUpdatedTimestamp: /* @__PURE__ */ new Date()
786
789
  };
787
790
  await this.tables?.powcaptcha.updateMany(
788
791
  { challenge: { $in: challenges } },
@@ -792,46 +795,6 @@ class ProviderDatabase extends mongo.MongoDatabase {
792
795
  { upsert: false }
793
796
  );
794
797
  }
795
- /**
796
- * Store a new frictionless token record
797
- */
798
- async storeFrictionlessTokenRecord(tokenRecord) {
799
- const doc = await this.tables.frictionlessToken.create(
800
- tokenRecord
801
- );
802
- return doc._id;
803
- }
804
- /** Update a frictionless token record */
805
- async updateFrictionlessTokenRecord(tokenId, updates) {
806
- const filter = { _id: tokenId };
807
- await this.tables.frictionlessToken.updateOne(filter, updates);
808
- }
809
- /** Get a frictionless token record */
810
- async getFrictionlessTokenRecordByTokenId(tokenId) {
811
- const filter = { _id: tokenId };
812
- const doc = await this.tables.frictionlessToken.findOne(
813
- filter
814
- );
815
- return doc ? doc : void 0;
816
- }
817
- /** Get many frictionless token records */
818
- async getFrictionlessTokenRecordsByTokenIds(tokenId) {
819
- const filter = {
820
- _id: { $in: tokenId }
821
- };
822
- return this.tables.frictionlessToken.find(filter).lean();
823
- }
824
- /**
825
- * Check if a frictionless token record exists.
826
- * Used to ensure that a token is not used more than once.
827
- */
828
- async getFrictionlessTokenRecordByToken(token) {
829
- const filter = { token };
830
- const record = await this.tables.frictionlessToken.findOne(
831
- filter
832
- );
833
- return record || void 0;
834
- }
835
798
  /**
836
799
  * Store a new session record
837
800
  */
@@ -848,6 +811,23 @@ class ProviderDatabase extends mongo.MongoDatabase {
848
811
  });
849
812
  }
850
813
  }
814
+ /**
815
+ * Get a session record by sessionId
816
+ */
817
+ async getSessionRecordBySessionId(sessionId) {
818
+ const filter = { sessionId };
819
+ const doc = await this.tables.session.findOne(filter).lean();
820
+ return doc || void 0;
821
+ }
822
+ /**
823
+ * Get a session record by token
824
+ * Used to ensure that a token is not used more than once.
825
+ */
826
+ async getSessionRecordByToken(token) {
827
+ const filter = { token };
828
+ const record = await this.tables.session.findOne(filter).lean();
829
+ return record || void 0;
830
+ }
851
831
  /**
852
832
  * Check if a session exists and mark it as removed
853
833
  * @returns The session record if it existed, undefined otherwise
@@ -863,7 +843,7 @@ class ProviderDatabase extends mongo.MongoDatabase {
863
843
  try {
864
844
  const session = await this.tables.session.findOneAndUpdate(filter, {
865
845
  deleted: true,
866
- lastUpdatedTimestamp: Date.now()
846
+ lastUpdatedTimestamp: /* @__PURE__ */ new Date()
867
847
  }).lean();
868
848
  return session || void 0;
869
849
  } catch (err) {
@@ -873,6 +853,29 @@ class ProviderDatabase extends mongo.MongoDatabase {
873
853
  });
874
854
  }
875
855
  }
856
+ /**
857
+ * Get an active session by user IP hash
858
+ * @param userSitekeyIpHash The hash of user, IP and sitekey combination
859
+ * @returns The session record if it exists and is not deleted, undefined otherwise
860
+ */
861
+ async getSessionByuserSitekeyIpHash(userSitekeyIpHash) {
862
+ this.logger.debug(() => ({
863
+ data: { action: "getting session by user IP hash", userSitekeyIpHash }
864
+ }));
865
+ const filter = {
866
+ userSitekeyIpHash,
867
+ deleted: { $exists: false }
868
+ };
869
+ try {
870
+ const session = await this.tables.session.findOne(filter).lean();
871
+ return session || void 0;
872
+ } catch (err) {
873
+ throw new common.ProsopoDBError("DATABASE.SESSION_GET_FAILED", {
874
+ context: { error: err, userSitekeyIpHash },
875
+ logger: this.logger
876
+ });
877
+ }
878
+ }
876
879
  /** Get unstored session records
877
880
  * @description Get session records that have not been stored yet
878
881
  * @param limit
@@ -928,21 +931,10 @@ class ProviderDatabase extends mongo.MongoDatabase {
928
931
  { upsert: false }
929
932
  );
930
933
  }
931
- /** Mark a list of token records as stored */
932
- async markFrictionlessTokenRecordsStored(tokenIds) {
933
- const updateDoc = {
934
- storedAtTimestamp: /* @__PURE__ */ new Date()
935
- };
936
- await this.tables?.frictionlessToken.updateMany(
937
- { _id: { $in: tokenIds } },
938
- { $set: updateDoc },
939
- { upsert: false }
940
- );
941
- }
942
934
  /**
943
935
  * @description Store a Dapp User's pending record
944
936
  */
945
- async storePendingImageCommitment(userAccount, requestHash, salt, deadlineTimestamp, requestedAtTimestamp, ipAddress, threshold, frictionlessTokenId) {
937
+ async storePendingImageCommitment(userAccount, requestHash, salt, deadlineTimestamp, requestedAtTimestamp, ipAddress, threshold, sessionId) {
946
938
  if (!is.isHex(requestHash)) {
947
939
  throw new common.ProsopoDBError("DATABASE.INVALID_HASH", {
948
940
  context: {
@@ -959,7 +951,7 @@ class ProviderDatabase extends mongo.MongoDatabase {
959
951
  deadlineTimestamp,
960
952
  requestedAtTimestamp: new Date(requestedAtTimestamp),
961
953
  ipAddress,
962
- frictionlessTokenId,
954
+ sessionId,
963
955
  threshold
964
956
  };
965
957
  await this.tables?.pending.updateOne(
@@ -1162,7 +1154,7 @@ class ProviderDatabase extends mongo.MongoDatabase {
1162
1154
  const result = { status: types.CaptchaStatus.approved };
1163
1155
  const updateDoc = {
1164
1156
  result,
1165
- lastUpdatedTimestamp: Date.now(),
1157
+ lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
1166
1158
  ...coords ? { coords } : {}
1167
1159
  };
1168
1160
  const filter = { id: commitmentId };
@@ -1183,7 +1175,7 @@ class ProviderDatabase extends mongo.MongoDatabase {
1183
1175
  try {
1184
1176
  const updateDoc = {
1185
1177
  result: { status: types.CaptchaStatus.disapproved, reason },
1186
- lastUpdatedTimestamp: Date.now(),
1178
+ lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
1187
1179
  ...coords ? { coords } : {}
1188
1180
  };
1189
1181
  const filter = { id: commitmentId };
@@ -1258,7 +1250,7 @@ class ProviderDatabase extends mongo.MongoDatabase {
1258
1250
  * @description Create the status of a scheduled task
1259
1251
  */
1260
1252
  async createScheduledTaskStatus(taskName, status) {
1261
- const now = (/* @__PURE__ */ new Date()).getTime();
1253
+ const now = /* @__PURE__ */ new Date();
1262
1254
  const doc = typesDatabase.ScheduledTaskSchema.parse({
1263
1255
  processName: taskName,
1264
1256
  datetime: now,
@@ -1273,7 +1265,7 @@ class ProviderDatabase extends mongo.MongoDatabase {
1273
1265
  async updateScheduledTaskStatus(taskId, status, result) {
1274
1266
  const update = {
1275
1267
  status,
1276
- updated: (/* @__PURE__ */ new Date()).getTime(),
1268
+ updated: /* @__PURE__ */ new Date(),
1277
1269
  ...result && { result }
1278
1270
  };
1279
1271
  const filter = { _id: taskId };
@@ -1319,6 +1311,13 @@ class ProviderDatabase extends mongo.MongoDatabase {
1319
1311
  });
1320
1312
  await this.tables?.client.bulkWrite(ops);
1321
1313
  }
1314
+ /**
1315
+ * @description Get all client records
1316
+ */
1317
+ async getAllClientRecords() {
1318
+ const docs = await this.tables?.client.find().lean();
1319
+ return docs || [];
1320
+ }
1322
1321
  /**
1323
1322
  * @description Get a client record
1324
1323
  */
@@ -1362,5 +1361,70 @@ class ProviderDatabase extends mongo.MongoDatabase {
1362
1361
  ).sort({ createdAt: -1 }).lean();
1363
1362
  return (keyRecords || []).map((record) => record.detectorKey);
1364
1363
  }
1364
+ /**
1365
+ * @description set client entropy
1366
+ */
1367
+ async setClientEntropy(account, entropy) {
1368
+ const filter = { account };
1369
+ await this.tables?.clientEntropy.updateOne(
1370
+ filter,
1371
+ { $set: { entropy } },
1372
+ { upsert: true }
1373
+ );
1374
+ }
1375
+ /**
1376
+ * @description get client entropy
1377
+ */
1378
+ async getClientEntropy(account) {
1379
+ const filter = { account };
1380
+ const doc = await this.tables?.clientEntropy.findOne(filter).lean();
1381
+ return doc ? doc.entropy : void 0;
1382
+ }
1383
+ /** Sample captcha records from the database */
1384
+ async sampleEntropy(sampleSize, siteKey) {
1385
+ const size = sampleSize ? Math.abs(Math.trunc(sampleSize)) : 1;
1386
+ const max = 1e4;
1387
+ if (size > max) {
1388
+ throw new common.ProsopoDBError("DATABASE.CAPTCHA_SAMPLE_SIZE_EXCEEDED", {
1389
+ context: {
1390
+ failedFuncName: this.sampleEntropy.name,
1391
+ sampleSize
1392
+ }
1393
+ });
1394
+ }
1395
+ const cursor = this.tables?.powcaptcha.aggregate([
1396
+ {
1397
+ $match: {
1398
+ dappAccount: siteKey,
1399
+ requestedAtTimestamp: {
1400
+ $gt: new Date((/* @__PURE__ */ new Date()).getTime() - TWENTY_FOUR_HOURS_IN_MS)
1401
+ }
1402
+ }
1403
+ },
1404
+ { $limit: max },
1405
+ { $sample: { size } },
1406
+ {
1407
+ $project: {
1408
+ _id: 0,
1409
+ frictionlessTokenId: 1
1410
+ }
1411
+ }
1412
+ ]);
1413
+ const docs = await cursor;
1414
+ if (docs?.length === 0) {
1415
+ return [];
1416
+ }
1417
+ return (await Promise.all(
1418
+ docs.map(async (doc) => {
1419
+ if (doc.frictionlessTokenId) {
1420
+ const tokenRecord = await this.getSessionRecordByToken(
1421
+ doc.frictionlessTokenId
1422
+ );
1423
+ return tokenRecord?.decryptedHeadHash;
1424
+ }
1425
+ return void 0;
1426
+ })
1427
+ )).filter((headHash) => headHash !== void 0);
1428
+ }
1365
1429
  }
1366
1430
  exports.ProviderDatabase = ProviderDatabase;
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const makeDir = require("make-dir");
3
4
  require("./base/index.cjs");
4
5
  const index = require("./databases/index.cjs");
5
6
  const mongo = require("./base/mongo.cjs");
@@ -7,6 +8,7 @@ const mongoMemory = require("./base/mongoMemory.cjs");
7
8
  const provider = require("./databases/provider.cjs");
8
9
  const captcha = require("./databases/captcha.cjs");
9
10
  const client = require("./databases/client.cjs");
11
+ console.debug(makeDir);
10
12
  exports.Databases = index.Databases;
11
13
  exports.MongoDatabase = mongo.MongoDatabase;
12
14
  exports.MongoMemoryDatabase = mongoMemory.MongoMemoryDatabase;
@@ -2,9 +2,10 @@ import { isHex } from "@polkadot/util/is";
2
2
  import { ProsopoDBError } from "@prosopo/common";
3
3
  import { connectToRedis, setupRedisIndex } from "@prosopo/redis-client";
4
4
  import { DatasetWithIdsAndTreeSchema, StoredStatusNames, CaptchaStatus, ApiParams, CaptchaStates } from "@prosopo/types";
5
- import { CaptchaRecordSchema, PoWCaptchaRecordSchema, DatasetRecordSchema, SolutionRecordSchema, UserCommitmentRecordSchema, UserSolutionRecordSchema, PendingRecordSchema, ScheduledTaskRecordSchema, ClientRecordSchema, FrictionlessTokenRecordSchema, SessionRecordSchema, DetectorRecordSchema, UserCommitmentSchema, ScheduledTaskSchema } from "@prosopo/types-database";
6
- import { redisAccessRulesIndex, createRedisAccessRulesStorage } from "@prosopo/user-access-policy";
5
+ import { CaptchaRecordSchema, PoWCaptchaRecordSchema, DatasetRecordSchema, SolutionRecordSchema, UserCommitmentRecordSchema, UserSolutionRecordSchema, PendingRecordSchema, ScheduledTaskRecordSchema, ClientRecordSchema, SessionRecordSchema, DetectorRecordSchema, ClientEntropyRecordSchema, UserCommitmentSchema, ScheduledTaskSchema } from "@prosopo/types-database";
6
+ import { accessRulesRedisIndex, createRedisAccessRulesStorage } from "@prosopo/user-access-policy/redis";
7
7
  import { MongoDatabase } from "../base/mongo.js";
8
+ const TWENTY_FOUR_HOURS_IN_MS = 24 * 60 * 60 * 1e3;
8
9
  var TableNames = /* @__PURE__ */ ((TableNames2) => {
9
10
  TableNames2["captcha"] = "captcha";
10
11
  TableNames2["dataset"] = "dataset";
@@ -15,9 +16,9 @@ var TableNames = /* @__PURE__ */ ((TableNames2) => {
15
16
  TableNames2["scheduler"] = "scheduler";
16
17
  TableNames2["powcaptcha"] = "powcaptcha";
17
18
  TableNames2["client"] = "client";
18
- TableNames2["frictionlessToken"] = "frictionlessToken";
19
19
  TableNames2["session"] = "session";
20
20
  TableNames2["detector"] = "detector";
21
+ TableNames2["clientEntropy"] = "clientEntropy";
21
22
  return TableNames2;
22
23
  })(TableNames || {});
23
24
  const PROVIDER_TABLES = [
@@ -66,11 +67,6 @@ const PROVIDER_TABLES = [
66
67
  modelName: "Client",
67
68
  schema: ClientRecordSchema
68
69
  },
69
- {
70
- collectionName: "frictionlessToken",
71
- modelName: "FrictionlessToken",
72
- schema: FrictionlessTokenRecordSchema
73
- },
74
70
  {
75
71
  collectionName: "session",
76
72
  modelName: "Session",
@@ -80,6 +76,11 @@ const PROVIDER_TABLES = [
80
76
  collectionName: "detector",
81
77
  modelName: "Detector",
82
78
  schema: DetectorRecordSchema
79
+ },
80
+ {
81
+ collectionName: "clientEntropy",
82
+ modelName: "ClientEntropy",
83
+ schema: ClientEntropyRecordSchema
83
84
  }
84
85
  ];
85
86
  class ProviderDatabase extends MongoDatabase {
@@ -112,8 +113,8 @@ class ProviderDatabase extends MongoDatabase {
112
113
  this.redisAccessRulesConnection = setupRedisIndex(
113
114
  this.redisConnection,
114
115
  {
115
- ...redisAccessRulesIndex,
116
- name: this.options.redis?.indexName || redisAccessRulesIndex.name
116
+ ...accessRulesRedisIndex,
117
+ name: this.options.redis?.indexName || accessRulesRedisIndex.name
117
118
  },
118
119
  this.logger
119
120
  );
@@ -446,7 +447,7 @@ class ProviderDatabase extends MongoDatabase {
446
447
  async storeUserImageCaptchaSolution(captchas, commit) {
447
448
  const commitmentRecord = UserCommitmentSchema.parse({
448
449
  ...commit,
449
- lastUpdatedTimestamp: Date.now()
450
+ lastUpdatedTimestamp: /* @__PURE__ */ new Date()
450
451
  });
451
452
  if (captchas.length) {
452
453
  const filter = {
@@ -486,18 +487,20 @@ class ProviderDatabase extends MongoDatabase {
486
487
  * @param ipAddress
487
488
  * @param headers
488
489
  * @param ja4
489
- * @param frictionlessTokenId
490
+ * @param sessionId
490
491
  * @param serverChecked
491
492
  * @param userSubmitted
492
493
  * @param storedStatus
493
494
  * @param userSignature
494
495
  * @returns {Promise<void>} A promise that resolves when the record is added.
495
496
  */
496
- async storePowCaptchaRecord(challenge, components, difficulty, providerSignature, ipAddress, headers, ja4, frictionlessTokenId, serverChecked = false, userSubmitted = false, storedStatus = StoredStatusNames.notStored, userSignature) {
497
+ async storePowCaptchaRecord(challenge, components, difficulty, providerSignature, ipAddress, headers, ja4, sessionId, serverChecked = false, userSubmitted = false, storedStatus = StoredStatusNames.notStored, userSignature) {
497
498
  const tables = this.getTables();
498
499
  const powCaptchaRecord = {
499
500
  challenge,
500
- ...components,
501
+ userAccount: components.userAccount,
502
+ dappAccount: components.dappAccount,
503
+ requestedAtTimestamp: new Date(components.requestedAtTimestamp),
501
504
  ipAddress,
502
505
  headers,
503
506
  ja4,
@@ -507,8 +510,8 @@ class ProviderDatabase extends MongoDatabase {
507
510
  difficulty,
508
511
  providerSignature,
509
512
  userSignature,
510
- lastUpdatedTimestamp: Date.now(),
511
- frictionlessTokenId
513
+ lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
514
+ sessionId
512
515
  };
513
516
  try {
514
517
  await tables.powcaptcha.create(powCaptchaRecord);
@@ -589,7 +592,7 @@ class ProviderDatabase extends MongoDatabase {
589
592
  */
590
593
  async updatePowCaptchaRecordResult(challenge, result, serverChecked = false, userSubmitted = false, userSignature) {
591
594
  const tables = this.getTables();
592
- const timestamp = Date.now();
595
+ const timestamp = /* @__PURE__ */ new Date();
593
596
  const update = {
594
597
  result,
595
598
  serverChecked,
@@ -690,7 +693,7 @@ class ProviderDatabase extends MongoDatabase {
690
693
  */
691
694
  async markDappUserCommitmentsStored(commitmentIds) {
692
695
  const updateDoc = {
693
- storedAtTimestamp: Date.now()
696
+ storedAtTimestamp: /* @__PURE__ */ new Date()
694
697
  };
695
698
  await this.tables?.commitment.updateMany(
696
699
  { id: { $in: commitmentIds } },
@@ -703,7 +706,7 @@ class ProviderDatabase extends MongoDatabase {
703
706
  async markDappUserCommitmentsChecked(commitmentIds) {
704
707
  const updateDoc = {
705
708
  [StoredStatusNames.serverChecked]: true,
706
- lastUpdatedTimestamp: Date.now()
709
+ lastUpdatedTimestamp: /* @__PURE__ */ new Date()
707
710
  };
708
711
  await this.tables?.commitment.updateMany(
709
712
  { id: { $in: commitmentIds } },
@@ -767,7 +770,7 @@ class ProviderDatabase extends MongoDatabase {
767
770
  */
768
771
  async markDappUserPoWCommitmentsStored(challenges) {
769
772
  const updateDoc = {
770
- storedAtTimestamp: Date.now()
773
+ storedAtTimestamp: /* @__PURE__ */ new Date()
771
774
  };
772
775
  await this.tables?.powcaptcha.updateMany(
773
776
  { challenge: { $in: challenges } },
@@ -780,7 +783,7 @@ class ProviderDatabase extends MongoDatabase {
780
783
  async markDappUserPoWCommitmentsChecked(challenges) {
781
784
  const updateDoc = {
782
785
  [StoredStatusNames.serverChecked]: true,
783
- lastUpdatedTimestamp: Date.now()
786
+ lastUpdatedTimestamp: /* @__PURE__ */ new Date()
784
787
  };
785
788
  await this.tables?.powcaptcha.updateMany(
786
789
  { challenge: { $in: challenges } },
@@ -790,46 +793,6 @@ class ProviderDatabase extends MongoDatabase {
790
793
  { upsert: false }
791
794
  );
792
795
  }
793
- /**
794
- * Store a new frictionless token record
795
- */
796
- async storeFrictionlessTokenRecord(tokenRecord) {
797
- const doc = await this.tables.frictionlessToken.create(
798
- tokenRecord
799
- );
800
- return doc._id;
801
- }
802
- /** Update a frictionless token record */
803
- async updateFrictionlessTokenRecord(tokenId, updates) {
804
- const filter = { _id: tokenId };
805
- await this.tables.frictionlessToken.updateOne(filter, updates);
806
- }
807
- /** Get a frictionless token record */
808
- async getFrictionlessTokenRecordByTokenId(tokenId) {
809
- const filter = { _id: tokenId };
810
- const doc = await this.tables.frictionlessToken.findOne(
811
- filter
812
- );
813
- return doc ? doc : void 0;
814
- }
815
- /** Get many frictionless token records */
816
- async getFrictionlessTokenRecordsByTokenIds(tokenId) {
817
- const filter = {
818
- _id: { $in: tokenId }
819
- };
820
- return this.tables.frictionlessToken.find(filter).lean();
821
- }
822
- /**
823
- * Check if a frictionless token record exists.
824
- * Used to ensure that a token is not used more than once.
825
- */
826
- async getFrictionlessTokenRecordByToken(token) {
827
- const filter = { token };
828
- const record = await this.tables.frictionlessToken.findOne(
829
- filter
830
- );
831
- return record || void 0;
832
- }
833
796
  /**
834
797
  * Store a new session record
835
798
  */
@@ -846,6 +809,23 @@ class ProviderDatabase extends MongoDatabase {
846
809
  });
847
810
  }
848
811
  }
812
+ /**
813
+ * Get a session record by sessionId
814
+ */
815
+ async getSessionRecordBySessionId(sessionId) {
816
+ const filter = { sessionId };
817
+ const doc = await this.tables.session.findOne(filter).lean();
818
+ return doc || void 0;
819
+ }
820
+ /**
821
+ * Get a session record by token
822
+ * Used to ensure that a token is not used more than once.
823
+ */
824
+ async getSessionRecordByToken(token) {
825
+ const filter = { token };
826
+ const record = await this.tables.session.findOne(filter).lean();
827
+ return record || void 0;
828
+ }
849
829
  /**
850
830
  * Check if a session exists and mark it as removed
851
831
  * @returns The session record if it existed, undefined otherwise
@@ -861,7 +841,7 @@ class ProviderDatabase extends MongoDatabase {
861
841
  try {
862
842
  const session = await this.tables.session.findOneAndUpdate(filter, {
863
843
  deleted: true,
864
- lastUpdatedTimestamp: Date.now()
844
+ lastUpdatedTimestamp: /* @__PURE__ */ new Date()
865
845
  }).lean();
866
846
  return session || void 0;
867
847
  } catch (err) {
@@ -871,6 +851,29 @@ class ProviderDatabase extends MongoDatabase {
871
851
  });
872
852
  }
873
853
  }
854
+ /**
855
+ * Get an active session by user IP hash
856
+ * @param userSitekeyIpHash The hash of user, IP and sitekey combination
857
+ * @returns The session record if it exists and is not deleted, undefined otherwise
858
+ */
859
+ async getSessionByuserSitekeyIpHash(userSitekeyIpHash) {
860
+ this.logger.debug(() => ({
861
+ data: { action: "getting session by user IP hash", userSitekeyIpHash }
862
+ }));
863
+ const filter = {
864
+ userSitekeyIpHash,
865
+ deleted: { $exists: false }
866
+ };
867
+ try {
868
+ const session = await this.tables.session.findOne(filter).lean();
869
+ return session || void 0;
870
+ } catch (err) {
871
+ throw new ProsopoDBError("DATABASE.SESSION_GET_FAILED", {
872
+ context: { error: err, userSitekeyIpHash },
873
+ logger: this.logger
874
+ });
875
+ }
876
+ }
874
877
  /** Get unstored session records
875
878
  * @description Get session records that have not been stored yet
876
879
  * @param limit
@@ -926,21 +929,10 @@ class ProviderDatabase extends MongoDatabase {
926
929
  { upsert: false }
927
930
  );
928
931
  }
929
- /** Mark a list of token records as stored */
930
- async markFrictionlessTokenRecordsStored(tokenIds) {
931
- const updateDoc = {
932
- storedAtTimestamp: /* @__PURE__ */ new Date()
933
- };
934
- await this.tables?.frictionlessToken.updateMany(
935
- { _id: { $in: tokenIds } },
936
- { $set: updateDoc },
937
- { upsert: false }
938
- );
939
- }
940
932
  /**
941
933
  * @description Store a Dapp User's pending record
942
934
  */
943
- async storePendingImageCommitment(userAccount, requestHash, salt, deadlineTimestamp, requestedAtTimestamp, ipAddress, threshold, frictionlessTokenId) {
935
+ async storePendingImageCommitment(userAccount, requestHash, salt, deadlineTimestamp, requestedAtTimestamp, ipAddress, threshold, sessionId) {
944
936
  if (!isHex(requestHash)) {
945
937
  throw new ProsopoDBError("DATABASE.INVALID_HASH", {
946
938
  context: {
@@ -957,7 +949,7 @@ class ProviderDatabase extends MongoDatabase {
957
949
  deadlineTimestamp,
958
950
  requestedAtTimestamp: new Date(requestedAtTimestamp),
959
951
  ipAddress,
960
- frictionlessTokenId,
952
+ sessionId,
961
953
  threshold
962
954
  };
963
955
  await this.tables?.pending.updateOne(
@@ -1160,7 +1152,7 @@ class ProviderDatabase extends MongoDatabase {
1160
1152
  const result = { status: CaptchaStatus.approved };
1161
1153
  const updateDoc = {
1162
1154
  result,
1163
- lastUpdatedTimestamp: Date.now(),
1155
+ lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
1164
1156
  ...coords ? { coords } : {}
1165
1157
  };
1166
1158
  const filter = { id: commitmentId };
@@ -1181,7 +1173,7 @@ class ProviderDatabase extends MongoDatabase {
1181
1173
  try {
1182
1174
  const updateDoc = {
1183
1175
  result: { status: CaptchaStatus.disapproved, reason },
1184
- lastUpdatedTimestamp: Date.now(),
1176
+ lastUpdatedTimestamp: /* @__PURE__ */ new Date(),
1185
1177
  ...coords ? { coords } : {}
1186
1178
  };
1187
1179
  const filter = { id: commitmentId };
@@ -1256,7 +1248,7 @@ class ProviderDatabase extends MongoDatabase {
1256
1248
  * @description Create the status of a scheduled task
1257
1249
  */
1258
1250
  async createScheduledTaskStatus(taskName, status) {
1259
- const now = (/* @__PURE__ */ new Date()).getTime();
1251
+ const now = /* @__PURE__ */ new Date();
1260
1252
  const doc = ScheduledTaskSchema.parse({
1261
1253
  processName: taskName,
1262
1254
  datetime: now,
@@ -1271,7 +1263,7 @@ class ProviderDatabase extends MongoDatabase {
1271
1263
  async updateScheduledTaskStatus(taskId, status, result) {
1272
1264
  const update = {
1273
1265
  status,
1274
- updated: (/* @__PURE__ */ new Date()).getTime(),
1266
+ updated: /* @__PURE__ */ new Date(),
1275
1267
  ...result && { result }
1276
1268
  };
1277
1269
  const filter = { _id: taskId };
@@ -1317,6 +1309,13 @@ class ProviderDatabase extends MongoDatabase {
1317
1309
  });
1318
1310
  await this.tables?.client.bulkWrite(ops);
1319
1311
  }
1312
+ /**
1313
+ * @description Get all client records
1314
+ */
1315
+ async getAllClientRecords() {
1316
+ const docs = await this.tables?.client.find().lean();
1317
+ return docs || [];
1318
+ }
1320
1319
  /**
1321
1320
  * @description Get a client record
1322
1321
  */
@@ -1360,6 +1359,71 @@ class ProviderDatabase extends MongoDatabase {
1360
1359
  ).sort({ createdAt: -1 }).lean();
1361
1360
  return (keyRecords || []).map((record) => record.detectorKey);
1362
1361
  }
1362
+ /**
1363
+ * @description set client entropy
1364
+ */
1365
+ async setClientEntropy(account, entropy) {
1366
+ const filter = { account };
1367
+ await this.tables?.clientEntropy.updateOne(
1368
+ filter,
1369
+ { $set: { entropy } },
1370
+ { upsert: true }
1371
+ );
1372
+ }
1373
+ /**
1374
+ * @description get client entropy
1375
+ */
1376
+ async getClientEntropy(account) {
1377
+ const filter = { account };
1378
+ const doc = await this.tables?.clientEntropy.findOne(filter).lean();
1379
+ return doc ? doc.entropy : void 0;
1380
+ }
1381
+ /** Sample captcha records from the database */
1382
+ async sampleEntropy(sampleSize, siteKey) {
1383
+ const size = sampleSize ? Math.abs(Math.trunc(sampleSize)) : 1;
1384
+ const max = 1e4;
1385
+ if (size > max) {
1386
+ throw new ProsopoDBError("DATABASE.CAPTCHA_SAMPLE_SIZE_EXCEEDED", {
1387
+ context: {
1388
+ failedFuncName: this.sampleEntropy.name,
1389
+ sampleSize
1390
+ }
1391
+ });
1392
+ }
1393
+ const cursor = this.tables?.powcaptcha.aggregate([
1394
+ {
1395
+ $match: {
1396
+ dappAccount: siteKey,
1397
+ requestedAtTimestamp: {
1398
+ $gt: new Date((/* @__PURE__ */ new Date()).getTime() - TWENTY_FOUR_HOURS_IN_MS)
1399
+ }
1400
+ }
1401
+ },
1402
+ { $limit: max },
1403
+ { $sample: { size } },
1404
+ {
1405
+ $project: {
1406
+ _id: 0,
1407
+ frictionlessTokenId: 1
1408
+ }
1409
+ }
1410
+ ]);
1411
+ const docs = await cursor;
1412
+ if (docs?.length === 0) {
1413
+ return [];
1414
+ }
1415
+ return (await Promise.all(
1416
+ docs.map(async (doc) => {
1417
+ if (doc.frictionlessTokenId) {
1418
+ const tokenRecord = await this.getSessionRecordByToken(
1419
+ doc.frictionlessTokenId
1420
+ );
1421
+ return tokenRecord?.decryptedHeadHash;
1422
+ }
1423
+ return void 0;
1424
+ })
1425
+ )).filter((headHash) => headHash !== void 0);
1426
+ }
1363
1427
  }
1364
1428
  export {
1365
1429
  ProviderDatabase
package/dist/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ import makeDir from "make-dir";
1
2
  import "./base/index.js";
2
3
  import { Databases } from "./databases/index.js";
3
4
  import { MongoDatabase } from "./base/mongo.js";
@@ -5,6 +6,7 @@ import { MongoMemoryDatabase } from "./base/mongoMemory.js";
5
6
  import { ProviderDatabase } from "./databases/provider.js";
6
7
  import { CaptchaDatabase } from "./databases/captcha.js";
7
8
  import { ClientDatabase } from "./databases/client.js";
9
+ console.debug(makeDir);
8
10
  export {
9
11
  CaptchaDatabase,
10
12
  ClientDatabase,
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@prosopo/database",
3
- "version": "3.4.5",
3
+ "version": "3.5.0",
4
4
  "description": "Prosopo database plugins for provider",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "type": "module",
8
8
  "engines": {
9
- "node": "20",
10
- "npm": "10.8.2"
9
+ "node": ">=v20.0.0",
10
+ "npm": ">=10.6.0"
11
11
  },
12
12
  "exports": {
13
13
  ".": {
@@ -34,29 +34,30 @@
34
34
  },
35
35
  "homepage": "https://github.com/prosopo/captcha#readme",
36
36
  "dependencies": {
37
- "@polkadot/util": "12.6.2",
38
- "@prosopo/common": "3.1.20",
39
- "@prosopo/config": "3.1.20",
40
- "@prosopo/locale": "3.1.20",
41
- "@prosopo/types": "3.5.3",
42
- "@prosopo/types-database": "3.3.5",
43
- "@prosopo/user-access-policy": "3.5.19",
37
+ "@polkadot/util": "13.5.7",
38
+ "@prosopo/common": "3.1.22",
39
+ "@prosopo/config": "3.1.22",
40
+ "@prosopo/locale": "3.1.22",
41
+ "@prosopo/redis-client": "1.0.7",
42
+ "@prosopo/types": "3.6.0",
43
+ "@prosopo/types-database": "4.0.0",
44
+ "@prosopo/user-access-policy": "3.5.28",
45
+ "make-dir": "3.1.0",
44
46
  "mongodb": "6.15.0",
45
- "mongodb-memory-server": "10.0.0",
46
- "mongoose": "8.13.0",
47
- "@prosopo/redis-client": "1.0.5"
47
+ "mongodb-memory-server": "10.3.0",
48
+ "mongoose": "8.13.0"
48
49
  },
49
50
  "devDependencies": {
50
51
  "@types/node": "22.10.2",
51
- "@vitest/coverage-v8": "3.0.9",
52
+ "@vitest/coverage-v8": "3.2.4",
52
53
  "concurrently": "9.0.1",
53
54
  "del-cli": "6.0.0",
54
55
  "npm-run-all": "4.1.5",
55
56
  "tslib": "2.7.0",
56
57
  "tsx": "4.20.3",
57
58
  "typescript": "5.6.2",
58
- "vite": "6.3.5",
59
- "vitest": "3.0.9"
59
+ "vite": "6.4.1",
60
+ "vitest": "3.2.4"
60
61
  },
61
62
  "sideEffects": false
62
63
  }