@webdecoy/fcaptcha 1.35.0 → 1.36.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.
package/engine.js CHANGED
@@ -495,7 +495,10 @@ function detectCDP(signals) {
495
495
  if (env.cdpRuntime && env.cdpRuntime.consoleAttached) {
496
496
  detections.push({
497
497
  category: 'cdp', score: 0.6, confidence: 0.5,
498
- reason: 'CDP/DevTools console consumer attached (automation protocol or open DevTools)'
498
+ reason: 'CDP/DevTools console consumer attached (automation protocol or open DevTools)',
499
+ // A developer with DevTools open produces exactly this signal, so it
500
+ // cannot be one of the independent views the corroboration floor counts.
501
+ nonCorroborating: true
499
502
  });
500
503
  }
501
504
 
@@ -912,33 +915,47 @@ function applyDispositiveFloor(score, detections) {
912
915
  * product. It treats absence of environmental evidence as evidence of absence — but
913
916
  * on this adversary a clean environment is the attack working, not innocence.
914
917
  *
915
- * The rule: when two or more behavioural categories independently reach 0.5, floor
916
- * the score at 0.6.
918
+ * The rule: when two or more behavioural categories independently reach 0.4, floor
919
+ * the score at 0.6. A signal a developer's own tooling produces is not an
920
+ * independent view: the DevTools console-attach probe is marked nonCorroborating,
921
+ * so it still scores but cannot be one of the two that agree.
917
922
  *
918
923
  * Constants swept over a 40-point grid against the labelled corpus
919
924
  * (bench/tools/sweep-corroboration.js), not reasoned about. No human in the
920
925
  * 126-sample panel reaches two agreeing behavioural categories at any threshold
921
- * tested, while 66 of 75 agents do at 0.5. Requiring three fails outright — all
922
- * sixteen such combinations leave the adversary allowed. The floor value does not
923
- * affect separation, so 0.6 is a policy choice: the block boundary, kept distinct
924
- * from the 0.9 reserved for a browser that declares its own automation.
926
+ * tested, and 0.3, 0.4 and 0.5 catch the same 66 of 75 agents. The bar moved from
927
+ * 0.5 to 0.4 on a live measurement the corpus cannot make (2026-09-09): an
928
+ * extension-driven click in a real Chrome on the public demo scored vision_ai 0.40
929
+ * and behavioral 0.44 with every environmental category clean — weighted sum
930
+ * 0.189, allowed at 0.5, floored at 0.4. The exclusion is what keeps the lower
931
+ * bar honest: the human-looking sessions beside it sat at behavioral 0.37 with cdp
932
+ * 0.30 from the console probe alone. Requiring three fails outright — all sixteen
933
+ * such combinations leave the adversary allowed. The floor value does not affect
934
+ * separation, so 0.6 is a policy choice: the block boundary, kept distinct from
935
+ * the 0.9 reserved for a browser that declares its own automation.
925
936
  *
926
937
  * Caveat: the adversary is one synthetic, hand-authored sample. This shows the
927
938
  * arithmetic works on the shape the corpus describes, not that it works on a real
928
939
  * source-patched browser.
929
940
  */
930
- const CORROBORATION_AGREE_AT = 0.5;
941
+ const CORROBORATION_AGREE_AT = 0.4;
931
942
  const CORROBORATION_MIN_AGREE = 2;
932
943
  const CORROBORATION_FLOOR = 0.6;
944
+ // Absorbs the few ulps a noisy-OR product lands from a round number (0.75×0.8 is
945
+ // 0.3999999999999999, not 0.4), so agreement does not depend on arithmetic order.
946
+ const CORROBORATION_EPSILON = 1e-9;
933
947
 
934
948
  // Categories a browser trips by how it moves rather than by what it is. A
935
949
  // patched binary can hide what it is; it cannot hide that nothing is moving the
936
950
  // pointer like a hand.
937
951
  const BEHAVIOURAL_CATEGORIES = ['vision_ai', 'behavioral', 'automation', 'cdp'];
938
952
 
939
- function applyCorroborationFloor(score, categoryScores) {
953
+ function applyCorroborationFloor(score, detections) {
954
+ // Only independent views count. A signal that a developer's own tooling
955
+ // produces (nonCorroborating) still scores, but cannot be one of the two.
956
+ const categoryScores = calculateCategoryScores(detections.filter((d) => !d.nonCorroborating));
940
957
  const agreeing = BEHAVIOURAL_CATEGORIES
941
- .filter((c) => (categoryScores[c] || 0) >= CORROBORATION_AGREE_AT).length;
958
+ .filter((c) => (categoryScores[c] || 0) >= CORROBORATION_AGREE_AT - CORROBORATION_EPSILON).length;
942
959
  return agreeing >= CORROBORATION_MIN_AGREE ? Math.max(score, CORROBORATION_FLOOR) : score;
943
960
  }
944
961
 
package/index.js CHANGED
@@ -345,7 +345,7 @@ class ScoringEngine {
345
345
  const categoryScores = calculateCategoryScores(detections, this.weights);
346
346
  const finalScore = applyCorroborationFloor(
347
347
  applyDispositiveFloor(calculateFinalScore(categoryScores, this.weights), detections),
348
- categoryScores
348
+ detections
349
349
  );
350
350
 
351
351
  let recommendation;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webdecoy/fcaptcha",
3
- "version": "1.35.0",
3
+ "version": "1.36.0",
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 version.test.js && node config.test.js && node clientip.test.js && node limits.test.js && node detection.test.js && node inputforensics.test.js && node interactionmode.test.js && node exemptions.test.js && node webbotauth.test.js && node siteverify.test.js && node redis-state.test.js && node index.test.js && node --test suspicion.test.js security.test.js",
18
+ "test": "node version.test.js && node config.test.js && node clientip.test.js && node limits.test.js && node detection.test.js && node inputforensics.test.js && node interactionmode.test.js && node exemptions.test.js && node webbotauth.test.js && node siteverify.test.js && node redis-state.test.js && node index.test.js && node --test suspicion.test.js security.test.js corroboration.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
@@ -728,7 +728,7 @@ async function runVerification(signals, ip, siteKey, userAgent, headers = {}, ja
728
728
  const categoryScores = calculateCategoryScores(detections);
729
729
  const finalScore = applyCorroborationFloor(
730
730
  applyDispositiveFloor(calculateFinalScore(categoryScores), detections),
731
- categoryScores
731
+ detections
732
732
  );
733
733
 
734
734
  let recommendation;