@prosopo/provider 4.14.3 → 4.15.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 (66) hide show
  1. package/.turbo/turbo-build$colon$cjs.log +11 -10
  2. package/.turbo/turbo-build$colon$tsc.log +24 -24
  3. package/.turbo/turbo-build.log +12 -11
  4. package/CHANGELOG.md +87 -0
  5. package/dist/api/admin/apiRemoveDetectorKeyEndpoint.d.ts.map +1 -1
  6. package/dist/api/blacklistRequestInspector.d.ts +8 -3
  7. package/dist/api/blacklistRequestInspector.d.ts.map +1 -1
  8. package/dist/api/blacklistRequestInspector.js +49 -11
  9. package/dist/api/blacklistRequestInspector.js.map +1 -1
  10. package/dist/api/captcha/getFrictionlessCaptchaChallenge/handler.d.ts.map +1 -1
  11. package/dist/api/captcha/getFrictionlessCaptchaChallenge/handler.js +19 -13
  12. package/dist/api/captcha/getFrictionlessCaptchaChallenge/handler.js.map +1 -1
  13. package/dist/api/captcha/getFrictionlessCaptchaChallenge/shortCircuit.d.ts +2 -0
  14. package/dist/api/captcha/getFrictionlessCaptchaChallenge/shortCircuit.d.ts.map +1 -1
  15. package/dist/api/captcha/getFrictionlessCaptchaChallenge/shortCircuit.js +7 -1
  16. package/dist/api/captcha/getFrictionlessCaptchaChallenge/shortCircuit.js.map +1 -1
  17. package/dist/api/captcha/submitPoWCaptchaSolution.d.ts +4 -1
  18. package/dist/api/captcha/submitPoWCaptchaSolution.d.ts.map +1 -1
  19. package/dist/api/captcha/submitPoWCaptchaSolution.js +12 -7
  20. package/dist/api/captcha/submitPoWCaptchaSolution.js.map +1 -1
  21. package/dist/api/handshakeTimingMiddleware.d.ts +2 -2
  22. package/dist/api/handshakeTimingMiddleware.js +11 -11
  23. package/dist/api/hardBlockVerdictCache.d.ts +18 -0
  24. package/dist/api/hardBlockVerdictCache.d.ts.map +1 -0
  25. package/dist/api/hardBlockVerdictCache.js +119 -0
  26. package/dist/api/hardBlockVerdictCache.js.map +1 -0
  27. package/dist/api/startProviderApi.d.ts.map +1 -1
  28. package/dist/api/startProviderApi.js +17 -1
  29. package/dist/api/startProviderApi.js.map +1 -1
  30. package/dist/cjs/api/blacklistRequestInspector.cjs +49 -11
  31. package/dist/cjs/api/captcha/getFrictionlessCaptchaChallenge/handler.cjs +19 -13
  32. package/dist/cjs/api/captcha/getFrictionlessCaptchaChallenge/shortCircuit.cjs +7 -1
  33. package/dist/cjs/api/captcha/submitPoWCaptchaSolution.cjs +12 -7
  34. package/dist/cjs/api/handshakeTimingMiddleware.cjs +11 -11
  35. package/dist/cjs/api/hardBlockVerdictCache.cjs +119 -0
  36. package/dist/cjs/api/startProviderApi.cjs +17 -1
  37. package/dist/cjs/index.cjs +1 -0
  38. package/dist/cjs/tasks/frictionless/frictionlessTasks.cjs +9 -9
  39. package/dist/cjs/tasks/spam/checkTrafficFilter.cjs +6 -9
  40. package/dist/index.js +2 -1
  41. package/dist/tasks/captchaManager.d.ts +1 -0
  42. package/dist/tasks/captchaManager.d.ts.map +1 -1
  43. package/dist/tasks/captchaManager.js.map +1 -1
  44. package/dist/tasks/frictionless/frictionlessTasks.d.ts +1 -1
  45. package/dist/tasks/frictionless/frictionlessTasks.js +9 -9
  46. package/dist/tasks/spam/checkTrafficFilter.d.ts.map +1 -1
  47. package/dist/tasks/spam/checkTrafficFilter.js +6 -9
  48. package/dist/tasks/spam/checkTrafficFilter.js.map +1 -1
  49. package/dist/tests/integration/api/blacklistRequestInspector.integration.test.js +2 -1
  50. package/dist/tests/integration/api/blacklistRequestInspector.integration.test.js.map +1 -1
  51. package/dist/tests/integration/api/blacklistRequestInspector.storm.integration.test.d.ts +2 -0
  52. package/dist/tests/integration/api/blacklistRequestInspector.storm.integration.test.d.ts.map +1 -0
  53. package/dist/tests/integration/api/blacklistRequestInspector.storm.integration.test.js +295 -0
  54. package/dist/tests/integration/api/blacklistRequestInspector.storm.integration.test.js.map +1 -0
  55. package/dist/tests/unit/api/blacklistRequestInspector.unit.test.js +8 -2
  56. package/dist/tests/unit/api/blacklistRequestInspector.unit.test.js.map +1 -1
  57. package/dist/tests/unit/api/handshakeTimingMiddleware.unit.test.js +22 -22
  58. package/dist/tests/unit/api/hardBlockVerdictCache.unit.test.d.ts +2 -0
  59. package/dist/tests/unit/api/hardBlockVerdictCache.unit.test.d.ts.map +1 -0
  60. package/dist/tests/unit/api/hardBlockVerdictCache.unit.test.js +192 -0
  61. package/dist/tests/unit/api/hardBlockVerdictCache.unit.test.js.map +1 -0
  62. package/dist/tests/unit/tasks/powCaptcha/powTasks.unit.test.js +2 -0
  63. package/dist/tests/unit/tasks/powCaptcha/powTasks.unit.test.js.map +1 -1
  64. package/dist/tests/unit/tasks/spam/checkTrafficFilter.unit.test.js +73 -1
  65. package/dist/tests/unit/tasks/spam/checkTrafficFilter.unit.test.js.map +1 -1
  66. package/package.json +13 -13
@@ -4,6 +4,7 @@ const node_crypto = require("node:crypto");
4
4
  const types = require("@prosopo/types");
5
5
  const userAccessPolicy = require("@prosopo/user-access-policy");
6
6
  const compositeIpAddress = require("../compositeIpAddress.cjs");
7
+ const hardBlockVerdictCache = require("./hardBlockVerdictCache.cjs");
7
8
  const metrics = require("./metrics.cjs");
8
9
  const getRequestUserScope = (requestHeaders, ja4, ip, user, headHash, coords, countryCode, asn) => {
9
10
  const userAgent = requestHeaders["user-agent"] ? requestHeaders["user-agent"].toString() : void 0;
@@ -130,8 +131,30 @@ const rankCandidateRules = (rules, request, requestClientId) => rules.filter((ru
130
131
  }
131
132
  return ruleHarshness(b) - ruleHarshness(a);
132
133
  });
134
+ const verdictCache = new hardBlockVerdictCache.HardBlockVerdictCache();
135
+ const getVerdictCache = () => verdictCache;
136
+ const REQUEST_MEMO_SYMBOL = Symbol.for("prosopo.accessRuleRequestMemo");
137
+ const getOrCreateRequestMemo = (requestMemoHost) => {
138
+ let memo = requestMemoHost[REQUEST_MEMO_SYMBOL];
139
+ if (memo === void 0) {
140
+ memo = /* @__PURE__ */ new Map();
141
+ requestMemoHost[REQUEST_MEMO_SYMBOL] = memo;
142
+ }
143
+ return memo;
144
+ };
133
145
  const getPrioritisedAccessRule = async (userAccessRulesStorage, userScope, clientId, options) => {
134
146
  const parsedUserScope = userAccessPolicy.userScopeInput.parse(userScope);
147
+ const blockOnly = options?.blockOnly ?? false;
148
+ const skipCache = options?.skipCache ?? false;
149
+ const requestMemoHost = options?.requestMemoHost;
150
+ const cacheKey = hardBlockVerdictCache.hardBlockCacheKey(clientId, parsedUserScope, blockOnly);
151
+ const requestMemo = requestMemoHost ? getOrCreateRequestMemo(requestMemoHost) : void 0;
152
+ if (requestMemo !== void 0) {
153
+ const memoHit = requestMemo.get(cacheKey);
154
+ if (memoHit !== void 0) {
155
+ return memoHit;
156
+ }
157
+ }
135
158
  const filter = {
136
159
  ...clientId && {
137
160
  policyScope: {
@@ -141,15 +164,25 @@ const getPrioritisedAccessRule = async (userAccessRulesStorage, userScope, clien
141
164
  policyScopeMatch: userAccessPolicy.FilterScopeMatch.Greedy,
142
165
  userScope: parsedUserScope,
143
166
  userScopeMatch: userAccessPolicy.FilterScopeMatch.Greedy,
144
- ...options?.blockOnly && { blockOnly: true }
167
+ ...blockOnly && { blockOnly: true }
145
168
  };
146
- const candidates = await userAccessRulesStorage.findRules(
147
- filter,
148
- true,
149
- // matchingFieldsOnly — engages server-side specificity rank
150
- true
151
- );
152
- return rankCandidateRules(candidates, parsedUserScope, clientId);
169
+ const compute = async () => {
170
+ const candidates = await userAccessRulesStorage.findRules(
171
+ filter,
172
+ true,
173
+ // matchingFieldsOnly — engages the split-query hot path
174
+ true
175
+ );
176
+ return rankCandidateRules(candidates, parsedUserScope, clientId);
177
+ };
178
+ let ranked;
179
+ if (skipCache) {
180
+ ranked = await compute();
181
+ } else {
182
+ ranked = await verdictCache.getOrCompute(cacheKey, compute);
183
+ }
184
+ requestMemo?.set(cacheKey, ranked);
185
+ return ranked;
153
186
  };
154
187
  class BlacklistRequestInspector {
155
188
  constructor(userAccessRulesStorage, environmentReadinessWaiter, db) {
@@ -169,7 +202,11 @@ class BlacklistRequestInspector {
169
202
  request.headers,
170
203
  request.body,
171
204
  request.logger,
172
- request.ipInfo
205
+ request.ipInfo,
206
+ // Share the per-request memo so a downstream captcha task
207
+ // hitting checkForHardBlock in the same request reuses this
208
+ // lookup rather than re-querying Redis.
209
+ request
173
210
  );
174
211
  if (shouldAbortRequest) {
175
212
  res.status(403).json({ error: "Forbidden" });
@@ -177,7 +214,7 @@ class BlacklistRequestInspector {
177
214
  }
178
215
  next();
179
216
  }
180
- async shouldAbortRequest(requestedRoute, rawIp, ja4, requestHeaders, requestBody, logger, ipInfo) {
217
+ async shouldAbortRequest(requestedRoute, rawIp, ja4, requestHeaders, requestBody, logger, ipInfo, requestMemoHost) {
181
218
  if (this.isApiUnrelatedRoute(requestedRoute)) {
182
219
  return false;
183
220
  }
@@ -222,7 +259,7 @@ class BlacklistRequestInspector {
222
259
  // candidate pool so the SERVER_SIDE_RANK_TOP_N cap can't
223
260
  // crowd out hard-block rules in clients with dense Restrict
224
261
  // rule populations.
225
- { blockOnly: true }
262
+ { blockOnly: true, requestMemoHost }
226
263
  );
227
264
  const enforceable = (accessPolicies ?? []).filter(
228
265
  (p) => !p.deferToVerify
@@ -352,4 +389,5 @@ class BlacklistRequestInspector {
352
389
  exports.BlacklistRequestInspector = BlacklistRequestInspector;
353
390
  exports.getPrioritisedAccessRule = getPrioritisedAccessRule;
354
391
  exports.getRequestUserScope = getRequestUserScope;
392
+ exports.getVerdictCache = getVerdictCache;
355
393
  exports.rankCandidateRules = rankCandidateRules;
@@ -168,11 +168,11 @@ const getFrictionlessCaptchaChallenge = (env, userAccessRulesStorage) => async (
168
168
  // Timing values are per-connection so they come from
169
169
  // the current request even in the dedup replay path —
170
170
  // dedup.session was created on a different TCP conn.
171
- ...req.tcpToChelloMs !== void 0 && {
172
- tcpToChelloMs: req.tcpToChelloMs
171
+ ...req.tcpToChelloUs !== void 0 && {
172
+ tcpToChelloUs: req.tcpToChelloUs
173
173
  },
174
- ...req.chelloToHandshakeMs !== void 0 && {
175
- chelloToHandshakeMs: req.chelloToHandshakeMs
174
+ ...req.chelloToHandshakeUs !== void 0 && {
175
+ chelloToHandshakeUs: req.chelloToHandshakeUs
176
176
  },
177
177
  // currentUrl uses the cached session's value to
178
178
  // match the rest of the dedup routing input (score,
@@ -250,7 +250,13 @@ const getFrictionlessCaptchaChallenge = (env, userAccessRulesStorage) => async (
250
250
  flatHeaders,
251
251
  sessionMode,
252
252
  userSitekeyIpHash,
253
- logger: req.logger
253
+ logger: req.logger,
254
+ ...req.tcpToChelloUs !== void 0 && {
255
+ tcpToChelloUs: req.tcpToChelloUs
256
+ },
257
+ ...req.chelloToHandshakeUs !== void 0 && {
258
+ chelloToHandshakeUs: req.chelloToHandshakeUs
259
+ }
254
260
  },
255
261
  res
256
262
  );
@@ -363,11 +369,11 @@ const getFrictionlessCaptchaChallenge = (env, userAccessRulesStorage) => async (
363
369
  ...entropyMathRandomFirst !== void 0 && {
364
370
  entropyMathRandomFirst
365
371
  },
366
- ...req.tcpToChelloMs !== void 0 && {
367
- tcpToChelloMs: req.tcpToChelloMs
372
+ ...req.tcpToChelloUs !== void 0 && {
373
+ tcpToChelloUs: req.tcpToChelloUs
368
374
  },
369
- ...req.chelloToHandshakeMs !== void 0 && {
370
- chelloToHandshakeMs: req.chelloToHandshakeMs
375
+ ...req.chelloToHandshakeUs !== void 0 && {
376
+ chelloToHandshakeUs: req.chelloToHandshakeUs
371
377
  }
372
378
  });
373
379
  const ipInfoMobile = req.ipInfo && "isValid" in req.ipInfo && req.ipInfo.isValid ? req.ipInfo.isMobile : void 0;
@@ -385,11 +391,11 @@ const getFrictionlessCaptchaChallenge = (env, userAccessRulesStorage) => async (
385
391
  headers: flatHeaders,
386
392
  userAgent: safeUserAgent,
387
393
  ...req.ja4 && { ja4: req.ja4 },
388
- ...req.tcpToChelloMs !== void 0 && {
389
- tcpToChelloMs: req.tcpToChelloMs
394
+ ...req.tcpToChelloUs !== void 0 && {
395
+ tcpToChelloUs: req.tcpToChelloUs
390
396
  },
391
- ...req.chelloToHandshakeMs !== void 0 && {
392
- chelloToHandshakeMs: req.chelloToHandshakeMs
397
+ ...req.chelloToHandshakeUs !== void 0 && {
398
+ chelloToHandshakeUs: req.chelloToHandshakeUs
393
399
  },
394
400
  ...currentUrl && { currentUrl }
395
401
  }
@@ -21,7 +21,13 @@ const runConfiguredCaptchaTypeShortCircuit = async (input, res) => {
21
21
  ipInfo: input.ipInfo,
22
22
  headers: input.flatHeaders,
23
23
  mode: input.sessionMode,
24
- userSitekeyIpHash: input.userSitekeyIpHash
24
+ userSitekeyIpHash: input.userSitekeyIpHash,
25
+ ...input.tcpToChelloUs !== void 0 && {
26
+ tcpToChelloUs: input.tcpToChelloUs
27
+ },
28
+ ...input.chelloToHandshakeUs !== void 0 && {
29
+ chelloToHandshakeUs: input.chelloToHandshakeUs
30
+ }
25
31
  };
26
32
  input.logger.info(() => ({
27
33
  msg: "Frictionless decision",
@@ -87,11 +87,11 @@ const submitPoWCaptchaSolution = (env) => async (req, res, next) => {
87
87
  userAgent,
88
88
  ...req.ja4 && { ja4: req.ja4 },
89
89
  ...fingerprintProof && { fingerprintProof },
90
- ...req.tcpToChelloMs !== void 0 && {
91
- tcpToChelloMs: req.tcpToChelloMs
90
+ ...req.tcpToChelloUs !== void 0 && {
91
+ tcpToChelloUs: req.tcpToChelloUs
92
92
  },
93
- ...req.chelloToHandshakeMs !== void 0 && {
94
- chelloToHandshakeMs: req.chelloToHandshakeMs
93
+ ...req.chelloToHandshakeUs !== void 0 && {
94
+ chelloToHandshakeUs: req.chelloToHandshakeUs
95
95
  }
96
96
  }
97
97
  });
@@ -120,7 +120,10 @@ const submitPoWCaptchaSolution = (env) => async (req, res, next) => {
120
120
  }
121
121
  }));
122
122
  }
123
- const escalation = await buildEscalation(tasks$1, result, challenge);
123
+ const escalation = await buildEscalation(tasks$1, result, challenge, {
124
+ tcpToChelloUs: req.tcpToChelloUs,
125
+ chelloToHandshakeUs: req.chelloToHandshakeUs
126
+ });
124
127
  const response = {
125
128
  status: "ok",
126
129
  // On escalation the user is not done — they still need to clear
@@ -148,7 +151,7 @@ const submitPoWCaptchaSolution = (env) => async (req, res, next) => {
148
151
  );
149
152
  }
150
153
  };
151
- const buildEscalation = async (tasks2, result, challenge) => {
154
+ const buildEscalation = async (tasks2, result, challenge, handshakeTiming) => {
152
155
  if (!result.verified || !result.routingOutput) return void 0;
153
156
  const routedType = result.routingOutput.captchaType;
154
157
  if (routedType !== types.CaptchaType.image && routedType !== types.CaptchaType.puzzle) {
@@ -187,7 +190,9 @@ const buildEscalation = async (tasks2, result, challenge) => {
187
190
  originSession.entropyCryptoFingerprint,
188
191
  originSession.entropyWallClockOffsetMs,
189
192
  originSession.entropyMathRandomFirst,
190
- originSession.currentUrl
193
+ originSession.currentUrl,
194
+ handshakeTiming?.tcpToChelloUs,
195
+ handshakeTiming?.chelloToHandshakeUs
191
196
  );
192
197
  if (tasks2.writeQueue) {
193
198
  await tasks2.writeQueue.cacheSessionEscalation(
@@ -2,8 +2,8 @@
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const apiExpressRouter = require("@prosopo/api-express-router");
4
4
  const logger = require("@prosopo/logger");
5
- const HEADER_TCP_TO_CHELLO = "x-tls-tcp-to-chello-ms";
6
- const HEADER_CHELLO_TO_HANDSHAKE = "x-tls-chello-to-handshake-ms";
5
+ const HEADER_TCP_TO_CHELLO = "x-tls-tcp-to-chello-us";
6
+ const HEADER_CHELLO_TO_HANDSHAKE = "x-tls-chello-to-handshake-us";
7
7
  const parseTimingHeader = (raw, logger2, headerName) => {
8
8
  if (raw === void 0) {
9
9
  return void 0;
@@ -25,12 +25,12 @@ const parseTimingHeader = (raw, logger2, headerName) => {
25
25
  const getHandshakeTiming = (headers, logger$1) => {
26
26
  const log = logger$1 ?? logger.getLogger("info", "provider:handshake-timing");
27
27
  return {
28
- tcpToChelloMs: parseTimingHeader(
28
+ tcpToChelloUs: parseTimingHeader(
29
29
  headers[HEADER_TCP_TO_CHELLO],
30
30
  log,
31
31
  HEADER_TCP_TO_CHELLO
32
32
  ),
33
- chelloToHandshakeMs: parseTimingHeader(
33
+ chelloToHandshakeUs: parseTimingHeader(
34
34
  headers[HEADER_CHELLO_TO_HANDSHAKE],
35
35
  log,
36
36
  HEADER_CHELLO_TO_HANDSHAKE
@@ -41,17 +41,17 @@ const handshakeTimingMiddleware = (env) => {
41
41
  return async (req, res, next) => {
42
42
  try {
43
43
  const timing = getHandshakeTiming(req.headers, req.logger);
44
- if (timing.tcpToChelloMs !== void 0) {
45
- req.tcpToChelloMs = timing.tcpToChelloMs;
44
+ if (timing.tcpToChelloUs !== void 0) {
45
+ req.tcpToChelloUs = timing.tcpToChelloUs;
46
46
  }
47
- if (timing.chelloToHandshakeMs !== void 0) {
48
- req.chelloToHandshakeMs = timing.chelloToHandshakeMs;
47
+ if (timing.chelloToHandshakeUs !== void 0) {
48
+ req.chelloToHandshakeUs = timing.chelloToHandshakeUs;
49
49
  }
50
- if (timing.tcpToChelloMs !== void 0 || timing.chelloToHandshakeMs !== void 0) {
50
+ if (timing.tcpToChelloUs !== void 0 || timing.chelloToHandshakeUs !== void 0) {
51
51
  req.logger = req.logger.with(
52
52
  {
53
- tcpToChelloMs: timing.tcpToChelloMs,
54
- chelloToHandshakeMs: timing.chelloToHandshakeMs
53
+ tcpToChelloUs: timing.tcpToChelloUs,
54
+ chelloToHandshakeUs: timing.chelloToHandshakeUs
55
55
  },
56
56
  "handshakeTiming"
57
57
  );
@@ -0,0 +1,119 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const DEFAULT_VERDICT_CACHE_TTL_MS = 1e4;
4
+ const DEFAULT_VERDICT_CACHE_MAX_ENTRIES = 5e4;
5
+ class HardBlockVerdictCache {
6
+ constructor(ttlMs = DEFAULT_VERDICT_CACHE_TTL_MS, maxEntries = DEFAULT_VERDICT_CACHE_MAX_ENTRIES) {
7
+ this.ttlMs = ttlMs;
8
+ this.maxEntries = maxEntries;
9
+ this.store = /* @__PURE__ */ new Map();
10
+ this.inflight = /* @__PURE__ */ new Map();
11
+ }
12
+ get(key) {
13
+ const entry = this.store.get(key);
14
+ if (entry === void 0) {
15
+ return void 0;
16
+ }
17
+ if (entry.expiresAt <= Date.now()) {
18
+ this.store.delete(key);
19
+ return void 0;
20
+ }
21
+ this.store.delete(key);
22
+ this.store.set(key, entry);
23
+ return entry.value;
24
+ }
25
+ set(key, value) {
26
+ this.store.delete(key);
27
+ if (this.store.size >= this.maxEntries) {
28
+ const oldest = this.store.keys().next().value;
29
+ if (oldest !== void 0) {
30
+ this.store.delete(oldest);
31
+ }
32
+ }
33
+ this.store.set(key, {
34
+ value,
35
+ expiresAt: Date.now() + this.ttlMs
36
+ });
37
+ }
38
+ /**
39
+ * Cache lookup with singleflight dedupe of concurrent misses.
40
+ *
41
+ * Fast path: cache hit → return immediately (with LRU move-to-tail
42
+ * via `get()`).
43
+ *
44
+ * Slow path: cache miss →
45
+ * 1. If an in-flight Promise exists for this key, join it. This
46
+ * is the coalescing behaviour — N concurrent identical misses
47
+ * all await the same underlying storage call.
48
+ * 2. Otherwise, invoke `compute()`, register the Promise, and
49
+ * populate the cache with the result on resolve. The Promise
50
+ * is removed from the in-flight map in a `finally` so a
51
+ * rejection doesn't wedge future callers.
52
+ *
53
+ * Rejection semantics: if `compute()` throws, all joined waiters
54
+ * see the same rejection. Callers should handle it — the underlying
55
+ * `getPrioritisedAccessRule` catches its own storage errors and
56
+ * returns `[]` (fail-open), so in practice this never propagates.
57
+ */
58
+ async getOrCompute(key, compute) {
59
+ const cached = this.get(key);
60
+ if (cached !== void 0) {
61
+ return cached;
62
+ }
63
+ const existing = this.inflight.get(key);
64
+ if (existing !== void 0) {
65
+ return existing;
66
+ }
67
+ const promise = (async () => {
68
+ const value = await compute();
69
+ this.set(key, value);
70
+ return value;
71
+ })();
72
+ this.inflight.set(key, promise);
73
+ try {
74
+ return await promise;
75
+ } finally {
76
+ if (this.inflight.get(key) === promise) {
77
+ this.inflight.delete(key);
78
+ }
79
+ }
80
+ }
81
+ // Test/ops hook — the running cache is not exposed on the wire, but
82
+ // admin tooling (e.g. a rule-mutation notifier) may need to flush
83
+ // after a bulk insert to bound the staleness window. Clears both
84
+ // the stored entries and any in-flight Promises so a rule mutation
85
+ // can't leave a stale computation in flight.
86
+ clear() {
87
+ this.store.clear();
88
+ this.inflight.clear();
89
+ }
90
+ size() {
91
+ return this.store.size;
92
+ }
93
+ // Test hook: number of in-flight computations. Not on the hot path.
94
+ inflightSize() {
95
+ return this.inflight.size;
96
+ }
97
+ }
98
+ const hardBlockCacheKey = (clientId, userScope, blockOnly) => {
99
+ const bo = blockOnly ? "1" : "0";
100
+ const parts = [
101
+ clientId ?? "",
102
+ userScope.numericIp === void 0 ? "" : userScope.numericIp.toString(),
103
+ userScope.numericIpMaskMin === void 0 ? "" : userScope.numericIpMaskMin.toString(),
104
+ userScope.numericIpMaskMax === void 0 ? "" : userScope.numericIpMaskMax.toString(),
105
+ userScope.userId ?? "",
106
+ userScope.ja4Hash ?? "",
107
+ userScope.headersHash ?? "",
108
+ userScope.userAgentHash ?? "",
109
+ userScope.headHash ?? "",
110
+ userScope.coords ?? "",
111
+ userScope.countryCode ?? "",
112
+ userScope.asn === void 0 ? "" : String(userScope.asn)
113
+ ];
114
+ return `${bo}|${parts.join("|")}`;
115
+ };
116
+ exports.DEFAULT_VERDICT_CACHE_MAX_ENTRIES = DEFAULT_VERDICT_CACHE_MAX_ENTRIES;
117
+ exports.DEFAULT_VERDICT_CACHE_TTL_MS = DEFAULT_VERDICT_CACHE_TTL_MS;
118
+ exports.HardBlockVerdictCache = HardBlockVerdictCache;
119
+ exports.hardBlockCacheKey = hardBlockCacheKey;
@@ -115,6 +115,17 @@ async function startProviderApi(env, admin = false, port) {
115
115
  ...api.getExpressApiRuleRateLimits()
116
116
  };
117
117
  const adminPaths = Object.values(types.AdminApiPaths);
118
+ const rateLimitHandler = (path2) => (req, res, _next, options) => {
119
+ env.logger.warn(() => ({
120
+ msg: "Rate limit exceeded",
121
+ data: {
122
+ path: path2,
123
+ ip: req.ip,
124
+ siteKey: req.headers["prosopo-site-key"]
125
+ }
126
+ }));
127
+ res.status(options.statusCode).send(options.message);
128
+ };
118
129
  for (const [path2, limit] of Object.entries(rateLimits)) {
119
130
  const enumPath = path2;
120
131
  if (adminPaths.includes(enumPath)) {
@@ -122,6 +133,7 @@ async function startProviderApi(env, admin = false, port) {
122
133
  enumPath,
123
134
  rateLimit({
124
135
  ...limit,
136
+ handler: rateLimitHandler(enumPath),
125
137
  keyGenerator: (req) => {
126
138
  const user = getUserFromJWT(req);
127
139
  return user || req.ip || "unknown";
@@ -133,6 +145,7 @@ async function startProviderApi(env, admin = false, port) {
133
145
  enumPath,
134
146
  rateLimit({
135
147
  ...limit,
148
+ handler: rateLimitHandler(enumPath),
136
149
  keyGenerator: (req) => {
137
150
  const siteKey = req.headers["prosopo-site-key"];
138
151
  return siteKey || req.ip || "unknown";
@@ -140,7 +153,10 @@ async function startProviderApi(env, admin = false, port) {
140
153
  })
141
154
  );
142
155
  } else {
143
- apiApp.use(enumPath, rateLimit(limit));
156
+ apiApp.use(
157
+ enumPath,
158
+ rateLimit({ ...limit, handler: rateLimitHandler(enumPath) })
159
+ );
144
160
  }
145
161
  }
146
162
  }
@@ -63,6 +63,7 @@ exports.compareIPs = ipComparison.compareIPs;
63
63
  exports.BlacklistRequestInspector = blacklistRequestInspector.BlacklistRequestInspector;
64
64
  exports.getPrioritisedAccessRule = blacklistRequestInspector.getPrioritisedAccessRule;
65
65
  exports.getRequestUserScope = blacklistRequestInspector.getRequestUserScope;
66
+ exports.getVerdictCache = blacklistRequestInspector.getVerdictCache;
66
67
  exports.rankCandidateRules = blacklistRequestInspector.rankCandidateRules;
67
68
  Object.defineProperty(exports, "FrictionlessReason", {
68
69
  enumerable: true,
@@ -73,8 +73,8 @@ class FrictionlessManager extends captchaManager.CaptchaManager {
73
73
  entropyCryptoFingerprint: params.entropyCryptoFingerprint,
74
74
  entropyWallClockOffsetMs: params.entropyWallClockOffsetMs,
75
75
  entropyMathRandomFirst: params.entropyMathRandomFirst,
76
- tcpToChelloMs: params.tcpToChelloMs,
77
- chelloToHandshakeMs: params.chelloToHandshakeMs
76
+ tcpToChelloUs: params.tcpToChelloUs,
77
+ chelloToHandshakeUs: params.chelloToHandshakeUs
78
78
  };
79
79
  }
80
80
  updateScore(score, scoreComponents) {
@@ -86,7 +86,7 @@ class FrictionlessManager extends captchaManager.CaptchaManager {
86
86
  checkLangRules(acceptLanguage) {
87
87
  return lang.checkLangRules(this.config, acceptLanguage);
88
88
  }
89
- async createSession(token, score, threshold, scoreComponents, ipAddress, captchaType, siteKey, solvedImagesCount, powDifficulty, userSitekeyIpHash, webView = false, iFrame = false, decryptedHeadHash = "", reason, blocked, deleted, ipInfo, headers, mode, simdReadings, entropyMathRandomFingerprint, entropyCryptoFingerprint, entropyWallClockOffsetMs, entropyMathRandomFirst, currentUrl, tcpToChelloMs, chelloToHandshakeMs) {
89
+ async createSession(token, score, threshold, scoreComponents, ipAddress, captchaType, siteKey, solvedImagesCount, powDifficulty, userSitekeyIpHash, webView = false, iFrame = false, decryptedHeadHash = "", reason, blocked, deleted, ipInfo, headers, mode, simdReadings, entropyMathRandomFingerprint, entropyCryptoFingerprint, entropyWallClockOffsetMs, entropyMathRandomFirst, currentUrl, tcpToChelloUs, chelloToHandshakeUs) {
90
90
  const sessionRecord = {
91
91
  sessionId: `${getSessionIDPrefix(this.config.host)}-${uuid.v4()}`,
92
92
  createdAt: /* @__PURE__ */ new Date(),
@@ -120,8 +120,8 @@ class FrictionlessManager extends captchaManager.CaptchaManager {
120
120
  entropyCryptoFingerprint,
121
121
  entropyWallClockOffsetMs,
122
122
  entropyMathRandomFirst,
123
- tcpToChelloMs,
124
- chelloToHandshakeMs
123
+ tcpToChelloUs,
124
+ chelloToHandshakeUs
125
125
  };
126
126
  await this.db.storeSessionRecord(sessionRecord);
127
127
  if (this.writeQueue) {
@@ -203,8 +203,8 @@ class FrictionlessManager extends captchaManager.CaptchaManager {
203
203
  effectiveParams.entropyWallClockOffsetMs,
204
204
  effectiveParams.entropyMathRandomFirst,
205
205
  effectiveParams.currentUrl,
206
- effectiveParams.tcpToChelloMs,
207
- effectiveParams.chelloToHandshakeMs
206
+ effectiveParams.tcpToChelloUs,
207
+ effectiveParams.chelloToHandshakeUs
208
208
  );
209
209
  if (this.routingContext && this.usageCounters) {
210
210
  this.usageCounters.incrManyAsync(
@@ -257,8 +257,8 @@ class FrictionlessManager extends captchaManager.CaptchaManager {
257
257
  effectiveParams.entropyWallClockOffsetMs,
258
258
  effectiveParams.entropyMathRandomFirst,
259
259
  effectiveParams.currentUrl,
260
- effectiveParams.tcpToChelloMs,
261
- effectiveParams.chelloToHandshakeMs
260
+ effectiveParams.tcpToChelloUs,
261
+ effectiveParams.chelloToHandshakeUs
262
262
  );
263
263
  }
264
264
  scoreIncreaseAccessPolicy(accessPolicy, baseBotScore, botScore, scoreComponents) {
@@ -23,7 +23,7 @@ const isDatacenterAllowlisted = (ipInfo, allowlist) => {
23
23
  );
24
24
  return candidates.some((c) => normalisedAllowlist.includes(c));
25
25
  };
26
- const evaluateIpInfo = (ipInfo, trafficFilter, options) => {
26
+ const evaluateIpInfo = (ipInfo, trafficFilter, isDnsExtra = false) => {
27
27
  if (!ipInfo || !ipInfo.isValid) {
28
28
  return { isBlocked: false };
29
29
  }
@@ -46,7 +46,8 @@ const evaluateIpInfo = (ipInfo, trafficFilter, options) => {
46
46
  return { isBlocked: true, reason: types.ResultReason.ABUSER_BLOCKED };
47
47
  }
48
48
  }
49
- if (trafficFilter.blockDatacenter && ipInfo.isDatacenter && ipInfo.providerType !== "isp" && !(options.suppressVpnDatacenterInteraction && ipInfo.isVPN && !trafficFilter.blockVpn) && !isDatacenterAllowlisted(ipInfo, trafficFilter.datacenterNameAllowlist)) {
49
+ const datacenterSuppressedByCategory = ipInfo.isVPN && !trafficFilter.blockVpn || ipInfo.isProxy && !trafficFilter.blockProxy || ipInfo.isTor && !trafficFilter.blockTor || ipInfo.isCrawler && !trafficFilter.blockCrawler;
50
+ if (trafficFilter.blockDatacenter && ipInfo.isDatacenter && ipInfo.providerType !== "isp" && !datacenterSuppressedByCategory && !isDatacenterAllowlisted(ipInfo, trafficFilter.datacenterNameAllowlist)) {
50
51
  return { isBlocked: true, reason: types.ResultReason.DATACENTER_BLOCKED };
51
52
  }
52
53
  if (trafficFilter.blockMobile && ipInfo.isMobile) {
@@ -55,15 +56,13 @@ const evaluateIpInfo = (ipInfo, trafficFilter, options) => {
55
56
  if (trafficFilter.blockSatellite && ipInfo.isSatellite) {
56
57
  return { isBlocked: true, reason: types.ResultReason.SATELLITE_BLOCKED };
57
58
  }
58
- if (trafficFilter.blockCrawler && ipInfo.isCrawler) {
59
+ if (!isDnsExtra && trafficFilter.blockCrawler && ipInfo.isCrawler) {
59
60
  return { isBlocked: true, reason: types.ResultReason.CRAWLER_BLOCKED };
60
61
  }
61
62
  return { isBlocked: false };
62
63
  };
63
64
  const checkTrafficFilter = (ipInfo, trafficFilter, extraIpInfos, dnsPathValid) => {
64
- const primary = evaluateIpInfo(ipInfo, trafficFilter, {
65
- suppressVpnDatacenterInteraction: true
66
- });
65
+ const primary = evaluateIpInfo(ipInfo, trafficFilter);
67
66
  if (primary.isBlocked) {
68
67
  return primary;
69
68
  }
@@ -71,9 +70,7 @@ const checkTrafficFilter = (ipInfo, trafficFilter, extraIpInfos, dnsPathValid) =
71
70
  return { isBlocked: false };
72
71
  }
73
72
  for (const extra of extraIpInfos ?? []) {
74
- const result = evaluateIpInfo(extra, trafficFilter, {
75
- suppressVpnDatacenterInteraction: false
76
- });
73
+ const result = evaluateIpInfo(extra, trafficFilter, true);
77
74
  if (result.isBlocked) {
78
75
  return result;
79
76
  }
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ import { ignoreMiddleware } from "./api/ignoreMiddleware.js";
19
19
  import { robotsMiddleware } from "./api/robotsMiddleware.js";
20
20
  import { getCompositeIpAddress, getIpAddressFromComposite } from "./compositeIpAddress.js";
21
21
  import { compareIPs } from "./services/ipComparison.js";
22
- import { BlacklistRequestInspector, getPrioritisedAccessRule, getRequestUserScope, rankCandidateRules } from "./api/blacklistRequestInspector.js";
22
+ import { BlacklistRequestInspector, getPrioritisedAccessRule, getRequestUserScope, getVerdictCache, rankCandidateRules } from "./api/blacklistRequestInspector.js";
23
23
  import "./tasks/frictionless/frictionlessTasks.js";
24
24
  import { FrictionlessReason } from "@prosopo/types";
25
25
  import { Tasks } from "./tasks/tasks.js";
@@ -48,6 +48,7 @@ export {
48
48
  getPrioritisedAccessRule,
49
49
  getRequestUserScope,
50
50
  getUserFromJWT,
51
+ getVerdictCache,
51
52
  handshakeTimingMiddleware,
52
53
  headerCheckMiddleware,
53
54
  ignoreMiddleware,
@@ -42,6 +42,7 @@ export declare class CaptchaManager {
42
42
  };
43
43
  getPrioritisedAccessPolicies(userAccessRulesStorage: AccessRulesStorage, clientId: string, userScope: UserScope | UserScopeRecord, options?: {
44
44
  blockOnly?: boolean;
45
+ requestMemoHost?: object;
45
46
  }): Promise<import("@prosopo/user-access-policy").AccessRule[]>;
46
47
  getDetectorKeys(): Promise<string[]>;
47
48
  decryptSimdReadings(encryptedData: string, decryptKeys: (string | undefined)[]): Promise<SimdReadingsResult | null>;
@@ -1 +1 @@
1
- {"version":3,"file":"captchaManager.d.ts","sourceRoot":"","sources":["../../src/tasks/captchaManager.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"captchaManager.d.ts","sourceRoot":"","sources":["../../src/tasks/captchaManager.ts"],"names":[],"mappings":"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"}