@webcrawlerapi/stealth 1.0.1 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -21,14 +21,14 @@ The two functions you normally need are:
21
21
  | `Function.prototype.toString` + anonymous injected globals | native-looking `toString` via a `WeakSet` cloak |
22
22
  | `navigator.webdriver` | deleted + faked to `false` |
23
23
  | WebDriver / Selenium / Playwright residue (`cdc_*`, `__webdriver_*`, `__selenium_*`, `__pw*`, `__playwright_*`) | removed |
24
- | `navigator.languages / platform / hardwareConcurrency / deviceMemory / vendor / onLine` | normalized |
24
+ | `navigator.languages / platform / hardwareConcurrency / deviceMemory` | left as the host reports them; overridable via `opts` |
25
25
  | `window.chrome` (`runtime`, `loadTimes`, `csi`, `app`) | faked when missing |
26
- | `navigator.userAgentData` (`brands`, `getHighEntropyValues`) | faked |
26
+ | `navigator.userAgentData` (`brands`, `getHighEntropyValues`) | faked **only when missing**; a real UA-CH object is left untouched |
27
27
  | `navigator.plugins` / `mimeTypes` (when empty) | faked |
28
28
  | `navigator.permissions.query` | notifications → `default` |
29
- | WebGL `UNMASKED_VENDOR_WEBGL` / `UNMASKED_RENDERER_WEBGL` | spoofed (hides SwiftShader / software GL) |
29
+ | WebGL `UNMASKED_VENDOR_WEBGL` / `UNMASKED_RENDERER_WEBGL` | spoofed with an **OS-consistent** GPU only when software GL (SwiftShader / llvmpipe) is detected, or when `webglRenderer`/`webglVendor` is set. A real GPU is reported as-is |
30
30
  | `matchMedia` pointer / hover | normalized |
31
- | `window.outerWidth` / `outerHeight` | corrected to inner + chrome |
31
+ | `window.outerWidth` / `outerHeight` | corrected only when smaller than the inner size |
32
32
  | `Error.prepareStackTrace` | installed if absent |
33
33
  | `User-Agent` header + `navigator.userAgent` (`HeadlessChrome` token) | read from `browser.userAgent()` and `HeadlessChrome` → `Chrome` via `page.setUserAgent`; never hardcoded, so the version always matches the real engine. Skip with `fixUserAgent: false`, or pass `userAgent` |
34
34
 
@@ -189,11 +189,12 @@ Optional launcher. Requires puppeteer via `opts.puppeteer` or resolved lazily
189
189
 
190
190
  | Key | Default | Used by |
191
191
  |-----|---------|---------|
192
- | `webglRenderer` / `renderer` | `ANGLE (Intel, ...)` | all |
193
- | `webglVendor` / `vendor` | `Google Inc. (Intel)` | all |
194
- | `hardwareConcurrency` | `8` | all |
195
- | `deviceMemory` | `8` | all |
196
- | `platform` | `Linux x86_64` | all |
192
+ | `webglRenderer` / `renderer` | OS-appropriate GPU (only applied when software GL is detected) | all |
193
+ | `webglVendor` / `vendor` | OS-appropriate vendor (only applied when software GL is detected) | all |
194
+ | `hardwareConcurrency` | host value | all |
195
+ | `deviceMemory` | host value | all |
196
+ | `languages` | host value | all |
197
+ | `platform` | host value | all |
197
198
  | `origin` / `permissions` | — | `newPage` |
198
199
  | `puppeteer` | lazily resolved | `launch` |
199
200
  | `executablePath` | `$CHROME_PATH` | `launch` |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webcrawlerapi/stealth",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Zero-dependency stealth layer for Puppeteer / CDP browsers (works with launch() and connect()).",
5
5
  "main": "stealth.js",
6
6
  "types": "stealth.d.ts",
package/stealth.d.ts CHANGED
@@ -1,13 +1,15 @@
1
1
  export interface StealthOptions {
2
- /** WebGL UNMASKED_RENDERER_WEBGL value to report. */
2
+ /** WebGL UNMASKED_RENDERER_WEBGL to report. Only applied when software GL is detected or this is set; defaults to an OS-appropriate renderer. */
3
3
  webglRenderer?: string;
4
- /** WebGL UNMASKED_VENDOR_WEBGL value to report. */
4
+ /** WebGL UNMASKED_VENDOR_WEBGL to report. Only applied when software GL is detected or this is set; defaults to an OS-appropriate vendor. */
5
5
  webglVendor?: string;
6
- /** navigator.hardwareConcurrency override. */
6
+ /** navigator.hardwareConcurrency override. Left as the host value when omitted. */
7
7
  hardwareConcurrency?: number;
8
- /** navigator.deviceMemory override. */
8
+ /** navigator.deviceMemory override. Left as the host value when omitted. */
9
9
  deviceMemory?: number;
10
- /** navigator.platform override. */
10
+ /** navigator.languages / language override. Left as the host value when omitted. */
11
+ languages?: string[];
12
+ /** navigator.platform override. Left as the host value when omitted. */
11
13
  platform?: string;
12
14
  /** Launch only: injected puppeteer instance. */
13
15
  puppeteer?: unknown;
package/stealth.js CHANGED
@@ -21,11 +21,7 @@ const UA =
21
21
  'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 ' +
22
22
  '(KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36';
23
23
 
24
- const VERSION = '1.0.1';
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)';
24
+ const VERSION = '1.0.3';
29
25
 
30
26
  /* ------------------------------------------------------------------ *
31
27
  * The in-page bootstrap. Runs inside every document before page JS. *
@@ -34,8 +30,59 @@ const DEFAULT_VENDOR = 'Google Inc. (Intel)';
34
30
  * ------------------------------------------------------------------ */
35
31
  function buildInitScript(cfg) {
36
32
  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)';
33
+
34
+ // One coherent profile per OS family. The values inside a profile are chosen
35
+ // together so the platform string, UA-CH hints, WebGL strings and geometry
36
+ // all agree with each other *and* with the real Chrome build running the page.
37
+ // (A Windows-only renderer like "Direct3D11" on a Linux platform is a
38
+ // contradiction a fingerprinting script can spot, so we never mix families.)
39
+ const PROFILES = {
40
+ windows: {
41
+ platform: 'Win32',
42
+ uaPlatform: 'Windows',
43
+ platformVersion: '10.0.0',
44
+ architecture: 'x86',
45
+ bitness: '64',
46
+ renderer: 'ANGLE (Intel, Intel(R) UHD Graphics 630 (0x00003E9B) Direct3D11 vs_5_0 ps_5_0, D3D11)',
47
+ vendor: 'Google Inc. (Intel)',
48
+ },
49
+ mac: {
50
+ platform: 'MacIntel',
51
+ uaPlatform: 'macOS',
52
+ platformVersion: '15.0.0',
53
+ architecture: 'arm',
54
+ bitness: '64',
55
+ renderer: 'ANGLE (Apple, ANGLE Metal Renderer: Apple M1, Unspecified Version)',
56
+ vendor: 'Google Inc. (Apple)',
57
+ },
58
+ linux: {
59
+ platform: 'Linux x86_64',
60
+ uaPlatform: 'Linux',
61
+ platformVersion: '',
62
+ architecture: 'x86',
63
+ bitness: '64',
64
+ renderer: 'ANGLE (Intel, Mesa Intel(R) UHD Graphics 630 (CFL GT2), OpenGL 4.6 (Core Profile) Mesa 22.0.5)',
65
+ vendor: 'Google Inc. (Intel)',
66
+ },
67
+ };
68
+
69
+ // Derive everything from the real browser so the profile can never drift
70
+ // away from the engine that is actually running.
71
+ const realUA = (typeof navigator !== 'undefined' && navigator.userAgent) || '';
72
+ const chromeVer = (realUA.match(/Chrome\/(\d+)/) || [])[1] || '120';
73
+ const fullVersion = chromeVer + '.0.0.0';
74
+ const osFamily = /Windows/i.test(realUA)
75
+ ? 'windows'
76
+ : /Macintosh|Mac OS X/i.test(realUA)
77
+ ? 'mac'
78
+ : 'linux';
79
+ const profile = PROFILES[osFamily];
80
+
81
+ // Optional overrides. Anything not supplied keeps the host's real value, so
82
+ // the page never contradicts the machine it is running on.
83
+ const renderer = cfg.renderer || profile.renderer;
84
+ const vendor = cfg.vendor || profile.vendor;
85
+ const languages = cfg.languages;
39
86
 
40
87
  const patched = new WeakSet();
41
88
 
@@ -117,17 +164,27 @@ function buildInitScript(cfg) {
117
164
  };
118
165
  }
119
166
 
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 (_) {}
167
+ // --- navigator.languages / language (only when the caller asked for it) ---
168
+ // Overriding these while leaving the Accept-Language request header alone
169
+ // would make the JS view disagree with the network view.
170
+ if (Array.isArray(languages) && languages.length) {
171
+ try { getter(Navigator.prototype, 'languages', Object.freeze(languages.slice()), 'languages'); } catch (_) {}
172
+ try { getter(Navigator.prototype, 'language', languages[0], 'language'); } catch (_) {}
173
+ }
123
174
 
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 (_) {}
175
+ // --- platform / vendor / hardware ---
176
+ // These already match the real engine, so they are left untouched unless an
177
+ // explicit override is supplied. Forcing a value the host does not actually
178
+ // have (e.g. hardwareConcurrency 8 on a 4-core box) is itself a tell.
179
+ if (cfg.platform) {
180
+ try { getter(Navigator.prototype, 'platform', cfg.platform, 'platform'); } catch (_) {}
181
+ }
182
+ if (cfg.hardwareConcurrency) {
183
+ try { getter(Navigator.prototype, 'hardwareConcurrency', cfg.hardwareConcurrency, 'hardwareConcurrency'); } catch (_) {}
184
+ }
185
+ if (cfg.deviceMemory) {
186
+ try { getter(Navigator.prototype, 'deviceMemory', cfg.deviceMemory, 'deviceMemory'); } catch (_) {}
187
+ }
131
188
 
132
189
  // --- plugins / mimeTypes (only when empty) ---
133
190
  try {
@@ -195,55 +252,92 @@ function buildInitScript(cfg) {
195
252
  } catch (_) {}
196
253
 
197
254
  // --- userAgentData ---
255
+ // When the engine exposes UA-CH it is already consistent with the UA string
256
+ // and the sec-ch-ua headers, so it is left alone. The synthetic object is only
257
+ // built when it is genuinely missing, and then from the same profile.
198
258
  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 {
259
+ if (!navigator.userAgentData) {
260
+ const grease = '24';
261
+ const brands = [
262
+ { brand: 'Chromium', version: chromeVer },
263
+ { brand: 'Google Chrome', version: chromeVer },
264
+ { brand: 'Not?A_Brand', version: grease },
265
+ ];
266
+ const fullVersionList = [
267
+ { brand: 'Chromium', version: fullVersion },
268
+ { brand: 'Google Chrome', version: fullVersion },
269
+ { brand: 'Not?A_Brand', version: grease + '.0.0.0' },
270
+ ];
271
+ const highEntropy = {
272
+ architecture: profile.architecture,
273
+ bitness: profile.bitness,
274
+ brands,
275
+ fullVersionList,
276
+ mobile: false,
277
+ model: '',
278
+ platform: profile.uaPlatform,
279
+ platformVersion: profile.platformVersion,
280
+ uaFullVersion: fullVersion,
281
+ wow64: false,
282
+ };
283
+ const data = {
284
+ brands, mobile: false, platform: profile.uaPlatform,
285
+ getHighEntropyValues: cloak(function getHighEntropyValues(hints) {
286
+ const out = { brands, mobile: false, platform: profile.uaPlatform };
287
+ (hints || []).forEach((h) => { if (h in highEntropy) out[h] = highEntropy[h]; });
288
+ return Promise.resolve(out);
289
+ }, 'getHighEntropyValues'),
290
+ toJSON: cloak(function toJSON() { return { brands, mobile: false, platform: profile.uaPlatform }; }, 'toJSON'),
291
+ };
223
292
  getter(Navigator.prototype, 'userAgentData', data, 'userAgentData');
224
293
  }
225
294
  } catch (_) {}
226
295
 
227
296
  // --- WebGL vendor / renderer ---
297
+ // Spoofing is applied only when the renderer gives the host away (software
298
+ // rendering such as SwiftShader / llvmpipe / Mesa OffScreen, or a server /
299
+ // datacenter GPU such as an NVIDIA L40S, A100, H100, T4 or vGPU slice) or the
300
+ // caller explicitly asked for it. When the engine reports an ordinary consumer
301
+ // GPU we leave it alone: swapping a real, coherent renderer for a different one
302
+ // only creates contradictions. A datacenter GPU is not a consumer GPU: real
303
+ // cloud-browser hosts (e.g. Kernel) expose one and bot protection (Reuters)
304
+ // blocks on it.
228
305
  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 });
306
+ let softwareGL = false;
307
+ try {
308
+ const probe = document.createElement('canvas');
309
+ const gl = probe.getContext('webgl') || probe.getContext('experimental-webgl');
310
+ if (gl) {
311
+ const dbg = gl.getExtension('WEBGL_debug_renderer_info');
312
+ const real = dbg
313
+ ? String(gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) || '')
314
+ : String(gl.getParameter(gl.RENDERER) || '');
315
+ softwareGL =
316
+ /swiftshader|llvmpipe|software|mesa offscreen|angle \(google/i.test(real) ||
317
+ /\b(?:[AHL]\d{2,3}[A-Z]?|T4|V100|K80|P100|P4|GRID|Tesla|vGPU)\b|-\d+Q\b|NVIDIA (?:A|H|L)\d/i.test(real);
318
+ }
319
+ } catch (_) {}
320
+
321
+ if (softwareGL || cfg.renderer || cfg.vendor) {
322
+ for (const name of ['WebGLRenderingContext', 'WebGL2RenderingContext']) {
323
+ const Ctor = window[name];
324
+ if (!Ctor || !Ctor.prototype) continue;
325
+ const origGetParameter = Ctor.prototype.getParameter;
326
+ const origGetExtension = Ctor.prototype.getExtension;
327
+ const getParameter = cloak(function getParameter(p) {
328
+ if (p === 37445) return vendor;
329
+ if (p === 37446) return renderer;
330
+ return origGetParameter.call(this, p);
331
+ }, 'getParameter');
332
+ const getExtension = cloak(function getExtension(n) {
333
+ if (n === 'WEBGL_debug_renderer_info') {
334
+ return { UNMASKED_VENDOR_WEBGL: 37445, UNMASKED_RENDERER_WEBGL: 37446 };
335
+ }
336
+ return origGetExtension.call(this, n);
337
+ }, 'getExtension');
338
+ Object.defineProperty(Ctor.prototype, 'getParameter', { value: getParameter, writable: true, configurable: true });
339
+ Object.defineProperty(Ctor.prototype, 'getExtension', { value: getExtension, writable: true, configurable: true });
340
+ }
247
341
  }
248
342
  } catch (_) {}
249
343
 
@@ -307,11 +401,14 @@ function buildInitScript(cfg) {
307
401
  } catch (_) {}
308
402
 
309
403
  // --- window geometry drift ---
404
+ // outerWidth/outerHeight must be >= the inner ones. Only correct the cases
405
+ // that are actually broken instead of forcing a value that may contradict the
406
+ // real window manager.
310
407
  try {
311
408
  const innerH = window.innerHeight || 900;
312
409
  const innerW = window.innerWidth || 1366;
313
- getter(window, 'outerHeight', innerH + 20, 'outerHeight');
314
- getter(window, 'outerWidth', innerW, 'outerWidth');
410
+ if (!window.outerWidth || window.outerWidth < innerW) getter(window, 'outerWidth', innerW + 16, 'outerWidth');
411
+ if (!window.outerHeight || window.outerHeight < innerH) getter(window, 'outerHeight', innerH + 88, 'outerHeight');
315
412
  } catch (_) {}
316
413
  }
317
414
 
@@ -321,12 +418,15 @@ function buildInitScript(cfg) {
321
418
 
322
419
  function resolveConfig(opts) {
323
420
  opts = opts || {};
421
+ // Only carry explicit overrides. Defaults are resolved in-page from the real
422
+ // browser (see buildInitScript) so the profile cannot contradict the engine.
324
423
  return {
325
- renderer: opts.webglRenderer || opts.renderer || DEFAULT_RENDERER,
326
- vendor: opts.webglVendor || opts.vendor || DEFAULT_VENDOR,
424
+ renderer: opts.webglRenderer || opts.renderer,
425
+ vendor: opts.webglVendor || opts.vendor,
327
426
  hardwareConcurrency: opts.hardwareConcurrency,
328
427
  deviceMemory: opts.deviceMemory,
329
428
  platform: opts.platform,
429
+ languages: opts.languages,
330
430
  };
331
431
  }
332
432