@prosopo/provider 3.12.3 → 3.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +214 -0
  2. package/dist/api/admin/apiAdminRoutesProvider.js +13 -18
  3. package/dist/api/admin/apiToggleMaintenanceModeEndpoint.js +40 -0
  4. package/dist/api/blacklistRequestInspector.js +4 -4
  5. package/dist/api/captcha/getFrictionlessCaptchaChallenge.js +338 -0
  6. package/dist/api/captcha/getImageCaptchaChallenge.js +150 -0
  7. package/dist/api/captcha/getPoWCaptchaChallenge.js +156 -0
  8. package/dist/api/captcha/submitImageCaptchaSolution.js +87 -0
  9. package/dist/api/captcha/submitPoWCaptchaSolution.js +77 -0
  10. package/dist/api/captcha.js +18 -606
  11. package/dist/api/verify.js +24 -1
  12. package/dist/cjs/api/admin/apiAdminRoutesProvider.cjs +13 -18
  13. package/dist/cjs/api/admin/apiRegisterSiteKeyEndpoint.cjs +2 -1
  14. package/dist/cjs/api/admin/apiRemoveDetectorKeyEndpoint.cjs +3 -2
  15. package/dist/cjs/api/admin/apiToggleMaintenanceModeEndpoint.cjs +41 -0
  16. package/dist/cjs/api/blacklistRequestInspector.cjs +3 -3
  17. package/dist/cjs/api/captcha/getFrictionlessCaptchaChallenge.cjs +337 -0
  18. package/dist/cjs/api/captcha/getImageCaptchaChallenge.cjs +149 -0
  19. package/dist/cjs/api/captcha/getPoWCaptchaChallenge.cjs +155 -0
  20. package/dist/cjs/api/captcha/submitImageCaptchaSolution.cjs +86 -0
  21. package/dist/cjs/api/captcha/submitPoWCaptchaSolution.cjs +76 -0
  22. package/dist/cjs/api/captcha.cjs +17 -605
  23. package/dist/cjs/api/ja4Middleware.cjs +2 -1
  24. package/dist/cjs/api/verify.cjs +24 -1
  25. package/dist/cjs/index.cjs +2 -0
  26. package/dist/cjs/schedulers/setClientEntropy.cjs +36 -0
  27. package/dist/cjs/tasks/captchaManager.cjs +7 -22
  28. package/dist/cjs/tasks/client/clientTasks.cjs +18 -36
  29. package/dist/cjs/tasks/detection/decodePayload.cjs +385 -714
  30. package/dist/cjs/tasks/detection/getBotScore.cjs +15 -2
  31. package/dist/cjs/tasks/frictionless/frictionlessTasks.cjs +136 -30
  32. package/dist/cjs/tasks/imgCaptcha/imgCaptchaTasks.cjs +25 -13
  33. package/dist/cjs/tasks/powCaptcha/powTasks.cjs +8 -8
  34. package/dist/cjs/tasks/tasks.cjs +1 -0
  35. package/dist/cjs/util.cjs +14 -1
  36. package/dist/cjs/utils/hashUserIp.cjs +9 -0
  37. package/dist/index.js +2 -0
  38. package/dist/schedulers/setClientEntropy.js +36 -0
  39. package/dist/tasks/captchaManager.js +5 -21
  40. package/dist/tasks/client/clientTasks.js +19 -37
  41. package/dist/tasks/detection/decodePayload.js +385 -714
  42. package/dist/tasks/detection/getBotScore.js +17 -4
  43. package/dist/tasks/frictionless/frictionlessTasks.js +137 -31
  44. package/dist/tasks/imgCaptcha/imgCaptchaTasks.js +25 -13
  45. package/dist/tasks/powCaptcha/powTasks.js +8 -8
  46. package/dist/tasks/tasks.js +1 -0
  47. package/dist/util.js +14 -1
  48. package/dist/utils/hashUserIp.js +9 -0
  49. package/package.json +24 -25
@@ -3,30 +3,25 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const types = require("@prosopo/types");
4
4
  const apiRegisterSiteKeyEndpoint = require("./apiRegisterSiteKeyEndpoint.cjs");
5
5
  const apiRemoveDetectorKeyEndpoint = require("./apiRemoveDetectorKeyEndpoint.cjs");
6
+ const apiToggleMaintenanceModeEndpoint = require("./apiToggleMaintenanceModeEndpoint.cjs");
6
7
  const apiUpdateDetectorKeyEndpoint = require("./apiUpdateDetectorKeyEndpoint.cjs");
7
8
  class ApiAdminRoutesProvider {
8
9
  constructor(tasks) {
9
10
  this.tasks = tasks;
10
11
  }
11
12
  getRoutes() {
12
- return [
13
- {
14
- path: types.AdminApiPaths.SiteKeyRegister,
15
- endpoint: new apiRegisterSiteKeyEndpoint.ApiRegisterSiteKeyEndpoint(this.tasks.clientTaskManager)
16
- },
17
- {
18
- path: types.AdminApiPaths.UpdateDetectorKey,
19
- endpoint: new apiUpdateDetectorKeyEndpoint.ApiUpdateDetectorKeyEndpoint(
20
- this.tasks.clientTaskManager
21
- )
22
- },
23
- {
24
- path: types.AdminApiPaths.RemoveDetectorKey,
25
- endpoint: new apiRemoveDetectorKeyEndpoint.ApiRemoveDetectorKeyEndpoint(
26
- this.tasks.clientTaskManager
27
- )
28
- }
29
- ];
13
+ return {
14
+ [types.AdminApiPaths.SiteKeyRegister]: new apiRegisterSiteKeyEndpoint.ApiRegisterSiteKeyEndpoint(
15
+ this.tasks.clientTaskManager
16
+ ),
17
+ [types.AdminApiPaths.UpdateDetectorKey]: new apiUpdateDetectorKeyEndpoint.ApiUpdateDetectorKeyEndpoint(
18
+ this.tasks.clientTaskManager
19
+ ),
20
+ [types.AdminApiPaths.RemoveDetectorKey]: new apiRemoveDetectorKeyEndpoint.ApiRemoveDetectorKeyEndpoint(
21
+ this.tasks.clientTaskManager
22
+ ),
23
+ [types.AdminApiPaths.ToggleMaintenanceMode]: new apiToggleMaintenanceModeEndpoint.ApiToggleMaintenanceModeEndpoint()
24
+ };
30
25
  }
31
26
  }
32
27
  exports.ApiAdminRoutesProvider = ApiAdminRoutesProvider;
@@ -3,13 +3,14 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const apiRoute = require("@prosopo/api-route");
4
4
  const common = require("@prosopo/common");
5
5
  const types = require("@prosopo/types");
6
+ var _documentCurrentScript = typeof document !== "undefined" ? document.currentScript : null;
6
7
  class ApiRegisterSiteKeyEndpoint {
7
8
  constructor(clientTaskManager) {
8
9
  this.clientTaskManager = clientTaskManager;
9
10
  }
10
11
  async processRequest(args, logger) {
11
12
  const { siteKey, tier, settings } = args;
12
- logger = logger || common.getLogger("info", module);
13
+ logger = logger || common.getLogger("info", typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("api/admin/apiRegisterSiteKeyEndpoint.cjs", document.baseURI).href);
13
14
  const temp = settings || types.ClientSettingsSchema.parse({});
14
15
  logger.info(() => ({ data: { siteKey }, msg: "`Registering site key" }));
15
16
  await this.clientTaskManager.registerSiteKey(siteKey, tier, temp);
@@ -3,15 +3,16 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const apiRoute = require("@prosopo/api-route");
4
4
  const common = require("@prosopo/common");
5
5
  const types = require("@prosopo/types");
6
+ var _documentCurrentScript = typeof document !== "undefined" ? document.currentScript : null;
6
7
  class ApiRemoveDetectorKeyEndpoint {
7
8
  constructor(clientTaskManager) {
8
9
  this.clientTaskManager = clientTaskManager;
9
10
  }
10
11
  async processRequest(args, logger) {
11
- logger = logger || common.getLogger("info", module);
12
+ logger = logger || common.getLogger("info", typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("api/admin/apiRemoveDetectorKeyEndpoint.cjs", document.baseURI).href);
12
13
  try {
13
14
  const { detectorKey, expirationInSeconds } = args;
14
- logger = logger || common.getLogger("info", module);
15
+ logger = logger || common.getLogger("info", typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("api/admin/apiRemoveDetectorKeyEndpoint.cjs", document.baseURI).href);
15
16
  logger.info(() => ({ msg: "Removing detector key" }));
16
17
  await this.clientTaskManager.removeDetectorKey(
17
18
  detectorKey,
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const apiRoute = require("@prosopo/api-route");
4
+ const common = require("@prosopo/common");
5
+ const types = require("@prosopo/types");
6
+ var _documentCurrentScript = typeof document !== "undefined" ? document.currentScript : null;
7
+ function getMaintenanceMode() {
8
+ return process.env.MAINTENANCE_MODE?.toLowerCase() === "true";
9
+ }
10
+ function setMaintenanceMode(enabled) {
11
+ process.env.MAINTENANCE_MODE = enabled ? "true" : "false";
12
+ }
13
+ class ApiToggleMaintenanceModeEndpoint {
14
+ async processRequest(args, logger) {
15
+ const { enabled } = args;
16
+ logger = logger || common.getLogger("info", typeof document === "undefined" ? require("url").pathToFileURL(__filename).href : _documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === "SCRIPT" && _documentCurrentScript.src || new URL("api/admin/apiToggleMaintenanceModeEndpoint.cjs", document.baseURI).href);
17
+ const previousMode = getMaintenanceMode();
18
+ logger.info(() => ({
19
+ data: { enabled, previous: previousMode },
20
+ msg: "Toggling maintenance mode"
21
+ }));
22
+ setMaintenanceMode(enabled);
23
+ const currentMode = getMaintenanceMode();
24
+ logger.info(() => ({
25
+ data: { enabled: currentMode },
26
+ msg: "Maintenance mode updated"
27
+ }));
28
+ return {
29
+ status: apiRoute.ApiEndpointResponseStatus.SUCCESS,
30
+ data: {
31
+ maintenanceMode: currentMode
32
+ }
33
+ };
34
+ }
35
+ getRequestArgsSchema() {
36
+ return types.ToggleMaintenanceModeBody;
37
+ }
38
+ }
39
+ exports.ApiToggleMaintenanceModeEndpoint = ApiToggleMaintenanceModeEndpoint;
40
+ exports.getMaintenanceMode = getMaintenanceMode;
41
+ exports.setMaintenanceMode = setMaintenanceMode;
@@ -34,16 +34,16 @@ const getPrioritisedAccessRule = async (userAccessRulesStorage, userScope, clien
34
34
  if (Object.values(scope).every((value) => value === void 0)) {
35
35
  continue;
36
36
  }
37
- const parsedUserScope = userAccessPolicy.userScopeInputSchema.parse(scope);
37
+ const parsedUserScope = userAccessPolicy.userScopeInput.parse(scope);
38
38
  const filter = {
39
39
  ...clientOrUndefined && {
40
40
  policyScope: {
41
41
  clientId: clientOrUndefined
42
42
  }
43
43
  },
44
- policyScopeMatch: userAccessPolicy.ScopeMatch.Exact,
44
+ policyScopeMatch: userAccessPolicy.FilterScopeMatch.Exact,
45
45
  userScope: parsedUserScope,
46
- userScopeMatch: userAccessPolicy.ScopeMatch.Exact
46
+ userScopeMatch: userAccessPolicy.FilterScopeMatch.Exact
47
47
  };
48
48
  policyPromises.push(userAccessRulesStorage.findRules(filter, true, true));
49
49
  }
@@ -0,0 +1,337 @@
1
+ "use strict";
2
+ const common = require("@prosopo/common");
3
+ const types = require("@prosopo/types");
4
+ const util = require("@prosopo/util");
5
+ const compositeIpAddress = require("../../compositeIpAddress.cjs");
6
+ const frictionlessTasks = require("../../tasks/frictionless/frictionlessTasks.cjs");
7
+ const frictionlessTasksUtils = require("../../tasks/frictionless/frictionlessTasksUtils.cjs");
8
+ require("../../tasks/index.cjs");
9
+ const hashUserAgent = require("../../utils/hashUserAgent.cjs");
10
+ const hashUserIp = require("../../utils/hashUserIp.cjs");
11
+ const apiToggleMaintenanceModeEndpoint = require("../admin/apiToggleMaintenanceModeEndpoint.cjs");
12
+ const blacklistRequestInspector = require("../blacklistRequestInspector.cjs");
13
+ const tasks = require("../../tasks/tasks.cjs");
14
+ const DEFAULT_FRICTIONLESS_THRESHOLD = 0.5;
15
+ const getRoundsFromSimScore = (simScore) => {
16
+ if (simScore >= 0.9) return 0;
17
+ if (simScore >= 0.8) return 3;
18
+ if (simScore >= 0.7) return 4;
19
+ if (simScore >= 0.6) return 6;
20
+ if (simScore >= 0.5) return 7;
21
+ return 8;
22
+ };
23
+ const getFrictionlessCaptchaChallenge = (env, userAccessRulesStorage) => async (req, res, next) => {
24
+ try {
25
+ const tasks$1 = new tasks.Tasks(env, req.logger);
26
+ const { token, headHash, dapp, user } = types.GetFrictionlessCaptchaChallengeRequestBody.parse(req.body);
27
+ if (apiToggleMaintenanceModeEndpoint.getMaintenanceMode()) {
28
+ req.logger.info(() => ({
29
+ msg: "Maintenance mode active - storing dummy token and sending PoW captcha",
30
+ data: { dapp, user }
31
+ }));
32
+ return res.json(
33
+ await tasks$1.frictionlessManager.sendPowCaptcha({
34
+ token,
35
+ score: 0,
36
+ threshold: 0.5,
37
+ scoreComponents: {
38
+ baseScore: 0
39
+ },
40
+ providerSelectEntropy: 0,
41
+ ipAddress: compositeIpAddress.getCompositeIpAddress(req.ip || ""),
42
+ webView: false,
43
+ iFrame: false,
44
+ decryptedHeadHash: ""
45
+ })
46
+ );
47
+ }
48
+ const existingToken = await tasks$1.db.getSessionRecordByToken(token);
49
+ if (existingToken) {
50
+ req.logger.info(() => ({
51
+ token: existingToken,
52
+ msg: "Token has already been used"
53
+ }));
54
+ return next(
55
+ new common.ProsopoApiError("API.BAD_REQUEST", {
56
+ context: {
57
+ code: 400,
58
+ siteKey: dapp,
59
+ user
60
+ },
61
+ i18n: req.i18n,
62
+ logger: req.logger
63
+ })
64
+ );
65
+ }
66
+ const userSitekeyIpHash = hashUserIp.hashUserIp(user, req.ip || "", dapp);
67
+ const existingSession = await tasks$1.db.getSessionByuserSitekeyIpHash(userSitekeyIpHash);
68
+ if (existingSession) {
69
+ req.logger.info(() => ({
70
+ msg: "Reusing existing session for user-IP-sitekey combination",
71
+ data: {
72
+ userSitekeyIpHash,
73
+ sessionId: existingSession.sessionId,
74
+ captchaType: existingSession.captchaType
75
+ }
76
+ }));
77
+ return res.json({
78
+ [types.ApiParams.captchaType]: existingSession.captchaType,
79
+ [types.ApiParams.sessionId]: existingSession.sessionId,
80
+ [types.ApiParams.status]: "ok"
81
+ });
82
+ }
83
+ const lScore = tasks$1.frictionlessManager.checkLangRules(
84
+ req.headers["accept-language"] || ""
85
+ );
86
+ const {
87
+ baseBotScore,
88
+ timestamp,
89
+ providerSelectEntropy,
90
+ userId,
91
+ userAgent,
92
+ webView,
93
+ iFrame,
94
+ decryptedHeadHash
95
+ } = await tasks$1.frictionlessManager.decryptPayload(token, headHash);
96
+ req.logger.debug(() => ({
97
+ msg: "Decrypted payload",
98
+ data: {
99
+ baseBotScore,
100
+ timestamp,
101
+ providerSelectEntropy,
102
+ userId,
103
+ userAgent,
104
+ webView
105
+ }
106
+ }));
107
+ let botScore = baseBotScore + lScore;
108
+ const clientRecord = await tasks$1.db.getClientRecord(dapp);
109
+ if (!clientRecord) {
110
+ return next(
111
+ new common.ProsopoApiError("API.SITE_KEY_NOT_REGISTERED", {
112
+ context: { code: 400, siteKey: dapp },
113
+ i18n: req.i18n,
114
+ logger: req.logger
115
+ })
116
+ );
117
+ }
118
+ const { valid, reason } = await tasks$1.frictionlessManager.isValidRequest(
119
+ clientRecord,
120
+ types.CaptchaType.frictionless,
121
+ env
122
+ );
123
+ if (!valid) {
124
+ return next(
125
+ new common.ProsopoApiError(reason || "API.BAD_REQUEST", {
126
+ context: {
127
+ code: 400,
128
+ siteKey: dapp,
129
+ user
130
+ },
131
+ i18n: req.i18n,
132
+ logger: req.logger
133
+ })
134
+ );
135
+ }
136
+ const botThreshold = clientRecord.settings?.frictionlessThreshold || DEFAULT_FRICTIONLESS_THRESHOLD;
137
+ let scoreComponents = {
138
+ baseScore: baseBotScore,
139
+ ...lScore && { lScore }
140
+ };
141
+ const ipAddress = compositeIpAddress.getCompositeIpAddress(req.ip || "");
142
+ tasks$1.frictionlessManager.setSessionParams({
143
+ token,
144
+ score: botScore,
145
+ threshold: botThreshold,
146
+ scoreComponents,
147
+ providerSelectEntropy,
148
+ ipAddress,
149
+ webView,
150
+ iFrame,
151
+ decryptedHeadHash
152
+ });
153
+ const userScope = blacklistRequestInspector.getRequestUserScope(
154
+ util.flatten(req.headers),
155
+ req.ja4,
156
+ req.ip,
157
+ user
158
+ );
159
+ const userAccessPolicy = (await tasks$1.frictionlessManager.getPrioritisedAccessPolicies(
160
+ userAccessRulesStorage,
161
+ dapp,
162
+ userScope
163
+ ))[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
+ if (userAccessPolicy) {
188
+ const scoreUpdate = tasks$1.frictionlessManager.scoreIncreaseAccessPolicy(
189
+ userAccessPolicy,
190
+ baseBotScore,
191
+ botScore,
192
+ scoreComponents
193
+ );
194
+ botScore = scoreUpdate.score;
195
+ scoreComponents = scoreUpdate.scoreComponents;
196
+ tasks$1.frictionlessManager.updateScore(botScore, scoreComponents);
197
+ if (userAccessPolicy.captchaType === types.CaptchaType.image) {
198
+ return res.json(
199
+ await tasks$1.frictionlessManager.sendImageCaptcha({
200
+ solvedImagesCount: userAccessPolicy.solvedImagesCount,
201
+ userSitekeyIpHash,
202
+ reason: frictionlessTasks.FrictionlessReason.USER_ACCESS_POLICY
203
+ })
204
+ );
205
+ }
206
+ if (userAccessPolicy.captchaType === types.CaptchaType.pow) {
207
+ return res.json(
208
+ await tasks$1.frictionlessManager.sendPowCaptcha({
209
+ userSitekeyIpHash,
210
+ reason: frictionlessTasks.FrictionlessReason.USER_ACCESS_POLICY
211
+ })
212
+ );
213
+ }
214
+ }
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
+ );
235
+ }
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
+ })
245
+ );
246
+ }
247
+ }
248
+ }
249
+ if (clientRecord.settings.disallowWebView && webView) {
250
+ tasks$1.logger.info(() => ({
251
+ msg: "WebView detected"
252
+ }));
253
+ const scoreUpdate = tasks$1.frictionlessManager.scoreIncreaseWebView(
254
+ baseBotScore,
255
+ botScore,
256
+ scoreComponents
257
+ );
258
+ botScore = scoreUpdate.score;
259
+ scoreComponents = scoreUpdate.scoreComponents;
260
+ tasks$1.frictionlessManager.updateScore(botScore, scoreComponents);
261
+ return res.json(
262
+ await tasks$1.frictionlessManager.sendImageCaptcha({
263
+ solvedImagesCount: env.config.captchas.solved.count * 2,
264
+ userSitekeyIpHash,
265
+ reason: frictionlessTasks.FrictionlessReason.WEBVIEW_DETECTED
266
+ })
267
+ );
268
+ }
269
+ if (frictionlessTasks.FrictionlessManager.timestampTooOld(timestamp)) {
270
+ const scoreUpdate = tasks$1.frictionlessManager.scoreIncreaseTimestamp(
271
+ timestamp,
272
+ baseBotScore,
273
+ botScore,
274
+ scoreComponents
275
+ );
276
+ botScore = scoreUpdate.score;
277
+ scoreComponents = scoreUpdate.scoreComponents;
278
+ tasks$1.frictionlessManager.updateScore(botScore, scoreComponents);
279
+ return res.json(
280
+ await tasks$1.frictionlessManager.sendImageCaptcha({
281
+ solvedImagesCount: frictionlessTasksUtils.timestampDecayFunction(timestamp),
282
+ userSitekeyIpHash,
283
+ reason: frictionlessTasks.FrictionlessReason.OLD_TIMESTAMP
284
+ })
285
+ );
286
+ }
287
+ const hostVerified = await tasks$1.frictionlessManager.hostVerified(
288
+ providerSelectEntropy
289
+ );
290
+ if (!hostVerified.verified) {
291
+ const scoreUpdate = tasks$1.frictionlessManager.scoreIncreaseUnverifiedHost(
292
+ hostVerified.domain,
293
+ baseBotScore,
294
+ botScore,
295
+ scoreComponents
296
+ );
297
+ botScore = scoreUpdate.score;
298
+ scoreComponents = scoreUpdate.scoreComponents;
299
+ tasks$1.frictionlessManager.updateScore(botScore, scoreComponents);
300
+ }
301
+ if (Number(botScore) > botThreshold) {
302
+ req.logger.info(() => ({
303
+ msg: "Bot score is greater than threshold",
304
+ data: {
305
+ botScore,
306
+ botThreshold,
307
+ token
308
+ }
309
+ }));
310
+ return res.json(
311
+ await tasks$1.frictionlessManager.sendImageCaptcha({
312
+ solvedImagesCount: env.config.captchas.solved.count,
313
+ userSitekeyIpHash,
314
+ reason: frictionlessTasks.FrictionlessReason.BOT_SCORE_ABOVE_THRESHOLD
315
+ })
316
+ );
317
+ }
318
+ return res.json(
319
+ await tasks$1.frictionlessManager.sendPowCaptcha({
320
+ userSitekeyIpHash
321
+ })
322
+ );
323
+ } catch (err) {
324
+ req.logger.error(() => ({
325
+ err,
326
+ msg: "Error in frictionless captcha challenge"
327
+ }));
328
+ return next(
329
+ new common.ProsopoApiError("API.BAD_REQUEST", {
330
+ context: { code: 400, error: err },
331
+ i18n: req.i18n,
332
+ logger: req.logger
333
+ })
334
+ );
335
+ }
336
+ };
337
+ module.exports = getFrictionlessCaptchaChallenge;
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ const common = require("@prosopo/common");
3
+ const datasets = require("@prosopo/datasets");
4
+ const types = require("@prosopo/types");
5
+ const util = require("@prosopo/util");
6
+ require("../../tasks/index.cjs");
7
+ const blacklistRequestInspector = require("../blacklistRequestInspector.cjs");
8
+ const validateAddress = require("../validateAddress.cjs");
9
+ const tasks = require("../../tasks/tasks.cjs");
10
+ const getImageCaptchaChallenge = (env, userAccessRulesStorage) => async (req, res, next) => {
11
+ const tasks$1 = new tasks.Tasks(env, req.logger);
12
+ let parsed;
13
+ if (!req.ip) {
14
+ return next(
15
+ new common.ProsopoApiError("API.BAD_REQUEST", {
16
+ context: { code: 400, error: "IP address not found" },
17
+ i18n: req.i18n,
18
+ logger: req.logger
19
+ })
20
+ );
21
+ }
22
+ const ipAddress = util.getIPAddress(req.ip || "");
23
+ try {
24
+ parsed = types.CaptchaRequestBody.parse(req.body);
25
+ } catch (err) {
26
+ return next(
27
+ new common.ProsopoApiError("CAPTCHA.PARSE_ERROR", {
28
+ context: { code: 400, error: err },
29
+ i18n: req.i18n,
30
+ logger: req.logger
31
+ })
32
+ );
33
+ }
34
+ const { datasetId, user, dapp, sessionId } = parsed;
35
+ validateAddress.validateSiteKey(dapp);
36
+ validateAddress.validateAddr(user);
37
+ try {
38
+ const clientRecord = await tasks$1.db.getClientRecord(dapp);
39
+ if (!clientRecord) {
40
+ return next(
41
+ new common.ProsopoApiError("API.SITE_KEY_NOT_REGISTERED", {
42
+ context: { code: 400, siteKey: dapp },
43
+ i18n: req.i18n,
44
+ logger: req.logger
45
+ })
46
+ );
47
+ }
48
+ const userScope = blacklistRequestInspector.getRequestUserScope(
49
+ util.flatten(req.headers),
50
+ req.ja4,
51
+ req.ip,
52
+ user
53
+ );
54
+ const userAccessPolicy = (await tasks$1.imgCaptchaManager.getPrioritisedAccessPolicies(
55
+ userAccessRulesStorage,
56
+ dapp,
57
+ userScope
58
+ ))[0];
59
+ const {
60
+ valid,
61
+ reason,
62
+ sessionId: validSessionId,
63
+ solvedImagesCount
64
+ } = await tasks$1.imgCaptchaManager.isValidRequest(
65
+ clientRecord,
66
+ types.CaptchaType.image,
67
+ env,
68
+ sessionId,
69
+ userAccessPolicy,
70
+ req.ip
71
+ );
72
+ if (!valid) {
73
+ return next(
74
+ new common.ProsopoApiError(reason || "API.BAD_REQUEST", {
75
+ context: {
76
+ code: 400,
77
+ siteKey: dapp,
78
+ user
79
+ },
80
+ i18n: req.i18n,
81
+ logger: req.logger
82
+ })
83
+ );
84
+ }
85
+ const captchaConfig = {
86
+ solved: {
87
+ count: solvedImagesCount || userAccessPolicy?.solvedImagesCount || env.config.captchas.solved.count
88
+ },
89
+ unsolved: {
90
+ count: userAccessPolicy?.unsolvedImagesCount || env.config.captchas.unsolved.count
91
+ }
92
+ };
93
+ const taskData = await tasks$1.imgCaptchaManager.getRandomCaptchasAndRequestHash(
94
+ datasetId,
95
+ user,
96
+ ipAddress,
97
+ captchaConfig,
98
+ clientRecord.settings.imageThreshold ?? 0.8,
99
+ validSessionId
100
+ );
101
+ const captchaResponse = {
102
+ [types.ApiParams.status]: "ok",
103
+ [types.ApiParams.captchas]: taskData.captchas.map((captcha) => ({
104
+ ...captcha,
105
+ target: req.t(`TARGET.${captcha.target}`),
106
+ items: captcha.items.map(
107
+ (item) => datasets.parseCaptchaAssets(item, env.assetsResolver)
108
+ )
109
+ })),
110
+ [types.ApiParams.requestHash]: taskData.requestHash,
111
+ [types.ApiParams.timestamp]: taskData.timestamp.toString(),
112
+ [types.ApiParams.signature]: {
113
+ [types.ApiParams.provider]: {
114
+ [types.ApiParams.requestHash]: taskData.signedRequestHash
115
+ }
116
+ }
117
+ };
118
+ req.logger.info(() => ({
119
+ msg: "Image captcha challenge issued",
120
+ data: {
121
+ captchaType: types.CaptchaType.image,
122
+ requestHash: taskData.requestHash,
123
+ solvedImagesCount: captchaConfig.solved.count,
124
+ user,
125
+ dapp,
126
+ sessionId
127
+ }
128
+ }));
129
+ return res.json(captchaResponse);
130
+ } catch (err) {
131
+ req.logger.error(() => ({
132
+ err,
133
+ data: req.params,
134
+ msg: "Error in image captcha challenge request"
135
+ }));
136
+ return next(
137
+ new common.ProsopoApiError("API.BAD_REQUEST", {
138
+ context: {
139
+ error: err,
140
+ code: 500,
141
+ params: req.params
142
+ },
143
+ i18n: req.i18n,
144
+ logger: req.logger
145
+ })
146
+ );
147
+ }
148
+ };
149
+ module.exports = getImageCaptchaChallenge;