@prosopo/provider 3.13.0 → 3.13.7

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.
@@ -10,6 +10,7 @@ const hashUserAgent = require("../../utils/hashUserAgent.cjs");
10
10
  const hashUserIp = require("../../utils/hashUserIp.cjs");
11
11
  const apiToggleMaintenanceModeEndpoint = require("../admin/apiToggleMaintenanceModeEndpoint.cjs");
12
12
  const blacklistRequestInspector = require("../blacklistRequestInspector.cjs");
13
+ const contextAwareValidation = require("./contextAwareValidation.cjs");
13
14
  const tasks = require("../../tasks/tasks.cjs");
14
15
  const DEFAULT_FRICTIONLESS_THRESHOLD = 0.5;
15
16
  const getRoundsFromSimScore = (simScore) => {
@@ -91,7 +92,8 @@ const getFrictionlessCaptchaChallenge = (env, userAccessRulesStorage) => async (
91
92
  userAgent,
92
93
  webView,
93
94
  iFrame,
94
- decryptedHeadHash
95
+ decryptedHeadHash,
96
+ decryptionFailed
95
97
  } = await tasks$1.frictionlessManager.decryptPayload(token, headHash);
96
98
  req.logger.debug(() => ({
97
99
  msg: "Decrypted payload",
@@ -161,29 +163,6 @@ const getFrictionlessCaptchaChallenge = (env, userAccessRulesStorage) => async (
161
163
  dapp,
162
164
  userScope
163
165
  ))[0];
164
- const headersUserAgent = req.headers["user-agent"];
165
- const hashedHeadersUserAgent = headersUserAgent ? hashUserAgent.hashUserAgent(headersUserAgent) : "";
166
- const headersProsopoUser = req.headers["prosopo-user"];
167
- if (hashedHeadersUserAgent !== userAgent || headersProsopoUser !== userId) {
168
- req.logger.info(() => ({
169
- msg: "User agent or user id does not match",
170
- data: {
171
- headersUserAgent,
172
- hashedHeadersUserAgent,
173
- userAgent,
174
- // This is the hashed user agent from the token
175
- headersProsopoUser,
176
- userId
177
- }
178
- }));
179
- return res.json(
180
- await tasks$1.frictionlessManager.sendImageCaptcha({
181
- solvedImagesCount: frictionlessTasksUtils.timestampDecayFunction(timestamp),
182
- userSitekeyIpHash,
183
- reason: frictionlessTasks.FrictionlessReason.USER_AGENT_MISMATCH
184
- })
185
- );
186
- }
187
166
  if (userAccessPolicy) {
188
167
  const scoreUpdate = tasks$1.frictionlessManager.scoreIncreaseAccessPolicy(
189
168
  userAccessPolicy,
@@ -212,37 +191,83 @@ const getFrictionlessCaptchaChallenge = (env, userAccessRulesStorage) => async (
212
191
  );
213
192
  }
214
193
  }
215
- if (clientRecord.settings.contextAware?.enabled) {
216
- const clientEntropy = await tasks$1.frictionlessManager.getClientEntropy(
217
- clientRecord.account
218
- );
219
- if (clientEntropy) {
220
- if (!decryptedHeadHash) {
221
- tasks$1.logger.info(() => ({
222
- msg: "No decryptedHeadHash in session for context aware client"
223
- }));
224
- return next(
225
- new common.ProsopoApiError("API.BAD_REQUEST", {
226
- context: {
227
- code: 400,
228
- siteKey: dapp,
229
- user
230
- },
231
- i18n: req.i18n,
232
- logger: req.logger
233
- })
234
- );
194
+ const headersUserAgent = req.headers["user-agent"];
195
+ const hashedHeadersUserAgent = headersUserAgent ? hashUserAgent.hashUserAgent(headersUserAgent) : "";
196
+ const headersProsopoUser = req.headers["prosopo-user"];
197
+ if (hashedHeadersUserAgent !== userAgent || headersProsopoUser !== userId) {
198
+ req.logger.info(() => ({
199
+ msg: "User agent or user id does not match",
200
+ data: {
201
+ headersUserAgent,
202
+ hashedHeadersUserAgent,
203
+ userAgent,
204
+ // This is the hashed user agent from the token
205
+ headersProsopoUser,
206
+ userId
235
207
  }
236
- const sim = util.compareBinaryStrings(decryptedHeadHash, clientEntropy);
237
- const isValidContext = sim >= clientRecord.settings.contextAware.threshold;
238
- if (!isValidContext) {
239
- return res.json(
240
- await tasks$1.frictionlessManager.sendImageCaptcha({
241
- solvedImagesCount: getRoundsFromSimScore(sim),
242
- userSitekeyIpHash,
243
- reason: frictionlessTasks.FrictionlessReason.CONTEXT_AWARE_VALIDATION_FAILED
244
- })
208
+ }));
209
+ return res.json(
210
+ await tasks$1.frictionlessManager.sendImageCaptcha({
211
+ solvedImagesCount: frictionlessTasksUtils.timestampDecayFunction(
212
+ timestamp,
213
+ decryptionFailed
214
+ ),
215
+ userSitekeyIpHash,
216
+ reason: frictionlessTasks.FrictionlessReason.USER_AGENT_MISMATCH
217
+ })
218
+ );
219
+ }
220
+ if (clientRecord.settings.contextAware?.enabled) {
221
+ const contexts = clientRecord.settings.contextAware?.contexts || {};
222
+ const hasDefault = contexts[types.ContextType.Default] !== void 0;
223
+ const hasWebview = contexts[types.ContextType.Webview] !== void 0;
224
+ let contextType;
225
+ if (hasDefault && hasWebview) {
226
+ contextType = contextAwareValidation.determineContextType(webView);
227
+ } else if (hasDefault) {
228
+ contextType = types.ContextType.Default;
229
+ } else if (hasWebview) {
230
+ contextType = types.ContextType.Webview;
231
+ } else {
232
+ contextType = void 0;
233
+ }
234
+ if (contextType) {
235
+ const clientEntropy = await tasks$1.frictionlessManager.getClientContextEntropy(
236
+ clientRecord.account,
237
+ contextType
238
+ );
239
+ if (clientEntropy) {
240
+ if (!decryptedHeadHash) {
241
+ tasks$1.logger.info(() => ({
242
+ msg: "No decryptedHeadHash in session for context aware client"
243
+ }));
244
+ return next(
245
+ new common.ProsopoApiError("API.BAD_REQUEST", {
246
+ context: {
247
+ code: 400,
248
+ siteKey: dapp,
249
+ user
250
+ },
251
+ i18n: req.i18n,
252
+ logger: req.logger
253
+ })
254
+ );
255
+ }
256
+ const threshold = contextAwareValidation.getContextThreshold(
257
+ clientRecord.settings,
258
+ contextType
245
259
  );
260
+ const sim = util.compareBinaryStrings(decryptedHeadHash, clientEntropy);
261
+ const isValidContext = sim >= threshold;
262
+ if (!isValidContext) {
263
+ return res.json(
264
+ await tasks$1.frictionlessManager.sendImageCaptcha({
265
+ solvedImagesCount: getRoundsFromSimScore(sim),
266
+ userSitekeyIpHash,
267
+ reason: frictionlessTasks.FrictionlessReason.CONTEXT_AWARE_VALIDATION_FAILED
268
+ })
269
+ );
270
+ }
246
271
  }
247
272
  }
248
273
  }
@@ -278,7 +303,10 @@ const getFrictionlessCaptchaChallenge = (env, userAccessRulesStorage) => async (
278
303
  tasks$1.frictionlessManager.updateScore(botScore, scoreComponents);
279
304
  return res.json(
280
305
  await tasks$1.frictionlessManager.sendImageCaptcha({
281
- solvedImagesCount: frictionlessTasksUtils.timestampDecayFunction(timestamp),
306
+ solvedImagesCount: frictionlessTasksUtils.timestampDecayFunction(
307
+ timestamp,
308
+ decryptionFailed
309
+ ),
282
310
  userSitekeyIpHash,
283
311
  reason: frictionlessTasks.FrictionlessReason.OLD_TIMESTAMP
284
312
  })
@@ -6,6 +6,7 @@ const database = require("@prosopo/database");
6
6
  const types = require("@prosopo/types");
7
7
  const util = require("@prosopo/util");
8
8
  const validateAddress = require("../../api/validateAddress.cjs");
9
+ const SAMPLE_SIZE = 100;
9
10
  const isValidPrivateKey = (privateKeyString) => {
10
11
  const privateKey = Buffer.from(privateKeyString, "base64").toString("ascii");
11
12
  try {
@@ -163,7 +164,13 @@ class ClientTaskManager {
163
164
  this.logger
164
165
  );
165
166
  const tenMinuteWindow = 10 * 60 * 1e3;
166
- const updatedAtTimestamp = lastTask?.updated ? lastTask.updated.getTime() - tenMinuteWindow || 0 : 0;
167
+ const updatedAtTimestamp = (() => {
168
+ const raw = lastTask?.updated;
169
+ if (!raw) return 0;
170
+ const ts = raw instanceof Date ? raw.getTime() : Date.parse(String(raw));
171
+ if (Number.isNaN(ts)) return 0;
172
+ return Math.max(ts - tenMinuteWindow, 0);
173
+ })();
167
174
  this.logger.info(() => ({
168
175
  msg: `Getting updated client records since ${new Date(updatedAtTimestamp).toDateString()}`
169
176
  }));
@@ -201,14 +208,71 @@ class ClientTaskManager {
201
208
  * @returns Promise<void>
202
209
  */
203
210
  async calculateClientEntropy() {
204
- const clients = await this.providerDB.getAllClientRecords();
205
- for (const client of clients) {
206
- const sampleEntropies = await this.providerDB.sampleEntropy(
207
- 100,
208
- client.account
211
+ const taskID = await this.providerDB.createScheduledTaskStatus(
212
+ types.ScheduledTaskNames.SetClientEntropy,
213
+ types.ScheduledTaskStatus.Running
214
+ );
215
+ try {
216
+ let clients = await this.providerDB.getAllClientRecords();
217
+ clients = clients.filter((client) => client.tier !== types.Tier.Free);
218
+ this.logger.info(() => ({
219
+ msg: `Calculating entropies for ${clients.length} clients`
220
+ }));
221
+ for (const client of clients) {
222
+ if (client.settings?.contextAware?.enabled) {
223
+ const contextTypes = Object.keys(
224
+ client.settings.contextAware.contexts ?? {}
225
+ );
226
+ for (const contextType of contextTypes) {
227
+ const contextSamples = await this.providerDB.sampleContextEntropy(
228
+ SAMPLE_SIZE,
229
+ client.account,
230
+ contextType
231
+ );
232
+ if (contextSamples.length < SAMPLE_SIZE) {
233
+ this.logger.info(() => ({
234
+ msg: `Skipping ${contextType} entropy calculation for client ${client.account} due to insufficient samples (${contextSamples.length}/${SAMPLE_SIZE})`
235
+ }));
236
+ continue;
237
+ }
238
+ const contextAvgEntropy = util.majorityAverage(contextSamples);
239
+ this.logger.info(() => ({
240
+ msg: `Calculated ${contextType} entropy for client ${client.account}: ${contextAvgEntropy}`
241
+ }));
242
+ await this.providerDB.setClientContextEntropy(
243
+ client.account,
244
+ contextType,
245
+ contextAvgEntropy
246
+ );
247
+ }
248
+ }
249
+ }
250
+ await this.providerDB.updateScheduledTaskStatus(
251
+ taskID,
252
+ types.ScheduledTaskStatus.Completed,
253
+ {
254
+ data: {
255
+ clientRecords: clients.length
256
+ }
257
+ }
258
+ );
259
+ } catch (e) {
260
+ const calculateClientEntropiesError = new common.ProsopoApiError(
261
+ "DATABASE.UNKNOWN",
262
+ {
263
+ context: { error: e },
264
+ logger: this.logger
265
+ }
266
+ );
267
+ this.logger.error(() => ({
268
+ err: calculateClientEntropiesError,
269
+ msg: "Error calculating client entropy"
270
+ }));
271
+ await this.providerDB.updateScheduledTaskStatus(
272
+ taskID,
273
+ types.ScheduledTaskStatus.Failed,
274
+ { error: String(e) }
209
275
  );
210
- const avgEntropy = util.majorityAverage(sampleEntropies);
211
- await this.providerDB.setClientEntropy(client.account, avgEntropy);
212
276
  }
213
277
  }
214
278
  async registerSiteKey(siteKey, tier, settings) {