@prosopo/provider 2.5.5 → 2.7.1

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 (74) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/dist/api/captcha.d.ts.map +1 -1
  3. package/dist/api/captcha.js +1 -1
  4. package/dist/api/captcha.js.map +1 -1
  5. package/dist/api/ja4Middleware.d.ts +6 -0
  6. package/dist/api/ja4Middleware.d.ts.map +1 -1
  7. package/dist/api/ja4Middleware.js +7 -7
  8. package/dist/api/ja4Middleware.js.map +1 -1
  9. package/dist/cjs/api/admin/apiAdminRoutesProvider.cjs +32 -0
  10. package/dist/cjs/api/admin/apiRegisterSiteKeyEndpoint.cjs +25 -0
  11. package/dist/cjs/api/admin/apiRemoveDetectorKeyEndpoint.cjs +32 -0
  12. package/dist/cjs/api/admin/apiUpdateDetectorKeyEndpoint.cjs +32 -0
  13. package/dist/cjs/api/admin/createApiAdminRoutesProvider.cjs +10 -0
  14. package/dist/cjs/api/authMiddleware.cjs +81 -0
  15. package/dist/cjs/api/blacklistRequestInspector.cjs +73 -0
  16. package/dist/cjs/api/block.cjs +24 -0
  17. package/dist/cjs/api/captcha.cjs +482 -0
  18. package/dist/cjs/api/domainMiddleware.cjs +89 -0
  19. package/dist/cjs/api/headerCheckMiddleware.cjs +29 -0
  20. package/dist/cjs/api/ignoreMiddleware.cjs +14 -0
  21. package/dist/cjs/api/ja4Middleware.cjs +73 -0
  22. package/dist/cjs/api/public.cjs +27 -0
  23. package/dist/cjs/api/requestLoggerMiddleware.cjs +14 -0
  24. package/dist/cjs/api/robotsMiddleware.cjs +12 -0
  25. package/dist/cjs/api/validateAddress.cjs +19 -0
  26. package/dist/cjs/api/verify.cjs +138 -0
  27. package/dist/cjs/index.cjs +41 -0
  28. package/dist/cjs/rules/lang.cjs +16 -0
  29. package/dist/cjs/schedulers/captchaScheduler.cjs +31 -0
  30. package/dist/cjs/schedulers/getClientList.cjs +29 -0
  31. package/dist/cjs/tasks/captchaManager.cjs +90 -0
  32. package/dist/cjs/tasks/client/clientTasks.cjs +281 -0
  33. package/dist/cjs/tasks/dataset/datasetTasks.cjs +30 -0
  34. package/dist/cjs/tasks/dataset/datasetTasksUtils.cjs +34 -0
  35. package/dist/cjs/tasks/detection/decodePayload.cjs +475 -0
  36. package/dist/cjs/tasks/detection/getBotScore.cjs +13 -0
  37. package/dist/cjs/tasks/frictionless/frictionlessTasks.cjs +121 -0
  38. package/dist/cjs/tasks/frictionless/frictionlessTasksUtils.cjs +11 -0
  39. package/dist/cjs/tasks/imgCaptcha/imgCaptchaTasks.cjs +366 -0
  40. package/dist/cjs/tasks/imgCaptcha/imgCaptchaTasksUtils.cjs +25 -0
  41. package/dist/cjs/tasks/index.cjs +4 -0
  42. package/dist/cjs/tasks/powCaptcha/powTasks.cjs +155 -0
  43. package/dist/cjs/tasks/powCaptcha/powTasksUtils.cjs +26 -0
  44. package/dist/cjs/tasks/tasks.cjs +51 -0
  45. package/dist/cjs/util.cjs +58 -0
  46. package/dist/schedulers/captchaScheduler.d.ts +1 -1
  47. package/dist/schedulers/captchaScheduler.d.ts.map +1 -1
  48. package/dist/schedulers/captchaScheduler.js +1 -7
  49. package/dist/schedulers/captchaScheduler.js.map +1 -1
  50. package/dist/schedulers/getClientList.d.ts +1 -1
  51. package/dist/schedulers/getClientList.d.ts.map +1 -1
  52. package/dist/schedulers/getClientList.js +1 -7
  53. package/dist/schedulers/getClientList.js.map +1 -1
  54. package/dist/tasks/client/clientTasks.d.ts +5 -2
  55. package/dist/tasks/client/clientTasks.d.ts.map +1 -1
  56. package/dist/tasks/client/clientTasks.js +55 -9
  57. package/dist/tasks/client/clientTasks.js.map +1 -1
  58. package/dist/tasks/imgCaptcha/imgCaptchaTasks.d.ts +1 -1
  59. package/dist/tasks/imgCaptcha/imgCaptchaTasks.d.ts.map +1 -1
  60. package/dist/tasks/imgCaptcha/imgCaptchaTasks.js +13 -3
  61. package/dist/tasks/imgCaptcha/imgCaptchaTasks.js.map +1 -1
  62. package/dist/tests/unit/api/ja4Middleware.unit.test.d.ts +2 -0
  63. package/dist/tests/unit/api/ja4Middleware.unit.test.d.ts.map +1 -0
  64. package/dist/tests/unit/api/ja4Middleware.unit.test.js +57 -0
  65. package/dist/tests/unit/api/ja4Middleware.unit.test.js.map +1 -0
  66. package/dist/tests/unit/schedulers/captchaScheduler.unit.test.js +2 -2
  67. package/dist/tests/unit/schedulers/captchaScheduler.unit.test.js.map +1 -1
  68. package/dist/tests/unit/tasks/captchaManager.unit.test.js +1 -0
  69. package/dist/tests/unit/tasks/captchaManager.unit.test.js.map +1 -1
  70. package/dist/tests/unit/tasks/client/clientTasks.unit.test.js +11 -0
  71. package/dist/tests/unit/tasks/client/clientTasks.unit.test.js.map +1 -1
  72. package/dist/tests/unit/tasks/imgCaptcha/imgCaptchaTasks.unit.test.js +2 -2
  73. package/dist/tests/unit/tasks/imgCaptcha/imgCaptchaTasks.unit.test.js.map +1 -1
  74. package/package.json +16 -15
@@ -0,0 +1,482 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const apiExpressRouter = require("@prosopo/api-express-router");
4
+ const common = require("@prosopo/common");
5
+ const datasets = require("@prosopo/datasets");
6
+ const types = require("@prosopo/types");
7
+ const userAccessPolicy = require("@prosopo/user-access-policy");
8
+ const util$1 = require("@prosopo/util");
9
+ const express = require("express");
10
+ const frictionlessTasks = require("../tasks/frictionless/frictionlessTasks.cjs");
11
+ const tasks = require("../tasks/tasks.cjs");
12
+ const util = require("../util.cjs");
13
+ const validateAddress = require("./validateAddress.cjs");
14
+ const DEFAULT_FRICTIONLESS_THRESHOLD = 0.5;
15
+ function prosopoRouter(env) {
16
+ const router = express.Router();
17
+ const tasks$1 = new tasks.Tasks(env);
18
+ const userAccessRulesStorage = env.getDb().getUserAccessRulesStorage();
19
+ router.post(
20
+ types.ClientApiPaths.GetImageCaptchaChallenge,
21
+ async (req, res, next) => {
22
+ let parsed;
23
+ if (!req.ip) {
24
+ return next(
25
+ new common.ProsopoApiError("API.BAD_REQUEST", {
26
+ context: { code: 400, error: "IP address not found" },
27
+ i18n: req.i18n,
28
+ logger: req.logger
29
+ })
30
+ );
31
+ }
32
+ const ipAddress = util.getIPAddress(req.ip || "");
33
+ try {
34
+ parsed = types.CaptchaRequestBody.parse(req.body);
35
+ } catch (err) {
36
+ return next(
37
+ new common.ProsopoApiError("CAPTCHA.PARSE_ERROR", {
38
+ context: { code: 400, error: err },
39
+ i18n: req.i18n,
40
+ logger: req.logger
41
+ })
42
+ );
43
+ }
44
+ const { datasetId, user, dapp, sessionId } = parsed;
45
+ validateAddress.validiateSiteKey(dapp);
46
+ validateAddress.validateAddr(user);
47
+ try {
48
+ const clientRecord = await tasks$1.db.getClientRecord(dapp);
49
+ if (!clientRecord) {
50
+ return next(
51
+ new common.ProsopoApiError("API.SITE_KEY_NOT_REGISTERED", {
52
+ context: { code: 400, siteKey: dapp },
53
+ i18n: req.i18n,
54
+ logger: req.logger
55
+ })
56
+ );
57
+ }
58
+ const imageCaptchaConfigResolver = userAccessPolicy.createImageCaptchaConfigResolver(
59
+ userAccessRulesStorage,
60
+ req.logger
61
+ );
62
+ const captchaConfig = await imageCaptchaConfigResolver.resolveConfig(
63
+ env.config.captchas,
64
+ ipAddress,
65
+ req.ja4,
66
+ user,
67
+ dapp
68
+ );
69
+ const { valid, reason, frictionlessTokenId } = await tasks$1.imgCaptchaManager.isValidRequest(
70
+ clientRecord,
71
+ types.CaptchaType.image,
72
+ sessionId
73
+ );
74
+ if (!valid) {
75
+ return next(
76
+ new common.ProsopoApiError(reason || "API.BAD_REQUEST", {
77
+ context: {
78
+ code: 400,
79
+ siteKey: dapp,
80
+ user
81
+ },
82
+ i18n: req.i18n,
83
+ logger: req.logger
84
+ })
85
+ );
86
+ }
87
+ const taskData = await tasks$1.imgCaptchaManager.getRandomCaptchasAndRequestHash(
88
+ datasetId,
89
+ user,
90
+ ipAddress,
91
+ captchaConfig,
92
+ clientRecord.settings.imageThreshold,
93
+ frictionlessTokenId
94
+ );
95
+ const captchaResponse = {
96
+ [types.ApiParams.status]: "ok",
97
+ [types.ApiParams.captchas]: taskData.captchas.map((captcha) => ({
98
+ ...captcha,
99
+ target: req.t(`TARGET.${captcha.target}`),
100
+ items: captcha.items.map(
101
+ (item) => datasets.parseCaptchaAssets(item, env.assetsResolver)
102
+ )
103
+ })),
104
+ [types.ApiParams.requestHash]: taskData.requestHash,
105
+ [types.ApiParams.timestamp]: taskData.timestamp.toString(),
106
+ [types.ApiParams.signature]: {
107
+ [types.ApiParams.provider]: {
108
+ [types.ApiParams.requestHash]: taskData.signedRequestHash
109
+ }
110
+ }
111
+ };
112
+ return res.json(captchaResponse);
113
+ } catch (err) {
114
+ req.logger.error({ err, params: req.params });
115
+ return next(
116
+ new common.ProsopoApiError("API.BAD_REQUEST", {
117
+ context: {
118
+ error: err,
119
+ code: 500,
120
+ params: req.params,
121
+ context: err
122
+ },
123
+ i18n: req.i18n,
124
+ logger: req.logger
125
+ })
126
+ );
127
+ }
128
+ }
129
+ );
130
+ router.post(
131
+ types.ClientApiPaths.SubmitImageCaptchaSolution,
132
+ async (req, res, next) => {
133
+ let parsed;
134
+ try {
135
+ parsed = types.CaptchaSolutionBody.parse(req.body);
136
+ } catch (err) {
137
+ return next(
138
+ new common.ProsopoApiError("CAPTCHA.PARSE_ERROR", {
139
+ context: { code: 400, error: err, body: req.body },
140
+ i18n: req.i18n,
141
+ logger: req.logger
142
+ })
143
+ );
144
+ }
145
+ const { user, dapp } = parsed;
146
+ validateAddress.validiateSiteKey(dapp);
147
+ validateAddress.validateAddr(user);
148
+ try {
149
+ const clientRecord = await tasks$1.db.getClientRecord(parsed.dapp);
150
+ if (!clientRecord) {
151
+ return next(
152
+ new common.ProsopoApiError("API.SITE_KEY_NOT_REGISTERED", {
153
+ context: { code: 400, siteKey: dapp },
154
+ i18n: req.i18n,
155
+ logger: req.logger
156
+ })
157
+ );
158
+ }
159
+ const result = await tasks$1.imgCaptchaManager.dappUserSolution(
160
+ user,
161
+ dapp,
162
+ parsed[types.ApiParams.requestHash],
163
+ parsed[types.ApiParams.captchas],
164
+ parsed[types.ApiParams.signature].user.timestamp,
165
+ Number.parseInt(parsed[types.ApiParams.timestamp]),
166
+ parsed[types.ApiParams.signature].provider.requestHash,
167
+ util.getIPAddress(req.ip || "").bigInt(),
168
+ util$1.flatten(req.headers),
169
+ req.ja4
170
+ );
171
+ const returnValue = {
172
+ status: req.i18n.t(
173
+ result.verified ? "API.CAPTCHA_PASSED" : "API.CAPTCHA_FAILED"
174
+ ),
175
+ ...result
176
+ };
177
+ return res.json(returnValue);
178
+ } catch (err) {
179
+ req.logger.error({ err, body: req.body });
180
+ return next(
181
+ new common.ProsopoApiError("API.BAD_REQUEST", {
182
+ context: {
183
+ code: 500,
184
+ siteKey: req.body.dapp,
185
+ error: err
186
+ },
187
+ i18n: req.i18n,
188
+ logger: req.logger
189
+ })
190
+ );
191
+ }
192
+ }
193
+ );
194
+ router.post(types.ClientApiPaths.GetPowCaptchaChallenge, async (req, res, next) => {
195
+ let parsed;
196
+ try {
197
+ parsed = types.GetPowCaptchaChallengeRequestBody.parse(req.body);
198
+ } catch (err) {
199
+ return next(
200
+ new common.ProsopoApiError("CAPTCHA.PARSE_ERROR", {
201
+ context: { code: 400, error: err },
202
+ i18n: req.i18n,
203
+ logger: req.logger
204
+ })
205
+ );
206
+ }
207
+ const { user, dapp, sessionId } = parsed;
208
+ validateAddress.validiateSiteKey(dapp);
209
+ validateAddress.validateAddr(user);
210
+ try {
211
+ const clientSettings = await tasks$1.db.getClientRecord(dapp);
212
+ if (!clientSettings) {
213
+ return next(
214
+ new common.ProsopoApiError("API.SITE_KEY_NOT_REGISTERED", {
215
+ context: { code: 400, siteKey: dapp },
216
+ i18n: req.i18n,
217
+ logger: req.logger
218
+ })
219
+ );
220
+ }
221
+ const { valid, reason, frictionlessTokenId } = await tasks$1.powCaptchaManager.isValidRequest(
222
+ clientSettings,
223
+ types.CaptchaType.pow,
224
+ sessionId
225
+ );
226
+ if (!valid) {
227
+ return next(
228
+ new common.ProsopoApiError(reason || "API.BAD_REQUEST", {
229
+ context: {
230
+ code: 400,
231
+ siteKey: dapp,
232
+ user
233
+ },
234
+ i18n: req.i18n,
235
+ logger: req.logger
236
+ })
237
+ );
238
+ }
239
+ const origin = req.headers.origin;
240
+ if (!origin) {
241
+ return next(
242
+ new common.ProsopoApiError("API.BAD_REQUEST", {
243
+ context: {
244
+ error: "Origin header not found",
245
+ code: 400,
246
+ siteKey: dapp,
247
+ user
248
+ },
249
+ i18n: req.i18n,
250
+ logger: req.logger
251
+ })
252
+ );
253
+ }
254
+ const challenge = await tasks$1.powCaptchaManager.getPowCaptchaChallenge(
255
+ user,
256
+ dapp,
257
+ origin,
258
+ clientSettings?.settings?.powDifficulty
259
+ );
260
+ await tasks$1.db.storePowCaptchaRecord(
261
+ challenge.challenge,
262
+ {
263
+ requestedAtTimestamp: challenge.requestedAtTimestamp,
264
+ userAccount: user,
265
+ dappAccount: dapp
266
+ },
267
+ challenge.difficulty,
268
+ challenge.providerSignature,
269
+ util.getIPAddress(req.ip || "").bigInt(),
270
+ util$1.flatten(req.headers),
271
+ req.ja4,
272
+ frictionlessTokenId
273
+ );
274
+ const getPowCaptchaResponse = {
275
+ [types.ApiParams.status]: "ok",
276
+ [types.ApiParams.challenge]: challenge.challenge,
277
+ [types.ApiParams.difficulty]: challenge.difficulty,
278
+ [types.ApiParams.timestamp]: challenge.requestedAtTimestamp.toString(),
279
+ [types.ApiParams.signature]: {
280
+ [types.ApiParams.provider]: {
281
+ [types.ApiParams.challenge]: challenge.providerSignature
282
+ }
283
+ }
284
+ };
285
+ return res.json(getPowCaptchaResponse);
286
+ } catch (err) {
287
+ req.logger.error({ err, body: req.body });
288
+ return next(
289
+ new common.ProsopoApiError("API.BAD_REQUEST", {
290
+ context: {
291
+ code: 500,
292
+ siteKey: req.body.dapp,
293
+ user: req.body.user,
294
+ error: err
295
+ },
296
+ i18n: req.i18n,
297
+ logger: req.logger
298
+ })
299
+ );
300
+ }
301
+ });
302
+ router.post(
303
+ types.ClientApiPaths.SubmitPowCaptchaSolution,
304
+ async (req, res, next) => {
305
+ let parsed;
306
+ try {
307
+ parsed = types.SubmitPowCaptchaSolutionBody.parse(req.body);
308
+ } catch (err) {
309
+ return next(
310
+ new common.ProsopoApiError("CAPTCHA.PARSE_ERROR", {
311
+ context: { code: 400, error: err, body: req.body },
312
+ i18n: req.i18n,
313
+ logger: req.logger
314
+ })
315
+ );
316
+ }
317
+ const {
318
+ challenge,
319
+ difficulty,
320
+ signature,
321
+ nonce,
322
+ verifiedTimeout,
323
+ dapp,
324
+ user
325
+ } = parsed;
326
+ validateAddress.validiateSiteKey(dapp);
327
+ validateAddress.validateAddr(user);
328
+ try {
329
+ const clientRecord = await tasks$1.db.getClientRecord(dapp);
330
+ if (!clientRecord) {
331
+ return next(
332
+ new common.ProsopoApiError("API.SITE_KEY_NOT_REGISTERED", {
333
+ context: { code: 400, siteKey: dapp },
334
+ i18n: req.i18n,
335
+ logger: req.logger
336
+ })
337
+ );
338
+ }
339
+ const verified = await tasks$1.powCaptchaManager.verifyPowCaptchaSolution(
340
+ challenge,
341
+ difficulty,
342
+ signature.provider.challenge,
343
+ nonce,
344
+ verifiedTimeout,
345
+ signature.user.timestamp,
346
+ util.getIPAddress(req.ip || ""),
347
+ util$1.flatten(req.headers)
348
+ );
349
+ const response = { status: "ok", verified };
350
+ return res.json(response);
351
+ } catch (err) {
352
+ req.logger.error({ err, body: req.body });
353
+ return next(
354
+ new common.ProsopoApiError("API.BAD_REQUEST", {
355
+ context: {
356
+ code: 500,
357
+ siteKey: req.body.dapp,
358
+ error: err
359
+ },
360
+ i18n: req.i18n,
361
+ logger: req.logger
362
+ })
363
+ );
364
+ }
365
+ }
366
+ );
367
+ router.post(
368
+ types.ClientApiPaths.GetFrictionlessCaptchaChallenge,
369
+ async (req, res, next) => {
370
+ try {
371
+ const { token, dapp, user } = types.GetFrictionlessCaptchaChallengeRequestBody.parse(req.body);
372
+ const existingToken = await tasks$1.db.getFrictionlessTokenRecordByToken(token);
373
+ if (existingToken) {
374
+ req.logger.info(`Token ${existingToken} has already been used`);
375
+ return res.json(
376
+ await tasks$1.frictionlessManager.sendImageCaptcha(
377
+ existingToken._id
378
+ )
379
+ );
380
+ }
381
+ const lScore = tasks$1.frictionlessManager.checkLangRules(
382
+ req.headers["accept-language"] || ""
383
+ );
384
+ const { baseBotScore, timestamp } = await tasks$1.frictionlessManager.decryptPayload(token);
385
+ const botScore = baseBotScore + lScore;
386
+ const clientRecord = await tasks$1.db.getClientRecord(dapp);
387
+ if (!clientRecord) {
388
+ return next(
389
+ new common.ProsopoApiError("API.SITE_KEY_NOT_REGISTERED", {
390
+ context: { code: 400, siteKey: dapp },
391
+ i18n: req.i18n,
392
+ logger: req.logger
393
+ })
394
+ );
395
+ }
396
+ const { valid, reason } = await tasks$1.frictionlessManager.isValidRequest(
397
+ clientRecord,
398
+ types.CaptchaType.frictionless
399
+ );
400
+ if (!valid) {
401
+ return next(
402
+ new common.ProsopoApiError(reason || "API.BAD_REQUEST", {
403
+ context: {
404
+ code: 400,
405
+ siteKey: dapp,
406
+ user
407
+ },
408
+ i18n: req.i18n,
409
+ logger: req.logger
410
+ })
411
+ );
412
+ }
413
+ const botThreshold = clientRecord.settings?.frictionlessThreshold || DEFAULT_FRICTIONLESS_THRESHOLD;
414
+ const tokenId = await tasks$1.db.storeFrictionlessTokenRecord({
415
+ token,
416
+ score: botScore,
417
+ threshold: botThreshold,
418
+ scoreComponents: {
419
+ baseScore: baseBotScore,
420
+ ...lScore && { lScore }
421
+ }
422
+ });
423
+ if (frictionlessTasks.FrictionlessManager.timestampTooOld(timestamp)) {
424
+ await tasks$1.frictionlessManager.scoreIncreaseTimestamp(
425
+ timestamp,
426
+ baseBotScore,
427
+ botScore,
428
+ tokenId
429
+ );
430
+ return res.json(
431
+ await tasks$1.frictionlessManager.sendImageCaptcha(tokenId)
432
+ );
433
+ }
434
+ const ipAddress = util.getIPAddress(req.ip || "");
435
+ const imageCaptchaConfigResolver = userAccessPolicy.createImageCaptchaConfigResolver(
436
+ userAccessRulesStorage,
437
+ req.logger
438
+ );
439
+ const imageCaptchaConfigDefined = await imageCaptchaConfigResolver.isConfigDefined(
440
+ dapp,
441
+ ipAddress,
442
+ req.ja4,
443
+ user
444
+ );
445
+ if (imageCaptchaConfigDefined) {
446
+ await tasks$1.frictionlessManager.scoreIncreaseAccessPolicy(
447
+ imageCaptchaConfigResolver.accessRule,
448
+ baseBotScore,
449
+ botScore,
450
+ tokenId
451
+ );
452
+ return res.json(
453
+ await tasks$1.frictionlessManager.sendImageCaptcha(tokenId)
454
+ );
455
+ }
456
+ if (Number(botScore) > botThreshold) {
457
+ req.logger.info({
458
+ message: `Bot score ${botScore} is greater than threshold ${botThreshold}`
459
+ });
460
+ return res.json(
461
+ await tasks$1.frictionlessManager.sendImageCaptcha(tokenId)
462
+ );
463
+ }
464
+ return res.json(
465
+ await tasks$1.frictionlessManager.sendPowCaptcha(tokenId)
466
+ );
467
+ } catch (err) {
468
+ req.logger.error("Error in frictionless captcha challenge:", err);
469
+ return next(
470
+ new common.ProsopoApiError("API.BAD_REQUEST", {
471
+ context: { code: 400, error: err },
472
+ i18n: req.i18n,
473
+ logger: req.logger
474
+ })
475
+ );
476
+ }
477
+ }
478
+ );
479
+ router.use(apiExpressRouter.handleErrors);
480
+ return router;
481
+ }
482
+ exports.prosopoRouter = prosopoRouter;
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const utilCrypto = require("@polkadot/util-crypto");
4
+ const apiExpressRouter = require("@prosopo/api-express-router");
5
+ const common = require("@prosopo/common");
6
+ const zod = require("zod");
7
+ require("../tasks/index.cjs");
8
+ const tasks = require("../tasks/tasks.cjs");
9
+ const domainMiddleware = (env) => {
10
+ const tasks$1 = new tasks.Tasks(env);
11
+ return async (req, res, next) => {
12
+ try {
13
+ const dapp = req.headers["prosopo-site-key"];
14
+ if (!dapp)
15
+ throw siteKeyNotRegisteredError(
16
+ req.i18n,
17
+ "No sitekey provided",
18
+ req.logger
19
+ );
20
+ try {
21
+ utilCrypto.validateAddress(dapp, false, 42);
22
+ } catch (err) {
23
+ throw invalidSiteKeyError(req.i18n, dapp, req.logger);
24
+ }
25
+ const clientSettings = await tasks$1.db.getClientRecord(dapp);
26
+ if (!clientSettings)
27
+ throw siteKeyNotRegisteredError(req.i18n, dapp, req.logger);
28
+ const allowedDomains = clientSettings.settings?.domains;
29
+ if (!allowedDomains)
30
+ throw siteKeyInvalidDomainError(
31
+ req.i18n,
32
+ dapp,
33
+ req.hostname,
34
+ req.logger
35
+ );
36
+ const origin = req.headers.origin;
37
+ if (!origin)
38
+ throw unauthorizedOriginError(req.i18n, void 0, req.logger);
39
+ for (const domain of allowedDomains) {
40
+ if (tasks$1.clientTaskManager.isSubdomainOrExactMatch(origin, domain)) {
41
+ next();
42
+ return;
43
+ }
44
+ }
45
+ throw unauthorizedOriginError(req.i18n, origin, req.logger);
46
+ } catch (err) {
47
+ if (err instanceof common.ProsopoApiError || err instanceof zod.ZodError || err instanceof SyntaxError) {
48
+ apiExpressRouter.handleErrors(err, req, res, next);
49
+ } else {
50
+ res.status(401).json({ error: "Unauthorized", message: err });
51
+ return;
52
+ }
53
+ }
54
+ };
55
+ };
56
+ const siteKeyNotRegisteredError = (i18n, dapp, logger) => {
57
+ return new common.ProsopoApiError("API.SITE_KEY_NOT_REGISTERED", {
58
+ context: { code: 400, siteKey: dapp },
59
+ i18n,
60
+ logger
61
+ });
62
+ };
63
+ const invalidSiteKeyError = (i18n, dapp, logger) => {
64
+ return new common.ProsopoApiError("API.INVALID_SITE_KEY", {
65
+ context: { code: 400, siteKey: dapp },
66
+ i18n,
67
+ logger
68
+ });
69
+ };
70
+ const unauthorizedOriginError = (i18n, origin, logger) => {
71
+ return new common.ProsopoApiError("API.UNAUTHORIZED_ORIGIN_URL", {
72
+ context: { code: 400, origin },
73
+ i18n,
74
+ logger
75
+ });
76
+ };
77
+ const siteKeyInvalidDomainError = (i18n, dapp, domain, logger) => {
78
+ return new common.ProsopoApiError("API.UNAUTHORIZED_ORIGIN_URL", {
79
+ context: {
80
+ code: 400,
81
+ message: "No domains are allowed for this site key. Please fix in the Procaptcha Portal",
82
+ siteKey: dapp,
83
+ domain
84
+ },
85
+ i18n,
86
+ logger
87
+ });
88
+ };
89
+ exports.domainMiddleware = domainMiddleware;
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const apiExpressRouter = require("@prosopo/api-express-router");
4
+ const validateAddress = require("./validateAddress.cjs");
5
+ const headerCheckMiddleware = (env) => {
6
+ return async (req, res, next) => {
7
+ try {
8
+ const user = req.headers["prosopo-user"];
9
+ const siteKey = req.headers["prosopo-site-key"];
10
+ if (!user) {
11
+ unauthorised(res);
12
+ return;
13
+ }
14
+ if (!siteKey) {
15
+ unauthorised(res);
16
+ return;
17
+ }
18
+ validateAddress.validiateSiteKey(siteKey, req.logger);
19
+ validateAddress.validateAddr(user, void 0, req.logger);
20
+ req.user = user;
21
+ req.siteKey = siteKey;
22
+ next();
23
+ } catch (err) {
24
+ return apiExpressRouter.handleErrors(err, req, res, next);
25
+ }
26
+ };
27
+ };
28
+ const unauthorised = (res) => res.status(401).json({ error: "Unauthorized", message: "Unauthorized" });
29
+ exports.headerCheckMiddleware = headerCheckMiddleware;
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const types = require("@prosopo/types");
4
+ function ignoreMiddleware() {
5
+ return (req, res, next) => {
6
+ if (req.originalUrl.indexOf(types.ApiPrefix) === -1) {
7
+ res.statusCode = 404;
8
+ res.send("Not Found");
9
+ return;
10
+ }
11
+ next();
12
+ };
13
+ }
14
+ exports.ignoreMiddleware = ignoreMiddleware;
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const node_crypto = require("node:crypto");
4
+ const node_stream = require("node:stream");
5
+ const apiExpressRouter = require("@prosopo/api-express-router");
6
+ const common = require("@prosopo/common");
7
+ const readTlsClientHello = require("read-tls-client-hello");
8
+ const DEFAULT_JA4 = "ja4";
9
+ const getJA4 = async (headers, logger) => {
10
+ logger = logger || common.getLoggerDefault();
11
+ if (process.env.NODE_ENV === "development") {
12
+ return { ja4PlusFingerprint: DEFAULT_JA4 };
13
+ }
14
+ try {
15
+ const xTlsClientHello = (headers["x-tls-clienthello"] || "").toString();
16
+ const xTlsVersion = (headers["x-tls-version"] || "").toString().toLowerCase();
17
+ const xTlsServerName = (headers["x-tls-server-name"] || "").toString();
18
+ const clientHelloBuffer = Buffer.from(xTlsClientHello, "base64");
19
+ logger.debug(
20
+ "ClientHello First Bytes:",
21
+ clientHelloBuffer.subarray(0, 5).toString("hex")
22
+ );
23
+ if (clientHelloBuffer[5] !== 1) {
24
+ logger.warn("Invalid ClientHello message: First byte is not 0x01");
25
+ return { ja4PlusFingerprint: DEFAULT_JA4 };
26
+ }
27
+ logger.debug("Headers TLS Version:", xTlsVersion);
28
+ const tlsVersion = xTlsVersion.replace(/(tls)|\./g, "");
29
+ const readableStream = new node_stream.Readable({
30
+ read() {
31
+ this.push(clientHelloBuffer);
32
+ }
33
+ });
34
+ const clientHello = await readTlsClientHello.readTlsClientHello(readableStream);
35
+ const { alpnProtocols } = clientHello;
36
+ const [_tlsVersion, cipherSuites, extensions] = clientHello.fingerprintData;
37
+ const transport = "t";
38
+ const sniIndicator = xTlsServerName ? "d" : "i";
39
+ const validCipherSuites = cipherSuites.filter(
40
+ (cs) => (cs & 3855) !== 2570
41
+ );
42
+ const cipherCount = validCipherSuites.length;
43
+ const validExtensions = extensions.filter(
44
+ (ext) => (ext & 3855) !== 2570
45
+ );
46
+ const extensionCount = validExtensions.length;
47
+ const alpn = alpnProtocols?.length ? alpnProtocols[0] : "";
48
+ const alpnLabel = alpn ? `${alpn[0]}${alpn[alpn.length - 1]}` : "00";
49
+ const sortedCiphers = validCipherSuites.map((cs) => cs.toString(16).padStart(4, "0")).sort().join(",");
50
+ const cipherHash = node_crypto.createHash("sha256").update(sortedCiphers).digest("hex").slice(0, 12);
51
+ const decimalString = extensions.sort((a, b) => a - b).map((ext) => ext.toString(10)).join("-");
52
+ const extensionHash = node_crypto.createHash("sha256").update(decimalString).digest("hex").slice(0, 12);
53
+ const ja4PlusFingerprint = `${transport}${tlsVersion}${sniIndicator}${cipherCount}${extensionCount}${alpnLabel}_${cipherHash}_${extensionHash}`;
54
+ return { ja4PlusFingerprint };
55
+ } catch (e) {
56
+ logger.error("Error generating JA4+ fingerprint:", e);
57
+ return { ja4PlusFingerprint: DEFAULT_JA4 };
58
+ }
59
+ };
60
+ const ja4Middleware = (env) => {
61
+ return async (req, res, next) => {
62
+ try {
63
+ const ja4 = await getJA4(req.headers, req.logger);
64
+ req.ja4 = ja4.ja4PlusFingerprint || "";
65
+ next();
66
+ } catch (err) {
67
+ return apiExpressRouter.handleErrors(err, req, res, next);
68
+ }
69
+ };
70
+ };
71
+ exports.DEFAULT_JA4 = DEFAULT_JA4;
72
+ exports.getJA4 = getJA4;
73
+ exports.ja4Middleware = ja4Middleware;