barebrowse 0.14.0 → 0.17.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/CHANGELOG.md +157 -0
- package/README.md +33 -1
- package/barebrowse.context.md +5 -2
- package/cli.js +10 -2
- package/mcp-server.js +14 -2
- package/package.json +1 -1
- package/src/auth.js +27 -13
- package/src/ax-snapshot.js +287 -0
- package/src/bidi.js +143 -0
- package/src/consent-firefox.js +125 -0
- package/src/consent-patterns.js +154 -0
- package/src/consent.js +5 -144
- package/src/daemon.js +162 -51
- package/src/firefox-page.js +445 -0
- package/src/firefox.js +203 -0
- package/src/index.js +88 -6
- package/src/network-idle.js +66 -28
- package/src/readable.js +32 -20
- package/src/stealth-firefox.js +42 -0
- package/src/stealth.js +107 -69
- package/types/auth.d.ts +18 -1
- package/types/ax-snapshot.d.ts +35 -0
- package/types/bidi.d.ts +10 -0
- package/types/consent-firefox.d.ts +9 -0
- package/types/consent-patterns.d.ts +12 -0
- package/types/daemon.d.ts +34 -0
- package/types/firefox-page.d.ts +21 -0
- package/types/firefox.d.ts +41 -0
- package/types/index.d.ts +13 -2
- package/types/network-idle.d.ts +16 -8
- package/types/readable.d.ts +8 -0
- package/types/stealth-firefox.d.ts +9 -0
- package/types/stealth.d.ts +31 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,162 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.17.0] - 2026-07-10
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Firefox/BiDi observability parity (parity plan Phase 2).** The Firefox
|
|
8
|
+
engine now backs the same console/network capture and network-idle wait the
|
|
9
|
+
Chromium/CDP engine has, over WebDriver BiDi events instead of CDP:
|
|
10
|
+
- **`page.waitForNetworkIdle()`** works on Firefox (was a CDP-only stub that
|
|
11
|
+
threw). Wired to `network.beforeRequestSent` / `responseCompleted` /
|
|
12
|
+
`fetchError`, reusing the same orphan-resilient idle core as the CDP path
|
|
13
|
+
(`waitForNetworkIdleBiDi` in `network-idle.js`).
|
|
14
|
+
- **Daemon console + network log capture** on Firefox: the `console-logs`,
|
|
15
|
+
`network-log`, and `wait-idle` CLI commands now return real data for a
|
|
16
|
+
`--engine firefox` session (previously empty / unsupported). Backed by
|
|
17
|
+
`log.entryAdded` + `network.*` events (`attachBiDiCapture` in `daemon.js`),
|
|
18
|
+
mirroring the CDP `Runtime.consoleAPICalled` / `Network.*` capture. BiDi's
|
|
19
|
+
`warn` console level is normalized to CDP's `warning` so `--level` filters
|
|
20
|
+
match cross-engine.
|
|
21
|
+
|
|
22
|
+
Event shapes were measured against real Firefox before wiring (per the Phase 1
|
|
23
|
+
lesson). Unit-tested against those shapes in `test/unit/network-idle.test.js`
|
|
24
|
+
and `test/unit/daemon.test.js`; live-verified in `test/integration/firefox.test.js`.
|
|
25
|
+
Still Firefox gaps (Phase 3+): ad/tracker block, dialog capture, `saveState`,
|
|
26
|
+
`waitForNavigation`, hybrid mode.
|
|
27
|
+
|
|
28
|
+
## [0.16.1] - 2026-07-10
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
|
|
32
|
+
- **Firefox consent auto-dismiss no longer risks clicking an unrelated button.**
|
|
33
|
+
A code review of v0.16.0 found (and reproduced) a false-positive: when a
|
|
34
|
+
consent *dialog* was detected but contained no in-dialog accept button, the
|
|
35
|
+
Firefox walker fell back to a page-wide "accept" scan that could auto-click an
|
|
36
|
+
unrelated `Accept all …` control elsewhere on the page (e.g. a ToS/signup
|
|
37
|
+
button) during `goto()`. The page-wide scan now runs **only** for banner-style
|
|
38
|
+
consent (no dialog container at all). Trade-off: an accept button rendered
|
|
39
|
+
*outside* its own dialog (some SourcePoint deployments) is no longer
|
|
40
|
+
auto-dismissed on Firefox — we accept that miss rather than risk a wrong
|
|
41
|
+
click. Regression-tested in `test/unit/consent-firefox.test.js`.
|
|
42
|
+
|
|
43
|
+
### Documentation
|
|
44
|
+
|
|
45
|
+
- **Firefox stealth + consent known limitations** captured in the PRD
|
|
46
|
+
(`docs/01-product/prd.md`), slated for the Phase 5 cross-engine fidelity
|
|
47
|
+
harness: the consent double AX-build per navigate (bounded latency, parity
|
|
48
|
+
with CDP), the single-click-without-re-verify behavior (`consent: false` to
|
|
49
|
+
opt out), and the latent `navigator.webdriver` own-property fallback. All
|
|
50
|
+
three were validated in the same review; none is a correctness defect on
|
|
51
|
+
current engines.
|
|
52
|
+
|
|
53
|
+
## [0.16.0] - 2026-07-10
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
|
|
57
|
+
- **Firefox anti-detection + consent auto-dismiss (BiDi parity Phase 1).** The
|
|
58
|
+
two most agent-breaking Firefox gaps are closed: headless bot-detection and
|
|
59
|
+
cookie-consent dialogs that were never dismissed. Both now work on
|
|
60
|
+
`connect({ engine: 'firefox' })` (and therefore the CLI/daemon/MCP Firefox
|
|
61
|
+
paths), behind the same flags as the CDP engine.
|
|
62
|
+
- **Stealth** (`src/stealth-firefox.js`, headless only, via BiDi
|
|
63
|
+
`script.addPreloadScript`). Deliberately *not* a port of Chromium's
|
|
64
|
+
`STEALTH_SCRIPT`: a POC measured stock Firefox-under-BiDi and found it
|
|
65
|
+
already exposes a realistic surface (no `window.chrome`, a real plugin set,
|
|
66
|
+
the real GPU, a UA with no "Headless" marker) — injecting Chromium's
|
|
67
|
+
Chrome-shims would have *created* a spoof tell worse than the one removed.
|
|
68
|
+
Firefox's only real tell is `navigator.webdriver` (`true`), so it gets a
|
|
69
|
+
minimal script: `navigator.webdriver` hiding + canvas-fingerprint noise.
|
|
70
|
+
`WEBDRIVER_PATCH` and `CANVAS_NOISE_PATCH` are now exported from
|
|
71
|
+
`stealth.js` and shared by both engines (single-sourcing the canvas
|
|
72
|
+
double-XOR fix).
|
|
73
|
+
- **Hardened `navigator.webdriver` hiding on both engines.** The shared
|
|
74
|
+
`WEBDRIVER_PATCH` now *deletes* the getter off `Navigator.prototype` instead
|
|
75
|
+
of shadowing it with an own property. A naive override hid the value but left
|
|
76
|
+
`navigator.hasOwnProperty('webdriver') === true` — a tell that advanced
|
|
77
|
+
anti-bot checks (e.g. sannysoft's "WebDriver New") detect. After the change
|
|
78
|
+
`navigator.webdriver` is `undefined` and both `hasOwnProperty` and
|
|
79
|
+
`'webdriver' in navigator` read `false`, matching a stock browser
|
|
80
|
+
(POC-verified on Chromium and Firefox). This improves the Chromium engine
|
|
81
|
+
too, not just Firefox.
|
|
82
|
+
- **Consent** (`src/consent-firefox.js`). A walker over the reconstructed
|
|
83
|
+
nested AX tree that clicks the "accept" button via the existing BiDi
|
|
84
|
+
`pointerClick`. The multilingual accept/dialog patterns were extracted to
|
|
85
|
+
`src/consent-patterns.js`, now imported by both the CDP (`consent.js`) and
|
|
86
|
+
BiDi walkers, so language coverage stays single-sourced. Runs after
|
|
87
|
+
`goto()` behind the same `consent !== false` flag as CDP.
|
|
88
|
+
- **Tests:** `test/unit/consent-firefox.test.js` (pure walker: dialog
|
|
89
|
+
scoping, pattern priority, non-English, banner fallback, false-positive
|
|
90
|
+
guard) plus Firefox integration tests (webdriver hidden at page-parse time,
|
|
91
|
+
no `window.chrome` spoof, consent auto-dismiss + a `consent: false` control
|
|
92
|
+
that proves the test can fail).
|
|
93
|
+
|
|
94
|
+
### Known gaps (Firefox engine)
|
|
95
|
+
|
|
96
|
+
- `hybrid` mode, ad/tracker blocking, and console/network log capture remain
|
|
97
|
+
Chromium-only (BiDi parity Phases 2–4). `saveState`, `waitForNavigation`,
|
|
98
|
+
and `waitForNetworkIdle` still throw a clear "not supported on the
|
|
99
|
+
Firefox/BiDi engine" error; download/dialog logs read empty.
|
|
100
|
+
- `reload()` still cannot honour `ignoreCache` (Firefox BiDi does not support
|
|
101
|
+
it yet).
|
|
102
|
+
- Consent auto-dismiss clicks once without re-verifying dismissal (the CDP
|
|
103
|
+
path's synthetic→real retry is unnecessary here — BiDi `pointerClick` is
|
|
104
|
+
already a real pointer event). Accessible-name computation remains a
|
|
105
|
+
high-value subset of the full W3C accname spec.
|
|
106
|
+
|
|
107
|
+
## [0.15.0] - 2026-07-10
|
|
108
|
+
|
|
109
|
+
### Added
|
|
110
|
+
|
|
111
|
+
- **Firefox support via WebDriver BiDi (`connect({ engine: 'firefox' })`).** CDP
|
|
112
|
+
is deprecated in Firefox, so Firefox is driven over the W3C-standard BiDi
|
|
113
|
+
protocol — a second transport (`src/bidi.js`) over the *same* `ws` dependency,
|
|
114
|
+
with no geckodriver and no new package. Selectable from the CLI
|
|
115
|
+
(`barebrowse open <url> --engine firefox`) and MCP (`BAREBROWSE_ENGINE=firefox`).
|
|
116
|
+
- New modules: `bidi.js` (BiDi JSON-RPC transport), `firefox.js` (launch/find/
|
|
117
|
+
reap), `ax-snapshot.js` (reconstructs a CDP-vocabulary ARIA tree in-page —
|
|
118
|
+
BiDi has no `getFullAXTree` — with implicit roles, accessible-name
|
|
119
|
+
computation, `aria-hidden`/visibility filtering, and shadow-DOM/`<slot>`
|
|
120
|
+
traversal), `firefox-page.js` (the BiDi-backed page object).
|
|
121
|
+
- `prune.js`, `aria.js`, and `readable.js` are reused unchanged across engines
|
|
122
|
+
(readable.js refactored to share `EXTRACT_EXPRESSION` + `finalizeReadable`).
|
|
123
|
+
- Covers `goto`, `snapshot`, `click`, `type`, `press`, `scroll`, `hover`,
|
|
124
|
+
`select`, `drag`, `upload`, `goBack`/`goForward`, `reload`, `screenshot`,
|
|
125
|
+
`pdf`, `tabs`/`switchTab`, `waitFor`, `readable`, `injectCookies`, `close`.
|
|
126
|
+
The navigation guard and upload sandbox are enforced on the Firefox path for
|
|
127
|
+
parity with CDP.
|
|
128
|
+
- Fidelity validated against real CDP snapshots; iframes (incl. nested +
|
|
129
|
+
multi-tab), shadow DOM, CSP, and SPA timing covered in
|
|
130
|
+
`test/integration/firefox.test.js` (18 tests).
|
|
131
|
+
- `goto`/`reload`/history navigation honour a timeout (a page that never
|
|
132
|
+
finishes loading rejects instead of hanging). Proxy honours the URL scheme
|
|
133
|
+
(`http`/`https` → HTTP+SSL, `socks`/`socks5`/`socks4` → SOCKS). Cookie
|
|
134
|
+
injection is host-scoped like CDP (shared `scopedCookiesForUrl`, not the
|
|
135
|
+
whole jar). Snapshot fidelity: a collapsed `<select>` surfaces as its value
|
|
136
|
+
(not every `<option>`), and bare text directly under `<body>` is kept.
|
|
137
|
+
|
|
138
|
+
- **Incognito mode — a clean, unauthenticated session (`incognito: true`).**
|
|
139
|
+
Skips ALL auth injection: no cookie extraction/injection and no
|
|
140
|
+
`storageState` loading, so the agent browses logged-out even though the
|
|
141
|
+
default throwaway profile is unchanged. The gate lives at the page-object
|
|
142
|
+
level, so it holds even when a caller (MCP `goto`, the daemon) injects
|
|
143
|
+
unconditionally. Available on `browse()`, `connect()`, both engines, MCP
|
|
144
|
+
(`browse` tool arg + `BAREBROWSE_INCOGNITO=1`), and CLI (`--incognito`).
|
|
145
|
+
Cookie injection is scoped to the target host on **both** engines via a
|
|
146
|
+
shared `scopedCookiesForUrl` (the Firefox path no longer loads the whole
|
|
147
|
+
cookie jar).
|
|
148
|
+
|
|
149
|
+
### Known gaps (Firefox engine)
|
|
150
|
+
|
|
151
|
+
- Consent auto-dismiss, stealth patches, and `hybrid` mode remain Chromium-only.
|
|
152
|
+
- `reload()` cannot honour `ignoreCache` (Firefox BiDi does not support it yet).
|
|
153
|
+
- The CLI daemon's console/network log capture is CDP-only, so those logs are
|
|
154
|
+
empty for a Firefox session. `saveState`, `waitForNavigation`, and
|
|
155
|
+
`waitForNetworkIdle` are CDP-only too — on Firefox they throw a clear
|
|
156
|
+
"not supported on the Firefox/BiDi engine" error, and download/dialog logs
|
|
157
|
+
read empty. Accessible-name computation is a high-value subset of the full
|
|
158
|
+
W3C accname spec.
|
|
159
|
+
|
|
3
160
|
## [0.14.0] - 2026-06-15
|
|
4
161
|
|
|
5
162
|
### Documentation
|
package/README.md
CHANGED
|
@@ -132,8 +132,40 @@ const snap = await page.snapshot();
|
|
|
132
132
|
await page.close(); // closes only the tab barebrowse opened — your browser keeps running
|
|
133
133
|
```
|
|
134
134
|
|
|
135
|
+
### Firefox (WebDriver BiDi)
|
|
136
|
+
|
|
137
|
+
CDP is deprecated in Firefox, so barebrowse drives it over the W3C WebDriver
|
|
138
|
+
BiDi protocol instead — a second transport over the same `ws` dependency, no
|
|
139
|
+
extra download. Same `page.*` API (the ARIA snapshot is reconstructed in-page
|
|
140
|
+
since BiDi has no `getFullAXTree`):
|
|
141
|
+
|
|
142
|
+
```js
|
|
143
|
+
const page = await connect({ engine: 'firefox' }); // headless by default
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
From the CLI: `barebrowse open <url> --engine firefox`. From MCP: set
|
|
147
|
+
`BAREBROWSE_ENGINE=firefox`. Firefox cookies (plaintext) reuse into the same
|
|
148
|
+
engine. Consent auto-dismiss and headless stealth work on Firefox too (as of
|
|
149
|
+
v0.16.0). Chromium (CDP) remains the default; `hybrid` mode, ad/tracker
|
|
150
|
+
blocking, and the daemon's console/network capture are still Chromium-only.
|
|
151
|
+
|
|
135
152
|
No clone profile, no fresh cookies — the agent sees what you see.
|
|
136
153
|
|
|
154
|
+
### Incognito (clean session)
|
|
155
|
+
|
|
156
|
+
Pass `incognito: true` for a clean, **unauthenticated** session — barebrowse
|
|
157
|
+
skips *all* auth injection (cookies + storage state), so the agent browses
|
|
158
|
+
logged out. It is not Chrome's `--incognito` flag: the session already runs in
|
|
159
|
+
a throwaway profile, so this gates the *other* auth source — your real browser
|
|
160
|
+
cookies. Works on both engines:
|
|
161
|
+
|
|
162
|
+
```js
|
|
163
|
+
const page = await connect({ incognito: true });
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
From the CLI: `barebrowse open <url> --incognito`. From MCP: set
|
|
167
|
+
`BAREBROWSE_INCOGNITO=1`.
|
|
168
|
+
|
|
137
169
|
## What it handles automatically
|
|
138
170
|
|
|
139
171
|
Cookie consent walls (29 languages, with real mouse click fallback for stubborn CMPs), login walls (cookie extraction from your browsers), bot detection (ARIA node count heuristic + stealth patches + automatic headed fallback — snapshot shows `[BOT CHALLENGE DETECTED]` warning when blocked), permission prompts, SPA navigation, JS dialogs, off-screen elements, pre-filled inputs, ARIA noise, and profile locking. The agent doesn't think about any of it.
|
|
@@ -223,7 +255,7 @@ URL -> find/launch browser (chromium.js)
|
|
|
223
255
|
## Requirements
|
|
224
256
|
|
|
225
257
|
- Node.js >= 22 (built-in WebSocket, built-in SQLite)
|
|
226
|
-
- Any Chromium-based browser installed (Chrome, Chromium, Brave, Edge, Vivaldi)
|
|
258
|
+
- Any Chromium-based browser installed (Chrome, Chromium, Brave, Edge, Vivaldi) — or Firefox (>= 121) for the `engine: 'firefox'` BiDi path
|
|
227
259
|
- Linux tested (Fedora/KDE). macOS/Windows cookie paths exist but untested.
|
|
228
260
|
|
|
229
261
|
## The bare ecosystem
|
package/barebrowse.context.md
CHANGED
|
@@ -46,6 +46,7 @@ const snapshot = await browse('https://example.com');
|
|
|
46
46
|
const snapshot = await browse('https://example.com', {
|
|
47
47
|
mode: 'headless', // 'headless' | 'headed' | 'hybrid'
|
|
48
48
|
cookies: true, // inject user's browser cookies
|
|
49
|
+
incognito: false, // clean, unauthenticated session: skip ALL auth injection
|
|
49
50
|
browser: 'firefox', // cookie source: 'firefox' | 'chromium' (auto-detected)
|
|
50
51
|
prune: true, // apply ARIA pruning (47-95% token reduction)
|
|
51
52
|
pruneMode: 'act', // 'act' (interactive elements) | 'read' (all content)
|
|
@@ -94,10 +95,12 @@ const snapshot = await browse('https://example.com', {
|
|
|
94
95
|
| `close()` | -- | void | Close page, disconnect CDP, kill browser (if headless) |
|
|
95
96
|
|
|
96
97
|
**connect() options** (in addition to mode/port/consent):
|
|
98
|
+
- `engine: 'chromium'|'firefox'` — Browser engine. Default `chromium` (CDP). `firefox` drives Firefox over WebDriver BiDi (CDP is deprecated there) — a separate transport with the same `page.*` API; the ARIA snapshot is reconstructed in-page since BiDi has no `getFullAXTree`. Consent auto-dismiss and stealth (headless anti-detection) work on Firefox as of v0.16.0; the CLI daemon's console/network capture and `waitForNetworkIdle` work as of v0.17.0. Still Chromium-only: `hybrid` mode and ad/tracker blocking. `reload()` ignores `ignoreCache` on Firefox (BiDi limitation).
|
|
97
99
|
- `port: 9222` — Attach to a Chromium already running with `--remote-debugging-port=N` instead of spawning one. The browser keeps running on `close()`. Stealth + permission denial + download capture are skipped to avoid mutating the user's running browser.
|
|
98
100
|
- `proxy: 'http://...'` — HTTP/SOCKS proxy for browser
|
|
99
101
|
- `viewport: '1280x720'` — Set viewport dimensions
|
|
100
102
|
- `storageState: 'file.json'` — Load cookies/localStorage from saved state
|
|
103
|
+
- `incognito: true|false` — Default `false`. When `true`, run a clean, unauthenticated session: skips `storageState` loading AND makes `injectCookies()` a no-op, so no auth ever enters the session even though callers (MCP `goto`, daemon) inject unconditionally. The temp profile is already throwaway; this gates the *other* auth source — the user's real browser cookies. Not Chrome's `--incognito` flag. Surfaced everywhere: library, MCP (`browse` tool param + `BAREBROWSE_INCOGNITO=1` env for the session), CLI (`--incognito`), bareagent (via `connect` opts).
|
|
101
104
|
- `downloadPath: '/abs/dir'` — Where downloads land. Default: per-session `mkdtemp` under `/tmp/barebrowse-dl-*` that gets removed on `close()`. Caller-supplied paths are not cleaned up — caller owns the lifecycle.
|
|
102
105
|
- `blockAds: true|false` — CDP-level URL blocking of 128 common ad/tracker patterns (Google ads/analytics, FB/Amazon/MS/Adobe ad+analytics, Segment/Amplitude/Mixpanel/Heap/PostHog, Hotjar/FullStory/LogRocket, Criteo/Taboola/Outbrain, the consumer-pixel cluster, AppNexus/Rubicon/PubMatic supply, marketing automation; v0.10.1 added AppsFlyer/Branch/Adjust, Cloudflare Web Analytics, Matomo Cloud). Default `true` for launched browsers, `false` in attach mode (would affect any tab in the user's running browser). Explicit `true` in attach mode is honored and follows the session across `switchTab()` (regression-tested). Shrinks ARIA snapshots and speeds page loads. On legacy Chromium lacking `Network.setBlockedURLs` a one-time `console.warn` surfaces the fallback.
|
|
103
106
|
- `blockUrls: ['*://foo.com/*', ...]` — Extra glob patterns (CDP `Network.setBlockedURLs` format) to block in addition to the default. Merged with the default unless `blockAds: false`.
|
|
@@ -186,8 +189,8 @@ barebrowse can inject cookies from the user's real browser sessions, bypassing l
|
|
|
186
189
|
|
|
187
190
|
| Obstacle | How | Mode |
|
|
188
191
|
|---|---|---|
|
|
189
|
-
| Cookie consent | ARIA scan
|
|
190
|
-
| Consent behind iframes |
|
|
192
|
+
| Cookie consent | ARIA scan for the accept button, 29 languages. CDP: `jsClick` (overlay-bypassing) then a real-mouse-click fallback for CMPs that ignore synthetic clicks. Firefox/BiDi: a single real pointer click. A page-wide accept scan runs only for banner-style consent (no dialog) — when a dialog is detected but has no in-dialog accept button, no page-wide scan runs, to avoid clicking an unrelated `Accept all …` control | Both engines |
|
|
193
|
+
| Consent behind iframes | Scanned/clicked in the owning frame (CDP `DOM.resolveNode`; Firefox resolves the ref in its browsing context) | Both engines |
|
|
191
194
|
| Permission prompts | Launch flags + CDP Browser.setPermission auto-deny | Both |
|
|
192
195
|
| Media autoplay blocked | `--autoplay-policy=no-user-gesture-required` | Both |
|
|
193
196
|
| Login walls | Cookie extraction from Firefox/Chromium + CDP injection | Both |
|
package/cli.js
CHANGED
|
@@ -108,9 +108,11 @@ async function cmdOpen() {
|
|
|
108
108
|
|
|
109
109
|
const url = args[1] && !args[1].startsWith('--') ? args[1] : undefined;
|
|
110
110
|
const opts = {
|
|
111
|
+
engine: parseFlag('--engine'),
|
|
111
112
|
mode: parseFlag('--mode') || 'headless',
|
|
112
113
|
port: parseFlag('--port'),
|
|
113
114
|
cookies: !hasFlag('--no-cookies'),
|
|
115
|
+
incognito: hasFlag('--incognito') || undefined,
|
|
114
116
|
browser: parseFlag('--browser'),
|
|
115
117
|
timeout: parseFlag('--timeout'),
|
|
116
118
|
pruneMode: parseFlag('--prune-mode') || 'act',
|
|
@@ -201,7 +203,7 @@ async function oneShot() {
|
|
|
201
203
|
const url = args[1];
|
|
202
204
|
const mode = args[2] || 'headless';
|
|
203
205
|
try {
|
|
204
|
-
const snapshot = await browse(url, { mode });
|
|
206
|
+
const snapshot = await browse(url, { mode, incognito: hasFlag('--incognito') || undefined });
|
|
205
207
|
process.stdout.write(snapshot + '\n');
|
|
206
208
|
process.exit(0);
|
|
207
209
|
} catch (err) {
|
|
@@ -213,9 +215,11 @@ async function oneShot() {
|
|
|
213
215
|
async function runDaemonInternal() {
|
|
214
216
|
const { runDaemon } = await import('./src/daemon.js');
|
|
215
217
|
const opts = {
|
|
218
|
+
engine: parseFlag('--engine'),
|
|
216
219
|
mode: parseFlag('--mode') || 'headless',
|
|
217
220
|
port: parseFlag('--port'),
|
|
218
221
|
cookies: !hasFlag('--no-cookies'),
|
|
222
|
+
incognito: hasFlag('--incognito') || undefined,
|
|
219
223
|
browser: parseFlag('--browser'),
|
|
220
224
|
timeout: parseFlag('--timeout'),
|
|
221
225
|
pruneMode: parseFlag('--prune-mode') || 'act',
|
|
@@ -480,9 +484,13 @@ Session:
|
|
|
480
484
|
barebrowse status Check session status
|
|
481
485
|
|
|
482
486
|
Open flags:
|
|
483
|
-
--
|
|
487
|
+
--engine=chromium|firefox Browser engine (default: chromium/CDP;
|
|
488
|
+
firefox drives over WebDriver BiDi)
|
|
489
|
+
--mode=headless|headed|hybrid Browser mode (default: headless;
|
|
490
|
+
hybrid is chromium-only)
|
|
484
491
|
--port=N CDP port for headed mode
|
|
485
492
|
--no-cookies Skip cookie injection
|
|
493
|
+
--incognito Clean, unauthenticated session (skip all auth injection)
|
|
486
494
|
--browser=firefox|chromium Cookie source browser
|
|
487
495
|
--timeout=N Navigation timeout in ms
|
|
488
496
|
--prune-mode=act|read Default pruning mode
|
package/mcp-server.js
CHANGED
|
@@ -121,7 +121,18 @@ let _pageConnecting = null;
|
|
|
121
121
|
async function getPage() {
|
|
122
122
|
if (_page) return _page;
|
|
123
123
|
if (_pageConnecting) return _pageConnecting;
|
|
124
|
-
|
|
124
|
+
// Engine selection is a launch-time setting (the browser persists across
|
|
125
|
+
// tool calls), so it's an env var, not a per-request flag. Firefox is driven
|
|
126
|
+
// over WebDriver BiDi and has no hybrid fallback — default it to headless
|
|
127
|
+
// (override with BAREBROWSE_MODE=headed).
|
|
128
|
+
// Incognito is a launch-time setting like engine/mode: the session persists
|
|
129
|
+
// across tool calls, so it's an env var (BAREBROWSE_INCOGNITO=1), not a
|
|
130
|
+
// per-request flag. Skips all auth injection for a clean, logged-out session.
|
|
131
|
+
const incognito = process.env.BAREBROWSE_INCOGNITO === '1';
|
|
132
|
+
const connectOpts = process.env.BAREBROWSE_ENGINE === 'firefox'
|
|
133
|
+
? { engine: 'firefox', mode: process.env.BAREBROWSE_MODE || 'headless', incognito }
|
|
134
|
+
: { mode: 'hybrid', incognito };
|
|
135
|
+
_pageConnecting = connect(connectOpts);
|
|
125
136
|
try {
|
|
126
137
|
_page = await _pageConnecting;
|
|
127
138
|
return _page;
|
|
@@ -156,6 +167,7 @@ export const TOOLS = [
|
|
|
156
167
|
url: { type: 'string', description: 'URL to browse' },
|
|
157
168
|
mode: { type: 'string', enum: ['headless', 'headed', 'hybrid'], description: 'Browser mode (default: headless)' },
|
|
158
169
|
pruneMode: { type: 'string', enum: ['act', 'read'], description: 'Pruning mode. "act" (default) keeps interactive elements and short labels — best for clicking/filling. "read" keeps paragraphs, headings, and long text — best for articles, docs, and content extraction. If the page is content-heavy and act-mode returns mostly empty, retry with "read".' },
|
|
170
|
+
incognito: { type: 'boolean', description: 'Clean, unauthenticated session: skip cookie injection so the page loads logged-out (default: false).' },
|
|
159
171
|
maxChars: { type: 'number', description: 'Max chars to return inline. Larger snapshots are saved to .barebrowse/ and a file path is returned instead. Default: 30000.' },
|
|
160
172
|
},
|
|
161
173
|
required: ['url'],
|
|
@@ -391,7 +403,7 @@ async function handleToolCall(name, args) {
|
|
|
391
403
|
case 'browse': {
|
|
392
404
|
let timer;
|
|
393
405
|
const text = await Promise.race([
|
|
394
|
-
browse(args.url, { mode: args.mode, pruneMode: args.pruneMode }),
|
|
406
|
+
browse(args.url, { mode: args.mode, pruneMode: args.pruneMode, incognito: args.incognito }),
|
|
395
407
|
new Promise((_, rej) => { timer = setTimeout(() => rej(new Error('browse timed out after 60s')), 60000); }),
|
|
396
408
|
]);
|
|
397
409
|
clearTimeout(timer);
|
package/package.json
CHANGED
package/src/auth.js
CHANGED
|
@@ -288,25 +288,39 @@ export function cookieDomainMatch(host, cookieDomain) {
|
|
|
288
288
|
}
|
|
289
289
|
|
|
290
290
|
/**
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
*
|
|
295
|
-
*
|
|
291
|
+
* Select the user's cookies that are in-scope for `url`. Shared by BOTH engines
|
|
292
|
+
* (CDP `authenticate` and Firefox/BiDi `injectCookies`) so they can't drift on
|
|
293
|
+
* scoping — a divergence here re-opens loading the entire cookie jar into an
|
|
294
|
+
* agent session. Returns the filtered cookie array; never injects.
|
|
295
|
+
*
|
|
296
|
+
* Coarse SQL pre-filter strips to a registrable-ish domain so the LIKE query
|
|
297
|
+
* returns a superset (incl. parent-domain cookies). slice(-2) is a cheap
|
|
298
|
+
* heuristic — it over-selects for multi-part eTLDs (co.uk) and as a substring
|
|
299
|
+
* match, so the precise RFC-6265 domain-match is what actually decides which
|
|
300
|
+
* cookies pass. Without it, browsing apple.com would match apple.com.evil.org
|
|
301
|
+
* and every *.co.uk site (verified).
|
|
302
|
+
* @param {string} url - URL whose host the cookies must match
|
|
303
|
+
* @param {object} [opts] - passed to extractCookies (e.g. { browser })
|
|
304
|
+
* @returns {Array<object>} cookies whose domain matches the URL host
|
|
296
305
|
*/
|
|
297
|
-
export
|
|
306
|
+
export function scopedCookiesForUrl(url, opts = {}) {
|
|
298
307
|
const fullHost = new URL(url).hostname.toLowerCase();
|
|
299
|
-
// Coarse SQL pre-filter: strip to a registrable-ish domain so the LIKE query
|
|
300
|
-
// returns a superset (incl. parent-domain cookies). slice(-2) is a cheap
|
|
301
|
-
// heuristic — it over-selects for multi-part eTLDs (co.uk) and as a substring
|
|
302
|
-
// match, so the precise RFC-6265 domain-match below is what actually decides
|
|
303
|
-
// which cookies get injected. Without it, browsing apple.com would inject
|
|
304
|
-
// cookies for apple.com.evil.org and every *.co.uk site (verified).
|
|
305
308
|
const noWww = fullHost.replace(/^www\./, '');
|
|
306
309
|
const parts = noWww.split('.');
|
|
307
310
|
const coarseDomain = parts.length > 2 ? parts.slice(-2).join('.') : noWww;
|
|
308
311
|
const candidates = extractCookies({ ...opts, domain: coarseDomain });
|
|
309
|
-
|
|
312
|
+
return candidates.filter((c) => cookieDomainMatch(fullHost, c.domain));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Extract cookies for a URL and inject them into a CDP session.
|
|
317
|
+
* Convenience function combining scopedCookiesForUrl + injectCookies.
|
|
318
|
+
* @param {object} session - CDP session handle
|
|
319
|
+
* @param {string} url - URL to extract cookies for
|
|
320
|
+
* @param {object} [opts] - Options passed to extractCookies
|
|
321
|
+
*/
|
|
322
|
+
export async function authenticate(session, url, opts = {}) {
|
|
323
|
+
const cookies = scopedCookiesForUrl(url, opts);
|
|
310
324
|
if (cookies.length > 0) {
|
|
311
325
|
await injectCookies(session, cookies);
|
|
312
326
|
}
|