agent-browser-stealth 0.17.0-fork.2 → 0.24.0-fork.2

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.
Files changed (122) hide show
  1. package/README.md +1256 -240
  2. package/bin/agent-browser-darwin-arm64 +0 -0
  3. package/bin/agent-browser-darwin-x64 +0 -0
  4. package/bin/agent-browser-linux-arm64 +0 -0
  5. package/bin/agent-browser-linux-x64 +0 -0
  6. package/bin/agent-browser-win32-x64.exe +0 -0
  7. package/bin/agent-browser.js +13 -2
  8. package/extensions/tab-group-cdp/content-script.js +425 -0
  9. package/extensions/tab-group-cdp/icons/icon.svg +7 -0
  10. package/extensions/tab-group-cdp/manifest.json +34 -0
  11. package/extensions/tab-group-cdp/page-bridge.js +133 -0
  12. package/extensions/tab-group-cdp/service-worker.js +2249 -0
  13. package/extensions/tab-group-cdp/sidepanel.css +258 -0
  14. package/extensions/tab-group-cdp/sidepanel.html +28 -0
  15. package/extensions/tab-group-cdp/sidepanel.js +1225 -0
  16. package/package.json +17 -69
  17. package/scripts/build-all-platforms.sh +6 -0
  18. package/scripts/check-version-sync.js +14 -2
  19. package/scripts/copy-native.js +8 -50
  20. package/scripts/postinstall.js +149 -165
  21. package/scripts/windows-debug/provision.sh +220 -0
  22. package/scripts/windows-debug/run.sh +92 -0
  23. package/scripts/windows-debug/start.sh +43 -0
  24. package/scripts/windows-debug/stop.sh +28 -0
  25. package/scripts/windows-debug/sync.sh +27 -0
  26. package/skills/agent-browser/SKILL.md +256 -159
  27. package/skills/agent-browser/references/authentication.md +101 -0
  28. package/skills/agent-browser/references/commands.md +34 -2
  29. package/skills/agent-browser/references/snapshot-refs.md +25 -0
  30. package/skills/agentcore/SKILL.md +115 -0
  31. package/skills/dogfood/SKILL.md +4 -2
  32. package/skills/electron/SKILL.md +26 -2
  33. package/skills/slack/SKILL.md +0 -9
  34. package/skills/slack/references/slack-tasks.md +2 -8
  35. package/skills/vercel-sandbox/SKILL.md +280 -0
  36. package/bin/agent-browser-local +0 -0
  37. package/bin/agent-browser-stealth +0 -0
  38. package/bin/agent-browser-stealth.d +0 -1
  39. package/dist/action-policy.d.ts +0 -14
  40. package/dist/action-policy.d.ts.map +0 -1
  41. package/dist/action-policy.js +0 -253
  42. package/dist/action-policy.js.map +0 -1
  43. package/dist/actions.d.ts +0 -21
  44. package/dist/actions.d.ts.map +0 -1
  45. package/dist/actions.js +0 -2139
  46. package/dist/actions.js.map +0 -1
  47. package/dist/auth-cli.d.ts +0 -2
  48. package/dist/auth-cli.d.ts.map +0 -1
  49. package/dist/auth-cli.js +0 -97
  50. package/dist/auth-cli.js.map +0 -1
  51. package/dist/auth-vault.d.ts +0 -36
  52. package/dist/auth-vault.d.ts.map +0 -1
  53. package/dist/auth-vault.js +0 -125
  54. package/dist/auth-vault.js.map +0 -1
  55. package/dist/browser.d.ts +0 -665
  56. package/dist/browser.d.ts.map +0 -1
  57. package/dist/browser.js +0 -3210
  58. package/dist/browser.js.map +0 -1
  59. package/dist/confirmation.d.ts +0 -8
  60. package/dist/confirmation.d.ts.map +0 -1
  61. package/dist/confirmation.js +0 -30
  62. package/dist/confirmation.js.map +0 -1
  63. package/dist/daemon.d.ts +0 -78
  64. package/dist/daemon.d.ts.map +0 -1
  65. package/dist/daemon.js +0 -744
  66. package/dist/daemon.js.map +0 -1
  67. package/dist/diff.d.ts +0 -18
  68. package/dist/diff.d.ts.map +0 -1
  69. package/dist/diff.js +0 -271
  70. package/dist/diff.js.map +0 -1
  71. package/dist/domain-filter.d.ts +0 -28
  72. package/dist/domain-filter.d.ts.map +0 -1
  73. package/dist/domain-filter.js +0 -149
  74. package/dist/domain-filter.js.map +0 -1
  75. package/dist/encryption.d.ts +0 -73
  76. package/dist/encryption.d.ts.map +0 -1
  77. package/dist/encryption.js +0 -171
  78. package/dist/encryption.js.map +0 -1
  79. package/dist/ios-actions.d.ts +0 -11
  80. package/dist/ios-actions.d.ts.map +0 -1
  81. package/dist/ios-actions.js +0 -228
  82. package/dist/ios-actions.js.map +0 -1
  83. package/dist/ios-manager.d.ts +0 -266
  84. package/dist/ios-manager.d.ts.map +0 -1
  85. package/dist/ios-manager.js +0 -1073
  86. package/dist/ios-manager.js.map +0 -1
  87. package/dist/protocol.d.ts +0 -26
  88. package/dist/protocol.d.ts.map +0 -1
  89. package/dist/protocol.js +0 -990
  90. package/dist/protocol.js.map +0 -1
  91. package/dist/snapshot.d.ts +0 -67
  92. package/dist/snapshot.d.ts.map +0 -1
  93. package/dist/snapshot.js +0 -514
  94. package/dist/snapshot.js.map +0 -1
  95. package/dist/state-utils.d.ts +0 -77
  96. package/dist/state-utils.d.ts.map +0 -1
  97. package/dist/state-utils.js +0 -178
  98. package/dist/state-utils.js.map +0 -1
  99. package/dist/stealth.d.ts +0 -41
  100. package/dist/stealth.d.ts.map +0 -1
  101. package/dist/stealth.js +0 -1743
  102. package/dist/stealth.js.map +0 -1
  103. package/dist/stream-server.d.ts +0 -117
  104. package/dist/stream-server.d.ts.map +0 -1
  105. package/dist/stream-server.js +0 -309
  106. package/dist/stream-server.js.map +0 -1
  107. package/dist/types.d.ts +0 -973
  108. package/dist/types.d.ts.map +0 -1
  109. package/dist/types.js +0 -2
  110. package/dist/types.js.map +0 -1
  111. package/scripts/check-creepjs-headless.js +0 -137
  112. package/scripts/check-daemon-pid-recovery.js +0 -148
  113. package/scripts/check-sannysoft-webdriver.js +0 -112
  114. package/scripts/check-stealth-regression.js +0 -199
  115. package/scripts/check-turnstile-testkey.ts +0 -125
  116. package/scripts/clawhub-sync.sh +0 -27
  117. package/scripts/sync-upstream.sh +0 -142
  118. package/scripts/verify-bundled-binaries.js +0 -71
  119. package/scripts/verify-native-version.js +0 -48
  120. package/scripts/verify-packed-host-binary.js +0 -88
  121. package/scripts/verify-registry-host-binary.js +0 -120
  122. package/skills/agent-browser-stealth/SKILL.md +0 -127
package/dist/stealth.js DELETED
@@ -1,1743 +0,0 @@
1
- /**
2
- * Stealth mode patches to prevent browser automation detection.
3
- *
4
- * These scripts run via addInitScript (before any page JS) and patch the
5
- * fingerprinting surfaces that anti-bot systems use to identify Playwright /
6
- * Puppeteer / headless Chrome.
7
- */
8
- /**
9
- * Chromium args that reduce automation fingerprint.
10
- * Intended to be merged into the user-supplied args array at launch time.
11
- */
12
- export const STEALTH_CHROMIUM_ARGS = [
13
- '--disable-blink-features=AutomationControlled',
14
- '--use-gl=angle',
15
- '--use-angle=default',
16
- ];
17
- const CDP_SOURCE_URL_SANITIZED = Symbol('ab.cdpSourceUrlSanitized');
18
- function stripSourceUrlLabels(input) {
19
- let output = input;
20
- output = output.replace(/\n?\s*\/\/[@#]\s*sourceURL=[^\n\r]*/gi, '');
21
- output = output.replace(/\n?\s*\/\*[@#]\s*sourceURL=[\s\S]*?\*\//gi, '');
22
- return output;
23
- }
24
- function sanitizeCdpPayload(method, params) {
25
- if (!params || typeof params !== 'object')
26
- return params;
27
- const sanitizeField = (payload, field) => {
28
- const value = payload[field];
29
- if (typeof value !== 'string')
30
- return payload;
31
- const cleaned = stripSourceUrlLabels(value);
32
- if (cleaned === value)
33
- return payload;
34
- return { ...payload, [field]: cleaned };
35
- };
36
- switch (method) {
37
- case 'Runtime.evaluate':
38
- case 'Runtime.compileScript':
39
- return sanitizeField(params, 'expression');
40
- case 'Runtime.callFunctionOn':
41
- return sanitizeField(params, 'functionDeclaration');
42
- case 'Page.addScriptToEvaluateOnNewDocument':
43
- return sanitizeField(params, 'source');
44
- default:
45
- return params;
46
- }
47
- }
48
- /**
49
- * Patch CDPSession.send so Runtime/Page script payloads no longer carry
50
- * sourceURL labels that reveal automation internals.
51
- */
52
- export function wrapCDPSessionSourceUrlSanitizer(session) {
53
- if (!session || typeof session.send !== 'function')
54
- return session;
55
- if (session[CDP_SOURCE_URL_SANITIZED])
56
- return session;
57
- const nativeSend = session.send.bind(session);
58
- const wrappedSend = (method, params) => {
59
- return nativeSend(method, sanitizeCdpPayload(method, params));
60
- };
61
- try {
62
- Object.defineProperty(session, 'send', {
63
- value: wrappedSend,
64
- configurable: true,
65
- writable: true,
66
- });
67
- }
68
- catch {
69
- try {
70
- session.send = wrappedSend;
71
- }
72
- catch {
73
- return session;
74
- }
75
- }
76
- try {
77
- Object.defineProperty(session, CDP_SOURCE_URL_SANITIZED, {
78
- value: true,
79
- configurable: false,
80
- enumerable: false,
81
- writable: false,
82
- });
83
- }
84
- catch {
85
- session[CDP_SOURCE_URL_SANITIZED] = true;
86
- }
87
- return session;
88
- }
89
- /**
90
- * Apply all stealth patches to a BrowserContext.
91
- * Must be called BEFORE any page is created / navigated.
92
- */
93
- export async function applyStealthScripts(context, options = {}) {
94
- await context.addInitScript({ content: buildStealthScript(options) });
95
- // Apply CDP-level User-Agent override so Workers also get the patched UA.
96
- // This must be done per-page since CDP sessions are page-scoped.
97
- for (const page of context.pages()) {
98
- await applyCDPStealthToPage(page, options);
99
- }
100
- context.on('page', (page) => applyCDPStealthToPage(page, options));
101
- }
102
- /**
103
- * Apply browser-level CDP overrides that affect all targets (including Workers).
104
- * Call this right after browser.launch() and before creating pages.
105
- */
106
- export async function applyBrowserLevelStealth(browser, options = {}) {
107
- try {
108
- const cdp = wrapCDPSessionSourceUrlSanitizer(await browser.newBrowserCDPSession());
109
- const version = await cdp.send('Browser.getVersion');
110
- const rawUA = version?.userAgent ?? '';
111
- const explicitUA = options.userAgent?.trim();
112
- if (!explicitUA && !rawUA.includes('HeadlessChrome')) {
113
- await cdp.detach();
114
- return;
115
- }
116
- const patchedUA = explicitUA || rawUA.replace(/HeadlessChrome/g, 'Chrome');
117
- const acceptLanguage = options.acceptLanguage ?? 'en-US,en;q=0.9';
118
- const metadata = buildUserAgentMetadata(patchedUA);
119
- // Override on all existing targets
120
- const { targetInfos } = await cdp.send('Target.getTargets');
121
- for (const target of targetInfos) {
122
- try {
123
- const { sessionId } = await cdp.send('Target.attachToTarget', {
124
- targetId: target.targetId,
125
- flatten: true,
126
- });
127
- await cdp.send('Emulation.setUserAgentOverride', {
128
- userAgent: patchedUA,
129
- acceptLanguage,
130
- platform: getPlatformString(),
131
- userAgentMetadata: metadata,
132
- });
133
- await cdp.send('Target.detachFromTarget', { sessionId }).catch(() => { });
134
- }
135
- catch {
136
- // Some targets don't support Emulation domain
137
- }
138
- }
139
- await cdp.detach();
140
- }
141
- catch {
142
- // newBrowserCDPSession not available -- silently skip
143
- }
144
- }
145
- async function applyCDPStealthToPage(page, options = {}) {
146
- try {
147
- const cdp = wrapCDPSessionSourceUrlSanitizer(await page.context().newCDPSession(page));
148
- const ua = await cdp.send('Browser.getVersion').catch(() => null);
149
- const rawUA = ua?.userAgent ?? '';
150
- const explicitUA = options.userAgent?.trim();
151
- const patchedUA = explicitUA || rawUA.replace(/HeadlessChrome/g, 'Chrome');
152
- const acceptLanguage = options.acceptLanguage ?? 'en-US,en;q=0.9';
153
- const metadata = buildUserAgentMetadata(patchedUA);
154
- await cdp.send('Emulation.setUserAgentOverride', {
155
- userAgent: patchedUA,
156
- acceptLanguage,
157
- platform: getPlatformString(),
158
- userAgentMetadata: metadata,
159
- });
160
- // Set default background color to opaque white (headless default is transparent)
161
- await cdp
162
- .send('Emulation.setDefaultBackgroundColorOverride', {
163
- color: { r: 255, g: 255, b: 255, a: 1 },
164
- })
165
- .catch(() => { });
166
- // Keep CDP session alive so the override persists for Workers
167
- }
168
- catch {
169
- // CDP not available (non-Chromium) -- silently skip
170
- }
171
- }
172
- function buildUserAgentMetadata(patchedUA) {
173
- const versionMatch = patchedUA.match(/Chrome\/(\d+)/);
174
- const majorVersion = versionMatch?.[1] ?? '120';
175
- const fullVersionMatch = patchedUA.match(/Chrome\/([\d.]+)/);
176
- const fullVersion = fullVersionMatch?.[1] ?? `${majorVersion}.0.0.0`;
177
- return {
178
- brands: [
179
- { brand: 'Chromium', version: majorVersion },
180
- { brand: 'Google Chrome', version: majorVersion },
181
- { brand: 'Not-A.Brand', version: '99' },
182
- ],
183
- fullVersionList: [
184
- { brand: 'Chromium', version: fullVersion },
185
- { brand: 'Google Chrome', version: fullVersion },
186
- { brand: 'Not-A.Brand', version: '99.0.0.0' },
187
- ],
188
- fullVersion: fullVersion,
189
- platform: getPlatformHint(),
190
- platformVersion: getPlatformVersionHint(),
191
- architecture: 'x86',
192
- model: '',
193
- mobile: false,
194
- bitness: '64',
195
- wow64: false,
196
- };
197
- }
198
- function getPlatformString() {
199
- if (typeof process !== 'undefined') {
200
- if (process.platform === 'darwin')
201
- return 'macOS';
202
- if (process.platform === 'win32')
203
- return 'Windows';
204
- }
205
- return 'Linux';
206
- }
207
- function getPlatformHint() {
208
- if (typeof process !== 'undefined') {
209
- if (process.platform === 'darwin')
210
- return 'macOS';
211
- if (process.platform === 'win32')
212
- return 'Windows';
213
- }
214
- return 'Linux';
215
- }
216
- function getPlatformVersionHint() {
217
- if (typeof process !== 'undefined') {
218
- if (process.platform === 'darwin')
219
- return '13.0.0';
220
- if (process.platform === 'win32')
221
- return '10.0.0';
222
- }
223
- return '6.1.0';
224
- }
225
- function normalizeLocale(locale) {
226
- if (!locale)
227
- return undefined;
228
- const trimmed = locale.trim();
229
- if (!trimmed)
230
- return undefined;
231
- const cleaned = trimmed.split(',')[0]?.split(';')[0]?.replace(/_/g, '-');
232
- if (!cleaned)
233
- return undefined;
234
- try {
235
- return new Intl.Locale(cleaned).toString();
236
- }
237
- catch {
238
- return undefined;
239
- }
240
- }
241
- function deriveLanguages(locale) {
242
- const normalized = normalizeLocale(locale) ?? 'en-US';
243
- const base = normalized.split('-')[0];
244
- if (!base || base === normalized)
245
- return [normalized];
246
- return [normalized, base];
247
- }
248
- function buildStealthScript(options) {
249
- const locale = normalizeLocale(options.locale) ?? 'en-US';
250
- const languages = deriveLanguages(locale);
251
- const configScript = `const __abStealth = ${JSON.stringify({
252
- locale,
253
- languages,
254
- allowWebGLContextFallback: options.allowWebGLContextFallback === true,
255
- })};`;
256
- // Each patch is an IIFE so variable scoping is clean
257
- return [
258
- configScript,
259
- patchNavigatorWebdriver(),
260
- patchCssSupportsWebdriverHeuristic(),
261
- patchDisableDevtoolAutoBootstrap(),
262
- patchChromeRuntime(),
263
- patchChromeLegacyApis(),
264
- patchIframeContentWindow(),
265
- patchNavigatorLanguages(),
266
- patchNavigatorVendor(),
267
- patchNavigatorPluginsAndMimeTypes(),
268
- patchNavigatorPermissions(),
269
- patchWebGLVendor(),
270
- patchCdcProperties(),
271
- patchSourceUrlStackTraces(),
272
- patchWindowDimensions(),
273
- patchScreenDimensions(),
274
- patchScreenAvailability(),
275
- patchNavigatorHardwareConcurrency(),
276
- patchNotificationPermission(),
277
- patchActiveTextColorHeuristic(),
278
- patchNavigatorConnection(),
279
- patchWorkerConnection(),
280
- patchNavigatorShare(),
281
- patchNavigatorContacts(),
282
- patchContentIndex(),
283
- patchPrefersColorSchemeHeuristic(),
284
- patchPdfViewerEnabled(),
285
- patchMediaCodecs(),
286
- patchMediaDevices(),
287
- patchUserAgentData(),
288
- patchUserAgent(),
289
- patchPerformanceMemory(),
290
- patchDefaultBackgroundColor(),
291
- ].join('\n');
292
- }
293
- // ---------------------------------------------------------------------------
294
- // Individual patches
295
- // ---------------------------------------------------------------------------
296
- /**
297
- * CreepJS uses CSS.supports('border-end-end-radius: initial') + webdriver
298
- * undefined to infer automation. Keep this one probe neutral.
299
- */
300
- function patchCssSupportsWebdriverHeuristic() {
301
- return `(function(){
302
- if (typeof CSS === 'undefined' || typeof CSS.supports !== 'function') return;
303
- const nativeSupports = CSS.supports.bind(CSS);
304
- const normalize = (value) => String(value).replace(/\\s+/g, ' ').trim().toLowerCase();
305
- const target = 'border-end-end-radius: initial';
306
- const patchedSupports = function(...args) {
307
- if (args.length === 1 && normalize(args[0]) === target) {
308
- return false;
309
- }
310
- if (args.length >= 2 && normalize(args[0] + ': ' + args[1]) === target) {
311
- return false;
312
- }
313
- return nativeSupports(...args);
314
- };
315
- try {
316
- Object.defineProperty(patchedSupports, 'name', { value: 'supports', configurable: true });
317
- Object.defineProperty(patchedSupports, 'toString', {
318
- value: () => nativeSupports.toString(),
319
- configurable: true,
320
- });
321
- } catch {}
322
- try {
323
- Object.defineProperty(CSS, 'supports', {
324
- value: patchedSupports,
325
- configurable: true,
326
- writable: true,
327
- });
328
- } catch {
329
- try { CSS.supports = patchedSupports; } catch {}
330
- }
331
- })();`;
332
- }
333
- /**
334
- * Some high-risk sites ship disable-devtool with auto bootstrap via
335
- * document.querySelector('[disable-devtool-auto]').
336
- *
337
- * Hiding only this selector prevents the default self-close/redirect behavior
338
- * without affecting normal selector queries.
339
- */
340
- function patchDisableDevtoolAutoBootstrap() {
341
- return `(function(){
342
- if (typeof Document === 'undefined') return;
343
- const AUTO_SELECTOR = '[disable-devtool-auto]';
344
- const NEVER_MATCH_SELECTOR = 'script[__ab_disable_devtool_never_match__="1"]';
345
- const normalize = (value) => {
346
- if (typeof value !== 'string') return '';
347
- return value.replace(/\\s+/g, '').toLowerCase();
348
- };
349
- const shouldHideSelector = (selector) => normalize(selector) === AUTO_SELECTOR;
350
-
351
- const patchQueryMethod = (proto, method) => {
352
- if (!proto) return;
353
- const native = proto[method];
354
- if (typeof native !== 'function') return;
355
-
356
- const wrapped = function(selector, ...args) {
357
- if (shouldHideSelector(selector)) {
358
- return native.call(this, NEVER_MATCH_SELECTOR, ...args);
359
- }
360
- return native.call(this, selector, ...args);
361
- };
362
-
363
- try {
364
- Object.defineProperty(wrapped, 'name', {
365
- value: native.name,
366
- configurable: true,
367
- });
368
- Object.defineProperty(wrapped, 'toString', {
369
- value: () => native.toString(),
370
- configurable: true,
371
- });
372
- } catch {}
373
-
374
- try {
375
- Object.defineProperty(proto, method, {
376
- value: wrapped,
377
- configurable: true,
378
- writable: true,
379
- });
380
- } catch {}
381
- };
382
-
383
- patchQueryMethod(Document.prototype, 'querySelector');
384
- patchQueryMethod(Document.prototype, 'querySelectorAll');
385
- if (typeof Element !== 'undefined') {
386
- patchQueryMethod(Element.prototype, 'querySelector');
387
- patchQueryMethod(Element.prototype, 'querySelectorAll');
388
- }
389
- })();`;
390
- }
391
- /**
392
- * Remove navigator.webdriver entirely.
393
- * Modern detection checks both value and property presence (`'webdriver' in navigator`).
394
- */
395
- function patchNavigatorWebdriver() {
396
- return `(function(){
397
- const removeWebdriver = (target) => {
398
- if (!target) return;
399
- try { delete target.webdriver; } catch {}
400
- };
401
- removeWebdriver(navigator);
402
- removeWebdriver(Object.getPrototypeOf(navigator));
403
- removeWebdriver(Navigator.prototype);
404
- if (typeof WorkerNavigator !== 'undefined') {
405
- removeWebdriver(WorkerNavigator.prototype);
406
- }
407
- })();`;
408
- }
409
- /**
410
- * Ensure window.chrome and window.chrome.runtime exist.
411
- * Headless Chrome (and Playwright) omit chrome.runtime which is a dead giveaway.
412
- */
413
- function patchChromeRuntime() {
414
- return `(function(){
415
- const chromeObject = ('chrome' in window && window.chrome) ? window.chrome : {};
416
- if (!('chrome' in window)) {
417
- try {
418
- Object.defineProperty(Window.prototype, 'chrome', {
419
- get: () => chromeObject,
420
- configurable: true,
421
- });
422
- } catch {
423
- try { Object.defineProperty(window, 'chrome', { value: chromeObject, configurable: true }); } catch {}
424
- }
425
- }
426
- if (!chromeObject.runtime) {
427
- const makeEvent = () => ({
428
- addListener: () => {},
429
- removeListener: () => {},
430
- hasListener: () => false,
431
- hasListeners: () => false,
432
- dispatch: () => {},
433
- });
434
- const makePort = () => ({
435
- name: '',
436
- sender: undefined,
437
- disconnect: () => {},
438
- onDisconnect: makeEvent(),
439
- onMessage: makeEvent(),
440
- postMessage: () => {},
441
- });
442
- const runtime = {
443
- id: undefined,
444
- connect: () => makePort(),
445
- sendMessage: () => undefined,
446
- onConnect: makeEvent(),
447
- onMessage: makeEvent(),
448
- };
449
- Object.defineProperty(chromeObject, 'runtime', {
450
- value: runtime,
451
- configurable: true,
452
- });
453
- }
454
- })();`;
455
- }
456
- /**
457
- * Add deprecated-but-still-probed Chrome APIs: chrome.app, chrome.csi, chrome.loadTimes.
458
- */
459
- function patchChromeLegacyApis() {
460
- return `(function(){
461
- const chromeObject = ('chrome' in window && window.chrome) ? window.chrome : null;
462
- if (!chromeObject) return;
463
- const nativeNow = Date.now;
464
- const nativeToString = Function.prototype.toString;
465
- const timing = window.performance && window.performance.timing ? window.performance.timing : null;
466
- const getNavigationEntry = () => {
467
- try {
468
- return performance.getEntriesByType('navigation')[0] || { nextHopProtocol: 'h2', type: 'other' };
469
- } catch {
470
- return { nextHopProtocol: 'h2', type: 'other' };
471
- }
472
- };
473
- const defineValue = (target, key, value) => {
474
- try {
475
- Object.defineProperty(target, key, {
476
- value,
477
- configurable: true,
478
- writable: true,
479
- });
480
- return true;
481
- } catch {
482
- return false;
483
- }
484
- };
485
- const patchFunctionShape = (fn, name) => {
486
- try {
487
- Object.defineProperty(fn, 'name', { value: name, configurable: true });
488
- Object.defineProperty(fn, 'toString', {
489
- value: () => nativeToString.call(nativeNow).replace('now', name),
490
- configurable: true,
491
- });
492
- } catch {}
493
- };
494
-
495
- if (!('app' in chromeObject)) {
496
- const invokeError = (name) => new TypeError('Error in invocation of app.' + name + '()');
497
- const app = {
498
- isInstalled: false,
499
- InstallState: {
500
- DISABLED: 'disabled',
501
- INSTALLED: 'installed',
502
- NOT_INSTALLED: 'not_installed',
503
- },
504
- RunningState: {
505
- CANNOT_RUN: 'cannot_run',
506
- READY_TO_RUN: 'ready_to_run',
507
- RUNNING: 'running',
508
- },
509
- getDetails: function getDetails() {
510
- if (arguments.length) throw invokeError('getDetails');
511
- return null;
512
- },
513
- getIsInstalled: function getIsInstalled() {
514
- if (arguments.length) throw invokeError('getIsInstalled');
515
- return false;
516
- },
517
- runningState: function runningState() {
518
- if (arguments.length) throw invokeError('runningState');
519
- return 'cannot_run';
520
- },
521
- };
522
- defineValue(chromeObject, 'app', app);
523
- }
524
-
525
- if (!('csi' in chromeObject) && timing) {
526
- const csi = function csi() {
527
- return {
528
- onloadT: timing.domContentLoadedEventEnd,
529
- startE: timing.navigationStart,
530
- pageT: Date.now() - timing.navigationStart,
531
- tran: 15,
532
- };
533
- };
534
- patchFunctionShape(csi, 'csi');
535
- defineValue(chromeObject, 'csi', csi);
536
- }
537
-
538
- if (!('loadTimes' in chromeObject) && timing) {
539
- const toFixed = (num, fixed) => {
540
- const matcher = new RegExp('^-?\\\\d+(?:.\\\\d{0,' + (fixed || -1) + '})?');
541
- const match = String(num).match(matcher);
542
- return match ? match[0] : String(num);
543
- };
544
- const loadTimes = function loadTimes() {
545
- const navigationEntry = getNavigationEntry();
546
- const nextHopProtocol = navigationEntry.nextHopProtocol || 'h2';
547
- let firstPaint = timing.loadEventEnd / 1000;
548
- try {
549
- const paintEntries = performance.getEntriesByType('paint');
550
- if (paintEntries && paintEntries[0] && typeof paintEntries[0].startTime === 'number') {
551
- firstPaint = (paintEntries[0].startTime + performance.timeOrigin) / 1000;
552
- }
553
- } catch {}
554
- return {
555
- connectionInfo: nextHopProtocol,
556
- npnNegotiatedProtocol: ['h2', 'hq'].includes(nextHopProtocol) ? nextHopProtocol : 'unknown',
557
- navigationType: navigationEntry.type || 'other',
558
- wasAlternateProtocolAvailable: false,
559
- wasFetchedViaSpdy: ['h2', 'hq'].includes(nextHopProtocol),
560
- wasNpnNegotiated: ['h2', 'hq'].includes(nextHopProtocol),
561
- firstPaintAfterLoadTime: 0,
562
- requestTime: timing.navigationStart / 1000,
563
- startLoadTime: timing.navigationStart / 1000,
564
- commitLoadTime: timing.responseStart / 1000,
565
- finishDocumentLoadTime: timing.domContentLoadedEventEnd / 1000,
566
- finishLoadTime: timing.loadEventEnd / 1000,
567
- firstPaintTime: toFixed(firstPaint, 3),
568
- };
569
- };
570
- patchFunctionShape(loadTimes, 'loadTimes');
571
- defineValue(chromeObject, 'loadTimes', loadTimes);
572
- }
573
- })();`;
574
- }
575
- /**
576
- * Fix srcdoc iframe.contentWindow signals used by classic HEADCHR_IFRAME checks.
577
- * We only intercept iframe creation and srcdoc assignment to keep impact minimal.
578
- */
579
- function patchIframeContentWindow() {
580
- return `(function(){
581
- if (typeof document === 'undefined' || typeof document.createElement !== 'function') return;
582
- const nativeCreateElement = document.createElement.bind(document);
583
- const nativeSrcdocDescriptor =
584
- typeof HTMLIFrameElement !== 'undefined'
585
- ? Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, 'srcdoc')
586
- : null;
587
- const srcdocGetter = nativeSrcdocDescriptor && nativeSrcdocDescriptor.get;
588
- const srcdocSetter = nativeSrcdocDescriptor && nativeSrcdocDescriptor.set;
589
- const iframeProxyMap = new WeakMap();
590
- const patchedIframes = new WeakSet();
591
-
592
- const ensureContentWindowProxy = (iframe) => {
593
- if (!iframe || iframeProxyMap.has(iframe)) return;
594
- try {
595
- if (iframe.contentWindow) return;
596
- } catch {}
597
- const proxy = new Proxy(window, {
598
- get(target, key) {
599
- if (key === 'self') return proxy;
600
- if (key === 'frameElement') return iframe;
601
- if (key === '0') return undefined;
602
- return Reflect.get(target, key, target);
603
- },
604
- });
605
- iframeProxyMap.set(iframe, proxy);
606
- try {
607
- Object.defineProperty(iframe, 'contentWindow', {
608
- get: () => proxy,
609
- set: () => undefined,
610
- enumerable: true,
611
- configurable: false,
612
- });
613
- } catch {}
614
- };
615
-
616
- const patchIframeSrcdoc = (iframe) => {
617
- if (!iframe || patchedIframes.has(iframe)) return;
618
- patchedIframes.add(iframe);
619
- try {
620
- Object.defineProperty(iframe, 'srcdoc', {
621
- configurable: true,
622
- get() {
623
- if (typeof srcdocGetter === 'function') {
624
- return srcdocGetter.call(this);
625
- }
626
- return '';
627
- },
628
- set(value) {
629
- ensureContentWindowProxy(this);
630
- if (typeof srcdocSetter === 'function') {
631
- srcdocSetter.call(this, value);
632
- } else {
633
- this.setAttribute('srcdoc', String(value ?? ''));
634
- }
635
- },
636
- });
637
- } catch {}
638
- };
639
-
640
- const patchedCreateElement = function(...args) {
641
- const element = nativeCreateElement(...args);
642
- try {
643
- const name = args && args.length > 0 ? String(args[0]).toLowerCase() : '';
644
- if (name === 'iframe') {
645
- patchIframeSrcdoc(element);
646
- }
647
- } catch {}
648
- return element;
649
- };
650
- try {
651
- Object.defineProperty(patchedCreateElement, 'name', {
652
- value: 'createElement',
653
- configurable: true,
654
- });
655
- Object.defineProperty(patchedCreateElement, 'toString', {
656
- value: () => nativeCreateElement.toString(),
657
- configurable: true,
658
- });
659
- } catch {}
660
- try {
661
- Object.defineProperty(document, 'createElement', {
662
- value: patchedCreateElement,
663
- configurable: true,
664
- writable: true,
665
- });
666
- } catch {
667
- try { document.createElement = patchedCreateElement; } catch {}
668
- }
669
- })();`;
670
- }
671
- /**
672
- * Keep navigator.language + navigator.languages aligned with launch locale.
673
- */
674
- function patchNavigatorLanguages() {
675
- return `(function(){
676
- const config = (typeof __abStealth === 'object' && __abStealth) ? __abStealth : null;
677
- if (!config || !Array.isArray(config.languages) || config.languages.length === 0) return;
678
- const locale = typeof config.locale === 'string' ? config.locale : config.languages[0];
679
- try {
680
- Object.defineProperty(navigator, 'language', {
681
- get: () => locale,
682
- configurable: true,
683
- });
684
- } catch {}
685
- try {
686
- Object.defineProperty(navigator, 'languages', {
687
- get: () => config.languages.slice(),
688
- configurable: true,
689
- });
690
- } catch {}
691
- })();`;
692
- }
693
- /**
694
- * Keep navigator.vendor aligned with regular Chrome.
695
- */
696
- function patchNavigatorVendor() {
697
- return `(function(){
698
- const ua = String(navigator.userAgent || '');
699
- if (!/Chrome\\//.test(ua) || /Firefox\\//.test(ua)) return;
700
- const target = 'Google Inc.';
701
- const proto = Object.getPrototypeOf(navigator);
702
- try {
703
- if (navigator.vendor === target) return;
704
- } catch {}
705
- const defineVendor = (targetObj) => {
706
- if (!targetObj) return false;
707
- try {
708
- Object.defineProperty(targetObj, 'vendor', {
709
- get: () => target,
710
- configurable: true,
711
- });
712
- return true;
713
- } catch {
714
- return false;
715
- }
716
- };
717
- if (defineVendor(proto)) {
718
- try { delete (navigator).vendor; } catch {}
719
- return;
720
- }
721
- defineVendor(navigator);
722
- })();`;
723
- }
724
- /**
725
- * Inject realistic navigator.plugins and navigator.mimeTypes arrays.
726
- * Headless Chrome reports an empty PluginArray; real Chrome always has a few.
727
- */
728
- function patchNavigatorPluginsAndMimeTypes() {
729
- return `(function(){
730
- const makeMimeType = (type, suffixes, description) => {
731
- const mime = Object.create(MimeType.prototype);
732
- Object.defineProperties(mime, {
733
- type: { value: type, enumerable: true },
734
- suffixes: { value: suffixes, enumerable: true },
735
- description: { value: description, enumerable: true },
736
- enabledPlugin: { value: null, writable: true, enumerable: true },
737
- });
738
- return mime;
739
- };
740
-
741
- const makePlugin = (name, description, filename, mimes) => {
742
- const plugin = Object.create(Plugin.prototype);
743
- Object.defineProperties(plugin, {
744
- name: { value: name, enumerable: true },
745
- description: { value: description, enumerable: true },
746
- filename: { value: filename, enumerable: true },
747
- length: { value: mimes.length, enumerable: true },
748
- });
749
- mimes.forEach((mime, i) => {
750
- Object.defineProperty(plugin, i, {
751
- value: mime,
752
- enumerable: true,
753
- });
754
- Object.defineProperty(plugin, mime.type, {
755
- value: mime,
756
- enumerable: false,
757
- });
758
- try { mime.enabledPlugin = plugin; } catch {}
759
- });
760
- return plugin;
761
- };
762
-
763
- const pdfMime = makeMimeType('application/pdf', 'pdf', 'Portable Document Format');
764
- const chromePdfMime = makeMimeType(
765
- 'application/x-google-chrome-pdf',
766
- 'pdf',
767
- 'Portable Document Format'
768
- );
769
- const naclMime = makeMimeType('application/x-nacl', '', 'Native Client Executable');
770
- const pnaclMime = makeMimeType('application/x-pnacl', '', 'Portable Native Client Executable');
771
-
772
- const plugins = [
773
- makePlugin('Chrome PDF Plugin', 'Portable Document Format', 'internal-pdf-viewer', [chromePdfMime]),
774
- makePlugin('Chrome PDF Viewer', '', 'mhjfbmdgcfjbbpaeojofohoefgiehjai', [pdfMime]),
775
- makePlugin('Native Client', '', 'internal-nacl-plugin', [naclMime, pnaclMime]),
776
- ];
777
- const pluginArray = Object.create(PluginArray.prototype);
778
- plugins.forEach((p, i) => {
779
- pluginArray[i] = p;
780
- pluginArray[p.name] = p;
781
- });
782
- Object.defineProperty(pluginArray, 'length', { get: () => plugins.length });
783
- pluginArray.item = (i) => plugins[i] || null;
784
- pluginArray.namedItem = (name) => plugins.find(p => p.name === name) || null;
785
- pluginArray.refresh = () => {};
786
- pluginArray[Symbol.iterator] = function*() { for (const p of plugins) yield p; };
787
-
788
- const mimeTypes = [chromePdfMime, pdfMime, naclMime, pnaclMime];
789
- const mimeTypeArray = Object.create(MimeTypeArray.prototype);
790
- mimeTypes.forEach((m, i) => {
791
- mimeTypeArray[i] = m;
792
- mimeTypeArray[m.type] = m;
793
- });
794
- Object.defineProperty(mimeTypeArray, 'length', { get: () => mimeTypes.length });
795
- mimeTypeArray.item = (i) => mimeTypes[i] || null;
796
- mimeTypeArray.namedItem = (name) => mimeTypes.find(m => m.type === name) || null;
797
- mimeTypeArray[Symbol.iterator] = function*() { for (const m of mimeTypes) yield m; };
798
-
799
- Object.defineProperty(navigator, 'plugins', {
800
- get: () => pluginArray,
801
- configurable: true,
802
- });
803
- Object.defineProperty(navigator, 'mimeTypes', {
804
- get: () => mimeTypeArray,
805
- configurable: true,
806
- });
807
- })();`;
808
- }
809
- /**
810
- * navigator.permissions.query({name:'notifications'}) should resolve to
811
- * 'denied' in a normal browser, but Playwright throws or returns 'prompt'.
812
- */
813
- function patchNavigatorPermissions() {
814
- return `(function(){
815
- if (!navigator.permissions || !navigator.permissions.query) return;
816
- const origQuery = navigator.permissions.query.bind(navigator.permissions);
817
- const makePermissionStatus = (state) => {
818
- if (typeof PermissionStatus !== 'undefined') {
819
- const status = Object.create(PermissionStatus.prototype);
820
- Object.defineProperty(status, 'state', {
821
- value: state,
822
- writable: false,
823
- enumerable: true,
824
- });
825
- Object.defineProperty(status, 'onchange', {
826
- value: null,
827
- writable: true,
828
- enumerable: true,
829
- });
830
- return status;
831
- }
832
- return { state, onchange: null };
833
- };
834
- const patchedQuery = new Proxy(origQuery, {
835
- apply(target, thisArg, argList) {
836
- const params = argList && argList[0];
837
- if (params && params.name === 'notifications') {
838
- const state = (typeof Notification !== 'undefined' && Notification.permission) || 'default';
839
- return Promise.resolve(makePermissionStatus(state));
840
- }
841
- return Reflect.apply(target, navigator.permissions, argList);
842
- }
843
- });
844
- try {
845
- Object.defineProperty(navigator.permissions, 'query', {
846
- value: patchedQuery,
847
- configurable: true,
848
- writable: true,
849
- });
850
- } catch {}
851
- })();`;
852
- }
853
- /**
854
- * WebGL vendor/renderer: headless Chrome uses SwiftShader which is distinctive.
855
- * Patch getParameter to return Intel GPU strings when SwiftShader is detected.
856
- */
857
- function patchWebGLVendor() {
858
- return `(function(){
859
- const getCtx = HTMLCanvasElement.prototype.getContext;
860
- const WEBGL_VENDOR = 'Intel Inc.';
861
- const WEBGL_RENDERER = 'Intel Iris OpenGL Engine';
862
- const DEBUG_RENDERER_INFO = {
863
- UNMASKED_VENDOR_WEBGL: 0x9245,
864
- UNMASKED_RENDERER_WEBGL: 0x9246,
865
- };
866
-
867
- const createFallbackWebGLContext = (canvas, requestedType) => {
868
- const isWebGL2 = requestedType === 'webgl2';
869
- const ctx = {
870
- __abFallbackWebGLContext: true,
871
- canvas,
872
- drawingBufferWidth: canvas.width || 300,
873
- drawingBufferHeight: canvas.height || 150,
874
- VENDOR: 0x1F00,
875
- RENDERER: 0x1F01,
876
- VERSION: 0x1F02,
877
- SHADING_LANGUAGE_VERSION: 0x8B8C,
878
- getExtension(name) {
879
- if (name === 'WEBGL_debug_renderer_info') return DEBUG_RENDERER_INFO;
880
- return null;
881
- },
882
- getSupportedExtensions() {
883
- return ['WEBGL_debug_renderer_info'];
884
- },
885
- getContextAttributes() {
886
- return {
887
- alpha: true,
888
- antialias: true,
889
- depth: true,
890
- desynchronized: false,
891
- failIfMajorPerformanceCaveat: false,
892
- powerPreference: 'default',
893
- premultipliedAlpha: true,
894
- preserveDrawingBuffer: false,
895
- stencil: false,
896
- };
897
- },
898
- getParameter(param) {
899
- if (param === DEBUG_RENDERER_INFO.UNMASKED_VENDOR_WEBGL || param === this.VENDOR) {
900
- return WEBGL_VENDOR;
901
- }
902
- if (param === DEBUG_RENDERER_INFO.UNMASKED_RENDERER_WEBGL || param === this.RENDERER) {
903
- return WEBGL_RENDERER;
904
- }
905
- if (param === this.VERSION) {
906
- return isWebGL2
907
- ? 'WebGL 2.0 (OpenGL ES 3.0 Chromium)'
908
- : 'WebGL 1.0 (OpenGL ES 2.0 Chromium)';
909
- }
910
- if (param === this.SHADING_LANGUAGE_VERSION) {
911
- return isWebGL2
912
- ? 'WebGL GLSL ES 3.00 (OpenGL ES GLSL ES 3.0 Chromium)'
913
- : 'WebGL GLSL ES 1.0 (OpenGL ES GLSL ES 1.0 Chromium)';
914
- }
915
- return 0;
916
- },
917
- getError() { return 0; },
918
- clear() {},
919
- clearColor() {},
920
- createBuffer() { return {}; },
921
- bindBuffer() {},
922
- bufferData() {},
923
- createProgram() { return {}; },
924
- createShader() { return {}; },
925
- shaderSource() {},
926
- compileShader() {},
927
- attachShader() {},
928
- linkProgram() {},
929
- useProgram() {},
930
- viewport() {},
931
- drawArrays() {},
932
- readPixels() {},
933
- finish() {},
934
- flush() {},
935
- };
936
- try {
937
- const proto =
938
- requestedType === 'webgl2' && typeof WebGL2RenderingContext !== 'undefined'
939
- ? WebGL2RenderingContext.prototype
940
- : typeof WebGLRenderingContext !== 'undefined'
941
- ? WebGLRenderingContext.prototype
942
- : null;
943
- if (proto) Object.setPrototypeOf(ctx, proto);
944
- } catch {}
945
- return ctx;
946
- };
947
-
948
- HTMLCanvasElement.prototype.getContext = function(type, attrs) {
949
- const ctx = getCtx.call(this, type, attrs);
950
- if (
951
- (type === 'webgl' || type === 'webgl2' || type === 'experimental-webgl') &&
952
- !ctx &&
953
- __abStealth &&
954
- __abStealth.allowWebGLContextFallback === true
955
- ) {
956
- return createFallbackWebGLContext(this, type);
957
- }
958
- if (ctx && (type === 'webgl' || type === 'webgl2' || type === 'experimental-webgl')) {
959
- const origGetParameter = ctx.getParameter.bind(ctx);
960
- ctx.getParameter = function(param) {
961
- const ext = ctx.getExtension('WEBGL_debug_renderer_info');
962
- if (ext) {
963
- if (param === ext.UNMASKED_VENDOR_WEBGL) {
964
- const real = origGetParameter(param);
965
- return (real && real.includes('SwiftShader')) ? WEBGL_VENDOR : real;
966
- }
967
- if (param === ext.UNMASKED_RENDERER_WEBGL) {
968
- const real = origGetParameter(param);
969
- return (real && real.includes('SwiftShader')) ? WEBGL_RENDERER : real;
970
- }
971
- }
972
- if (param === ctx.VENDOR) return WEBGL_VENDOR;
973
- if (param === ctx.RENDERER) return WEBGL_RENDERER;
974
- return origGetParameter(param);
975
- };
976
- }
977
- return ctx;
978
- };
979
- })();`;
980
- }
981
- /**
982
- * Remove Playwright's injected cdc_ (Chrome DevTools) properties on document.
983
- * Some older detection scripts look for these on the document element.
984
- */
985
- function patchCdcProperties() {
986
- return `(function(){
987
- const clean = (target) => {
988
- for (const key of Object.keys(target)) {
989
- if (/^cdc_|^\\$cdc_/.test(key)) {
990
- delete target[key];
991
- }
992
- }
993
- };
994
- clean(document);
995
- if (document.documentElement) clean(document.documentElement);
996
- })();`;
997
- }
998
- /**
999
- * Remove Playwright/Puppeteer sourceURL tokens from error stacks.
1000
- * This mirrors the intent of the sourceurl evasion: reduce obvious
1001
- * automation-only script labels in stack traces.
1002
- */
1003
- function patchSourceUrlStackTraces() {
1004
- return `(function(){
1005
- if (typeof Error === 'undefined') return;
1006
- const sanitizeStack = (value) => {
1007
- if (typeof value !== 'string') return value;
1008
- let stack = value;
1009
- stack = stack.replace(/\\/\\/# sourceURL=.*$/gm, '');
1010
- stack = stack.replace(/__playwright_evaluation_script__/g, '<anonymous>');
1011
- stack = stack.replace(/__puppeteer_evaluation_script__/g, '<anonymous>');
1012
- stack = stack.replace(/__pw_evaluation_script__/g, '<anonymous>');
1013
- return stack;
1014
- };
1015
-
1016
- const nativePrepare = Error.prepareStackTrace;
1017
- Error.prepareStackTrace = function(error, structuredStackTrace) {
1018
- let stackString;
1019
- if (typeof nativePrepare === 'function') {
1020
- stackString = nativePrepare.call(this, error, structuredStackTrace);
1021
- } else {
1022
- const name = error && error.name ? String(error.name) : 'Error';
1023
- const message = error && error.message ? String(error.message) : '';
1024
- const header = message ? name + ': ' + message : name;
1025
- const frames = Array.isArray(structuredStackTrace)
1026
- ? structuredStackTrace.map((frame) => ' at ' + String(frame))
1027
- : [];
1028
- stackString = [header].concat(frames).join('\\n');
1029
- }
1030
- return sanitizeStack(String(stackString));
1031
- };
1032
-
1033
- if (typeof Error.captureStackTrace === 'function') {
1034
- const nativeCapture = Error.captureStackTrace;
1035
- Error.captureStackTrace = function(targetObject, constructorOpt) {
1036
- nativeCapture.call(this, targetObject, constructorOpt);
1037
- try {
1038
- const stack = targetObject && targetObject.stack;
1039
- if (typeof stack === 'string') {
1040
- Object.defineProperty(targetObject, 'stack', {
1041
- value: sanitizeStack(stack),
1042
- configurable: true,
1043
- writable: true,
1044
- });
1045
- }
1046
- } catch {}
1047
- };
1048
- }
1049
- })();`;
1050
- }
1051
- /**
1052
- * contentWindow on cross-origin iframes: Playwright sometimes returns null
1053
- * where real browsers return a (restricted) Window object.
1054
- */
1055
- function patchWindowDimensions() {
1056
- return `(function(){
1057
- const widthDelta = 12;
1058
- const heightDelta = 74;
1059
- const patchWidth =
1060
- !Number.isFinite(window.outerWidth) ||
1061
- window.outerWidth === 0 ||
1062
- Math.abs(window.outerWidth - window.innerWidth) <= 1;
1063
- const patchHeight =
1064
- !Number.isFinite(window.outerHeight) ||
1065
- window.outerHeight === 0 ||
1066
- Math.abs(window.outerHeight - window.innerHeight) <= 1;
1067
- if (patchWidth) {
1068
- try {
1069
- Object.defineProperty(window, 'outerWidth', {
1070
- get: () => Math.max(window.innerWidth + widthDelta, window.innerWidth),
1071
- configurable: true,
1072
- });
1073
- } catch {}
1074
- }
1075
- if (patchHeight) {
1076
- try {
1077
- Object.defineProperty(window, 'outerHeight', {
1078
- get: () => Math.max(window.innerHeight + heightDelta, window.innerHeight),
1079
- configurable: true,
1080
- });
1081
- } catch {}
1082
- }
1083
- const patchScreenPosition =
1084
- (!Number.isFinite(window.screenX) || !Number.isFinite(window.screenY)) ||
1085
- (window.screenX === 0 && window.screenY === 0 && (patchWidth || patchHeight));
1086
- if (patchScreenPosition) {
1087
- try {
1088
- Object.defineProperty(window, 'screenX', {
1089
- get: () => 16,
1090
- configurable: true,
1091
- });
1092
- Object.defineProperty(window, 'screenY', {
1093
- get: () => 72,
1094
- configurable: true,
1095
- });
1096
- Object.defineProperty(window, 'screenLeft', {
1097
- get: () => 16,
1098
- configurable: true,
1099
- });
1100
- Object.defineProperty(window, 'screenTop', {
1101
- get: () => 72,
1102
- configurable: true,
1103
- });
1104
- } catch {}
1105
- }
1106
- })();`;
1107
- }
1108
- /**
1109
- * Make Screen avail* values look like a desktop with taskbar/menu bar reserved space.
1110
- */
1111
- function patchScreenAvailability() {
1112
- return `(function(){
1113
- const patchNumber = (target, key, value) => {
1114
- try {
1115
- Object.defineProperty(target, key, {
1116
- get: () => value,
1117
- configurable: true,
1118
- });
1119
- } catch {}
1120
- };
1121
- const availWidth = Number(screen.availWidth);
1122
- const availHeight = Number(screen.availHeight);
1123
- const width = Number(screen.width);
1124
- const height = Number(screen.height);
1125
- if (Number.isFinite(width) && Number.isFinite(availWidth) && availWidth >= width) {
1126
- patchNumber(screen, 'availWidth', Math.max(width - 8, 0));
1127
- }
1128
- if (Number.isFinite(height) && Number.isFinite(availHeight) && availHeight >= height) {
1129
- patchNumber(screen, 'availHeight', Math.max(height - 40, 0));
1130
- }
1131
- if (Number.isFinite(screen.availLeft) && screen.availLeft === 0) {
1132
- patchNumber(screen, 'availLeft', 0);
1133
- }
1134
- if (Number.isFinite(screen.availTop) && screen.availTop === 0) {
1135
- patchNumber(screen, 'availTop', 24);
1136
- }
1137
- })();`;
1138
- }
1139
- /**
1140
- * Avoid screen == viewport fingerprints common in headless defaults.
1141
- */
1142
- function patchScreenDimensions() {
1143
- return `(function(){
1144
- const patchNumber = (target, key, value) => {
1145
- try {
1146
- Object.defineProperty(target, key, {
1147
- get: () => value,
1148
- configurable: true,
1149
- });
1150
- } catch {}
1151
- };
1152
- const width = Number(screen.width);
1153
- const height = Number(screen.height);
1154
- const innerWidth = Number(window.innerWidth);
1155
- const innerHeight = Number(window.innerHeight);
1156
- if (
1157
- Number.isFinite(width) &&
1158
- Number.isFinite(height) &&
1159
- Number.isFinite(innerWidth) &&
1160
- Number.isFinite(innerHeight) &&
1161
- width === innerWidth &&
1162
- height === innerHeight
1163
- ) {
1164
- patchNumber(screen, 'width', Math.max(innerWidth + 86, 1366));
1165
- patchNumber(screen, 'height', Math.max(innerHeight + 48, 768));
1166
- }
1167
- })();`;
1168
- }
1169
- /**
1170
- * navigator.hardwareConcurrency: headless often reports 2 (CI);
1171
- * real desktops typically have >= 4 cores.
1172
- */
1173
- function patchNavigatorHardwareConcurrency() {
1174
- return `(function(){
1175
- if (navigator.hardwareConcurrency < 4) {
1176
- Object.defineProperty(navigator, 'hardwareConcurrency', {
1177
- get: () => 4,
1178
- configurable: true,
1179
- });
1180
- }
1181
- })();`;
1182
- }
1183
- /**
1184
- * Keep notifications in "default" state instead of denied-by-default headless behavior.
1185
- */
1186
- function patchNotificationPermission() {
1187
- return `(function(){
1188
- if (typeof Notification === 'undefined') return;
1189
- const current = Notification.permission;
1190
- if (current === 'granted') return;
1191
- try {
1192
- Object.defineProperty(Notification, 'permission', {
1193
- get: () => 'default',
1194
- configurable: true,
1195
- });
1196
- } catch {}
1197
- })();`;
1198
- }
1199
- /**
1200
- * CreepJS probes `background-color: ActiveText` and flags Chromium when the
1201
- * computed value resolves to `rgb(255, 0, 0)`. Rewrite only that exact probe.
1202
- */
1203
- function patchActiveTextColorHeuristic() {
1204
- return `(function(){
1205
- if (typeof Element === 'undefined' || !Element.prototype) return;
1206
- const nativeSetAttribute = Element.prototype.setAttribute;
1207
- if (typeof nativeSetAttribute !== 'function') return;
1208
- const normalize = (value) => String(value).replace(/\\s+/g, ' ').trim().toLowerCase();
1209
- const probeStyle = 'background-color: activetext';
1210
- const replacement = 'background-color: rgb(0, 0, 0)';
1211
- const patchedSetAttribute = function(name, value) {
1212
- if (String(name).toLowerCase() === 'style' && normalize(value) === probeStyle) {
1213
- return nativeSetAttribute.call(this, name, replacement);
1214
- }
1215
- return nativeSetAttribute.call(this, name, value);
1216
- };
1217
- try {
1218
- Object.defineProperty(patchedSetAttribute, 'name', {
1219
- value: 'setAttribute',
1220
- configurable: true,
1221
- });
1222
- Object.defineProperty(patchedSetAttribute, 'toString', {
1223
- value: () => nativeSetAttribute.toString(),
1224
- configurable: true,
1225
- });
1226
- } catch {}
1227
- try {
1228
- Object.defineProperty(Element.prototype, 'setAttribute', {
1229
- value: patchedSetAttribute,
1230
- configurable: true,
1231
- writable: true,
1232
- });
1233
- } catch {
1234
- try { Element.prototype.setAttribute = patchedSetAttribute; } catch {}
1235
- }
1236
- })();`;
1237
- }
1238
- /**
1239
- * Add missing connection.downlinkMax in Chromium headless environments.
1240
- */
1241
- function patchNavigatorConnection() {
1242
- return `(function(){
1243
- if (!navigator.connection) return;
1244
- const conn = navigator.connection;
1245
- if (typeof conn.downlinkMax === 'number') return;
1246
- const defineDownlinkMax = (target) => {
1247
- if (!target) return false;
1248
- try {
1249
- Object.defineProperty(target, 'downlinkMax', {
1250
- get: () => 10,
1251
- configurable: true,
1252
- });
1253
- return true;
1254
- } catch {
1255
- return false;
1256
- }
1257
- };
1258
- try {
1259
- const proto = Object.getPrototypeOf(conn);
1260
- if (defineDownlinkMax(proto)) {
1261
- try { delete conn.downlinkMax; } catch {}
1262
- return;
1263
- }
1264
- } catch {}
1265
- defineDownlinkMax(conn);
1266
- })();`;
1267
- }
1268
- /**
1269
- * Ensure same-origin dedicated workers expose navigator.connection.downlinkMax too.
1270
- * Skip cross-origin worker URLs to avoid breaking anti-bot challenge workers.
1271
- */
1272
- function patchWorkerConnection() {
1273
- return `(function(){
1274
- if (typeof Worker !== 'function') return;
1275
- const isCloudflareChallengeRuntime = (() => {
1276
- try {
1277
- const host = String(location.hostname || '').toLowerCase();
1278
- const path = String(location.pathname || '');
1279
- if (host === 'challenges.cloudflare.com') return true;
1280
- return /\\/cdn-cgi\\/challenge-platform\\//.test(path);
1281
- } catch {
1282
- return false;
1283
- }
1284
- })();
1285
- // Cloudflare challenge workers are sensitive to constructor wrapping.
1286
- // Keep native Worker behavior in this runtime to avoid importScripts(blob) failures.
1287
- if (isCloudflareChallengeRuntime) return;
1288
- const NativeWorker = Worker;
1289
- const workerPrelude = \`
1290
- (() => {
1291
- try {
1292
- if (!navigator || !navigator.connection) return;
1293
- const conn = navigator.connection;
1294
- if (typeof conn.downlinkMax === 'number') return;
1295
- const defineDownlinkMax = (target) => {
1296
- if (!target) return false;
1297
- try {
1298
- Object.defineProperty(target, 'downlinkMax', {
1299
- get: () => 10,
1300
- configurable: true,
1301
- });
1302
- return true;
1303
- } catch {
1304
- return false;
1305
- }
1306
- };
1307
- try {
1308
- const proto = Object.getPrototypeOf(conn);
1309
- if (defineDownlinkMax(proto)) {
1310
- try { delete conn.downlinkMax; } catch {}
1311
- return;
1312
- }
1313
- } catch {}
1314
- defineDownlinkMax(conn);
1315
- } catch {}
1316
- })();
1317
- \`;
1318
- const buildPatchedScript = (url, options) => {
1319
- const scriptUrl = String(url);
1320
- const isModule = options && options.type === 'module';
1321
- const loader = isModule
1322
- ? \`import \${JSON.stringify(scriptUrl)};\`
1323
- : \`importScripts(\${JSON.stringify(scriptUrl)});\`;
1324
- return \`\${workerPrelude}\\n\${loader}\`;
1325
- };
1326
- const resolveWorkerUrl = (value) => {
1327
- try {
1328
- return new URL(String(value), location.href);
1329
- } catch {
1330
- return null;
1331
- }
1332
- };
1333
- const shouldPatchWorker = (value) => {
1334
- const resolved = resolveWorkerUrl(value);
1335
- if (!resolved) return false;
1336
- if (resolved.protocol === 'blob:') return resolved.origin === location.origin;
1337
- if (resolved.protocol === 'http:' || resolved.protocol === 'https:') {
1338
- return resolved.origin === location.origin;
1339
- }
1340
- if (resolved.protocol === 'file:') return location.protocol === 'file:';
1341
- return false;
1342
- };
1343
- const WrappedWorker = function(scriptURL, options) {
1344
- if (!shouldPatchWorker(scriptURL)) {
1345
- return new NativeWorker(scriptURL, options);
1346
- }
1347
- try {
1348
- const source = buildPatchedScript(scriptURL, options);
1349
- const blob = new Blob([source], { type: 'application/javascript' });
1350
- const patchedUrl = URL.createObjectURL(blob);
1351
- const worker = new NativeWorker(patchedUrl, options);
1352
- try {
1353
- setTimeout(() => URL.revokeObjectURL(patchedUrl), 0);
1354
- } catch {}
1355
- return worker;
1356
- } catch {
1357
- return new NativeWorker(scriptURL, options);
1358
- }
1359
- };
1360
- WrappedWorker.prototype = NativeWorker.prototype;
1361
- try {
1362
- Object.setPrototypeOf(WrappedWorker, NativeWorker);
1363
- } catch {}
1364
- try {
1365
- Object.defineProperty(WrappedWorker, 'name', { value: 'Worker', configurable: true });
1366
- } catch {}
1367
- try {
1368
- Object.defineProperty(WrappedWorker, 'toString', {
1369
- value: () => NativeWorker.toString(),
1370
- configurable: true,
1371
- });
1372
- } catch {}
1373
- try {
1374
- Object.defineProperty(window, 'Worker', {
1375
- value: WrappedWorker,
1376
- configurable: true,
1377
- writable: true,
1378
- });
1379
- } catch {}
1380
- })();`;
1381
- }
1382
- /**
1383
- * CreepJS marks light-scheme defaults as a weak headless signal. Keep
1384
- * `(prefers-color-scheme: light)` neutral without affecting other media queries.
1385
- */
1386
- function patchPrefersColorSchemeHeuristic() {
1387
- return `(function(){
1388
- if (typeof window.matchMedia !== 'function') return;
1389
- const nativeMatchMedia = window.matchMedia.bind(window);
1390
- const normalize = (query) => String(query).replace(/\\s+/g, ' ').trim().toLowerCase();
1391
- const prefersLight = '(prefers-color-scheme: light)';
1392
- const patchMediaQueryList = (mql) => {
1393
- if (!mql || typeof mql !== 'object') return mql;
1394
- return new Proxy(mql, {
1395
- get(target, prop) {
1396
- if (prop === 'matches') return false;
1397
- const value = Reflect.get(target, prop, target);
1398
- if (typeof value === 'function') {
1399
- return value.bind(target);
1400
- }
1401
- return value;
1402
- },
1403
- });
1404
- };
1405
- const patchedMatchMedia = function(query) {
1406
- const mql = nativeMatchMedia(query);
1407
- if (normalize(query) === prefersLight) {
1408
- return patchMediaQueryList(mql);
1409
- }
1410
- return mql;
1411
- };
1412
- try {
1413
- Object.defineProperty(patchedMatchMedia, 'name', { value: 'matchMedia', configurable: true });
1414
- Object.defineProperty(patchedMatchMedia, 'toString', {
1415
- value: () => nativeMatchMedia.toString(),
1416
- configurable: true,
1417
- });
1418
- } catch {}
1419
- try {
1420
- Object.defineProperty(window, 'matchMedia', {
1421
- value: patchedMatchMedia,
1422
- configurable: true,
1423
- writable: true,
1424
- });
1425
- } catch {
1426
- try { window.matchMedia = patchedMatchMedia; } catch {}
1427
- }
1428
- })();`;
1429
- }
1430
- /**
1431
- * Add share/canShare APIs expected on modern Chromium desktop.
1432
- */
1433
- function patchNavigatorShare() {
1434
- return `(function(){
1435
- if (typeof navigator.share !== 'function') {
1436
- try {
1437
- Object.defineProperty(navigator, 'share', {
1438
- value: async () => undefined,
1439
- configurable: true,
1440
- });
1441
- } catch {}
1442
- }
1443
- if (typeof navigator.canShare !== 'function') {
1444
- try {
1445
- Object.defineProperty(navigator, 'canShare', {
1446
- value: () => true,
1447
- configurable: true,
1448
- });
1449
- } catch {}
1450
- }
1451
- })();`;
1452
- }
1453
- /**
1454
- * Add Contacts Manager stub to avoid "missing contacts manager" signals.
1455
- */
1456
- function patchNavigatorContacts() {
1457
- return `(function(){
1458
- const ContactsCtor = typeof ContactsManager === 'function'
1459
- ? ContactsManager
1460
- : function ContactsManager() {};
1461
- try {
1462
- Object.defineProperty(window, 'ContactsManager', {
1463
- value: ContactsCtor,
1464
- configurable: true,
1465
- });
1466
- } catch {}
1467
- const manager = Object.create(ContactsCtor.prototype || Object.prototype);
1468
- if (typeof manager.select !== 'function') {
1469
- manager.select = async () => [];
1470
- }
1471
- if (typeof manager.getProperties !== 'function') {
1472
- manager.getProperties = () => ['name', 'email', 'tel', 'address', 'icon'];
1473
- }
1474
- const defineContacts = (target) => {
1475
- if (!target) return false;
1476
- try {
1477
- Object.defineProperty(target, 'contacts', {
1478
- get: () => manager,
1479
- configurable: true,
1480
- });
1481
- return true;
1482
- } catch {
1483
- return false;
1484
- }
1485
- };
1486
- if (defineContacts(navigator)) return;
1487
- try {
1488
- defineContacts(Object.getPrototypeOf(navigator));
1489
- } catch {}
1490
- })();`;
1491
- }
1492
- /**
1493
- * Expose ContentIndex APIs expected on modern Chromium.
1494
- */
1495
- function patchContentIndex() {
1496
- return `(function(){
1497
- const ContentIndexCtor = typeof ContentIndex === 'function'
1498
- ? ContentIndex
1499
- : function ContentIndex() {};
1500
- try {
1501
- Object.defineProperty(window, 'ContentIndex', {
1502
- value: ContentIndexCtor,
1503
- configurable: true,
1504
- });
1505
- } catch {}
1506
- const index = Object.create(ContentIndexCtor.prototype || Object.prototype);
1507
- if (typeof index.add !== 'function') {
1508
- index.add = async () => undefined;
1509
- }
1510
- if (typeof index.delete !== 'function') {
1511
- index.delete = async () => undefined;
1512
- }
1513
- if (typeof index.getAll !== 'function') {
1514
- index.getAll = async () => [];
1515
- }
1516
- if (typeof ServiceWorkerRegistration === 'undefined') return;
1517
- const defineIndex = (key) => {
1518
- try {
1519
- Object.defineProperty(ServiceWorkerRegistration.prototype, key, {
1520
- get: () => index,
1521
- configurable: true,
1522
- });
1523
- return true;
1524
- } catch {
1525
- return false;
1526
- }
1527
- };
1528
- if (!('contentIndex' in ServiceWorkerRegistration.prototype)) {
1529
- defineIndex('contentIndex');
1530
- }
1531
- if (!('index' in ServiceWorkerRegistration.prototype)) {
1532
- defineIndex('index');
1533
- }
1534
- })();`;
1535
- }
1536
- /**
1537
- * Chromium exposes navigator.pdfViewerEnabled=true in normal browsing mode.
1538
- */
1539
- function patchPdfViewerEnabled() {
1540
- return `(function(){
1541
- if (navigator.pdfViewerEnabled === true) return;
1542
- try {
1543
- Object.defineProperty(navigator, 'pdfViewerEnabled', {
1544
- get: () => true,
1545
- configurable: true,
1546
- });
1547
- } catch {}
1548
- })();`;
1549
- }
1550
- /**
1551
- * Chromium headless can under-report support for common media codecs.
1552
- * Patch canPlayType for a narrow set of high-signal probes.
1553
- */
1554
- function patchMediaCodecs() {
1555
- return `(function(){
1556
- if (typeof HTMLMediaElement === 'undefined' || !HTMLMediaElement.prototype) return;
1557
- const nativeCanPlayType = HTMLMediaElement.prototype.canPlayType;
1558
- if (typeof nativeCanPlayType !== 'function') return;
1559
- const parseInput = (value) => {
1560
- const input = String(value || '').trim();
1561
- const [mimePart, codecPart] = input.split(';');
1562
- const mime = String(mimePart || '').trim().toLowerCase();
1563
- const codecs = [];
1564
- if (codecPart && codecPart.includes('codecs=')) {
1565
- const normalized = codecPart
1566
- .replace(/^[^=]*=/, '')
1567
- .replace(/^\\s*["']?/, '')
1568
- .replace(/["']?\\s*$/, '');
1569
- normalized
1570
- .split(',')
1571
- .map((codec) => codec.trim().toLowerCase())
1572
- .filter(Boolean)
1573
- .forEach((codec) => codecs.push(codec));
1574
- }
1575
- return { mime, codecs };
1576
- };
1577
- const patchedCanPlayType = function(type) {
1578
- const { mime, codecs } = parseInput(type);
1579
- if (mime === 'video/mp4' && codecs.includes('avc1.42e01e')) {
1580
- return 'probably';
1581
- }
1582
- if (mime === 'audio/x-m4a' && codecs.length === 0) {
1583
- return 'maybe';
1584
- }
1585
- if (mime === 'audio/aac' && codecs.length === 0) {
1586
- return 'probably';
1587
- }
1588
- return nativeCanPlayType.call(this, type);
1589
- };
1590
- try {
1591
- Object.defineProperty(patchedCanPlayType, 'name', {
1592
- value: 'canPlayType',
1593
- configurable: true,
1594
- });
1595
- Object.defineProperty(patchedCanPlayType, 'toString', {
1596
- value: () => nativeCanPlayType.toString(),
1597
- configurable: true,
1598
- });
1599
- } catch {}
1600
- try {
1601
- Object.defineProperty(HTMLMediaElement.prototype, 'canPlayType', {
1602
- value: patchedCanPlayType,
1603
- configurable: true,
1604
- writable: true,
1605
- });
1606
- } catch {
1607
- try { HTMLMediaElement.prototype.canPlayType = patchedCanPlayType; } catch {}
1608
- }
1609
- })();`;
1610
- }
1611
- /**
1612
- * navigator.mediaDevices.enumerateDevices should return at least some devices
1613
- * instead of an empty array (headless default).
1614
- */
1615
- function patchMediaDevices() {
1616
- return `(function(){
1617
- if (!navigator.mediaDevices) return;
1618
- const orig = navigator.mediaDevices.enumerateDevices;
1619
- if (!orig) return;
1620
- navigator.mediaDevices.enumerateDevices = async function() {
1621
- const devices = await orig.call(navigator.mediaDevices);
1622
- if (devices.length === 0) {
1623
- return [
1624
- { deviceId: 'default', kind: 'audioinput', label: '', groupId: 'default' },
1625
- { deviceId: 'default', kind: 'videoinput', label: '', groupId: 'default' },
1626
- { deviceId: 'default', kind: 'audiooutput', label: '', groupId: 'default' },
1627
- ];
1628
- }
1629
- return devices;
1630
- };
1631
- })();`;
1632
- }
1633
- /**
1634
- * Replace "HeadlessChrome" with "Chrome" in navigator.userAgent so
1635
- * UA-based detection is bypassed at the JavaScript level.
1636
- */
1637
- function patchUserAgent() {
1638
- return `(function(){
1639
- const ua = navigator.userAgent;
1640
- if (ua.includes('HeadlessChrome')) {
1641
- const patched = ua.replace(/HeadlessChrome/g, 'Chrome');
1642
- Object.defineProperty(navigator, 'userAgent', {
1643
- get: () => patched,
1644
- configurable: true,
1645
- });
1646
- Object.defineProperty(navigator, 'appVersion', {
1647
- get: () => patched.replace('Mozilla/', ''),
1648
- configurable: true,
1649
- });
1650
- }
1651
- })();`;
1652
- }
1653
- /**
1654
- * Ensure userAgentData does not expose "HeadlessChrome" brand tokens.
1655
- */
1656
- function patchUserAgentData() {
1657
- return `(function(){
1658
- const uaData = navigator.userAgentData;
1659
- if (!uaData) return;
1660
- const sanitizeBrand = (brand) => {
1661
- if (typeof brand !== 'string') return brand;
1662
- return brand.replace(/HeadlessChrome/gi, 'Google Chrome');
1663
- };
1664
- const patchBrandList = (value) => {
1665
- if (!Array.isArray(value)) return value;
1666
- return value.map((entry) => ({
1667
- ...entry,
1668
- brand: sanitizeBrand(entry.brand),
1669
- }));
1670
- };
1671
- const patched = Object.create(Object.getPrototypeOf(uaData));
1672
- Object.defineProperties(patched, {
1673
- brands: {
1674
- get: () => patchBrandList(uaData.brands),
1675
- enumerable: true,
1676
- },
1677
- mobile: {
1678
- get: () => uaData.mobile,
1679
- enumerable: true,
1680
- },
1681
- platform: {
1682
- get: () => uaData.platform,
1683
- enumerable: true,
1684
- },
1685
- });
1686
- patched.toJSON = () => ({
1687
- brands: patchBrandList(uaData.brands),
1688
- mobile: uaData.mobile,
1689
- platform: uaData.platform,
1690
- });
1691
- patched.getHighEntropyValues = async (hints) => {
1692
- const values = await uaData.getHighEntropyValues(hints);
1693
- if (values && typeof values === 'object') {
1694
- if ('brands' in values) values.brands = patchBrandList(values.brands);
1695
- if ('fullVersionList' in values) {
1696
- values.fullVersionList = patchBrandList(values.fullVersionList);
1697
- }
1698
- }
1699
- return values;
1700
- };
1701
- try {
1702
- Object.defineProperty(navigator, 'userAgentData', {
1703
- get: () => patched,
1704
- configurable: true,
1705
- });
1706
- } catch {}
1707
- })();`;
1708
- }
1709
- /**
1710
- * Provide a fake performance.memory (Chrome-only, non-standard).
1711
- * Headless Chrome omits this; some detectors check for its presence.
1712
- */
1713
- function patchPerformanceMemory() {
1714
- return `(function(){
1715
- if (!performance.memory) {
1716
- Object.defineProperty(performance, 'memory', {
1717
- get: () => ({
1718
- jsHeapSizeLimit: 2172649472,
1719
- totalJSHeapSize: 35839739,
1720
- usedJSHeapSize: 22592767,
1721
- }),
1722
- configurable: true,
1723
- });
1724
- }
1725
- })();`;
1726
- }
1727
- /**
1728
- * Headless Chrome has a transparent default background (rgba(0,0,0,0)).
1729
- * Real browsers have an opaque white background. Set it early to avoid
1730
- * the "hasKnownBgColor" detection.
1731
- */
1732
- function patchDefaultBackgroundColor() {
1733
- return `(function(){
1734
- if (document.documentElement) {
1735
- const style = getComputedStyle(document.documentElement);
1736
- const bg = style.backgroundColor;
1737
- if (!bg || bg === 'rgba(0, 0, 0, 0)' || bg === 'transparent') {
1738
- document.documentElement.style.backgroundColor = 'rgb(255, 255, 255)';
1739
- }
1740
- }
1741
- })();`;
1742
- }
1743
- //# sourceMappingURL=stealth.js.map