@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.
@@ -1,5 +1,5 @@
1
1
 
2
- > @poncho-ai/browser@0.3.4 build /Users/cesar/Dev/latitude/poncho-ai/packages/browser
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
  CLI Building entry: src/index.ts
@@ -7,8 +7,8 @@
7
7
  CLI tsup v8.5.1
8
8
  CLI Target: es2022
9
9
  ESM Build start
10
- ESM dist/index.js 24.62 KB
11
- ESM ⚡️ Build success in 34ms
10
+ ESM dist/index.js 34.91 KB
11
+ ESM ⚡️ Build success in 62ms
12
12
  DTS Build start
13
- DTS ⚡️ Build success in 3120ms
14
- DTS dist/index.d.ts 4.13 KB
13
+ DTS ⚡️ Build success in 4777ms
14
+ DTS dist/index.d.ts 12.44 KB
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
- export { type BrowserConfig, type BrowserFrame, BrowserSession, type BrowserStatus, type BrowserStoragePersistence, type KeyboardInputEvent, type MouseInputEvent, type ScreencastOptions, type ScrollInputEvent, type ViewportOptions, createBrowserTools };
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
- await mgr.launch({
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
- createBrowserTools
1011
+ STEALTH_ARGS,
1012
+ STEALTH_INIT_SCRIPT,
1013
+ buildStealthArgs,
1014
+ createBrowserTools,
1015
+ defaultUserAgent
759
1016
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poncho-ai/browser",
3
- "version": "0.3.4",
3
+ "version": "0.4.0",
4
4
  "description": "Browser automation for Poncho agents, powered by agent-browser",
5
5
  "repository": {
6
6
  "type": "git",
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { BrowserSession } from "./session.js";
2
2
  export { createBrowserTools } from "./tools.js";
3
+ export { defaultUserAgent, STEALTH_ARGS, buildStealthArgs, STEALTH_INIT_SCRIPT } from "./stealth.js";
3
4
  export type {
4
5
  BrowserConfig,
5
6
  BrowserFrame,
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<{ send(method: string, params?: Record<string, unknown>): Promise<unknown> }>;
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
- await mgr.launch({
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
  }
@@ -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
-  RUN  v1.6.1 /Users/cesar/Dev/latitude/poncho-ai/packages/browser
7
-
8
- include: **/*.{test,spec}.?(c|m)[jt]s?(x)
9
- exclude: **/node_modules/**, **/dist/**, **/cypress/**, **/.{idea,git,cache,output,temp}/**, **/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build,eslint,prettier}.config.*
10
- watch exclude: **/node_modules/**, **/dist/**
11
- No test files found, exiting with code 0
12
-