@webdecoy/fcaptcha 1.12.0 → 1.14.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/package.json +1 -1
- package/server.js +73 -0
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -18,6 +18,43 @@ const SECRET_KEY = process.env.FCAPTCHA_SECRET || 'dev-secret-change-in-producti
|
|
|
18
18
|
const PORT = process.env.PORT || 3000;
|
|
19
19
|
const TRUSTED_JA4_HEADERS = detection.getTrustedJA4HeaderNames();
|
|
20
20
|
|
|
21
|
+
// Verdict logging is off by default: a self-hosted FCaptcha emits no per-request
|
|
22
|
+
// logs unless the operator opts in via FCAPTCHA_LOG_VERDICTS (1/true/yes/on).
|
|
23
|
+
// When on, each /api/verify and /api/score logs one privacy-safe JSON line
|
|
24
|
+
// (score, recommendation, category scores, and per-hit category/score/confidence)
|
|
25
|
+
// for observability and tuning. It deliberately omits IP, user agent, raw
|
|
26
|
+
// signals, and the free-text detection `reason`, which can interpolate
|
|
27
|
+
// visitor-derived data (reverse-DNS hostname, UA/header fragments, field ids).
|
|
28
|
+
//
|
|
29
|
+
// FCAPTCHA_LOG_VERDICTS_INCLUDE_RAW additionally includes that free-text reason.
|
|
30
|
+
// Separate and off by default because reasons can carry visitor-derived data —
|
|
31
|
+
// only enable in trusted debugging contexts with no privacy obligations.
|
|
32
|
+
const envFlag = (name) => ['1', 'true', 'yes', 'on']
|
|
33
|
+
.includes(String(process.env[name] || '').trim().toLowerCase());
|
|
34
|
+
const VERDICT_LOGGING_ENABLED = envFlag('FCAPTCHA_LOG_VERDICTS');
|
|
35
|
+
const VERDICT_LOG_INCLUDE_RAW = envFlag('FCAPTCHA_LOG_VERDICTS_INCLUDE_RAW');
|
|
36
|
+
if (VERDICT_LOGGING_ENABLED && VERDICT_LOG_INCLUDE_RAW) {
|
|
37
|
+
console.warn('WARNING: FCAPTCHA_LOG_VERDICTS_INCLUDE_RAW enabled — verdict logs include free-text detection reasons that may contain visitor-derived data (hostnames, UA/header fragments, field ids). Do not enable where you have privacy obligations.');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function logVerdict(endpoint, siteKey, result) {
|
|
41
|
+
if (!VERDICT_LOGGING_ENABLED || !result) return;
|
|
42
|
+
console.log(JSON.stringify({
|
|
43
|
+
event: 'verdict',
|
|
44
|
+
endpoint,
|
|
45
|
+
siteKey,
|
|
46
|
+
success: result.success,
|
|
47
|
+
score: result.score,
|
|
48
|
+
recommendation: result.recommendation,
|
|
49
|
+
categoryScores: result.categoryScores,
|
|
50
|
+
detections: (result.detections || []).map((d) => {
|
|
51
|
+
const det = { category: d.category, score: d.score, confidence: d.confidence };
|
|
52
|
+
if (VERDICT_LOG_INCLUDE_RAW) det.reason = d.reason;
|
|
53
|
+
return det;
|
|
54
|
+
})
|
|
55
|
+
}));
|
|
56
|
+
}
|
|
57
|
+
|
|
21
58
|
// Serve the browser widget alongside the API so deployments expose a single
|
|
22
59
|
// origin to integrators (the implicit contract behind <serverUrl>/fcaptcha.js).
|
|
23
60
|
// Set FCAPTCHA_SERVE_CLIENT=false to opt out — useful when hosting the widget
|
|
@@ -458,6 +495,39 @@ function detectHeadless(signals, userAgent) {
|
|
|
458
495
|
return detections;
|
|
459
496
|
}
|
|
460
497
|
|
|
498
|
+
// Flags anti-detection patch traces collected by the client. FALSE-POSITIVE-
|
|
499
|
+
// SAFE: a genuine browser never produces these (internal contradictions /
|
|
500
|
+
// native-function tampering, not environment-shape heuristics that would
|
|
501
|
+
// misfire on real Linux/VPN users). Targets stealth agents (e.g. Manus AI)
|
|
502
|
+
// driving real-but-patched Chromium. Only the two FP-safe signals are scored;
|
|
503
|
+
// the client also collects privacy-extension-ambiguous artifacts (patched_*)
|
|
504
|
+
// for observability that are intentionally NOT scored. Keep in sync with Go/Py.
|
|
505
|
+
function detectStealthArtifacts(signals) {
|
|
506
|
+
const detections = [];
|
|
507
|
+
const env = signals.environmental || {};
|
|
508
|
+
|
|
509
|
+
// Function.prototype.toString proxied — the signature move of stealth
|
|
510
|
+
// frameworks (used to make their other native overrides look untouched).
|
|
511
|
+
const artifactSignals = (env.stealthArtifacts && env.stealthArtifacts.signals) || [];
|
|
512
|
+
if (artifactSignals.includes('tostring_proxied')) {
|
|
513
|
+
detections.push({
|
|
514
|
+
category: 'headless', score: 0.9, confidence: 0.85,
|
|
515
|
+
reason: 'Function.prototype.toString is proxied (stealth automation patch)'
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Notification.permission === 'denied' while the Permissions API reports
|
|
520
|
+
// 'prompt': a state a real browser cannot reach (classic headless tell).
|
|
521
|
+
if (env.permissionProbe && env.permissionProbe.contradiction === true) {
|
|
522
|
+
detections.push({
|
|
523
|
+
category: 'headless', score: 0.85, confidence: 0.85,
|
|
524
|
+
reason: 'Notification permission contradicts Permissions API (headless/stealth tell)'
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return detections;
|
|
529
|
+
}
|
|
530
|
+
|
|
461
531
|
function detectAutomation(signals) {
|
|
462
532
|
const detections = [];
|
|
463
533
|
const env = signals.environmental || {};
|
|
@@ -1061,6 +1131,7 @@ function runVerification(signals, ip, siteKey, userAgent, headers = {}, ja3Hash
|
|
|
1061
1131
|
detections.push(
|
|
1062
1132
|
...detectVisionAI(signals),
|
|
1063
1133
|
...detectHeadless(signals, userAgent),
|
|
1134
|
+
...detectStealthArtifacts(signals),
|
|
1064
1135
|
...detectAutomation(signals),
|
|
1065
1136
|
...detectCDP(signals),
|
|
1066
1137
|
...detectBehavioral(signals),
|
|
@@ -1168,6 +1239,7 @@ app.post('/api/verify', (req, res) => {
|
|
|
1168
1239
|
}
|
|
1169
1240
|
|
|
1170
1241
|
const result = runVerification(signals, ip, siteKey, userAgent, headers, ja3Hash, powSolution, signalsJson, powTiming);
|
|
1242
|
+
logVerdict('verify', siteKey, result);
|
|
1171
1243
|
res.json(result);
|
|
1172
1244
|
});
|
|
1173
1245
|
|
|
@@ -1191,6 +1263,7 @@ app.post('/api/score', (req, res) => {
|
|
|
1191
1263
|
}
|
|
1192
1264
|
|
|
1193
1265
|
const result = runVerification(signals, ip, siteKey, userAgent, headers, ja3Hash, powSolution, signalsJson, powTiming);
|
|
1266
|
+
logVerdict('score', siteKey, result);
|
|
1194
1267
|
res.json({
|
|
1195
1268
|
success: result.success,
|
|
1196
1269
|
score: result.score,
|