Add Firefox cookie import and stealth mode

- Firefox cookie importer: reads cookies.sqlite with WAL-safe copy,
  profile detection via profiles.ini, cross-platform paths, domain filtering
- Stealth mode: opt-in via launch(stealth: true), patches navigator.webdriver,
  plugins/mimeTypes, permissions API, WebGL renderer, iframe isolation,
  languages, plus realistic Safari UA and context hardening
- Import tool now accepts 'safari' | 'firefox' source
- STEALTH.md reference documentation
- Upgraded @types/node to v25 for node:sqlite support

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-12 23:03:15 +02:00
parent 8ca72632b2
commit 1d3192cffd
19 changed files with 1189 additions and 67 deletions
+257
View File
@@ -0,0 +1,257 @@
# Anti-Bot Stealth Reference
Research notes on making Playwright WebKit less detectable by anti-bot systems. Compiled April 2026.
## Current State
Browse uses **Playwright WebKit** with a bare context — no stealth patches. This is trivially detected by every major anti-bot system (Cloudflare, DataDome, PerimeterX/HUMAN, Akamai).
### Detection Vectors
| Vector | Severity | Fixable from JS? |
|--------|----------|------------------|
| `navigator.webdriver` set to `true` | Critical | Yes |
| Empty `navigator.plugins` / `mimeTypes` | High | Yes |
| Default viewport (800x600-ish) | High | Yes |
| Missing/generic User-Agent | High | Yes |
| WebGL renderer = SwiftShader / generic | Medium | Yes |
| Permissions API inconsistencies | Medium | Yes |
| iframe cross-frame fingerprinting | Medium | Yes |
| TLS fingerprint (JA3/JA4) | Critical | **No** |
| IP reputation (datacenter IPs) | Critical | **No** |
| ML behavioral analysis | High | **No** |
| Cloudflare Turnstile / JS challenges | High | **No** |
## Stealth Ecosystem & WebKit
The two main stealth libraries **only support Chromium**:
- **`playwright-stealth`** (Python) — patches ~12 Chrome-specific APIs
- **`playwright-extra`** + stealth plugin (Node.js) — ~17 evasion modules targeting Chrome internals
WebKit and Firefox have entirely different internals. No stealth plugin exists for either. All patches for WebKit must be applied manually via `addInitScript()`.
## Recommended Patches
All patches use `context.addInitScript()` which runs before any page script in **any** Playwright engine (WebKit included).
### 1. WebDriver Flag
The single most important patch. Set to `undefined`, not `false` — some detectors specifically check for `false` as a signal of patching.
```typescript
await context.addInitScript(() => {
Object.defineProperty(navigator, 'webdriver', {
get: () => undefined,
});
});
```
### 2. Context Hardening
Configure the browser context to look like a real Safari session:
```typescript
const context = await browser.newContext({
viewport: { width: 1920, height: 1080 },
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15',
locale: 'en-US',
timezoneId: 'Europe/Warsaw',
colorScheme: 'light',
extraHTTPHeaders: {
'Accept-Language': 'en-US,en;q=0.9',
},
});
```
Key points:
- Viewport should be realistic (1920x1080, 1440x900, 1536x864)
- User-Agent must match the engine — use a Safari UA for WebKit
- Locale, timezone, and Accept-Language should be consistent with each other
### 3. Plugins & MimeTypes
Headless reports empty arrays. Fake them:
```typescript
await context.addInitScript(() => {
Object.defineProperty(navigator, 'plugins', {
get: () => [1, 2, 3, 4, 5],
});
Object.defineProperty(navigator, 'mimeTypes', {
get: () => [1, 2],
});
});
```
A more sophisticated version would create proper `PluginArray` and `MimeTypeArray` objects with `item()`, `namedItem()`, and `refresh()` methods, but the simple version passes most checks.
### 4. Permissions API
Fix the inconsistency between `Notification.permission` and `navigator.permissions.query`:
```typescript
await context.addInitScript(() => {
const originalQuery = window.navigator.permissions.query;
window.navigator.permissions.query = (parameters: any) =>
parameters.name === 'notifications'
? Promise.resolve({ state: Notification.permission } as PermissionStatus)
: originalQuery(parameters);
});
```
### 5. WebGL Renderer
Mask the GPU vendor/renderer strings. Parameters 37445 and 37446 are `UNMASKED_VENDOR_WEBGL` and `UNMASKED_RENDERER_WEBGL`:
```typescript
await context.addInitScript(() => {
const getParameter = WebGLRenderingContext.prototype.getParameter;
WebGLRenderingContext.prototype.getParameter = function (parameter) {
if (parameter === 37445) return 'Apple GPU';
if (parameter === 37446) return 'Apple M1 Pro';
return getParameter.call(this, parameter);
};
});
```
Choose values that match the User-Agent. Apple GPU + Apple Silicon for Safari on macOS.
### 6. iframe ContentWindow Isolation
Some fingerprinters check `navigator.webdriver` inside iframes to catch incomplete patches:
```typescript
await context.addInitScript(() => {
const desc = Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, 'contentWindow');
Object.defineProperty(HTMLIFrameElement.prototype, 'contentWindow', {
get: function () {
const win = desc?.get?.call(this);
if (win) {
try {
Object.defineProperty(win.navigator, 'webdriver', {
get: () => undefined,
});
} catch (_) {}
}
return win;
},
});
});
```
### 7. Session Persistence
Fresh browser contexts with no cookies or history are a strong bot signal. Use browse's existing `session_save` / `session_restore` tools to persist cookies, localStorage, and sessionStorage across runs.
## What Cannot Be Fixed from JavaScript
### TLS Fingerprinting (JA3/JA4)
Anti-bot systems fingerprint the TLS Client Hello handshake — cipher suites, extensions, and their ordering. WebKit's TLS stack is compiled C++; no amount of JavaScript can change it. Playwright WebKit's JA3 hash doesn't match any shipping Safari release.
**Workarounds:**
- Residential proxies with TLS relay (proxy terminates TLS with its own stack)
- `curl-impersonate` for non-browser HTTP requests
- Switch to Chromium where TLS fingerprint matches real Chrome more closely
### IP Reputation
Datacenter IPs (Hetzner, AWS, GCP, etc.) are pre-flagged in commercial anti-bot databases.
**Workarounds:**
- Residential proxy rotation (BrightData, Oxylabs, etc.)
- Mobile proxies
- Running from a real residential IP (home connection)
### Behavioral Analysis
DataDome, Cloudflare, and PerimeterX use ML models trained on billions of real sessions. They analyze:
- Mouse movement patterns (speed, acceleration, curves)
- Scroll behavior (chunked vs smooth, pause patterns)
- Typing cadence
- Navigation timing
- Click patterns (direct element clicks vs natural approach)
**Workarounds:**
- Add realistic delays between actions (`page.waitForTimeout(random)`)
- Simulate mouse movements before clicks
- Scroll in chunks with pauses
- Type character by character with variable delays
### CAPTCHA / JavaScript Challenges
Cloudflare Turnstile, hCaptcha, and reCAPTCHA require real interaction or solving services.
**Workarounds:**
- CAPTCHA solving APIs: CapSolver, 2Captcha (~$2-5 per 1,000 solves)
- Wait for challenge resolution: 3-8 seconds after navigation
- Detect challenge pages by checking for known markers (`"Just a moment"`, `cf-challenge`, `_cf_chl_opt`)
## Implementation Strategy
### Recommended: Stealth Flag
Add an opt-in `stealth` option to `launch()`:
```typescript
async launch(options?: { stealth?: boolean }): Promise<void> {
this.browser = await webkit.launch({ headless: this.options.headless });
this.context = await this.browser.newContext({
viewport: { width: this.options.width, height: this.options.height },
...(options?.stealth && {
userAgent: SAFARI_USER_AGENT,
locale: 'en-US',
timezoneId: Intl.DateTimeFormat().resolvedOptions().timeZone,
colorScheme: 'light',
extraHTTPHeaders: { 'Accept-Language': 'en-US,en;q=0.9' },
}),
});
if (options?.stealth) {
await this.applyStealthPatches();
}
this.page = await this.context.newPage();
}
```
This keeps the default clean for testing while allowing stealth for real-world browsing.
### Nuclear Option: Chromium Engine
If stealth becomes a core requirement, add a `browser` engine option:
```typescript
launch({ engine: 'chromium', stealth: true })
```
Chromium has the richest stealth ecosystem:
- `playwright-extra` + stealth plugin (17 evasion modules)
- `playwright-with-fingerprints` (full fingerprint replacement)
- Better TLS fingerprint match to real Chrome
- Most anti-bot systems are tuned for Chrome, so evasions are better tested
Trade-off: Chromium is ~200MB heavier than WebKit.
## Anti-Bot Provider Cheat Sheet
| Provider | Primary Detection | Difficulty |
|----------|-------------------|------------|
| Cloudflare (standard) | TLS + JS challenge | Medium |
| Cloudflare (Turnstile) | Interactive challenge | Hard |
| DataDome | Behavioral analysis | Hard |
| PerimeterX / HUMAN | Deep fingerprinting (`_px` scripts) | Hard |
| Akamai Bot Manager | TLS + sensor data | Hard |
| Kasada | Obfuscated JS challenge | Very Hard |
| Basic WAFs | User-Agent + rate limiting | Easy |
## References
- [Playwright Anti-Bot Detection: What Works (2026) | AlterLab](https://alterlab.io/blog/playwright-anti-bot-detection-what-actually-works-in-2026)
- [Playwright Stealth: Bypass Bot Detection | Scrapfly](https://scrapfly.io/blog/posts/playwright-stealth-bypass-bot-detection)
- [Playwright Stealth Mode: The 7 Patches That Matter | DEV Community](https://dev.to/vhub_systems_ed5641f65d59/playwright-stealth-mode-in-2026-the-7-patches-that-actually-matter-46bp)
- [How to Avoid Bot Detection with Playwright | BrowserStack](https://www.browserstack.com/guide/playwright-bot-detection)
- [How To Make Playwright Undetectable | ScrapeOps](https://scrapeops.io/playwright-web-scraping-playbook/nodejs-playwright-make-playwright-undetectable/)
- [Detecting Vanilla Playwright | ScrapingAnt](https://scrapingant.com/blog/detect-playwright-bot)
- [Playwright Fingerprinting: Explained & Bypass | ZenRows](https://www.zenrows.com/blog/playwright-fingerprint)
+7
View File
@@ -13,6 +13,13 @@ export declare class ClaudeBrowser {
private interceptPatterns; private interceptPatterns;
constructor(options?: BrowserOptions); constructor(options?: BrowserOptions);
launch(): Promise<void>; launch(): Promise<void>;
/**
* Apply stealth patches via addInitScript.
* These run before any page script in all Playwright engines (WebKit included).
* Scripts are passed as strings since they execute in browser context, not Node.
* See STEALTH.md for full documentation.
*/
private applyStealthPatches;
private enterFullscreen; private enterFullscreen;
private previewAction; private previewAction;
private setupErrorListener; private setupErrorListener;
+1 -1
View File
@@ -1 +1 @@
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"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"} {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../src/browser.ts"],"names":[],"mappings":"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"}
+120 -1
View File
@@ -3,6 +3,7 @@ import { resolve } from 'node:path';
import { promisify } from 'node:util'; import { promisify } from 'node:util';
import { webkit } from 'playwright'; import { webkit } from 'playwright';
const execAsync = promisify(exec); const execAsync = promisify(exec);
import * as firefox from './firefox.js';
import * as image from './image.js'; import * as image from './image.js';
import * as safari from './safari.js'; import * as safari from './safari.js';
export class ClaudeBrowser { export class ClaudeBrowser {
@@ -24,16 +25,32 @@ export class ClaudeBrowser {
fullscreen: options.fullscreen ?? false, fullscreen: options.fullscreen ?? false,
preview: options.preview ?? false, preview: options.preview ?? false,
previewDelay: options.previewDelay ?? 2000, previewDelay: options.previewDelay ?? 2000,
stealth: options.stealth ?? false,
}; };
} }
async launch() { async launch() {
this.browser = await webkit.launch({ headless: this.options.headless }); this.browser = await webkit.launch({ headless: this.options.headless });
this.context = await this.browser.newContext({ const contextOptions = {
viewport: { viewport: {
width: this.options.width, width: this.options.width,
height: this.options.height, height: this.options.height,
}, },
};
if (this.options.stealth) {
Object.assign(contextOptions, {
userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15',
locale: 'en-US',
timezoneId: Intl.DateTimeFormat().resolvedOptions().timeZone,
colorScheme: 'light',
extraHTTPHeaders: {
'Accept-Language': 'en-US,en;q=0.9',
},
}); });
}
this.context = await this.browser.newContext(contextOptions);
if (this.options.stealth) {
await this.applyStealthPatches();
}
this.page = await this.context.newPage(); this.page = await this.context.newPage();
this.setupConsoleListener(this.page); this.setupConsoleListener(this.page);
this.setupNetworkListener(this.page); this.setupNetworkListener(this.page);
@@ -43,6 +60,85 @@ export class ClaudeBrowser {
await this.enterFullscreen(); await this.enterFullscreen();
} }
} }
/**
* Apply stealth patches via addInitScript.
* These run before any page script in all Playwright engines (WebKit included).
* Scripts are passed as strings since they execute in browser context, not Node.
* See STEALTH.md for full documentation.
*/
async applyStealthPatches() {
if (!this.context)
return;
// 1. WebDriver flag — set to undefined, not false
// Some detectors specifically check for false as a signal of patching
await this.context.addInitScript(`
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
`);
// 2. Plugins & MimeTypes — headless reports empty arrays
await this.context.addInitScript(`
Object.defineProperty(navigator, 'plugins', {
get: () => [
{ name: 'PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
{ name: 'Chrome PDF Viewer', filename: 'internal-pdf-viewer', description: '' },
{ name: 'Chromium PDF Viewer', filename: 'internal-pdf-viewer', description: '' },
],
});
Object.defineProperty(navigator, 'mimeTypes', {
get: () => [
{ type: 'application/pdf', suffixes: 'pdf', description: 'Portable Document Format' },
],
});
`);
// 3. Permissions API — fix notifications query inconsistency
await this.context.addInitScript(`
const __origQuery = navigator.permissions.query.bind(navigator.permissions);
Object.defineProperty(navigator.permissions, 'query', {
value: (params) =>
params.name === 'notifications'
? Promise.resolve({ state: Notification.permission })
: __origQuery(params),
});
`);
// 4. WebGL renderer masking — spoof GPU vendor/renderer
// Params 37445 (UNMASKED_VENDOR_WEBGL) and 37446 (UNMASKED_RENDERER_WEBGL)
await this.context.addInitScript(`
const __origGetParam = WebGLRenderingContext.prototype.getParameter;
WebGLRenderingContext.prototype.getParameter = function(p) {
if (p === 37445) return 'Apple GPU';
if (p === 37446) return 'Apple M1 Pro';
return __origGetParam.call(this, p);
};
if (typeof WebGL2RenderingContext !== 'undefined') {
const __origGetParam2 = WebGL2RenderingContext.prototype.getParameter;
WebGL2RenderingContext.prototype.getParameter = function(p) {
if (p === 37445) return 'Apple GPU';
if (p === 37446) return 'Apple M1 Pro';
return __origGetParam2.call(this, p);
};
}
`);
// 5. iframe contentWindow isolation — apply webdriver patch in child frames
await this.context.addInitScript(`
const __iframeDesc = Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, 'contentWindow');
if (__iframeDesc) {
Object.defineProperty(HTMLIFrameElement.prototype, 'contentWindow', {
get: function() {
const win = __iframeDesc.get?.call(this);
if (win) {
try {
Object.defineProperty(win.navigator, 'webdriver', { get: () => undefined });
} catch(e) {}
}
return win;
},
});
}
`);
// 6. Languages consistency
await this.context.addInitScript(`
Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });
`);
}
async enterFullscreen() { async enterFullscreen() {
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
console.warn('Native fullscreen only supported on macOS'); console.warn('Native fullscreen only supported on macOS');
@@ -699,6 +795,29 @@ export class ClaudeBrowser {
domains, domains,
}; };
} }
if (cmd.source === 'firefox') {
const cookies = firefox.importFirefoxCookies({
domain: cmd.domain,
profile: cmd.profile,
});
if (cookies.length === 0) {
return {
ok: true,
imported: 0,
source: 'firefox',
domains: [],
};
}
const playwrightCookies = cookies.map(firefox.toPlaywrightCookie);
await context.addCookies(playwrightCookies);
const domains = [...new Set(cookies.map((c) => c.domain))];
return {
ok: true,
imported: cookies.length,
source: 'firefox',
domains,
};
}
return { ok: false, error: `Unknown import source: ${cmd.source}` }; return { ok: false, error: `Unknown import source: ${cmd.source}` };
} }
async executeCommand(cmd) { async executeCommand(cmd) {
+1 -1
View File
File diff suppressed because one or more lines are too long
+58
View File
@@ -0,0 +1,58 @@
/**
* Firefox cookie importer
*
* Reads cookies from Firefox's cookies.sqlite database.
* Firefox stores cookies as plain unencrypted SQLite — no binary parsing needed.
*
* Database schema (moz_cookies table):
* id, originAttributes, name, value, host, path, expiry,
* lastAccessed, creationTime, isSecure, isHttpOnly,
* inBrowserElement, sameSite, rawSameSite, schemeMap
*
* Note: expiry is Unix seconds, but lastAccessed/creationTime are microseconds.
*
* Firefox holds an exclusive WAL lock while running, so we copy the database
* files (cookies.sqlite + WAL + SHM) to a temp directory before reading.
*/
export interface FirefoxCookie {
name: string;
value: string;
domain: string;
path: string;
expires: number;
secure: boolean;
httpOnly: boolean;
sameSite: 'None' | 'Lax' | 'Strict';
}
interface FirefoxProfile {
name: string;
path: string;
isRelative: boolean;
isDefault: boolean;
}
/**
* List available Firefox profiles
*/
export declare function listFirefoxProfiles(): FirefoxProfile[];
/**
* Import cookies from Firefox's cookies.sqlite database
*/
export declare function importFirefoxCookies(options?: {
profile?: string;
domain?: string;
}): FirefoxCookie[];
/**
* Convert FirefoxCookie to Playwright cookie format
*/
export declare function toPlaywrightCookie(cookie: FirefoxCookie): {
name: string;
value: string;
domain: string;
path: string;
expires: number;
secure: boolean;
httpOnly: boolean;
sameSite: 'Strict' | 'Lax' | 'None';
};
export {};
//# sourceMappingURL=firefox.d.ts.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"firefox.d.ts","sourceRoot":"","sources":["../src/firefox.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAOH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;CACrC;AAED,UAAU,cAAc;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,OAAO,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;CACpB;AAuFD;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,cAAc,EAAE,CAItD;AAsFD;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,CAAC,EAAE;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GAAG,aAAa,EAAE,CAmClB;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,aAAa,GAAG;IACzD,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,QAAQ,GAAG,KAAK,GAAG,MAAM,CAAC;CACrC,CAWA"}
+229
View File
@@ -0,0 +1,229 @@
/**
* Firefox cookie importer
*
* Reads cookies from Firefox's cookies.sqlite database.
* Firefox stores cookies as plain unencrypted SQLite — no binary parsing needed.
*
* Database schema (moz_cookies table):
* id, originAttributes, name, value, host, path, expiry,
* lastAccessed, creationTime, isSecure, isHttpOnly,
* inBrowserElement, sameSite, rawSameSite, schemeMap
*
* Note: expiry is Unix seconds, but lastAccessed/creationTime are microseconds.
*
* Firefox holds an exclusive WAL lock while running, so we copy the database
* files (cookies.sqlite + WAL + SHM) to a temp directory before reading.
*/
import { copyFileSync, existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
import { homedir, platform, tmpdir } from 'node:os';
import { join } from 'node:path';
import { DatabaseSync } from 'node:sqlite';
/**
* Finalize a partial profile into a full FirefoxProfile if valid
*/
function finalizeProfile(partial) {
if (!partial?.name || !partial?.path)
return null;
return {
name: partial.name,
path: partial.path,
isRelative: partial.isRelative ?? true,
isDefault: partial.isDefault ?? false,
};
}
/**
* Apply a key=value line to a partial profile
*/
function applyProfileField(profile, key, value) {
if (key === 'Name')
profile.name = value;
else if (key === 'Path')
profile.path = value;
else if (key === 'IsRelative')
profile.isRelative = value === '1';
else if (key === 'Default')
profile.isDefault = value === '1';
}
/**
* Process a single line of profiles.ini, updating state
*/
function processIniLine(line, current, profiles) {
const trimmed = line.trim();
if (trimmed.startsWith('[Profile') || trimmed.startsWith('[Install')) {
const finalized = finalizeProfile(current);
if (finalized)
profiles.push(finalized);
return trimmed.startsWith('[Profile') ? {} : null;
}
if (current) {
const eqIdx = trimmed.indexOf('=');
if (eqIdx !== -1)
applyProfileField(current, trimmed.slice(0, eqIdx), trimmed.slice(eqIdx + 1));
}
return current;
}
/**
* Parse Firefox profiles.ini to find available profiles
*/
function parseProfilesIni(iniPath) {
if (!existsSync(iniPath))
return [];
const lines = readFileSync(iniPath, 'utf-8').split('\n');
const profiles = [];
let current = null;
for (const line of lines) {
current = processIniLine(line, current, profiles);
}
const last = finalizeProfile(current);
if (last)
profiles.push(last);
return profiles;
}
/**
* Get the Firefox profiles root directory for the current platform
*/
function getFirefoxRoot() {
const home = homedir();
switch (platform()) {
case 'darwin':
return join(home, 'Library/Application Support/Firefox');
case 'linux':
return join(home, '.mozilla/firefox');
case 'win32':
return join(process.env.APPDATA || join(home, 'AppData/Roaming'), 'Mozilla/Firefox');
default:
throw new Error(`Unsupported platform: ${platform()}`);
}
}
/**
* List available Firefox profiles
*/
export function listFirefoxProfiles() {
const root = getFirefoxRoot();
const iniPath = join(root, 'profiles.ini');
return parseProfilesIni(iniPath);
}
/**
* Resolve a FirefoxProfile to its absolute path
*/
function profileToAbsolutePath(root, p) {
return p.isRelative ? join(root, p.path) : p.path;
}
/**
* Resolve the full path to a Firefox profile directory
*/
function resolveProfilePath(profile) {
const root = getFirefoxRoot();
const profiles = listFirefoxProfiles();
if (!profile) {
const defaultProfile = profiles.find((p) => p.isDefault) || profiles[0];
if (!defaultProfile)
throw new Error('No Firefox profiles found. Is Firefox installed?');
return profileToAbsolutePath(root, defaultProfile);
}
// Try exact match by name or path
const match = profiles.find((p) => p.name === profile || p.path === profile);
if (match)
return profileToAbsolutePath(root, match);
// Try as direct path fragment in Profiles dir
const directPath = join(root, 'Profiles', profile);
if (existsSync(directPath))
return directPath;
// Try as absolute path
if (existsSync(profile))
return profile;
const available = profiles.map((p) => p.name).join(', ');
throw new Error(`Firefox profile not found: "${profile}". Available: ${available}`);
}
/**
* Safely copy the Firefox cookies database to a temp directory.
* Copies cookies.sqlite + WAL + SHM files to avoid lock conflicts.
*/
function copyDatabaseSafely(dbPath) {
if (!existsSync(dbPath)) {
throw new Error(`Firefox cookies database not found at: ${dbPath}\nMake sure Firefox has been used at least once.`);
}
const tmpDir = mkdtempSync(join(tmpdir(), 'browse-fx-'));
const dbName = 'cookies.sqlite';
try {
// Copy main database
copyFileSync(dbPath, join(tmpDir, dbName));
// Copy WAL and SHM if they exist (needed for up-to-date reads)
for (const ext of ['-wal', '-shm']) {
const src = `${dbPath}${ext}`;
if (existsSync(src)) {
copyFileSync(src, join(tmpDir, `${dbName}${ext}`));
}
}
return { tmpDir, tmpDbPath: join(tmpDir, dbName) };
}
catch (err) {
// Clean up on failure
rmSync(tmpDir, { recursive: true, force: true });
throw err;
}
}
/**
* Convert Firefox sameSite integer to string
* 0 = None, 1 = Lax, 2 = Strict
*/
function sameSiteToString(value) {
switch (value) {
case 2:
return 'Strict';
case 1:
return 'Lax';
default:
return 'None';
}
}
/**
* Import cookies from Firefox's cookies.sqlite database
*/
export function importFirefoxCookies(options) {
const profilePath = resolveProfilePath(options?.profile);
const dbPath = join(profilePath, 'cookies.sqlite');
const { tmpDir, tmpDbPath } = copyDatabaseSafely(dbPath);
try {
const db = new DatabaseSync(tmpDbPath, { readOnly: true });
let query = 'SELECT name, value, host, path, expiry, isSecure, isHttpOnly, sameSite FROM moz_cookies';
const params = [];
if (options?.domain) {
const domain = options.domain.toLowerCase();
query += ' WHERE LOWER(host) = ? OR LOWER(host) = ? OR LOWER(host) LIKE ?';
params.push(domain, `.${domain}`, `%.${domain}`);
}
const stmt = db.prepare(query);
const rows = params.length > 0 ? stmt.all(...params) : stmt.all();
db.close();
return rows.map((row) => ({
name: row.name,
value: row.value,
domain: row.host,
path: row.path,
expires: row.expiry,
secure: row.isSecure === 1,
httpOnly: row.isHttpOnly === 1,
sameSite: sameSiteToString(row.sameSite),
}));
}
finally {
rmSync(tmpDir, { recursive: true, force: true });
}
}
/**
* Convert FirefoxCookie to Playwright cookie format
*/
export function toPlaywrightCookie(cookie) {
return {
name: cookie.name,
value: cookie.value,
domain: cookie.domain,
path: cookie.path,
expires: cookie.expires,
secure: cookie.secure,
httpOnly: cookie.httpOnly,
sameSite: cookie.sameSite,
};
}
//# sourceMappingURL=firefox.js.map
+1
View File
File diff suppressed because one or more lines are too long
Vendored
+16 -5
View File
@@ -18,6 +18,7 @@ let browserOptions = {
fullscreen: false, fullscreen: false,
preview: false, preview: false,
previewDelay: 2000, previewDelay: 2000,
stealth: false,
}; };
let browser = new ClaudeBrowser(browserOptions); let browser = new ClaudeBrowser(browserOptions);
let launched = false; let launched = false;
@@ -72,7 +73,12 @@ server.tool('launch', 'Launch the browser with specific options. Call before got
previewDelay: z.number().optional().default(2000).describe('Preview highlight duration in ms'), previewDelay: z.number().optional().default(2000).describe('Preview highlight duration in ms'),
width: z.number().optional().default(1280).describe('Viewport width'), width: z.number().optional().default(1280).describe('Viewport width'),
height: z.number().optional().default(800).describe('Viewport height'), height: z.number().optional().default(800).describe('Viewport height'),
}, withLogging('launch', async ({ headed, fullscreen, preview, previewDelay, width, height }) => { stealth: z
.boolean()
.optional()
.default(false)
.describe('Enable stealth mode to reduce bot detection. Patches navigator.webdriver, plugins, WebGL, permissions, and sets a realistic Safari user-agent. See STEALTH.md for details.'),
}, withLogging('launch', async ({ headed, fullscreen, preview, previewDelay, width, height, stealth }) => {
// Close existing browser if launched // Close existing browser if launched
if (launched) { if (launched) {
await browser.close(); await browser.close();
@@ -86,6 +92,7 @@ server.tool('launch', 'Launch the browser with specific options. Call before got
fullscreen, fullscreen,
preview, preview,
previewDelay, previewDelay,
stealth,
}; };
// Create new browser with updated options // Create new browser with updated options
browser = new ClaudeBrowser(browserOptions); browser = new ClaudeBrowser(browserOptions);
@@ -93,12 +100,13 @@ server.tool('launch', 'Launch the browser with specific options. Call before got
launched = true; launched = true;
return textResult(JSON.stringify({ return textResult(JSON.stringify({
ok: true, ok: true,
message: 'Browser launched', message: `Browser launched${stealth ? ' (stealth mode)' : ''}`,
options: { options: {
headed: !browserOptions.headless, headed: !browserOptions.headless,
fullscreen, fullscreen,
preview, preview,
previewDelay, previewDelay,
stealth,
viewport: { width, height }, viewport: { width, height },
}, },
})); }));
@@ -447,13 +455,16 @@ server.tool('session_restore', 'Restore a previously saved session state from a
})); }));
})); }));
// Browser import // Browser import
server.tool('import', 'Import cookies from Safari browser (macOS only). Requires Full Disk Access permission.', { server.tool('import', 'Import cookies from Safari or Firefox browser. Safari requires Full Disk Access permission (macOS only). Firefox works on macOS, Linux, and Windows.', {
source: z.enum(['safari']).describe('Browser to import from (currently only Safari supported)'), source: z.enum(['safari', 'firefox']).describe('Browser to import from'),
domain: z domain: z
.string() .string()
.optional() .optional()
.describe('Filter cookies to specific domain (e.g., "github.com")'), .describe('Filter cookies to specific domain (e.g., "github.com")'),
profile: z.string().optional().describe('Safari profile/WebKit data store ID (optional)'), profile: z
.string()
.optional()
.describe('Safari profile/WebKit data store ID, or Firefox profile name (optional)'),
}, withLogging('import', async ({ source, domain, profile }) => { }, withLogging('import', async ({ source, domain, profile }) => {
await ensureLaunched(); await ensureLaunched();
const result = await browser.executeCommand({ cmd: 'import', source, domain, profile }); const result = await browser.executeCommand({ cmd: 'import', source, domain, profile });
+1 -1
View File
File diff suppressed because one or more lines are too long
+2 -1
View File
@@ -5,6 +5,7 @@ export interface BrowserOptions {
fullscreen?: boolean; fullscreen?: boolean;
preview?: boolean; preview?: boolean;
previewDelay?: number; previewDelay?: number;
stealth?: boolean;
} }
export interface ElementInfo { export interface ElementInfo {
tag: string; tag: string;
@@ -254,7 +255,7 @@ export interface EmulateCommand {
} }
export interface ImportCommand { export interface ImportCommand {
cmd: 'import'; cmd: 'import';
source: 'safari'; source: 'safari' | 'firefox';
domain?: string; domain?: string;
profile?: string; profile?: string;
} }
+1 -1
View File
File diff suppressed because one or more lines are too long
+11 -11
View File
@@ -1,13 +1,13 @@
{ {
"name": "@saiden/browse", "name": "@saiden/browse",
"version": "0.2.20", "version": "0.2.20-8ca7263",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@saiden/browse", "name": "@saiden/browse",
"version": "0.2.20", "version": "0.2.20-8ca7263",
"license": "MIT", "license": "BUSL-1.1",
"dependencies": { "dependencies": {
"@modelcontextprotocol/sdk": "^1.26.0", "@modelcontextprotocol/sdk": "^1.26.0",
"chalk": "^5.6.2", "chalk": "^5.6.2",
@@ -21,7 +21,7 @@
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^1.9.0", "@biomejs/biome": "^1.9.0",
"@types/node": "^20.11.0", "@types/node": "^25.6.0",
"@types/sharp": "^0.31.1", "@types/sharp": "^0.31.1",
"@vitest/coverage-v8": "^4.0.18", "@vitest/coverage-v8": "^4.0.18",
"tsx": "^4.21.0", "tsx": "^4.21.0",
@@ -1636,13 +1636,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.19.33", "version": "25.6.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.33.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
"integrity": "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw==", "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~7.19.0"
} }
}, },
"node_modules/@types/sharp": { "node_modules/@types/sharp": {
@@ -3479,9 +3479,9 @@
} }
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "6.21.0", "version": "7.19.2",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "@saiden/browse", "name": "@saiden/browse",
"version": "0.2.20", "version": "0.2.20-8ca7263",
"description": "Headless browser automation for Claude Code using Playwright WebKit", "description": "Headless browser automation for Claude Code using Playwright WebKit",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
@@ -56,7 +56,7 @@
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^1.9.0", "@biomejs/biome": "^1.9.0",
"@types/node": "^20.11.0", "@types/node": "^25.6.0",
"@types/sharp": "^0.31.1", "@types/sharp": "^0.31.1",
"@vitest/coverage-v8": "^4.0.18", "@vitest/coverage-v8": "^4.0.18",
"tsx": "^4.21.0", "tsx": "^4.21.0",
+137 -1
View File
@@ -4,6 +4,7 @@ import { promisify } from 'node:util';
import { type Browser, type BrowserContext, type Page, type Route, webkit } from 'playwright'; import { type Browser, type BrowserContext, type Page, type Route, webkit } from 'playwright';
const execAsync = promisify(exec); const execAsync = promisify(exec);
import * as firefox from './firefox.js';
import * as image from './image.js'; import * as image from './image.js';
import * as safari from './safari.js'; import * as safari from './safari.js';
import type { import type {
@@ -49,17 +50,39 @@ export class ClaudeBrowser {
fullscreen: options.fullscreen ?? false, fullscreen: options.fullscreen ?? false,
preview: options.preview ?? false, preview: options.preview ?? false,
previewDelay: options.previewDelay ?? 2000, previewDelay: options.previewDelay ?? 2000,
stealth: options.stealth ?? false,
}; };
} }
async launch(): Promise<void> { async launch(): Promise<void> {
this.browser = await webkit.launch({ headless: this.options.headless }); this.browser = await webkit.launch({ headless: this.options.headless });
this.context = await this.browser.newContext({
const contextOptions: Record<string, unknown> = {
viewport: { viewport: {
width: this.options.width, width: this.options.width,
height: this.options.height, height: this.options.height,
}, },
};
if (this.options.stealth) {
Object.assign(contextOptions, {
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.5 Safari/605.1.15',
locale: 'en-US',
timezoneId: Intl.DateTimeFormat().resolvedOptions().timeZone,
colorScheme: 'light' as const,
extraHTTPHeaders: {
'Accept-Language': 'en-US,en;q=0.9',
},
}); });
}
this.context = await this.browser.newContext(contextOptions);
if (this.options.stealth) {
await this.applyStealthPatches();
}
this.page = await this.context.newPage(); this.page = await this.context.newPage();
this.setupConsoleListener(this.page); this.setupConsoleListener(this.page);
this.setupNetworkListener(this.page); this.setupNetworkListener(this.page);
@@ -71,6 +94,91 @@ export class ClaudeBrowser {
} }
} }
/**
* Apply stealth patches via addInitScript.
* These run before any page script in all Playwright engines (WebKit included).
* Scripts are passed as strings since they execute in browser context, not Node.
* See STEALTH.md for full documentation.
*/
private async applyStealthPatches(): Promise<void> {
if (!this.context) return;
// 1. WebDriver flag — set to undefined, not false
// Some detectors specifically check for false as a signal of patching
await this.context.addInitScript(`
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
`);
// 2. Plugins & MimeTypes — headless reports empty arrays
await this.context.addInitScript(`
Object.defineProperty(navigator, 'plugins', {
get: () => [
{ name: 'PDF Viewer', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
{ name: 'Chrome PDF Viewer', filename: 'internal-pdf-viewer', description: '' },
{ name: 'Chromium PDF Viewer', filename: 'internal-pdf-viewer', description: '' },
],
});
Object.defineProperty(navigator, 'mimeTypes', {
get: () => [
{ type: 'application/pdf', suffixes: 'pdf', description: 'Portable Document Format' },
],
});
`);
// 3. Permissions API — fix notifications query inconsistency
await this.context.addInitScript(`
const __origQuery = navigator.permissions.query.bind(navigator.permissions);
Object.defineProperty(navigator.permissions, 'query', {
value: (params) =>
params.name === 'notifications'
? Promise.resolve({ state: Notification.permission })
: __origQuery(params),
});
`);
// 4. WebGL renderer masking — spoof GPU vendor/renderer
// Params 37445 (UNMASKED_VENDOR_WEBGL) and 37446 (UNMASKED_RENDERER_WEBGL)
await this.context.addInitScript(`
const __origGetParam = WebGLRenderingContext.prototype.getParameter;
WebGLRenderingContext.prototype.getParameter = function(p) {
if (p === 37445) return 'Apple GPU';
if (p === 37446) return 'Apple M1 Pro';
return __origGetParam.call(this, p);
};
if (typeof WebGL2RenderingContext !== 'undefined') {
const __origGetParam2 = WebGL2RenderingContext.prototype.getParameter;
WebGL2RenderingContext.prototype.getParameter = function(p) {
if (p === 37445) return 'Apple GPU';
if (p === 37446) return 'Apple M1 Pro';
return __origGetParam2.call(this, p);
};
}
`);
// 5. iframe contentWindow isolation — apply webdriver patch in child frames
await this.context.addInitScript(`
const __iframeDesc = Object.getOwnPropertyDescriptor(HTMLIFrameElement.prototype, 'contentWindow');
if (__iframeDesc) {
Object.defineProperty(HTMLIFrameElement.prototype, 'contentWindow', {
get: function() {
const win = __iframeDesc.get?.call(this);
if (win) {
try {
Object.defineProperty(win.navigator, 'webdriver', { get: () => undefined });
} catch(e) {}
}
return win;
},
});
}
`);
// 6. Languages consistency
await this.context.addInitScript(`
Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en'] });
`);
}
private async enterFullscreen(): Promise<void> { private async enterFullscreen(): Promise<void> {
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
console.warn('Native fullscreen only supported on macOS'); console.warn('Native fullscreen only supported on macOS');
@@ -797,6 +905,34 @@ export class ClaudeBrowser {
}; };
} }
if (cmd.source === 'firefox') {
const cookies = firefox.importFirefoxCookies({
domain: cmd.domain,
profile: cmd.profile,
});
if (cookies.length === 0) {
return {
ok: true,
imported: 0,
source: 'firefox',
domains: [],
};
}
const playwrightCookies = cookies.map(firefox.toPlaywrightCookie);
await context.addCookies(playwrightCookies);
const domains = [...new Set(cookies.map((c) => c.domain))];
return {
ok: true,
imported: cookies.length,
source: 'firefox',
domains,
};
}
return { ok: false, error: `Unknown import source: ${cmd.source}` }; return { ok: false, error: `Unknown import source: ${cmd.source}` };
} }
+285
View File
@@ -0,0 +1,285 @@
/**
* Firefox cookie importer
*
* Reads cookies from Firefox's cookies.sqlite database.
* Firefox stores cookies as plain unencrypted SQLite — no binary parsing needed.
*
* Database schema (moz_cookies table):
* id, originAttributes, name, value, host, path, expiry,
* lastAccessed, creationTime, isSecure, isHttpOnly,
* inBrowserElement, sameSite, rawSameSite, schemeMap
*
* Note: expiry is Unix seconds, but lastAccessed/creationTime are microseconds.
*
* Firefox holds an exclusive WAL lock while running, so we copy the database
* files (cookies.sqlite + WAL + SHM) to a temp directory before reading.
*/
import { copyFileSync, existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
import { homedir, platform, tmpdir } from 'node:os';
import { join } from 'node:path';
import { DatabaseSync } from 'node:sqlite';
export interface FirefoxCookie {
name: string;
value: string;
domain: string;
path: string;
expires: number; // Unix timestamp (seconds)
secure: boolean;
httpOnly: boolean;
sameSite: 'None' | 'Lax' | 'Strict';
}
interface FirefoxProfile {
name: string;
path: string;
isRelative: boolean;
isDefault: boolean;
}
/**
* Finalize a partial profile into a full FirefoxProfile if valid
*/
function finalizeProfile(partial: Partial<FirefoxProfile> | null): FirefoxProfile | null {
if (!partial?.name || !partial?.path) return null;
return {
name: partial.name,
path: partial.path,
isRelative: partial.isRelative ?? true,
isDefault: partial.isDefault ?? false,
};
}
/**
* Apply a key=value line to a partial profile
*/
function applyProfileField(profile: Partial<FirefoxProfile>, key: string, value: string): void {
if (key === 'Name') profile.name = value;
else if (key === 'Path') profile.path = value;
else if (key === 'IsRelative') profile.isRelative = value === '1';
else if (key === 'Default') profile.isDefault = value === '1';
}
/**
* Process a single line of profiles.ini, updating state
*/
function processIniLine(
line: string,
current: Partial<FirefoxProfile> | null,
profiles: FirefoxProfile[]
): Partial<FirefoxProfile> | null {
const trimmed = line.trim();
if (trimmed.startsWith('[Profile') || trimmed.startsWith('[Install')) {
const finalized = finalizeProfile(current);
if (finalized) profiles.push(finalized);
return trimmed.startsWith('[Profile') ? {} : null;
}
if (current) {
const eqIdx = trimmed.indexOf('=');
if (eqIdx !== -1) applyProfileField(current, trimmed.slice(0, eqIdx), trimmed.slice(eqIdx + 1));
}
return current;
}
/**
* Parse Firefox profiles.ini to find available profiles
*/
function parseProfilesIni(iniPath: string): FirefoxProfile[] {
if (!existsSync(iniPath)) return [];
const lines = readFileSync(iniPath, 'utf-8').split('\n');
const profiles: FirefoxProfile[] = [];
let current: Partial<FirefoxProfile> | null = null;
for (const line of lines) {
current = processIniLine(line, current, profiles);
}
const last = finalizeProfile(current);
if (last) profiles.push(last);
return profiles;
}
/**
* Get the Firefox profiles root directory for the current platform
*/
function getFirefoxRoot(): string {
const home = homedir();
switch (platform()) {
case 'darwin':
return join(home, 'Library/Application Support/Firefox');
case 'linux':
return join(home, '.mozilla/firefox');
case 'win32':
return join(process.env.APPDATA || join(home, 'AppData/Roaming'), 'Mozilla/Firefox');
default:
throw new Error(`Unsupported platform: ${platform()}`);
}
}
/**
* List available Firefox profiles
*/
export function listFirefoxProfiles(): FirefoxProfile[] {
const root = getFirefoxRoot();
const iniPath = join(root, 'profiles.ini');
return parseProfilesIni(iniPath);
}
/**
* Resolve a FirefoxProfile to its absolute path
*/
function profileToAbsolutePath(root: string, p: FirefoxProfile): string {
return p.isRelative ? join(root, p.path) : p.path;
}
/**
* Resolve the full path to a Firefox profile directory
*/
function resolveProfilePath(profile?: string): string {
const root = getFirefoxRoot();
const profiles = listFirefoxProfiles();
if (!profile) {
const defaultProfile = profiles.find((p) => p.isDefault) || profiles[0];
if (!defaultProfile) throw new Error('No Firefox profiles found. Is Firefox installed?');
return profileToAbsolutePath(root, defaultProfile);
}
// Try exact match by name or path
const match = profiles.find((p) => p.name === profile || p.path === profile);
if (match) return profileToAbsolutePath(root, match);
// Try as direct path fragment in Profiles dir
const directPath = join(root, 'Profiles', profile);
if (existsSync(directPath)) return directPath;
// Try as absolute path
if (existsSync(profile)) return profile;
const available = profiles.map((p) => p.name).join(', ');
throw new Error(`Firefox profile not found: "${profile}". Available: ${available}`);
}
/**
* Safely copy the Firefox cookies database to a temp directory.
* Copies cookies.sqlite + WAL + SHM files to avoid lock conflicts.
*/
function copyDatabaseSafely(dbPath: string): { tmpDir: string; tmpDbPath: string } {
if (!existsSync(dbPath)) {
throw new Error(
`Firefox cookies database not found at: ${dbPath}\nMake sure Firefox has been used at least once.`
);
}
const tmpDir = mkdtempSync(join(tmpdir(), 'browse-fx-'));
const dbName = 'cookies.sqlite';
try {
// Copy main database
copyFileSync(dbPath, join(tmpDir, dbName));
// Copy WAL and SHM if they exist (needed for up-to-date reads)
for (const ext of ['-wal', '-shm']) {
const src = `${dbPath}${ext}`;
if (existsSync(src)) {
copyFileSync(src, join(tmpDir, `${dbName}${ext}`));
}
}
return { tmpDir, tmpDbPath: join(tmpDir, dbName) };
} catch (err) {
// Clean up on failure
rmSync(tmpDir, { recursive: true, force: true });
throw err;
}
}
/**
* Convert Firefox sameSite integer to string
* 0 = None, 1 = Lax, 2 = Strict
*/
function sameSiteToString(value: number): 'None' | 'Lax' | 'Strict' {
switch (value) {
case 2:
return 'Strict';
case 1:
return 'Lax';
default:
return 'None';
}
}
/**
* Import cookies from Firefox's cookies.sqlite database
*/
export function importFirefoxCookies(options?: {
profile?: string;
domain?: string;
}): FirefoxCookie[] {
const profilePath = resolveProfilePath(options?.profile);
const dbPath = join(profilePath, 'cookies.sqlite');
const { tmpDir, tmpDbPath } = copyDatabaseSafely(dbPath);
try {
const db = new DatabaseSync(tmpDbPath, { readOnly: true });
let query =
'SELECT name, value, host, path, expiry, isSecure, isHttpOnly, sameSite FROM moz_cookies';
const params: string[] = [];
if (options?.domain) {
const domain = options.domain.toLowerCase();
query += ' WHERE LOWER(host) = ? OR LOWER(host) = ? OR LOWER(host) LIKE ?';
params.push(domain, `.${domain}`, `%.${domain}`);
}
const stmt = db.prepare(query);
const rows = params.length > 0 ? stmt.all(...params) : stmt.all();
db.close();
return (rows as Record<string, unknown>[]).map((row) => ({
name: row.name as string,
value: row.value as string,
domain: row.host as string,
path: row.path as string,
expires: row.expiry as number,
secure: (row.isSecure as number) === 1,
httpOnly: (row.isHttpOnly as number) === 1,
sameSite: sameSiteToString(row.sameSite as number),
}));
} finally {
rmSync(tmpDir, { recursive: true, force: true });
}
}
/**
* Convert FirefoxCookie to Playwright cookie format
*/
export function toPlaywrightCookie(cookie: FirefoxCookie): {
name: string;
value: string;
domain: string;
path: string;
expires: number;
secure: boolean;
httpOnly: boolean;
sameSite: 'Strict' | 'Lax' | 'None';
} {
return {
name: cookie.name,
value: cookie.value,
domain: cookie.domain,
path: cookie.path,
expires: cookie.expires,
secure: cookie.secure,
httpOnly: cookie.httpOnly,
sameSite: cookie.sameSite,
};
}
+22 -6
View File
@@ -20,6 +20,7 @@ let browserOptions = {
fullscreen: false, fullscreen: false,
preview: false, preview: false,
previewDelay: 2000, previewDelay: 2000,
stealth: false,
}; };
let browser = new ClaudeBrowser(browserOptions); let browser = new ClaudeBrowser(browserOptions);
let launched = false; let launched = false;
@@ -86,8 +87,17 @@ server.tool(
previewDelay: z.number().optional().default(2000).describe('Preview highlight duration in ms'), previewDelay: z.number().optional().default(2000).describe('Preview highlight duration in ms'),
width: z.number().optional().default(1280).describe('Viewport width'), width: z.number().optional().default(1280).describe('Viewport width'),
height: z.number().optional().default(800).describe('Viewport height'), height: z.number().optional().default(800).describe('Viewport height'),
stealth: z
.boolean()
.optional()
.default(false)
.describe(
'Enable stealth mode to reduce bot detection. Patches navigator.webdriver, plugins, WebGL, permissions, and sets a realistic Safari user-agent. See STEALTH.md for details.'
),
}, },
withLogging('launch', async ({ headed, fullscreen, preview, previewDelay, width, height }) => { withLogging(
'launch',
async ({ headed, fullscreen, preview, previewDelay, width, height, stealth }) => {
// Close existing browser if launched // Close existing browser if launched
if (launched) { if (launched) {
await browser.close(); await browser.close();
@@ -102,6 +112,7 @@ server.tool(
fullscreen, fullscreen,
preview, preview,
previewDelay, previewDelay,
stealth,
}; };
// Create new browser with updated options // Create new browser with updated options
@@ -112,17 +123,19 @@ server.tool(
return textResult( return textResult(
JSON.stringify({ JSON.stringify({
ok: true, ok: true,
message: 'Browser launched', message: `Browser launched${stealth ? ' (stealth mode)' : ''}`,
options: { options: {
headed: !browserOptions.headless, headed: !browserOptions.headless,
fullscreen, fullscreen,
preview, preview,
previewDelay, previewDelay,
stealth,
viewport: { width, height }, viewport: { width, height },
}, },
}) })
); );
}) }
)
); );
// Navigation // Navigation
@@ -680,14 +693,17 @@ server.tool(
// Browser import // Browser import
server.tool( server.tool(
'import', 'import',
'Import cookies from Safari browser (macOS only). Requires Full Disk Access permission.', 'Import cookies from Safari or Firefox browser. Safari requires Full Disk Access permission (macOS only). Firefox works on macOS, Linux, and Windows.',
{ {
source: z.enum(['safari']).describe('Browser to import from (currently only Safari supported)'), source: z.enum(['safari', 'firefox']).describe('Browser to import from'),
domain: z domain: z
.string() .string()
.optional() .optional()
.describe('Filter cookies to specific domain (e.g., "github.com")'), .describe('Filter cookies to specific domain (e.g., "github.com")'),
profile: z.string().optional().describe('Safari profile/WebKit data store ID (optional)'), profile: z
.string()
.optional()
.describe('Safari profile/WebKit data store ID, or Firefox profile name (optional)'),
}, },
withLogging('import', async ({ source, domain, profile }) => { withLogging('import', async ({ source, domain, profile }) => {
await ensureLaunched(); await ensureLaunched();
+3 -2
View File
@@ -5,6 +5,7 @@ export interface BrowserOptions {
fullscreen?: boolean; fullscreen?: boolean;
preview?: boolean; preview?: boolean;
previewDelay?: number; previewDelay?: number;
stealth?: boolean;
} }
export interface ElementInfo { export interface ElementInfo {
@@ -301,10 +302,10 @@ export interface EmulateCommand {
device: string; device: string;
} }
// Safari import // Browser import (Safari, Firefox)
export interface ImportCommand { export interface ImportCommand {
cmd: 'import'; cmd: 'import';
source: 'safari'; source: 'safari' | 'firefox';
domain?: string; domain?: string;
profile?: string; profile?: string;
} }