leapfrog-mcp 0.6.2 → 0.6.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/dist/adaptive-wait.d.ts +72 -0
- package/dist/adaptive-wait.js +695 -0
- package/dist/api-intelligence.d.ts +82 -0
- package/dist/api-intelligence.js +575 -0
- package/dist/captcha-solver.d.ts +26 -0
- package/dist/captcha-solver.js +547 -0
- package/dist/cdp-connector.d.ts +33 -0
- package/dist/cdp-connector.js +176 -0
- package/dist/consent-dismiss.d.ts +33 -0
- package/dist/consent-dismiss.js +358 -0
- package/dist/crash-recovery.d.ts +74 -0
- package/dist/crash-recovery.js +242 -0
- package/dist/domain-knowledge.d.ts +149 -0
- package/dist/domain-knowledge.js +449 -0
- package/dist/harness-intelligence.d.ts +65 -0
- package/dist/harness-intelligence.js +432 -0
- package/dist/humanize-fingerprint.d.ts +42 -0
- package/dist/humanize-fingerprint.js +161 -0
- package/dist/humanize-mouse.d.ts +95 -0
- package/dist/humanize-mouse.js +275 -0
- package/dist/humanize-pause.d.ts +48 -0
- package/dist/humanize-pause.js +111 -0
- package/dist/humanize-scroll.d.ts +67 -0
- package/dist/humanize-scroll.js +185 -0
- package/dist/humanize-typing.d.ts +60 -0
- package/dist/humanize-typing.js +258 -0
- package/dist/humanize-utils.d.ts +62 -0
- package/dist/humanize-utils.js +100 -0
- package/dist/intervention.d.ts +65 -0
- package/dist/intervention.js +591 -0
- package/dist/logger.d.ts +13 -0
- package/dist/logger.js +47 -0
- package/dist/network-intelligence.d.ts +70 -0
- package/dist/network-intelligence.js +424 -0
- package/dist/page-classifier.d.ts +33 -0
- package/dist/page-classifier.js +1000 -0
- package/dist/paginate.d.ts +42 -0
- package/dist/paginate.js +693 -0
- package/dist/recording.d.ts +72 -0
- package/dist/recording.js +934 -0
- package/dist/script-executor.d.ts +31 -0
- package/dist/script-executor.js +249 -0
- package/dist/session-hud.d.ts +20 -0
- package/dist/session-hud.js +134 -0
- package/dist/snapshot-differ.d.ts +26 -0
- package/dist/snapshot-differ.js +225 -0
- package/dist/ssrf.d.ts +28 -0
- package/dist/ssrf.js +290 -0
- package/dist/stealth-audit.d.ts +27 -0
- package/dist/stealth-audit.js +719 -0
- package/dist/stealth.d.ts +195 -0
- package/dist/stealth.js +1157 -0
- package/dist/tab-manager.d.ts +14 -0
- package/dist/tab-manager.js +306 -0
- package/dist/tiles-coordinator.d.ts +106 -0
- package/dist/tiles-coordinator.js +358 -0
- package/package.json +1 -1
package/dist/stealth.js
ADDED
|
@@ -0,0 +1,1157 @@
|
|
|
1
|
+
// ─── Anti-Bot Evasion ─────────────────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Stealth patches for headless Chromium to avoid bot detection.
|
|
4
|
+
// Three modes controlled by LEAP_STEALTH env var:
|
|
5
|
+
//
|
|
6
|
+
// LEAP_STEALTH=true (default) — full stealth: automation removal + identity faking
|
|
7
|
+
// LEAP_STEALTH=passive — passive only: remove automation signals, do NOT fake identity
|
|
8
|
+
// LEAP_STEALTH=false — stealth completely disabled
|
|
9
|
+
//
|
|
10
|
+
// Passive mode was introduced because advanced fingerprinters (CreepJS) detect
|
|
11
|
+
// INCONSISTENCIES from identity faking (fake plugins, WebGL, platform, etc.)
|
|
12
|
+
// as "lies." A browser that simply removes automation signals without faking
|
|
13
|
+
// identity scores 0% lies / 0% bot, versus 33% lies / 20% bot with full stealth.
|
|
14
|
+
//
|
|
15
|
+
// Standalone module — no cross-dependencies on logger or session-manager.
|
|
16
|
+
//
|
|
17
|
+
// ─── Evasion Index ────────────────────────────────────────────────────────
|
|
18
|
+
//
|
|
19
|
+
// Category A — Automation Signal Removal (PASSIVE + ACTIVE):
|
|
20
|
+
// These remove evidence that a browser is automated.
|
|
21
|
+
// 2. navigator.webdriver = true → init script, re-applied every navigation
|
|
22
|
+
// 14. sourceurl stripping (Playwright injects sourceURL comments)
|
|
23
|
+
// 15. Runtime.enable CDP detection → Error.prepareStackTrace filter
|
|
24
|
+
// - Playwright globals cleanup (__pwInitScripts, __playwright, etc.)
|
|
25
|
+
// - ChromeDriver property removal
|
|
26
|
+
// - framenavigated webdriver re-deletion listener
|
|
27
|
+
//
|
|
28
|
+
// Category B — Identity Faking (ACTIVE ONLY):
|
|
29
|
+
// These create a fake identity. Advanced fingerprinters detect these as lies.
|
|
30
|
+
// 1. HeadlessChrome in Client Hints brands → userAgentData override
|
|
31
|
+
// 4. SwiftShader WebGL vendor/renderer → WebGL1/2 override
|
|
32
|
+
// 5. Connection RTT = 0 → navigator.connection override
|
|
33
|
+
// 7. outerHeight === innerHeight → fake chrome offset (85px)
|
|
34
|
+
// 8. 0 mime types → MimeTypeArray spoof
|
|
35
|
+
// 9. Platform mismatch with custom UA → inferPlatformFromUA()
|
|
36
|
+
// 10. chrome.app emulation
|
|
37
|
+
// 11. iframe contentWindow protection
|
|
38
|
+
// 12. media codecs spoofing (canPlayType override)
|
|
39
|
+
// 13. document.hasFocus() override
|
|
40
|
+
// 16. Permissions.prototype.query override
|
|
41
|
+
// 17. AudioContext fingerprint noise
|
|
42
|
+
// 18. WebRTC IP filtering
|
|
43
|
+
// 19. Font enumeration spoofing
|
|
44
|
+
// - navigator.plugins spoofing (5 fake plugins)
|
|
45
|
+
// - navigator.languages override
|
|
46
|
+
// - navigator.hardwareConcurrency/deviceMemory spoofing
|
|
47
|
+
// - Notification.permission override
|
|
48
|
+
// - Canvas fingerprint noise (session-seeded PRNG)
|
|
49
|
+
// - chrome.runtime/loadTimes/csi emulation
|
|
50
|
+
// - Worker/SharedWorker UA leak prevention
|
|
51
|
+
//
|
|
52
|
+
// Already passing (do not touch):
|
|
53
|
+
// - UA string looks like real Chrome 136
|
|
54
|
+
// - window.chrome present, 5 plugins, languages correct
|
|
55
|
+
// - No Selenium/PhantomJS markers (all 17 absent)
|
|
56
|
+
// - DevTools Protocol not detected
|
|
57
|
+
// - Canvas fingerprint consistent across iframes
|
|
58
|
+
// - Battery API realistic
|
|
59
|
+
// ── rebrowser-patches integration note ────────────────────────────────────
|
|
60
|
+
// rebrowser-patches (https://github.com/nicedoc/rebrowser-patches) patches
|
|
61
|
+
// Playwright's CDP layer to avoid Runtime.enable detection. If we integrate
|
|
62
|
+
// it in the future, the entry point is:
|
|
63
|
+
// 1. Replace playwright-core import with rebrowser-playwright
|
|
64
|
+
// 2. Call `rebrowser.patch()` before any browser launch in session-manager
|
|
65
|
+
// 3. The CDP stealth script (getCdpStealthScript) would become redundant
|
|
66
|
+
// Evaluate as a dependency change in a dedicated PR — do not install inline.
|
|
67
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
68
|
+
export class StealthMode {
|
|
69
|
+
/**
|
|
70
|
+
* Strip sourceURL comments from init script text.
|
|
71
|
+
* Playwright injects `//# sourceURL=__playwright_evaluation_script__` into
|
|
72
|
+
* eval'd scripts which bot detectors look for. This strips any sourceURL
|
|
73
|
+
* or sourceMappingURL directives from our init scripts before injection.
|
|
74
|
+
*/
|
|
75
|
+
sanitizeSourceURL(script) {
|
|
76
|
+
return script.replace(/\/\/[#@]\s*source(Mapping)?URL\s*=\s*[^\n]*/g, '');
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Chromium launch args that reduce automation fingerprinting.
|
|
80
|
+
*
|
|
81
|
+
* In passive mode, only automation-hiding args are included.
|
|
82
|
+
* In active mode, GPU-related args for WebGL faking are also added.
|
|
83
|
+
*/
|
|
84
|
+
getLaunchArgs() {
|
|
85
|
+
const mode = this.getMode();
|
|
86
|
+
// Category A args — remove automation signals (passive + active)
|
|
87
|
+
const args = [
|
|
88
|
+
// P0 #2: Suppress navigator.webdriver at Blink level
|
|
89
|
+
"--disable-blink-features=AutomationControlled",
|
|
90
|
+
// BUG-003: Disable AutomationControlled feature flag to prevent
|
|
91
|
+
// "HeadlessChrome" from appearing in Client Hints brands
|
|
92
|
+
"--disable-features=AutomationControlled",
|
|
93
|
+
// Standard headless-mode hardening
|
|
94
|
+
"--disable-dev-shm-usage",
|
|
95
|
+
"--no-first-run",
|
|
96
|
+
"--no-default-browser-check",
|
|
97
|
+
];
|
|
98
|
+
// Category B args — GPU/identity faking (active only)
|
|
99
|
+
if (mode === 'active') {
|
|
100
|
+
// P1 #4: Force real GPU rendering instead of SwiftShader
|
|
101
|
+
// SwiftShader shows "Google SwiftShader" in UNMASKED_VENDOR/RENDERER
|
|
102
|
+
args.push("--use-gl=angle", "--use-angle=default");
|
|
103
|
+
}
|
|
104
|
+
return args;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Realistic Chrome user agent string for macOS.
|
|
108
|
+
*/
|
|
109
|
+
getDefaultUserAgent() {
|
|
110
|
+
return "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36";
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Infer the correct navigator.platform value from a user-agent string.
|
|
114
|
+
* Prevents P2 #9: platform mismatch when custom UA says Windows but
|
|
115
|
+
* platform says MacIntel.
|
|
116
|
+
*/
|
|
117
|
+
inferPlatformFromUA(ua) {
|
|
118
|
+
if (/Windows/i.test(ua))
|
|
119
|
+
return "Win32";
|
|
120
|
+
if (/Macintosh|Mac OS X/i.test(ua))
|
|
121
|
+
return "MacIntel";
|
|
122
|
+
if (/Linux/i.test(ua))
|
|
123
|
+
return "Linux x86_64";
|
|
124
|
+
if (/CrOS/i.test(ua))
|
|
125
|
+
return "Linux x86_64";
|
|
126
|
+
if (/Android/i.test(ua))
|
|
127
|
+
return "Linux armv8l";
|
|
128
|
+
if (/iPhone|iPad/i.test(ua))
|
|
129
|
+
return "iPhone";
|
|
130
|
+
return "MacIntel"; // safe default
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Returns a random delay in ms (200-500) for dialog auto-dismiss.
|
|
134
|
+
* P1 #6: Instant dismiss (< 30ms) is a headless signal.
|
|
135
|
+
* Call this from dialog handlers in session-manager and tab-manager.
|
|
136
|
+
*/
|
|
137
|
+
getDialogDelay() {
|
|
138
|
+
return 200 + Math.floor(Math.random() * 300);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Derive the correct navigator.platform string from process.platform.
|
|
142
|
+
* Used as the fallback when no UA string is available to infer from.
|
|
143
|
+
*/
|
|
144
|
+
getPlatformFromProcess() {
|
|
145
|
+
switch (process.platform) {
|
|
146
|
+
case "win32": return "Win32";
|
|
147
|
+
case "darwin": return "MacIntel";
|
|
148
|
+
case "linux": return "Linux x86_64";
|
|
149
|
+
default: return "MacIntel";
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Extract the Chrome major version number from a UA string.
|
|
154
|
+
* Falls back to 136 (current default UA) if not found.
|
|
155
|
+
*/
|
|
156
|
+
extractChromeMajorVersion(ua) {
|
|
157
|
+
const match = ua.match(/Chrome\/(\d+)\./);
|
|
158
|
+
return match ? parseInt(match[1], 10) : 136;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Generate a numeric seed from a string (simple djb2 hash).
|
|
162
|
+
* Used to seed per-session PRNG for canvas/audio noise determinism.
|
|
163
|
+
*/
|
|
164
|
+
hashSeed(str) {
|
|
165
|
+
let hash = 5381;
|
|
166
|
+
for (let i = 0; i < str.length; i++) {
|
|
167
|
+
hash = ((hash << 5) + hash + str.charCodeAt(i)) | 0;
|
|
168
|
+
}
|
|
169
|
+
return Math.abs(hash);
|
|
170
|
+
}
|
|
171
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
172
|
+
// Init Script — Split into Passive (Category A) and Active (Category B)
|
|
173
|
+
// ────────────────────────────────────────────────────────────────────────
|
|
174
|
+
/**
|
|
175
|
+
* Category A: Passive init script — removes automation signals only.
|
|
176
|
+
* These patches make an automated browser look like a non-automated browser
|
|
177
|
+
* WITHOUT faking its identity. Safe against advanced fingerprinters like CreepJS
|
|
178
|
+
* because they don't introduce detectable inconsistencies ("lies").
|
|
179
|
+
*
|
|
180
|
+
* Patches included:
|
|
181
|
+
* - Playwright globals cleanup (__pwInitScripts, __playwright, etc.)
|
|
182
|
+
* - navigator.webdriver deletion (P0 #2)
|
|
183
|
+
* - ChromeDriver property removal
|
|
184
|
+
* - sourceURL stripping (#14)
|
|
185
|
+
*/
|
|
186
|
+
getPassiveInitScript() {
|
|
187
|
+
return `
|
|
188
|
+
// ────────────────────────────────────────────────────────────────────
|
|
189
|
+
// BUG FIX: __pwInitScripts race condition
|
|
190
|
+
// Delete Playwright automation globals IMMEDIATELY at the top of the
|
|
191
|
+
// very first init script. This closes the window where page JS or
|
|
192
|
+
// subsequent init scripts could observe __pwInitScripts before the
|
|
193
|
+
// separate cleanup script runs. Critical for v0.6.0 which adds 3+
|
|
194
|
+
// more init scripts, widening the race window.
|
|
195
|
+
// ────────────────────────────────────────────────────────────────────
|
|
196
|
+
(function() {
|
|
197
|
+
var globals = ['__pwInitScripts', '__playwright__binding__', '__playwright'];
|
|
198
|
+
for (var i = 0; i < globals.length; i++) {
|
|
199
|
+
try { delete window[globals[i]]; } catch(e) {}
|
|
200
|
+
try {
|
|
201
|
+
Object.defineProperty(window, globals[i], {
|
|
202
|
+
get: function() { return undefined; },
|
|
203
|
+
set: function() {},
|
|
204
|
+
configurable: true,
|
|
205
|
+
});
|
|
206
|
+
} catch(e) {}
|
|
207
|
+
}
|
|
208
|
+
})();
|
|
209
|
+
|
|
210
|
+
// ────────────────────────────────────────────────────────────────────
|
|
211
|
+
// P0 #2: Hide navigator.webdriver
|
|
212
|
+
// BUG-004: Use delete + defineProperty + prototype patch for full coverage.
|
|
213
|
+
// Some detection scripts check the prototype directly or use 'in' operator.
|
|
214
|
+
// The --disable-blink-features=AutomationControlled launch arg should
|
|
215
|
+
// handle this, but QA found it does not always take effect. This
|
|
216
|
+
// redundant override ensures webdriver is hidden on every navigation.
|
|
217
|
+
// ────────────────────────────────────────────────────────────────────
|
|
218
|
+
try {
|
|
219
|
+
delete Object.getPrototypeOf(navigator).webdriver;
|
|
220
|
+
delete Navigator.prototype.webdriver;
|
|
221
|
+
delete navigator.webdriver;
|
|
222
|
+
} catch (e) { /* already deleted or non-configurable */ }
|
|
223
|
+
if ('webdriver' in navigator) {
|
|
224
|
+
try {
|
|
225
|
+
Object.defineProperty(navigator, 'webdriver', {
|
|
226
|
+
get: () => undefined,
|
|
227
|
+
configurable: true,
|
|
228
|
+
enumerable: false,
|
|
229
|
+
});
|
|
230
|
+
} catch (e) { /* best effort */ }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ────────────────────────────────────────────────────────────────────
|
|
234
|
+
// Remove ChromeDriver detection property
|
|
235
|
+
// ────────────────────────────────────────────────────────────────────
|
|
236
|
+
delete window.cdc_adoQpoasnfa76pfcZLmcfl_;
|
|
237
|
+
|
|
238
|
+
// ────────────────────────────────────────────────────────────────────
|
|
239
|
+
// #14: sourceurl stripping
|
|
240
|
+
// Playwright injects //# sourceURL= comments into evaluated scripts.
|
|
241
|
+
// Detection sites look for these as automation fingerprints.
|
|
242
|
+
// We override Error.prepareStackTrace to strip sourceURL references.
|
|
243
|
+
// ────────────────────────────────────────────────────────────────────
|
|
244
|
+
(function() {
|
|
245
|
+
const origPrepareStackTrace = Error.prepareStackTrace;
|
|
246
|
+
Error.prepareStackTrace = function(error, callSites) {
|
|
247
|
+
if (origPrepareStackTrace) {
|
|
248
|
+
return origPrepareStackTrace(error, callSites);
|
|
249
|
+
}
|
|
250
|
+
const stack = callSites
|
|
251
|
+
.filter(site => {
|
|
252
|
+
const filename = site.getFileName() || '';
|
|
253
|
+
return !filename.startsWith('pptr:') &&
|
|
254
|
+
!filename.includes('__playwright') &&
|
|
255
|
+
!filename.includes('sourceURL');
|
|
256
|
+
})
|
|
257
|
+
.map(site => ' at ' + site.toString())
|
|
258
|
+
.join('\\n');
|
|
259
|
+
return error.toString() + '\\n' + stack;
|
|
260
|
+
};
|
|
261
|
+
})();
|
|
262
|
+
`;
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Category B: Active init script — identity faking patches.
|
|
266
|
+
* These create a fake browser identity (plugins, WebGL, platform, etc.).
|
|
267
|
+
* Advanced fingerprinters like CreepJS can detect these as "lies" because
|
|
268
|
+
* they create inconsistencies between reported and actual values.
|
|
269
|
+
*
|
|
270
|
+
* Only applied when LEAP_STEALTH=true (active mode, the default).
|
|
271
|
+
*
|
|
272
|
+
* Patches included:
|
|
273
|
+
* - Session-seeded PRNG (mulberry32)
|
|
274
|
+
* - Client Hints brands override (P0 #1)
|
|
275
|
+
* - chrome.app/runtime/loadTimes/csi emulation (#10)
|
|
276
|
+
* - Permissions.prototype.query override (P2 #16)
|
|
277
|
+
* - navigator.plugins spoofing (5 fake plugins)
|
|
278
|
+
* - MimeTypeArray spoof (P2 #8)
|
|
279
|
+
* - navigator.languages override
|
|
280
|
+
* - navigator.platform override (P2 #9)
|
|
281
|
+
* - hardwareConcurrency/deviceMemory spoofing
|
|
282
|
+
* - Notification.permission override
|
|
283
|
+
* - WebGL vendor/renderer override (P1 #4)
|
|
284
|
+
* - Connection RTT override (P1 #5)
|
|
285
|
+
* - outerHeight/outerWidth fake (P2 #7)
|
|
286
|
+
* - document.hasFocus() override (#13)
|
|
287
|
+
* - Media codecs spoofing (#12)
|
|
288
|
+
* - Canvas fingerprint noise (Phase 2.2)
|
|
289
|
+
* - AudioContext fingerprint noise (P3 #17)
|
|
290
|
+
* - WebRTC IP filtering (P3 #18)
|
|
291
|
+
* - Font enumeration spoofing (P3 #19)
|
|
292
|
+
* - iframe contentWindow protection (#11)
|
|
293
|
+
* - Worker/SharedWorker UA leak prevention
|
|
294
|
+
*/
|
|
295
|
+
getActiveInitScript(platform, ua, fingerprint, sessionSeed) {
|
|
296
|
+
const chromeVersion = ua
|
|
297
|
+
? this.extractChromeMajorVersion(ua)
|
|
298
|
+
: this.extractChromeMajorVersion(this.getDefaultUserAgent());
|
|
299
|
+
// Phase 2.1/2.5: Use fingerprint values when available, otherwise defaults
|
|
300
|
+
const webglVendor = fingerprint?.webgl?.vendor ?? 'Google Inc. (Apple)';
|
|
301
|
+
const webglRenderer = fingerprint?.webgl?.renderer ?? 'ANGLE (Apple, Apple M1 Pro, OpenGL 4.1)';
|
|
302
|
+
const deviceMemory = fingerprint?.deviceMemory ?? 8;
|
|
303
|
+
const hardwareConcurrency = fingerprint?.hardwareConcurrency ?? 8;
|
|
304
|
+
// Phase 2.2/2.3: Session seed for deterministic PRNG (canvas + audio noise)
|
|
305
|
+
const seed = sessionSeed ?? this.hashSeed(ua ?? 'default-session');
|
|
306
|
+
return `
|
|
307
|
+
// ────────────────────────────────────────────────────────────────────
|
|
308
|
+
// Phase 2.2/2.3: Session-seeded PRNG (mulberry32)
|
|
309
|
+
// Deterministic within a session — same canvas/audio operations produce
|
|
310
|
+
// identical output. Prevents tampering detection from non-deterministic noise.
|
|
311
|
+
// ────────────────────────────────────────────────────────────────────
|
|
312
|
+
var __leapSeed = ${seed} >>> 0;
|
|
313
|
+
function __leapRandom() {
|
|
314
|
+
__leapSeed |= 0; __leapSeed = __leapSeed + 0x6D2B79F5 | 0;
|
|
315
|
+
var t = Math.imul(__leapSeed ^ __leapSeed >>> 15, 1 | __leapSeed);
|
|
316
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
317
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ────────────────────────────────────────────────────────────────────
|
|
321
|
+
// P0 #1 + BUG-003 (consolidated): Client Hints brands override
|
|
322
|
+
// Phase 2.6: Merged two overlapping patches into one clean replacement
|
|
323
|
+
// of the entire NavigatorUAData interface.
|
|
324
|
+
// ────────────────────────────────────────────────────────────────────
|
|
325
|
+
if (navigator.userAgentData) {
|
|
326
|
+
const cleanBrands = [
|
|
327
|
+
{ brand: "Chromium", version: "${chromeVersion}" },
|
|
328
|
+
{ brand: "Google Chrome", version: "${chromeVersion}" },
|
|
329
|
+
{ brand: "Not_A Brand", version: "24" },
|
|
330
|
+
];
|
|
331
|
+
const cleanFullBrands = [
|
|
332
|
+
{ brand: "Chromium", version: "${chromeVersion}.0.0.0" },
|
|
333
|
+
{ brand: "Google Chrome", version: "${chromeVersion}.0.0.0" },
|
|
334
|
+
{ brand: "Not_A Brand", version: "24.0.0.0" },
|
|
335
|
+
];
|
|
336
|
+
|
|
337
|
+
const cleanUAData = {
|
|
338
|
+
brands: cleanBrands,
|
|
339
|
+
mobile: false,
|
|
340
|
+
platform: ${JSON.stringify(platform.startsWith("Win") ? "Windows" : platform.startsWith("Mac") ? "macOS" : platform.startsWith("Linux") ? "Linux" : "macOS")},
|
|
341
|
+
getHighEntropyValues: function(hints) {
|
|
342
|
+
return Promise.resolve({
|
|
343
|
+
brands: cleanFullBrands,
|
|
344
|
+
mobile: false,
|
|
345
|
+
platform: this.platform,
|
|
346
|
+
platformVersion: ${JSON.stringify(platform.startsWith("Win") ? "10.0.0" : platform.startsWith("Mac") ? "15.0.0" : "6.6.0")},
|
|
347
|
+
architecture: "x86",
|
|
348
|
+
bitness: "64",
|
|
349
|
+
model: "",
|
|
350
|
+
uaFullVersion: "${chromeVersion}.0.0.0",
|
|
351
|
+
fullVersionList: cleanFullBrands,
|
|
352
|
+
wow64: false,
|
|
353
|
+
});
|
|
354
|
+
},
|
|
355
|
+
toJSON: function() {
|
|
356
|
+
return {
|
|
357
|
+
brands: cleanBrands,
|
|
358
|
+
mobile: false,
|
|
359
|
+
platform: this.platform,
|
|
360
|
+
};
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
Object.defineProperty(navigator, 'userAgentData', {
|
|
365
|
+
get: () => cleanUAData,
|
|
366
|
+
configurable: true,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// ────────────────────────────────────────────────────────────────────
|
|
371
|
+
// Fake window.chrome object (already passing — preserve)
|
|
372
|
+
// Includes chrome.app emulation (puppeteer-stealth module #1)
|
|
373
|
+
// ────────────────────────────────────────────────────────────────────
|
|
374
|
+
if (!window.chrome) {
|
|
375
|
+
Object.defineProperty(window, 'chrome', {
|
|
376
|
+
value: {},
|
|
377
|
+
configurable: true,
|
|
378
|
+
writable: true,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// #10: chrome.app emulation — real Chrome always has this object
|
|
383
|
+
if (!window.chrome.app) {
|
|
384
|
+
window.chrome.app = {
|
|
385
|
+
isInstalled: false,
|
|
386
|
+
InstallState: {
|
|
387
|
+
DISABLED: 'disabled',
|
|
388
|
+
INSTALLED: 'installed',
|
|
389
|
+
NOT_INSTALLED: 'not_installed',
|
|
390
|
+
},
|
|
391
|
+
RunningState: {
|
|
392
|
+
CANNOT_RUN: 'cannot_run',
|
|
393
|
+
READY_TO_RUN: 'ready_to_run',
|
|
394
|
+
RUNNING: 'running',
|
|
395
|
+
},
|
|
396
|
+
getDetails: function() { return null; },
|
|
397
|
+
getIsInstalled: function() { return false; },
|
|
398
|
+
installState: function(callback) {
|
|
399
|
+
if (typeof callback === 'function') {
|
|
400
|
+
callback('not_installed');
|
|
401
|
+
}
|
|
402
|
+
return 'not_installed';
|
|
403
|
+
},
|
|
404
|
+
runningState: function() { return 'cannot_run'; },
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// chrome.runtime — already passing, preserve exact shape
|
|
409
|
+
if (!window.chrome.runtime) {
|
|
410
|
+
window.chrome.runtime = {
|
|
411
|
+
onConnect: { addListener() {}, removeListener() {} },
|
|
412
|
+
onMessage: { addListener() {}, removeListener() {} },
|
|
413
|
+
connect() { return { onDisconnect: { addListener() {} } }; },
|
|
414
|
+
sendMessage() {},
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// chrome.loadTimes — already passing, preserve
|
|
419
|
+
if (!window.chrome.loadTimes) {
|
|
420
|
+
window.chrome.loadTimes = function() {
|
|
421
|
+
return {
|
|
422
|
+
commitLoadTime: Date.now() / 1000 - 2,
|
|
423
|
+
connectionInfo: 'h2',
|
|
424
|
+
finishDocumentLoadTime: Date.now() / 1000 - 0.5,
|
|
425
|
+
finishLoadTime: Date.now() / 1000 - 0.1,
|
|
426
|
+
firstPaintAfterLoadTime: 0,
|
|
427
|
+
firstPaintTime: Date.now() / 1000 - 1.5,
|
|
428
|
+
navigationType: 'Other',
|
|
429
|
+
npnNegotiatedProtocol: 'h2',
|
|
430
|
+
requestTime: Date.now() / 1000 - 2.5,
|
|
431
|
+
startLoadTime: Date.now() / 1000 - 2,
|
|
432
|
+
wasAlternateProtocolAvailable: false,
|
|
433
|
+
wasFetchedViaSpdy: true,
|
|
434
|
+
wasNpnNegotiated: true,
|
|
435
|
+
};
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// chrome.csi — already passing, preserve
|
|
440
|
+
if (!window.chrome.csi) {
|
|
441
|
+
window.chrome.csi = function() {
|
|
442
|
+
return {
|
|
443
|
+
onloadT: Date.now(),
|
|
444
|
+
pageT: Date.now() - performance.timing.navigationStart,
|
|
445
|
+
startE: performance.timing.navigationStart,
|
|
446
|
+
tran: 15,
|
|
447
|
+
};
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ────────────────────────────────────────────────────────────────────
|
|
452
|
+
// P2: Comprehensive Permissions.prototype.query override
|
|
453
|
+
// Bot detection scripts query various permissions and check for
|
|
454
|
+
// unexpected throws or inconsistent states. Real browsers return
|
|
455
|
+
// "prompt" for most permissions and "granted" for geolocation.
|
|
456
|
+
// Defends against: CreepJS permissions fingerprint, FingerprintJS Pro
|
|
457
|
+
// ────────────────────────────────────────────────────────────────────
|
|
458
|
+
(function() {
|
|
459
|
+
const originalQuery = window.Permissions.prototype.query;
|
|
460
|
+
const promptPermissions = [
|
|
461
|
+
'notifications', 'push', 'midi', 'camera', 'microphone',
|
|
462
|
+
'speaker', 'device-info', 'background-fetch', 'background-sync',
|
|
463
|
+
'bluetooth', 'persistent-storage', 'ambient-light-sensor',
|
|
464
|
+
'accelerometer', 'gyroscope', 'magnetometer', 'screen-wake-lock',
|
|
465
|
+
'nfc', 'display-capture', 'idle-detection', 'periodic-background-sync'
|
|
466
|
+
];
|
|
467
|
+
window.Permissions.prototype.query = function(desc) {
|
|
468
|
+
if (promptPermissions.includes(desc.name)) {
|
|
469
|
+
return Promise.resolve({ state: 'prompt', onchange: null });
|
|
470
|
+
}
|
|
471
|
+
if (desc.name === 'geolocation') {
|
|
472
|
+
return Promise.resolve({ state: 'granted', onchange: null });
|
|
473
|
+
}
|
|
474
|
+
return originalQuery.call(this, desc);
|
|
475
|
+
};
|
|
476
|
+
})();
|
|
477
|
+
|
|
478
|
+
// ────────────────────────────────────────────────────────────────────
|
|
479
|
+
// Fix navigator.plugins (5 fake plugins)
|
|
480
|
+
// ────────────────────────────────────────────────────────────────────
|
|
481
|
+
Object.defineProperty(navigator, 'plugins', {
|
|
482
|
+
get: () => {
|
|
483
|
+
const fakePlugins = [
|
|
484
|
+
{ name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
|
|
485
|
+
{ name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '' },
|
|
486
|
+
{ name: 'Native Client', filename: 'internal-nacl-plugin', description: '' },
|
|
487
|
+
{ name: 'Chromium PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
|
|
488
|
+
{ name: 'Chromium PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '' },
|
|
489
|
+
];
|
|
490
|
+
const pluginArray = Object.create(PluginArray.prototype);
|
|
491
|
+
for (let i = 0; i < fakePlugins.length; i++) {
|
|
492
|
+
const p = Object.create(Plugin.prototype);
|
|
493
|
+
Object.defineProperties(p, {
|
|
494
|
+
name: { value: fakePlugins[i].name, enumerable: true },
|
|
495
|
+
filename: { value: fakePlugins[i].filename, enumerable: true },
|
|
496
|
+
description: { value: fakePlugins[i].description, enumerable: true },
|
|
497
|
+
length: { value: 0, enumerable: true },
|
|
498
|
+
});
|
|
499
|
+
pluginArray[i] = p;
|
|
500
|
+
}
|
|
501
|
+
Object.defineProperty(pluginArray, 'length', { value: fakePlugins.length });
|
|
502
|
+
pluginArray.item = (index) => pluginArray[index] || null;
|
|
503
|
+
pluginArray.namedItem = (name) => {
|
|
504
|
+
for (let i = 0; i < fakePlugins.length; i++) {
|
|
505
|
+
if (pluginArray[i].name === name) return pluginArray[i];
|
|
506
|
+
}
|
|
507
|
+
return null;
|
|
508
|
+
};
|
|
509
|
+
pluginArray.refresh = () => {};
|
|
510
|
+
return pluginArray;
|
|
511
|
+
},
|
|
512
|
+
configurable: true,
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// ────────────────────────────────────────────────────────────────────
|
|
516
|
+
// P2 #8: Spoof MimeTypeArray (headless reports 0 mime types)
|
|
517
|
+
// Real Chrome always has at least 2 PDF-related mime types.
|
|
518
|
+
// ────────────────────────────────────────────────────────────────────
|
|
519
|
+
Object.defineProperty(navigator, 'mimeTypes', {
|
|
520
|
+
get: () => {
|
|
521
|
+
const fakeMimes = [
|
|
522
|
+
{ type: 'application/pdf', suffixes: 'pdf', description: 'Portable Document Format', enabledPlugin: navigator.plugins[0] },
|
|
523
|
+
{ type: 'text/pdf', suffixes: 'pdf', description: 'Portable Document Format', enabledPlugin: navigator.plugins[0] },
|
|
524
|
+
];
|
|
525
|
+
const mimeArray = Object.create(MimeTypeArray.prototype);
|
|
526
|
+
for (let i = 0; i < fakeMimes.length; i++) {
|
|
527
|
+
const m = Object.create(MimeType.prototype);
|
|
528
|
+
Object.defineProperties(m, {
|
|
529
|
+
type: { value: fakeMimes[i].type, enumerable: true },
|
|
530
|
+
suffixes: { value: fakeMimes[i].suffixes, enumerable: true },
|
|
531
|
+
description: { value: fakeMimes[i].description, enumerable: true },
|
|
532
|
+
enabledPlugin: { value: fakeMimes[i].enabledPlugin, enumerable: true },
|
|
533
|
+
});
|
|
534
|
+
mimeArray[i] = m;
|
|
535
|
+
mimeArray[fakeMimes[i].type] = m;
|
|
536
|
+
}
|
|
537
|
+
Object.defineProperty(mimeArray, 'length', { value: fakeMimes.length });
|
|
538
|
+
mimeArray.item = (index) => mimeArray[index] || null;
|
|
539
|
+
mimeArray.namedItem = (name) => mimeArray[name] || null;
|
|
540
|
+
return mimeArray;
|
|
541
|
+
},
|
|
542
|
+
configurable: true,
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
// ────────────────────────────────────────────────────────────────────
|
|
546
|
+
// Fix navigator.languages
|
|
547
|
+
// ────────────────────────────────────────────────────────────────────
|
|
548
|
+
Object.defineProperty(navigator, 'languages', {
|
|
549
|
+
get: () => ['en-US', 'en'],
|
|
550
|
+
configurable: true,
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// ────────────────────────────────────────────────────────────────────
|
|
554
|
+
// P2 #9: Override navigator.platform — uses UA-inferred value
|
|
555
|
+
// Prevents mismatch when custom UA says Windows but platform says MacIntel.
|
|
556
|
+
// ────────────────────────────────────────────────────────────────────
|
|
557
|
+
Object.defineProperty(navigator, 'platform', {
|
|
558
|
+
get: () => ${JSON.stringify(platform)},
|
|
559
|
+
configurable: true,
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
// ────────────────────────────────────────────────────────────────────
|
|
563
|
+
// Phase 2.5: hardwareConcurrency from per-session fingerprint
|
|
564
|
+
// ────────────────────────────────────────────────────────────────────
|
|
565
|
+
Object.defineProperty(navigator, 'hardwareConcurrency', {
|
|
566
|
+
get: () => ${hardwareConcurrency},
|
|
567
|
+
configurable: true,
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// ────────────────────────────────────────────────────────────────────
|
|
571
|
+
// Phase 2.5: deviceMemory from per-session fingerprint
|
|
572
|
+
// ────────────────────────────────────────────────────────────────────
|
|
573
|
+
Object.defineProperty(navigator, 'deviceMemory', {
|
|
574
|
+
get: () => ${deviceMemory},
|
|
575
|
+
configurable: true,
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
// ────────────────────────────────────────────────────────────────────
|
|
579
|
+
// Notification.permission returns 'default'
|
|
580
|
+
// ────────────────────────────────────────────────────────────────────
|
|
581
|
+
Object.defineProperty(Notification, 'permission', {
|
|
582
|
+
get: () => 'default',
|
|
583
|
+
configurable: true,
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
// ────────────────────────────────────────────────────────────────────
|
|
587
|
+
// P1 #4: WebGL vendor/renderer override (SwiftShader → real GPU)
|
|
588
|
+
// Phase 2.1: WebGL vendor/renderer from per-session fingerprint (9 GPU models)
|
|
589
|
+
// ────────────────────────────────────────────────────────────────────
|
|
590
|
+
(function() {
|
|
591
|
+
const WEBGL_VENDOR = ${JSON.stringify(webglVendor)};
|
|
592
|
+
const WEBGL_RENDERER = ${JSON.stringify(webglRenderer)};
|
|
593
|
+
const UNMASKED_VENDOR_WEBGL = 0x9245;
|
|
594
|
+
const UNMASKED_RENDERER_WEBGL = 0x9246;
|
|
595
|
+
|
|
596
|
+
function patchWebGLContext(proto) {
|
|
597
|
+
if (!proto) return;
|
|
598
|
+
const origGetParameter = proto.getParameter;
|
|
599
|
+
proto.getParameter = function(param) {
|
|
600
|
+
if (param === UNMASKED_VENDOR_WEBGL) return WEBGL_VENDOR;
|
|
601
|
+
if (param === UNMASKED_RENDERER_WEBGL) return WEBGL_RENDERER;
|
|
602
|
+
return origGetParameter.call(this, param);
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
if (typeof WebGLRenderingContext !== 'undefined') {
|
|
607
|
+
patchWebGLContext(WebGLRenderingContext.prototype);
|
|
608
|
+
}
|
|
609
|
+
if (typeof WebGL2RenderingContext !== 'undefined') {
|
|
610
|
+
patchWebGLContext(WebGL2RenderingContext.prototype);
|
|
611
|
+
}
|
|
612
|
+
})();
|
|
613
|
+
|
|
614
|
+
// ────────────────────────────────────────────────────────────────────
|
|
615
|
+
// P1 #5: Connection RTT = 0 → override navigator.connection
|
|
616
|
+
// ────────────────────────────────────────────────────────────────────
|
|
617
|
+
if (navigator.connection) {
|
|
618
|
+
const connectionOverrides = {
|
|
619
|
+
rtt: 50,
|
|
620
|
+
downlink: 10,
|
|
621
|
+
effectiveType: '4g',
|
|
622
|
+
saveData: false,
|
|
623
|
+
};
|
|
624
|
+
for (const [key, value] of Object.entries(connectionOverrides)) {
|
|
625
|
+
try {
|
|
626
|
+
Object.defineProperty(navigator.connection, key, {
|
|
627
|
+
get: () => value,
|
|
628
|
+
configurable: true,
|
|
629
|
+
});
|
|
630
|
+
} catch (e) {
|
|
631
|
+
// Some browsers seal this — graceful skip
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
// ────────────────────────────────────────────────────────────────────
|
|
637
|
+
// P2 #7: outerHeight === innerHeight → headless has no chrome
|
|
638
|
+
// ────────────────────────────────────────────────────────────────────
|
|
639
|
+
(function() {
|
|
640
|
+
const chromeHeight = 85;
|
|
641
|
+
const chromeWidth = 0;
|
|
642
|
+
|
|
643
|
+
Object.defineProperty(window, 'outerHeight', {
|
|
644
|
+
get: () => window.innerHeight + chromeHeight,
|
|
645
|
+
configurable: true,
|
|
646
|
+
});
|
|
647
|
+
Object.defineProperty(window, 'outerWidth', {
|
|
648
|
+
get: () => window.innerWidth + chromeWidth,
|
|
649
|
+
configurable: true,
|
|
650
|
+
});
|
|
651
|
+
})();
|
|
652
|
+
|
|
653
|
+
// ────────────────────────────────────────────────────────────────────
|
|
654
|
+
// #13: document.hasFocus() override
|
|
655
|
+
// ────────────────────────────────────────────────────────────────────
|
|
656
|
+
Document.prototype.hasFocus = function() {
|
|
657
|
+
return true;
|
|
658
|
+
};
|
|
659
|
+
|
|
660
|
+
// ────────────────────────────────────────────────────────────────────
|
|
661
|
+
// #12: Media codecs spoofing (canPlayType override)
|
|
662
|
+
// ────────────────────────────────────────────────────────────────────
|
|
663
|
+
(function() {
|
|
664
|
+
const codecResponses = {
|
|
665
|
+
'audio/mpeg': 'probably',
|
|
666
|
+
'audio/wav': 'probably',
|
|
667
|
+
'audio/ogg; codecs="vorbis"': 'probably',
|
|
668
|
+
'audio/mp4; codecs="mp4a.40.2"': 'probably',
|
|
669
|
+
'audio/webm; codecs="opus"': 'probably',
|
|
670
|
+
'video/mp4; codecs="avc1.42E01E"': 'probably',
|
|
671
|
+
'video/mp4; codecs="avc1.42E01E, mp4a.40.2"': 'probably',
|
|
672
|
+
'video/webm; codecs="vp8"': 'probably',
|
|
673
|
+
'video/webm; codecs="vp8, vorbis"': 'probably',
|
|
674
|
+
'video/webm; codecs="vp9"': 'probably',
|
|
675
|
+
'video/ogg; codecs="theora"': 'probably',
|
|
676
|
+
};
|
|
677
|
+
|
|
678
|
+
const origCanPlayType = HTMLMediaElement.prototype.canPlayType;
|
|
679
|
+
HTMLMediaElement.prototype.canPlayType = function(type) {
|
|
680
|
+
if (codecResponses[type]) return codecResponses[type];
|
|
681
|
+
return origCanPlayType.call(this, type);
|
|
682
|
+
};
|
|
683
|
+
})();
|
|
684
|
+
|
|
685
|
+
// ────────────────────────────────────────────────────────────────────
|
|
686
|
+
// Phase 2.2: Canvas fingerprint noise — session-seeded PRNG
|
|
687
|
+
// ────────────────────────────────────────────────────────────────────
|
|
688
|
+
const origToDataURL = HTMLCanvasElement.prototype.toDataURL;
|
|
689
|
+
HTMLCanvasElement.prototype.toDataURL = function(type, quality) {
|
|
690
|
+
const ctx = this.getContext('2d');
|
|
691
|
+
if (ctx && this.width > 0 && this.height > 0) {
|
|
692
|
+
try {
|
|
693
|
+
const imageData = ctx.getImageData(0, 0, Math.min(this.width, 16), Math.min(this.height, 16));
|
|
694
|
+
const data = imageData.data;
|
|
695
|
+
var canvasSeed = ${seed} >>> 0;
|
|
696
|
+
for (var ci = 0; ci < Math.min(data.length, 32); ci++) { canvasSeed = (canvasSeed + data[ci]) | 0; }
|
|
697
|
+
function canvasRandom() {
|
|
698
|
+
canvasSeed |= 0; canvasSeed = canvasSeed + 0x6D2B79F5 | 0;
|
|
699
|
+
var t = Math.imul(canvasSeed ^ canvasSeed >>> 15, 1 | canvasSeed);
|
|
700
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
701
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
702
|
+
}
|
|
703
|
+
for (let i = 0; i < Math.min(data.length, 64); i += 16) {
|
|
704
|
+
const channel = i + Math.floor(canvasRandom() * 3);
|
|
705
|
+
const delta = canvasRandom() > 0.5 ? 1 : -1;
|
|
706
|
+
data[channel] = Math.max(0, Math.min(255, data[channel] + delta));
|
|
707
|
+
}
|
|
708
|
+
ctx.putImageData(imageData, 0, 0);
|
|
709
|
+
} catch (e) {
|
|
710
|
+
// SecurityError from cross-origin canvas — skip silently
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
return origToDataURL.call(this, type, quality);
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
// ────────────────────────────────────────────────────────────────────
|
|
717
|
+
// P3: AudioContext fingerprint noise — session-seeded PRNG
|
|
718
|
+
// ────────────────────────────────────────────────────────────────────
|
|
719
|
+
(function() {
|
|
720
|
+
var audioSeed = ${seed} >>> 0;
|
|
721
|
+
function audioRandom() {
|
|
722
|
+
audioSeed |= 0; audioSeed = audioSeed + 0x6D2B79F5 | 0;
|
|
723
|
+
var t = Math.imul(audioSeed ^ audioSeed >>> 15, 1 | audioSeed);
|
|
724
|
+
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
|
|
725
|
+
return ((t ^ t >>> 14) >>> 0) / 4294967296;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const originalGetChannelData = AudioBuffer.prototype.getChannelData;
|
|
729
|
+
AudioBuffer.prototype.getChannelData = function(channel) {
|
|
730
|
+
const data = originalGetChannelData.call(this, channel);
|
|
731
|
+
if (this.length < 4096 && this.sampleRate === 44100) {
|
|
732
|
+
audioSeed = (${seed} + channel * 7919) >>> 0;
|
|
733
|
+
for (let i = 0; i < data.length; i++) {
|
|
734
|
+
data[i] += (audioRandom() * 0.0002 - 0.0001);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
return data;
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
if (typeof AnalyserNode !== 'undefined') {
|
|
741
|
+
const originalGetFloat = AnalyserNode.prototype.getFloatFrequencyData;
|
|
742
|
+
AnalyserNode.prototype.getFloatFrequencyData = function(array) {
|
|
743
|
+
originalGetFloat.call(this, array);
|
|
744
|
+
audioSeed = ${seed} >>> 0;
|
|
745
|
+
for (let i = 0; i < array.length; i++) {
|
|
746
|
+
array[i] += (audioRandom() * 0.1 - 0.05);
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
})();
|
|
751
|
+
|
|
752
|
+
// ────────────────────────────────────────────────────────────────────
|
|
753
|
+
// P3: WebRTC leak prevention
|
|
754
|
+
// ────────────────────────────────────────────────────────────────────
|
|
755
|
+
(function() {
|
|
756
|
+
const originalRTC = window.RTCPeerConnection;
|
|
757
|
+
if (originalRTC) {
|
|
758
|
+
window.RTCPeerConnection = function(config, constraints) {
|
|
759
|
+
if (config && config.iceServers) {
|
|
760
|
+
// Allow configured TURN servers
|
|
761
|
+
} else {
|
|
762
|
+
config = config || {};
|
|
763
|
+
config.iceServers = [];
|
|
764
|
+
}
|
|
765
|
+
const pc = new originalRTC(config, constraints);
|
|
766
|
+
const originalAddEvent = pc.addEventListener.bind(pc);
|
|
767
|
+
pc.addEventListener = function(type, listener, options) {
|
|
768
|
+
if (type === 'icecandidate') {
|
|
769
|
+
const wrappedListener = function(event) {
|
|
770
|
+
if (event.candidate && event.candidate.candidate) {
|
|
771
|
+
if (/((10\\.)|(172\\.(1[6-9]|2\\d|3[01])\\.)|(192\\.168\\.))/.test(event.candidate.candidate)) {
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
listener.call(this, event);
|
|
776
|
+
};
|
|
777
|
+
return originalAddEvent(type, wrappedListener, options);
|
|
778
|
+
}
|
|
779
|
+
return originalAddEvent(type, listener, options);
|
|
780
|
+
};
|
|
781
|
+
return pc;
|
|
782
|
+
};
|
|
783
|
+
window.RTCPeerConnection.prototype = originalRTC.prototype;
|
|
784
|
+
Object.keys(originalRTC).forEach(function(key) {
|
|
785
|
+
window.RTCPeerConnection[key] = originalRTC[key];
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
})();
|
|
789
|
+
|
|
790
|
+
// ────────────────────────────────────────────────────────────────────
|
|
791
|
+
// P3: Font enumeration spoofing
|
|
792
|
+
// ────────────────────────────────────────────────────────────────────
|
|
793
|
+
(function() {
|
|
794
|
+
var standardFonts = ['Arial', 'Arial Black', 'Comic Sans MS', 'Courier New', 'Georgia', 'Impact', 'Times New Roman', 'Trebuchet MS', 'Verdana', 'Lucida Console', 'Tahoma', 'Palatino Linotype'];
|
|
795
|
+
if (document.fonts && document.fonts.check) {
|
|
796
|
+
var originalCheck = document.fonts.check.bind(document.fonts);
|
|
797
|
+
document.fonts.check = function(font, text) {
|
|
798
|
+
var fontName = font.replace(/[\\d.]+px\\s*/, '').replace(/["']/g, '').trim();
|
|
799
|
+
if (standardFonts.some(function(f) { return fontName.toLowerCase().includes(f.toLowerCase()); })) {
|
|
800
|
+
return true;
|
|
801
|
+
}
|
|
802
|
+
var hash = 0;
|
|
803
|
+
for (var i = 0; i < fontName.length; i++) {
|
|
804
|
+
hash = ((hash << 5) - hash + fontName.charCodeAt(i)) | 0;
|
|
805
|
+
}
|
|
806
|
+
return (Math.abs(hash) % 100) < 70;
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
})();
|
|
810
|
+
|
|
811
|
+
// ────────────────────────────────────────────────────────────────────
|
|
812
|
+
// #11: iframe contentWindow protection
|
|
813
|
+
// ────────────────────────────────────────────────────────────────────
|
|
814
|
+
(function() {
|
|
815
|
+
const origHTMLIFrameElement = Object.getOwnPropertyDescriptor(
|
|
816
|
+
HTMLIFrameElement.prototype, 'contentWindow'
|
|
817
|
+
);
|
|
818
|
+
if (origHTMLIFrameElement && origHTMLIFrameElement.get) {
|
|
819
|
+
Object.defineProperty(HTMLIFrameElement.prototype, 'contentWindow', {
|
|
820
|
+
get: function() {
|
|
821
|
+
const win = origHTMLIFrameElement.get.call(this);
|
|
822
|
+
if (win) {
|
|
823
|
+
try {
|
|
824
|
+
if ('webdriver' in win.navigator) {
|
|
825
|
+
delete Object.getPrototypeOf(win.navigator).webdriver;
|
|
826
|
+
}
|
|
827
|
+
} catch (e) {
|
|
828
|
+
// Cross-origin iframe — cannot access, which is fine
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return win;
|
|
832
|
+
},
|
|
833
|
+
configurable: true,
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
})();
|
|
837
|
+
|
|
838
|
+
// ────────────────────────────────────────────────────────────────────
|
|
839
|
+
// BUG-3: Worker/SharedWorker UA leak prevention
|
|
840
|
+
// ────────────────────────────────────────────────────────────────────
|
|
841
|
+
(function() {
|
|
842
|
+
var targetPlatform = ${JSON.stringify(platform)};
|
|
843
|
+
|
|
844
|
+
function buildWorkerPreamble() {
|
|
845
|
+
return 'Object.defineProperty(self.navigator, "webdriver", { get: function() { return undefined; }, configurable: true, enumerable: false });' +
|
|
846
|
+
'try { Object.defineProperty(self.navigator, "platform", { get: function() { return "' + targetPlatform + '"; }, configurable: true }); } catch(e) {}';
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (typeof Worker !== 'undefined') {
|
|
850
|
+
var OriginalWorker = Worker;
|
|
851
|
+
var preamble = buildWorkerPreamble();
|
|
852
|
+
window.Worker = function(scriptURL, options) {
|
|
853
|
+
var url = String(scriptURL);
|
|
854
|
+
var isModule = options && options.type === 'module';
|
|
855
|
+
if (!isModule && !url.startsWith('blob:')) {
|
|
856
|
+
try {
|
|
857
|
+
var wrapperCode = preamble + ';importScripts("' + url.replace(/"/g, '\\\\"') + '");';
|
|
858
|
+
var blob = new Blob([wrapperCode], { type: 'application/javascript' });
|
|
859
|
+
var blobURL = URL.createObjectURL(blob);
|
|
860
|
+
var worker = new OriginalWorker(blobURL, options);
|
|
861
|
+
URL.revokeObjectURL(blobURL);
|
|
862
|
+
return worker;
|
|
863
|
+
} catch(e) {
|
|
864
|
+
// Fall through to original constructor on any error
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return new OriginalWorker(scriptURL, options);
|
|
868
|
+
};
|
|
869
|
+
window.Worker.prototype = OriginalWorker.prototype;
|
|
870
|
+
try {
|
|
871
|
+
Object.defineProperty(window.Worker, 'length', { value: OriginalWorker.length });
|
|
872
|
+
Object.defineProperty(window.Worker, 'name', { value: 'Worker' });
|
|
873
|
+
} catch(e) {}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (typeof SharedWorker !== 'undefined') {
|
|
877
|
+
var OriginalSharedWorker = SharedWorker;
|
|
878
|
+
var sharedPreamble = buildWorkerPreamble();
|
|
879
|
+
window.SharedWorker = function(scriptURL, options) {
|
|
880
|
+
var url = String(scriptURL);
|
|
881
|
+
var nameOrOpts = options;
|
|
882
|
+
var isModule = nameOrOpts && typeof nameOrOpts === 'object' && nameOrOpts.type === 'module';
|
|
883
|
+
if (!isModule && !url.startsWith('blob:')) {
|
|
884
|
+
try {
|
|
885
|
+
var wrapperCode = sharedPreamble + ';importScripts("' + url.replace(/"/g, '\\\\"') + '");';
|
|
886
|
+
var blob = new Blob([wrapperCode], { type: 'application/javascript' });
|
|
887
|
+
var blobURL = URL.createObjectURL(blob);
|
|
888
|
+
var worker = new OriginalSharedWorker(blobURL, nameOrOpts);
|
|
889
|
+
URL.revokeObjectURL(blobURL);
|
|
890
|
+
return worker;
|
|
891
|
+
} catch(e) {}
|
|
892
|
+
}
|
|
893
|
+
return new OriginalSharedWorker(scriptURL, nameOrOpts);
|
|
894
|
+
};
|
|
895
|
+
window.SharedWorker.prototype = OriginalSharedWorker.prototype;
|
|
896
|
+
try {
|
|
897
|
+
Object.defineProperty(window.SharedWorker, 'length', { value: OriginalSharedWorker.length });
|
|
898
|
+
Object.defineProperty(window.SharedWorker, 'name', { value: 'SharedWorker' });
|
|
899
|
+
} catch(e) {}
|
|
900
|
+
}
|
|
901
|
+
})();
|
|
902
|
+
`;
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* JavaScript to inject via page.addInitScript() that patches
|
|
906
|
+
* common bot-detection vectors in the page's execution context.
|
|
907
|
+
*
|
|
908
|
+
* This script runs BEFORE page JavaScript on every navigation,
|
|
909
|
+
* including in iframes (Playwright propagates addInitScript to all frames).
|
|
910
|
+
*
|
|
911
|
+
* In passive mode, only Category A (automation signal removal) patches are applied.
|
|
912
|
+
* In active mode, both Category A and Category B (identity faking) patches are applied.
|
|
913
|
+
*/
|
|
914
|
+
getInitScript(platform, ua, fingerprint, sessionSeed, modeOverride) {
|
|
915
|
+
const mode = modeOverride ?? this.getMode();
|
|
916
|
+
if (mode === 'off')
|
|
917
|
+
return '';
|
|
918
|
+
// Passive patches always included (both passive and active modes)
|
|
919
|
+
let script = this.getPassiveInitScript();
|
|
920
|
+
// Active patches only in active mode
|
|
921
|
+
if (mode === 'active') {
|
|
922
|
+
const safePlatform = platform ?? this.getPlatformFromProcess();
|
|
923
|
+
script += '\n' + this.getActiveInitScript(safePlatform, ua, fingerprint, sessionSeed);
|
|
924
|
+
}
|
|
925
|
+
return script;
|
|
926
|
+
}
|
|
927
|
+
/**
|
|
928
|
+
* Remove Playwright-specific global variables that bot detection scripts
|
|
929
|
+
* trivially check for. These are injected by Playwright's init script
|
|
930
|
+
* mechanism and are dead giveaways of automation.
|
|
931
|
+
*/
|
|
932
|
+
getPlaywrightGlobalsCleanupScript() {
|
|
933
|
+
return `
|
|
934
|
+
// ────────────────────────────────────────────────────────────────────
|
|
935
|
+
// Phase 1.2: Delete Playwright automation globals
|
|
936
|
+
// __pwInitScripts, __playwright__binding__, __playwright are injected
|
|
937
|
+
// by Playwright's bootstrapper and are trivially detectable.
|
|
938
|
+
// ────────────────────────────────────────────────────────────────────
|
|
939
|
+
(function() {
|
|
940
|
+
var globals = ['__pwInitScripts', '__playwright__binding__', '__playwright'];
|
|
941
|
+
for (var i = 0; i < globals.length; i++) {
|
|
942
|
+
try { delete window[globals[i]]; } catch(e) {}
|
|
943
|
+
try {
|
|
944
|
+
Object.defineProperty(window, globals[i], {
|
|
945
|
+
get: function() { return undefined; },
|
|
946
|
+
set: function() {},
|
|
947
|
+
configurable: true,
|
|
948
|
+
});
|
|
949
|
+
} catch(e) {}
|
|
950
|
+
}
|
|
951
|
+
})();
|
|
952
|
+
`;
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* P1: Runtime.enable CDP detection bypass init script.
|
|
956
|
+
* Default ON — disable with LEAP_CDP_STEALTH=false.
|
|
957
|
+
*
|
|
958
|
+
* When Playwright sends Runtime.enable via CDP, bot detection scripts
|
|
959
|
+
* can observe Playwright/DevTools stack frames in Error.prepareStackTrace.
|
|
960
|
+
* This patch intercepts prepareStackTrace and filters out any frames
|
|
961
|
+
* originating from __playwright, pptr:, or devtools:// — making the
|
|
962
|
+
* stack trace indistinguishable from a real browser.
|
|
963
|
+
*
|
|
964
|
+
* This is separate from the #14 sourceURL stripping patch because it
|
|
965
|
+
* performs a deeper filter (also strips devtools:// frames) and is
|
|
966
|
+
* opt-in due to potential debugging interference.
|
|
967
|
+
*
|
|
968
|
+
* Defends against: Patchright/rebrowser CDP detection, DataDome Runtime.enable check
|
|
969
|
+
*/
|
|
970
|
+
getCdpStealthScript() {
|
|
971
|
+
return `
|
|
972
|
+
// ────────────────────────────────────────────────────────────────────
|
|
973
|
+
// P1: Runtime.enable CDP detection bypass (default ON, LEAP_CDP_STEALTH=false to disable)
|
|
974
|
+
// ────────────────────────────────────────────────────────────────────
|
|
975
|
+
(function() {
|
|
976
|
+
const originalPrepareStackTrace = Error.prepareStackTrace;
|
|
977
|
+
Error.prepareStackTrace = function(error, stack) {
|
|
978
|
+
const filtered = stack.filter(function(frame) {
|
|
979
|
+
const fileName = frame.getFileName() || '';
|
|
980
|
+
return !fileName.includes('__playwright') &&
|
|
981
|
+
!fileName.includes('pptr:') &&
|
|
982
|
+
!fileName.includes('devtools://');
|
|
983
|
+
});
|
|
984
|
+
if (originalPrepareStackTrace) {
|
|
985
|
+
return originalPrepareStackTrace(error, filtered);
|
|
986
|
+
}
|
|
987
|
+
return error.toString() + '\\n' + filtered.map(function(f) { return ' at ' + f.toString(); }).join('\\n');
|
|
988
|
+
};
|
|
989
|
+
})();
|
|
990
|
+
`;
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Whether CDP stealth mode is enabled.
|
|
994
|
+
* Default ON — disable with LEAP_CDP_STEALTH=false.
|
|
995
|
+
* The Error.prepareStackTrace filter hides Playwright/DevTools stack frames
|
|
996
|
+
* from bot detection scripts. Safe for production; only disable if you need
|
|
997
|
+
* raw stack traces for debugging.
|
|
998
|
+
*/
|
|
999
|
+
isCdpStealthEnabled() {
|
|
1000
|
+
return process.env.LEAP_CDP_STEALTH !== "false";
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Apply the stealth init script to a Playwright page.
|
|
1004
|
+
* Call this immediately after page creation, before navigating.
|
|
1005
|
+
*
|
|
1006
|
+
* Respects the current stealth mode:
|
|
1007
|
+
* - passive: applies only Category A patches (automation signal removal)
|
|
1008
|
+
* - active: applies Category A + Category B (identity faking)
|
|
1009
|
+
*
|
|
1010
|
+
* Also applies:
|
|
1011
|
+
* - Playwright globals cleanup (__pwInitScripts, __playwright__binding__, __playwright)
|
|
1012
|
+
* - CDP stealth script (default ON, disable via LEAP_CDP_STEALTH=false)
|
|
1013
|
+
*/
|
|
1014
|
+
async applyToPage(page, userAgent, fingerprint, modeOverride) {
|
|
1015
|
+
const mode = modeOverride ?? this.getMode();
|
|
1016
|
+
if (mode === 'off')
|
|
1017
|
+
return;
|
|
1018
|
+
// BUG-1 fix: Always infer platform from the UA string that the browser is actually using.
|
|
1019
|
+
const effectiveUA = userAgent ?? this.getDefaultUserAgent();
|
|
1020
|
+
const platform = this.inferPlatformFromUA(effectiveUA);
|
|
1021
|
+
// Derive a session seed from the fingerprint for deterministic PRNG (Phase 2.2/2.3)
|
|
1022
|
+
const sessionSeed = fingerprint
|
|
1023
|
+
? this.hashSeed(fingerprint.userAgent + fingerprint.webgl.renderer)
|
|
1024
|
+
: this.hashSeed(userAgent ?? 'default-session');
|
|
1025
|
+
// Sanitize sourceURL comments from all init scripts before injection (Phase 1.3)
|
|
1026
|
+
await page.addInitScript(this.sanitizeSourceURL(this.getInitScript(platform, userAgent, fingerprint, sessionSeed, mode)));
|
|
1027
|
+
// Remove trivially detectable Playwright globals that leak automation context
|
|
1028
|
+
await page.addInitScript(this.sanitizeSourceURL(this.getPlaywrightGlobalsCleanupScript()));
|
|
1029
|
+
// P1: CDP stealth (default ON, disable via LEAP_CDP_STEALTH=false)
|
|
1030
|
+
// CDP stealth is Category A (automation signal removal) — applied in both modes
|
|
1031
|
+
if (this.isCdpStealthEnabled()) {
|
|
1032
|
+
await page.addInitScript(this.sanitizeSourceURL(this.getCdpStealthScript()));
|
|
1033
|
+
}
|
|
1034
|
+
// P0 #2 post-navigation fix: Playwright re-adds navigator.webdriver AFTER
|
|
1035
|
+
// init scripts run. This listener deletes it after every frame load so both
|
|
1036
|
+
// `typeof navigator.webdriver === "undefined"` AND `'webdriver' in navigator === false`.
|
|
1037
|
+
// This is Category A — applied in both passive and active modes.
|
|
1038
|
+
page.on("framenavigated", async (frame) => {
|
|
1039
|
+
try {
|
|
1040
|
+
await frame.evaluate(() => {
|
|
1041
|
+
try {
|
|
1042
|
+
delete Object.getPrototypeOf(navigator).webdriver;
|
|
1043
|
+
delete Navigator.prototype.webdriver;
|
|
1044
|
+
delete navigator.webdriver;
|
|
1045
|
+
}
|
|
1046
|
+
catch (e) { /* cross-origin or already deleted */ }
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
catch { /* frame may be detached */ }
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Get the current stealth mode.
|
|
1054
|
+
* 'active' — full stealth (automation removal + identity faking). Default.
|
|
1055
|
+
* 'passive' — remove automation signals only, no identity faking.
|
|
1056
|
+
* 'off' — stealth completely disabled.
|
|
1057
|
+
*/
|
|
1058
|
+
getMode() {
|
|
1059
|
+
const val = process.env.LEAP_STEALTH?.toLowerCase();
|
|
1060
|
+
if (val === 'false' || val === 'off')
|
|
1061
|
+
return 'off';
|
|
1062
|
+
if (val === 'passive')
|
|
1063
|
+
return 'passive';
|
|
1064
|
+
if (val === 'auto')
|
|
1065
|
+
return 'active'; // auto mode: base is active, bandit overrides per-navigation
|
|
1066
|
+
return 'active'; // default — backwards compatible (true, unset, or any other value)
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* Whether the stealth mode is bandit-driven (LEAP_STEALTH=auto).
|
|
1070
|
+
* When true, the EXP3 bandit selects the stealth mode per-domain
|
|
1071
|
+
* per-navigation instead of using a fixed mode.
|
|
1072
|
+
*
|
|
1073
|
+
* When LEAP_STEALTH is unset or explicitly set to true/passive/false,
|
|
1074
|
+
* the user's choice takes precedence and the bandit is advisory only
|
|
1075
|
+
* (it still records outcomes for learning, but doesn't override the mode).
|
|
1076
|
+
*/
|
|
1077
|
+
isBanditMode() {
|
|
1078
|
+
const val = process.env.LEAP_STEALTH?.toLowerCase();
|
|
1079
|
+
return val === 'auto';
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Whether stealth mode is enabled (passive or active).
|
|
1083
|
+
* Returns true unless LEAP_STEALTH=false/off is set.
|
|
1084
|
+
* Backwards compatible — callers that only need to know "is stealth on at all?"
|
|
1085
|
+
* can keep using this method.
|
|
1086
|
+
*/
|
|
1087
|
+
isEnabled() {
|
|
1088
|
+
return this.getMode() !== 'off';
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* Build Sec-CH-UA header value from a Chrome version and platform.
|
|
1092
|
+
* Phase 2.4: These HTTP headers are sent BEFORE JS runs, so they must
|
|
1093
|
+
* be set via extraHTTPHeaders to stay in sync with the JS-side override.
|
|
1094
|
+
*/
|
|
1095
|
+
buildSecChUaHeaders(chromeVersion, platform) {
|
|
1096
|
+
let chPlatform = "macOS";
|
|
1097
|
+
if (platform.startsWith("Win"))
|
|
1098
|
+
chPlatform = "Windows";
|
|
1099
|
+
else if (platform.startsWith("Linux"))
|
|
1100
|
+
chPlatform = "Linux";
|
|
1101
|
+
return {
|
|
1102
|
+
"Sec-CH-UA": `"Chromium";v="${chromeVersion}", "Google Chrome";v="${chromeVersion}", "Not_A Brand";v="24"`,
|
|
1103
|
+
"Sec-CH-UA-Mobile": "?0",
|
|
1104
|
+
"Sec-CH-UA-Platform": `"${chPlatform}"`,
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* BrowserContext options to merge into context creation.
|
|
1109
|
+
* Returns stealth-appropriate defaults (user agent, locale, timezone, headers).
|
|
1110
|
+
*
|
|
1111
|
+
* In passive mode, returns minimal options (locale/timezone for consistency)
|
|
1112
|
+
* but does NOT set a faked userAgent or Sec-CH-UA headers.
|
|
1113
|
+
*
|
|
1114
|
+
* In active mode, returns the full set including faked UA and headers.
|
|
1115
|
+
*
|
|
1116
|
+
* BUG-005 / P0 #3 FIX: When a custom user agent is provided, we still return
|
|
1117
|
+
* locale/timezone/extraHTTPHeaders — only the userAgent field is omitted.
|
|
1118
|
+
*/
|
|
1119
|
+
getContextOptions(customUserAgent, fingerprint) {
|
|
1120
|
+
const mode = this.getMode();
|
|
1121
|
+
if (mode === 'off') {
|
|
1122
|
+
return {};
|
|
1123
|
+
}
|
|
1124
|
+
// Passive mode: minimal context options — no identity faking
|
|
1125
|
+
if (mode === 'passive') {
|
|
1126
|
+
return {
|
|
1127
|
+
locale: "en-US",
|
|
1128
|
+
timezoneId: "America/New_York",
|
|
1129
|
+
extraHTTPHeaders: {
|
|
1130
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
1131
|
+
},
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
1134
|
+
// Active mode: full stealth context options
|
|
1135
|
+
const ua = customUserAgent ?? this.getDefaultUserAgent();
|
|
1136
|
+
const chromeVersion = this.extractChromeMajorVersion(ua);
|
|
1137
|
+
const platform = this.inferPlatformFromUA(ua);
|
|
1138
|
+
// Phase 2.4: Sync Sec-CH-UA HTTP headers with fingerprint/UA
|
|
1139
|
+
const secChUaHeaders = this.buildSecChUaHeaders(chromeVersion, platform);
|
|
1140
|
+
const opts = {
|
|
1141
|
+
locale: "en-US",
|
|
1142
|
+
timezoneId: "America/New_York",
|
|
1143
|
+
extraHTTPHeaders: {
|
|
1144
|
+
"Accept-Language": "en-US,en;q=0.9",
|
|
1145
|
+
...secChUaHeaders,
|
|
1146
|
+
},
|
|
1147
|
+
};
|
|
1148
|
+
// Only set the default UA if no custom one was provided.
|
|
1149
|
+
// Custom UA is applied by session-manager after merging these options.
|
|
1150
|
+
if (!customUserAgent) {
|
|
1151
|
+
opts.userAgent = this.getDefaultUserAgent();
|
|
1152
|
+
}
|
|
1153
|
+
return opts;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
export const stealth = new StealthMode();
|
|
1157
|
+
export default stealth;
|