@poncho-ai/browser 0.3.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +6 -0
- package/dist/index.d.ts +45 -1
- package/dist/index.js +261 -4
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/session.ts +107 -4
- package/src/stealth.ts +195 -0
- package/src/types.ts +7 -0
- package/.turbo/turbo-test.log +0 -12
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @poncho-ai/browser@0.
|
|
2
|
+
> @poncho-ai/browser@0.4.0 build /home/runner/work/poncho-ai/poncho-ai/packages/browser
|
|
3
3
|
> tsup src/index.ts --format esm --dts
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.ts
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
[34mCLI[39m tsup v8.5.1
|
|
8
8
|
[34mCLI[39m Target: es2022
|
|
9
9
|
[34mESM[39m Build start
|
|
10
|
-
[32mESM[39m [1mdist/index.js [22m[
|
|
11
|
-
[32mESM[39m ⚡️ Build success in
|
|
10
|
+
[32mESM[39m [1mdist/index.js [22m[32m34.91 KB[39m
|
|
11
|
+
[32mESM[39m ⚡️ Build success in 62ms
|
|
12
12
|
[34mDTS[39m Build start
|
|
13
|
-
[32mDTS[39m ⚡️ Build success in
|
|
14
|
-
[32mDTS[39m [1mdist/index.d.ts [22m[
|
|
13
|
+
[32mDTS[39m ⚡️ Build success in 4777ms
|
|
14
|
+
[32mDTS[39m [1mdist/index.d.ts [22m[32m12.44 KB[39m
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @poncho-ai/browser
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`d997362`](https://github.com/cesr/poncho-ai/commit/d997362b114f6e9c5d95794cedff2c7675e32ca5) Thanks [@cesr](https://github.com/cesr)! - Add stealth mode to browser automation (enabled by default). Reduces bot-detection fingerprints with a realistic Chrome user-agent, navigator.webdriver override, window.chrome shim, fake plugins, WebGL patches, and anti-automation Chrome flags. Configurable via `stealth` and `userAgent` options in `poncho.config.js`.
|
|
8
|
+
|
|
3
9
|
## 0.3.4
|
|
4
10
|
|
|
5
11
|
### Patch Changes
|
package/dist/index.d.ts
CHANGED
|
@@ -56,6 +56,13 @@ interface BrowserConfig {
|
|
|
56
56
|
sessionName?: string;
|
|
57
57
|
executablePath?: string;
|
|
58
58
|
headless?: boolean;
|
|
59
|
+
/** Custom user-agent string. When `stealth` is enabled a realistic Chrome UA
|
|
60
|
+
* is used by default — set this to override it. */
|
|
61
|
+
userAgent?: string;
|
|
62
|
+
/** Reduce bot-detection fingerprints. Defaults to `true`.
|
|
63
|
+
* Patches navigator.webdriver, injects window.chrome shim, sets a realistic
|
|
64
|
+
* user-agent, and passes anti-automation Chrome flags. */
|
|
65
|
+
stealth?: boolean;
|
|
59
66
|
storagePersistence?: BrowserStoragePersistence;
|
|
60
67
|
}
|
|
61
68
|
|
|
@@ -66,6 +73,8 @@ declare class BrowserSession {
|
|
|
66
73
|
private readonly sessionId;
|
|
67
74
|
private manager;
|
|
68
75
|
private readonly tabs;
|
|
76
|
+
private _contextStealthInstalled;
|
|
77
|
+
private readonly _uaOverrideApplied;
|
|
69
78
|
private _lockQueue;
|
|
70
79
|
private _locked;
|
|
71
80
|
private _screencastConversation;
|
|
@@ -73,6 +82,19 @@ declare class BrowserSession {
|
|
|
73
82
|
get profileDir(): string;
|
|
74
83
|
private lock;
|
|
75
84
|
private unlock;
|
|
85
|
+
private get stealthEnabled();
|
|
86
|
+
private get stealthUserAgent();
|
|
87
|
+
/**
|
|
88
|
+
* Install the stealth init script on the Playwright BrowserContext.
|
|
89
|
+
* This runs before ALL page scripts on every navigation across every tab.
|
|
90
|
+
* Only needs to be called once per browser launch.
|
|
91
|
+
*/
|
|
92
|
+
private installContextStealth;
|
|
93
|
+
/**
|
|
94
|
+
* Override the user-agent via CDP on the current page target.
|
|
95
|
+
* CDP Network.setUserAgentOverride is per-target, so call per-tab.
|
|
96
|
+
*/
|
|
97
|
+
private overrideUserAgentOnPage;
|
|
76
98
|
private launchFreshManager;
|
|
77
99
|
private ensureManager;
|
|
78
100
|
private evictOldestTab;
|
|
@@ -119,4 +141,26 @@ declare class BrowserSession {
|
|
|
119
141
|
|
|
120
142
|
declare function createBrowserTools(getSession: () => BrowserSession, getConversationId: () => string): ToolDefinition[];
|
|
121
143
|
|
|
122
|
-
|
|
144
|
+
/**
|
|
145
|
+
* Returns a realistic Chrome user-agent string for the host OS.
|
|
146
|
+
* Uses a recent stable Chrome version to blend in with normal traffic.
|
|
147
|
+
*/
|
|
148
|
+
declare function defaultUserAgent(): string;
|
|
149
|
+
/**
|
|
150
|
+
* Static Chromium flags that suppress automation fingerprints.
|
|
151
|
+
*/
|
|
152
|
+
declare const STEALTH_ARGS: string[];
|
|
153
|
+
/**
|
|
154
|
+
* Build the full stealth args array including the browser-level user-agent
|
|
155
|
+
* override. The `--user-agent` flag sets the UA globally — including in
|
|
156
|
+
* Web Workers and Service Workers — which context-level overrides can't reach.
|
|
157
|
+
*/
|
|
158
|
+
declare function buildStealthArgs(userAgent: string): string[];
|
|
159
|
+
/**
|
|
160
|
+
* JS source injected via Playwright context.addInitScript().
|
|
161
|
+
* Runs before ALL page scripts on every navigation, every tab.
|
|
162
|
+
* Each section has its own try/catch so one failure doesn't block the rest.
|
|
163
|
+
*/
|
|
164
|
+
declare const STEALTH_INIT_SCRIPT = "\n(() => {\n // 1. navigator.webdriver \u2192 false\n try {\n Object.defineProperty(Navigator.prototype, 'webdriver', {\n get: () => false,\n configurable: true,\n });\n } catch {}\n\n // 2. window.chrome stub (headless Chromium omits it)\n try {\n if (!window.chrome) {\n const chrome = {\n app: {\n isInstalled: false,\n InstallState: { DISABLED: 'disabled', INSTALLED: 'installed', NOT_INSTALLED: 'not_installed' },\n RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' },\n getDetails() { return null; },\n getIsInstalled() { return false; },\n installState() { return 'not_installed'; },\n },\n runtime: {\n OnInstalledReason: { CHROME_UPDATE: 'chrome_update', INSTALL: 'install', SHARED_MODULE_UPDATE: 'shared_module_update', UPDATE: 'update' },\n OnRestartRequiredReason: { APP_UPDATE: 'app_update', OS_UPDATE: 'os_update', PERIODIC: 'periodic' },\n PlatformArch: { ARM: 'arm', ARM64: 'arm64', MIPS: 'mips', MIPS64: 'mips64', X86_32: 'x86-32', X86_64: 'x86-64' },\n PlatformNaclArch: { ARM: 'arm', MIPS: 'mips', MIPS64: 'mips64', X86_32: 'x86-32', X86_64: 'x86-64' },\n PlatformOs: { ANDROID: 'android', CROS: 'cros', FUCHSIA: 'fuchsia', LINUX: 'linux', MAC: 'mac', OPENBSD: 'openbsd', WIN: 'win' },\n RequestUpdateCheckStatus: { NO_UPDATE: 'no_update', THROTTLED: 'throttled', UPDATE_AVAILABLE: 'update_available' },\n connect() { return { onMessage: { addListener() {}, removeListener() {} }, postMessage() {}, disconnect() {} }; },\n sendMessage() {},\n id: undefined,\n },\n csi() { return {}; },\n loadTimes() { return {}; },\n };\n Object.defineProperty(window, 'chrome', {\n value: chrome,\n writable: false,\n enumerable: true,\n configurable: false,\n });\n }\n } catch {}\n\n // 3. navigator.plugins \u2014 expose the plugins a real Chrome has\n try {\n const makeMimeType = (type, desc, suffixes, plugin) => {\n const mt = Object.create(MimeType.prototype);\n Object.defineProperties(mt, {\n type: { get: () => type },\n description: { get: () => desc },\n suffixes: { get: () => suffixes },\n enabledPlugin: { get: () => plugin },\n });\n return mt;\n };\n const fakePluginDefs = [\n { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format', hasMime: true },\n { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '', hasMime: false },\n { name: 'Native Client', filename: 'internal-nacl-plugin', description: '', hasMime: false },\n ];\n const plugins = [];\n for (const fp of fakePluginDefs) {\n const p = Object.create(Plugin.prototype);\n const mime = fp.hasMime ? makeMimeType('application/x-google-chrome-pdf', 'Portable Document Format', 'pdf', p) : null;\n Object.defineProperties(p, {\n name: { get: () => fp.name },\n filename: { get: () => fp.filename },\n description: { get: () => fp.description },\n length: { get: () => mime ? 1 : 0 },\n });\n if (mime) Object.defineProperty(p, 0, { get: () => mime });\n Object.defineProperty(p, 'item', { value: (i) => i === 0 && mime ? mime : null });\n Object.defineProperty(p, 'namedItem', { value: (n) => mime && n === mime.type ? mime : null });\n Object.defineProperty(p, Symbol.iterator, { value: function*() { if (mime) yield mime; } });\n plugins.push(p);\n }\n const pluginArray = Object.create(PluginArray.prototype);\n plugins.forEach((p, i) => Object.defineProperty(pluginArray, i, { get: () => p, enumerable: true }));\n Object.defineProperty(pluginArray, 'length', { get: () => plugins.length });\n Object.defineProperty(pluginArray, 'item', { value: (i) => plugins[i] ?? null });\n Object.defineProperty(pluginArray, 'namedItem', { value: (n) => plugins.find(p => p.name === n) ?? null });\n Object.defineProperty(pluginArray, 'refresh', { value: () => {} });\n Object.defineProperty(pluginArray, Symbol.iterator, { value: function*() { for (const p of plugins) yield p; } });\n Object.defineProperty(navigator, 'plugins', { get: () => pluginArray, configurable: true });\n } catch {}\n\n // 4. navigator.languages\n try {\n Object.defineProperty(navigator, 'languages', {\n get: () => Object.freeze(['en-US', 'en']),\n configurable: true,\n });\n } catch {}\n\n // 5. Notification.permission \u2014 return \"default\" (headless returns \"denied\")\n try {\n if (typeof Notification !== 'undefined' && Notification.permission === 'denied') {\n Object.defineProperty(Notification, 'permission', {\n get: () => 'default',\n configurable: true,\n });\n }\n } catch {}\n\n // 6. WebGL vendor/renderer \u2014 hide SwiftShader (headless giveaway)\n try {\n const UNMASKED_VENDOR = 0x9245;\n const UNMASKED_RENDERER = 0x9246;\n const spoofVendor = 'Google Inc. (Intel)';\n const spoofRenderer = 'ANGLE (Intel, Intel(R) Iris(TM) Plus Graphics, OpenGL 4.1)';\n for (const Ctx of [WebGLRenderingContext, typeof WebGL2RenderingContext !== 'undefined' ? WebGL2RenderingContext : null].filter(Boolean)) {\n const orig = Ctx.prototype.getParameter;\n Ctx.prototype.getParameter = function(param) {\n if (param === UNMASKED_VENDOR) return spoofVendor;\n if (param === UNMASKED_RENDERER) return spoofRenderer;\n return orig.call(this, param);\n };\n }\n } catch {}\n\n // 7. Prevent iframe-based detection of navigator.webdriver\n try {\n const origCreate = Document.prototype.createElement;\n Document.prototype.createElement = function(...args) {\n const el = origCreate.apply(this, args);\n if (el.nodeName === 'IFRAME') {\n const origGet = Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, 'contentWindow')?.get;\n if (origGet) {\n Object.defineProperty(el, 'contentWindow', {\n get() {\n const w = origGet.call(this);\n if (w) {\n try {\n Object.defineProperty(w.navigator, 'webdriver', { get: () => false, configurable: true });\n } catch {}\n }\n return w;\n },\n configurable: true,\n });\n }\n }\n return el;\n };\n } catch {}\n})();\n";
|
|
165
|
+
|
|
166
|
+
export { type BrowserConfig, type BrowserFrame, BrowserSession, type BrowserStatus, type BrowserStoragePersistence, type KeyboardInputEvent, type MouseInputEvent, STEALTH_ARGS, STEALTH_INIT_SCRIPT, type ScreencastOptions, type ScrollInputEvent, type ViewportOptions, buildStealthArgs, createBrowserTools, defaultUserAgent };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,182 @@
|
|
|
1
1
|
// src/session.ts
|
|
2
2
|
import { resolve, join } from "path";
|
|
3
|
-
import { homedir, tmpdir } from "os";
|
|
3
|
+
import { homedir, tmpdir, platform as platform2 } from "os";
|
|
4
4
|
import { mkdir, readFile, unlink } from "fs/promises";
|
|
5
|
+
|
|
6
|
+
// src/stealth.ts
|
|
7
|
+
import { platform } from "os";
|
|
8
|
+
function defaultUserAgent() {
|
|
9
|
+
const chromeVersion = "145.0.7632.117";
|
|
10
|
+
const os = platform();
|
|
11
|
+
if (os === "darwin") {
|
|
12
|
+
return `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
|
|
13
|
+
}
|
|
14
|
+
if (os === "win32") {
|
|
15
|
+
return `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
|
|
16
|
+
}
|
|
17
|
+
return `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
|
|
18
|
+
}
|
|
19
|
+
var STEALTH_ARGS = [
|
|
20
|
+
"--disable-blink-features=AutomationControlled",
|
|
21
|
+
"--enable-webgl",
|
|
22
|
+
"--use-gl=angle",
|
|
23
|
+
"--enable-features=VaapiVideoDecoder",
|
|
24
|
+
"--ignore-gpu-blocklist"
|
|
25
|
+
];
|
|
26
|
+
function buildStealthArgs(userAgent) {
|
|
27
|
+
return [...STEALTH_ARGS, `--user-agent=${userAgent}`];
|
|
28
|
+
}
|
|
29
|
+
var STEALTH_INIT_SCRIPT = `
|
|
30
|
+
(() => {
|
|
31
|
+
// 1. navigator.webdriver \u2192 false
|
|
32
|
+
try {
|
|
33
|
+
Object.defineProperty(Navigator.prototype, 'webdriver', {
|
|
34
|
+
get: () => false,
|
|
35
|
+
configurable: true,
|
|
36
|
+
});
|
|
37
|
+
} catch {}
|
|
38
|
+
|
|
39
|
+
// 2. window.chrome stub (headless Chromium omits it)
|
|
40
|
+
try {
|
|
41
|
+
if (!window.chrome) {
|
|
42
|
+
const chrome = {
|
|
43
|
+
app: {
|
|
44
|
+
isInstalled: false,
|
|
45
|
+
InstallState: { DISABLED: 'disabled', INSTALLED: 'installed', NOT_INSTALLED: 'not_installed' },
|
|
46
|
+
RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' },
|
|
47
|
+
getDetails() { return null; },
|
|
48
|
+
getIsInstalled() { return false; },
|
|
49
|
+
installState() { return 'not_installed'; },
|
|
50
|
+
},
|
|
51
|
+
runtime: {
|
|
52
|
+
OnInstalledReason: { CHROME_UPDATE: 'chrome_update', INSTALL: 'install', SHARED_MODULE_UPDATE: 'shared_module_update', UPDATE: 'update' },
|
|
53
|
+
OnRestartRequiredReason: { APP_UPDATE: 'app_update', OS_UPDATE: 'os_update', PERIODIC: 'periodic' },
|
|
54
|
+
PlatformArch: { ARM: 'arm', ARM64: 'arm64', MIPS: 'mips', MIPS64: 'mips64', X86_32: 'x86-32', X86_64: 'x86-64' },
|
|
55
|
+
PlatformNaclArch: { ARM: 'arm', MIPS: 'mips', MIPS64: 'mips64', X86_32: 'x86-32', X86_64: 'x86-64' },
|
|
56
|
+
PlatformOs: { ANDROID: 'android', CROS: 'cros', FUCHSIA: 'fuchsia', LINUX: 'linux', MAC: 'mac', OPENBSD: 'openbsd', WIN: 'win' },
|
|
57
|
+
RequestUpdateCheckStatus: { NO_UPDATE: 'no_update', THROTTLED: 'throttled', UPDATE_AVAILABLE: 'update_available' },
|
|
58
|
+
connect() { return { onMessage: { addListener() {}, removeListener() {} }, postMessage() {}, disconnect() {} }; },
|
|
59
|
+
sendMessage() {},
|
|
60
|
+
id: undefined,
|
|
61
|
+
},
|
|
62
|
+
csi() { return {}; },
|
|
63
|
+
loadTimes() { return {}; },
|
|
64
|
+
};
|
|
65
|
+
Object.defineProperty(window, 'chrome', {
|
|
66
|
+
value: chrome,
|
|
67
|
+
writable: false,
|
|
68
|
+
enumerable: true,
|
|
69
|
+
configurable: false,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
} catch {}
|
|
73
|
+
|
|
74
|
+
// 3. navigator.plugins \u2014 expose the plugins a real Chrome has
|
|
75
|
+
try {
|
|
76
|
+
const makeMimeType = (type, desc, suffixes, plugin) => {
|
|
77
|
+
const mt = Object.create(MimeType.prototype);
|
|
78
|
+
Object.defineProperties(mt, {
|
|
79
|
+
type: { get: () => type },
|
|
80
|
+
description: { get: () => desc },
|
|
81
|
+
suffixes: { get: () => suffixes },
|
|
82
|
+
enabledPlugin: { get: () => plugin },
|
|
83
|
+
});
|
|
84
|
+
return mt;
|
|
85
|
+
};
|
|
86
|
+
const fakePluginDefs = [
|
|
87
|
+
{ name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format', hasMime: true },
|
|
88
|
+
{ name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '', hasMime: false },
|
|
89
|
+
{ name: 'Native Client', filename: 'internal-nacl-plugin', description: '', hasMime: false },
|
|
90
|
+
];
|
|
91
|
+
const plugins = [];
|
|
92
|
+
for (const fp of fakePluginDefs) {
|
|
93
|
+
const p = Object.create(Plugin.prototype);
|
|
94
|
+
const mime = fp.hasMime ? makeMimeType('application/x-google-chrome-pdf', 'Portable Document Format', 'pdf', p) : null;
|
|
95
|
+
Object.defineProperties(p, {
|
|
96
|
+
name: { get: () => fp.name },
|
|
97
|
+
filename: { get: () => fp.filename },
|
|
98
|
+
description: { get: () => fp.description },
|
|
99
|
+
length: { get: () => mime ? 1 : 0 },
|
|
100
|
+
});
|
|
101
|
+
if (mime) Object.defineProperty(p, 0, { get: () => mime });
|
|
102
|
+
Object.defineProperty(p, 'item', { value: (i) => i === 0 && mime ? mime : null });
|
|
103
|
+
Object.defineProperty(p, 'namedItem', { value: (n) => mime && n === mime.type ? mime : null });
|
|
104
|
+
Object.defineProperty(p, Symbol.iterator, { value: function*() { if (mime) yield mime; } });
|
|
105
|
+
plugins.push(p);
|
|
106
|
+
}
|
|
107
|
+
const pluginArray = Object.create(PluginArray.prototype);
|
|
108
|
+
plugins.forEach((p, i) => Object.defineProperty(pluginArray, i, { get: () => p, enumerable: true }));
|
|
109
|
+
Object.defineProperty(pluginArray, 'length', { get: () => plugins.length });
|
|
110
|
+
Object.defineProperty(pluginArray, 'item', { value: (i) => plugins[i] ?? null });
|
|
111
|
+
Object.defineProperty(pluginArray, 'namedItem', { value: (n) => plugins.find(p => p.name === n) ?? null });
|
|
112
|
+
Object.defineProperty(pluginArray, 'refresh', { value: () => {} });
|
|
113
|
+
Object.defineProperty(pluginArray, Symbol.iterator, { value: function*() { for (const p of plugins) yield p; } });
|
|
114
|
+
Object.defineProperty(navigator, 'plugins', { get: () => pluginArray, configurable: true });
|
|
115
|
+
} catch {}
|
|
116
|
+
|
|
117
|
+
// 4. navigator.languages
|
|
118
|
+
try {
|
|
119
|
+
Object.defineProperty(navigator, 'languages', {
|
|
120
|
+
get: () => Object.freeze(['en-US', 'en']),
|
|
121
|
+
configurable: true,
|
|
122
|
+
});
|
|
123
|
+
} catch {}
|
|
124
|
+
|
|
125
|
+
// 5. Notification.permission \u2014 return "default" (headless returns "denied")
|
|
126
|
+
try {
|
|
127
|
+
if (typeof Notification !== 'undefined' && Notification.permission === 'denied') {
|
|
128
|
+
Object.defineProperty(Notification, 'permission', {
|
|
129
|
+
get: () => 'default',
|
|
130
|
+
configurable: true,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
} catch {}
|
|
134
|
+
|
|
135
|
+
// 6. WebGL vendor/renderer \u2014 hide SwiftShader (headless giveaway)
|
|
136
|
+
try {
|
|
137
|
+
const UNMASKED_VENDOR = 0x9245;
|
|
138
|
+
const UNMASKED_RENDERER = 0x9246;
|
|
139
|
+
const spoofVendor = 'Google Inc. (Intel)';
|
|
140
|
+
const spoofRenderer = 'ANGLE (Intel, Intel(R) Iris(TM) Plus Graphics, OpenGL 4.1)';
|
|
141
|
+
for (const Ctx of [WebGLRenderingContext, typeof WebGL2RenderingContext !== 'undefined' ? WebGL2RenderingContext : null].filter(Boolean)) {
|
|
142
|
+
const orig = Ctx.prototype.getParameter;
|
|
143
|
+
Ctx.prototype.getParameter = function(param) {
|
|
144
|
+
if (param === UNMASKED_VENDOR) return spoofVendor;
|
|
145
|
+
if (param === UNMASKED_RENDERER) return spoofRenderer;
|
|
146
|
+
return orig.call(this, param);
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
} catch {}
|
|
150
|
+
|
|
151
|
+
// 7. Prevent iframe-based detection of navigator.webdriver
|
|
152
|
+
try {
|
|
153
|
+
const origCreate = Document.prototype.createElement;
|
|
154
|
+
Document.prototype.createElement = function(...args) {
|
|
155
|
+
const el = origCreate.apply(this, args);
|
|
156
|
+
if (el.nodeName === 'IFRAME') {
|
|
157
|
+
const origGet = Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, 'contentWindow')?.get;
|
|
158
|
+
if (origGet) {
|
|
159
|
+
Object.defineProperty(el, 'contentWindow', {
|
|
160
|
+
get() {
|
|
161
|
+
const w = origGet.call(this);
|
|
162
|
+
if (w) {
|
|
163
|
+
try {
|
|
164
|
+
Object.defineProperty(w.navigator, 'webdriver', { get: () => false, configurable: true });
|
|
165
|
+
} catch {}
|
|
166
|
+
}
|
|
167
|
+
return w;
|
|
168
|
+
},
|
|
169
|
+
configurable: true,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return el;
|
|
174
|
+
};
|
|
175
|
+
} catch {}
|
|
176
|
+
})();
|
|
177
|
+
`;
|
|
178
|
+
|
|
179
|
+
// src/session.ts
|
|
5
180
|
var BrowserManagerCtor;
|
|
6
181
|
async function getBrowserManagerCtor() {
|
|
7
182
|
if (!BrowserManagerCtor) {
|
|
@@ -17,6 +192,10 @@ var BrowserSession = class {
|
|
|
17
192
|
manager;
|
|
18
193
|
// Tab management: conversationId → tab state
|
|
19
194
|
tabs = /* @__PURE__ */ new Map();
|
|
195
|
+
// Whether context-level stealth init script has been installed
|
|
196
|
+
_contextStealthInstalled = false;
|
|
197
|
+
// Track which tabs have had per-page CDP UA override applied
|
|
198
|
+
_uaOverrideApplied = /* @__PURE__ */ new Set();
|
|
20
199
|
// Serialization lock for tab-switching operations
|
|
21
200
|
_lockQueue = [];
|
|
22
201
|
_locked = false;
|
|
@@ -57,18 +236,80 @@ var BrowserSession = class {
|
|
|
57
236
|
// -----------------------------------------------------------------------
|
|
58
237
|
// Core browser + tab management
|
|
59
238
|
// -----------------------------------------------------------------------
|
|
239
|
+
get stealthEnabled() {
|
|
240
|
+
return this.config.stealth !== false;
|
|
241
|
+
}
|
|
242
|
+
get stealthUserAgent() {
|
|
243
|
+
if (this.config.userAgent) return this.config.userAgent;
|
|
244
|
+
if (this.stealthEnabled) return defaultUserAgent();
|
|
245
|
+
return void 0;
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Install the stealth init script on the Playwright BrowserContext.
|
|
249
|
+
* This runs before ALL page scripts on every navigation across every tab.
|
|
250
|
+
* Only needs to be called once per browser launch.
|
|
251
|
+
*/
|
|
252
|
+
async installContextStealth(mgr) {
|
|
253
|
+
if (this._contextStealthInstalled) return;
|
|
254
|
+
const ctx = mgr.getContext();
|
|
255
|
+
if (!ctx) {
|
|
256
|
+
console.warn("[poncho][browser] Cannot install stealth: no browser context");
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
await ctx.addInitScript({ content: STEALTH_INIT_SCRIPT });
|
|
261
|
+
this._contextStealthInstalled = true;
|
|
262
|
+
console.log("[poncho][browser] Stealth init script installed on context");
|
|
263
|
+
} catch (err) {
|
|
264
|
+
console.warn("[poncho][browser] Failed to install stealth init script:", err?.message ?? err);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Override the user-agent via CDP on the current page target.
|
|
269
|
+
* CDP Network.setUserAgentOverride is per-target, so call per-tab.
|
|
270
|
+
*/
|
|
271
|
+
async overrideUserAgentOnPage(mgr, conversationId) {
|
|
272
|
+
if (this._uaOverrideApplied.has(conversationId)) return;
|
|
273
|
+
const ua = this.stealthUserAgent;
|
|
274
|
+
if (!ua) return;
|
|
275
|
+
try {
|
|
276
|
+
const cdp = await mgr.getCDPSession();
|
|
277
|
+
await cdp.send("Network.setUserAgentOverride", {
|
|
278
|
+
userAgent: ua,
|
|
279
|
+
acceptLanguage: "en-US,en;q=0.9",
|
|
280
|
+
platform: platform2() === "darwin" ? "macOS" : platform2() === "win32" ? "Win32" : "Linux x86_64"
|
|
281
|
+
});
|
|
282
|
+
this._uaOverrideApplied.add(conversationId);
|
|
283
|
+
} catch (err) {
|
|
284
|
+
console.warn("[poncho][browser] Failed to override UA via CDP:", err?.message ?? err);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
60
287
|
async launchFreshManager() {
|
|
61
288
|
const Ctor = await getBrowserManagerCtor();
|
|
62
289
|
const mgr = new Ctor();
|
|
63
290
|
const viewport = this.config.viewport ?? { width: 1280, height: 720 };
|
|
64
291
|
await mkdir(this.profileDir, { recursive: true });
|
|
65
|
-
|
|
292
|
+
const launchOpts = {
|
|
66
293
|
action: "launch",
|
|
67
294
|
headless: this.config.headless ?? true,
|
|
68
295
|
viewport: { width: viewport.width ?? 1280, height: viewport.height ?? 720 },
|
|
69
296
|
executablePath: this.config.executablePath,
|
|
70
297
|
profile: this.profileDir
|
|
71
|
-
}
|
|
298
|
+
};
|
|
299
|
+
if (this.stealthEnabled) {
|
|
300
|
+
const ua = this.stealthUserAgent;
|
|
301
|
+
launchOpts.userAgent = ua;
|
|
302
|
+
launchOpts.args = buildStealthArgs(ua);
|
|
303
|
+
console.log("[poncho][browser] Launching with stealth mode enabled (UA: " + ua + ")");
|
|
304
|
+
} else if (this.config.userAgent) {
|
|
305
|
+
launchOpts.userAgent = this.config.userAgent;
|
|
306
|
+
}
|
|
307
|
+
await mgr.launch(launchOpts);
|
|
308
|
+
this._contextStealthInstalled = false;
|
|
309
|
+
this._uaOverrideApplied.clear();
|
|
310
|
+
if (this.stealthEnabled) {
|
|
311
|
+
await this.installContextStealth(mgr);
|
|
312
|
+
}
|
|
72
313
|
try {
|
|
73
314
|
const cdp = await mgr.getCDPSession();
|
|
74
315
|
await cdp.send("Debugger.disable");
|
|
@@ -89,6 +330,8 @@ var BrowserSession = class {
|
|
|
89
330
|
} catch {
|
|
90
331
|
}
|
|
91
332
|
this.manager = void 0;
|
|
333
|
+
this._contextStealthInstalled = false;
|
|
334
|
+
this._uaOverrideApplied.clear();
|
|
92
335
|
for (const [cid, tab] of this.tabs) {
|
|
93
336
|
if (tab.tabIndex >= 0) {
|
|
94
337
|
tab.tabIndex = -1;
|
|
@@ -129,6 +372,7 @@ var BrowserSession = class {
|
|
|
129
372
|
oldest.tab.url = void 0;
|
|
130
373
|
this.emitStatus(oldest.cid);
|
|
131
374
|
this.tabs.delete(oldest.cid);
|
|
375
|
+
this._uaOverrideApplied.delete(oldest.cid);
|
|
132
376
|
}
|
|
133
377
|
/** Reconcile tab indices with the manager's actual page list. */
|
|
134
378
|
async reconcileTabs(mgr) {
|
|
@@ -208,6 +452,8 @@ var BrowserSession = class {
|
|
|
208
452
|
} catch {
|
|
209
453
|
}
|
|
210
454
|
this.manager = void 0;
|
|
455
|
+
this._contextStealthInstalled = false;
|
|
456
|
+
this._uaOverrideApplied.clear();
|
|
211
457
|
for (const [, t] of this.tabs) {
|
|
212
458
|
if (t.tabIndex >= 0) {
|
|
213
459
|
t.tabIndex = -1;
|
|
@@ -225,6 +471,10 @@ var BrowserSession = class {
|
|
|
225
471
|
async _doOpen(conversationId, url) {
|
|
226
472
|
const mgr = await this.ensureManager();
|
|
227
473
|
const tab = await this.switchToConversation(mgr, conversationId);
|
|
474
|
+
if (this.stealthEnabled) {
|
|
475
|
+
await this.installContextStealth(mgr);
|
|
476
|
+
await this.overrideUserAgentOnPage(mgr, conversationId);
|
|
477
|
+
}
|
|
228
478
|
const page = mgr.getPage();
|
|
229
479
|
await page.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 });
|
|
230
480
|
tab.url = page.url();
|
|
@@ -341,6 +591,7 @@ var BrowserSession = class {
|
|
|
341
591
|
tab.url = void 0;
|
|
342
592
|
this.emitStatus(conversationId);
|
|
343
593
|
this.tabs.delete(conversationId);
|
|
594
|
+
this._uaOverrideApplied.delete(conversationId);
|
|
344
595
|
} finally {
|
|
345
596
|
this.unlock();
|
|
346
597
|
}
|
|
@@ -564,6 +815,8 @@ var BrowserSession = class {
|
|
|
564
815
|
} catch {
|
|
565
816
|
}
|
|
566
817
|
this.manager = void 0;
|
|
818
|
+
this._contextStealthInstalled = false;
|
|
819
|
+
this._uaOverrideApplied.clear();
|
|
567
820
|
for (const [cid, tab] of this.tabs) {
|
|
568
821
|
tab.active = false;
|
|
569
822
|
tab.url = void 0;
|
|
@@ -755,5 +1008,9 @@ function createBrowserTools(getSession, getConversationId) {
|
|
|
755
1008
|
}
|
|
756
1009
|
export {
|
|
757
1010
|
BrowserSession,
|
|
758
|
-
|
|
1011
|
+
STEALTH_ARGS,
|
|
1012
|
+
STEALTH_INIT_SCRIPT,
|
|
1013
|
+
buildStealthArgs,
|
|
1014
|
+
createBrowserTools,
|
|
1015
|
+
defaultUserAgent
|
|
759
1016
|
};
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
package/src/session.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { resolve, join } from "node:path";
|
|
2
|
-
import { homedir, tmpdir } from "node:os";
|
|
2
|
+
import { homedir, tmpdir, platform } from "node:os";
|
|
3
3
|
import { mkdir, readFile, unlink } from "node:fs/promises";
|
|
4
4
|
import type {
|
|
5
5
|
BrowserConfig,
|
|
@@ -10,16 +10,26 @@ import type {
|
|
|
10
10
|
KeyboardInputEvent,
|
|
11
11
|
ScrollInputEvent,
|
|
12
12
|
} from "./types.js";
|
|
13
|
+
import { defaultUserAgent, buildStealthArgs, STEALTH_INIT_SCRIPT } from "./stealth.js";
|
|
13
14
|
|
|
14
15
|
type FrameListener = (frame: BrowserFrame) => void;
|
|
15
16
|
type StatusListener = (status: BrowserStatus) => void;
|
|
16
17
|
|
|
17
18
|
let BrowserManagerCtor: (new () => BrowserManagerInstance) | undefined;
|
|
18
19
|
|
|
20
|
+
interface CDPSessionHandle {
|
|
21
|
+
send(method: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface BrowserContextHandle {
|
|
25
|
+
addInitScript(script: string | { path?: string; content?: string }): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
|
|
19
28
|
interface BrowserManagerInstance {
|
|
20
29
|
isLaunched(): boolean;
|
|
21
30
|
launch(options: Record<string, unknown>): Promise<void>;
|
|
22
31
|
getPage(): { url(): string; title(): Promise<string>; screenshot(opts?: Record<string, unknown>): Promise<Buffer>; goBack(): Promise<unknown>; goForward(): Promise<unknown>; evaluate(fn: string | (() => unknown)): Promise<unknown> };
|
|
32
|
+
getContext(): BrowserContextHandle | null;
|
|
23
33
|
getSnapshot(options?: { interactive?: boolean; compact?: boolean }): Promise<{ tree: string; refs: Record<string, unknown> }>;
|
|
24
34
|
getLocatorFromRef(ref: string): { click(): Promise<void> } | null;
|
|
25
35
|
getLocator(selector: string): { fill(text: string): Promise<void>; click(): Promise<void> };
|
|
@@ -36,7 +46,7 @@ interface BrowserManagerInstance {
|
|
|
36
46
|
isScreencasting(): boolean;
|
|
37
47
|
injectMouseEvent(params: Record<string, unknown>): Promise<void>;
|
|
38
48
|
injectKeyboardEvent(params: Record<string, unknown>): Promise<void>;
|
|
39
|
-
getCDPSession(): Promise<
|
|
49
|
+
getCDPSession(): Promise<CDPSessionHandle>;
|
|
40
50
|
saveStorageState(path: string): Promise<void>;
|
|
41
51
|
close(): Promise<void>;
|
|
42
52
|
setViewport(width: number, height: number): Promise<void>;
|
|
@@ -70,6 +80,12 @@ export class BrowserSession {
|
|
|
70
80
|
// Tab management: conversationId → tab state
|
|
71
81
|
private readonly tabs = new Map<string, ConversationTab>();
|
|
72
82
|
|
|
83
|
+
// Whether context-level stealth init script has been installed
|
|
84
|
+
private _contextStealthInstalled = false;
|
|
85
|
+
|
|
86
|
+
// Track which tabs have had per-page CDP UA override applied
|
|
87
|
+
private readonly _uaOverrideApplied = new Set<string>();
|
|
88
|
+
|
|
73
89
|
// Serialization lock for tab-switching operations
|
|
74
90
|
private _lockQueue: Array<() => void> = [];
|
|
75
91
|
private _locked = false;
|
|
@@ -116,6 +132,58 @@ export class BrowserSession {
|
|
|
116
132
|
// Core browser + tab management
|
|
117
133
|
// -----------------------------------------------------------------------
|
|
118
134
|
|
|
135
|
+
private get stealthEnabled(): boolean {
|
|
136
|
+
return this.config.stealth !== false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
private get stealthUserAgent(): string | undefined {
|
|
140
|
+
if (this.config.userAgent) return this.config.userAgent;
|
|
141
|
+
if (this.stealthEnabled) return defaultUserAgent();
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Install the stealth init script on the Playwright BrowserContext.
|
|
147
|
+
* This runs before ALL page scripts on every navigation across every tab.
|
|
148
|
+
* Only needs to be called once per browser launch.
|
|
149
|
+
*/
|
|
150
|
+
private async installContextStealth(mgr: BrowserManagerInstance): Promise<void> {
|
|
151
|
+
if (this._contextStealthInstalled) return;
|
|
152
|
+
const ctx = mgr.getContext();
|
|
153
|
+
if (!ctx) {
|
|
154
|
+
console.warn("[poncho][browser] Cannot install stealth: no browser context");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
await ctx.addInitScript({ content: STEALTH_INIT_SCRIPT });
|
|
159
|
+
this._contextStealthInstalled = true;
|
|
160
|
+
console.log("[poncho][browser] Stealth init script installed on context");
|
|
161
|
+
} catch (err) {
|
|
162
|
+
console.warn("[poncho][browser] Failed to install stealth init script:", (err as Error)?.message ?? err);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Override the user-agent via CDP on the current page target.
|
|
168
|
+
* CDP Network.setUserAgentOverride is per-target, so call per-tab.
|
|
169
|
+
*/
|
|
170
|
+
private async overrideUserAgentOnPage(mgr: BrowserManagerInstance, conversationId: string): Promise<void> {
|
|
171
|
+
if (this._uaOverrideApplied.has(conversationId)) return;
|
|
172
|
+
const ua = this.stealthUserAgent;
|
|
173
|
+
if (!ua) return;
|
|
174
|
+
try {
|
|
175
|
+
const cdp = await mgr.getCDPSession();
|
|
176
|
+
await cdp.send("Network.setUserAgentOverride", {
|
|
177
|
+
userAgent: ua,
|
|
178
|
+
acceptLanguage: "en-US,en;q=0.9",
|
|
179
|
+
platform: platform() === "darwin" ? "macOS" : platform() === "win32" ? "Win32" : "Linux x86_64",
|
|
180
|
+
});
|
|
181
|
+
this._uaOverrideApplied.add(conversationId);
|
|
182
|
+
} catch (err) {
|
|
183
|
+
console.warn("[poncho][browser] Failed to override UA via CDP:", (err as Error)?.message ?? err);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
119
187
|
private async launchFreshManager(): Promise<BrowserManagerInstance> {
|
|
120
188
|
const Ctor = await getBrowserManagerCtor();
|
|
121
189
|
const mgr = new Ctor();
|
|
@@ -123,13 +191,33 @@ export class BrowserSession {
|
|
|
123
191
|
const viewport = this.config.viewport ?? { width: 1280, height: 720 };
|
|
124
192
|
await mkdir(this.profileDir, { recursive: true });
|
|
125
193
|
|
|
126
|
-
|
|
194
|
+
const launchOpts: Record<string, unknown> = {
|
|
127
195
|
action: "launch",
|
|
128
196
|
headless: this.config.headless ?? true,
|
|
129
197
|
viewport: { width: viewport.width ?? 1280, height: viewport.height ?? 720 },
|
|
130
198
|
executablePath: this.config.executablePath,
|
|
131
199
|
profile: this.profileDir,
|
|
132
|
-
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
if (this.stealthEnabled) {
|
|
203
|
+
const ua = this.stealthUserAgent!;
|
|
204
|
+
launchOpts.userAgent = ua;
|
|
205
|
+
launchOpts.args = buildStealthArgs(ua);
|
|
206
|
+
console.log("[poncho][browser] Launching with stealth mode enabled (UA: " + ua + ")");
|
|
207
|
+
} else if (this.config.userAgent) {
|
|
208
|
+
launchOpts.userAgent = this.config.userAgent;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
await mgr.launch(launchOpts as Parameters<BrowserManagerInstance["launch"]>[0]);
|
|
212
|
+
|
|
213
|
+
// Reset stealth tracking for fresh browser
|
|
214
|
+
this._contextStealthInstalled = false;
|
|
215
|
+
this._uaOverrideApplied.clear();
|
|
216
|
+
|
|
217
|
+
// Install context-level stealth (covers all tabs, all navigations)
|
|
218
|
+
if (this.stealthEnabled) {
|
|
219
|
+
await this.installContextStealth(mgr);
|
|
220
|
+
}
|
|
133
221
|
|
|
134
222
|
try {
|
|
135
223
|
const cdp = await mgr.getCDPSession();
|
|
@@ -149,6 +237,8 @@ export class BrowserSession {
|
|
|
149
237
|
// Manager exists but is dead/stale -- discard it
|
|
150
238
|
try { await this.manager.close(); } catch { /* */ }
|
|
151
239
|
this.manager = undefined;
|
|
240
|
+
this._contextStealthInstalled = false;
|
|
241
|
+
this._uaOverrideApplied.clear();
|
|
152
242
|
// Clear tab state since they belonged to the dead browser
|
|
153
243
|
for (const [cid, tab] of this.tabs) {
|
|
154
244
|
if (tab.tabIndex >= 0) {
|
|
@@ -186,6 +276,7 @@ export class BrowserSession {
|
|
|
186
276
|
oldest.tab.url = undefined;
|
|
187
277
|
this.emitStatus(oldest.cid);
|
|
188
278
|
this.tabs.delete(oldest.cid);
|
|
279
|
+
this._uaOverrideApplied.delete(oldest.cid);
|
|
189
280
|
}
|
|
190
281
|
|
|
191
282
|
/** Reconcile tab indices with the manager's actual page list. */
|
|
@@ -267,6 +358,8 @@ export class BrowserSession {
|
|
|
267
358
|
console.log("[poncho][browser] Browser died mid-open, relaunching...");
|
|
268
359
|
try { await this.manager?.close(); } catch { /* */ }
|
|
269
360
|
this.manager = undefined;
|
|
361
|
+
this._contextStealthInstalled = false;
|
|
362
|
+
this._uaOverrideApplied.clear();
|
|
270
363
|
for (const [, t] of this.tabs) {
|
|
271
364
|
if (t.tabIndex >= 0) { t.tabIndex = -1; t.active = false; t.url = undefined; }
|
|
272
365
|
}
|
|
@@ -281,6 +374,13 @@ export class BrowserSession {
|
|
|
281
374
|
private async _doOpen(conversationId: string, url: string): Promise<{ title?: string }> {
|
|
282
375
|
const mgr = await this.ensureManager();
|
|
283
376
|
const tab = await this.switchToConversation(mgr, conversationId);
|
|
377
|
+
|
|
378
|
+
// Ensure context-level stealth is installed (covers reused managers too)
|
|
379
|
+
if (this.stealthEnabled) {
|
|
380
|
+
await this.installContextStealth(mgr);
|
|
381
|
+
await this.overrideUserAgentOnPage(mgr, conversationId);
|
|
382
|
+
}
|
|
383
|
+
|
|
284
384
|
const page = mgr.getPage();
|
|
285
385
|
|
|
286
386
|
await (page as unknown as { goto(url: string, opts?: Record<string, unknown>): Promise<unknown> })
|
|
@@ -402,6 +502,7 @@ export class BrowserSession {
|
|
|
402
502
|
tab.url = undefined;
|
|
403
503
|
this.emitStatus(conversationId);
|
|
404
504
|
this.tabs.delete(conversationId);
|
|
505
|
+
this._uaOverrideApplied.delete(conversationId);
|
|
405
506
|
} finally {
|
|
406
507
|
this.unlock();
|
|
407
508
|
}
|
|
@@ -632,6 +733,8 @@ export class BrowserSession {
|
|
|
632
733
|
await this.persistStorageState();
|
|
633
734
|
try { await this.manager?.close(); } catch { /* */ }
|
|
634
735
|
this.manager = undefined;
|
|
736
|
+
this._contextStealthInstalled = false;
|
|
737
|
+
this._uaOverrideApplied.clear();
|
|
635
738
|
for (const [cid, tab] of this.tabs) {
|
|
636
739
|
tab.active = false;
|
|
637
740
|
tab.url = undefined;
|
package/src/stealth.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { platform } from "node:os";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns a realistic Chrome user-agent string for the host OS.
|
|
5
|
+
* Uses a recent stable Chrome version to blend in with normal traffic.
|
|
6
|
+
*/
|
|
7
|
+
export function defaultUserAgent(): string {
|
|
8
|
+
const chromeVersion = "145.0.7632.117";
|
|
9
|
+
const os = platform();
|
|
10
|
+
|
|
11
|
+
if (os === "darwin") {
|
|
12
|
+
return `Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (os === "win32") {
|
|
16
|
+
return `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return `Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${chromeVersion} Safari/537.36`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Static Chromium flags that suppress automation fingerprints.
|
|
24
|
+
*/
|
|
25
|
+
export const STEALTH_ARGS: string[] = [
|
|
26
|
+
"--disable-blink-features=AutomationControlled",
|
|
27
|
+
"--enable-webgl",
|
|
28
|
+
"--use-gl=angle",
|
|
29
|
+
"--enable-features=VaapiVideoDecoder",
|
|
30
|
+
"--ignore-gpu-blocklist",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Build the full stealth args array including the browser-level user-agent
|
|
35
|
+
* override. The `--user-agent` flag sets the UA globally — including in
|
|
36
|
+
* Web Workers and Service Workers — which context-level overrides can't reach.
|
|
37
|
+
*/
|
|
38
|
+
export function buildStealthArgs(userAgent: string): string[] {
|
|
39
|
+
return [...STEALTH_ARGS, `--user-agent=${userAgent}`];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* JS source injected via Playwright context.addInitScript().
|
|
44
|
+
* Runs before ALL page scripts on every navigation, every tab.
|
|
45
|
+
* Each section has its own try/catch so one failure doesn't block the rest.
|
|
46
|
+
*/
|
|
47
|
+
export const STEALTH_INIT_SCRIPT = `
|
|
48
|
+
(() => {
|
|
49
|
+
// 1. navigator.webdriver → false
|
|
50
|
+
try {
|
|
51
|
+
Object.defineProperty(Navigator.prototype, 'webdriver', {
|
|
52
|
+
get: () => false,
|
|
53
|
+
configurable: true,
|
|
54
|
+
});
|
|
55
|
+
} catch {}
|
|
56
|
+
|
|
57
|
+
// 2. window.chrome stub (headless Chromium omits it)
|
|
58
|
+
try {
|
|
59
|
+
if (!window.chrome) {
|
|
60
|
+
const chrome = {
|
|
61
|
+
app: {
|
|
62
|
+
isInstalled: false,
|
|
63
|
+
InstallState: { DISABLED: 'disabled', INSTALLED: 'installed', NOT_INSTALLED: 'not_installed' },
|
|
64
|
+
RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' },
|
|
65
|
+
getDetails() { return null; },
|
|
66
|
+
getIsInstalled() { return false; },
|
|
67
|
+
installState() { return 'not_installed'; },
|
|
68
|
+
},
|
|
69
|
+
runtime: {
|
|
70
|
+
OnInstalledReason: { CHROME_UPDATE: 'chrome_update', INSTALL: 'install', SHARED_MODULE_UPDATE: 'shared_module_update', UPDATE: 'update' },
|
|
71
|
+
OnRestartRequiredReason: { APP_UPDATE: 'app_update', OS_UPDATE: 'os_update', PERIODIC: 'periodic' },
|
|
72
|
+
PlatformArch: { ARM: 'arm', ARM64: 'arm64', MIPS: 'mips', MIPS64: 'mips64', X86_32: 'x86-32', X86_64: 'x86-64' },
|
|
73
|
+
PlatformNaclArch: { ARM: 'arm', MIPS: 'mips', MIPS64: 'mips64', X86_32: 'x86-32', X86_64: 'x86-64' },
|
|
74
|
+
PlatformOs: { ANDROID: 'android', CROS: 'cros', FUCHSIA: 'fuchsia', LINUX: 'linux', MAC: 'mac', OPENBSD: 'openbsd', WIN: 'win' },
|
|
75
|
+
RequestUpdateCheckStatus: { NO_UPDATE: 'no_update', THROTTLED: 'throttled', UPDATE_AVAILABLE: 'update_available' },
|
|
76
|
+
connect() { return { onMessage: { addListener() {}, removeListener() {} }, postMessage() {}, disconnect() {} }; },
|
|
77
|
+
sendMessage() {},
|
|
78
|
+
id: undefined,
|
|
79
|
+
},
|
|
80
|
+
csi() { return {}; },
|
|
81
|
+
loadTimes() { return {}; },
|
|
82
|
+
};
|
|
83
|
+
Object.defineProperty(window, 'chrome', {
|
|
84
|
+
value: chrome,
|
|
85
|
+
writable: false,
|
|
86
|
+
enumerable: true,
|
|
87
|
+
configurable: false,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
} catch {}
|
|
91
|
+
|
|
92
|
+
// 3. navigator.plugins — expose the plugins a real Chrome has
|
|
93
|
+
try {
|
|
94
|
+
const makeMimeType = (type, desc, suffixes, plugin) => {
|
|
95
|
+
const mt = Object.create(MimeType.prototype);
|
|
96
|
+
Object.defineProperties(mt, {
|
|
97
|
+
type: { get: () => type },
|
|
98
|
+
description: { get: () => desc },
|
|
99
|
+
suffixes: { get: () => suffixes },
|
|
100
|
+
enabledPlugin: { get: () => plugin },
|
|
101
|
+
});
|
|
102
|
+
return mt;
|
|
103
|
+
};
|
|
104
|
+
const fakePluginDefs = [
|
|
105
|
+
{ name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format', hasMime: true },
|
|
106
|
+
{ name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '', hasMime: false },
|
|
107
|
+
{ name: 'Native Client', filename: 'internal-nacl-plugin', description: '', hasMime: false },
|
|
108
|
+
];
|
|
109
|
+
const plugins = [];
|
|
110
|
+
for (const fp of fakePluginDefs) {
|
|
111
|
+
const p = Object.create(Plugin.prototype);
|
|
112
|
+
const mime = fp.hasMime ? makeMimeType('application/x-google-chrome-pdf', 'Portable Document Format', 'pdf', p) : null;
|
|
113
|
+
Object.defineProperties(p, {
|
|
114
|
+
name: { get: () => fp.name },
|
|
115
|
+
filename: { get: () => fp.filename },
|
|
116
|
+
description: { get: () => fp.description },
|
|
117
|
+
length: { get: () => mime ? 1 : 0 },
|
|
118
|
+
});
|
|
119
|
+
if (mime) Object.defineProperty(p, 0, { get: () => mime });
|
|
120
|
+
Object.defineProperty(p, 'item', { value: (i) => i === 0 && mime ? mime : null });
|
|
121
|
+
Object.defineProperty(p, 'namedItem', { value: (n) => mime && n === mime.type ? mime : null });
|
|
122
|
+
Object.defineProperty(p, Symbol.iterator, { value: function*() { if (mime) yield mime; } });
|
|
123
|
+
plugins.push(p);
|
|
124
|
+
}
|
|
125
|
+
const pluginArray = Object.create(PluginArray.prototype);
|
|
126
|
+
plugins.forEach((p, i) => Object.defineProperty(pluginArray, i, { get: () => p, enumerable: true }));
|
|
127
|
+
Object.defineProperty(pluginArray, 'length', { get: () => plugins.length });
|
|
128
|
+
Object.defineProperty(pluginArray, 'item', { value: (i) => plugins[i] ?? null });
|
|
129
|
+
Object.defineProperty(pluginArray, 'namedItem', { value: (n) => plugins.find(p => p.name === n) ?? null });
|
|
130
|
+
Object.defineProperty(pluginArray, 'refresh', { value: () => {} });
|
|
131
|
+
Object.defineProperty(pluginArray, Symbol.iterator, { value: function*() { for (const p of plugins) yield p; } });
|
|
132
|
+
Object.defineProperty(navigator, 'plugins', { get: () => pluginArray, configurable: true });
|
|
133
|
+
} catch {}
|
|
134
|
+
|
|
135
|
+
// 4. navigator.languages
|
|
136
|
+
try {
|
|
137
|
+
Object.defineProperty(navigator, 'languages', {
|
|
138
|
+
get: () => Object.freeze(['en-US', 'en']),
|
|
139
|
+
configurable: true,
|
|
140
|
+
});
|
|
141
|
+
} catch {}
|
|
142
|
+
|
|
143
|
+
// 5. Notification.permission — return "default" (headless returns "denied")
|
|
144
|
+
try {
|
|
145
|
+
if (typeof Notification !== 'undefined' && Notification.permission === 'denied') {
|
|
146
|
+
Object.defineProperty(Notification, 'permission', {
|
|
147
|
+
get: () => 'default',
|
|
148
|
+
configurable: true,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
} catch {}
|
|
152
|
+
|
|
153
|
+
// 6. WebGL vendor/renderer — hide SwiftShader (headless giveaway)
|
|
154
|
+
try {
|
|
155
|
+
const UNMASKED_VENDOR = 0x9245;
|
|
156
|
+
const UNMASKED_RENDERER = 0x9246;
|
|
157
|
+
const spoofVendor = 'Google Inc. (Intel)';
|
|
158
|
+
const spoofRenderer = 'ANGLE (Intel, Intel(R) Iris(TM) Plus Graphics, OpenGL 4.1)';
|
|
159
|
+
for (const Ctx of [WebGLRenderingContext, typeof WebGL2RenderingContext !== 'undefined' ? WebGL2RenderingContext : null].filter(Boolean)) {
|
|
160
|
+
const orig = Ctx.prototype.getParameter;
|
|
161
|
+
Ctx.prototype.getParameter = function(param) {
|
|
162
|
+
if (param === UNMASKED_VENDOR) return spoofVendor;
|
|
163
|
+
if (param === UNMASKED_RENDERER) return spoofRenderer;
|
|
164
|
+
return orig.call(this, param);
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
} catch {}
|
|
168
|
+
|
|
169
|
+
// 7. Prevent iframe-based detection of navigator.webdriver
|
|
170
|
+
try {
|
|
171
|
+
const origCreate = Document.prototype.createElement;
|
|
172
|
+
Document.prototype.createElement = function(...args) {
|
|
173
|
+
const el = origCreate.apply(this, args);
|
|
174
|
+
if (el.nodeName === 'IFRAME') {
|
|
175
|
+
const origGet = Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, 'contentWindow')?.get;
|
|
176
|
+
if (origGet) {
|
|
177
|
+
Object.defineProperty(el, 'contentWindow', {
|
|
178
|
+
get() {
|
|
179
|
+
const w = origGet.call(this);
|
|
180
|
+
if (w) {
|
|
181
|
+
try {
|
|
182
|
+
Object.defineProperty(w.navigator, 'webdriver', { get: () => false, configurable: true });
|
|
183
|
+
} catch {}
|
|
184
|
+
}
|
|
185
|
+
return w;
|
|
186
|
+
},
|
|
187
|
+
configurable: true,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return el;
|
|
192
|
+
};
|
|
193
|
+
} catch {}
|
|
194
|
+
})();
|
|
195
|
+
`;
|
package/src/types.ts
CHANGED
|
@@ -62,5 +62,12 @@ export interface BrowserConfig {
|
|
|
62
62
|
sessionName?: string;
|
|
63
63
|
executablePath?: string;
|
|
64
64
|
headless?: boolean;
|
|
65
|
+
/** Custom user-agent string. When `stealth` is enabled a realistic Chrome UA
|
|
66
|
+
* is used by default — set this to override it. */
|
|
67
|
+
userAgent?: string;
|
|
68
|
+
/** Reduce bot-detection fingerprints. Defaults to `true`.
|
|
69
|
+
* Patches navigator.webdriver, injects window.chrome shim, sets a realistic
|
|
70
|
+
* user-agent, and passes anti-automation Chrome flags. */
|
|
71
|
+
stealth?: boolean;
|
|
65
72
|
storagePersistence?: BrowserStoragePersistence;
|
|
66
73
|
}
|
package/.turbo/turbo-test.log
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
> @poncho-ai/browser@0.3.0 test /Users/cesar/Dev/latitude/poncho-ai/packages/browser
|
|
3
|
-
> vitest --passWithNoTests
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
[7m[1m[36m RUN [39m[22m[27m [36mv1.6.1[39m [90m/Users/cesar/Dev/latitude/poncho-ai/packages/browser[39m
|
|
7
|
-
|
|
8
|
-
[2minclude: [22m[33m**/*.{test,spec}.?(c|m)[jt]s?(x)[39m
|
|
9
|
-
[2mexclude: [22m[33m**/node_modules/**[2m, [22m**/dist/**[2m, [22m**/cypress/**[2m, [22m**/.{idea,git,cache,output,temp}/**[2m, [22m**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*[39m
|
|
10
|
-
[2mwatch exclude: [22m[33m**/node_modules/**[2m, [22m**/dist/**[39m
|
|
11
|
-
No test files found, exiting with code 0
|
|
12
|
-
|