@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
@@ -0,0 +1,150 @@
1
+ import { ProsopoApiError } from "@prosopo/common";
2
+ import { parseCaptchaAssets } from "@prosopo/datasets";
3
+ import { CaptchaRequestBody, CaptchaType, ApiParams } from "@prosopo/types";
4
+ import { getIPAddress, flatten } from "@prosopo/util";
5
+ import "../../tasks/index.js";
6
+ import { getRequestUserScope } from "../blacklistRequestInspector.js";
7
+ import { validateSiteKey, validateAddr } from "../validateAddress.js";
8
+ import { Tasks } from "../../tasks/tasks.js";
9
+ const getImageCaptchaChallenge = (env, userAccessRulesStorage) => async (req, res, next) => {
10
+ const tasks = new Tasks(env, req.logger);
11
+ let parsed;
12
+ if (!req.ip) {
13
+ return next(
14
+ new ProsopoApiError("API.BAD_REQUEST", {
15
+ context: { code: 400, error: "IP address not found" },
16
+ i18n: req.i18n,
17
+ logger: req.logger
18
+ })
19
+ );
20
+ }
21
+ const ipAddress = getIPAddress(req.ip || "");
22
+ try {
23
+ parsed = CaptchaRequestBody.parse(req.body);
24
+ } catch (err) {
25
+ return next(
26
+ new ProsopoApiError("CAPTCHA.PARSE_ERROR", {
27
+ context: { code: 400, error: err },
28
+ i18n: req.i18n,
29
+ logger: req.logger
30
+ })
31
+ );
32
+ }
33
+ const { datasetId, user, dapp, sessionId } = parsed;
34
+ validateSiteKey(dapp);
35
+ validateAddr(user);
36
+ try {
37
+ const clientRecord = await tasks.db.getClientRecord(dapp);
38
+ if (!clientRecord) {
39
+ return next(
40
+ new ProsopoApiError("API.SITE_KEY_NOT_REGISTERED", {
41
+ context: { code: 400, siteKey: dapp },
42
+ i18n: req.i18n,
43
+ logger: req.logger
44
+ })
45
+ );
46
+ }
47
+ const userScope = getRequestUserScope(
48
+ flatten(req.headers),
49
+ req.ja4,
50
+ req.ip,
51
+ user
52
+ );
53
+ const userAccessPolicy = (await tasks.imgCaptchaManager.getPrioritisedAccessPolicies(
54
+ userAccessRulesStorage,
55
+ dapp,
56
+ userScope
57
+ ))[0];
58
+ const {
59
+ valid,
60
+ reason,
61
+ sessionId: validSessionId,
62
+ solvedImagesCount
63
+ } = await tasks.imgCaptchaManager.isValidRequest(
64
+ clientRecord,
65
+ CaptchaType.image,
66
+ env,
67
+ sessionId,
68
+ userAccessPolicy,
69
+ req.ip
70
+ );
71
+ if (!valid) {
72
+ return next(
73
+ new ProsopoApiError(reason || "API.BAD_REQUEST", {
74
+ context: {
75
+ code: 400,
76
+ siteKey: dapp,
77
+ user
78
+ },
79
+ i18n: req.i18n,
80
+ logger: req.logger
81
+ })
82
+ );
83
+ }
84
+ const captchaConfig = {
85
+ solved: {
86
+ count: solvedImagesCount || userAccessPolicy?.solvedImagesCount || env.config.captchas.solved.count
87
+ },
88
+ unsolved: {
89
+ count: userAccessPolicy?.unsolvedImagesCount || env.config.captchas.unsolved.count
90
+ }
91
+ };
92
+ const taskData = await tasks.imgCaptchaManager.getRandomCaptchasAndRequestHash(
93
+ datasetId,
94
+ user,
95
+ ipAddress,
96
+ captchaConfig,
97
+ clientRecord.settings.imageThreshold ?? 0.8,
98
+ validSessionId
99
+ );
100
+ const captchaResponse = {
101
+ [ApiParams.status]: "ok",
102
+ [ApiParams.captchas]: taskData.captchas.map((captcha) => ({
103
+ ...captcha,
104
+ target: req.t(`TARGET.${captcha.target}`),
105
+ items: captcha.items.map(
106
+ (item) => parseCaptchaAssets(item, env.assetsResolver)
107
+ )
108
+ })),
109
+ [ApiParams.requestHash]: taskData.requestHash,
110
+ [ApiParams.timestamp]: taskData.timestamp.toString(),
111
+ [ApiParams.signature]: {
112
+ [ApiParams.provider]: {
113
+ [ApiParams.requestHash]: taskData.signedRequestHash
114
+ }
115
+ }
116
+ };
117
+ req.logger.info(() => ({
118
+ msg: "Image captcha challenge issued",
119
+ data: {
120
+ captchaType: CaptchaType.image,
121
+ requestHash: taskData.requestHash,
122
+ solvedImagesCount: captchaConfig.solved.count,
123
+ user,
124
+ dapp,
125
+ sessionId
126
+ }
127
+ }));
128
+ return res.json(captchaResponse);
129
+ } catch (err) {
130
+ req.logger.error(() => ({
131
+ err,
132
+ data: req.params,
133
+ msg: "Error in image captcha challenge request"
134
+ }));
135
+ return next(
136
+ new ProsopoApiError("API.BAD_REQUEST", {
137
+ context: {
138
+ error: err,
139
+ code: 500,
140
+ params: req.params
141
+ },
142
+ i18n: req.i18n,
143
+ logger: req.logger
144
+ })
145
+ );
146
+ }
147
+ };
148
+ export {
149
+ getImageCaptchaChallenge as default
150
+ };
@@ -0,0 +1,156 @@
1
+ import { ProsopoApiError } from "@prosopo/common";
2
+ import { GetPowCaptchaChallengeRequestBody, CaptchaType, ApiParams } from "@prosopo/types";
3
+ import { flatten } from "@prosopo/util";
4
+ import { getCompositeIpAddress } from "../../compositeIpAddress.js";
5
+ import "../../tasks/index.js";
6
+ import { getRequestUserScope } from "../blacklistRequestInspector.js";
7
+ import { validateSiteKey, validateAddr } from "../validateAddress.js";
8
+ import { Tasks } from "../../tasks/tasks.js";
9
+ const getPoWCaptchaChallenge = (env, userAccessRulesStorage) => async (req, res, next) => {
10
+ let parsed;
11
+ const tasks = new Tasks(env);
12
+ tasks.setLogger(req.logger);
13
+ try {
14
+ parsed = GetPowCaptchaChallengeRequestBody.parse(req.body);
15
+ } catch (err) {
16
+ return next(
17
+ new ProsopoApiError("CAPTCHA.PARSE_ERROR", {
18
+ context: { code: 400, error: err },
19
+ i18n: req.i18n,
20
+ logger: req.logger
21
+ })
22
+ );
23
+ }
24
+ const { user, dapp, sessionId } = parsed;
25
+ validateSiteKey(dapp);
26
+ validateAddr(user);
27
+ try {
28
+ const clientSettings = await tasks.db.getClientRecord(dapp);
29
+ if (!clientSettings) {
30
+ return next(
31
+ new ProsopoApiError("API.SITE_KEY_NOT_REGISTERED", {
32
+ context: { code: 400, siteKey: dapp },
33
+ i18n: req.i18n,
34
+ logger: req.logger
35
+ })
36
+ );
37
+ }
38
+ const userScope = getRequestUserScope(
39
+ flatten(req.headers),
40
+ req.ja4,
41
+ req.ip,
42
+ user
43
+ );
44
+ const userAccessPolicy = (await tasks.powCaptchaManager.getPrioritisedAccessPolicies(
45
+ userAccessRulesStorage,
46
+ dapp,
47
+ userScope
48
+ ))[0];
49
+ const {
50
+ valid,
51
+ reason,
52
+ sessionId: validSessionId,
53
+ powDifficulty
54
+ } = await tasks.powCaptchaManager.isValidRequest(
55
+ clientSettings,
56
+ CaptchaType.pow,
57
+ env,
58
+ sessionId,
59
+ userAccessPolicy,
60
+ req.ip
61
+ );
62
+ if (!valid) {
63
+ return next(
64
+ new ProsopoApiError(reason || "API.BAD_REQUEST", {
65
+ context: {
66
+ code: 400,
67
+ siteKey: dapp,
68
+ user
69
+ },
70
+ i18n: req.i18n,
71
+ logger: req.logger
72
+ })
73
+ );
74
+ }
75
+ const origin = req.headers.origin;
76
+ if (!origin) {
77
+ return next(
78
+ new ProsopoApiError("API.BAD_REQUEST", {
79
+ context: {
80
+ error: "Origin header not found",
81
+ code: 400,
82
+ siteKey: dapp,
83
+ user
84
+ },
85
+ i18n: req.i18n,
86
+ logger: req.logger
87
+ })
88
+ );
89
+ }
90
+ const difficulty = powDifficulty || userAccessPolicy?.powDifficulty || clientSettings?.settings?.powDifficulty;
91
+ const challenge = await tasks.powCaptchaManager.getPowCaptchaChallenge(
92
+ user,
93
+ dapp,
94
+ origin,
95
+ difficulty
96
+ );
97
+ await tasks.db.storePowCaptchaRecord(
98
+ challenge.challenge,
99
+ {
100
+ requestedAtTimestamp: challenge.requestedAtTimestamp,
101
+ userAccount: user,
102
+ dappAccount: dapp
103
+ },
104
+ challenge.difficulty,
105
+ challenge.providerSignature,
106
+ getCompositeIpAddress(req.ip || ""),
107
+ flatten(req.headers),
108
+ req.ja4,
109
+ validSessionId
110
+ );
111
+ const getPowCaptchaResponse = {
112
+ [ApiParams.status]: "ok",
113
+ [ApiParams.challenge]: challenge.challenge,
114
+ [ApiParams.difficulty]: challenge.difficulty,
115
+ [ApiParams.timestamp]: challenge.requestedAtTimestamp.toString(),
116
+ [ApiParams.signature]: {
117
+ [ApiParams.provider]: {
118
+ [ApiParams.challenge]: challenge.providerSignature
119
+ }
120
+ }
121
+ };
122
+ req.logger.info(() => ({
123
+ msg: "PoW captcha challenge issued",
124
+ data: {
125
+ captchaType: CaptchaType.pow,
126
+ challenge: challenge.challenge,
127
+ difficulty: challenge.difficulty,
128
+ user,
129
+ dapp,
130
+ session: sessionId
131
+ }
132
+ }));
133
+ return res.json(getPowCaptchaResponse);
134
+ } catch (err) {
135
+ req.logger.error(() => ({
136
+ err,
137
+ body: req.body,
138
+ msg: "Error in PoW captcha challenge request"
139
+ }));
140
+ return next(
141
+ new ProsopoApiError("API.BAD_REQUEST", {
142
+ context: {
143
+ code: 500,
144
+ siteKey: req.body.dapp,
145
+ user: req.body.user,
146
+ error: err
147
+ },
148
+ i18n: req.i18n,
149
+ logger: req.logger
150
+ })
151
+ );
152
+ }
153
+ };
154
+ export {
155
+ getPoWCaptchaChallenge as default
156
+ };
@@ -0,0 +1,87 @@
1
+ import { ProsopoApiError } from "@prosopo/common";
2
+ import { CaptchaSolutionBody, ApiParams } from "@prosopo/types";
3
+ import { getIPAddress, flatten } from "@prosopo/util";
4
+ import "../../tasks/index.js";
5
+ import { getMaintenanceMode } from "../admin/apiToggleMaintenanceModeEndpoint.js";
6
+ import { validateSiteKey, validateAddr } from "../validateAddress.js";
7
+ import { Tasks } from "../../tasks/tasks.js";
8
+ const submitImageCaptchaSolution = (env, userAccessRulesStorage) => async (req, res, next) => {
9
+ const tasks = new Tasks(env, req.logger);
10
+ if (getMaintenanceMode()) {
11
+ req.logger.info(() => ({
12
+ msg: "Maintenance mode active - returning verified for image captcha"
13
+ }));
14
+ const result = {
15
+ status: "ok",
16
+ captchas: [],
17
+ verified: true
18
+ };
19
+ return res.json(result);
20
+ }
21
+ let parsed;
22
+ try {
23
+ parsed = CaptchaSolutionBody.parse(req.body);
24
+ } catch (err) {
25
+ return next(
26
+ new ProsopoApiError("CAPTCHA.PARSE_ERROR", {
27
+ context: { code: 400, error: err, body: req.body },
28
+ i18n: req.i18n,
29
+ logger: req.logger
30
+ })
31
+ );
32
+ }
33
+ const { user, dapp } = parsed;
34
+ validateSiteKey(dapp);
35
+ validateAddr(user);
36
+ try {
37
+ const clientRecord = await tasks.db.getClientRecord(parsed.dapp);
38
+ if (!clientRecord) {
39
+ return next(
40
+ new ProsopoApiError("API.SITE_KEY_NOT_REGISTERED", {
41
+ context: { code: 400, siteKey: dapp },
42
+ i18n: req.i18n,
43
+ logger: req.logger
44
+ })
45
+ );
46
+ }
47
+ const result = await tasks.imgCaptchaManager.dappUserSolution(
48
+ user,
49
+ dapp,
50
+ parsed[ApiParams.requestHash],
51
+ parsed[ApiParams.captchas],
52
+ parsed[ApiParams.signature].user.timestamp,
53
+ Number.parseInt(parsed[ApiParams.timestamp]),
54
+ parsed[ApiParams.signature].provider.requestHash,
55
+ getIPAddress(req.ip || ""),
56
+ flatten(req.headers),
57
+ req.ja4
58
+ );
59
+ const returnValue = {
60
+ status: req.i18n.t(
61
+ result.verified ? "API.CAPTCHA_PASSED" : "API.CAPTCHA_FAILED"
62
+ ),
63
+ ...result
64
+ };
65
+ return res.json(returnValue);
66
+ } catch (err) {
67
+ req.logger.error(() => ({
68
+ err,
69
+ body: req.body,
70
+ msg: "Error in image captcha solution submission"
71
+ }));
72
+ return next(
73
+ new ProsopoApiError("API.BAD_REQUEST", {
74
+ context: {
75
+ code: 500,
76
+ siteKey: req.body.dapp,
77
+ error: err
78
+ },
79
+ i18n: req.i18n,
80
+ logger: req.logger
81
+ })
82
+ );
83
+ }
84
+ };
85
+ export {
86
+ submitImageCaptchaSolution as default
87
+ };
@@ -0,0 +1,77 @@
1
+ import { ProsopoApiError } from "@prosopo/common";
2
+ import { SubmitPowCaptchaSolutionBody } from "@prosopo/types";
3
+ import { getIPAddress, flatten } from "@prosopo/util";
4
+ import { Tasks } from "../../tasks/tasks.js";
5
+ import { getMaintenanceMode } from "../admin/apiToggleMaintenanceModeEndpoint.js";
6
+ import { validateSiteKey, validateAddr } from "../validateAddress.js";
7
+ const submitPoWCaptchaSolution = (env) => async (req, res, next) => {
8
+ let parsed;
9
+ const tasks = new Tasks(env, req.logger);
10
+ if (getMaintenanceMode()) {
11
+ req.logger.info(() => ({
12
+ msg: "Maintenance mode active - returning verified"
13
+ }));
14
+ const response = {
15
+ status: "ok",
16
+ verified: true
17
+ };
18
+ return res.json(response);
19
+ }
20
+ try {
21
+ parsed = SubmitPowCaptchaSolutionBody.parse(req.body);
22
+ } catch (err) {
23
+ return next(
24
+ new ProsopoApiError("CAPTCHA.PARSE_ERROR", {
25
+ context: { code: 400, error: err, body: req.body },
26
+ i18n: req.i18n,
27
+ logger: req.logger
28
+ })
29
+ );
30
+ }
31
+ const { challenge, signature, nonce, verifiedTimeout, dapp, user } = parsed;
32
+ validateSiteKey(dapp);
33
+ validateAddr(user);
34
+ try {
35
+ const clientRecord = await tasks.db.getClientRecord(dapp);
36
+ if (!clientRecord) {
37
+ return next(
38
+ new ProsopoApiError("API.SITE_KEY_NOT_REGISTERED", {
39
+ context: { code: 400, siteKey: dapp },
40
+ i18n: req.i18n,
41
+ logger: req.logger
42
+ })
43
+ );
44
+ }
45
+ const verified = await tasks.powCaptchaManager.verifyPowCaptchaSolution(
46
+ challenge,
47
+ signature.provider.challenge,
48
+ nonce,
49
+ verifiedTimeout,
50
+ signature.user.timestamp,
51
+ getIPAddress(req.ip || ""),
52
+ flatten(req.headers)
53
+ );
54
+ const response = { status: "ok", verified };
55
+ return res.json(response);
56
+ } catch (err) {
57
+ req.logger.error(() => ({
58
+ err,
59
+ body: req.body,
60
+ msg: "Error in PoW captcha solution submission"
61
+ }));
62
+ return next(
63
+ new ProsopoApiError("API.BAD_REQUEST", {
64
+ context: {
65
+ code: 500,
66
+ siteKey: req.body.dapp,
67
+ error: err
68
+ },
69
+ i18n: req.i18n,
70
+ logger: req.logger
71
+ })
72
+ );
73
+ }
74
+ };
75
+ export {
76
+ submitPoWCaptchaSolution as default
77
+ };