@webcrawlerapi/stealth 0.0.0-stage → 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 WebCrawlerAPI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,235 @@
1
- # Temporary Holding Version
1
+ # @webcrawlerapi/stealth
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A **zero-dependency** stealth layer for Puppeteer (or any CDP-driven browser).
4
+ It installs anti-detection patches into the page **before any page script runs**,
5
+ so fingerprints are already clean by the time the target's own JS looks at them.
6
+
7
+ Works with **both** ways of driving Chrome:
8
+
9
+ - `puppeteer.launch()` — you start the browser
10
+ - `puppeteer.connect()` — you attach to a browser that is already running
11
+
12
+ The two functions you normally need are:
13
+
14
+ - **`installStealth(page, opts)`** — patches a page
15
+ - **`installOopifHook(browser, opts)`** — patches sandboxed / out-of-process iframes
16
+
17
+ ## What it patches
18
+
19
+ | Vector | Patch |
20
+ |--------|-------|
21
+ | `Function.prototype.toString` + anonymous injected globals | native-looking `toString` via a `WeakSet` cloak |
22
+ | `navigator.webdriver` | deleted + faked to `false` |
23
+ | WebDriver / Selenium / Playwright residue (`cdc_*`, `__webdriver_*`, `__selenium_*`, `__pw*`, `__playwright_*`) | removed |
24
+ | `navigator.languages / platform / hardwareConcurrency / deviceMemory / vendor / onLine` | normalized |
25
+ | `window.chrome` (`runtime`, `loadTimes`, `csi`, `app`) | faked when missing |
26
+ | `navigator.userAgentData` (`brands`, `getHighEntropyValues`) | faked |
27
+ | `navigator.plugins` / `mimeTypes` (when empty) | faked |
28
+ | `navigator.permissions.query` | notifications → `default` |
29
+ | WebGL `UNMASKED_VENDOR_WEBGL` / `UNMASKED_RENDERER_WEBGL` | spoofed (hides SwiftShader / software GL) |
30
+ | `matchMedia` pointer / hover | normalized |
31
+ | `window.outerWidth` / `outerHeight` | corrected to inner + chrome |
32
+ | `Error.prepareStackTrace` | installed if absent |
33
+
34
+ ---
35
+
36
+ ## Install (without npm)
37
+
38
+ There is nothing to fetch from the npm registry — the whole module is a single
39
+ dependency-free file. Use whichever fits:
40
+
41
+ ### Option 1 — clone the repo
42
+
43
+ ```sh
44
+ git clone https://github.com/WebCrawlerAPI/stealth.git
45
+ cp stealth/stealth.js /your/project/
46
+ ```
47
+
48
+ ### Option 2 — single-file download
49
+
50
+ ```sh
51
+ # install.sh ships with the repo
52
+ GH_TOKEN=ghp_xxx sh install.sh # writes ./stealth.js
53
+
54
+ # or, by hand (the repo is private → token required)
55
+ curl -fsSL \
56
+ -H "Authorization: Bearer $GH_TOKEN" \
57
+ -H "Accept: application/vnd.github.raw" \
58
+ "https://api.github.com/repos/WebCrawlerAPI/stealth/contents/stealth.js?ref=main" \
59
+ -o stealth.js
60
+ ```
61
+
62
+ Then:
63
+
64
+ ```js
65
+ const stealth = require('./stealth.js');
66
+ ```
67
+
68
+ No `npm install`, no transitive dependencies. (A `package.json` is included for
69
+ projects that *do* use npm, but it declares **no dependencies**.)
70
+
71
+ ---
72
+
73
+ ## Quick start
74
+
75
+ ```js
76
+ const stealth = require('./stealth.js');
77
+
78
+ const browser = await /* your browser */;
79
+ const page = await browser.newPage();
80
+
81
+ await stealth.installStealth(page, {}); // 1. register patches (BEFORE goto)
82
+ stealth.installOopifHook(browser, {}); // 2. patch sandboxed iframes too
83
+
84
+ await page.goto(url); // 3. navigate
85
+ ```
86
+
87
+ > Order matters: `installStealth` uses `page.evaluateOnNewDocument`, so it must
88
+ > run **before** the navigation you want patched.
89
+
90
+ ---
91
+
92
+ ## Usage with `launch()`
93
+
94
+ If you start the browser yourself, use the bundled helper (it needs puppeteer —
95
+ pass it in or have it resolvable):
96
+
97
+ ```js
98
+ const puppeteer = require('puppeteer-core');
99
+ const stealth = require('./stealth.js');
100
+
101
+ const browser = await stealth.launch({
102
+ puppeteer, // or it is required lazily
103
+ executablePath: '/usr/bin/google-chrome',
104
+ headless: false,
105
+ userDataDir: '/tmp/my-profile', // use a FRESH profile
106
+ });
107
+
108
+ const page = await stealth.newPage(browser, { origin: 'https://example.com' });
109
+ await page.goto('https://example.com');
110
+ ```
111
+
112
+ `newPage()` is shorthand for `browser.newPage()` + `installStealth()` +
113
+ `installOopifHook()`.
114
+
115
+ ## Usage with `connect()`
116
+
117
+ This is the case when you attach to an already-running browser (e.g. one started
118
+ with `--remote-debugging-port=9222`, or a browser on a server / remote desktop):
119
+
120
+ ```js
121
+ const puppeteer = require('puppeteer-core');
122
+ const stealth = require('./stealth.js');
123
+
124
+ const browser = await puppeteer.connect({
125
+ browserURL: 'http://127.0.0.1:9222', // or browserWSEndpoint: 'ws://...'
126
+ });
127
+
128
+ const page = await browser.newPage();
129
+ await stealth.installStealth(page, {}); // MUST run before goto()
130
+ stealth.installOopifHook(browser, {});
131
+
132
+ await page.goto('https://example.com');
133
+ // ...do work...
134
+ await page.close();
135
+ browser.disconnect(); // do NOT close() a browser you didn't start
136
+ ```
137
+
138
+ ### If the page is already loaded
139
+
140
+ `installStealth()` only affects **future** documents. When you attach to a page
141
+ that is already on the target URL, also patch the current document and reload:
142
+
143
+ ```js
144
+ await stealth.installStealth(page, {}); // future navigations
145
+ await stealth.injectNow(page); // current document
146
+ await page.reload({ waitUntil: 'domcontentloaded' });
147
+ ```
148
+
149
+ ---
150
+
151
+ ## API
152
+
153
+ ### `await installStealth(page, opts?)`
154
+ Registers the bootstrap for every document the page loads from now on.
155
+ Must be called **before** the navigation you want patched.
156
+ Returns the page.
157
+
158
+ ### `installOopifHook(browser, opts?)`
159
+ Hooks `targetcreated` and injects the bootstrap into out-of-process / sandboxed
160
+ iframes (which get their own CDP target and are **not** covered by
161
+ page-level `evaluateOnNewDocument`). Idempotent. Returns the browser.
162
+
163
+ ### `await injectNow(page, opts?)`
164
+ Runs the bootstrap in the page's **current** document. Use after `connect()`
165
+ when the page is already loaded, then reload.
166
+
167
+ ### `await newPage(browser, opts?)`
168
+ `browser.newPage()` + `installStealth` + `installOopifHook`.
169
+
170
+ ### `buildSource(opts?)`
171
+ Returns the bootstrap as a **string** — handy for a raw CDP call:
172
+
173
+ ```js
174
+ await session.send('Page.addScriptToEvaluateOnNewDocument', {
175
+ source: stealth.buildSource({}),
176
+ runImmediately: true,
177
+ });
178
+ ```
179
+
180
+ ### `await humanMouse(page, points)`
181
+ Tiny human-ish mouse helper: `[{ x, y, steps?, click?, delay? }, ...]`.
182
+
183
+ ### `await launch(opts?)`
184
+ Optional launcher. Requires puppeteer via `opts.puppeteer` or resolved lazily
185
+ (`puppeteer-core`, then `puppeteer`). Never touched unless you call it.
186
+
187
+ ## Options (`opts`)
188
+
189
+ | Key | Default | Used by |
190
+ |-----|---------|---------|
191
+ | `webglRenderer` / `renderer` | `ANGLE (Intel, ...)` | all |
192
+ | `webglVendor` / `vendor` | `Google Inc. (Intel)` | all |
193
+ | `hardwareConcurrency` | `8` | all |
194
+ | `deviceMemory` | `8` | all |
195
+ | `platform` | `Linux x86_64` | all |
196
+ | `origin` / `permissions` | — | `newPage` |
197
+ | `puppeteer` | lazily resolved | `launch` |
198
+ | `executablePath` | `$CHROME_PATH` | `launch` |
199
+ | `headless` | `false` | `launch` |
200
+ | `userDataDir` | `~/.cache/chrome-stealth-profile` | `launch` |
201
+ | `args` | `[]` | `launch` |
202
+
203
+ ---
204
+
205
+ ## Verify it is working
206
+
207
+ ```js
208
+ await stealth.installStealth(page, {});
209
+ await page.goto('about:blank');
210
+ console.log(await page.evaluate(() => {
211
+ const c = document.createElement('canvas').getContext('webgl');
212
+ const e = c.getExtension('WEBGL_debug_renderer_info');
213
+ return {
214
+ webdriver: navigator.webdriver, // false
215
+ plugins: navigator.plugins.length, // > 0
216
+ gl: [c.getParameter(e.UNMASKED_VENDOR_WEBGL)], // not "SwiftShader"
217
+ };
218
+ }));
219
+ ```
220
+
221
+ ## Troubleshooting
222
+
223
+ - **Still detected after `connect()`** — you probably called `goto()` before
224
+ `installStealth()`. Install first, then navigate (or use `injectNow()` + reload).
225
+ - **Fresh vs reused profile** — a persistent profile can carry a *stale, already
226
+ flagged* cookie for the target. Test with a brand-new `userDataDir` to rule
227
+ that out.
228
+ - **Software WebGL** — servers with no GPU report SwiftShader. The WebGL patch
229
+ hides this in the page, but make sure `installStealth` actually ran.
230
+ - **Sandboxed iframes** — if the challenge runs in a `<iframe sandbox>`, make
231
+ sure `installOopifHook(browser)` was called.
232
+
233
+ ## License
234
+
235
+ MIT
package/package.json CHANGED
@@ -1,6 +1,40 @@
1
1
  {
2
2
  "name": "@webcrawlerapi/stealth",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "Zero-dependency stealth layer for Puppeteer / CDP browsers (works with launch() and connect()).",
5
+ "main": "stealth.js",
6
+ "types": "stealth.d.ts",
7
+ "files": [
8
+ "stealth.js",
9
+ "stealth.d.ts",
10
+ "README.md",
11
+ "LICENSE"
12
+ ],
13
+ "scripts": {
14
+ "example:connect": "node examples/connect.js",
15
+ "example:launch": "node examples/launch.js"
16
+ },
17
+ "engines": {
18
+ "node": ">=16"
19
+ },
20
+ "peerDependencies": {
21
+ "puppeteer": ">=19",
22
+ "puppeteer-core": ">=19"
23
+ },
24
+ "peerDependenciesMeta": {
25
+ "puppeteer": {
26
+ "optional": true
27
+ },
28
+ "puppeteer-core": {
29
+ "optional": true
30
+ }
31
+ },
32
+ "license": "MIT",
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/WebCrawlerAPI/stealth.git"
39
+ }
40
+ }
package/stealth.d.ts ADDED
@@ -0,0 +1,55 @@
1
+ export interface StealthOptions {
2
+ /** WebGL UNMASKED_RENDERER_WEBGL value to report. */
3
+ webglRenderer?: string;
4
+ /** WebGL UNMASKED_VENDOR_WEBGL value to report. */
5
+ webglVendor?: string;
6
+ /** navigator.hardwareConcurrency override. */
7
+ hardwareConcurrency?: number;
8
+ /** navigator.deviceMemory override. */
9
+ deviceMemory?: number;
10
+ /** navigator.platform override. */
11
+ platform?: string;
12
+ /** Launch only: injected puppeteer instance. */
13
+ puppeteer?: unknown;
14
+ /** Launch only: chrome executable. */
15
+ executablePath?: string;
16
+ /** Launch only. */
17
+ headless?: boolean;
18
+ /** Launch only: profile dir. */
19
+ userDataDir?: string;
20
+ /** Launch only: extra chrome args. */
21
+ args?: string[];
22
+ /** newPage only: origin for overridePermissions. */
23
+ origin?: string;
24
+ /** newPage only: permissions to grant. */
25
+ permissions?: string[];
26
+ }
27
+
28
+ /** Minimal structural types so the module has no puppeteer dependency. */
29
+ export interface PageLike {
30
+ evaluateOnNewDocument(fn: unknown, arg?: unknown): Promise<unknown>;
31
+ evaluate(fnOrSource: unknown): Promise<unknown>;
32
+ mouse: {
33
+ move(x: number, y: number, opts?: { steps?: number }): Promise<void>;
34
+ down(): Promise<void>;
35
+ up(): Promise<void>;
36
+ };
37
+ }
38
+
39
+ export interface BrowserLike {
40
+ on(event: string, listener: (target: any) => void): unknown;
41
+ newPage(): Promise<PageLike>;
42
+ defaultBrowserContext?(): { overridePermissions(origin: string, permissions: string[]): Promise<void> };
43
+ }
44
+
45
+ export declare const VERSION: string;
46
+ export declare const UA: string;
47
+ export declare function buildInitScript(cfg?: StealthOptions): void;
48
+ export declare function buildSource(opts?: StealthOptions): string;
49
+ export declare function installStealth(page: PageLike, opts?: StealthOptions): Promise<PageLike>;
50
+ export declare function injectNow(page: PageLike, opts?: StealthOptions): Promise<PageLike>;
51
+ export declare function installOopifHook(browser: BrowserLike, opts?: StealthOptions): BrowserLike;
52
+ export declare function newPage(browser: BrowserLike, opts?: StealthOptions): Promise<PageLike>;
53
+ export declare function humanMouse(page: PageLike, points: Array<{ x: number; y: number; steps?: number; click?: boolean; delay?: number }>): Promise<void>;
54
+ export declare function getPuppeteer(injected?: unknown): any;
55
+ export declare function launch(opts?: StealthOptions): Promise<BrowserLike>;
package/stealth.js ADDED
@@ -0,0 +1,477 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @webcrawlerapi/stealth
5
+ * Zero-dependency stealth layer for Puppeteer / any CDP-driven browser.
6
+ *
7
+ * Works with BOTH puppeteer.launch() and puppeteer.connect():
8
+ *
9
+ * const stealth = require('./stealth.js');
10
+ * const page = await browser.newPage(); // or reuse an existing page
11
+ * await stealth.installStealth(page, {}); // BEFORE any goto()
12
+ * stealth.installOopifHook(browser, {}); // patches sandboxed iframes too
13
+ * await page.goto(url);
14
+ *
15
+ * There are no runtime dependencies. Puppeteer is only required if you use
16
+ * the optional `launch()` helper, and even then it is looked up lazily or can
17
+ * be injected via `opts.puppeteer`.
18
+ */
19
+
20
+ const UA =
21
+ 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ' +
22
+ '(KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36';
23
+
24
+ const VERSION = '1.0.0';
25
+
26
+ const DEFAULT_RENDERER =
27
+ 'ANGLE (Intel, Intel(R) UHD Graphics 630 (0x00003E9B) Direct3D11 vs_5_0 ps_5_0, D3D11)';
28
+ const DEFAULT_VENDOR = 'Google Inc. (Intel)';
29
+
30
+ /* ------------------------------------------------------------------ *
31
+ * The in-page bootstrap. Runs inside every document before page JS. *
32
+ * Serialized by evaluateOnNewDocument() / addScriptToEvaluateOnNewDocument()
33
+ * so it must be fully self-contained (no closure over this module).
34
+ * ------------------------------------------------------------------ */
35
+ function buildInitScript(cfg) {
36
+ cfg = cfg || {};
37
+ const renderer = cfg.renderer || 'ANGLE (Intel, Intel(R) UHD Graphics 630 (0x00003E9B) Direct3D11 vs_5_0 ps_5_0, D3D11)';
38
+ const vendor = cfg.vendor || 'Google Inc. (Intel)';
39
+
40
+ const patched = new WeakSet();
41
+
42
+ try { Object.defineProperty(window, '__stealth_ts', { value: Date.now(), configurable: true }); } catch (_) {}
43
+
44
+ // --- native-looking Function.prototype.toString ---
45
+ const origToString = Function.prototype.toString;
46
+ const fakeToString = function toString() {
47
+ if (patched.has(this)) {
48
+ const name = (this && this.name) || '';
49
+ return 'function ' + name + '() { [native code] }';
50
+ }
51
+ return origToString.call(this);
52
+ };
53
+ patched.add(fakeToString);
54
+ try { Object.defineProperty(fakeToString, 'name', { value: 'toString' }); } catch (_) {}
55
+ try { Object.defineProperty(fakeToString, 'length', { value: 0 }); } catch (_) {}
56
+ Object.defineProperty(Function.prototype, 'toString', {
57
+ value: fakeToString,
58
+ writable: true,
59
+ configurable: true,
60
+ });
61
+
62
+ function cloak(fn, name) {
63
+ if (typeof fn !== 'function') return fn;
64
+ patched.add(fn);
65
+ try {
66
+ if (name) Object.defineProperty(fn, 'name', { value: name, configurable: true });
67
+ Object.defineProperty(fn, 'length', { value: fn.length, configurable: true });
68
+ } catch (_) {}
69
+ return fn;
70
+ }
71
+
72
+ function getter(obj, prop, value, name) {
73
+ const get = cloak(function () { return value; }, 'get ' + (name || prop));
74
+ Object.defineProperty(obj, prop, {
75
+ get,
76
+ set: undefined,
77
+ enumerable: true,
78
+ configurable: true,
79
+ });
80
+ }
81
+
82
+ // --- navigator.webdriver ---
83
+ try { delete Navigator.prototype.webdriver; } catch (_) {}
84
+ try { getter(Navigator.prototype, 'webdriver', false, 'webdriver'); } catch (_) {}
85
+
86
+ // --- window.chrome ---
87
+ if (!window.chrome || typeof window.chrome !== 'object') window.chrome = {};
88
+ if (!window.chrome.runtime) {
89
+ Object.defineProperty(window.chrome, 'runtime', {
90
+ value: {
91
+ connect: cloak(function connect() {}, 'connect'),
92
+ sendMessage: cloak(function sendMessage() {}, 'sendMessage'),
93
+ id: undefined,
94
+ },
95
+ configurable: true,
96
+ });
97
+ }
98
+ if (!window.chrome.loadTimes) {
99
+ Object.defineProperty(window.chrome, 'loadTimes', {
100
+ value: cloak(function loadTimes() {
101
+ return { requestTime: 0, startLoadTime: 0, commitLoadTime: 0, finishLoadTime: 0, navigationType: 'Other', wasNpnNegotiated: false, npnNegotiatedProtocol: 'unknown', wasAlternateProtocolAvailable: false, connectionInfo: 'unknown' };
102
+ }, 'loadTimes'),
103
+ configurable: true,
104
+ });
105
+ }
106
+ if (!window.chrome.csi) {
107
+ Object.defineProperty(window.chrome, 'csi', {
108
+ value: cloak(function csi() { return { onloadT: Date.now(), startE: Date.now(), pageT: 1, tran: 15 }; }, 'csi'),
109
+ configurable: true,
110
+ });
111
+ }
112
+ if (!window.chrome.app) {
113
+ window.chrome.app = {
114
+ isInstalled: false,
115
+ InstallState: { DISABLED: 'disabled', INSTALLED: 'installed', NOT_INSTALLED: 'not_installed' },
116
+ RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' },
117
+ };
118
+ }
119
+
120
+ // --- navigator.languages / language ---
121
+ try { getter(Navigator.prototype, 'languages', Object.freeze(['en-US', 'en']), 'languages'); } catch (_) {}
122
+ try { getter(Navigator.prototype, 'language', 'en-US', 'language'); } catch (_) {}
123
+
124
+ // --- hardware / platform ---
125
+ try { getter(Navigator.prototype, 'hardwareConcurrency', cfg.hardwareConcurrency || 8, 'hardwareConcurrency'); } catch (_) {}
126
+ try { getter(Navigator.prototype, 'deviceMemory', cfg.deviceMemory || 8, 'deviceMemory'); } catch (_) {}
127
+ try { getter(Navigator.prototype, 'platform', cfg.platform || 'Linux x86_64', 'platform'); } catch (_) {}
128
+ try { getter(Navigator.prototype, 'maxTouchPoints', 0, 'maxTouchPoints'); } catch (_) {}
129
+ try { getter(Navigator.prototype, 'vendor', 'Google Inc.', 'vendor'); } catch (_) {}
130
+ try { getter(Navigator.prototype, 'onLine', true, 'onLine'); } catch (_) {}
131
+
132
+ // --- plugins / mimeTypes (only when empty) ---
133
+ try {
134
+ if (!navigator.plugins || navigator.plugins.length === 0) {
135
+ function mkMime(type, suffixes, description) {
136
+ return { type, suffixes, description, enabledPlugin: null };
137
+ }
138
+ const pluginDefs = [
139
+ { name: 'PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format', mimes: [
140
+ { type: 'application/pdf', suffixes: 'pdf', description: 'Portable Document Format' },
141
+ { type: 'text/pdf', suffixes: 'pdf', description: 'Portable Document Format' },
142
+ ]},
143
+ { name: 'Chrome PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format', mimes: [
144
+ { type: 'application/pdf', suffixes: 'pdf', description: 'Portable Document Format' },
145
+ { type: 'text/pdf', suffixes: 'pdf', description: 'Portable Document Format' },
146
+ ]},
147
+ ];
148
+ const plugins = [];
149
+ const mimes = [];
150
+ for (const p of pluginDefs) {
151
+ const plugin = { name: p.name, filename: p.filename, description: p.description, length: p.mimes.length };
152
+ p.mimes.forEach((m, i) => {
153
+ const mm = mkMime(m.type, m.suffixes, m.description);
154
+ mm.enabledPlugin = plugin;
155
+ plugin[i] = mm;
156
+ if (!mimes.some((x) => x.type === m.type)) mimes.push(mm);
157
+ });
158
+ plugins.push(plugin);
159
+ }
160
+ plugins.item = cloak(function item(i) { return this[i] || null; }, 'item');
161
+ plugins.namedItem = cloak(function namedItem() { return null; }, 'namedItem');
162
+ plugins.refresh = cloak(function refresh() {}, 'refresh');
163
+ mimes.item = cloak(function item(i) { return this[i] || null; }, 'item');
164
+ mimes.namedItem = cloak(function namedItem() { return null; }, 'namedItem');
165
+ Object.defineProperty(plugins, 'length', { value: pluginDefs.length });
166
+ Object.defineProperty(mimes, 'length', { value: mimes.length });
167
+ try {
168
+ const pProto = Object.getPrototypeOf(navigator.plugins);
169
+ Object.defineProperty(pProto, 'item', { value: cloak(function item(i) { return this[i] || null; }, 'item'), writable: true, configurable: true });
170
+ Object.defineProperty(pProto, 'namedItem', { value: cloak(function namedItem() { return null; }, 'namedItem'), writable: true, configurable: true });
171
+ Object.defineProperty(pProto, 'refresh', { value: cloak(function refresh() {}, 'refresh'), writable: true, configurable: true });
172
+ const mProto = Object.getPrototypeOf(navigator.mimeTypes);
173
+ Object.defineProperty(mProto, 'item', { value: cloak(function item(i) { return this[i] || null; }, 'item'), writable: true, configurable: true });
174
+ Object.defineProperty(mProto, 'namedItem', { value: cloak(function namedItem() { return null; }, 'namedItem'), writable: true, configurable: true });
175
+ } catch (_) {}
176
+ getter(Navigator.prototype, 'plugins', plugins, 'plugins');
177
+ getter(Navigator.prototype, 'mimeTypes', mimes, 'mimeTypes');
178
+ }
179
+ } catch (_) {}
180
+
181
+ // --- permissions ---
182
+ try {
183
+ const origQuery = navigator.permissions.query.bind(navigator.permissions);
184
+ const query = cloak(function query(params) {
185
+ if (params && params.name === 'notifications') {
186
+ return Promise.resolve({ state: 'default', name: 'notifications', onchange: null });
187
+ }
188
+ return origQuery(params);
189
+ }, 'query');
190
+ try {
191
+ Object.defineProperty(Permissions.prototype, 'query', { value: query, writable: true, configurable: true });
192
+ } catch (_) {
193
+ navigator.permissions.query = query;
194
+ }
195
+ } catch (_) {}
196
+
197
+ // --- userAgentData ---
198
+ try {
199
+ const brands = [
200
+ { brand: 'Chromium', version: '151' },
201
+ { brand: 'Not?A_Brand', version: '24' },
202
+ { brand: 'Google Chrome', version: '151' },
203
+ ];
204
+ const fullVersionList = brands.map((b) => ({ brand: b.brand, version: b.brand === 'Not?A_Brand' ? '24.0.0.0' : '151.0.7922.108' }));
205
+ const highEntropy = {
206
+ architecture: 'x86', bitness: '64', brands, fullVersionList, mobile: false, model: '',
207
+ platform: 'Linux', platformVersion: '6.8.0', uaFullVersion: '151.0.7922.108', wow64: false,
208
+ };
209
+ const data = {
210
+ brands, mobile: false, platform: 'Linux',
211
+ getHighEntropyValues: cloak(function getHighEntropyValues(hints) {
212
+ const out = { brands, mobile: false, platform: 'Linux' };
213
+ (hints || []).forEach((h) => { if (h in highEntropy) out[h] = highEntropy[h]; });
214
+ return Promise.resolve(out);
215
+ }, 'getHighEntropyValues'),
216
+ toJSON: cloak(function toJSON() { return { brands, mobile: false, platform: 'Linux' }; }, 'toJSON'),
217
+ };
218
+ if (navigator.userAgentData) {
219
+ try {
220
+ Object.defineProperty(Navigator.prototype, 'userAgentData', { get: cloak(function () { return data; }, 'get userAgentData'), configurable: true });
221
+ } catch (_) {}
222
+ } else {
223
+ getter(Navigator.prototype, 'userAgentData', data, 'userAgentData');
224
+ }
225
+ } catch (_) {}
226
+
227
+ // --- WebGL vendor / renderer ---
228
+ try {
229
+ for (const name of ['WebGLRenderingContext', 'WebGL2RenderingContext']) {
230
+ const Ctor = window[name];
231
+ if (!Ctor || !Ctor.prototype) continue;
232
+ const origGetParameter = Ctor.prototype.getParameter;
233
+ const origGetExtension = Ctor.prototype.getExtension;
234
+ const getParameter = cloak(function getParameter(p) {
235
+ if (p === 37445) return vendor;
236
+ if (p === 37446) return renderer;
237
+ return origGetParameter.call(this, p);
238
+ }, 'getParameter');
239
+ const getExtension = cloak(function getExtension(n) {
240
+ if (n === 'WEBGL_debug_renderer_info') {
241
+ return { UNMASKED_VENDOR_WEBGL: 37445, UNMASKED_RENDERER_WEBGL: 37446 };
242
+ }
243
+ return origGetExtension.call(this, n);
244
+ }, 'getExtension');
245
+ Object.defineProperty(Ctor.prototype, 'getParameter', { value: getParameter, writable: true, configurable: true });
246
+ Object.defineProperty(Ctor.prototype, 'getExtension', { value: getExtension, writable: true, configurable: true });
247
+ }
248
+ } catch (_) {}
249
+
250
+ // --- remove automation residue globals ---
251
+ try {
252
+ const keys = [
253
+ 'cdc_adoQpoasnfa76pfcZLmcfl_Array', 'cdc_adoQpoasnfa76pfcZLmcfl_Promise',
254
+ 'cdc_adoQpoasnfa76pfcZLmcfl_Symbol',
255
+ '__webdriver_evaluate', '__selenium_evaluate', '__fxdriver_evaluate',
256
+ '__driver_evaluate', '__webdriver_script_function', '__webdriver_script_func',
257
+ '__webdriver_script_fn', '__webdriverFunc', '_WEBDRIVER_ELEM_CACHE',
258
+ 'domAutomation', 'domAutomationController', '__lastWatirAlert',
259
+ '__lastWatirConfirm', '__lastWatirPrompt', '__playwright_builtins__',
260
+ '__pwInitScripts', '__playwright__binding__', '__pwWebSocketDispatch',
261
+ '__playwright__binding__controller__',
262
+ ];
263
+ for (const k of keys) {
264
+ try {
265
+ if (k in window) delete window[k];
266
+ if (k in document) delete document[k];
267
+ } catch (_) {}
268
+ }
269
+ try { if (navigator.webdriver) delete navigator.webdriver; } catch (_) {}
270
+ } catch (_) {}
271
+
272
+ // --- Error.prepareStackTrace integrity ---
273
+ try {
274
+ if (typeof Error.prepareStackTrace !== 'function') {
275
+ Object.defineProperty(Error, 'prepareStackTrace', {
276
+ value: cloak(function prepareStackTrace(err, stack) {
277
+ return String(err) + '\n' + stack.map((s) => ' at ' + s.toString()).join('\n');
278
+ }, 'prepareStackTrace'),
279
+ writable: true,
280
+ configurable: true,
281
+ });
282
+ }
283
+ } catch (_) {}
284
+
285
+ // --- matchMedia: pointer / hover ---
286
+ try {
287
+ const OVERRIDES = {
288
+ 'any-pointer: fine': true, 'any-pointer: coarse': false, 'any-pointer: none': false,
289
+ 'pointer: fine': true, 'pointer: coarse': false, 'pointer: none': false,
290
+ 'any-hover: hover': true, 'any-hover: none': false,
291
+ 'hover: hover': true, 'hover: none': false,
292
+ };
293
+ const norm = (q) => String(q).toLowerCase().replace(/\s+/g, ' ').trim().replace(/^\(/, '').replace(/\)$/, '').trim();
294
+ const orig = window.matchMedia.bind(window);
295
+ const matchMedia = cloak(function matchMedia(query) {
296
+ const mql = orig(query);
297
+ const key = norm(query);
298
+ if (Object.prototype.hasOwnProperty.call(OVERRIDES, key)) {
299
+ const val = OVERRIDES[key];
300
+ try {
301
+ Object.defineProperty(mql, 'matches', { get: cloak(function () { return val; }, 'get matches'), configurable: true });
302
+ } catch (_) {}
303
+ }
304
+ return mql;
305
+ }, 'matchMedia');
306
+ Object.defineProperty(window, 'matchMedia', { value: matchMedia, writable: true, configurable: true });
307
+ } catch (_) {}
308
+
309
+ // --- window geometry drift ---
310
+ try {
311
+ const innerH = window.innerHeight || 900;
312
+ const innerW = window.innerWidth || 1366;
313
+ getter(window, 'outerHeight', innerH + 20, 'outerHeight');
314
+ getter(window, 'outerWidth', innerW, 'outerWidth');
315
+ } catch (_) {}
316
+ }
317
+
318
+ /* ------------------------------------------------------------------ *
319
+ * Public API *
320
+ * ------------------------------------------------------------------ */
321
+
322
+ function resolveConfig(opts) {
323
+ opts = opts || {};
324
+ return {
325
+ renderer: opts.webglRenderer || opts.renderer || DEFAULT_RENDERER,
326
+ vendor: opts.webglVendor || opts.vendor || DEFAULT_VENDOR,
327
+ hardwareConcurrency: opts.hardwareConcurrency,
328
+ deviceMemory: opts.deviceMemory,
329
+ platform: opts.platform,
330
+ };
331
+ }
332
+
333
+ /** Serialized source string for CDP `Page.addScriptToEvaluateOnNewDocument`. */
334
+ function buildSource(opts) {
335
+ const cfg = resolveConfig(opts);
336
+ return '(' + buildInitScript.toString() + ')(' + JSON.stringify(cfg) + ');';
337
+ }
338
+
339
+ const INSTALLED = Symbol.for('@webcrawlerapi/stealth.installed');
340
+
341
+ /**
342
+ * Install the stealth bootstrap into a page. Call BEFORE the first goto().
343
+ * Compatible with puppeteer.launch(), puppeteer.connect(), and any object
344
+ * exposing `evaluateOnNewDocument(fn, arg)`.
345
+ *
346
+ * @returns {Promise<any>} the same page
347
+ */
348
+ async function installStealth(page, opts) {
349
+ if (!page || typeof page.evaluateOnNewDocument !== 'function') {
350
+ throw new TypeError('installStealth(page): page must expose evaluateOnNewDocument()');
351
+ }
352
+ opts = opts || {};
353
+ const cfg = resolveConfig(opts);
354
+ await page.evaluateOnNewDocument(buildInitScript, cfg);
355
+ try { page[INSTALLED] = true; } catch (_) {}
356
+ return page;
357
+ }
358
+
359
+ /**
360
+ * Also inject the bootstrap into documents that are ALREADY loaded (useful when
361
+ * you attach with connect() to a page that is already on the target URL).
362
+ * The patches will not re-run on navigation unless installStealth() is used too.
363
+ */
364
+ async function injectNow(page, opts) {
365
+ if (!page || typeof page.evaluate !== 'function') {
366
+ throw new TypeError('injectNow(page): page must expose evaluate()');
367
+ }
368
+ await page.evaluate(buildSource(opts));
369
+ return page;
370
+ }
371
+
372
+ /**
373
+ * Patch out-of-process / sandboxed iframes (DataDome's challenge runs there).
374
+ * These get their own CDP target and are NOT covered by page-level
375
+ * evaluateOnNewDocument.
376
+ */
377
+ function installOopifHook(browser, opts) {
378
+ if (!browser || typeof browser.on !== 'function') {
379
+ throw new TypeError('installOopifHook(browser): browser must expose on()');
380
+ }
381
+ opts = opts || {};
382
+ if (browser.__stealthOopifHooked) return browser;
383
+ browser.__stealthOopifHooked = true;
384
+
385
+ const source = buildSource(opts);
386
+ browser.on('targetcreated', async (target) => {
387
+ let type = 'unknown';
388
+ try { type = target.type(); } catch (_) {}
389
+ if (type === 'browser' || type === 'page' || type === 'service_worker') return;
390
+ let session;
391
+ try {
392
+ session = await target.createCDPSession();
393
+ await session.send('Page.enable').catch(() => {});
394
+ await session.send('Page.addScriptToEvaluateOnNewDocument', { source, runImmediately: true }).catch(() => {});
395
+ await session.send('Runtime.runIfWaitingForDebugger').catch(() => {});
396
+ } catch (_) {}
397
+ });
398
+ return browser;
399
+ }
400
+
401
+ /** Convenience: open a page and patch it in one call. */
402
+ async function newPage(browser, opts) {
403
+ if (!browser || typeof browser.newPage !== 'function') {
404
+ throw new TypeError('newPage(browser): browser must expose newPage()');
405
+ }
406
+ opts = opts || {};
407
+ const context = typeof browser.defaultBrowserContext === 'function' ? browser.defaultBrowserContext() : null;
408
+ if (context && opts.origin && typeof context.overridePermissions === 'function') {
409
+ await context.overridePermissions(opts.origin, opts.permissions || []).catch(() => {});
410
+ }
411
+ const page = await browser.newPage();
412
+ await installStealth(page, opts);
413
+ installOopifHook(browser, opts);
414
+ return page;
415
+ }
416
+
417
+ async function humanMouse(page, points) {
418
+ for (let i = 0; i < points.length; i++) {
419
+ const p = points[i];
420
+ await page.mouse.move(p.x, p.y, { steps: p.steps || 12 });
421
+ if (p.click) { await page.mouse.down(); await page.mouse.up(); }
422
+ await new Promise((r) => setTimeout(r, p.delay == null ? 40 : p.delay));
423
+ }
424
+ }
425
+
426
+ /** Lazily resolve a puppeteer module (never required unless launch() is used). */
427
+ function getPuppeteer(injected) {
428
+ if (injected) return injected;
429
+ for (const name of ['puppeteer-core', 'puppeteer']) {
430
+ try { return require(name); } catch (_) {}
431
+ }
432
+ throw new Error('launch() needs puppeteer; pass opts.puppeteer or install puppeteer/puppeteer-core');
433
+ }
434
+
435
+ /** Optional launcher. Requires puppeteer only when actually called. */
436
+ async function launch(opts) {
437
+ opts = opts || {};
438
+ const puppeteer = getPuppeteer(opts.puppeteer);
439
+ const os = require('os');
440
+ const path = require('path');
441
+ const args = opts.args || [];
442
+ const browser = await puppeteer.launch({
443
+ executablePath: opts.executablePath || process.env.CHROME_PATH || undefined,
444
+ headless: opts.headless != null ? opts.headless : false,
445
+ userDataDir: opts.userDataDir || path.join(os.homedir(), '.cache', 'chrome-stealth-profile'),
446
+ defaultViewport: opts.viewport || { width: 1366, height: 900 },
447
+ protocolTimeout: opts.protocolTimeout || 180000,
448
+ ignoreDefaultArgs: ['--enable-automation', '--headless'],
449
+ args: [
450
+ '--no-sandbox',
451
+ '--disable-setuid-sandbox',
452
+ '--disable-blink-features=AutomationControlled',
453
+ '--disable-infobars',
454
+ '--disable-dev-shm-usage',
455
+ '--no-first-run',
456
+ '--no-default-browser-check',
457
+ '--window-size=1366,900',
458
+ '--lang=en-US',
459
+ ...args,
460
+ ],
461
+ });
462
+ return browser;
463
+ }
464
+
465
+ module.exports = {
466
+ VERSION,
467
+ UA,
468
+ buildInitScript,
469
+ buildSource,
470
+ installStealth,
471
+ injectNow,
472
+ installOopifHook,
473
+ newPage,
474
+ humanMouse,
475
+ getPuppeteer,
476
+ launch,
477
+ };