@niksbanna/bot-detector 1.0.2 → 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 +19 -0
- 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 +1 -1
- 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 -19
- 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
package/dist/bot-detector.esm.js
CHANGED
|
@@ -200,6 +200,7 @@ var ScoringEngine = class {
|
|
|
200
200
|
*/
|
|
201
201
|
getBreakdown() {
|
|
202
202
|
const breakdown = [];
|
|
203
|
+
const score = this.calculate();
|
|
203
204
|
for (const [signalId, data] of this._results) {
|
|
204
205
|
breakdown.push({
|
|
205
206
|
signalId,
|
|
@@ -207,7 +208,7 @@ var ScoringEngine = class {
|
|
|
207
208
|
confidence: data.confidence,
|
|
208
209
|
weight: data.weight,
|
|
209
210
|
contribution: data.contribution,
|
|
210
|
-
percentOfScore:
|
|
211
|
+
percentOfScore: score > 0 ? (data.contribution / score * 100).toFixed(1) : "0.0"
|
|
211
212
|
});
|
|
212
213
|
}
|
|
213
214
|
return breakdown.sort((a, b) => b.contribution - a.contribution);
|
|
@@ -350,7 +351,7 @@ __publicField(_VerdictEngine, "DEFAULT_THRESHOLDS", {
|
|
|
350
351
|
var VerdictEngine = _VerdictEngine;
|
|
351
352
|
|
|
352
353
|
// src/core/BotDetector.js
|
|
353
|
-
var BotDetector = class
|
|
354
|
+
var BotDetector = class {
|
|
354
355
|
/**
|
|
355
356
|
* Creates a new BotDetector instance.
|
|
356
357
|
* @param {Object} [options={}] - Configuration options
|
|
@@ -463,17 +464,17 @@ var BotDetector = class _BotDetector {
|
|
|
463
464
|
return true;
|
|
464
465
|
});
|
|
465
466
|
const detectionPromises = signalsToRun.map(async (signal) => {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
467
|
+
let timeoutId;
|
|
468
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
469
|
+
timeoutId = setTimeout(() => resolve({
|
|
470
|
+
triggered: false,
|
|
471
|
+
value: null,
|
|
472
|
+
confidence: 0,
|
|
473
|
+
error: "timeout"
|
|
474
|
+
}), this._detectionTimeout);
|
|
475
|
+
});
|
|
476
|
+
const result = await Promise.race([signal.run(), timeoutPromise]);
|
|
477
|
+
clearTimeout(timeoutId);
|
|
477
478
|
return { signal, result };
|
|
478
479
|
});
|
|
479
480
|
const results = await Promise.all(detectionPromises);
|
|
@@ -560,12 +561,20 @@ var BotDetector = class _BotDetector {
|
|
|
560
561
|
}
|
|
561
562
|
}
|
|
562
563
|
/**
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
*
|
|
564
|
+
* @deprecated Use `createDetector()` from '@niksbanna/bot-detector' instead.
|
|
565
|
+
* This method cannot load default signals from here due to module boundaries.
|
|
566
|
+
*
|
|
567
|
+
* @example
|
|
568
|
+
* // Correct:
|
|
569
|
+
* import { createDetector } from '@niksbanna/bot-detector';
|
|
570
|
+
* const detector = createDetector();
|
|
571
|
+
*
|
|
572
|
+
* @throws {Error} Always — to prevent silent empty-detector bugs.
|
|
566
573
|
*/
|
|
567
|
-
static withDefaults(
|
|
568
|
-
|
|
574
|
+
static withDefaults() {
|
|
575
|
+
throw new Error(
|
|
576
|
+
"BotDetector.withDefaults() is not supported. Use createDetector() from '@niksbanna/bot-detector' instead:\n import { createDetector } from '@niksbanna/bot-detector';\n const detector = createDetector();"
|
|
577
|
+
);
|
|
569
578
|
}
|
|
570
579
|
};
|
|
571
580
|
|
|
@@ -621,9 +630,6 @@ var HeadlessSignal = class extends Signal {
|
|
|
621
630
|
if (typeof window.chrome === "undefined") {
|
|
622
631
|
indicators.push("missing-chrome-object");
|
|
623
632
|
confidence = Math.max(confidence, 0.6);
|
|
624
|
-
} else if (!window.chrome.runtime) {
|
|
625
|
-
indicators.push("missing-chrome-runtime");
|
|
626
|
-
confidence = Math.max(confidence, 0.4);
|
|
627
633
|
}
|
|
628
634
|
}
|
|
629
635
|
if (navigator.plugins && navigator.plugins.length === 0) {
|
|
@@ -689,7 +695,8 @@ var NavigatorAnomalySignal = class extends Signal {
|
|
|
689
695
|
totalScore += 1;
|
|
690
696
|
}
|
|
691
697
|
checksPerformed++;
|
|
692
|
-
|
|
698
|
+
const isModernChrome = ua.includes("Chrome") && !ua.includes("Chromium");
|
|
699
|
+
if (!isModernChrome && (!platform || platform === "" || platform === "undefined")) {
|
|
693
700
|
anomalies.push("empty-platform");
|
|
694
701
|
totalScore += 1;
|
|
695
702
|
}
|
|
@@ -801,7 +808,7 @@ var MouseMovementSignal = class extends Signal {
|
|
|
801
808
|
super(options);
|
|
802
809
|
this._movements = [];
|
|
803
810
|
this._isTracking = false;
|
|
804
|
-
this._trackingDuration = options.trackingDuration ||
|
|
811
|
+
this._trackingDuration = Math.min(options.trackingDuration || 2500, 2500);
|
|
805
812
|
this._minMovements = options.minMovements || 5;
|
|
806
813
|
this._boundHandler = null;
|
|
807
814
|
}
|
|
@@ -955,7 +962,7 @@ var KeyboardPatternSignal = class extends Signal {
|
|
|
955
962
|
super(options);
|
|
956
963
|
this._keystrokes = [];
|
|
957
964
|
this._isTracking = false;
|
|
958
|
-
this._trackingDuration = options.trackingDuration ||
|
|
965
|
+
this._trackingDuration = Math.min(options.trackingDuration || 2500, 2500);
|
|
959
966
|
this._minKeystrokes = options.minKeystrokes || 10;
|
|
960
967
|
this._boundKeydownHandler = null;
|
|
961
968
|
this._boundKeyupHandler = null;
|
|
@@ -1250,7 +1257,7 @@ var ScrollBehaviorSignal = class extends Signal {
|
|
|
1250
1257
|
super(options);
|
|
1251
1258
|
this._scrollEvents = [];
|
|
1252
1259
|
this._isTracking = false;
|
|
1253
|
-
this._trackingDuration = options.trackingDuration ||
|
|
1260
|
+
this._trackingDuration = Math.min(options.trackingDuration || 2500, 2500);
|
|
1254
1261
|
this._boundHandler = null;
|
|
1255
1262
|
}
|
|
1256
1263
|
/**
|
|
@@ -1914,22 +1921,26 @@ var PageLoadSignal = class extends Signal {
|
|
|
1914
1921
|
}
|
|
1915
1922
|
const timing = performance.timing;
|
|
1916
1923
|
const navigationStart = timing.navigationStart;
|
|
1917
|
-
const
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
const
|
|
1922
|
-
const
|
|
1923
|
-
const
|
|
1924
|
-
|
|
1924
|
+
const safeTimingDiff = (end, start) => {
|
|
1925
|
+
if (end === 0 || start === 0) return null;
|
|
1926
|
+
return end - start;
|
|
1927
|
+
};
|
|
1928
|
+
const domContentLoaded = safeTimingDiff(timing.domContentLoadedEventEnd, navigationStart);
|
|
1929
|
+
const domComplete = safeTimingDiff(timing.domComplete, navigationStart);
|
|
1930
|
+
const loadComplete = safeTimingDiff(timing.loadEventEnd, navigationStart);
|
|
1931
|
+
const dnsLookup = safeTimingDiff(timing.domainLookupEnd, timing.domainLookupStart);
|
|
1932
|
+
const tcpConnection = safeTimingDiff(timing.connectEnd, timing.connectStart);
|
|
1933
|
+
const serverResponse = safeTimingDiff(timing.responseEnd, timing.requestStart);
|
|
1934
|
+
const domProcessing = safeTimingDiff(timing.domComplete, timing.domLoading);
|
|
1935
|
+
if (domContentLoaded !== null && domContentLoaded > 0 && domContentLoaded < 10) {
|
|
1925
1936
|
anomalies.push("instant-dom-content-loaded");
|
|
1926
1937
|
confidence = Math.max(confidence, 0.7);
|
|
1927
1938
|
}
|
|
1928
|
-
if (dnsLookup === 0 && tcpConnection === 0 && serverResponse < 5) {
|
|
1939
|
+
if (dnsLookup === 0 && tcpConnection === 0 && serverResponse !== null && serverResponse < 5) {
|
|
1929
1940
|
anomalies.push("zero-network-timing");
|
|
1930
1941
|
confidence = Math.max(confidence, 0.4);
|
|
1931
1942
|
}
|
|
1932
|
-
if (domContentLoaded < 0 || domComplete < 0 || loadComplete < 0) {
|
|
1943
|
+
if (domContentLoaded !== null && domContentLoaded < 0 || domComplete !== null && domComplete < 0 || loadComplete !== null && loadComplete < 0) {
|
|
1933
1944
|
anomalies.push("negative-timing");
|
|
1934
1945
|
confidence = Math.max(confidence, 0.8);
|
|
1935
1946
|
}
|
|
@@ -1939,18 +1950,21 @@ var PageLoadSignal = class extends Signal {
|
|
|
1939
1950
|
confidence = Math.max(confidence, 0.7);
|
|
1940
1951
|
}
|
|
1941
1952
|
}
|
|
1942
|
-
if (domProcessing > 3e4) {
|
|
1953
|
+
if (domProcessing !== null && domProcessing > 3e4) {
|
|
1943
1954
|
anomalies.push("excessive-dom-processing");
|
|
1944
1955
|
confidence = Math.max(confidence, 0.3);
|
|
1945
1956
|
}
|
|
1946
1957
|
const scriptsLoadedTime = timing.domContentLoadedEventStart - timing.responseEnd;
|
|
1947
|
-
if (scriptsLoadedTime > 0 && scriptsLoadedTime < 5) {
|
|
1958
|
+
if (timing.responseEnd > 0 && timing.domContentLoadedEventStart > 0 && scriptsLoadedTime > 0 && scriptsLoadedTime < 5) {
|
|
1948
1959
|
anomalies.push("instant-script-execution");
|
|
1949
1960
|
confidence = Math.max(confidence, 0.4);
|
|
1950
1961
|
}
|
|
1951
|
-
const
|
|
1952
|
-
const
|
|
1953
|
-
|
|
1962
|
+
const perfBefore = performance.now();
|
|
1963
|
+
const spinEnd = perfBefore + 2;
|
|
1964
|
+
while (performance.now() < spinEnd) {
|
|
1965
|
+
}
|
|
1966
|
+
const perfAfter = performance.now();
|
|
1967
|
+
if (perfAfter === perfBefore) {
|
|
1954
1968
|
anomalies.push("frozen-performance-now");
|
|
1955
1969
|
confidence = Math.max(confidence, 0.6);
|
|
1956
1970
|
}
|
|
@@ -2060,9 +2074,10 @@ var DOMContentTimingSignal = class extends Signal {
|
|
|
2060
2074
|
confidence = Math.max(confidence, 0.4);
|
|
2061
2075
|
}
|
|
2062
2076
|
try {
|
|
2077
|
+
const randomId = `__bdt_${Math.random().toString(36).slice(2)}`;
|
|
2063
2078
|
const startMutation = performance.now();
|
|
2064
2079
|
const testDiv = document.createElement("div");
|
|
2065
|
-
testDiv.id =
|
|
2080
|
+
testDiv.id = randomId;
|
|
2066
2081
|
document.body.appendChild(testDiv);
|
|
2067
2082
|
const afterAppend = performance.now();
|
|
2068
2083
|
document.body.removeChild(testDiv);
|
|
@@ -2172,23 +2187,30 @@ var PuppeteerSignal = class extends Signal {
|
|
|
2172
2187
|
indicators.push("default-viewport");
|
|
2173
2188
|
confidence = Math.max(confidence, 0.3);
|
|
2174
2189
|
}
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2190
|
+
const FRAMEWORK_PREFIXES = [
|
|
2191
|
+
"__zone_symbol__",
|
|
2192
|
+
// Angular / Zone.js
|
|
2193
|
+
"__next",
|
|
2194
|
+
// Next.js
|
|
2195
|
+
"__webpack",
|
|
2196
|
+
// webpack
|
|
2197
|
+
"__react",
|
|
2198
|
+
// React DevTools
|
|
2199
|
+
"__REACT",
|
|
2200
|
+
"__vite",
|
|
2201
|
+
// Vite
|
|
2202
|
+
"__nuxt"
|
|
2203
|
+
// Nuxt.js
|
|
2204
|
+
];
|
|
2179
2205
|
const suspiciousBindings = Object.keys(window).filter((key) => {
|
|
2180
|
-
|
|
2206
|
+
if (!key.startsWith("__")) return false;
|
|
2207
|
+
if (typeof window[key] !== "function") return false;
|
|
2208
|
+
return !FRAMEWORK_PREFIXES.some((prefix) => key.startsWith(prefix));
|
|
2181
2209
|
});
|
|
2182
|
-
if (suspiciousBindings.length >
|
|
2210
|
+
if (suspiciousBindings.length > 10) {
|
|
2183
2211
|
indicators.push("suspicious-bindings");
|
|
2184
2212
|
confidence = Math.max(confidence, 0.5);
|
|
2185
2213
|
}
|
|
2186
|
-
if (typeof window.chrome !== "undefined") {
|
|
2187
|
-
if (!window.chrome.runtime) {
|
|
2188
|
-
indicators.push("incomplete-chrome-object");
|
|
2189
|
-
confidence = Math.max(confidence, 0.4);
|
|
2190
|
-
}
|
|
2191
|
-
}
|
|
2192
2214
|
const triggered = indicators.length > 0;
|
|
2193
2215
|
return this.createResult(triggered, { indicators }, confidence);
|
|
2194
2216
|
}
|
|
@@ -2228,10 +2250,6 @@ var PlaywrightSignal = class extends Signal {
|
|
|
2228
2250
|
indicators.push("playwright-ua-marker");
|
|
2229
2251
|
confidence = Math.max(confidence, ua.includes("Playwright") ? 1 : 0.7);
|
|
2230
2252
|
}
|
|
2231
|
-
if (navigator.webdriver === true) {
|
|
2232
|
-
indicators.push("webdriver-flag");
|
|
2233
|
-
confidence = Math.max(confidence, 0.8);
|
|
2234
|
-
}
|
|
2235
2253
|
try {
|
|
2236
2254
|
const windowKeys = Object.keys(window);
|
|
2237
2255
|
const pwBindings = windowKeys.filter((k) => k.startsWith("__pw"));
|
|
@@ -2289,10 +2307,6 @@ var SeleniumSignal = class extends Signal {
|
|
|
2289
2307
|
async detect() {
|
|
2290
2308
|
const indicators = [];
|
|
2291
2309
|
let confidence = 0;
|
|
2292
|
-
if (navigator.webdriver === true) {
|
|
2293
|
-
indicators.push("webdriver-flag");
|
|
2294
|
-
confidence = Math.max(confidence, 1);
|
|
2295
|
-
}
|
|
2296
2310
|
const seleniumGlobals = [
|
|
2297
2311
|
"_selenium",
|
|
2298
2312
|
"callSelenium",
|
|
@@ -2459,14 +2473,10 @@ var PhantomJSSignal = class extends Signal {
|
|
|
2459
2473
|
}
|
|
2460
2474
|
const phantomProps = [
|
|
2461
2475
|
"__PHANTOM__",
|
|
2462
|
-
"PHANTOM"
|
|
2463
|
-
"Buffer",
|
|
2464
|
-
// PhantomJS exposes Node.js Buffer
|
|
2465
|
-
"process"
|
|
2466
|
-
// May expose Node.js process
|
|
2476
|
+
"PHANTOM"
|
|
2467
2477
|
];
|
|
2468
2478
|
for (const prop of phantomProps) {
|
|
2469
|
-
if (prop in window
|
|
2479
|
+
if (prop in window) {
|
|
2470
2480
|
indicators.push(`phantom-prop-${prop.toLowerCase()}`);
|
|
2471
2481
|
confidence = Math.max(confidence, 0.9);
|
|
2472
2482
|
}
|
|
@@ -2541,13 +2551,44 @@ function createDetector(options = {}) {
|
|
|
2541
2551
|
instantBotSignals = ["webdriver", "puppeteer", "playwright", "selenium", "phantomjs"],
|
|
2542
2552
|
...detectorOptions
|
|
2543
2553
|
} = options;
|
|
2544
|
-
const
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2554
|
+
const signalClasses = includeInteractionSignals ? [
|
|
2555
|
+
WebDriverSignal,
|
|
2556
|
+
HeadlessSignal,
|
|
2557
|
+
NavigatorAnomalySignal,
|
|
2558
|
+
PermissionsSignal,
|
|
2559
|
+
PluginsSignal,
|
|
2560
|
+
WebGLSignal,
|
|
2561
|
+
CanvasSignal,
|
|
2562
|
+
AudioContextSignal,
|
|
2563
|
+
ScreenSignal,
|
|
2564
|
+
PageLoadSignal,
|
|
2565
|
+
DOMContentTimingSignal,
|
|
2566
|
+
PuppeteerSignal,
|
|
2567
|
+
PlaywrightSignal,
|
|
2568
|
+
SeleniumSignal,
|
|
2569
|
+
PhantomJSSignal,
|
|
2570
|
+
MouseMovementSignal,
|
|
2571
|
+
KeyboardPatternSignal,
|
|
2572
|
+
InteractionTimingSignal,
|
|
2573
|
+
ScrollBehaviorSignal
|
|
2574
|
+
] : [
|
|
2575
|
+
WebDriverSignal,
|
|
2576
|
+
HeadlessSignal,
|
|
2577
|
+
NavigatorAnomalySignal,
|
|
2578
|
+
PermissionsSignal,
|
|
2579
|
+
PluginsSignal,
|
|
2580
|
+
WebGLSignal,
|
|
2581
|
+
CanvasSignal,
|
|
2582
|
+
AudioContextSignal,
|
|
2583
|
+
ScreenSignal,
|
|
2584
|
+
PageLoadSignal,
|
|
2585
|
+
DOMContentTimingSignal,
|
|
2586
|
+
PuppeteerSignal,
|
|
2587
|
+
PlaywrightSignal,
|
|
2588
|
+
SeleniumSignal,
|
|
2589
|
+
PhantomJSSignal
|
|
2590
|
+
];
|
|
2591
|
+
const signals = signalClasses.map((Cls) => new Cls());
|
|
2551
2592
|
return new BotDetector({
|
|
2552
2593
|
signals,
|
|
2553
2594
|
instantBotSignals,
|