@webdecoy/fcaptcha 1.28.1 → 1.28.2

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 (2) hide show
  1. package/index.js +13 -2
  2. package/package.json +2 -2
package/index.js CHANGED
@@ -246,6 +246,7 @@ class ScoringEngine {
246
246
  // Verify signals and return score
247
247
  verify(signals, ip, siteKey, userAgent, headers = {}, powSolution = null) {
248
248
  const detections = [];
249
+ let powSatisfied = false;
249
250
 
250
251
  // Run all detection modules
251
252
  detections.push(...this._detectVisionAI(signals));
@@ -272,6 +273,7 @@ class ScoringEngine {
272
273
  reason: `PoW verification failed: ${powResult.reason}`
273
274
  });
274
275
  } else if (powResult.serverElapsed < BASE_MIN_AGE_MS) {
276
+ powSatisfied = true;
275
277
  // Under the universal baseline nothing legitimate can have happened —
276
278
  // no human completes an interaction that fast.
277
279
  detections.push({
@@ -281,6 +283,7 @@ class ScoringEngine {
281
283
  reason: `Challenge solved too fast (${powResult.serverElapsed}ms server-side)`
282
284
  });
283
285
  } else if (powResult.serverElapsed < powResult.minAgeMs) {
286
+ powSatisfied = true;
284
287
  // Between the baseline and this source's own elevated floor is weaker
285
288
  // evidence: a client predating adaptive cost, or one served from a
286
289
  // stale cache, does not know to wait. It contributes rather than
@@ -291,6 +294,8 @@ class ScoringEngine {
291
294
  confidence: 0.5,
292
295
  reason: `Challenge submitted before the required delay for this source (${powResult.serverElapsed}ms of ${powResult.minAgeMs}ms)`
293
296
  });
297
+ } else {
298
+ powSatisfied = true;
294
299
  }
295
300
  } else {
296
301
  detections.push({
@@ -336,7 +341,12 @@ class ScoringEngine {
336
341
  else if (finalScore < 0.6) recommendation = 'challenge';
337
342
  else recommendation = 'block';
338
343
 
339
- const success = finalScore < 0.5;
344
+ // PoW is a precondition, not weighted evidence. Expressing a missing or
345
+ // invalid solution only as a bot-category score lets the category weight
346
+ // dilute it below the allow threshold and used to mint a token for an empty
347
+ // request. Keep this gate independent of scoring so weight changes cannot
348
+ // reopen the bypass.
349
+ const success = finalScore < 0.5 && powSatisfied;
340
350
  const token = success ? this._generateToken(ip, siteKey, finalScore) : null;
341
351
 
342
352
  // Feed the ledger so the next challenge this source asks for is priced on
@@ -350,7 +360,8 @@ class ScoringEngine {
350
360
  timestamp: Math.floor(Date.now() / 1000),
351
361
  recommendation,
352
362
  categoryScores,
353
- detections
363
+ detections,
364
+ ...(!powSatisfied ? { reason: 'pow_not_satisfied' } : {})
354
365
  };
355
366
  }
356
367
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webdecoy/fcaptcha",
3
- "version": "1.28.1",
3
+ "version": "1.28.2",
4
4
  "description": "Open source CAPTCHA with PoW, bot detection, and Vision AI protection",
5
5
  "main": "index.js",
6
6
  "exports": {
@@ -15,7 +15,7 @@
15
15
  "test:clientip": "node clientip.test.js",
16
16
  "test:limits": "node limits.test.js",
17
17
  "test:detection": "node detection.test.js",
18
- "test": "node clientip.test.js && node limits.test.js && node detection.test.js && node inputforensics.test.js && node webbotauth.test.js && node siteverify.test.js && node --test suspicion.test.js",
18
+ "test": "node clientip.test.js && node limits.test.js && node detection.test.js && node inputforensics.test.js && node webbotauth.test.js && node siteverify.test.js && node index.test.js && node --test suspicion.test.js",
19
19
  "test:siteverify": "node siteverify.test.js",
20
20
  "test:forensics": "node inputforensics.test.js",
21
21
  "test:suspicion": "node --test suspicion.test.js"