@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/.github/workflows/npm-publish.yml +3 -3
- package/CHANGELOG.md +940 -0
- package/CLAUDE.md +65 -0
- package/README.md +31 -0
- package/lib/adblock.js +4 -3
- package/lib/browserexit.js +61 -96
- package/lib/browserhealth.js +16 -4
- package/lib/cdp.js +17 -169
- package/lib/compare.js +0 -4
- package/lib/compress.js +6 -15
- package/lib/dry-run.js +1 -1
- package/lib/fingerprint.js +47 -37
- package/lib/flowproxy.js +8 -8
- package/lib/ghost-cursor.js +258 -0
- package/lib/grep.js +1 -1
- package/lib/interaction.js +23 -45
- package/lib/openvpn_vpn.js +16 -21
- package/lib/output.js +12 -6
- package/lib/validate_rules.js +12 -27
- package/nwss.js +147 -52
- package/package.json +5 -1
- package/.clauderc +0 -30
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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 !== '.'
|
|
439
|
+
if (outputDir !== '.') {
|
|
440
440
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
441
441
|
}
|
|
442
442
|
|
package/lib/fingerprint.js
CHANGED
|
@@ -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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
31
|
+
function grepContent(content, searchPatterns, options = {}) {
|
|
32
32
|
const {
|
|
33
33
|
ignoreCase = true,
|
|
34
34
|
wholeWord = false,
|
package/lib/interaction.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
1014
|
-
|
|
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
|
*
|