@fanboynz/network-scanner 2.0.54 → 2.0.56

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.
@@ -15,7 +15,7 @@ const CHROME_TEMP_PATHS = [
15
15
  ];
16
16
 
17
17
  const CHROME_TEMP_PATTERNS = [
18
- '.com.google.Chrome.*', // Google Chrome temp files
18
+ 'com.google.Chrome.*', // Google Chrome temp files (no leading dot)
19
19
  '.org.chromium.Chromium.*',
20
20
  'puppeteer-*'
21
21
  ];
@@ -39,6 +39,7 @@ async function cleanupChromeTempFiles(options = {}) {
39
39
 
40
40
  // Base cleanup commands for standard temp directories
41
41
  const cleanupCommands = [
42
+ 'rm -rf /tmp/com.google.Chrome.* 2>/dev/null || true',
42
43
  'rm -rf /tmp/.com.google.Chrome.* 2>/dev/null || true',
43
44
  'rm -rf /tmp/.org.chromium.Chromium.* 2>/dev/null || true',
44
45
  'rm -rf /tmp/puppeteer-* 2>/dev/null || true',
@@ -48,6 +49,7 @@ async function cleanupChromeTempFiles(options = {}) {
48
49
 
49
50
  // Add snap-specific cleanup if requested
50
51
  if (includeSnapTemp || comprehensive) {
52
+ cleanupCommands.push('rm -rf /dev/shm/com.google.Chrome.* 2>/dev/null || true');
51
53
  cleanupCommands.push(
52
54
  'rm -rf /tmp/snap-private-tmp/snap.chromium/tmp/.org.chromium.Chromium.* 2>/dev/null || true',
53
55
  'rm -rf /tmp/snap-private-tmp/snap.chromium/tmp/puppeteer-* 2>/dev/null || true'
package/lib/cloudflare.js CHANGED
@@ -1,5 +1,20 @@
1
1
  /**
2
2
  * Cloudflare bypass and challenge handling module - Optimized with smart detection and adaptive timeouts
3
+ * Version: 2.7.0 - Major fixes and performance overhaul
4
+ * - Fix: Challenge solvers had empty if-blocks (JS/Turnstile/Legacy never executed in non-debug mode)
5
+ * - Fix: a[href*="continue"] false positive removed (matched nearly every website)
6
+ * - Perf: Domain-level detection cache (was per-URL, now per-hostname)
7
+ * - Perf: Timeout outcome caching (domain times out once -> all subsequent URLs skip instantly)
8
+ * - Perf: Short-circuit quick detection (title/URL -> fast selectors -> slow text, early return at each stage)
9
+ * - Perf: Eliminated body.textContent in quick detection (was extracting entire DOM text tree)
10
+ * - Perf: Capped body.textContent to 2KB in analyzeCloudflareChallenge
11
+ * - Perf: No-indicator pages skip immediately regardless of config (was 10-15s wasted)
12
+ * - Perf: Quick detection timeout 4s->2s, retries 2->1
13
+ * - Perf: PAGE_EVALUATION timeout 12s->5s, detached frame delay 3s->1s
14
+ * - Perf: Inner timeouts tightened to fit within outer adaptive timeouts
15
+ * - Perf: CHALLENGE_SOLVING 30s->12s, TURNSTILE_COMPLETION 20s->10s, JS_CHALLENGE_BUFFER 26s->12s
16
+ * - Perf: MAX_RETRIES 3->2, baseDelay 1000->800ms, maxDelay 8000->5000ms
17
+ * - Perf: Parallel detection gated behind cloudflare config (was running on every URL)
3
18
  * Version: 2.6.3 - Fixes Cannot read properties of undefined (reading 'hasIndicators')
4
19
  * Version: 2.6.2 - Further detached Frame fixes
5
20
  * Version: 2.6.1 - timeoutId is not defined & race condition fix
@@ -20,7 +35,7 @@ const { formatLogMessage } = require('./colorize');
20
35
  /**
21
36
  * Module version information
22
37
  */
23
- const CLOUDFLARE_MODULE_VERSION = '2.6.3';
38
+ const CLOUDFLARE_MODULE_VERSION = '2.7.0';
24
39
 
25
40
  /**
26
41
  * Timeout constants for various operations (in milliseconds)
@@ -28,13 +43,13 @@ const CLOUDFLARE_MODULE_VERSION = '2.6.3';
28
43
  * All values tuned for maximum scanning speed while maintaining functionality
29
44
  */
30
45
  const TIMEOUTS = {
31
- PAGE_EVALUATION: 12000, // Standard page evaluation timeout
32
- PAGE_EVALUATION_SAFE: 12000, // Safe page evaluation with extra buffer
46
+ PAGE_EVALUATION: 5000, // Standard page evaluation timeout (DOM queries are instant)
47
+ PAGE_EVALUATION_SAFE: 5000, // Safe page evaluation with extra buffer
33
48
  PHISHING_CLICK: 3000, // Timeout for clicking phishing continue button
34
49
  PHISHING_NAVIGATION: 8000, // Wait for navigation after phishing bypass
35
- JS_CHALLENGE_BUFFER: 26000, // JS challenge with safety buffer
36
- TURNSTILE_COMPLETION: 20000, // Turnstile completion check
37
- TURNSTILE_COMPLETION_BUFFER: 25000, // Turnstile completion with buffer
50
+ JS_CHALLENGE_BUFFER: 12000, // JS challenge -- must fit within 15s adaptive outer timeout
51
+ TURNSTILE_COMPLETION: 10000, // Turnstile completion check -- fits within adaptive timeout
52
+ TURNSTILE_COMPLETION_BUFFER: 12000, // Turnstile completion with buffer
38
53
  CLICK_TIMEOUT: 5000, // Standard click operation timeout
39
54
  CLICK_TIMEOUT_BUFFER: 1000, // Click timeout safety buffer
40
55
  NAVIGATION_TIMEOUT: 15000, // Standard navigation timeout
@@ -45,21 +60,21 @@ const TIMEOUTS = {
45
60
  ADAPTIVE_TIMEOUT_AUTO_WITHOUT_INDICATORS: 10000, // Adaptive timeout for auto-detected without indicators
46
61
  // New timeouts for enhanced functionality
47
62
  RETRY_DELAY: 1000, // Delay between retry attempts
48
- MAX_RETRIES: 3, // Maximum retry attempts for operations
63
+ MAX_RETRIES: 2, // Maximum retry attempts (only 2 fit within 25s outer timeout)
49
64
  CHALLENGE_POLL_INTERVAL: 500, // Interval for polling challenge completion
50
65
  CHALLENGE_MAX_POLLS: 20 // Maximum polling attempts
51
66
  };
52
67
 
53
68
  // Fast timeout constants - optimized for speed
54
69
  const FAST_TIMEOUTS = {
55
- QUICK_DETECTION: 4000, // Fast Cloudflare detection
70
+ QUICK_DETECTION: 2000, // Fast Cloudflare detection (DOM check, instant on loaded pages)
56
71
  PHISHING_WAIT: 1000, // Fast phishing check
57
72
  CHALLENGE_WAIT: 500, // Fast challenge detection
58
73
  ELEMENT_INTERACTION_DELAY: 250, // Fast element interactions
59
74
  SELECTOR_WAIT: 3000, // Fast selector waits
60
75
  TURNSTILE_OPERATION: 6000, // Fast Turnstile operations
61
76
  JS_CHALLENGE: 10000, // Fast JS challenge completion
62
- CHALLENGE_SOLVING: 30000, // Fast overall challenge solving
77
+ CHALLENGE_SOLVING: 12000, // Overall challenge solving -- fits within 15s adaptive outer
63
78
  CHALLENGE_COMPLETION: 8000 // Fast completion check
64
79
  };
65
80
 
@@ -68,7 +83,7 @@ const FAST_TIMEOUTS = {
68
83
  * Returns {found, clicked, x, y} - coordinates allow fallback mouse.click
69
84
  */
70
85
  async function clickInShadowDOM(context, selectors, forceDebug = false, waitMs = 1500) {
71
- // Try Puppeteer's pierce/ selector first handles CLOSED shadow roots via CDP
86
+ // Try Puppeteer's pierce/ selector first -- handles CLOSED shadow roots via CDP
72
87
  for (const selector of selectors) {
73
88
  try {
74
89
  // Wait for element to appear (handles delayed rendering)
@@ -77,7 +92,7 @@ async function clickInShadowDOM(context, selectors, forceDebug = false, waitMs =
77
92
  if (element) {
78
93
  const box = await element.boundingBox();
79
94
  if (box && box.width > 0 && box.height > 0) {
80
- if (forceDebug) console.log(formatLogMessage('cloudflare', `pierce/${selector} matched in ${Date.now() - start}ms box: ${box.width}x${box.height} at (${box.x},${box.y})`));
95
+ if (forceDebug) console.log(formatLogMessage('cloudflare', `pierce/${selector} matched in ${Date.now() - start}ms -- box: ${box.width}x${box.height} at (${box.x},${box.y})`));
81
96
  await element.click();
82
97
  await element.dispose();
83
98
  return { found: true, clicked: true, selector, x: box.x + box.width / 2, y: box.y + box.height / 2 };
@@ -185,9 +200,9 @@ function detectChallengeLoop(url, previousUrls = []) {
185
200
  * Retry configuration with exponential backoff
186
201
  */
187
202
  const RETRY_CONFIG = {
188
- maxAttempts: 3,
189
- baseDelay: 1000,
190
- maxDelay: 8000,
203
+ maxAttempts: 2, // Only 2 attempts fit within 25s outer timeout
204
+ baseDelay: 800, // Slightly faster retry delay
205
+ maxDelay: 5000, // Lower max delay cap
191
206
  backoffMultiplier: 2,
192
207
  retryableErrors: [ERROR_TYPES.NETWORK, ERROR_TYPES.TIMEOUT, ERROR_TYPES.ELEMENT_NOT_FOUND, ERROR_TYPES.DETACHED_FRAME]
193
208
  };
@@ -209,7 +224,7 @@ class CloudflareDetectionCache {
209
224
  getCacheKey(url) {
210
225
  try {
211
226
  const urlObj = new URL(url);
212
- return `${urlObj.hostname}${urlObj.pathname}`;
227
+ return urlObj.hostname; // Domain-level caching: all URLs from same host share one entry
213
228
  } catch {
214
229
  return url;
215
230
  }
@@ -441,8 +456,8 @@ async function safePageEvaluate(page, func, timeout = TIMEOUTS.PAGE_EVALUATION_S
441
456
  if (forceDebug) {
442
457
  console.warn(formatLogMessage('cloudflare', `Detached frame detected on attempt ${attempt}/${maxRetries} - using longer delay`));
443
458
  }
444
- // For detached frames, wait longer
445
- await new Promise(resolve => setTimeout(resolve, 3000)); // Longer delay
459
+ // For detached frames, brief delay before retry
460
+ await new Promise(resolve => setTimeout(resolve, 1000));
446
461
 
447
462
  // For detached frames, only retry once more
448
463
  if (attempt >= 2) {
@@ -541,44 +556,67 @@ async function quickCloudflareDetection(page, forceDebug = false) {
541
556
  // Perform actual detection with enhanced error handling
542
557
  const quickCheck = await safePageEvaluate(page, () => {
543
558
  const title = document.title || '';
544
- const bodyText = document.body ? document.body.textContent.substring(0, 500) : '';
545
559
  const url = window.location.href;
546
560
 
547
- // Enhanced indicators with 2025 patterns
548
- const hasCloudflareIndicators =
561
+ // FAST PATH: Check title + URL first (string ops, no DOM traversal)
562
+ const titleMatch =
549
563
  title.includes('Just a moment') ||
550
564
  title.includes('Checking your browser') ||
551
565
  title.includes('Attention Required') ||
552
- title.includes('Security check') || // New pattern
566
+ title.includes('Security check');
567
+
568
+ const urlMatch =
569
+ url.includes('/cdn-cgi/challenge-platform/') ||
570
+ url.includes('cloudflare.com');
571
+
572
+ if (titleMatch || urlMatch) {
573
+ return { hasIndicators: true, title, url, bodySnippet: '' };
574
+ }
575
+
576
+ // MEDIUM PATH: Check a few fast selectors before expensive text extraction
577
+ const selectorMatch =
578
+ document.querySelector('[data-ray]') ||
579
+ document.querySelector('[data-cf-challenge]') ||
580
+ document.querySelector('.cf-challenge-running') ||
581
+ document.querySelector('.cf-turnstile') ||
582
+ document.querySelector('.cf-managed-challenge') ||
583
+ document.querySelector('[data-cf-managed]') ||
584
+ document.querySelector('script[src*="/cdn-cgi/challenge-platform/"]');
585
+
586
+ if (selectorMatch) {
587
+ return { hasIndicators: true, title, url, bodySnippet: '' };
588
+ }
589
+
590
+ // SLOW PATH: Extract limited body text only if fast checks failed
591
+ // Use body.innerText capped to first child nodes instead of full textContent
592
+ let bodyText = '';
593
+ if (document.body) {
594
+ const el = document.body.querySelector('.main-wrapper, .main-content, #challenge-body-text, .cf-challenge-container');
595
+ bodyText = el ? el.textContent.substring(0, 300) : (document.body.firstElementChild ? document.body.firstElementChild.textContent.substring(0, 300) : '');
596
+ }
597
+
598
+ const textMatch =
553
599
  bodyText.includes('Cloudflare') ||
554
600
  bodyText.includes('cf-ray') ||
555
601
  bodyText.includes('Verify you are human') ||
556
602
  bodyText.includes('This website has been reported for potential phishing') ||
557
603
  bodyText.includes('Please wait while we verify') ||
558
- bodyText.includes('Checking if the site connection is secure') || // New pattern
559
- url.includes('/cdn-cgi/challenge-platform/') ||
560
- url.includes('cloudflare.com') ||
561
- document.querySelector('[data-ray]') ||
562
- document.querySelector('[data-cf-challenge]') ||
563
- document.querySelector('.cf-challenge-running') ||
604
+ bodyText.includes('Checking if the site connection is secure');
605
+
606
+ // Remaining slower selectors
607
+ const slowSelectorMatch =
564
608
  document.querySelector('.cf-challenge-container') ||
565
- document.querySelector('.cf-turnstile') ||
566
609
  document.querySelector('.ctp-checkbox-container') ||
567
610
  document.querySelector('iframe[src*="challenges.cloudflare.com"]') ||
568
- document.querySelector('iframe[title*="Cloudflare security challenge"]') ||
569
- document.querySelector('script[src*="/cdn-cgi/challenge-platform/"]') ||
570
- document.querySelector('a[href*="continue"]') ||
571
- // New selectors for 2025
572
- document.querySelector('.cf-managed-challenge') ||
573
- document.querySelector('[data-cf-managed]');
611
+ document.querySelector('iframe[title*="Cloudflare security challenge"]');
574
612
 
575
613
  return {
576
- hasIndicators: hasCloudflareIndicators,
614
+ hasIndicators: !!(textMatch || slowSelectorMatch),
577
615
  title,
578
616
  url,
579
617
  bodySnippet: bodyText.substring(0, 200)
580
618
  };
581
- }, FAST_TIMEOUTS.QUICK_DETECTION, { maxRetries: 2, forceDebug });
619
+ }, FAST_TIMEOUTS.QUICK_DETECTION, { maxRetries: 1, forceDebug });
582
620
 
583
621
  // Cache the result
584
622
  detectionCache.set(currentPageUrl, quickCheck);
@@ -607,7 +645,7 @@ async function quickCloudflareDetection(page, forceDebug = false) {
607
645
  */
608
646
  async function analyzeCloudflareChallenge(page) {
609
647
  try {
610
- // CDP-level frame check bypasses closed shadow roots
648
+ // CDP-level frame check -- bypasses closed shadow roots
611
649
  const frames = page.frames();
612
650
  const hasChallengeFrame = frames.some(f => {
613
651
  const url = f.url();
@@ -616,7 +654,8 @@ async function analyzeCloudflareChallenge(page) {
616
654
 
617
655
  const result = await safePageEvaluate(page, () => {
618
656
  const title = document.title || '';
619
- const bodyText = document.body ? document.body.textContent : '';
657
+ // Cap text extraction -- on content-heavy pages body.textContent can be megabytes
658
+ const bodyText = document.body ? document.body.textContent.substring(0, 2000) : '';
620
659
 
621
660
  // Updated selectors for 2025 Cloudflare challenges
622
661
  const hasTurnstileIframe = document.querySelector('iframe[title*="Cloudflare security challenge"]') !== null ||
@@ -649,8 +688,7 @@ async function analyzeCloudflareChallenge(page) {
649
688
  bodyText.includes('Please wait while we verify');
650
689
 
651
690
  const hasPhishingWarning = bodyText.includes('This website has been reported for potential phishing') ||
652
- title.includes('Attention Required') ||
653
- document.querySelector('a[href*="continue"]') !== null;
691
+ title.includes('Attention Required');
654
692
 
655
693
  const hasTurnstileResponse = document.querySelector('input[name="cf-turnstile-response"]') !== null;
656
694
 
@@ -687,7 +725,7 @@ async function analyzeCloudflareChallenge(page) {
687
725
  };
688
726
  }, TIMEOUTS.PAGE_EVALUATION);
689
727
 
690
- // Merge CDP frame detection catches iframes behind closed shadow roots
728
+ // Merge CDP frame detection -- catches iframes behind closed shadow roots
691
729
  if (hasChallengeFrame && !result.hasTurnstileIframe) {
692
730
  result.hasTurnstileIframe = true;
693
731
  result.isTurnstile = true;
@@ -1088,20 +1126,10 @@ async function attemptChallengeSolve(page, currentUrl, challengeInfo, forceDebug
1088
1126
  if (jsResult.success) {
1089
1127
  // Wait for redirect after challenge completion
1090
1128
  try {
1091
- const startUrl = await page.url();
1092
- await page.waitForFunction(
1093
- (origUrl) => {
1094
- const bodyText = document.body?.textContent || '';
1095
- return document.title !== 'Just a moment...' ||
1096
- window.location.href !== origUrl ||
1097
- bodyText.includes('Verification successful');
1098
- },
1099
- { timeout: 10000 },
1100
- startUrl
1101
- );
1102
- if (forceDebug) console.log(formatLogMessage('cloudflare', `Challenge page cleared for ${currentUrl}`));
1103
- } catch (_) {
1104
- if (forceDebug) console.log(formatLogMessage('cloudflare', `Challenge page not cleared after 10s � continuing`));
1129
+ await page.waitForNavigation({ waitUntil: 'domcontentloaded', timeout: 10000 });
1130
+ if (forceDebug) console.log(formatLogMessage('cloudflare', `Post-challenge redirect completed for ${currentUrl}`));
1131
+ } catch (navErr) {
1132
+ if (forceDebug) console.log(formatLogMessage('cloudflare', `Post-challenge redirect timeout (may already be on target page): ${navErr.message}`));
1105
1133
  }
1106
1134
  result.success = true;
1107
1135
  result.method = 'js_challenge_wait';
@@ -1183,7 +1211,7 @@ async function handleEmbeddedIframeChallenge(page, forceDebug = false) {
1183
1211
  try {
1184
1212
  if (forceDebug) console.log(formatLogMessage('cloudflare', `Checking for embedded iframe challenges`));
1185
1213
 
1186
- // Use CDP-level frame detection bypasses closed shadow roots
1214
+ // Use CDP-level frame detection -- bypasses closed shadow roots
1187
1215
  const frames = page.frames();
1188
1216
  if (forceDebug) {
1189
1217
  console.log(formatLogMessage('cloudflare', `Available frames (${frames.length}):`));
@@ -1624,10 +1652,11 @@ async function handleCloudflareProtection(page, currentUrl, siteConfig, forceDeb
1624
1652
 
1625
1653
  // Early return structure when no Cloudflare indicators found
1626
1654
  // Sets attempted: false, success: true for both protection types
1627
-
1628
- // Only proceed if we have indicators OR explicit config enables Cloudflare handling
1629
- if (!quickDetection.hasIndicators && !cfPhishEnabled && !cfBypassEnabled) {
1630
- if (forceDebug) console.log(formatLogMessage('cloudflare', `No Cloudflare indicators found and no explicit config, skipping protection handling for ${currentUrl}`));
1655
+ // Skip immediately if no Cloudflare indicators detected
1656
+ // Trust the detection -- explicit config only matters when indicators ARE found
1657
+ // This avoids a 10s adaptive timeout on non-Cloudflare sites
1658
+ if (!quickDetection.hasIndicators) {
1659
+ if (forceDebug) console.log(formatLogMessage('cloudflare', `No Cloudflare indicators found, skipping protection handling for ${currentUrl}`));
1631
1660
  if (forceDebug) console.log(formatLogMessage('cloudflare', `Quick detection details: title="${quickDetection.title}", bodySnippet="${quickDetection.bodySnippet}"`));
1632
1661
  return {
1633
1662
  phishingWarning: { attempted: false, success: true },
@@ -1663,10 +1692,24 @@ async function handleCloudflareProtection(page, currentUrl, siteConfig, forceDeb
1663
1692
  console.log(formatLogMessage('cloudflare', `Using adaptive timeout of ${adaptiveTimeout}ms for ${currentUrl} (indicators: ${quickDetection.hasIndicators}, explicit config: ${!!(siteConfig.cloudflare_phish || siteConfig.cloudflare_bypass)})`));
1664
1693
  }
1665
1694
 
1666
- return await Promise.race([
1667
- performCloudflareHandling(page, currentUrl, siteConfig, cfDebug),
1695
+ // Check if this domain already timed out -- skip immediately
1696
+ try {
1697
+ const outcomeCacheKey = 'outcome:' + new URL(currentUrl).hostname;
1698
+ const cachedOutcome = detectionCache.cache.get(outcomeCacheKey);
1699
+ if (cachedOutcome && cachedOutcome.data && cachedOutcome.data.timedOut && Date.now() - cachedOutcome.timestamp < detectionCache.ttl) {
1700
+ if (forceDebug) console.log(formatLogMessage('cloudflare', `Skipping ${currentUrl} -- domain already timed out on a previous URL`));
1701
+ return cachedOutcome.data;
1702
+ }
1703
+ } catch (e) { /* malformed URL, proceed normally */ }
1704
+
1705
+ let adaptiveTimeoutId = null;
1706
+ const handlingResult = await Promise.race([
1707
+ performCloudflareHandling(page, currentUrl, siteConfig, cfDebug).then(r => {
1708
+ if (adaptiveTimeoutId) clearTimeout(adaptiveTimeoutId);
1709
+ return r;
1710
+ }),
1668
1711
  new Promise((resolve) => {
1669
- setTimeout(() => {
1712
+ adaptiveTimeoutId = setTimeout(() => {
1670
1713
  console.warn(formatLogMessage('cloudflare', `Adaptive timeout (${adaptiveTimeout}ms) for ${currentUrl} - continuing with scan`));
1671
1714
  resolve({
1672
1715
  phishingWarning: { attempted: false, success: true },
@@ -1678,6 +1721,16 @@ async function handleCloudflareProtection(page, currentUrl, siteConfig, forceDeb
1678
1721
  }, adaptiveTimeout);
1679
1722
  })
1680
1723
  ]);
1724
+
1725
+ // Cache timeout results at domain level so subsequent URLs skip immediately
1726
+ if (handlingResult.timedOut) {
1727
+ try {
1728
+ const setOutcomeKey = 'outcome:' + new URL(currentUrl).hostname;
1729
+ detectionCache.cache.set(setOutcomeKey, { data: handlingResult, timestamp: Date.now() });
1730
+ } catch (e) { /* malformed URL, skip caching */ }
1731
+ }
1732
+
1733
+ return handlingResult;
1681
1734
  } catch (error) {
1682
1735
  result.overallSuccess = false;
1683
1736
  result.errors.push(`Cloudflare handling failed: ${error.message}`);
@@ -1809,8 +1862,7 @@ async function parallelChallengeDetection(page, forceDebug = false) {
1809
1862
  { type: 'turnstile', detected: document.querySelector('.cf-turnstile') !== null ||
1810
1863
  document.querySelector('iframe[src*="challenges.cloudflare.com"]') !== null ||
1811
1864
  document.querySelector('.ctp-checkbox-container') !== null },
1812
- { type: 'phishing', detected: bodyText.includes('This website has been reported for potential phishing') ||
1813
- document.querySelector('a[href*="continue"]') !== null },
1865
+ { type: 'phishing', detected: bodyText.includes('This website has been reported for potential phishing') },
1814
1866
  { type: 'managed', detected: document.querySelector('.cf-managed-challenge') !== null ||
1815
1867
  document.querySelector('[data-cf-managed]') !== null }
1816
1868
  ];
@@ -24,28 +24,6 @@ function seededRandom(seed) {
24
24
  const _fingerprintCache = new Map();
25
25
 
26
26
  // Type-specific property spoofing functions for monomorphic optimization
27
- function spoofNavigatorProperties(navigator, properties, options = {}) {
28
- if (!navigator || typeof navigator !== 'object') return false;
29
-
30
- for (const [prop, descriptor] of Object.entries(properties)) {
31
- if (!safeDefineProperty(navigator, prop, descriptor, options)) {
32
- if (options.debug) console.log(`[fingerprint] Failed to spoof navigator.${prop}`);
33
- }
34
- }
35
- return true;
36
- }
37
-
38
- function spoofScreenProperties(screen, properties, options = {}) {
39
- if (!screen || typeof screen !== 'object') return false;
40
-
41
- for (const [prop, descriptor] of Object.entries(properties)) {
42
- if (!safeDefineProperty(screen, prop, descriptor, options)) {
43
- if (options.debug) console.log(`[fingerprint] Failed to spoof screen.${prop}`);
44
- }
45
- }
46
- return true;
47
- }
48
-
49
27
  // Built-in properties that should not be modified
50
28
  const BUILT_IN_PROPERTIES = new Set([
51
29
  'href', 'origin', 'protocol', 'host', 'hostname', 'port', 'pathname', 'search', 'hash',
@@ -379,6 +357,27 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
379
357
  return false;
380
358
  };
381
359
  })();
360
+
361
+ // Function.prototype.toString protection — make spoofed functions appear native
362
+ // Must be installed BEFORE any property overrides so all spoofs are protected
363
+ const nativeFunctionStore = new WeakMap();
364
+ const originalToString = Function.prototype.toString;
365
+
366
+ function maskAsNative(fn, nativeName) {
367
+ if (typeof fn === 'function') {
368
+ nativeFunctionStore.set(fn, nativeName || fn.name || '');
369
+ }
370
+ return fn;
371
+ }
372
+
373
+ Function.prototype.toString = function() {
374
+ if (nativeFunctionStore.has(this)) {
375
+ return `function ${nativeFunctionStore.get(this)}() { [native code] }`;
376
+ }
377
+ return originalToString.call(this);
378
+ };
379
+ // Protect the toString override itself
380
+ nativeFunctionStore.set(Function.prototype.toString, 'toString');
382
381
 
383
382
  // Create safe property definition helper
384
383
  function safeDefinePropertyLocal(target, property, descriptor) {
@@ -439,10 +438,14 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
439
438
  // Remove webdriver properties
440
439
  //
441
440
  safeExecute(() => {
442
- try {
443
- delete navigator.webdriver;
444
- } catch (e) {}
445
- safeDefinePropertyLocal(navigator, 'webdriver', { get: () => false });
441
+ // In real Chrome, webdriver lives on Navigator.prototype, not the instance.
442
+ // Override it there so Object.getOwnPropertyDescriptor(navigator, 'webdriver') returns undefined.
443
+ try { delete navigator.webdriver; } catch (e) {}
444
+ Object.defineProperty(Navigator.prototype, 'webdriver', {
445
+ get: () => false,
446
+ configurable: true,
447
+ enumerable: true
448
+ });
446
449
  }, 'webdriver removal');
447
450
 
448
451
  // Remove automation properties
@@ -861,6 +864,8 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
861
864
  if (typeof originalStack === 'string') {
862
865
  return originalStack
863
866
  .replace(/.*puppeteer.*\n?/gi, '')
867
+ .replace(/.*__puppeteer_evaluation_script__.*\n?/gi, '')
868
+ .replace(/.*evaluateOnNewDocument.*\n?/gi, '')
864
869
  .replace(/.*chrome-devtools.*\n?/gi, '')
865
870
  .replace(/.*webdriver.*\n?/gi, '')
866
871
  .replace(/.*automation.*\n?/gi, '')
@@ -1635,6 +1640,55 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
1635
1640
  });
1636
1641
  }, 'location URL masking');
1637
1642
 
1643
+ // Bulk-mask all spoofed prototype methods so toString() returns "[native code]"
1644
+ // Must run AFTER all overrides are applied
1645
+ safeExecute(() => {
1646
+ const protoMasks = [
1647
+ [WebGLRenderingContext.prototype, ['getParameter', 'getExtension', 'getSupportedExtensions']],
1648
+ [HTMLCanvasElement.prototype, ['getContext', 'toDataURL', 'toBlob']],
1649
+ [CanvasRenderingContext2D.prototype, ['getImageData', 'fillText', 'strokeText', 'measureText']],
1650
+ [EventTarget.prototype, ['addEventListener', 'removeEventListener']],
1651
+ [Date.prototype, ['getTimezoneOffset']],
1652
+ ];
1653
+ if (typeof WebGL2RenderingContext !== 'undefined') {
1654
+ protoMasks.push([WebGL2RenderingContext.prototype, ['getParameter', 'getExtension']]);
1655
+ }
1656
+ protoMasks.forEach(([proto, methods]) => {
1657
+ methods.forEach(name => {
1658
+ if (typeof proto[name] === 'function') maskAsNative(proto[name], name);
1659
+ });
1660
+ });
1661
+
1662
+ // Mask navigator/window method overrides
1663
+ if (typeof navigator.permissions?.query === 'function') maskAsNative(navigator.permissions.query, 'query');
1664
+ if (typeof navigator.getBattery === 'function') maskAsNative(navigator.getBattery, 'getBattery');
1665
+ if (typeof speechSynthesis?.getVoices === 'function') maskAsNative(speechSynthesis.getVoices, 'getVoices');
1666
+ if (typeof performance.now === 'function') maskAsNative(performance.now, 'now');
1667
+ if (typeof Notification?.requestPermission === 'function') maskAsNative(Notification.requestPermission, 'requestPermission');
1668
+ if (typeof window.RTCPeerConnection === 'function') maskAsNative(window.RTCPeerConnection, 'RTCPeerConnection');
1669
+ if (typeof window.Image === 'function') maskAsNative(window.Image, 'Image');
1670
+ if (typeof window.fetch === 'function') maskAsNative(window.fetch, 'fetch');
1671
+ if (typeof window.PointerEvent === 'function') maskAsNative(window.PointerEvent, 'PointerEvent');
1672
+
1673
+ // Mask property getters on navigator
1674
+ const navProps = ['userAgentData', 'connection', 'pdfViewerEnabled', 'webdriver',
1675
+ 'hardwareConcurrency', 'deviceMemory', 'platform', 'maxTouchPoints'];
1676
+ navProps.forEach(prop => {
1677
+ // Check both instance and prototype (webdriver lives on prototype)
1678
+ const desc = Object.getOwnPropertyDescriptor(navigator, prop)
1679
+ || Object.getOwnPropertyDescriptor(Navigator.prototype, prop);
1680
+ if (desc?.get) maskAsNative(desc.get, 'get ' + prop);
1681
+ });
1682
+
1683
+ // Mask window property getters
1684
+ ['screenX', 'screenY', 'outerWidth', 'outerHeight'].forEach(prop => {
1685
+ const desc = Object.getOwnPropertyDescriptor(window, prop);
1686
+ if (desc?.get) maskAsNative(desc.get, 'get ' + prop);
1687
+ });
1688
+
1689
+ if (debugEnabled) console.log('[fingerprint] toString protection applied to all spoofed functions');
1690
+ }, 'Function.prototype.toString bulk masking');
1691
+
1638
1692
  }, ua, forceDebug, selectedGpu);
1639
1693
  } catch (stealthErr) {
1640
1694
  if (stealthErr.message.includes('Session closed') ||
@@ -2030,9 +2084,6 @@ async function applyAllFingerprintSpoofing(page, siteConfig, forceDebug, current
2030
2084
  }
2031
2085
 
2032
2086
  // Legacy compatibility function - maintained for backwards compatibility
2033
- function safeExecuteSpoofing(spoofFunction, description, forceDebug = false) {
2034
- return safeSpoofingExecution(spoofFunction, description, { debug: forceDebug });
2035
- }
2036
2087
 
2037
2088
 
2038
2089
  module.exports = {
@@ -2044,7 +2095,6 @@ module.exports = {
2044
2095
  applyAllFingerprintSpoofing,
2045
2096
  simulateHumanBehavior,
2046
2097
  safeDefineProperty,
2047
- safeExecuteSpoofing, // Legacy compatibility
2048
2098
  safeSpoofingExecution,
2049
2099
  DEFAULT_PLATFORM,
2050
2100
  DEFAULT_TIMEZONE
@@ -123,6 +123,20 @@ const ELEMENT_INTERACTION = {
123
123
  MISTAKE_RATE: 0.02 // Probability of typing mistakes (0.02 = 2% chance)
124
124
  };
125
125
 
126
+ // === CONTENT CLICK CONSTANTS ===
127
+ // For triggering document-level onclick handlers (e.g., Monetag onclick_static)
128
+ // These clicks target the page content area, not specific UI elements
129
+ // NOTE: No preDelay needed — mouse movements + scrolling already provide ~1s
130
+ // of activity before clicks fire, which is enough for async ad script registration
131
+ const CONTENT_CLICK = {
132
+ CLICK_COUNT: 2, // Two attempts (primary + backup if first suppressed)
133
+ INTER_CLICK_MIN: 300, // Minimum ms between clicks (above Monetag 250ms cooldown)
134
+ INTER_CLICK_MAX: 500, // Maximum ms between clicks
135
+ PRE_CLICK_DELAY: 300, // Small buffer for late-loading async ad scripts
136
+ VIEWPORT_INSET: 0.2, // Avoid outer 20% of viewport (menus, overlays)
137
+ MOUSE_APPROACH_STEPS: 3 // Minimal steps — just enough for non-instant movement
138
+ };
139
+
126
140
  // === INTENSITY SETTINGS ===
127
141
  // Pre-configured intensity levels - modify these to change overall behavior
128
142
  const INTENSITY_SETTINGS = {
@@ -606,6 +620,97 @@ async function interactWithElements(page, options = {}) {
606
620
  }
607
621
  }
608
622
 
623
+ /**
624
+ * Clicks random spots in the page content area to trigger document-level
625
+ * onclick handlers (Monetag onclick_static, similar popunder SDKs).
626
+ *
627
+ * WHY THIS EXISTS:
628
+ * Ad onclick SDKs attach a single listener on `document` (capture phase)
629
+ * that fires on ANY click with `isTrusted: true`. They don't care which
630
+ * element was clicked — just that a real input event reached the document.
631
+ * `interactWithElements()` hunts for <button>/<a> which may not exist or
632
+ * may be excluded by the SDK's own filter. This function simply clicks
633
+ * the content area of the page where the SDK will always accept the event.
634
+ *
635
+ * TIMING:
636
+ * - 300ms preDelay: small buffer after mouse/scroll activity (~1.2s) for
637
+ * any late-loading async ad scripts to finish registering listeners.
638
+ * - Spaces clicks 300-500ms apart (above Monetag's 250ms cooldown).
639
+ * - Total time: ~1.1s for 2 clicks (preDelay + move + pause + click + gap).
640
+ *
641
+ * TARGETING:
642
+ * - Clicks within the inner 60% of the viewport to avoid sticky headers,
643
+ * footers, sidebars, cookie banners, and overlay close buttons.
644
+ * - Each click gets a fresh random position with natural mouse approach.
645
+ *
646
+ * @param {import('puppeteer').Page} page
647
+ * @param {object} [options]
648
+ * @param {number} [options.clicks] Number of click attempts
649
+ * @param {number} [options.preDelay] Ms to wait before first click
650
+ * @param {number} [options.interClickMin] Min ms between clicks
651
+ * @param {number} [options.interClickMax] Max ms between clicks
652
+ * @param {boolean} [options.forceDebug] Log click coordinates
653
+ */
654
+ async function performContentClicks(page, options = {}) {
655
+ const {
656
+ clicks = CONTENT_CLICK.CLICK_COUNT,
657
+ preDelay = CONTENT_CLICK.PRE_CLICK_DELAY,
658
+ interClickMin = CONTENT_CLICK.INTER_CLICK_MIN,
659
+ interClickMax = CONTENT_CLICK.INTER_CLICK_MAX,
660
+ forceDebug = false
661
+ } = options;
662
+
663
+ try {
664
+ if (page.isClosed()) return;
665
+
666
+ const viewport = await getCachedViewport(page);
667
+ const inset = CONTENT_CLICK.VIEWPORT_INSET;
668
+ const minX = Math.floor(viewport.width * inset);
669
+ const maxX = Math.floor(viewport.width * (1 - inset));
670
+ const minY = Math.floor(viewport.height * inset);
671
+ const maxY = Math.floor(viewport.height * (1 - inset));
672
+
673
+ // Wait for ad scripts to register their listeners
674
+ await fastTimeout(preDelay);
675
+
676
+ let lastX = minX + Math.floor(Math.random() * (maxX - minX));
677
+ let lastY = minY + Math.floor(Math.random() * (maxY - minY));
678
+
679
+ for (let i = 0; i < clicks; i++) {
680
+ if (page.isClosed()) break;
681
+
682
+ // Random position in content zone
683
+ const targetX = minX + Math.floor(Math.random() * (maxX - minX));
684
+ const targetY = minY + Math.floor(Math.random() * (maxY - minY));
685
+
686
+ // Natural mouse approach (few steps, no need for elaborate curves)
687
+ await humanLikeMouseMove(page, lastX, lastY, targetX, targetY, {
688
+ steps: CONTENT_CLICK.MOUSE_APPROACH_STEPS,
689
+ curve: 0.03 + Math.random() * 0.04,
690
+ jitter: 1
691
+ });
692
+
693
+ // Brief human-like pause, then click
694
+ await fastTimeout(TIMING.CLICK_PAUSE_MIN + Math.random() * (TIMING.CLICK_PAUSE_MAX - TIMING.CLICK_PAUSE_MIN));
695
+ await page.mouse.click(targetX, targetY);
696
+
697
+ if (forceDebug) {
698
+ console.log(`[interaction] Content click ${i + 1}/${clicks} at (${targetX}, ${targetY})`);
699
+ }
700
+
701
+ lastX = targetX;
702
+ lastY = targetY;
703
+
704
+ // Inter-click gap (skip after last click)
705
+ if (i < clicks - 1) {
706
+ await fastTimeout(interClickMin + Math.random() * (interClickMax - interClickMin));
707
+ }
708
+ }
709
+ } catch (err) {
710
+ // Content clicks are supplementary — never break the scan
711
+ }
712
+ }
713
+
609
714
  /**
610
715
  * Simulates realistic typing behavior with human characteristics
611
716
  *
@@ -876,12 +981,22 @@ async function performPageInteraction(page, currentUrl, options = {}, forceDebug
876
981
  }
877
982
  }
878
983
 
879
- // Element interaction
984
+ // Click interaction — two strategies for maximum ad script coverage
985
+ // 1. Content area clicks: triggers document-level onclick handlers
986
+ // (Monetag, similar popunder SDKs that listen on document)
987
+ // 2. Element clicks: interacts with specific UI elements
988
+ // (ad scripts that attach to specific clickable elements)
880
989
  if (includeElementClicks) {
881
- await interactWithElements(page, {
882
- maxAttempts: 2,
883
- avoidDestructive: true
884
- });
990
+ if (checkTimeout()) return; // Emergency timeout check
991
+ // Primary: content area clicks for document-level onclick handlers
992
+ await performContentClicks(page, { forceDebug });
993
+ // Secondary: targeted element clicks (fast, 1 attempt only)
994
+ if (!checkTimeout()) {
995
+ await interactWithElements(page, {
996
+ maxAttempts: 1,
997
+ avoidDestructive: true
998
+ });
999
+ }
885
1000
  }
886
1001
 
887
1002
  // Periodic memory cleanup during interaction
@@ -1105,6 +1220,7 @@ module.exports = {
1105
1220
  humanLikeMouseMove,
1106
1221
  simulateScrolling,
1107
1222
  interactWithElements,
1223
+ performContentClicks,
1108
1224
  simulateTyping,
1109
1225
  generateRandomCoordinates
1110
1226
  };
package/nwss.js CHANGED
@@ -20,7 +20,8 @@ const {
20
20
  handleCloudflareProtection,
21
21
  getCacheStats,
22
22
  clearDetectionCache,
23
- parallelChallengeDetection
23
+ parallelChallengeDetection,
24
+ cleanup: cleanupCloudflareCache
24
25
  } = require('./lib/cloudflare');
25
26
  // FP Bypass
26
27
  const { handleFlowProxyProtection, getFlowProxyTimeouts } = require('./lib/flowproxy');
@@ -39,7 +40,7 @@ const { processResults } = require('./lib/post-processing');
39
40
  // Colorize various text when used
40
41
  const { colorize, colors, messageColors, tags, formatLogMessage } = require('./lib/colorize');
41
42
  // Enhanced mouse interaction and page simulation
42
- const { performPageInteraction, createInteractionConfig } = require('./lib/interaction');
43
+ const { performPageInteraction, createInteractionConfig, performContentClicks, humanLikeMouseMove } = require('./lib/interaction');
43
44
  // Domain detection cache for performance optimization
44
45
  const { createGlobalHelpers, getTotalDomainsSkipped, getDetectedDomainsCount } = require('./lib/domain-cache');
45
46
  const { createSmartCache } = require('./lib/smart-cache'); // Smart cache system
@@ -84,8 +85,8 @@ const TIMEOUTS = Object.freeze({
84
85
  });
85
86
 
86
87
  const CACHE_LIMITS = Object.freeze({
87
- DISK_CACHE_SIZE: 52428800, // 50MB
88
- MEDIA_CACHE_SIZE: 52428800, // 50MB
88
+ DISK_CACHE_SIZE: 1, // Effectively disabled — forcereload clears cache between loads
89
+ MEDIA_CACHE_SIZE: 1, // Effectively disabled — no media caching needed for scanning
89
90
  DEFAULT_CACHE_PATH: '.cache',
90
91
  DEFAULT_MAX_SIZE: 5000
91
92
  });
@@ -1020,11 +1021,28 @@ function flushLogBuffers() {
1020
1021
  for (const [filePath, entries] of _logBuffers) {
1021
1022
  if (entries.length > 0) {
1022
1023
  try {
1023
- fs.appendFileSync(filePath, entries.join(''));
1024
+ const data = entries.join('');
1025
+ entries.length = 0; // Clear buffer immediately
1026
+ fs.writeFile(filePath, data, { flag: 'a' }, (err) => {
1027
+ if (err) {
1028
+ console.warn(formatLogMessage('warn', `Failed to flush log buffer to ${filePath}: ${err.message}`));
1029
+ }
1030
+ });
1024
1031
  } catch (err) {
1025
1032
  console.warn(formatLogMessage('warn', `Failed to flush log buffer to ${filePath}: ${err.message}`));
1026
1033
  }
1027
- entries.length = 0; // Clear buffer
1034
+ }
1035
+ }
1036
+ }
1037
+
1038
+ // Synchronous flush for exit handlers — guarantees data is written before process exits
1039
+ function flushLogBuffersSync() {
1040
+ for (const [filePath, entries] of _logBuffers) {
1041
+ if (entries.length > 0) {
1042
+ try {
1043
+ fs.appendFileSync(filePath, entries.join(''));
1044
+ } catch (err) { /* best effort on exit */ }
1045
+ entries.length = 0;
1028
1046
  }
1029
1047
  }
1030
1048
  }
@@ -1426,12 +1444,19 @@ function setupFrameHandling(page, forceDebug) {
1426
1444
  '--use-mock-keychain',
1427
1445
  '--disable-client-side-phishing-detection',
1428
1446
  '--enable-features=NetworkService',
1429
- // Disk space controls - 50MB cache limits
1430
- `--disk-cache-size=${CACHE_LIMITS.DISK_CACHE_SIZE}`, // 50MB disk cache
1431
- `--media-cache-size=${CACHE_LIMITS.MEDIA_CACHE_SIZE}`, // 50MB media cache
1447
+ // Disk space controls - minimal cache for scanning workloads
1448
+ `--disk-cache-size=${CACHE_LIMITS.DISK_CACHE_SIZE}`,
1449
+ `--media-cache-size=${CACHE_LIMITS.MEDIA_CACHE_SIZE}`,
1432
1450
  '--disable-application-cache',
1433
1451
  '--disable-offline-load-stale-cache',
1434
1452
  '--disable-background-downloads',
1453
+ // DISK I/O REDUCTION: Eliminate unnecessary Chrome disk writes
1454
+ '--disable-breakpad', // No crash dump files
1455
+ '--disable-component-update', // No component update downloads
1456
+ '--disable-logging', // No Chrome internal log files
1457
+ '--log-level=3', // Fatal errors only (suppresses verbose disk logging)
1458
+ '--no-service-autorun', // No background service disk activity
1459
+ '--disable-domain-reliability', // No reliability monitor disk writes
1435
1460
  // PERFORMANCE: Enhanced Puppeteer 23.x optimizations
1436
1461
  '--disable-features=AudioServiceOutOfProcess,VizDisplayCompositor',
1437
1462
  '--disable-features=TranslateUI,BlinkGenPropertyTrees,Translate',
@@ -1447,7 +1472,6 @@ function setupFrameHandling(page, forceDebug) {
1447
1472
  '--disable-features=SafeBrowsing',
1448
1473
  '--disable-dev-shm-usage',
1449
1474
  '--disable-sync',
1450
- '--disable-gpu', // WebGL null-context handled by fingerprint.js Proxy mock
1451
1475
  '--mute-audio',
1452
1476
  '--disable-translate',
1453
1477
  '--window-size=1920,1080',
@@ -1467,6 +1491,12 @@ function setupFrameHandling(page, forceDebug) {
1467
1491
  '--disable-background-timer-throttling',
1468
1492
  '--disable-features=site-per-process', // Better for single-site scanning
1469
1493
  '--no-zygote', // Better process isolation
1494
+ // PERFORMANCE: Process and memory reduction for high concurrency
1495
+ '--renderer-process-limit=10', // Cap renderer processes (default: unlimited)
1496
+ '--disable-accelerated-2d-canvas', // Software canvas only (we spoof it anyway)
1497
+ '--disable-hang-monitor', // Remove per-renderer hang check overhead
1498
+ '--disable-features=PaintHolding', // Don't hold frames in renderer memory
1499
+ '--js-flags=--max-old-space-size=512', // Cap V8 heap per renderer to 512MB
1470
1500
  ...extraArgs,
1471
1501
  ],
1472
1502
  // Optimized timeouts for Puppeteer 23.x performance
@@ -1518,7 +1548,7 @@ function setupFrameHandling(page, forceDebug) {
1518
1548
  // Set up cleanup on process termination
1519
1549
  process.on('SIGINT', async () => {
1520
1550
  if (forceDebug) console.log(formatLogMessage('debug', 'SIGINT received, performing cleanup...'));
1521
- flushLogBuffers();
1551
+ flushLogBuffersSync();
1522
1552
  if (_logFlushTimer) clearInterval(_logFlushTimer);
1523
1553
  await performEmergencyCleanup();
1524
1554
  process.exit(0);
@@ -1526,7 +1556,7 @@ function setupFrameHandling(page, forceDebug) {
1526
1556
 
1527
1557
  process.on('SIGTERM', async () => {
1528
1558
  if (forceDebug) console.log(formatLogMessage('debug', 'SIGTERM received, performing cleanup...'));
1529
- flushLogBuffers();
1559
+ flushLogBuffersSync();
1530
1560
  if (_logFlushTimer) clearInterval(_logFlushTimer);
1531
1561
  await performEmergencyCleanup();
1532
1562
  process.exit(0);
@@ -1557,6 +1587,7 @@ function setupFrameHandling(page, forceDebug) {
1557
1587
  }
1558
1588
  wgDisconnectAll(forceDebug);
1559
1589
  ovpnDisconnectAll(forceDebug);
1590
+ cleanupCloudflareCache();
1560
1591
  }
1561
1592
 
1562
1593
  let siteCounter = 0;
@@ -3220,7 +3251,9 @@ function setupFrameHandling(page, forceDebug) {
3220
3251
  siteCounter++;
3221
3252
 
3222
3253
  // Enhanced Cloudflare handling with parallel detection
3223
- if (siteConfig.cloudflare_parallel_detection !== false) { // Enable by default
3254
+ // Only run parallel detection if cloudflare handling is explicitly configured
3255
+ const hasCloudflareConfig = siteConfig.cloudflare_bypass || siteConfig.cloudflare_phish;
3256
+ if (hasCloudflareConfig && siteConfig.cloudflare_parallel_detection !== false) {
3224
3257
  try {
3225
3258
  const parallelResult = await parallelChallengeDetection(page, forceDebug);
3226
3259
  if (parallelResult.hasAnyChallenge && forceDebug) {
@@ -3660,6 +3693,32 @@ function setupFrameHandling(page, forceDebug) {
3660
3693
  }
3661
3694
  }
3662
3695
 
3696
+ // Post-reload interaction: trigger onclick ad scripts (Monetag etc.)
3697
+ // Each reload gives a fresh session with a new random ad domain —
3698
+ // without clicks the SDK never fires and we miss those domains.
3699
+ if (interactEnabled && !page.isClosed()) {
3700
+ try {
3701
+ // Brief wait for ad scripts to re-register after reload
3702
+ await fastTimeout(800);
3703
+ // Quick mouse moves to build movement score (Monetag tracks this)
3704
+ const vp = page.viewport() || { width: 1920, height: 1080 };
3705
+ const startX = 200 + Math.floor(Math.random() * (vp.width - 400));
3706
+ const startY = 200 + Math.floor(Math.random() * (vp.height - 400));
3707
+ await page.mouse.move(startX, startY);
3708
+ for (let m = 0; m < 2; m++) {
3709
+ const endX = 200 + Math.floor(Math.random() * (vp.width - 400));
3710
+ const endY = 200 + Math.floor(Math.random() * (vp.height - 400));
3711
+ await humanLikeMouseMove(page, startX, startY, endX, endY, { steps: 3, curve: 0.04, jitter: 1 });
3712
+ }
3713
+ // Content clicks to trigger document-level onclick handlers
3714
+ await performContentClicks(page, { clicks: 2, preDelay: 200, forceDebug });
3715
+ if (forceDebug) console.log(formatLogMessage('debug', `Post-reload interaction completed for reload #${i}`));
3716
+ } catch (postReloadInteractErr) {
3717
+ // Non-critical — continue with remaining reloads
3718
+ if (forceDebug) console.log(formatLogMessage('debug', `Post-reload interaction failed: ${postReloadInteractErr.message}`));
3719
+ }
3720
+ }
3721
+
3663
3722
  // Only add delay if we're continuing with more reloads
3664
3723
  if (i < totalReloads) {
3665
3724
  // Reduce delay for problematic sites
@@ -4404,7 +4463,7 @@ function setupFrameHandling(page, forceDebug) {
4404
4463
  }
4405
4464
 
4406
4465
  // Flush any remaining buffered log entries before compression/exit
4407
- flushLogBuffers();
4466
+ flushLogBuffersSync();
4408
4467
  if (_logFlushTimer) {
4409
4468
  clearInterval(_logFlushTimer);
4410
4469
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fanboynz/network-scanner",
3
- "version": "2.0.54",
3
+ "version": "2.0.56",
4
4
  "description": "A Puppeteer-based network scanner for analyzing web traffic, generating adblock filter rules, and identifying third-party requests. Features include fingerprint spoofing, Cloudflare bypass, content analysis with curl/grep, and multiple output formats.",
5
5
  "main": "nwss.js",
6
6
  "scripts": {