@prosopo/database 4.1.4 → 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.
@@ -45,9 +45,9 @@ dist/cjs/index.cjs 1.66 kB │ gzip: 0.49 kB
45
45
  dist/cjs/databases/client.cjs 1.79 kB │ gzip: 0.77 kB
46
46
  dist/cjs/mongooseOptions.cjs 3.74 kB │ gzip: 1.56 kB
47
47
  dist/cjs/base/mongo.cjs 4.27 kB │ gzip: 1.41 kB
48
- dist/cjs/databases/centralDbStreamer.cjs 5.53 kB │ gzip: 1.37 kB
49
- dist/cjs/databases/captcha.cjs 7.38 kB │ gzip: 2.18 kB
48
+ dist/cjs/databases/centralDbStreamer.cjs 6.40 kB │ gzip: 1.52 kB
49
+ dist/cjs/databases/captcha.cjs 8.20 kB │ gzip: 2.36 kB
50
50
  dist/cjs/redisCache.cjs 14.31 kB │ gzip: 4.46 kB
51
- dist/cjs/databases/provider.cjs 60.73 kB │ gzip: 12.01 kB
51
+ dist/cjs/databases/provider.cjs 67.79 kB │ gzip: 13.13 kB
52
52
 
53
- ✓ built in 74ms
53
+ ✓ built in 68ms
@@ -1,5 +1,5 @@
1
1
  $ tsc --build --verbose
2
- 1:37:55 PM - Projects in this build:
2
+ 10:12:01 AM - Projects in this build:
3
3
  * ../../dev/config/tsconfig.json
4
4
  * ../util/tsconfig.json
5
5
  * ../locale/tsconfig.json
@@ -14,31 +14,31 @@ $ tsc --build --verbose
14
14
  * ../types-database/tsconfig.json
15
15
  * tsconfig.json
16
16
 
17
- 1:37:55 PM - Project '../../dev/config/tsconfig.json' is up to date because newest input '../../dev/config/src/webpack/webpack.config.ts' is older than output '../../dev/config/tsconfig.tsbuildinfo'
17
+ 10:12:01 AM - Project '../../dev/config/tsconfig.json' is up to date because newest input '../../dev/config/src/webpack/webpack.config.ts' is older than output '../../dev/config/tsconfig.tsbuildinfo'
18
18
 
19
- 1:37:55 PM - Project '../util/tsconfig.json' is up to date because newest input '../util/src/url.ts' is older than output '../util/tsconfig.tsbuildinfo'
19
+ 10:12:01 AM - Project '../util/tsconfig.json' is up to date because newest input '../util/src/table.ts' is older than output '../util/tsconfig.tsbuildinfo'
20
20
 
21
- 1:37:55 PM - Project '../locale/tsconfig.json' is up to date because newest input '../locale/src/translationKey.ts' is older than output '../locale/tsconfig.tsbuildinfo'
21
+ 10:12:01 AM - Project '../locale/tsconfig.json' is up to date because newest input '../locale/src/translationKey.ts' is older than output '../locale/tsconfig.tsbuildinfo'
22
22
 
23
- 1:37:55 PM - Project '../logger/tsconfig.json' is up to date because newest input '../logger/src/index.ts' is older than output '../logger/tsconfig.tsbuildinfo'
23
+ 10:12:01 AM - Project '../logger/tsconfig.json' is up to date because newest input '../logger/src/logger.ts' is older than output '../logger/tsconfig.tsbuildinfo'
24
24
 
25
- 1:37:55 PM - Project '../util-crypto/tsconfig.json' is up to date because newest input '../util-crypto/src/types.ts' is older than output '../util-crypto/tsconfig.tsbuildinfo'
25
+ 10:12:01 AM - Project '../util-crypto/tsconfig.json' is up to date because newest input '../util-crypto/src/types.ts' is older than output '../util-crypto/tsconfig.tsbuildinfo'
26
26
 
27
- 1:37:56 PM - Project '../types/tsconfig.json' is up to date because newest input '../types/src/types.test-d.ts' is older than output '../types/tsconfig.tsbuildinfo'
27
+ 10:12:01 AM - Project '../types/tsconfig.json' is up to date because newest input '../types/src/types.test-d.ts' is older than output '../types/tsconfig.tsbuildinfo'
28
28
 
29
- 1:37:56 PM - Project '../common/tsconfig.json' is up to date because newest input '../common/src/index.ts' is older than output '../common/tsconfig.tsbuildinfo'
29
+ 10:12:01 AM - Project '../common/tsconfig.json' is up to date because newest input '../common/src/tests/utils/batches.unit.test.ts' is older than output '../common/tsconfig.tsbuildinfo'
30
30
 
31
- 1:37:56 PM - Project '../api-route/tsconfig.json' is up to date because newest input '../api-route/src/apiRoutes.ts' is older than output '../api-route/tsconfig.tsbuildinfo'
31
+ 10:12:01 AM - Project '../api-route/tsconfig.json' is up to date because newest input '../api-route/src/apiRoutes.ts' is older than output '../api-route/tsconfig.tsbuildinfo'
32
32
 
33
- 1:37:56 PM - Project '../redis-client/tsconfig.json' is up to date because newest input '../redis-client/src/index.ts' is older than output '../redis-client/tsconfig.tsbuildinfo'
33
+ 10:12:01 AM - Project '../redis-client/tsconfig.json' is up to date because newest input '../redis-client/src/index.ts' is older than output '../redis-client/tsconfig.tsbuildinfo'
34
34
 
35
- 1:37:56 PM - Project '../api/tsconfig.json' is up to date because newest input '../api/src/tests/apiHarness.ts' is older than output '../api/tsconfig.tsbuildinfo'
35
+ 10:12:01 AM - Project '../api/tsconfig.json' is up to date because newest input '../api/src/index.ts' is older than output '../api/tsconfig.tsbuildinfo'
36
36
 
37
- 1:37:56 PM - Project '../user-access-policy/tsconfig.json' is up to date because newest input '../user-access-policy/src/ruleRecord.ts' is older than output '../user-access-policy/tsconfig.tsbuildinfo'
37
+ 10:12:01 AM - Project '../user-access-policy/tsconfig.json' is up to date because newest input '../user-access-policy/src/transformRule.ts' is older than output '../user-access-policy/tsconfig.tsbuildinfo'
38
38
 
39
- 1:37:56 PM - Project '../types-database/tsconfig.json' is up to date because newest input '../types-database/src/index.ts' is older than output '../types-database/tsconfig.tsbuildinfo'
39
+ 10:12:01 AM - Project '../types-database/tsconfig.json' is up to date because newest input '../types-database/src/types.test-d.ts' is older than output '../types-database/tsconfig.tsbuildinfo'
40
40
 
41
- 1:37:56 PM - Project 'tsconfig.json' is out of date because output file 'tsconfig.tsbuildinfo' does not exist
41
+ 10:12:01 AM - Project 'tsconfig.json' is out of date because output file 'tsconfig.tsbuildinfo' does not exist
42
42
 
43
- 1:37:56 PM - Building project '/home/runner/work/captcha/captcha/packages/database/tsconfig.json'...
43
+ 10:12:01 AM - Building project '/home/runner/work/captcha/captcha/packages/database/tsconfig.json'...
44
44
 
@@ -46,9 +46,9 @@ dist/index.js 1.07 kB │ gzip: 0.38 kB
46
46
  dist/databases/client.js 1.70 kB │ gzip: 0.75 kB
47
47
  dist/mongooseOptions.js 3.58 kB │ gzip: 1.53 kB
48
48
  dist/base/mongo.js 4.01 kB │ gzip: 1.32 kB
49
- dist/databases/centralDbStreamer.js 5.47 kB │ gzip: 1.35 kB
50
- dist/databases/captcha.js 7.30 kB │ gzip: 2.17 kB
49
+ dist/databases/centralDbStreamer.js 6.33 kB │ gzip: 1.51 kB
50
+ dist/databases/captcha.js 8.13 kB │ gzip: 2.36 kB
51
51
  dist/redisCache.js 14.26 kB │ gzip: 4.44 kB
52
- dist/databases/provider.js 59.41 kB │ gzip: 12.03 kB
52
+ dist/databases/provider.js 66.18 kB │ gzip: 13.13 kB
53
53
 
54
- ✓ built in 144ms
54
+ ✓ built in 88ms
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # @prosopo/database
2
2
 
3
+ ## 4.2.0
4
+ ### Minor Changes
5
+
6
+ - b299a91: An audio challenge, offered only as an accessibility alternative, like reCAPTCHA's audio option. The user hears a short sequence of spoken digits and types them in.
7
+
8
+ It is off by default and needs two switches. Prosopo has to turn on the `captchaTypeFeatureFlags.audio` feature flag for the site, which the site owner cannot do, and the site owner has to set `audioAccessibilityEnabled`. When both are on, image, puzzle and icon-order challenges show a "Use audio instead" button. Pressing it swaps the visual challenge for the audio one. After a wrong answer the user stays on audio and gets a fresh clip.
9
+
10
+ Audio is never a captcha type that can be selected or routed to. A site's `captchaType`, traffic-filter categories, Restrict rules and the site-key CLI all reject it, and routing, PoW escalation and the severity tiers leave it out. The provider only serves audio against the visual session the user was already given, and only on a site with both switches on; any other audio request is refused before the session is used up, and a request without a session is always refused.
11
+
12
+ The spoken digits are synthesised by `@prosopo/audio-assets`, so there is no recorded set of clips to collect. The answer never leaves the provider, and a challenge can be submitted and verified only once, even under concurrent requests. `@prosopo/procaptcha-audio` is the widget. It and the "Use audio instead" button are built on the shared widget code in `@prosopo/procaptcha-common`, and the provider side is built on the shared interactive-captcha code that puzzle and icon-order use.
13
+
14
+ The demo playground has audio pages, and there is an end-to-end test for it.
15
+ - e13d7a8: New captcha type: `iconOrder`. The user is shown a frame of icons and a legend, and clicks the legend's icons in the order given.
16
+
17
+ Icon-order is off by default. Only Prosopo can switch it on for a site, with the `captchaTypeFeatureFlags.iconOrder` feature flag; the site owner cannot. A site without the flag is never served icon-order by any route, and the challenge endpoint refuses it. Once the flag is on, the owner can still keep icon-order out of the frictionless flow with `frictionlessTypes.iconOrder`, the same way as image and puzzle.
18
+
19
+ The answer never leaves the provider. Icon positions are stored on the challenge record, and the widget receives only the rendered frame and legend. Grading checks order as well as position, and each icon's hit radius scales with its size. Verifying a token is single-use under concurrent requests.
20
+
21
+ `@prosopo/icon-order-assets` draws the imagery, and `@prosopo/procaptcha-icon-order` is the widget. Its text is translated into every supported language.
22
+
23
+ Puzzle and icon-order now share their server code: challenge and solution handlers, the verify route, the submit and verify pipeline, and the database record methods. The widget code they have in common moves into `@prosopo/procaptcha-common`: the lazy mount wrapper, manager expiry and dispose, spent-session handling, behavioural data encryption and trusted click coordinates. Puzzle's behaviour is unchanged.
24
+
25
+ The demo playground has icon-order pages, and there is an end-to-end test for it.
26
+
27
+ ### Patch Changes
28
+
29
+ - Updated dependencies [b299a91]
30
+ - Updated dependencies [e13d7a8]
31
+ - Updated dependencies [270395d]
32
+ - @prosopo/types@5.14.0
33
+ - @prosopo/types-database@5.9.0
34
+ - @prosopo/user-access-policy@3.14.16
35
+ - @prosopo/common@3.1.63
36
+
3
37
  ## 4.1.4
4
38
  ### Patch Changes
5
39
 
@@ -26,6 +26,16 @@ var CAPTCHA_TABLES = [
26
26
  collectionName: "puzzlecaptcha",
27
27
  modelName: "PuzzleCaptcha",
28
28
  schema: _prosopo_types_database.StoredPuzzleCaptchaRecordSchema
29
+ },
30
+ {
31
+ collectionName: "audiocaptcha",
32
+ modelName: "AudioCaptcha",
33
+ schema: _prosopo_types_database.StoredAudioCaptchaRecordSchema
34
+ },
35
+ {
36
+ collectionName: "iconordercaptcha",
37
+ modelName: "IconOrderCaptcha",
38
+ schema: _prosopo_types_database.StoredIconOrderCaptchaRecordSchema
29
39
  }
30
40
  ];
31
41
  var CaptchaDatabase = class CaptchaDatabase extends require_mongo.MongoDatabase {
@@ -111,7 +121,7 @@ var CaptchaDatabase = class CaptchaDatabase extends require_mongo.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 require_mongo.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 _prosopo_common.ProsopoDBError("DATABASE.QUERY_ERROR", {
@@ -113,27 +113,45 @@ var CentralDbStreamer = class CentralDbStreamer {
113
113
  * Fire-and-forget: errors are logged, never thrown.
114
114
  */
115
115
  streamPuzzleRecord(record, markStored) {
116
+ this.streamChallengeRecord("puzzlecaptcha", "puzzle", record, markStored);
117
+ }
118
+ /**
119
+ * Stream a partial puzzle update by fetching the full record first, then upserting.
120
+ */
121
+ streamPuzzleUpdate(getFullRecord, markStored) {
122
+ this.streamChallengeUpdate(getFullRecord, (record) => this.streamPuzzleRecord(record, markStored), "puzzle");
123
+ }
124
+ streamAudioRecord(record, markStored) {
125
+ this.streamChallengeRecord("audiocaptcha", "audio", record, markStored);
126
+ }
127
+ streamAudioUpdate(getFullRecord, markStored) {
128
+ this.streamChallengeUpdate(getFullRecord, (record) => this.streamAudioRecord(record, markStored), "audio");
129
+ }
130
+ streamIconOrderRecord(record, markStored) {
131
+ this.streamChallengeRecord("iconordercaptcha", "icon-order", record, markStored);
132
+ }
133
+ streamIconOrderUpdate(getFullRecord, markStored) {
134
+ this.streamChallengeUpdate(getFullRecord, (record) => this.streamIconOrderRecord(record, markStored), "icon-order");
135
+ }
136
+ streamChallengeRecord(table, label, record, markStored) {
116
137
  const timestamp = this.getRecordTimestamp(record);
117
138
  this.ensureConnected().then(() => {
118
139
  const { _id, ...safeDoc } = record;
119
- return this.db.tables.puzzlecaptcha.updateOne({ challenge: safeDoc.challenge }, { $set: safeDoc }, { upsert: true });
140
+ return this.db.tables[table].updateOne({ challenge: safeDoc.challenge }, { $set: safeDoc }, { upsert: true });
120
141
  }).then(() => markStored?.(timestamp)).catch((err) => {
121
142
  this.logger.error(() => ({
122
143
  err,
123
- msg: "Failed to stream puzzle record to central DB"
144
+ msg: `Failed to stream ${label} record to central DB`
124
145
  }));
125
146
  });
126
147
  }
127
- /**
128
- * Stream a partial puzzle update by fetching the full record first, then upserting.
129
- */
130
- streamPuzzleUpdate(getFullRecord, markStored) {
148
+ streamChallengeUpdate(getFullRecord, stream, label) {
131
149
  getFullRecord().then((record) => {
132
- if (record) this.streamPuzzleRecord(record, markStored);
150
+ if (record) stream(record);
133
151
  }).catch((err) => {
134
152
  this.logger.error(() => ({
135
153
  err,
136
- msg: "Failed to fetch puzzle record for central DB streaming"
154
+ msg: `Failed to fetch ${label} record for central DB streaming`
137
155
  }));
138
156
  });
139
157
  }