@niksbanna/bot-detector 1.0.0 → 1.0.3
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/README.md +56 -2
- package/dist/bot-detector.cjs.js +115 -74
- package/dist/bot-detector.cjs.js.map +2 -2
- package/dist/bot-detector.esm.js +115 -74
- package/dist/bot-detector.esm.js.map +2 -2
- package/dist/bot-detector.iife.js +115 -74
- package/dist/bot-detector.iife.js.map +2 -2
- package/dist/bot-detector.iife.min.js +3 -1
- package/package.json +5 -4
- package/src/core/BotDetector.js +29 -18
- package/src/core/ScoringEngine.js +3 -2
- package/src/index.js +18 -10
- package/src/signals/automation/PhantomJSSignal.js +2 -4
- package/src/signals/automation/PlaywrightSignal.js +0 -6
- package/src/signals/automation/PuppeteerSignal.js +20 -14
- package/src/signals/automation/SeleniumSignal.js +2 -5
- package/src/signals/behavior/KeyboardPatternSignal.js +1 -1
- package/src/signals/behavior/MouseMovementSignal.js +1 -1
- package/src/signals/behavior/ScrollBehaviorSignal.js +1 -1
- package/src/signals/environment/HeadlessSignal.js +4 -4
- package/src/signals/environment/NavigatorAnomalySignal.js +3 -2
- package/src/signals/timing/DOMContentTimingSignal.js +2 -1
- package/src/signals/timing/PageLoadSignal.js +38 -20
|
@@ -254,6 +254,7 @@ var BotDetectorLib = (() => {
|
|
|
254
254
|
*/
|
|
255
255
|
getBreakdown() {
|
|
256
256
|
const breakdown = [];
|
|
257
|
+
const score = this.calculate();
|
|
257
258
|
for (const [signalId, data] of this._results) {
|
|
258
259
|
breakdown.push({
|
|
259
260
|
signalId,
|
|
@@ -261,7 +262,7 @@ var BotDetectorLib = (() => {
|
|
|
261
262
|
confidence: data.confidence,
|
|
262
263
|
weight: data.weight,
|
|
263
264
|
contribution: data.contribution,
|
|
264
|
-
percentOfScore:
|
|
265
|
+
percentOfScore: score > 0 ? (data.contribution / score * 100).toFixed(1) : "0.0"
|
|
265
266
|
});
|
|
266
267
|
}
|
|
267
268
|
return breakdown.sort((a, b) => b.contribution - a.contribution);
|
|
@@ -404,7 +405,7 @@ var BotDetectorLib = (() => {
|
|
|
404
405
|
var VerdictEngine = _VerdictEngine;
|
|
405
406
|
|
|
406
407
|
// src/core/BotDetector.js
|
|
407
|
-
var BotDetector = class
|
|
408
|
+
var BotDetector = class {
|
|
408
409
|
/**
|
|
409
410
|
* Creates a new BotDetector instance.
|
|
410
411
|
* @param {Object} [options={}] - Configuration options
|
|
@@ -517,17 +518,17 @@ var BotDetectorLib = (() => {
|
|
|
517
518
|
return true;
|
|
518
519
|
});
|
|
519
520
|
const detectionPromises = signalsToRun.map(async (signal) => {
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
521
|
+
let timeoutId;
|
|
522
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
523
|
+
timeoutId = setTimeout(() => resolve({
|
|
524
|
+
triggered: false,
|
|
525
|
+
value: null,
|
|
526
|
+
confidence: 0,
|
|
527
|
+
error: "timeout"
|
|
528
|
+
}), this._detectionTimeout);
|
|
529
|
+
});
|
|
530
|
+
const result = await Promise.race([signal.run(), timeoutPromise]);
|
|
531
|
+
clearTimeout(timeoutId);
|
|
531
532
|
return { signal, result };
|
|
532
533
|
});
|
|
533
534
|
const results = await Promise.all(detectionPromises);
|
|
@@ -614,12 +615,20 @@ var BotDetectorLib = (() => {
|
|
|
614
615
|
}
|
|
615
616
|
}
|
|
616
617
|
/**
|
|
617
|
-
*
|
|
618
|
-
*
|
|
619
|
-
*
|
|
618
|
+
* @deprecated Use `createDetector()` from '@niksbanna/bot-detector' instead.
|
|
619
|
+
* This method cannot load default signals from here due to module boundaries.
|
|
620
|
+
*
|
|
621
|
+
* @example
|
|
622
|
+
* // Correct:
|
|
623
|
+
* import { createDetector } from '@niksbanna/bot-detector';
|
|
624
|
+
* const detector = createDetector();
|
|
625
|
+
*
|
|
626
|
+
* @throws {Error} Always — to prevent silent empty-detector bugs.
|
|
620
627
|
*/
|
|
621
|
-
static withDefaults(
|
|
622
|
-
|
|
628
|
+
static withDefaults() {
|
|
629
|
+
throw new Error(
|
|
630
|
+
"BotDetector.withDefaults() is not supported. Use createDetector() from '@niksbanna/bot-detector' instead:\n import { createDetector } from '@niksbanna/bot-detector';\n const detector = createDetector();"
|
|
631
|
+
);
|
|
623
632
|
}
|
|
624
633
|
};
|
|
625
634
|
|
|
@@ -675,9 +684,6 @@ var BotDetectorLib = (() => {
|
|
|
675
684
|
if (typeof window.chrome === "undefined") {
|
|
676
685
|
indicators.push("missing-chrome-object");
|
|
677
686
|
confidence = Math.max(confidence, 0.6);
|
|
678
|
-
} else if (!window.chrome.runtime) {
|
|
679
|
-
indicators.push("missing-chrome-runtime");
|
|
680
|
-
confidence = Math.max(confidence, 0.4);
|
|
681
687
|
}
|
|
682
688
|
}
|
|
683
689
|
if (navigator.plugins && navigator.plugins.length === 0) {
|
|
@@ -743,7 +749,8 @@ var BotDetectorLib = (() => {
|
|
|
743
749
|
totalScore += 1;
|
|
744
750
|
}
|
|
745
751
|
checksPerformed++;
|
|
746
|
-
|
|
752
|
+
const isModernChrome = ua.includes("Chrome") && !ua.includes("Chromium");
|
|
753
|
+
if (!isModernChrome && (!platform || platform === "" || platform === "undefined")) {
|
|
747
754
|
anomalies.push("empty-platform");
|
|
748
755
|
totalScore += 1;
|
|
749
756
|
}
|
|
@@ -855,7 +862,7 @@ var BotDetectorLib = (() => {
|
|
|
855
862
|
super(options);
|
|
856
863
|
this._movements = [];
|
|
857
864
|
this._isTracking = false;
|
|
858
|
-
this._trackingDuration = options.trackingDuration ||
|
|
865
|
+
this._trackingDuration = Math.min(options.trackingDuration || 2500, 2500);
|
|
859
866
|
this._minMovements = options.minMovements || 5;
|
|
860
867
|
this._boundHandler = null;
|
|
861
868
|
}
|
|
@@ -1009,7 +1016,7 @@ var BotDetectorLib = (() => {
|
|
|
1009
1016
|
super(options);
|
|
1010
1017
|
this._keystrokes = [];
|
|
1011
1018
|
this._isTracking = false;
|
|
1012
|
-
this._trackingDuration = options.trackingDuration ||
|
|
1019
|
+
this._trackingDuration = Math.min(options.trackingDuration || 2500, 2500);
|
|
1013
1020
|
this._minKeystrokes = options.minKeystrokes || 10;
|
|
1014
1021
|
this._boundKeydownHandler = null;
|
|
1015
1022
|
this._boundKeyupHandler = null;
|
|
@@ -1304,7 +1311,7 @@ var BotDetectorLib = (() => {
|
|
|
1304
1311
|
super(options);
|
|
1305
1312
|
this._scrollEvents = [];
|
|
1306
1313
|
this._isTracking = false;
|
|
1307
|
-
this._trackingDuration = options.trackingDuration ||
|
|
1314
|
+
this._trackingDuration = Math.min(options.trackingDuration || 2500, 2500);
|
|
1308
1315
|
this._boundHandler = null;
|
|
1309
1316
|
}
|
|
1310
1317
|
/**
|
|
@@ -1968,22 +1975,26 @@ var BotDetectorLib = (() => {
|
|
|
1968
1975
|
}
|
|
1969
1976
|
const timing = performance.timing;
|
|
1970
1977
|
const navigationStart = timing.navigationStart;
|
|
1971
|
-
const
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
const
|
|
1976
|
-
const
|
|
1977
|
-
const
|
|
1978
|
-
|
|
1978
|
+
const safeTimingDiff = (end, start) => {
|
|
1979
|
+
if (end === 0 || start === 0) return null;
|
|
1980
|
+
return end - start;
|
|
1981
|
+
};
|
|
1982
|
+
const domContentLoaded = safeTimingDiff(timing.domContentLoadedEventEnd, navigationStart);
|
|
1983
|
+
const domComplete = safeTimingDiff(timing.domComplete, navigationStart);
|
|
1984
|
+
const loadComplete = safeTimingDiff(timing.loadEventEnd, navigationStart);
|
|
1985
|
+
const dnsLookup = safeTimingDiff(timing.domainLookupEnd, timing.domainLookupStart);
|
|
1986
|
+
const tcpConnection = safeTimingDiff(timing.connectEnd, timing.connectStart);
|
|
1987
|
+
const serverResponse = safeTimingDiff(timing.responseEnd, timing.requestStart);
|
|
1988
|
+
const domProcessing = safeTimingDiff(timing.domComplete, timing.domLoading);
|
|
1989
|
+
if (domContentLoaded !== null && domContentLoaded > 0 && domContentLoaded < 10) {
|
|
1979
1990
|
anomalies.push("instant-dom-content-loaded");
|
|
1980
1991
|
confidence = Math.max(confidence, 0.7);
|
|
1981
1992
|
}
|
|
1982
|
-
if (dnsLookup === 0 && tcpConnection === 0 && serverResponse < 5) {
|
|
1993
|
+
if (dnsLookup === 0 && tcpConnection === 0 && serverResponse !== null && serverResponse < 5) {
|
|
1983
1994
|
anomalies.push("zero-network-timing");
|
|
1984
1995
|
confidence = Math.max(confidence, 0.4);
|
|
1985
1996
|
}
|
|
1986
|
-
if (domContentLoaded < 0 || domComplete < 0 || loadComplete < 0) {
|
|
1997
|
+
if (domContentLoaded !== null && domContentLoaded < 0 || domComplete !== null && domComplete < 0 || loadComplete !== null && loadComplete < 0) {
|
|
1987
1998
|
anomalies.push("negative-timing");
|
|
1988
1999
|
confidence = Math.max(confidence, 0.8);
|
|
1989
2000
|
}
|
|
@@ -1993,18 +2004,21 @@ var BotDetectorLib = (() => {
|
|
|
1993
2004
|
confidence = Math.max(confidence, 0.7);
|
|
1994
2005
|
}
|
|
1995
2006
|
}
|
|
1996
|
-
if (domProcessing > 3e4) {
|
|
2007
|
+
if (domProcessing !== null && domProcessing > 3e4) {
|
|
1997
2008
|
anomalies.push("excessive-dom-processing");
|
|
1998
2009
|
confidence = Math.max(confidence, 0.3);
|
|
1999
2010
|
}
|
|
2000
2011
|
const scriptsLoadedTime = timing.domContentLoadedEventStart - timing.responseEnd;
|
|
2001
|
-
if (scriptsLoadedTime > 0 && scriptsLoadedTime < 5) {
|
|
2012
|
+
if (timing.responseEnd > 0 && timing.domContentLoadedEventStart > 0 && scriptsLoadedTime > 0 && scriptsLoadedTime < 5) {
|
|
2002
2013
|
anomalies.push("instant-script-execution");
|
|
2003
2014
|
confidence = Math.max(confidence, 0.4);
|
|
2004
2015
|
}
|
|
2005
|
-
const
|
|
2006
|
-
const
|
|
2007
|
-
|
|
2016
|
+
const perfBefore = performance.now();
|
|
2017
|
+
const spinEnd = perfBefore + 2;
|
|
2018
|
+
while (performance.now() < spinEnd) {
|
|
2019
|
+
}
|
|
2020
|
+
const perfAfter = performance.now();
|
|
2021
|
+
if (perfAfter === perfBefore) {
|
|
2008
2022
|
anomalies.push("frozen-performance-now");
|
|
2009
2023
|
confidence = Math.max(confidence, 0.6);
|
|
2010
2024
|
}
|
|
@@ -2114,9 +2128,10 @@ var BotDetectorLib = (() => {
|
|
|
2114
2128
|
confidence = Math.max(confidence, 0.4);
|
|
2115
2129
|
}
|
|
2116
2130
|
try {
|
|
2131
|
+
const randomId = `__bdt_${Math.random().toString(36).slice(2)}`;
|
|
2117
2132
|
const startMutation = performance.now();
|
|
2118
2133
|
const testDiv = document.createElement("div");
|
|
2119
|
-
testDiv.id =
|
|
2134
|
+
testDiv.id = randomId;
|
|
2120
2135
|
document.body.appendChild(testDiv);
|
|
2121
2136
|
const afterAppend = performance.now();
|
|
2122
2137
|
document.body.removeChild(testDiv);
|
|
@@ -2226,23 +2241,30 @@ var BotDetectorLib = (() => {
|
|
|
2226
2241
|
indicators.push("default-viewport");
|
|
2227
2242
|
confidence = Math.max(confidence, 0.3);
|
|
2228
2243
|
}
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2244
|
+
const FRAMEWORK_PREFIXES = [
|
|
2245
|
+
"__zone_symbol__",
|
|
2246
|
+
// Angular / Zone.js
|
|
2247
|
+
"__next",
|
|
2248
|
+
// Next.js
|
|
2249
|
+
"__webpack",
|
|
2250
|
+
// webpack
|
|
2251
|
+
"__react",
|
|
2252
|
+
// React DevTools
|
|
2253
|
+
"__REACT",
|
|
2254
|
+
"__vite",
|
|
2255
|
+
// Vite
|
|
2256
|
+
"__nuxt"
|
|
2257
|
+
// Nuxt.js
|
|
2258
|
+
];
|
|
2233
2259
|
const suspiciousBindings = Object.keys(window).filter((key) => {
|
|
2234
|
-
|
|
2260
|
+
if (!key.startsWith("__")) return false;
|
|
2261
|
+
if (typeof window[key] !== "function") return false;
|
|
2262
|
+
return !FRAMEWORK_PREFIXES.some((prefix) => key.startsWith(prefix));
|
|
2235
2263
|
});
|
|
2236
|
-
if (suspiciousBindings.length >
|
|
2264
|
+
if (suspiciousBindings.length > 10) {
|
|
2237
2265
|
indicators.push("suspicious-bindings");
|
|
2238
2266
|
confidence = Math.max(confidence, 0.5);
|
|
2239
2267
|
}
|
|
2240
|
-
if (typeof window.chrome !== "undefined") {
|
|
2241
|
-
if (!window.chrome.runtime || !window.chrome.runtime.id) {
|
|
2242
|
-
indicators.push("incomplete-chrome-object");
|
|
2243
|
-
confidence = Math.max(confidence, 0.4);
|
|
2244
|
-
}
|
|
2245
|
-
}
|
|
2246
2268
|
const triggered = indicators.length > 0;
|
|
2247
2269
|
return this.createResult(triggered, { indicators }, confidence);
|
|
2248
2270
|
}
|
|
@@ -2282,10 +2304,6 @@ var BotDetectorLib = (() => {
|
|
|
2282
2304
|
indicators.push("playwright-ua-marker");
|
|
2283
2305
|
confidence = Math.max(confidence, ua.includes("Playwright") ? 1 : 0.7);
|
|
2284
2306
|
}
|
|
2285
|
-
if (navigator.webdriver === true) {
|
|
2286
|
-
indicators.push("webdriver-flag");
|
|
2287
|
-
confidence = Math.max(confidence, 0.8);
|
|
2288
|
-
}
|
|
2289
2307
|
try {
|
|
2290
2308
|
const windowKeys = Object.keys(window);
|
|
2291
2309
|
const pwBindings = windowKeys.filter((k) => k.startsWith("__pw"));
|
|
@@ -2343,10 +2361,6 @@ var BotDetectorLib = (() => {
|
|
|
2343
2361
|
async detect() {
|
|
2344
2362
|
const indicators = [];
|
|
2345
2363
|
let confidence = 0;
|
|
2346
|
-
if (navigator.webdriver === true) {
|
|
2347
|
-
indicators.push("webdriver-flag");
|
|
2348
|
-
confidence = Math.max(confidence, 1);
|
|
2349
|
-
}
|
|
2350
2364
|
const seleniumGlobals = [
|
|
2351
2365
|
"_selenium",
|
|
2352
2366
|
"callSelenium",
|
|
@@ -2513,14 +2527,10 @@ var BotDetectorLib = (() => {
|
|
|
2513
2527
|
}
|
|
2514
2528
|
const phantomProps = [
|
|
2515
2529
|
"__PHANTOM__",
|
|
2516
|
-
"PHANTOM"
|
|
2517
|
-
"Buffer",
|
|
2518
|
-
// PhantomJS exposes Node.js Buffer
|
|
2519
|
-
"process"
|
|
2520
|
-
// May expose Node.js process
|
|
2530
|
+
"PHANTOM"
|
|
2521
2531
|
];
|
|
2522
2532
|
for (const prop of phantomProps) {
|
|
2523
|
-
if (prop in window
|
|
2533
|
+
if (prop in window) {
|
|
2524
2534
|
indicators.push(`phantom-prop-${prop.toLowerCase()}`);
|
|
2525
2535
|
confidence = Math.max(confidence, 0.9);
|
|
2526
2536
|
}
|
|
@@ -2595,13 +2605,44 @@ var BotDetectorLib = (() => {
|
|
|
2595
2605
|
instantBotSignals = ["webdriver", "puppeteer", "playwright", "selenium", "phantomjs"],
|
|
2596
2606
|
...detectorOptions
|
|
2597
2607
|
} = options;
|
|
2598
|
-
const
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2608
|
+
const signalClasses = includeInteractionSignals ? [
|
|
2609
|
+
WebDriverSignal,
|
|
2610
|
+
HeadlessSignal,
|
|
2611
|
+
NavigatorAnomalySignal,
|
|
2612
|
+
PermissionsSignal,
|
|
2613
|
+
PluginsSignal,
|
|
2614
|
+
WebGLSignal,
|
|
2615
|
+
CanvasSignal,
|
|
2616
|
+
AudioContextSignal,
|
|
2617
|
+
ScreenSignal,
|
|
2618
|
+
PageLoadSignal,
|
|
2619
|
+
DOMContentTimingSignal,
|
|
2620
|
+
PuppeteerSignal,
|
|
2621
|
+
PlaywrightSignal,
|
|
2622
|
+
SeleniumSignal,
|
|
2623
|
+
PhantomJSSignal,
|
|
2624
|
+
MouseMovementSignal,
|
|
2625
|
+
KeyboardPatternSignal,
|
|
2626
|
+
InteractionTimingSignal,
|
|
2627
|
+
ScrollBehaviorSignal
|
|
2628
|
+
] : [
|
|
2629
|
+
WebDriverSignal,
|
|
2630
|
+
HeadlessSignal,
|
|
2631
|
+
NavigatorAnomalySignal,
|
|
2632
|
+
PermissionsSignal,
|
|
2633
|
+
PluginsSignal,
|
|
2634
|
+
WebGLSignal,
|
|
2635
|
+
CanvasSignal,
|
|
2636
|
+
AudioContextSignal,
|
|
2637
|
+
ScreenSignal,
|
|
2638
|
+
PageLoadSignal,
|
|
2639
|
+
DOMContentTimingSignal,
|
|
2640
|
+
PuppeteerSignal,
|
|
2641
|
+
PlaywrightSignal,
|
|
2642
|
+
SeleniumSignal,
|
|
2643
|
+
PhantomJSSignal
|
|
2644
|
+
];
|
|
2645
|
+
const signals = signalClasses.map((Cls) => new Cls());
|
|
2605
2646
|
return new BotDetector({
|
|
2606
2647
|
signals,
|
|
2607
2648
|
instantBotSignals,
|