@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.
@@ -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: this.calculate() > 0 ? (data.contribution / this.calculate() * 100).toFixed(1) : "0.0"
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 _BotDetector {
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
- const result = await Promise.race([
467
- signal.run(),
468
- new Promise(
469
- (resolve) => setTimeout(() => resolve({
470
- triggered: false,
471
- value: null,
472
- confidence: 0,
473
- error: "timeout"
474
- }), this._detectionTimeout)
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
- * Create a detector with default signals.
564
- * @param {Object} [options={}] - Configuration options
565
- * @returns {BotDetector}
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(options = {}) {
568
- return new _BotDetector(options);
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
- if (!platform || platform === "" || platform === "undefined") {
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 || 3e3;
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 || 5e3;
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 || 3e3;
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 domContentLoaded = timing.domContentLoadedEventEnd - navigationStart;
1918
- const domComplete = timing.domComplete - navigationStart;
1919
- const loadComplete = timing.loadEventEnd - navigationStart;
1920
- const dnsLookup = timing.domainLookupEnd - timing.domainLookupStart;
1921
- const tcpConnection = timing.connectEnd - timing.connectStart;
1922
- const serverResponse = timing.responseEnd - timing.requestStart;
1923
- const domProcessing = timing.domComplete - timing.domLoading;
1924
- if (domContentLoaded > 0 && domContentLoaded < 10) {
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 perfNow1 = performance.now();
1952
- const perfNow2 = performance.now();
1953
- if (perfNow1 === perfNow2 && perfNow1 > 0) {
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 = "__bot_detection_test__";
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
- if (navigator.webdriver === true) {
2176
- indicators.push("webdriver-flag");
2177
- confidence = Math.max(confidence, 0.9);
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
- return key.startsWith("__") && !key.startsWith("__zone_symbol__") && typeof window[key] === "function";
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 > 5) {
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 && prop !== "Buffer" && prop !== "process") {
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 signals = includeInteractionSignals ? [...defaultInstantSignals, ...defaultInteractionSignals.map((s) => {
2545
- const SignalClass = s.constructor;
2546
- return new SignalClass(s.options);
2547
- })] : defaultInstantSignals.map((s) => {
2548
- const SignalClass = s.constructor;
2549
- return new SignalClass(s.options);
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,