@fanboynz/network-scanner 2.0.57 → 2.0.59

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/lib/compress.js CHANGED
@@ -24,9 +24,7 @@ async function compressFile(filePath, removeOriginal = true) {
24
24
  const handleError = (error) => {
25
25
  // Clean up partial compressed file on error
26
26
  try {
27
- if (fs.existsSync(compressedPath)) {
28
- fs.unlinkSync(compressedPath);
29
- }
27
+ fs.unlinkSync(compressedPath);
30
28
  } catch (cleanupErr) {
31
29
  // Ignore cleanup errors
32
30
  }
@@ -69,18 +67,11 @@ async function compressMultipleFiles(filePaths, removeOriginals = true) {
69
67
 
70
68
  for (const filePath of filePaths) {
71
69
  try {
72
- if (fs.existsSync(filePath)) {
73
- const compressedPath = await compressFile(filePath, removeOriginals);
74
- results.successful.push({
75
- original: filePath,
76
- compressed: compressedPath
77
- });
78
- } else {
79
- results.failed.push({
80
- path: filePath,
81
- error: 'File does not exist'
82
- });
83
- }
70
+ const compressedPath = await compressFile(filePath, removeOriginals);
71
+ results.successful.push({
72
+ original: filePath,
73
+ compressed: compressedPath
74
+ });
84
75
  } catch (error) {
85
76
  results.failed.push({
86
77
  path: filePath,
package/lib/dry-run.js CHANGED
@@ -436,7 +436,7 @@ function writeDryRunOutput(outputFile, dryRunOutput, silentMode = false) {
436
436
  // Ensure output directory exists
437
437
  const path = require('path');
438
438
  const outputDir = path.dirname(outputFile);
439
- if (outputDir !== '.' && !fs.existsSync(outputDir)) {
439
+ if (outputDir !== '.') {
440
440
  fs.mkdirSync(outputDir, { recursive: true });
441
441
  }
442
442
 
@@ -41,8 +41,6 @@ const USER_AGENT_COLLECTIONS = Object.freeze(new Map([
41
41
  ['safari', "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.6 Safari/605.1.15"]
42
42
  ]));
43
43
 
44
- // Timezone configuration with offsets
45
-
46
44
  // GPU pool — realistic vendor/renderer combos per OS (used for WebGL spoofing)
47
45
  const GPU_POOL = {
48
46
  windows: [
@@ -86,12 +84,20 @@ function selectGpuForUserAgent(userAgentString) {
86
84
  const pool = GPU_POOL[osKey];
87
85
  return pool[Math.floor(Math.random() * pool.length)];
88
86
  }
89
- const TIMEZONE_CONFIG = {
90
- 'America/New_York': { offset: 300, abbr: 'EST' },
91
- 'America/Los_Angeles': { offset: 480, abbr: 'PST' },
92
- 'Europe/London': { offset: 0, abbr: 'GMT' },
93
- 'America/Chicago': { offset: 360, abbr: 'CST' }
94
- };
87
+
88
+ /**
89
+ * Checks if an error is a session/protocol closed error (common during page navigation)
90
+ */
91
+ function isSessionClosedError(err) {
92
+ const msg = err.message;
93
+ return msg.includes('Session closed') ||
94
+ msg.includes('addScriptToEvaluateOnNewDocument timed out') ||
95
+ msg.includes('Target closed') ||
96
+ msg.includes('Protocol error') || err.name === 'ProtocolError' ||
97
+ msg.includes('detached Frame') || msg.includes('Navigating frame was detached') ||
98
+ msg.includes('Cannot find context') ||
99
+ msg.includes('Execution context was destroyed');
100
+ }
95
101
 
96
102
  /**
97
103
  * Safely defines a property with comprehensive error handling
@@ -406,11 +412,6 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
406
412
  }
407
413
  }
408
414
 
409
- // Add cached descriptors helper for page context
410
- const CACHED_DESCRIPTORS = {
411
- getter: (fn) => ({ get: fn, enumerable: true, configurable: true })
412
- };
413
-
414
415
  // Add monomorphic spoofing functions for page context
415
416
  function spoofNavigatorProperties(navigator, properties) {
416
417
  for (const [prop, descriptor] of Object.entries(properties)) {
@@ -1689,15 +1690,31 @@ async function applyUserAgentSpoofing(page, siteConfig, forceDebug, currentUrl)
1689
1690
  if (debugEnabled) console.log('[fingerprint] toString protection applied to all spoofed functions');
1690
1691
  }, 'Function.prototype.toString bulk masking');
1691
1692
 
1693
+ // Trigger interaction-gated scripts (GTM, Monetag etc.) on page load
1694
+ safeExecute(() => {
1695
+ function triggerInteraction() {
1696
+ setTimeout(() => {
1697
+ const x = Math.floor(Math.random() * 800) + 100;
1698
+ const y = Math.floor(Math.random() * 400) + 100;
1699
+ window.dispatchEvent(new MouseEvent('mousemove', {
1700
+ clientX: x, clientY: y, pageX: x, pageY: y, bubbles: true, cancelable: true, view: window
1701
+ }));
1702
+ window.dispatchEvent(new Event('scroll', { bubbles: true }));
1703
+ document.dispatchEvent(new KeyboardEvent('keydown', {
1704
+ key: 'Tab', code: 'Tab', bubbles: true
1705
+ }));
1706
+ }, 50);
1707
+ }
1708
+ if (document.readyState === 'loading') {
1709
+ document.addEventListener('DOMContentLoaded', triggerInteraction, { once: true });
1710
+ } else {
1711
+ triggerInteraction();
1712
+ }
1713
+ }, 'interaction-gated script trigger');
1714
+
1692
1715
  }, ua, forceDebug, selectedGpu);
1693
1716
  } catch (stealthErr) {
1694
- if (stealthErr.message.includes('Session closed') ||
1695
- stealthErr.message.includes('addScriptToEvaluateOnNewDocument timed out') ||
1696
- stealthErr.message.includes('Target closed') ||
1697
- stealthErr.message.includes('Protocol error') || stealthErr.name === 'ProtocolError' ||
1698
- stealthErr.message.includes('detached Frame') || stealthErr.message.includes('Navigating frame was detached') ||
1699
- stealthErr.message.includes('Cannot find context') ||
1700
- stealthErr.message.includes('Execution context was destroyed')) {
1717
+ if (isSessionClosedError(stealthErr)) {
1701
1718
  if (forceDebug) console.log(`[debug] Page closed during stealth injection: ${currentUrl}`);
1702
1719
  return;
1703
1720
  }
@@ -1749,13 +1766,7 @@ async function applyBraveSpoofing(page, siteConfig, forceDebug, currentUrl) {
1749
1766
  }
1750
1767
  }, forceDebug);
1751
1768
  } catch (braveErr) {
1752
- if (braveErr.message.includes('Session closed') ||
1753
- braveErr.message.includes('addScriptToEvaluateOnNewDocument timed out') ||
1754
- braveErr.message.includes('Target closed') ||
1755
- braveErr.message.includes('Protocol error') || braveErr.name === 'ProtocolError' ||
1756
- braveErr.message.includes('detached Frame') || braveErr.message.includes('Navigating frame was detached') ||
1757
- braveErr.message.includes('Cannot find context') ||
1758
- braveErr.message.includes('Execution context was destroyed')) {
1769
+ if (isSessionClosedError(braveErr)) {
1759
1770
  if (forceDebug) console.log(`[debug] Page closed during Brave injection: ${currentUrl}`);
1760
1771
  return;
1761
1772
  }
@@ -1918,13 +1929,7 @@ async function applyFingerprintProtection(page, siteConfig, forceDebug, currentU
1918
1929
 
1919
1930
  }, { spoof, debugEnabled: forceDebug });
1920
1931
  } catch (err) {
1921
- if (err.message.includes('Session closed') ||
1922
- err.message.includes('addScriptToEvaluateOnNewDocument timed out') ||
1923
- err.message.includes('Target closed') ||
1924
- err.message.includes('Protocol error') || err.name === 'ProtocolError' ||
1925
- err.message.includes('detached Frame') || err.message.includes('Navigating frame was detached') ||
1926
- err.message.includes('Cannot find context') ||
1927
- err.message.includes('Execution context was destroyed')) {
1932
+ if (isSessionClosedError(err)) {
1928
1933
  if (forceDebug) console.log(`[debug] Page closed during fingerprint injection: ${currentUrl}`);
1929
1934
  return;
1930
1935
  }
@@ -2008,6 +2013,10 @@ async function simulateHumanBehavior(page, forceDebug) {
2008
2013
  document.dispatchEvent(new MouseEvent('mousemove', {
2009
2014
  clientX: mouseX,
2010
2015
  clientY: mouseY,
2016
+ pageX: mouseX + (window.scrollX || 0),
2017
+ pageY: mouseY + (window.scrollY || 0),
2018
+ screenX: mouseX + (window.screenX || 0),
2019
+ screenY: mouseY + (window.screenY || 0),
2011
2020
  bubbles: true,
2012
2021
  cancelable: true,
2013
2022
  view: window,
@@ -2021,6 +2030,10 @@ async function simulateHumanBehavior(page, forceDebug) {
2021
2030
  document.dispatchEvent(new MouseEvent('click', {
2022
2031
  clientX: mouseX,
2023
2032
  clientY: mouseY,
2033
+ pageX: mouseX + (window.scrollX || 0),
2034
+ pageY: mouseY + (window.scrollY || 0),
2035
+ screenX: mouseX + (window.screenX || 0),
2036
+ screenY: mouseY + (window.screenY || 0),
2024
2037
  bubbles: true,
2025
2038
  cancelable: true,
2026
2039
  view: window
@@ -2083,9 +2096,6 @@ async function applyAllFingerprintSpoofing(page, siteConfig, forceDebug, current
2083
2096
  }
2084
2097
  }
2085
2098
 
2086
- // Legacy compatibility function - maintained for backwards compatibility
2087
-
2088
-
2089
2099
  module.exports = {
2090
2100
  generateRealisticFingerprint,
2091
2101
  getRealisticScreenResolution,
package/lib/flowproxy.js CHANGED
@@ -41,6 +41,13 @@ const FAST_TIMEOUTS = {
41
41
  * Gets module version information
42
42
  * @returns {object} Version information object
43
43
  */
44
+ // Protocols to skip — FlowProxy only protects web traffic
45
+ const SKIP_PATTERNS = [
46
+ 'about:', 'chrome:', 'chrome-extension:', 'chrome-error:', 'chrome-search:',
47
+ 'devtools:', 'edge:', 'moz-extension:', 'safari-extension:', 'webkit:',
48
+ 'data:', 'blob:', 'javascript:', 'vbscript:', 'file:', 'ftp:', 'ftps:'
49
+ ];
50
+
44
51
  function getModuleInfo() {
45
52
  return {
46
53
  version: FLOWPROXY_MODULE_VERSION,
@@ -73,15 +80,8 @@ function shouldProcessUrl(url, forceDebug = false) {
73
80
  }
74
81
 
75
82
  // Skip browser-internal and special protocol URLs
76
- // These protocols are not relevant for FlowProxy protection
77
- const skipPatterns = [
78
- 'about:', 'chrome:', 'chrome-extension:', 'chrome-error:', 'chrome-search:',
79
- 'devtools:', 'edge:', 'moz-extension:', 'safari-extension:', 'webkit:',
80
- 'data:', 'blob:', 'javascript:', 'vbscript:', 'file:', 'ftp:', 'ftps:'
81
- ];
82
-
83
83
  const urlLower = url.toLowerCase();
84
- for (const pattern of skipPatterns) {
84
+ for (const pattern of SKIP_PATTERNS) {
85
85
  if (urlLower.startsWith(pattern)) {
86
86
  if (forceDebug) {
87
87
  console.log(`[flowproxy][url-validation] Skipping ${pattern} URL: ${url.substring(0, 100)}${url.length > 100 ? '...' : ''}`);
@@ -0,0 +1,258 @@
1
+ // === Ghost Cursor Module ===
2
+ // Optional wrapper around the ghost-cursor npm package for advanced Bezier-based mouse movements.
3
+ // Falls back gracefully to built-in interaction.js mouse if ghost-cursor is not installed.
4
+ //
5
+ // USAGE (JSON config):
6
+ // "cursor_mode": "ghost" Enable ghost-cursor for this site
7
+ // "ghost_cursor_speed": 1.5 Movement speed multiplier (default: 1.0)
8
+ // "ghost_cursor_hesitate": 100 Delay (ms) before clicking (default: 50)
9
+ // "ghost_cursor_overshoot": 500 Max overshoot distance in px (default: auto)
10
+ //
11
+ // USAGE (CLI):
12
+ // --ghost-cursor Enable ghost-cursor globally
13
+ //
14
+ // INSTALL:
15
+ // npm install ghost-cursor (optional dependency)
16
+
17
+ const { formatLogMessage } = require('./colorize');
18
+
19
+ let ghostCursorModule = null;
20
+ let ghostCursorAvailable = false;
21
+
22
+ // Attempt to load ghost-cursor at module init — optional dependency
23
+ try {
24
+ ghostCursorModule = require('ghost-cursor');
25
+ ghostCursorAvailable = true;
26
+ } catch {
27
+ // ghost-cursor not installed — this is fine, built-in mouse will be used
28
+ }
29
+
30
+ /**
31
+ * Check if ghost-cursor is available
32
+ * @returns {boolean}
33
+ */
34
+ function isGhostCursorAvailable() {
35
+ return ghostCursorAvailable;
36
+ }
37
+
38
+ /**
39
+ * Create a ghost-cursor instance bound to a Puppeteer page
40
+ * @param {import('puppeteer').Page} page - Puppeteer page instance
41
+ * @param {object} options - Configuration options
42
+ * @param {boolean} options.forceDebug - Enable debug logging
43
+ * @param {number} options.startX - Starting X coordinate (default: 0)
44
+ * @param {number} options.startY - Starting Y coordinate (default: 0)
45
+ * @returns {object|null} Ghost cursor instance or null if unavailable
46
+ */
47
+ function createGhostCursor(page, options = {}) {
48
+ if (!ghostCursorAvailable) {
49
+ return null;
50
+ }
51
+
52
+ const { forceDebug, startX = 0, startY = 0 } = options;
53
+
54
+ try {
55
+ const cursor = ghostCursorModule.createCursor(page, { x: startX, y: startY });
56
+
57
+ if (forceDebug) {
58
+ console.log(formatLogMessage('debug', '[ghost-cursor] Cursor instance created'));
59
+ }
60
+
61
+ return cursor;
62
+ } catch (err) {
63
+ if (forceDebug) {
64
+ console.log(formatLogMessage('debug', `[ghost-cursor] Failed to create cursor: ${err.message}`));
65
+ }
66
+ return null;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Move cursor to coordinates using ghost-cursor Bezier paths
72
+ * Drop-in replacement for humanLikeMouseMove when ghost-cursor is active
73
+ *
74
+ * @param {object} cursor - Ghost cursor instance from createGhostCursor()
75
+ * @param {number} toX - Target X coordinate
76
+ * @param {number} toY - Target Y coordinate
77
+ * @param {object} options - Movement options
78
+ * @param {number} options.moveSpeed - Speed multiplier (default: auto/random)
79
+ * @param {number} options.moveDelay - Delay after movement in ms (default: 0)
80
+ * @param {boolean} options.randomizeMoveDelay - Randomize move delay (default: true)
81
+ * @param {number} options.overshootThreshold - Max overshoot distance in px
82
+ * @param {boolean} options.forceDebug - Enable debug logging
83
+ * @returns {Promise<boolean>} true if movement succeeded
84
+ */
85
+ async function ghostMove(cursor, toX, toY, options = {}) {
86
+ if (!cursor) return false;
87
+
88
+ const {
89
+ moveSpeed,
90
+ moveDelay = 0,
91
+ randomizeMoveDelay = true,
92
+ overshootThreshold,
93
+ forceDebug
94
+ } = options;
95
+
96
+ try {
97
+ const moveOpts = {};
98
+ if (moveSpeed !== undefined) moveOpts.moveSpeed = moveSpeed;
99
+ if (moveDelay > 0) moveOpts.moveDelay = moveDelay;
100
+ if (randomizeMoveDelay !== undefined) moveOpts.randomizeMoveDelay = randomizeMoveDelay;
101
+ if (overshootThreshold !== undefined) moveOpts.overshootThreshold = overshootThreshold;
102
+
103
+ await cursor.moveTo({ x: toX, y: toY }, moveOpts);
104
+
105
+ if (forceDebug) {
106
+ console.log(formatLogMessage('debug', `[ghost-cursor] Moved to (${Math.round(toX)}, ${Math.round(toY)})`));
107
+ }
108
+
109
+ return true;
110
+ } catch (err) {
111
+ if (forceDebug) {
112
+ console.log(formatLogMessage('debug', `[ghost-cursor] Move failed: ${err.message}`));
113
+ }
114
+ return false;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Click on a CSS selector or coordinates using ghost-cursor
120
+ *
121
+ * @param {object} cursor - Ghost cursor instance
122
+ * @param {string|{x: number, y: number}} target - CSS selector or {x, y} coordinates
123
+ * @param {object} options - Click options
124
+ * @param {number} options.hesitate - Delay (ms) before clicking (default: 50)
125
+ * @param {number} options.waitForClick - Delay (ms) between mousedown/mouseup (default: auto)
126
+ * @param {number} options.moveDelay - Delay (ms) after moving to target
127
+ * @param {number} options.paddingPercentage - Click point within element (0=edge, 100=center)
128
+ * @param {boolean} options.forceDebug - Enable debug logging
129
+ * @returns {Promise<boolean>} true if click succeeded
130
+ */
131
+ async function ghostClick(cursor, target, options = {}) {
132
+ if (!cursor) return false;
133
+
134
+ const {
135
+ hesitate = 50,
136
+ waitForClick,
137
+ moveDelay,
138
+ paddingPercentage,
139
+ forceDebug
140
+ } = options;
141
+
142
+ try {
143
+ const clickOpts = { hesitate };
144
+ if (waitForClick !== undefined) clickOpts.waitForClick = waitForClick;
145
+ if (moveDelay !== undefined) clickOpts.moveDelay = moveDelay;
146
+ if (paddingPercentage !== undefined) clickOpts.paddingPercentage = paddingPercentage;
147
+
148
+ if (typeof target === 'string') {
149
+ await cursor.click(target, clickOpts);
150
+ } else {
151
+ // For coordinate clicks, move first then use page click
152
+ await cursor.moveTo(target);
153
+ // Small hesitation before clicking
154
+ if (hesitate > 0) {
155
+ await new Promise(resolve => setTimeout(resolve, hesitate));
156
+ }
157
+ const page = cursor._page || cursor.page;
158
+ if (page && typeof page.mouse?.click === 'function') {
159
+ await page.mouse.click(target.x, target.y);
160
+ }
161
+ }
162
+
163
+ if (forceDebug) {
164
+ const label = typeof target === 'string' ? target : `(${Math.round(target.x)}, ${Math.round(target.y)})`;
165
+ console.log(formatLogMessage('debug', `[ghost-cursor] Clicked ${label}`));
166
+ }
167
+
168
+ return true;
169
+ } catch (err) {
170
+ if (forceDebug) {
171
+ console.log(formatLogMessage('debug', `[ghost-cursor] Click failed: ${err.message}`));
172
+ }
173
+ return false;
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Perform a random idle mouse movement using ghost-cursor
179
+ *
180
+ * @param {object} cursor - Ghost cursor instance
181
+ * @param {object} options - Options
182
+ * @param {boolean} options.forceDebug - Enable debug logging
183
+ * @returns {Promise<boolean>} true if movement succeeded
184
+ */
185
+ async function ghostRandomMove(cursor, options = {}) {
186
+ if (!cursor) return false;
187
+
188
+ try {
189
+ await cursor.randomMove();
190
+ if (options.forceDebug) {
191
+ console.log(formatLogMessage('debug', '[ghost-cursor] Random movement performed'));
192
+ }
193
+ return true;
194
+ } catch (err) {
195
+ if (options.forceDebug) {
196
+ console.log(formatLogMessage('debug', `[ghost-cursor] Random move failed: ${err.message}`));
197
+ }
198
+ return false;
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Generate a Bezier path between two points (standalone, no browser needed)
204
+ *
205
+ * @param {{x: number, y: number}} from - Start point
206
+ * @param {{x: number, y: number}} to - End point
207
+ * @param {object} options - Path options
208
+ * @param {number} options.spreadOverride - Override curve spread
209
+ * @param {number} options.moveSpeed - Movement speed
210
+ * @returns {Array<{x: number, y: number}>|null} Array of path points or null
211
+ */
212
+ function ghostPath(from, to, options = {}) {
213
+ if (!ghostCursorAvailable || !ghostCursorModule.path) return null;
214
+
215
+ try {
216
+ return ghostCursorModule.path(from, to, options);
217
+ } catch {
218
+ return null;
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Resolve ghost-cursor settings from site config and CLI flags.
224
+ * Returns null if ghost-cursor should not be used.
225
+ *
226
+ * @param {object} siteConfig - Per-site JSON configuration
227
+ * @param {boolean} globalGhostCursor - CLI --ghost-cursor flag
228
+ * @param {boolean} forceDebug - Debug logging flag
229
+ * @returns {object|null} Resolved ghost-cursor options or null
230
+ */
231
+ function resolveGhostCursorConfig(siteConfig, globalGhostCursor, forceDebug) {
232
+ const enabled = globalGhostCursor || siteConfig.cursor_mode === 'ghost';
233
+
234
+ if (!enabled) return null;
235
+
236
+ if (!ghostCursorAvailable) {
237
+ console.warn(formatLogMessage('warn', '[ghost-cursor] cursor_mode "ghost" requested but ghost-cursor package is not installed. Run: npm install ghost-cursor'));
238
+ return null;
239
+ }
240
+
241
+ return {
242
+ moveSpeed: siteConfig.ghost_cursor_speed || undefined,
243
+ hesitate: siteConfig.ghost_cursor_hesitate ?? 50,
244
+ overshootThreshold: siteConfig.ghost_cursor_overshoot || undefined,
245
+ duration: siteConfig.ghost_cursor_duration || siteConfig.interact_duration || 2000,
246
+ forceDebug
247
+ };
248
+ }
249
+
250
+ module.exports = {
251
+ isGhostCursorAvailable,
252
+ createGhostCursor,
253
+ ghostMove,
254
+ ghostClick,
255
+ ghostRandomMove,
256
+ ghostPath,
257
+ resolveGhostCursorConfig
258
+ };
package/lib/grep.js CHANGED
@@ -28,7 +28,7 @@ const GREP_DEFAULTS = {
28
28
  * @param {object} options - Grep options
29
29
  * @returns {Promise<object>} Object with found boolean, matchedPattern, and allMatches array
30
30
  */
31
- async function grepContent(content, searchPatterns, options = {}) {
31
+ function grepContent(content, searchPatterns, options = {}) {
32
32
  const {
33
33
  ignoreCase = true,
34
34
  wholeWord = false,
@@ -374,11 +374,14 @@ async function humanLikeMouseMove(page, fromX, fromY, toX, toY, options = {}) {
374
374
  }
375
375
 
376
376
  for (let i = 0; i <= actualSteps; i++) {
377
+ // Bail out if page closed mid-movement
378
+ try { if (page.isClosed()) return; } catch { return; }
379
+
377
380
  const progress = i / actualSteps;
378
-
381
+
379
382
  // Apply easing curve for more natural movement
380
- const easedProgress = progress < 0.5
381
- ? 2 * progress * progress
383
+ const easedProgress = progress < 0.5
384
+ ? 2 * progress * progress
382
385
  : 1 - Math.pow(-2 * progress + 2, 2) / 2;
383
386
 
384
387
  // Calculate base position
@@ -390,7 +393,7 @@ async function humanLikeMouseMove(page, fromX, fromY, toX, toY, options = {}) {
390
393
  const curveIntensity = Math.sin((i / actualSteps) * Math.PI) * curve * distance * MOUSE_MOVEMENT.CURVE_INTENSITY_RATIO;
391
394
  const perpX = -(toY - fromY) / distance;
392
395
  const perpY = (toX - fromX) / distance;
393
-
396
+
394
397
  currentX += perpX * curveIntensity;
395
398
  currentY += perpY * curveIntensity;
396
399
  }
@@ -402,7 +405,7 @@ async function humanLikeMouseMove(page, fromX, fromY, toX, toY, options = {}) {
402
405
  }
403
406
 
404
407
  await page.mouse.move(currentX, currentY);
405
-
408
+
406
409
  // Variable delay between movements
407
410
  if (i < actualSteps) {
408
411
  const delay = Math.floor(Math.random() * (maxDelay - minDelay + 1)) + minDelay;
@@ -457,14 +460,16 @@ async function simulateScrolling(page, options = {}) {
457
460
 
458
461
  try {
459
462
  for (let i = 0; i < amount; i++) {
463
+ try { if (page.isClosed()) return; } catch { return; }
464
+
460
465
  const scrollDelta = direction === 'down' ? SCROLLING.SCROLL_DELTA : -SCROLLING.SCROLL_DELTA;
461
-
466
+
462
467
  // Smooth scrolling by breaking into smaller increments
463
468
  for (let j = 0; j < smoothness; j++) {
464
469
  await page.mouse.wheel({ deltaY: scrollDelta / smoothness });
465
470
  await fastTimeout(SCROLLING.SMOOTH_INCREMENT_DELAY);
466
471
  }
467
-
472
+
468
473
  if (i < amount - 1) {
469
474
  await fastTimeout(pauseBetween);
470
475
  }
@@ -547,6 +552,8 @@ async function interactWithElements(page, options = {}) {
547
552
 
548
553
  // Very short timeout since page should already be loaded
549
554
  await page.waitForSelector('body', { timeout: 1000 });
555
+ // Re-check after async wait — page may have closed during selector wait
556
+ if (page.isClosed()) return;
550
557
  } catch (bodyWaitErr) {
551
558
  if (options.forceDebug) {
552
559
  console.log(`[interaction] Page not ready for element interaction: ${bodyWaitErr.message}`);
@@ -677,7 +684,7 @@ async function performContentClicks(page, options = {}) {
677
684
  let lastY = minY + Math.floor(Math.random() * (maxY - minY));
678
685
 
679
686
  for (let i = 0; i < clicks; i++) {
680
- if (page.isClosed()) break;
687
+ try { if (page.isClosed()) break; } catch { break; }
681
688
 
682
689
  // Random position in content zone
683
690
  const targetX = minX + Math.floor(Math.random() * (maxX - minX));
@@ -813,11 +820,6 @@ function cleanupInteractionMemory(force = false) {
813
820
  cachedViewport = null;
814
821
  lastViewportCheck = 0;
815
822
  }
816
-
817
- // Force garbage collection if available (helps with memory pressure)
818
- if (global.gc) {
819
- global.gc();
820
- }
821
823
  }
822
824
 
823
825
  /**
@@ -915,7 +917,9 @@ async function performPageInteraction(page, currentUrl, options = {}, forceDebug
915
917
  const maxY = viewport.height;
916
918
 
917
919
  if (forceDebug) {
918
- console.log(`[interaction] Starting enhanced interaction simulation for ${new URL(currentUrl).hostname} (${intensity} intensity)`);
920
+ let hostname = currentUrl;
921
+ try { hostname = new URL(currentUrl).hostname; } catch {}
922
+ console.log(`[interaction] Starting enhanced interaction simulation for ${hostname} (${intensity} intensity)`);
919
923
  }
920
924
 
921
925
  // Configure intensity settings
@@ -1010,8 +1014,11 @@ async function performPageInteraction(page, currentUrl, options = {}, forceDebug
1010
1014
  try {
1011
1015
  const bodyElement = await page.$('body');
1012
1016
  if (bodyElement) {
1013
- await page.hover('body');
1014
- await bodyElement.dispose();
1017
+ try {
1018
+ await page.hover('body');
1019
+ } finally {
1020
+ await bodyElement.dispose();
1021
+ }
1015
1022
  }
1016
1023
  } catch (hoverErr) {
1017
1024
  // Silently handle hover failures - not critical
@@ -1037,35 +1044,6 @@ async function performPageInteraction(page, currentUrl, options = {}, forceDebug
1037
1044
  }
1038
1045
  }
1039
1046
 
1040
- /**
1041
- * Performs minimal interaction for very slow or problematic pages
1042
- * Only does basic mouse movement without body validation
1043
- */
1044
- async function performMinimalInteraction(page, currentUrl, options = {}, forceDebug = false) {
1045
- try {
1046
- if (page.isClosed()) return;
1047
-
1048
- const viewport = await getCachedViewport(page);
1049
- const maxX = viewport.width;
1050
- const maxY = viewport.height;
1051
-
1052
- if (forceDebug) {
1053
- console.log(`[interaction] Performing minimal interaction for slow page: ${new URL(currentUrl).hostname}`);
1054
- }
1055
-
1056
- // Just do basic mouse movement without body-dependent operations
1057
- const startPos = generateRandomCoordinates(maxX, maxY);
1058
- const endPos = generateRandomCoordinates(maxX, maxY);
1059
-
1060
- await page.mouse.move(startPos.x, startPos.y);
1061
- await fastTimeout(200);
1062
- await humanLikeMouseMove(page, startPos.x, startPos.y, endPos.x, endPos.y);
1063
-
1064
- } catch (minimalErr) {
1065
- // Even minimal interaction failed - page is truly broken
1066
- }
1067
- }
1068
-
1069
1047
  /**
1070
1048
  * Creates an optimized interaction configuration based on site characteristics
1071
1049
  *