@prosopo/provider 5.0.5 → 5.1.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 (59) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +12 -11
  2. package/.turbo/turbo-build$colon$tsc.log +24 -24
  3. package/.turbo/turbo-build.log +13 -12
  4. package/CHANGELOG.md +37 -0
  5. package/dist/api/captcha/getFrictionlessCaptchaChallenge/handler.d.ts.map +1 -1
  6. package/dist/api/captcha/getFrictionlessCaptchaChallenge/handler.js +12 -0
  7. package/dist/api/captcha/getFrictionlessCaptchaChallenge/handler.js.map +1 -1
  8. package/dist/api/captcha/getImageCaptchaChallenge.d.ts.map +1 -1
  9. package/dist/api/captcha/getImageCaptchaChallenge.js +13 -1
  10. package/dist/api/captcha/getImageCaptchaChallenge.js.map +1 -1
  11. package/dist/api/captcha/getPoWCaptchaChallenge.d.ts.map +1 -1
  12. package/dist/api/captcha/getPoWCaptchaChallenge.js +12 -1
  13. package/dist/api/captcha/getPoWCaptchaChallenge.js.map +1 -1
  14. package/dist/api/captcha/getPuzzleCaptchaChallenge.d.ts.map +1 -1
  15. package/dist/api/captcha/getPuzzleCaptchaChallenge.js +12 -1
  16. package/dist/api/captcha/getPuzzleCaptchaChallenge.js.map +1 -1
  17. package/dist/api/captcha/trafficFilterRequestTime.d.ts +38 -0
  18. package/dist/api/captcha/trafficFilterRequestTime.d.ts.map +1 -0
  19. package/dist/api/captcha/trafficFilterRequestTime.js +109 -0
  20. package/dist/api/captcha/trafficFilterRequestTime.js.map +1 -0
  21. package/dist/cjs/api/captcha/getFrictionlessCaptchaChallenge/handler.cjs +13 -0
  22. package/dist/cjs/api/captcha/getImageCaptchaChallenge.cjs +13 -1
  23. package/dist/cjs/api/captcha/getPoWCaptchaChallenge.cjs +12 -1
  24. package/dist/cjs/api/captcha/getPuzzleCaptchaChallenge.cjs +12 -1
  25. package/dist/cjs/api/captcha/trafficFilterRequestTime.cjs +111 -0
  26. package/dist/cjs/tasks/captchaManager.cjs +13 -6
  27. package/dist/cjs/tasks/dnsEvent/enrichDnsEvent.cjs +10 -8
  28. package/dist/cjs/tasks/puzzleCaptcha/puzzleTasks.cjs +2 -3
  29. package/dist/cjs/tasks/spam/checkTrafficFilter.cjs +101 -75
  30. package/dist/tasks/captchaManager.d.ts.map +1 -1
  31. package/dist/tasks/captchaManager.js +14 -7
  32. package/dist/tasks/captchaManager.js.map +1 -1
  33. package/dist/tasks/dnsEvent/enrichDnsEvent.d.ts.map +1 -1
  34. package/dist/tasks/dnsEvent/enrichDnsEvent.js +10 -8
  35. package/dist/tasks/dnsEvent/enrichDnsEvent.js.map +1 -1
  36. package/dist/tasks/puzzleCaptcha/puzzleTasks.d.ts.map +1 -1
  37. package/dist/tasks/puzzleCaptcha/puzzleTasks.js +2 -3
  38. package/dist/tasks/puzzleCaptcha/puzzleTasks.js.map +1 -1
  39. package/dist/tasks/spam/checkTrafficFilter.d.ts +18 -1
  40. package/dist/tasks/spam/checkTrafficFilter.d.ts.map +1 -1
  41. package/dist/tasks/spam/checkTrafficFilter.js +101 -77
  42. package/dist/tasks/spam/checkTrafficFilter.js.map +1 -1
  43. package/dist/tests/integration/clientSettingsPersistence.integration.test.js +24 -17
  44. package/dist/tests/integration/clientSettingsPersistence.integration.test.js.map +1 -1
  45. package/dist/tests/integration/trafficFilterHierarchy.integration.test.d.ts +2 -0
  46. package/dist/tests/integration/trafficFilterHierarchy.integration.test.d.ts.map +1 -0
  47. package/dist/tests/integration/trafficFilterHierarchy.integration.test.js +215 -0
  48. package/dist/tests/integration/trafficFilterHierarchy.integration.test.js.map +1 -0
  49. package/dist/tests/unit/api/captcha/trafficFilterRequestTime.unit.test.d.ts +2 -0
  50. package/dist/tests/unit/api/captcha/trafficFilterRequestTime.unit.test.d.ts.map +1 -0
  51. package/dist/tests/unit/api/captcha/trafficFilterRequestTime.unit.test.js +255 -0
  52. package/dist/tests/unit/api/captcha/trafficFilterRequestTime.unit.test.js.map +1 -0
  53. package/dist/tests/unit/tasks/dnsEvent/enrichDnsEvent.unit.test.js +6 -9
  54. package/dist/tests/unit/tasks/dnsEvent/enrichDnsEvent.unit.test.js.map +1 -1
  55. package/dist/tests/unit/tasks/puzzleCaptcha/puzzleTasks.unit.test.js +95 -0
  56. package/dist/tests/unit/tasks/puzzleCaptcha/puzzleTasks.unit.test.js.map +1 -1
  57. package/dist/tests/unit/tasks/spam/checkTrafficFilter.unit.test.js +272 -98
  58. package/dist/tests/unit/tasks/spam/checkTrafficFilter.unit.test.js.map +1 -1
  59. package/package.json +13 -13
@@ -0,0 +1,38 @@
1
+ import type { Logger } from "@prosopo/logger";
2
+ import { CaptchaType, type IPInfoResponse, type ITrafficFilter } from "@prosopo/types";
3
+ import type { ClientRecord } from "@prosopo/types-database";
4
+ import type { Response } from "express";
5
+ import type { FrictionlessManager } from "../../tasks/frictionless/frictionlessTasks.js";
6
+ import { type ResolvedChallengePolicy, type TrafficBlockReason } from "../../tasks/spam/checkTrafficFilter.js";
7
+ export type RequestTimeTrafficVerdict = {
8
+ kind: "pass";
9
+ } | {
10
+ kind: "block";
11
+ reason: TrafficBlockReason;
12
+ } | {
13
+ kind: "challenge";
14
+ captchaType: CaptchaType | undefined;
15
+ powDifficulty?: number;
16
+ solvedImagesCount?: number;
17
+ puzzleTolerance?: number;
18
+ sourceCategories: ResolvedChallengePolicy["sourceCategories"];
19
+ };
20
+ export declare const applyTrafficFilterAtRequestTime: (ipInfo: IPInfoResponse | undefined, trafficFilter: Partial<ITrafficFilter> | undefined, logger?: Logger) => RequestTimeTrafficVerdict;
21
+ export type FrictionlessTrafficFilterOutcome = {
22
+ handled: false;
23
+ } | {
24
+ handled: true;
25
+ response: Response;
26
+ };
27
+ export type FrictionlessTrafficFilterInput = {
28
+ verdict: RequestTimeTrafficVerdict;
29
+ frictionlessManager: FrictionlessManager;
30
+ clientRecord: ClientRecord;
31
+ userSitekeyIpHash: string;
32
+ dapp: string;
33
+ ipInfo: IPInfoResponse | undefined;
34
+ flatHeaders: Record<string, string>;
35
+ logger: Logger;
36
+ };
37
+ export declare const handleFrictionlessTrafficFilter: (input: FrictionlessTrafficFilterInput, res: Response) => Promise<FrictionlessTrafficFilterOutcome>;
38
+ //# sourceMappingURL=trafficFilterRequestTime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trafficFilterRequestTime.d.ts","sourceRoot":"","sources":["../../../src/api/captcha/trafficFilterRequestTime.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EACN,WAAW,EAEX,KAAK,cAAc,EACnB,KAAK,cAAc,EAEnB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACxC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,+CAA+C,CAAC;AACzF,OAAO,EACN,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,EAGvB,MAAM,wCAAwC,CAAC;AAEhD,MAAM,MAAM,yBAAyB,GAClC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,kBAAkB,CAAA;CAAE,GAC7C;IACA,IAAI,EAAE,WAAW,CAAC;IAGlB,WAAW,EAAE,WAAW,GAAG,SAAS,CAAC;IACrC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,EAAE,uBAAuB,CAAC,kBAAkB,CAAC,CAAC;CAC7D,CAAC;AAeL,eAAO,MAAM,+BAA+B,WACnC,cAAc,GAAG,SAAS,iBACnB,OAAO,CAAC,cAAc,CAAC,GAAG,SAAS,WACzC,MAAM,KACb,yBAwCF,CAAC;AAEF,MAAM,MAAM,gCAAgC,GACzC;IAAE,OAAO,EAAE,KAAK,CAAA;CAAE,GAClB;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC;AAEzC,MAAM,MAAM,8BAA8B,GAAG;IAC5C,OAAO,EAAE,yBAAyB,CAAC;IACnC,mBAAmB,EAAE,mBAAmB,CAAC;IACzC,YAAY,EAAE,YAAY,CAAC;IAC3B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,cAAc,GAAG,SAAS,CAAC;IACnC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,MAAM,EAAE,MAAM,CAAC;CACf,CAAC;AAUF,eAAO,MAAM,+BAA+B,UACpC,8BAA8B,OAChC,QAAQ,KACX,OAAO,CAAC,gCAAgC,CAgE1C,CAAC"}
@@ -0,0 +1,109 @@
1
+ import { checkTrafficFilter, resolveChallengePolicy } from "../../tasks/spam/checkTrafficFilter.js";
2
+ import { CaptchaType, TrafficFilterAction } from "@prosopo/types";
3
+ //#region src/api/captcha/trafficFilterRequestTime.ts
4
+ /**
5
+ * Evaluate the site's `trafficFilter` against the current request's IP info
6
+ * at challenge-request time. Runs before captcha issuance so that a matched
7
+ * `block` policy short-circuits with 401 (no captcha issued, mirroring the
8
+ * `blockMiddleware` semantics) and a matched `challenge` policy hands back
9
+ * an override bundle for the caller to fold into its captcha-type +
10
+ * parameter selection.
11
+ *
12
+ * The verdict is authoritative for the request-time gate. Callers still run
13
+ * `resolveTrafficFilterCheck` at submit time to catch `block` policies that
14
+ * apply to the current (verify-time) IP even when the challenge-time IP
15
+ * passed.
16
+ */
17
+ var applyTrafficFilterAtRequestTime = (ipInfo, trafficFilter, logger) => {
18
+ if (!trafficFilter) return { kind: "pass" };
19
+ const result = checkTrafficFilter(ipInfo, {
20
+ abuser: { action: TrafficFilterAction.Block },
21
+ ...trafficFilter
22
+ });
23
+ if (result.isBlocked) {
24
+ logger?.info(() => ({
25
+ msg: "Traffic filter rejected challenge request",
26
+ data: { reason: result.reason }
27
+ }));
28
+ return {
29
+ kind: "block",
30
+ reason: result.reason
31
+ };
32
+ }
33
+ const resolved = resolveChallengePolicy(result.matches);
34
+ if (!resolved) return { kind: "pass" };
35
+ logger?.info(() => ({
36
+ msg: "Traffic filter applied challenge overrides",
37
+ data: {
38
+ categories: resolved.sourceCategories,
39
+ captchaType: resolved.captchaType,
40
+ powDifficulty: resolved.powDifficulty,
41
+ solvedImagesCount: resolved.solvedImagesCount,
42
+ puzzleTolerance: resolved.puzzleTolerance
43
+ }
44
+ }));
45
+ return {
46
+ kind: "challenge",
47
+ captchaType: resolved.captchaType,
48
+ powDifficulty: resolved.powDifficulty,
49
+ solvedImagesCount: resolved.solvedImagesCount,
50
+ puzzleTolerance: resolved.puzzleTolerance,
51
+ sourceCategories: resolved.sourceCategories
52
+ };
53
+ };
54
+ /**
55
+ * Frictionless-flow companion to `applyTrafficFilterAtRequestTime`. When the
56
+ * verdict is `block`, respond 401 and short-circuit. When the verdict is
57
+ * `challenge` and names a concrete captchaType, dispatch to the matching
58
+ * `send{Image,Pow,Puzzle}Captcha` helper with the resolved parameter
59
+ * overrides. Otherwise returns `{ handled: false }` so the caller falls
60
+ * through to the normal decision machine.
61
+ */
62
+ var handleFrictionlessTrafficFilter = async (input, res) => {
63
+ const { verdict } = input;
64
+ if (verdict.kind === "pass") return { handled: false };
65
+ if (verdict.kind === "block") return {
66
+ handled: true,
67
+ response: res.status(401).json({ error: "Unauthorized" })
68
+ };
69
+ const baseParams = {
70
+ userSitekeyIpHash: input.userSitekeyIpHash,
71
+ siteKey: input.dapp,
72
+ ipInfo: input.ipInfo,
73
+ headers: input.flatHeaders
74
+ };
75
+ const logDispatch = (captchaType) => {
76
+ input.logger.info(() => ({
77
+ msg: "Frictionless decision",
78
+ data: {
79
+ decision: "traffic_filter",
80
+ captchaType,
81
+ categories: verdict.sourceCategories
82
+ }
83
+ }));
84
+ };
85
+ let response;
86
+ if (verdict.captchaType === CaptchaType.image) {
87
+ logDispatch(CaptchaType.image);
88
+ response = await input.frictionlessManager.sendImageCaptcha({
89
+ ...baseParams,
90
+ solvedImagesCount: verdict.solvedImagesCount ? Math.min(verdict.solvedImagesCount, input.clientRecord.settings.imageMaxRounds) : input.clientRecord.settings.imageMaxRounds
91
+ });
92
+ } else if (verdict.captchaType === CaptchaType.pow) {
93
+ logDispatch(CaptchaType.pow);
94
+ response = await input.frictionlessManager.sendPowCaptcha({
95
+ ...baseParams,
96
+ ...verdict.powDifficulty !== void 0 && { powDifficulty: verdict.powDifficulty }
97
+ });
98
+ } else if (verdict.captchaType === CaptchaType.puzzle) {
99
+ logDispatch(CaptchaType.puzzle);
100
+ response = await input.frictionlessManager.sendPuzzleCaptcha(baseParams);
101
+ }
102
+ if (response) return {
103
+ handled: true,
104
+ response: res.json(response)
105
+ };
106
+ return { handled: false };
107
+ };
108
+ //#endregion
109
+ export { applyTrafficFilterAtRequestTime, handleFrictionlessTrafficFilter };
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trafficFilterRequestTime.js","sourceRoot":"","sources":["../../../src/api/captcha/trafficFilterRequestTime.ts"],"names":[],"mappings":"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"}
@@ -10,6 +10,7 @@ const require_hashUserIp = require("../../../utils/hashUserIp.cjs");
10
10
  const require_normalizeRequestIp = require("../../../utils/normalizeRequestIp.cjs");
11
11
  const require_testSiteKey = require("../../testSiteKey.cjs");
12
12
  const require_maintenanceModeResponses = require("../maintenanceModeResponses.cjs");
13
+ const require_trafficFilterRequestTime = require("../trafficFilterRequestTime.cjs");
13
14
  const require_honeypotResponse = require("./honeypotResponse.cjs");
14
15
  const require_accessPolicy = require("./accessPolicy.cjs");
15
16
  require("./constants.cjs");
@@ -313,6 +314,18 @@ var handler_default = (env, userAccessRulesStorage) => async (req, res, next) =>
313
314
  if (accessPolicyOutcome.handled) return accessPolicyOutcome.response;
314
315
  botScore = accessPolicyOutcome.botScore;
315
316
  scoreComponents = accessPolicyOutcome.scoreComponents;
317
+ const trafficFilterVerdict = require_trafficFilterRequestTime.applyTrafficFilterAtRequestTime(req.ipInfo, clientRecord.settings?.trafficFilter, req.logger);
318
+ const trafficFilterOutcome = await require_trafficFilterRequestTime.handleFrictionlessTrafficFilter({
319
+ verdict: trafficFilterVerdict,
320
+ frictionlessManager: tasks.frictionlessManager,
321
+ clientRecord,
322
+ userSitekeyIpHash,
323
+ dapp,
324
+ ipInfo: req.ipInfo,
325
+ flatHeaders,
326
+ logger: req.logger
327
+ }, res);
328
+ if (trafficFilterOutcome.handled) return trafficFilterOutcome.response;
316
329
  return await require_decisionMachine.runDecisionMachine({
317
330
  tasks,
318
331
  env,
@@ -5,6 +5,7 @@ const require_tasks = require("../../tasks/tasks.cjs");
5
5
  const require_apiToggleMaintenanceModeEndpoint = require("../admin/apiToggleMaintenanceModeEndpoint.cjs");
6
6
  const require_normalizeRequestIp = require("../../utils/normalizeRequestIp.cjs");
7
7
  const require_maintenanceModeResponses = require("./maintenanceModeResponses.cjs");
8
+ const require_trafficFilterRequestTime = require("./trafficFilterRequestTime.cjs");
8
9
  let _prosopo_common = require("@prosopo/common");
9
10
  let _prosopo_types = require("@prosopo/types");
10
11
  let _prosopo_util = require("@prosopo/util");
@@ -83,8 +84,19 @@ var getImageCaptchaChallenge_default = (env, userAccessRulesStorage) => async (r
83
84
  i18n: req.i18n,
84
85
  logger: req.logger
85
86
  }));
87
+ const trafficVerdict = require_trafficFilterRequestTime.applyTrafficFilterAtRequestTime(req.ipInfo, clientRecord.settings?.trafficFilter, req.logger);
88
+ if (trafficVerdict.kind === "block") return next(new _prosopo_common.ProsopoApiError(trafficVerdict.reason, {
89
+ context: {
90
+ code: 401,
91
+ siteKey: dapp,
92
+ user
93
+ },
94
+ i18n: req.i18n,
95
+ logger: req.logger
96
+ }));
97
+ const trafficSolvedImagesCount = trafficVerdict.kind === "challenge" ? trafficVerdict.solvedImagesCount : void 0;
86
98
  const captchaConfig = {
87
- solved: { count: Math.min(solvedImagesCount || userAccessPolicy?.solvedImagesCount || env.config.captchas.solved.count, clientRecord.settings.imageMaxRounds ?? _prosopo_types.imageMaxRoundsDefault) },
99
+ solved: { count: Math.min(trafficSolvedImagesCount || solvedImagesCount || userAccessPolicy?.solvedImagesCount || env.config.captchas.solved.count, clientRecord.settings.imageMaxRounds ?? _prosopo_types.imageMaxRoundsDefault) },
88
100
  unsolved: { count: userAccessPolicy?.unsolvedImagesCount || env.config.captchas.unsolved.count }
89
101
  };
90
102
  if (validSessionId && simdReadings) await tasks.frictionlessManager.decryptAndAttachSimdReadingsIfAbsent(validSessionId, simdReadings, _prosopo_types.SimdReadingsStage.challenge).catch((updateErr) => {
@@ -6,6 +6,7 @@ const require_tasks = require("../../tasks/tasks.cjs");
6
6
  const require_apiToggleMaintenanceModeEndpoint = require("../admin/apiToggleMaintenanceModeEndpoint.cjs");
7
7
  const require_normalizeRequestIp = require("../../utils/normalizeRequestIp.cjs");
8
8
  const require_maintenanceModeResponses = require("./maintenanceModeResponses.cjs");
9
+ const require_trafficFilterRequestTime = require("./trafficFilterRequestTime.cjs");
9
10
  let _prosopo_common = require("@prosopo/common");
10
11
  let _prosopo_types = require("@prosopo/types");
11
12
  let _prosopo_util = require("@prosopo/util");
@@ -77,7 +78,17 @@ var getPoWCaptchaChallenge_default = (env, userAccessRulesStorage) => async (req
77
78
  i18n: req.i18n,
78
79
  logger: req.logger
79
80
  }));
80
- const difficulty = powDifficulty || userAccessPolicy?.powDifficulty || clientSettings?.settings?.powDifficulty;
81
+ const trafficVerdict = require_trafficFilterRequestTime.applyTrafficFilterAtRequestTime(req.ipInfo, clientSettings.settings?.trafficFilter, req.logger);
82
+ if (trafficVerdict.kind === "block") return next(new _prosopo_common.ProsopoApiError(trafficVerdict.reason, {
83
+ context: {
84
+ code: 401,
85
+ siteKey: dapp,
86
+ user
87
+ },
88
+ i18n: req.i18n,
89
+ logger: req.logger
90
+ }));
91
+ const difficulty = (trafficVerdict.kind === "challenge" ? trafficVerdict.powDifficulty : void 0) || powDifficulty || userAccessPolicy?.powDifficulty || clientSettings?.settings?.powDifficulty;
81
92
  const challenge = await tasks.powCaptchaManager.getPowCaptchaChallenge(user, dapp, origin, difficulty);
82
93
  if (validSessionId && simdReadings) await tasks.frictionlessManager.decryptAndAttachSimdReadingsIfAbsent(validSessionId, simdReadings, _prosopo_types.SimdReadingsStage.challenge).catch((updateErr) => {
83
94
  req.logger.warn(() => ({
@@ -6,6 +6,7 @@ const require_tasks = require("../../tasks/tasks.cjs");
6
6
  const require_apiToggleMaintenanceModeEndpoint = require("../admin/apiToggleMaintenanceModeEndpoint.cjs");
7
7
  const require_normalizeRequestIp = require("../../utils/normalizeRequestIp.cjs");
8
8
  const require_maintenanceModeResponses = require("./maintenanceModeResponses.cjs");
9
+ const require_trafficFilterRequestTime = require("./trafficFilterRequestTime.cjs");
9
10
  let _prosopo_common = require("@prosopo/common");
10
11
  let _prosopo_types = require("@prosopo/types");
11
12
  let _prosopo_util = require("@prosopo/util");
@@ -77,7 +78,17 @@ var getPuzzleCaptchaChallenge_default = (env, userAccessRulesStorage) => async (
77
78
  i18n: req.i18n,
78
79
  logger: req.logger
79
80
  }));
80
- const tolerance = clientSettings?.settings?.puzzleTolerance;
81
+ const trafficVerdict = require_trafficFilterRequestTime.applyTrafficFilterAtRequestTime(req.ipInfo, clientSettings.settings?.trafficFilter, req.logger);
82
+ if (trafficVerdict.kind === "block") return next(new _prosopo_common.ProsopoApiError(trafficVerdict.reason, {
83
+ context: {
84
+ code: 401,
85
+ siteKey: dapp,
86
+ user
87
+ },
88
+ i18n: req.i18n,
89
+ logger: req.logger
90
+ }));
91
+ const tolerance = (trafficVerdict.kind === "challenge" ? trafficVerdict.puzzleTolerance : void 0) ?? clientSettings?.settings?.puzzleTolerance;
81
92
  const challenge = await tasks.puzzleCaptchaManager.getPuzzleCaptchaChallenge(user, dapp, origin, tolerance);
82
93
  if (validSessionId && simdReadings) await tasks.frictionlessManager.decryptAndAttachSimdReadingsIfAbsent(validSessionId, simdReadings, _prosopo_types.SimdReadingsStage.challenge).catch((updateErr) => {
83
94
  req.logger.warn(() => ({
@@ -0,0 +1,111 @@
1
+ const require_checkTrafficFilter = require("../../tasks/spam/checkTrafficFilter.cjs");
2
+ let _prosopo_types = require("@prosopo/types");
3
+ //#region src/api/captcha/trafficFilterRequestTime.ts
4
+ /**
5
+ * Evaluate the site's `trafficFilter` against the current request's IP info
6
+ * at challenge-request time. Runs before captcha issuance so that a matched
7
+ * `block` policy short-circuits with 401 (no captcha issued, mirroring the
8
+ * `blockMiddleware` semantics) and a matched `challenge` policy hands back
9
+ * an override bundle for the caller to fold into its captcha-type +
10
+ * parameter selection.
11
+ *
12
+ * The verdict is authoritative for the request-time gate. Callers still run
13
+ * `resolveTrafficFilterCheck` at submit time to catch `block` policies that
14
+ * apply to the current (verify-time) IP even when the challenge-time IP
15
+ * passed.
16
+ */
17
+ var applyTrafficFilterAtRequestTime = (ipInfo, trafficFilter, logger) => {
18
+ if (!trafficFilter) return { kind: "pass" };
19
+ const effective = {
20
+ abuser: { action: _prosopo_types.TrafficFilterAction.Block },
21
+ ...trafficFilter
22
+ };
23
+ const result = require_checkTrafficFilter.checkTrafficFilter(ipInfo, effective);
24
+ if (result.isBlocked) {
25
+ logger?.info(() => ({
26
+ msg: "Traffic filter rejected challenge request",
27
+ data: { reason: result.reason }
28
+ }));
29
+ return {
30
+ kind: "block",
31
+ reason: result.reason
32
+ };
33
+ }
34
+ const resolved = require_checkTrafficFilter.resolveChallengePolicy(result.matches);
35
+ if (!resolved) return { kind: "pass" };
36
+ logger?.info(() => ({
37
+ msg: "Traffic filter applied challenge overrides",
38
+ data: {
39
+ categories: resolved.sourceCategories,
40
+ captchaType: resolved.captchaType,
41
+ powDifficulty: resolved.powDifficulty,
42
+ solvedImagesCount: resolved.solvedImagesCount,
43
+ puzzleTolerance: resolved.puzzleTolerance
44
+ }
45
+ }));
46
+ return {
47
+ kind: "challenge",
48
+ captchaType: resolved.captchaType,
49
+ powDifficulty: resolved.powDifficulty,
50
+ solvedImagesCount: resolved.solvedImagesCount,
51
+ puzzleTolerance: resolved.puzzleTolerance,
52
+ sourceCategories: resolved.sourceCategories
53
+ };
54
+ };
55
+ /**
56
+ * Frictionless-flow companion to `applyTrafficFilterAtRequestTime`. When the
57
+ * verdict is `block`, respond 401 and short-circuit. When the verdict is
58
+ * `challenge` and names a concrete captchaType, dispatch to the matching
59
+ * `send{Image,Pow,Puzzle}Captcha` helper with the resolved parameter
60
+ * overrides. Otherwise returns `{ handled: false }` so the caller falls
61
+ * through to the normal decision machine.
62
+ */
63
+ var handleFrictionlessTrafficFilter = async (input, res) => {
64
+ const { verdict } = input;
65
+ if (verdict.kind === "pass") return { handled: false };
66
+ if (verdict.kind === "block") return {
67
+ handled: true,
68
+ response: res.status(401).json({ error: "Unauthorized" })
69
+ };
70
+ const baseParams = {
71
+ userSitekeyIpHash: input.userSitekeyIpHash,
72
+ siteKey: input.dapp,
73
+ ipInfo: input.ipInfo,
74
+ headers: input.flatHeaders
75
+ };
76
+ const logDispatch = (captchaType) => {
77
+ input.logger.info(() => ({
78
+ msg: "Frictionless decision",
79
+ data: {
80
+ decision: "traffic_filter",
81
+ captchaType,
82
+ categories: verdict.sourceCategories
83
+ }
84
+ }));
85
+ };
86
+ let response;
87
+ if (verdict.captchaType === _prosopo_types.CaptchaType.image) {
88
+ logDispatch(_prosopo_types.CaptchaType.image);
89
+ response = await input.frictionlessManager.sendImageCaptcha({
90
+ ...baseParams,
91
+ solvedImagesCount: verdict.solvedImagesCount ? Math.min(verdict.solvedImagesCount, input.clientRecord.settings.imageMaxRounds) : input.clientRecord.settings.imageMaxRounds
92
+ });
93
+ } else if (verdict.captchaType === _prosopo_types.CaptchaType.pow) {
94
+ logDispatch(_prosopo_types.CaptchaType.pow);
95
+ response = await input.frictionlessManager.sendPowCaptcha({
96
+ ...baseParams,
97
+ ...verdict.powDifficulty !== void 0 && { powDifficulty: verdict.powDifficulty }
98
+ });
99
+ } else if (verdict.captchaType === _prosopo_types.CaptchaType.puzzle) {
100
+ logDispatch(_prosopo_types.CaptchaType.puzzle);
101
+ response = await input.frictionlessManager.sendPuzzleCaptcha(baseParams);
102
+ }
103
+ if (response) return {
104
+ handled: true,
105
+ response: res.json(response)
106
+ };
107
+ return { handled: false };
108
+ };
109
+ //#endregion
110
+ exports.applyTrafficFilterAtRequestTime = applyTrafficFilterAtRequestTime;
111
+ exports.handleFrictionlessTrafficFilter = handleFrictionlessTrafficFilter;
@@ -438,7 +438,10 @@ var CaptchaManager = class CaptchaManager {
438
438
  return require_checkSpamEmail.checkSpamEmail(email, this.db, this.config, this.logger);
439
439
  }
440
440
  /**
441
- * Resolves the IP info to feed to `checkTrafficFilter` and runs the check.
441
+ * Resolves the IP info to feed to `checkTrafficFilter` and runs the check
442
+ * at submit time. Only `action: "block"` matches produce `isBlocked:true`;
443
+ * `action: "challenge"` matches were already applied at request time and
444
+ * are ignored here.
442
445
  *
443
446
  * - The captcha record already carries the IPInfoResponse from request
444
447
  * time (ipInfoMiddleware → storeXxxRecord), so by default we reuse it
@@ -448,9 +451,10 @@ var CaptchaManager = class CaptchaManager {
448
451
  * and may differ from the IP that originally requested the captcha.
449
452
  * - When the session carries a `dnsEvent`, its `peerIp` and `resolverIp`
450
453
  * are enriched and passed alongside the primary IP.
451
- * - `blockAbuser` defaults to true so abusive networks are always
452
- * blocked even when the site hasn't configured a trafficFilter.
453
- * - Returns `{ isBlocked: false }` if every filter flag is off, without
454
+ * - The abuser category defaults to `{action:"block"}` so abusive
455
+ * networks are always blocked even when the site hasn't configured a
456
+ * trafficFilter.
457
+ * - Returns `{ isBlocked: false }` if no category is active, without
454
458
  * consulting the payload at all.
455
459
  *
456
460
  * Callers handle the "blocked" branch themselves (each verify path
@@ -459,10 +463,13 @@ var CaptchaManager = class CaptchaManager {
459
463
  */
460
464
  async resolveTrafficFilterCheck(env, recordIpInfo, trafficFilter, currentIp, enrichedDnsEvent) {
461
465
  const effective = {
462
- blockAbuser: true,
466
+ abuser: { action: _prosopo_types.TrafficFilterAction.Block },
463
467
  ...trafficFilter
464
468
  };
465
- if (!Object.values(effective).some((v) => v)) return { isBlocked: false };
469
+ if (!(effective.vpn !== void 0 || effective.proxy !== void 0 || effective.tor !== void 0 || effective.abuser !== void 0 || effective.datacenter !== void 0 || effective.mobile !== void 0 || effective.satellite !== void 0 || effective.crawler !== void 0)) return {
470
+ isBlocked: false,
471
+ matches: []
472
+ };
466
473
  const ipInfo = currentIp ? await env.ipInfoService.lookup(currentIp) : recordIpInfo;
467
474
  return require_checkTrafficFilter.checkTrafficFilter(ipInfo, effective, require_enrichDnsEvent.extraIpInfosFromEnrichedDnsEvent(enrichedDnsEvent), enrichedDnsEvent?.pathValid);
468
475
  }
@@ -46,15 +46,17 @@ var extraIpInfosFromEnrichedDnsEvent = (enriched) => {
46
46
  * - client IP is a consumer ISP but resolver is datacenter (compound)
47
47
  *
48
48
  * When `trafficFilter` is supplied, the datacenter and abuser contributions
49
- * are gated on the same rules `checkTrafficFilter` uses for the client IP:
49
+ * are gated on the same rules `checkTrafficFilter` uses for the client IP.
50
+ * A category counts here whenever the operator has configured any policy
51
+ * for it (block or challenge) — either intent signals suspicion.
50
52
  *
51
- * - `blockDatacenter` must be opted in (default off); IPs whose
53
+ * - datacenter category must be configured; IPs whose
52
54
  * `providerType === "isp"` short-circuit; named allowlist skips
53
- * - `blockAbuser` opt-in (default on) with the abuser-score threshold
55
+ * - abuser category defaults to configured, with the score threshold
54
56
  * - cross-category suppression: VPN / proxy / Tor / crawler IPs that also
55
57
  * carry `isDatacenter=true` are NOT counted as datacenter when the
56
- * operator hasn't opted into blocking that more specific category —
57
- * mirrors the `datacenterSuppressedByCategory` rule in evaluateIpInfo.
58
+ * operator hasn't configured that more specific category — mirrors
59
+ * the `datacenterSuppressedByCategory` rule in evaluateIpInfo.
58
60
  *
59
61
  * `pathValid` is a protocol signal, not a category — it always contributes
60
62
  * regardless of trafficFilter. When `trafficFilter` is omitted, all
@@ -68,16 +70,16 @@ var computeDnsAsymmetry = (enriched, clientIpInfo, trafficFilter) => {
68
70
  const countDc = (ip) => {
69
71
  if (!ip?.isValid || !ip.isDatacenter) return false;
70
72
  if (!trafficFilter) return true;
71
- if (trafficFilter.blockDatacenter !== true) return false;
73
+ if (trafficFilter.datacenter === void 0) return false;
72
74
  if (require_checkTrafficFilter.isDatacenterDenylisted(ip, trafficFilter.datacenterNameDenylist)) return true;
73
- if (ip.isVPN && trafficFilter.blockVpn !== true || ip.isProxy && trafficFilter.blockProxy !== true || ip.isTor && trafficFilter.blockTor !== true || ip.isCrawler && trafficFilter.blockCrawler !== true) return false;
75
+ if (ip.isVPN && trafficFilter.vpn === void 0 || ip.isProxy && trafficFilter.proxy === void 0 || ip.isTor && trafficFilter.tor === void 0 || ip.isCrawler && trafficFilter.crawler === void 0) return false;
74
76
  if (ip.providerType === "isp") return false;
75
77
  return !require_checkTrafficFilter.isDatacenterAllowlisted(ip, trafficFilter.datacenterNameAllowlist);
76
78
  };
77
79
  const countAbuser = (ip) => {
78
80
  if (!ip?.isValid || !ip.isAbuser) return false;
79
81
  if (!trafficFilter) return true;
80
- if (trafficFilter.blockAbuser === false) return false;
82
+ if (trafficFilter.abuser === void 0) return false;
81
83
  const threshold = trafficFilter.abuserScoreThreshold ?? _prosopo_types.trafficFilterAbuserScoreThresholdDefault;
82
84
  return Math.max(ip.abuserScore ?? 0, ip.companyAbuserScore ?? 0) >= threshold;
83
85
  };
@@ -127,6 +127,7 @@ var PuzzleCaptchaManager = class extends require_captchaManager.CaptchaManager {
127
127
  const dappAccount = (0, _prosopo_util.at)(challengeSplit, 2);
128
128
  this.usageCounters.incrManyAsync(dappAccount, require_usageCounters.buildAllWindowIncrements("solved", _prosopo_types.CaptchaType.puzzle, ipAddress.address, userAccount));
129
129
  }
130
+ await this.db.updatePuzzleCaptchaRecord(challenge, { puzzleEvents });
130
131
  if (behavioralData) try {
131
132
  const bundle = await this.resolveBundleBySessionId(challengeRecord.sessionId);
132
133
  const decryptedData = await this.decryptBehavioralData(behavioralData, bundle);
@@ -153,8 +154,7 @@ var PuzzleCaptchaManager = class extends require_captchaManager.CaptchaManager {
153
154
  };
154
155
  await this.db.updatePuzzleCaptchaRecord(challenge, {
155
156
  behavioralDataPacked: packedData,
156
- deviceCapability: decryptedData.deviceCapability,
157
- puzzleEvents
157
+ deviceCapability: decryptedData.deviceCapability
158
158
  });
159
159
  }
160
160
  } catch (error) {
@@ -163,7 +163,6 @@ var PuzzleCaptchaManager = class extends require_captchaManager.CaptchaManager {
163
163
  err: error
164
164
  }));
165
165
  }
166
- else await this.db.updatePuzzleCaptchaRecord(challenge, { puzzleEvents });
167
166
  if (clientMetaData?.hp) await this.db.updatePuzzleCaptchaRecord(challenge, { clientMetaData: { hp: clientMetaData.hp } });
168
167
  await this.db.updatePuzzleCaptchaRecordResult(challenge, result, false, true, userTimestampSignature, coords);
169
168
  if (challengeRecord.sessionId) {