@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.
- package/index.js +13 -2
- package/package.json +2 -2
- 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
|
-
|
|
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.
|
|
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
|
-
//
|
|
1430
|
-
//
|
|
1431
|
-
//
|
|
1432
|
-
|
|
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
|
|
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
|
|
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
|
|