@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.
@@ -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: this.calculate() > 0 ? (data.contribution / this.calculate() * 100).toFixed(1) : "0.0"
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 _BotDetector {
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
- const result = await Promise.race([
521
- signal.run(),
522
- new Promise(
523
- (resolve) => setTimeout(() => resolve({
524
- triggered: false,
525
- value: null,
526
- confidence: 0,
527
- error: "timeout"
528
- }), this._detectionTimeout)
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
- * Create a detector with default signals.
618
- * @param {Object} [options={}] - Configuration options
619
- * @returns {BotDetector}
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(options = {}) {
622
- return new _BotDetector(options);
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
- if (!platform || platform === "" || platform === "undefined") {
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 || 3e3;
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 || 5e3;
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 || 3e3;
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 domContentLoaded = timing.domContentLoadedEventEnd - navigationStart;
1972
- const domComplete = timing.domComplete - navigationStart;
1973
- const loadComplete = timing.loadEventEnd - navigationStart;
1974
- const dnsLookup = timing.domainLookupEnd - timing.domainLookupStart;
1975
- const tcpConnection = timing.connectEnd - timing.connectStart;
1976
- const serverResponse = timing.responseEnd - timing.requestStart;
1977
- const domProcessing = timing.domComplete - timing.domLoading;
1978
- if (domContentLoaded > 0 && domContentLoaded < 10) {
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 perfNow1 = performance.now();
2006
- const perfNow2 = performance.now();
2007
- if (perfNow1 === perfNow2 && perfNow1 > 0) {
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 = "__bot_detection_test__";
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
- if (navigator.webdriver === true) {
2230
- indicators.push("webdriver-flag");
2231
- confidence = Math.max(confidence, 0.9);
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
- return key.startsWith("__") && typeof window[key] === "function";
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 > 3) {
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 && prop !== "Buffer" && prop !== "process") {
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 signals = includeInteractionSignals ? [...defaultInstantSignals, ...defaultInteractionSignals.map((s) => {
2599
- const SignalClass = s.constructor;
2600
- return new SignalClass(s.options);
2601
- })] : defaultInstantSignals.map((s) => {
2602
- const SignalClass = s.constructor;
2603
- return new SignalClass(s.options);
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,