@webdecoy/fcaptcha 1.28.0 → 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 (3) hide show
  1. package/index.js +13 -2
  2. package/package.json +2 -2
  3. package/server.js +12 -9
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.0",
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"
package/server.js CHANGED
@@ -1425,11 +1425,13 @@ function runVerification(signals, ip, siteKey, userAgent, headers = {}, ja3Hash
1425
1425
  category: 'bot',
1426
1426
  score: 0.7,
1427
1427
  confidence: 0.8,
1428
- reason: `PoW verification failed: ${powVerification.reason}`,
1429
- // A solution that does not verify against a challenge this server
1430
- // issued is not weak evidence of automation, it is proof the challenge
1431
- // was not completed. See DISPOSITIVE_FLOOR.
1432
- dispositive: true
1428
+ reason: `PoW verification failed: ${powVerification.reason}`
1429
+ // Deliberately NOT dispositive. The gate already withholds the token,
1430
+ // which is the security requirement. Flooring the score as well says
1431
+ // "blatant bot", and a failed proof of work does not mean that: the
1432
+ // challenge expires after five minutes, challenges live only in memory
1433
+ // so every deploy invalidates the outstanding ones, and a double-click
1434
+ // replays a solution. All ordinary things that happen to real people.
1433
1435
  });
1434
1436
  } else {
1435
1437
  powSatisfied = true;
@@ -1447,8 +1449,8 @@ function runVerification(signals, ip, siteKey, userAgent, headers = {}, ja3Hash
1447
1449
  category: 'bot',
1448
1450
  score: 0.9,
1449
1451
  confidence: 0.9,
1450
- reason: 'Challenge nonce mismatch (signals not bound to challenge)',
1451
- dispositive: true
1452
+ reason: 'Challenge nonce mismatch (signals not bound to challenge)'
1453
+ // Not dispositive — see above; a stale challenge produces this too.
1452
1454
  });
1453
1455
  }
1454
1456
  }
@@ -1480,8 +1482,9 @@ function runVerification(signals, ip, siteKey, userAgent, headers = {}, ja3Hash
1480
1482
  category: 'bot',
1481
1483
  score: 0.9,
1482
1484
  confidence: 0.95,
1483
- reason: 'No PoW solution provided',
1484
- dispositive: true
1485
+ reason: 'No PoW solution provided'
1486
+ // Not dispositive — the gate refuses the token; inflating the score on
1487
+ // top only mislabels whoever hit a stale page.
1485
1488
  });
1486
1489
  }
1487
1490