@webdecoy/fcaptcha 1.45.0 → 1.46.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/detection.js +0 -12
- package/engine.js +3 -11
- package/experimental.js +23 -1
- package/package.json +1 -1
- package/server.js +12 -12
- package/siteverify.js +14 -0
package/detection.js
CHANGED
|
@@ -1031,18 +1031,6 @@ function analyzeFonts(fontsInfo, userAgent) {
|
|
|
1031
1031
|
});
|
|
1032
1032
|
}
|
|
1033
1033
|
|
|
1034
|
-
// Mac UA but no SF Pro (modern macOS)
|
|
1035
|
-
if ((ua.includes('mac os x') || ua.includes('macintosh')) && fontsInfo.hasSFPro === false &&
|
|
1036
|
-
ua.includes('10_15') === false && ua.includes('10_14') === false && fontsInfo.count > 5) {
|
|
1037
|
-
// SF Pro is on macOS 10.15+ (Catalina and later)
|
|
1038
|
-
detections.push({
|
|
1039
|
-
category: 'bot',
|
|
1040
|
-
score: 0.3,
|
|
1041
|
-
confidence: 0.4,
|
|
1042
|
-
reason: 'Modern macOS UA but SF Pro font not detected'
|
|
1043
|
-
});
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
1034
|
// Linux UA but no DejaVu Sans (very common on Linux)
|
|
1047
1035
|
if (ua.includes('linux') && !ua.includes('android') &&
|
|
1048
1036
|
fontsInfo.hasDejaVuSans === false && fontsInfo.count > 5) {
|
package/engine.js
CHANGED
|
@@ -585,17 +585,9 @@ function detectCDP(signals) {
|
|
|
585
585
|
}
|
|
586
586
|
}
|
|
587
587
|
|
|
588
|
-
//
|
|
589
|
-
//
|
|
590
|
-
|
|
591
|
-
detections.push({
|
|
592
|
-
category: 'cdp', score: 0.6, confidence: 0.5,
|
|
593
|
-
reason: 'CDP/DevTools console consumer attached (automation protocol or open DevTools)',
|
|
594
|
-
// A developer with DevTools open produces exactly this signal, so it
|
|
595
|
-
// cannot be one of the independent views the corroboration floor counts.
|
|
596
|
-
nonCorroborating: true
|
|
597
|
-
});
|
|
598
|
-
}
|
|
588
|
+
// cdpRuntime.consoleAttached is collected but not scored: measured on real
|
|
589
|
+
// visitors it fires in every Chrome and Safari session (no DevTools, no
|
|
590
|
+
// extensions), so it cannot tell a person from an agent.
|
|
599
591
|
|
|
600
592
|
if (cdp.detected) {
|
|
601
593
|
const signalList = cdp.signals || [];
|
package/experimental.js
CHANGED
|
@@ -8,6 +8,27 @@ const { calculateCategoryScores, BEHAVIOURAL_CATEGORIES, CORROBORATION_AGREE_AT,
|
|
|
8
8
|
// change. An old selector must never silently opt an operator into a new policy.
|
|
9
9
|
const EXPERIMENTAL_POLICY = 'stealth-corroboration-v1';
|
|
10
10
|
const ANIMATION_POLICY = 'animation-consistency-v1';
|
|
11
|
+
const POINTER_POLICY = 'pointer-movement-v1';
|
|
12
|
+
const POINTER_MIN_SAMPLES = 30;
|
|
13
|
+
const POINTER_ZERO_RATIO_BELOW = 0.05;
|
|
14
|
+
|
|
15
|
+
// Moves where the pointer position changed but movementX/Y read 0. Measured on
|
|
16
|
+
// real mice and trackpads in Chrome, Safari and Firefox, at 1x and 2x pixel
|
|
17
|
+
// ratio: 14-38% of moves. Stealth CDP-driven Chrome: 0-3%. Observe only until
|
|
18
|
+
// production traffic shows the rate on hardware the bench never saw.
|
|
19
|
+
function pointerObservation(signals) {
|
|
20
|
+
const b = signals.behavioral || {};
|
|
21
|
+
const f = b.inputForensics || {};
|
|
22
|
+
const samples = f.pointerMoveSamples;
|
|
23
|
+
const ratio = f.pointerMoveZeroRatio;
|
|
24
|
+
const known = (b.touchEvents ?? 0) < 3 && Number.isFinite(samples) && samples >= POINTER_MIN_SAMPLES &&
|
|
25
|
+
Number.isFinite(ratio) && ratio >= 0 && ratio <= 1;
|
|
26
|
+
const status = !known ? 'unknown' : ratio < POINTER_ZERO_RATIO_BELOW ? 'detected' : 'clear';
|
|
27
|
+
return { mode: 'observe', status, detections: status === 'detected' ? [{
|
|
28
|
+
id: 'pointer-movement-never-zero',
|
|
29
|
+
reason: 'Pointer moves always report movement; experimental observation, not proof of automation',
|
|
30
|
+
}] : [] };
|
|
31
|
+
}
|
|
11
32
|
|
|
12
33
|
function animationObservation(signals) {
|
|
13
34
|
const probe = signals.environmental?.animationConsistency;
|
|
@@ -55,8 +76,9 @@ function evaluateExperimental(signals, productionScore, detections, blocking = f
|
|
|
55
76
|
observations: {
|
|
56
77
|
[ANIMATION_POLICY]: animationObservation(signals),
|
|
57
78
|
[IDENTITY_POLICY]: identityObservation(signals),
|
|
79
|
+
[POINTER_POLICY]: pointerObservation(signals),
|
|
58
80
|
},
|
|
59
81
|
};
|
|
60
82
|
}
|
|
61
83
|
|
|
62
|
-
module.exports = { evaluateExperimental, EXPERIMENTAL_POLICY };
|
|
84
|
+
module.exports = { evaluateExperimental, pointerObservation, EXPERIMENTAL_POLICY, POINTER_POLICY };
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -28,7 +28,8 @@ const {
|
|
|
28
28
|
sanitizeAction,
|
|
29
29
|
sanitizeCdata,
|
|
30
30
|
secretMatches,
|
|
31
|
-
siteverifyAsync
|
|
31
|
+
siteverifyAsync,
|
|
32
|
+
asRecaptchaScore
|
|
32
33
|
} = require('./siteverify');
|
|
33
34
|
|
|
34
35
|
const app = express();
|
|
@@ -960,17 +961,16 @@ app.post('/api/token/verify', asyncRoute(async (req, res) => {
|
|
|
960
961
|
// integration at this server should be a base-URL change and nothing else.
|
|
961
962
|
// See siteverify.js for the adapter itself.
|
|
962
963
|
async function siteverifyHandler(req, res) {
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
);
|
|
964
|
+
const out = await siteverifyAsync({
|
|
965
|
+
body: req.body,
|
|
966
|
+
// Bind to this server's token store, so replay state is shared with the
|
|
967
|
+
// native endpoint rather than kept in a parallel universe.
|
|
968
|
+
verifyToken,
|
|
969
|
+
expectedSecret: VERIFY_SECRET,
|
|
970
|
+
idempotencyStore: IDEMPOTENCY,
|
|
971
|
+
requireSecret: !LEGACY_UNAUTH_VERIFY
|
|
972
|
+
});
|
|
973
|
+
res.json(req.path === '/recaptcha/api/siteverify' ? asRecaptchaScore(out) : out);
|
|
974
974
|
}
|
|
975
975
|
|
|
976
976
|
app.post('/turnstile/v0/siteverify', asyncRoute(siteverifyHandler));
|
package/siteverify.js
CHANGED
|
@@ -237,6 +237,19 @@ class IdempotencyStore {
|
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Restate a response in reCAPTCHA v3's score direction.
|
|
242
|
+
*
|
|
243
|
+
* reCAPTCHA scores run 1.0 = human, 0.0 = bot, and every integration compares
|
|
244
|
+
* `score >= threshold`. FCaptcha's run the other way, so served unchanged on the
|
|
245
|
+
* reCAPTCHA path they would reject people and admit bots. The native score
|
|
246
|
+
* stays available as `fcaptcha_score`.
|
|
247
|
+
*/
|
|
248
|
+
function asRecaptchaScore(out) {
|
|
249
|
+
if (!out || typeof out.score !== 'number') return out;
|
|
250
|
+
return { ...out, score: Math.round((1 - out.score) * 10000) / 10000, fcaptcha_score: out.score };
|
|
251
|
+
}
|
|
252
|
+
|
|
240
253
|
/** A failing siteverify response carrying the given codes. */
|
|
241
254
|
function failure(...codes) {
|
|
242
255
|
return { success: false, 'error-codes': codes };
|
|
@@ -355,6 +368,7 @@ async function siteverifyAsync({
|
|
|
355
368
|
|
|
356
369
|
module.exports = {
|
|
357
370
|
ERROR_CODES,
|
|
371
|
+
asRecaptchaScore,
|
|
358
372
|
HostnameAllowlist,
|
|
359
373
|
IdempotencyStore,
|
|
360
374
|
hostFromUrl,
|