Compare commits

...
14 Commits
Author SHA1 Message Date
CloakHQ ca5cce2222 release: v0.3.5 — persistent context, Windows zip fix, community PRs
- Add launch_persistent_context() Python + JS with examples
- Document persistent context API in both READMEs
- Bump version to 0.3.5
- Credit @evelaa123 and @yahooguntu in CHANGELOG
2026-03-04 19:23:16 +01:00
Cloak-HQandGitHub de54e67f74 Merge pull request #22 from evelaa123/feat/launch-persistent-context
feat: add launchPersistentContext() to avoid incognito detection
2026-03-04 19:10:23 +01:00
Cloak-HQandGitHub ef066fa091 Merge pull request #23 from evelaa123/fix/windows-zip-extraction
fix(windows): zip extraction fails when primary download server is down
2026-03-04 18:34:25 +01:00
lilos 5237065385 fix(windows): destroy fileStream on failed download to prevent zip lock 2026-03-04 13:38:09 +03:00
lilos 8e83b8c399 fix: LaunchPersistentContextOptions extends LaunchContextOptions 2026-03-04 12:31:33 +03:00
lilos c44b04a953 feat: add launch_persistent_context + async variant (Python), fix import os at module level 2026-03-04 12:19:56 +03:00
lilos 51c3f464a5 feat: add launchPersistentContext() to avoid incognito detection 2026-03-04 12:00:49 +03:00
CloakHQ ed0ecf0e48 docs: add macOS fingerprint profile troubleshooting note 2026-03-04 03:51:05 +01:00
CloakHQ 46049a15d3 feat: Windows .zip download support, binary v145.0.7632.109.2
- Add get_archive_ext() / get_archive_name() for platform-aware archive format (.zip on Windows, .tar.gz elsewhere)
- Add _extract_zip() / extractZip() with path traversal protection
- Python: zipfile module extraction
- JS: PowerShell Expand-Archive on Windows, system unzip on others
- Bump all platform versions to 145.0.7632.109.2 (4 platforms: linux-x64, darwin-arm64, darwin-x64, windows-x64)
- Update checksum lookup, temp file naming, and auto-update asset matching to use archive helpers
2026-03-04 01:23:07 +01:00
CloakHQ 11b3bcb701 release: v0.3.4 — 26 patches, auto-spoof, timezone fix, README refresh 2026-03-04 01:11:50 +01:00
CloakHQ 28de7bb147 refactor: simplify stealth args — rely on binary auto-generation (v14+)
Binary v14+ auto-generates hardware concurrency, device memory, screen
dimensions, and window size from the fingerprint seed. Remove these
explicit flags from Python/JS wrapper defaults and update README:

- Remove 5 flags from get_default_stealth_args() in both wrappers
- Move hardware-concurrency, device-memory, screen-width, screen-height
  to the Additional Flags table with auto-generated defaults documented
- Update code examples to use --fingerprint instead of --window-size
- Simplify fingerprint defaults table to show only wrapper-set flags
2026-03-03 21:27:01 +01:00
CloakHQ 0c64a32122 release: v0.3.3 — Windows x64, macOS v145, auto-spoof docs
Bump wrapper to 0.3.3. Update README fingerprint section to
document auto-spoof behavior (zero-config stealth). Improve
reCAPTCHA test with wait_for_selector instead of blind sleep.
2026-03-03 20:05:16 +01:00
CloakHQ f9887943c0 feat: add Windows x64 support, update macOS to v145 2026-03-03 08:57:07 +01:00
CloakHQ 55418add96 feat: macOS v145 wrapper prep — GPU flags, version bump, README update
- Add explicit Mac GPU flags (Apple M3 Metal renderer) to stealth args
- Bump macOS platform versions to 145.0.7632.109
- Update README fingerprint table to reflect actual Mac GPU defaults
- Add warning about binary requiring explicit flags without wrapper
- Fix stealth_test.py wait_until for reCAPTCHA page
2026-03-03 08:57:07 +01:00
20 changed files with 657 additions and 154 deletions
+32
View File
@@ -6,6 +6,38 @@ Changes are tagged: **[wrapper]** for Python/JS wrapper, **[binary]** for Chromi
--- ---
## [0.3.5] — 2026-03-04
- **[wrapper]** Add `launch_persistent_context()` and `launch_persistent_context_async()` (Python) — persistent browser profiles with cookie/localStorage persistence across sessions, avoids incognito detection (thanks [@evelaa123](https://github.com/evelaa123), [@yahooguntu](https://github.com/yahooguntu) — PRs #22, #17)
- **[wrapper]** Add `launchPersistentContext()` (JS/TS) — same feature for JavaScript with full type support
- **[wrapper]** Fix Windows zip extraction failure when primary download server is down — file handle leak caused `ERROR_SHARING_VIOLATION` on fallback download (thanks [@evelaa123](https://github.com/evelaa123) — PR #23)
## [0.3.4] — 2026-03-04
Binary v14: auto-spoof restored with seed, wrapper simplified to match.
- **[binary]** Restore full auto-spoof when `--fingerprint=seed` is set — all randomized properties now derive from the seed consistently
- **[binary]** Auto-inject random fingerprint seed at startup if none provided. Binary is stealthy with zero flags
- **[binary]** 26 source-level C++ patches (up from 25)
- **[wrapper]** Simplify default stealth args — remove flags the binary now auto-generates. Wrapper still sets platform profile on Linux and `--no-sandbox`
- **[wrapper]** Fix timezone in `launch_context()` — use Playwright's per-context timezone instead of binary flag, fixing mismatch when creating new browser contexts with geoip
- **[wrapper]** Clarify README platform detection behavior
## [0.3.3] — 2026-03-03
All platforms now run Chromium 145 v2 with 25 patches. Windows x64 added.
- **[binary]** Auto-spoof by default — binary is stealthy with zero flags. Random fingerprint seed auto-generated at startup, no wrapper or configuration required
- **[binary]** Platform-aware auto-detection — GPU, screen dimensions, and User-Agent automatically match the real OS (macOS, Linux, Windows) without explicit flags
- **[binary]** Expanded GPU model database for realistic per-session diversity
- **[binary]** First macOS v145 builds (arm64 + x64) — 25 patches, up from 16 on v142
- **[binary]** First Windows x64 v145 build — 25 patches
- **[wrapper]** Add Windows x64 platform support — auto-download, binary path resolution, and platform detection
- **[wrapper]** Upgrade macOS (arm64 + x64) from Chromium 142 to 145 — all platforms now ship the same 25-patch build
- **[wrapper]** Add explicit Mac GPU flags (`Apple M3 Metal` renderer) to default stealth args for consistent WebGL fingerprints
- **[wrapper]** Improve reCAPTCHA stealth test — wait for score element instead of blind sleep
- **[wrapper]** JS: add `win32-x64` platform mapping, Windows binary path (`chrome.exe`)
## [0.3.1] — 2026-03-03 ## [0.3.1] — 2026-03-03
- **[wrapper]** Auto-check for wrapper updates on startup (PyPI/npm). Notifies users when a newer wrapper version is available. Runs once per process, respects `CLOAKBROWSER_AUTO_UPDATE=false`. - **[wrapper]** Auto-check for wrapper updates on startup (PyPI/npm). Notifies users when a newer wrapper version is available. Runs once per process, respects `CLOAKBROWSER_AUTO_UPDATE=false`.
+89 -35
View File
@@ -35,7 +35,7 @@ Drop-in Playwright/Puppeteer replacement for Python and JavaScript.<br>
Same API, same code — just swap the import. <strong>3 lines of code, 30 seconds to unblock.</strong> Same API, same code — just swap the import. <strong>3 lines of code, 30 seconds to unblock.</strong>
</p> </p>
- 🔒 **25 source-level C++ patches** — not JS injection, not config flags - 🔒 **26 source-level C++ patches** — not JS injection, not config flags
- 🛡️ **CDP stealth built-in** — uses [Patchright](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright) to reduce Playwright's automation footprint - 🛡️ **CDP stealth built-in** — uses [Patchright](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright) to reduce Playwright's automation footprint
- 🎯 **0.9 reCAPTCHA v3 score** — human-level, server-verified - 🎯 **0.9 reCAPTCHA v3 score** — human-level, server-verified
- ☁️ **Passes Cloudflare Turnstile**, FingerprintJS, BrowserScan — 30/30 tests - ☁️ **Passes Cloudflare Turnstile**, FingerprintJS, BrowserScan — 30/30 tests
@@ -104,14 +104,16 @@ page.goto("https://example.com")
> ⭐ **Star** to show support — **[Watch releases](https://github.com/CloakHQ/CloakBrowser/subscription)** to get notified when new builds drop. > ⭐ **Star** to show support — **[Watch releases](https://github.com/CloakHQ/CloakBrowser/subscription)** to get notified when new builds drop.
## What's New in v0.3.0 ## What's New in v0.3.4
- **Chromium 145** (Linux) — latest stable, 25 fingerprint patches (up from 16). macOS v145 coming soon - **All 4 platforms** Linux x64, macOS arm64, macOS x64, and Windows x64 all on Chromium 145
- **9 new patches** — screen dimensions, device memory, audio, WebGL, and more - **26 fingerprint patches** — 10 new patches since v142 (screen, device memory, audio, WebGL, auto-spoof, and more)
- **SHA-256 checksum verification** — binary downloads are verified for integrity - **Stealthy with zero flags** — binary auto-generates a random fingerprint seed at startup. No configuration required
- **CDP hardening** — audited and patched known automation detection vectors - **Deterministic seeds** — `--fingerprint=seed` produces the same identity across launches for session persistence
- **Full stealth audit** — every patch reviewed for detection vectors, multiple fixes shipped - **Full stealth audit** — every patch reviewed for detection vectors, multiple fixes shipped
- **Timezone & locale from proxy IP** — `launch(proxy="...", geoip=True)` auto-detects timezone and locale - **Timezone & locale from proxy IP** — `launch(proxy="...", geoip=True)` auto-detects timezone and locale
- **SHA-256 checksum verification** — binary downloads are verified for integrity
- **CDP hardening** — audited and patched known automation detection vectors
See the full [CHANGELOG.md](CHANGELOG.md) for details. See the full [CHANGELOG.md](CHANGELOG.md) for details.
@@ -175,11 +177,11 @@ All tests verified against live detection services. Last tested: Mar 2026 (Chrom
CloakBrowser is a thin wrapper (Python + JavaScript) around a custom-built Chromium binary: CloakBrowser is a thin wrapper (Python + JavaScript) around a custom-built Chromium binary:
1. **You install**`pip install cloakbrowser` or `npm install cloakbrowser` 1. **You install**`pip install cloakbrowser` or `npm install cloakbrowser`
2. **First launch** → binary auto-downloads for your platform (Linux x64: Chromium 145, macOS: Chromium 142) 2. **First launch** → binary auto-downloads for your platform (Chromium 145)
3. **Every launch** → Playwright or Puppeteer starts with our binary + stealth args 3. **Every launch** → Playwright or Puppeteer starts with our binary + stealth args
4. **You write code** → standard Playwright/Puppeteer API, nothing new to learn 4. **You write code** → standard Playwright/Puppeteer API, nothing new to learn
The binary includes 25 source-level patches covering canvas, WebGL, audio, fonts, GPU, screen properties, hardware reporting, and automation signal removal. The binary includes 26 source-level patches covering canvas, WebGL, audio, fonts, GPU, screen properties, hardware reporting, and automation signal removal.
These are compiled into the Chromium binary — not injected via JavaScript, not set via flags. These are compiled into the Chromium binary — not injected via JavaScript, not set via flags.
@@ -202,7 +204,7 @@ browser = launch(headless=False)
browser = launch(proxy="http://user:pass@proxy:8080") browser = launch(proxy="http://user:pass@proxy:8080")
# With extra Chrome args # With extra Chrome args
browser = launch(args=["--disable-gpu", "--window-size=1920,1080"]) browser = launch(args=["--disable-gpu"])
# With timezone and locale (sets both binary flags and Playwright context) # With timezone and locale (sets both binary flags and Playwright context)
browser = launch(timezone="America/New_York", locale="en-US") browser = launch(timezone="America/New_York", locale="en-US")
@@ -237,7 +239,7 @@ asyncio.run(main())
### `launch_context()` ### `launch_context()`
Convenience function that creates browser + context with common options: Convenience function that creates browser + context in one call with user agent, viewport, locale, and timezone:
```python ```python
from cloakbrowser import launch_context from cloakbrowser import launch_context
@@ -249,8 +251,31 @@ context = launch_context(
timezone_id="America/New_York", timezone_id="America/New_York",
) )
page = context.new_page() page = context.new_page()
page.goto("https://protected-site.com")
context.close()
``` ```
### `launch_persistent_context()`
Same as `launch_context()`, but with a persistent user profile. Cookies, localStorage, and cache persist across sessions. Also avoids incognito detection by services like BrowserScan.
```python
from cloakbrowser import launch_persistent_context
# First run — creates the profile
ctx = launch_persistent_context("./my-profile", headless=False)
page = ctx.new_page()
page.goto("https://protected-site.com")
ctx.close() # profile saved
# Next run — cookies, localStorage restored automatically
ctx = launch_persistent_context("./my-profile", headless=False)
```
Supports all the same options as `launch_context()`: `proxy`, `user_agent`, `viewport`, `locale`, `timezone_id`, `color_scheme`, `geoip`.
Async version: `launch_persistent_context_async()`.
### Utility Functions ### Utility Functions
```python ```python
@@ -274,7 +299,7 @@ CloakBrowser ships a TypeScript package with full type definitions. Choose Playw
### Playwright (default) ### Playwright (default)
```javascript ```javascript
import { launch, launchContext } from 'cloakbrowser'; import { launch, launchContext, launchPersistentContext } from 'cloakbrowser';
// Basic // Basic
const browser = await launch(); const browser = await launch();
@@ -283,7 +308,7 @@ const browser = await launch();
const browser = await launch({ const browser = await launch({
headless: false, headless: false,
proxy: 'http://user:pass@proxy:8080', proxy: 'http://user:pass@proxy:8080',
args: ['--window-size=1920,1080'], args: ['--fingerprint=12345'],
timezone: 'America/New_York', timezone: 'America/New_York',
locale: 'en-US', locale: 'en-US',
}); });
@@ -296,6 +321,13 @@ const context = await launchContext({
timezoneId: 'America/New_York', timezoneId: 'America/New_York',
}); });
const page = await context.newPage(); const page = await context.newPage();
// Persistent profile — cookies/localStorage survive restarts, avoids incognito detection
const ctx = await launchPersistentContext({
userDataDir: './chrome-profile',
headless: false,
proxy: 'http://user:pass@proxy:8080',
});
``` ```
> **Note:** Each example above is standalone — not meant to run as one block. > **Note:** Each example above is standalone — not meant to run as one block.
@@ -342,7 +374,17 @@ clearCache();
## Fingerprint Management ## Fingerprint Management
Every launch automatically generates a **unique fingerprint**. A random seed (1000099999) drives all seed-based patches — canvas, WebGL, audio, fonts, and client rects all produce consistent, correlated values derived from that single seed. The binary is **stealthy by default** — no flags needed. It auto-generates a random fingerprint seed at startup and spoofs all detectable values (GPU, hardware specs, screen dimensions, canvas, WebGL, audio, fonts). Every launch produces a fresh, coherent identity.
**How fingerprinting works:**
| Scenario | What happens |
|----------|-------------|
| **No flags** | Random seed auto-generated at startup. GPU, screen, hardware specs, and all noise patches are spoofed automatically. Fresh identity each launch. |
| **`--fingerprint=seed`** | Deterministic identity from the seed. Same seed = same fingerprint across launches. Use this for session persistence (returning visitor). |
| **`--fingerprint=seed` + explicit flags** | Explicit flags override individual auto-generated values. The seed fills in everything else. |
The binary detects its platform at compile time — a macOS binary reports as macOS with Apple GPU, a Linux binary reports as Linux with NVIDIA GPU. The **wrapper** overrides this on Linux by passing `--fingerprint-platform=windows`, so sessions appear as Windows desktops (more common fingerprint, harder to cluster). Use `--fingerprint-platform` for cross-platform spoofing when running the binary directly.
> **Tip: Use a fixed seed when revisiting the same site.** A random seed makes every session look like a different device — which can be suspicious when hitting the same site repeatedly from the same IP. For reCAPTCHA v3 Enterprise and similar scoring systems, a fixed seed produces a consistent fingerprint across sessions, making you look like a returning visitor: > **Tip: Use a fixed seed when revisiting the same site.** A random seed makes every session look like a different device — which can be suspicious when hitting the same site repeatedly from the same IP. For reCAPTCHA v3 Enterprise and similar scoring systems, a fixed seed produces a consistent fingerprint across sessions, making you look like a returning visitor:
> ```python > ```python
@@ -354,21 +396,20 @@ Every launch automatically generates a **unique fingerprint**. A random seed (10
### Default Fingerprint ### Default Fingerprint
Every `launch()` call sets these automatically. Defaults are **platform-aware** macOS runs as a native Mac browser, Linux spoofs Windows: Every `launch()` call sets these automatically. The **wrapper** applies platform-aware defaults — on Linux it spoofs as Windows for a more common fingerprint, on macOS it runs as a native Mac browser:
| Flag | Linux Default | macOS Default | Controls | | Flag | Linux/Windows Default | macOS Default | Controls |
|------|--------------|---------------|----------| |------|--------------|---------------|----------|
| `--fingerprint` | Random (1000099999) | Random (1000099999) | Master seed for canvas, WebGL, audio, fonts, client rects | | `--fingerprint` | Random (1000099999) | Random (1000099999) | Master seed for canvas, WebGL, audio, fonts, client rects |
| `--fingerprint-platform` | `windows` | `macos` | `navigator.platform`, User-Agent OS, GPU pool selection | | `--fingerprint-platform` | `windows` | `macos` | `navigator.platform`, User-Agent OS, GPU pool selection |
| `--fingerprint-hardware-concurrency` | `8` | *(not set — uses real value)* | `navigator.hardwareConcurrency` | | `--fingerprint-gpu-vendor` | `NVIDIA Corporation` | `Google Inc. (Apple)` | WebGL `UNMASKED_VENDOR_WEBGL` |
| `--fingerprint-gpu-vendor` | `NVIDIA Corporation` | *(not set — native Apple GPU)* | WebGL `UNMASKED_VENDOR_WEBGL` | | `--fingerprint-gpu-renderer` | `NVIDIA GeForce RTX 3070` | `ANGLE (Apple, ANGLE Metal Renderer: Apple M3, Unspecified Version)` | WebGL `UNMASKED_RENDERER_WEBGL` |
| `--fingerprint-gpu-renderer` | `NVIDIA GeForce RTX 3070` | *(not set — native Metal renderer)* | WebGL `UNMASKED_RENDERER_WEBGL` |
| `--fingerprint-device-memory` | `8` | *(not set)* | `navigator.deviceMemory` |
| `--fingerprint-screen-width` | `1920` | *(not set)* | Screen width reporting |
| `--fingerprint-screen-height` | `1080` | *(not set)* | Screen height reporting |
| `--window-size` | `1920,1080` | *(not set)* | Browser window dimensions |
> **Important:** `--fingerprint-platform` should always be set. Without it, platform-specific patches (GPU, UA, screen, taskbar) won't activate. The wrapper handles this automatically. The binary auto-generates hardware concurrency (8), device memory (8), and screen dimensions (1920x1080 on Windows/Linux, 1440x900 on macOS) from the seed. Override with explicit flags if needed.
> **Using the binary directly?** It works out of the box with zero flags — the binary auto-spoofs everything. Pass `--fingerprint=seed` for a persistent identity, or use explicit flags like `--fingerprint-gpu-renderer` to override any auto-generated value.
> **Production tip:** For better stealth at scale, pass your own GPU, screen, and hardware values instead of relying on defaults. Custom parameters make your sessions harder to cluster by anti-bot systems that look for uniform fingerprint profiles.
### Additional Flags ### Additional Flags
@@ -376,6 +417,10 @@ Supported by the binary but **not set by default** — pass via `args` to custom
| Flag | Controls | | Flag | Controls |
|------|----------| |------|----------|
| `--fingerprint-hardware-concurrency` | `navigator.hardwareConcurrency` (auto-generated: `8`) |
| `--fingerprint-device-memory` | `navigator.deviceMemory` in GB (auto-generated: `8`) |
| `--fingerprint-screen-width` | Screen width (auto-generated: `1920` Win/Linux, `1440` macOS) |
| `--fingerprint-screen-height` | Screen height (auto-generated: `1080` Win/Linux, `900` macOS) |
| `--fingerprint-brand` | Browser brand: `Chrome`, `Edge`, `Opera`, `Vivaldi` | | `--fingerprint-brand` | Browser brand: `Chrome`, `Edge`, `Opera`, `Vivaldi` |
| `--fingerprint-brand-version` | Brand version (UA + Client Hints) | | `--fingerprint-brand-version` | Brand version (UA + Client Hints) |
| `--fingerprint-platform-version` | Client Hints platform version | | `--fingerprint-platform-version` | Client Hints platform version |
@@ -397,7 +442,6 @@ browser = launch(args=["--fingerprint=42069"])
browser = launch(stealth_args=False, args=[ browser = launch(stealth_args=False, args=[
"--fingerprint=42069", "--fingerprint=42069",
"--fingerprint-platform=windows", "--fingerprint-platform=windows",
"--fingerprint-hardware-concurrency=8",
"--fingerprint-gpu-vendor=NVIDIA Corporation", "--fingerprint-gpu-vendor=NVIDIA Corporation",
"--fingerprint-gpu-renderer=NVIDIA GeForce RTX 3070", "--fingerprint-gpu-renderer=NVIDIA GeForce RTX 3070",
]) ])
@@ -425,27 +469,27 @@ browser = launch(args=[
| Platform | Chromium | Patches | Status | | Platform | Chromium | Patches | Status |
|---|---|---|---| |---|---|---|---|
| Linux x86_64 | 145 | 25 | ✅ Latest | | Linux x86_64 | 145 | 26 | ✅ Latest |
| macOS arm64 (Apple Silicon) | 142 | 16 | ✅ Available (v145 coming soon) | | macOS arm64 (Apple Silicon) | 145 | 26 | ✅ Latest |
| macOS x86_64 (Intel) | 142 | 16 | ✅ Available (v145 coming soon) | | macOS x86_64 (Intel) | 145 | 26 | ✅ Latest |
| Windows | — | | Planned | | Windows x86_64 | 145 | 26 | ✅ Latest |
The wrapper auto-downloads the correct binary for your platform. Linux gets Chromium 145 with all 25 patches. macOS currently runs Chromium 142 (16 patches) — the v145 macOS build is in progress. The wrapper auto-downloads the correct binary for your platform.
**macOS first launch:** The binary is ad-hoc signed. On first run, macOS Gatekeeper will block it. Right-click the app → **Open** → click **Open** in the dialog. This is only needed once. **macOS first launch:** The binary is ad-hoc signed. On first run, macOS Gatekeeper will block it. Right-click the app → **Open** → click **Open** in the dialog. This is only needed once.
**On Windows?** You can still use CloakBrowser via Docker or with your own Chromium binary by setting `CLOAKBROWSER_BINARY_PATH=/path/to/chrome`.
## Examples ## Examples
**Python** — see [`examples/`](examples/): **Python** — see [`examples/`](examples/):
- [`basic.py`](examples/basic.py) — Launch and load a page - [`basic.py`](examples/basic.py) — Launch and load a page
- [`persistent_context.py`](examples/persistent_context.py) — Persistent profile with cookie/localStorage persistence
- [`recaptcha_score.py`](examples/recaptcha_score.py) — Check your reCAPTCHA v3 score - [`recaptcha_score.py`](examples/recaptcha_score.py) — Check your reCAPTCHA v3 score
- [`stealth_test.py`](examples/stealth_test.py) — Run against all detection services - [`stealth_test.py`](examples/stealth_test.py) — Run against all detection services
- [`fingerprint_scan_test.py`](examples/fingerprint_scan_test.py) — Test against fingerprint-scan.com and CreepJS - [`fingerprint_scan_test.py`](examples/fingerprint_scan_test.py) — Test against fingerprint-scan.com and CreepJS
**JavaScript** — see [`js/examples/`](js/examples/): **JavaScript** — see [`js/examples/`](js/examples/):
- [`basic-playwright.ts`](js/examples/basic-playwright.ts) — Playwright launch and load - [`basic-playwright.ts`](js/examples/basic-playwright.ts) — Playwright launch and load
- [`persistent-context.ts`](js/examples/persistent-context.ts) — Persistent profile with cookie/localStorage persistence
- [`basic-puppeteer.ts`](js/examples/basic-puppeteer.ts) — Puppeteer launch and load - [`basic-puppeteer.ts`](js/examples/basic-puppeteer.ts) — Puppeteer launch and load
- [`stealth-test.ts`](js/examples/stealth-test.ts) — Full 6-site detection test suite - [`stealth-test.ts`](js/examples/stealth-test.ts) — Full 6-site detection test suite
@@ -453,13 +497,12 @@ The wrapper auto-downloads the correct binary for your platform. Linux gets Chro
| Feature | Status | | Feature | Status |
|---------|--------| |---------|--------|
| Linux x64 — Chromium 145 (25 patches) | ✅ Released | | Linux x64 — Chromium 145 (26 patches) | ✅ Released |
| macOS arm64/x64 — Chromium 142 (16 patches) | ✅ Released | | macOS arm64/x64 — Chromium 145 (26 patches) | ✅ Released |
| macOS arm64/x64 — Chromium 145 | 🔨 In progress | | Windows x64 — Chromium 145 (26 patches) | ✅ Released |
| JavaScript/Puppeteer + Playwright support | ✅ Released | | JavaScript/Puppeteer + Playwright support | ✅ Released |
| Fingerprint rotation per session | ✅ Released | | Fingerprint rotation per session | ✅ Released |
| Built-in proxy rotation | 📋 Planned | | Built-in proxy rotation | 📋 Planned |
| Windows support | 📋 Planned |
## Docker ## Docker
@@ -543,6 +586,13 @@ const browser = await launch({ args: ['--disable-http2'] });
Only use this flag for sites that require it — most sites work fine with HTTP/2. Only use this flag for sites that require it — most sites work fine with HTTP/2.
**Something not working? Make sure you're on the latest wrapper**
Older versions may use outdated stealth args or download an older binary:
```bash
pip install -U cloakbrowser # Python
npm install cloakbrowser@latest # JavaScript
```
**Binary download fails / timeout** **Binary download fails / timeout**
Set a custom download URL or use a local binary: Set a custom download URL or use a local binary:
```bash ```bash
@@ -561,6 +611,10 @@ You do NOT need `playwright install chromium`. CloakBrowser downloads its own bi
patchright install-deps chromium patchright install-deps chromium
``` ```
**macOS: Blocked on some sites that pass on Linux**
The macOS fingerprint profile has known inconsistencies that aggressive bot detection catches. If a site blocks you on macOS but works on Linux, switch to a Windows fingerprint profile by passing `stealth_args=False` and manually setting `--fingerprint-platform=windows` with matching GPU flags (see [Fingerprint Management](#fingerprint-management) for the full flag list).
**reCAPTCHA v3 scores are low (0.10.3)** **reCAPTCHA v3 scores are low (0.10.3)**
Avoid `page.wait_for_timeout()` — it sends CDP protocol commands that reCAPTCHA detects. Use native sleep instead: Avoid `page.wait_for_timeout()` — it sends CDP protocol commands that reCAPTCHA detects. Use native sleep instead:
+3 -1
View File
@@ -11,7 +11,7 @@ Usage:
browser.close() browser.close()
""" """
from .browser import launch, launch_async, launch_context from .browser import launch, launch_async, launch_context, launch_persistent_context, launch_persistent_context_async
from .config import CHROMIUM_VERSION, get_default_stealth_args from .config import CHROMIUM_VERSION, get_default_stealth_args
from .download import binary_info, check_for_update, clear_cache, ensure_binary from .download import binary_info, check_for_update, clear_cache, ensure_binary
from ._version import __version__ from ._version import __version__
@@ -20,6 +20,8 @@ __all__ = [
"launch", "launch",
"launch_async", "launch_async",
"launch_context", "launch_context",
"launch_persistent_context",
"launch_persistent_context_async",
"ensure_binary", "ensure_binary",
"clear_cache", "clear_cache",
"binary_info", "binary_info",
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.3.1" __version__ = "0.3.5"
+199 -1
View File
@@ -15,6 +15,7 @@ Usage:
from __future__ import annotations from __future__ import annotations
import logging import logging
import os
from typing import Any, Literal from typing import Any, Literal
from urllib.parse import unquote, urlparse, urlunparse from urllib.parse import unquote, urlparse, urlunparse
@@ -159,6 +160,200 @@ async def launch_async(
return browser return browser
def launch_persistent_context(
user_data_dir: str | os.PathLike,
headless: bool = True,
proxy: str | None = None,
args: list[str] | None = None,
stealth_args: bool = True,
user_agent: str | None = None,
viewport: dict | None = None,
locale: str | None = None,
timezone_id: str | None = None,
color_scheme: Literal["light", "dark", "no-preference"] | None = None,
geoip: bool = False,
**kwargs: Any,
) -> Any:
"""Launch stealth browser with a persistent profile and return a BrowserContext.
This persists cookies, localStorage, cache, and other browser state across
sessions by storing them in ``user_data_dir``. Also avoids incognito detection
by services like BrowserScan (-10% penalty).
Args:
user_data_dir: Path to the directory where browser profile data is stored.
Created automatically if it doesn't exist. Reuse the same path across
sessions to restore cookies, localStorage, cached credentials, etc.
headless: Run in headless mode (default True).
proxy: Proxy server URL (e.g. 'http://proxy:8080' or 'socks5://proxy:1080').
args: Additional Chromium CLI arguments.
stealth_args: Include default stealth fingerprint args (default True).
user_agent: Custom user agent string.
viewport: Viewport size dict, e.g. {"width": 1920, "height": 1080}.
locale: Browser locale, e.g. "en-US".
timezone_id: Timezone, e.g. "America/New_York".
color_scheme: Color scheme preference 'light', 'dark', or 'no-preference'.
Default: None (uses Chromium default, which is 'light').
geoip: Auto-detect timezone/locale from proxy IP (default False).
Requires ``pip install cloakbrowser[geoip]``.
**kwargs: Passed directly to playwright.chromium.launch_persistent_context().
Returns:
Playwright BrowserContext object backed by a persistent profile.
Call ``.close()`` when done this also stops the Playwright instance.
Example:
>>> from cloakbrowser import launch_persistent_context
>>> ctx = launch_persistent_context("./my-profile", headless=False)
>>> page = ctx.new_page()
>>> page.goto("https://protected-site.com")
>>> ctx.close() # Profile is saved; re-use path next run to restore state.
"""
from patchright.sync_api import sync_playwright
binary_path = ensure_binary()
timezone_id, locale = _maybe_resolve_geoip(geoip, proxy, timezone_id, locale)
chrome_args = _build_args(stealth_args, args, timezone=timezone_id, locale=locale)
logger.debug(
"Launching persistent stealth Chromium (headless=%s, user_data_dir=%s)",
headless,
user_data_dir,
)
context_kwargs: dict[str, Any] = {}
if user_agent:
context_kwargs["user_agent"] = user_agent
context_kwargs["viewport"] = viewport or DEFAULT_VIEWPORT
if locale:
context_kwargs["locale"] = locale
if timezone_id:
context_kwargs["timezone_id"] = timezone_id
if color_scheme:
context_kwargs["color_scheme"] = color_scheme
context_kwargs.update(kwargs)
pw = sync_playwright().start()
context = pw.chromium.launch_persistent_context(
user_data_dir=os.fspath(user_data_dir),
executable_path=binary_path,
headless=headless,
args=chrome_args,
ignore_default_args=["--enable-automation"],
**_build_proxy_kwargs(proxy),
**context_kwargs,
)
# Patch close() to also stop the Playwright instance
_original_close = context.close
def _close_with_cleanup() -> None:
_original_close()
pw.stop()
context.close = _close_with_cleanup
return context
async def launch_persistent_context_async(
user_data_dir: str | os.PathLike,
headless: bool = True,
proxy: str | None = None,
args: list[str] | None = None,
stealth_args: bool = True,
user_agent: str | None = None,
viewport: dict | None = None,
locale: str | None = None,
timezone_id: str | None = None,
color_scheme: Literal["light", "dark", "no-preference"] | None = None,
geoip: bool = False,
**kwargs: Any,
) -> Any:
"""Async version of launch_persistent_context().
Launch stealth browser with a persistent profile and return a BrowserContext.
This persists cookies, localStorage, cache, and other browser state across
sessions by storing them in ``user_data_dir``.
Args:
user_data_dir: Path to the directory where browser profile data is stored.
Created automatically if it doesn't exist.
headless: Run in headless mode (default True).
proxy: Proxy server URL (e.g. 'http://proxy:8080' or 'socks5://proxy:1080').
args: Additional Chromium CLI arguments.
stealth_args: Include default stealth fingerprint args (default True).
user_agent: Custom user agent string.
viewport: Viewport size dict, e.g. {"width": 1920, "height": 1080}.
locale: Browser locale, e.g. "en-US".
timezone_id: Timezone, e.g. "America/New_York".
color_scheme: Color scheme preference 'light', 'dark', or 'no-preference'.
geoip: Auto-detect timezone/locale from proxy IP (default False).
**kwargs: Passed directly to playwright.chromium.launch_persistent_context().
Returns:
Playwright BrowserContext object backed by a persistent profile (async API).
Call ``await .close()`` when done.
Example:
>>> import asyncio
>>> from cloakbrowser import launch_persistent_context_async
>>>
>>> async def main():
... ctx = await launch_persistent_context_async("./my-profile", headless=False)
... page = await ctx.new_page()
... await page.goto("https://protected-site.com")
... await ctx.close()
>>>
>>> asyncio.run(main())
"""
from patchright.async_api import async_playwright
binary_path = ensure_binary()
timezone_id, locale = _maybe_resolve_geoip(geoip, proxy, timezone_id, locale)
chrome_args = _build_args(stealth_args, args, timezone=timezone_id, locale=locale)
logger.debug(
"Launching persistent stealth Chromium async (headless=%s, user_data_dir=%s)",
headless,
user_data_dir,
)
context_kwargs: dict[str, Any] = {}
if user_agent:
context_kwargs["user_agent"] = user_agent
context_kwargs["viewport"] = viewport or DEFAULT_VIEWPORT
if locale:
context_kwargs["locale"] = locale
if timezone_id:
context_kwargs["timezone_id"] = timezone_id
if color_scheme:
context_kwargs["color_scheme"] = color_scheme
context_kwargs.update(kwargs)
pw = await async_playwright().start()
context = await pw.chromium.launch_persistent_context(
user_data_dir=os.fspath(user_data_dir),
executable_path=binary_path,
headless=headless,
args=chrome_args,
ignore_default_args=["--enable-automation"],
**_build_proxy_kwargs(proxy),
**context_kwargs,
)
# Patch close() to also stop the Playwright instance
_original_close = context.close
async def _close_with_cleanup() -> None:
await _original_close()
await pw.stop()
context.close = _close_with_cleanup
return context
def launch_context( def launch_context(
headless: bool = True, headless: bool = True,
proxy: str | None = None, proxy: str | None = None,
@@ -198,8 +393,11 @@ def launch_context(
# Resolve geoip BEFORE launch() to avoid double-resolution and ensure # Resolve geoip BEFORE launch() to avoid double-resolution and ensure
# resolved values flow to both binary flags AND context params # resolved values flow to both binary flags AND context params
timezone_id, locale = _maybe_resolve_geoip(geoip, proxy, timezone_id, locale) timezone_id, locale = _maybe_resolve_geoip(geoip, proxy, timezone_id, locale)
# Skip --fingerprint-timezone binary flag: it only applies to the default
# context and interferes with Playwright's timezone_id on new contexts.
# Timezone is set via browser.new_context(timezone_id=...) below instead.
browser = launch(headless=headless, proxy=proxy, args=args, stealth_args=stealth_args, browser = launch(headless=headless, proxy=proxy, args=args, stealth_args=stealth_args,
timezone=timezone_id, locale=locale) timezone=None, locale=locale)
context_kwargs: dict[str, Any] = {} context_kwargs: dict[str, Any] = {}
if user_agent: if user_agent:
+30 -19
View File
@@ -11,17 +11,17 @@ from ._version import __version__
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Chromium version shipped with this release. # Chromium version shipped with this release.
# Different platforms may ship different versions (e.g. Linux gets v145 first, # Different platforms may ship different versions during transition periods.
# macOS stays on v142 until Mac builds are ready).
# CHROMIUM_VERSION is the latest across all platforms (for display/reference). # CHROMIUM_VERSION is the latest across all platforms (for display/reference).
# Use get_chromium_version() for the current platform's actual version. # Use get_chromium_version() for the current platform's actual version.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
CHROMIUM_VERSION = "145.0.7632.109" CHROMIUM_VERSION = "145.0.7632.109.2"
PLATFORM_CHROMIUM_VERSIONS: dict[str, str] = { PLATFORM_CHROMIUM_VERSIONS: dict[str, str] = {
"linux-x64": "145.0.7632.109", "linux-x64": "145.0.7632.109.2",
"darwin-arm64": "142.0.7444.175", "darwin-arm64": "145.0.7632.109.2",
"darwin-x64": "142.0.7444.175", "darwin-x64": "145.0.7632.109.2",
"windows-x64": "145.0.7632.109.2",
} }
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -47,18 +47,17 @@ def get_default_stealth_args() -> list[str]:
# Tell the fingerprint patches we're on macOS so GPU/UA match natively # Tell the fingerprint patches we're on macOS so GPU/UA match natively
return base + [ return base + [
"--fingerprint-platform=macos", "--fingerprint-platform=macos",
"--fingerprint-gpu-vendor=Google Inc. (Apple)",
"--fingerprint-gpu-renderer=ANGLE (Apple, ANGLE Metal Renderer: Apple M3, Unspecified Version)",
] ]
# Linux: spoof as Windows # Linux/Windows: Windows fingerprint profile
# Hardware concurrency, device memory, screen, and window size are
# auto-generated by the binary from the seed (v14+).
return base + [ return base + [
"--fingerprint-platform=windows", "--fingerprint-platform=windows",
"--fingerprint-hardware-concurrency=8",
"--fingerprint-device-memory=8",
"--fingerprint-gpu-vendor=NVIDIA Corporation", "--fingerprint-gpu-vendor=NVIDIA Corporation",
"--fingerprint-gpu-renderer=NVIDIA GeForce RTX 3070", "--fingerprint-gpu-renderer=NVIDIA GeForce RTX 3070",
"--fingerprint-screen-width=1920",
"--fingerprint-screen-height=1080",
"--window-size=1920,1080",
] ]
@@ -77,6 +76,8 @@ SUPPORTED_PLATFORMS: dict[tuple[str, str], str] = {
("Linux", "aarch64"): "linux-arm64", ("Linux", "aarch64"): "linux-arm64",
("Darwin", "arm64"): "darwin-arm64", ("Darwin", "arm64"): "darwin-arm64",
("Darwin", "x86_64"): "darwin-x64", ("Darwin", "x86_64"): "darwin-x64",
("Windows", "AMD64"): "windows-x64",
("Windows", "x86_64"): "windows-x64",
} }
# Platforms with pre-built binaries available for download (derived from version map). # Platforms with pre-built binaries available for download (derived from version map).
@@ -130,6 +131,8 @@ def get_binary_path(version: str | None = None) -> Path:
if platform.system() == "Darwin": if platform.system() == "Darwin":
# macOS: Chromium.app bundle # macOS: Chromium.app bundle
return binary_dir / "Chromium.app" / "Contents" / "MacOS" / "Chromium" return binary_dir / "Chromium.app" / "Contents" / "MacOS" / "Chromium"
elif platform.system() == "Windows":
return binary_dir / "chrome.exe"
else: else:
# Linux: flat binary # Linux: flat binary
return binary_dir / "chrome" return binary_dir / "chrome"
@@ -148,9 +151,8 @@ def check_platform_available() -> None:
available = ", ".join(sorted(AVAILABLE_PLATFORMS)) available = ", ".join(sorted(AVAILABLE_PLATFORMS))
import sys import sys
sys.exit( sys.exit(
f"\n\033[1mCloakBrowser\033[0m — Pre-built binaries are currently only available for: {available}.\n" f"\n\033[1mCloakBrowser\033[0m — Pre-built binaries are currently only available for: {available}.\n\n"
f"Windows builds are coming soon.\n\n" f"To use CloakBrowser now, set CLOAKBROWSER_BINARY_PATH to a local Chromium binary."
f"To use CloakBrowser now, run in Docker (see README) or set CLOAKBROWSER_BINARY_PATH."
) )
@@ -202,18 +204,27 @@ GITHUB_DOWNLOAD_BASE_URL = (
) )
def get_archive_ext() -> str:
"""Return the archive extension for the current platform (.zip for Windows, .tar.gz otherwise)."""
return ".zip" if platform.system() == "Windows" else ".tar.gz"
def get_archive_name(tag: str | None = None) -> str:
"""Return the archive filename for a platform tag (e.g. 'cloakbrowser-linux-x64.tar.gz')."""
t = tag or get_platform_tag()
return f"cloakbrowser-{t}{get_archive_ext()}"
def get_download_url(version: str | None = None) -> str: def get_download_url(version: str | None = None) -> str:
"""Return the full download URL for the current platform's binary archive.""" """Return the full download URL for the current platform's binary archive."""
v = version or get_chromium_version() v = version or get_chromium_version()
tag = get_platform_tag() return f"{DOWNLOAD_BASE_URL}/chromium-v{v}/{get_archive_name()}"
return f"{DOWNLOAD_BASE_URL}/chromium-v{v}/cloakbrowser-{tag}.tar.gz"
def get_fallback_download_url(version: str | None = None) -> str: def get_fallback_download_url(version: str | None = None) -> str:
"""Return the GitHub Releases fallback URL for the binary archive.""" """Return the GitHub Releases fallback URL for the binary archive."""
v = version or get_chromium_version() v = version or get_chromium_version()
tag = get_platform_tag() return f"{GITHUB_DOWNLOAD_BASE_URL}/chromium-v{v}/{get_archive_name()}"
return f"{GITHUB_DOWNLOAD_BASE_URL}/chromium-v{v}/cloakbrowser-{tag}.tar.gz"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
+46 -24
View File
@@ -28,6 +28,8 @@ from .config import (
GITHUB_DOWNLOAD_BASE_URL, GITHUB_DOWNLOAD_BASE_URL,
_version_newer, _version_newer,
check_platform_available, check_platform_available,
get_archive_ext,
get_archive_name,
get_binary_dir, get_binary_dir,
get_binary_path, get_binary_path,
get_cache_dir, get_cache_dir,
@@ -123,7 +125,7 @@ def _download_and_extract(version: str | None = None) -> None:
binary_dir.parent.mkdir(parents=True, exist_ok=True) binary_dir.parent.mkdir(parents=True, exist_ok=True)
# Download to temp file first (atomic — no partial downloads in cache) # Download to temp file first (atomic — no partial downloads in cache)
with tempfile.NamedTemporaryFile(suffix=".tar.gz", delete=False) as tmp: with tempfile.NamedTemporaryFile(suffix=get_archive_ext(), delete=False) as tmp:
tmp_path = Path(tmp.name) tmp_path = Path(tmp.name)
try: try:
@@ -155,7 +157,7 @@ def _download_and_extract(version: str | None = None) -> None:
def _verify_download_checksum(file_path: Path, version: str | None = None) -> None: def _verify_download_checksum(file_path: Path, version: str | None = None) -> None:
"""Fetch SHA256SUMS and verify the downloaded file. Warn if unavailable, fail on mismatch.""" """Fetch SHA256SUMS and verify the downloaded file. Warn if unavailable, fail on mismatch."""
checksums = _fetch_checksums(version) checksums = _fetch_checksums(version)
tarball_name = f"cloakbrowser-{get_platform_tag()}.tar.gz" tarball_name = get_archive_name()
if checksums is None: if checksums is None:
logger.warning("SHA256SUMS not available for this release — skipping checksum verification") logger.warning("SHA256SUMS not available for this release — skipping checksum verification")
@@ -256,7 +258,7 @@ def _download_file(url: str, dest: Path) -> None:
def _extract_archive( def _extract_archive(
archive_path: Path, dest_dir: Path, binary_path: Path | None = None archive_path: Path, dest_dir: Path, binary_path: Path | None = None
) -> None: ) -> None:
"""Extract tar.gz archive to destination directory.""" """Extract tar.gz or zip archive to destination directory."""
logger.info("Extracting to %s", dest_dir) logger.info("Extracting to %s", dest_dir)
# Clean existing dir if partial download existed # Clean existing dir if partial download existed
@@ -266,26 +268,12 @@ def _extract_archive(
dest_dir.mkdir(parents=True, exist_ok=True) dest_dir.mkdir(parents=True, exist_ok=True)
with tarfile.open(archive_path, "r:gz") as tar: if str(archive_path).endswith(".zip"):
# Security: prevent path traversal _extract_zip(archive_path, dest_dir)
safe_members = [] else:
for member in tar.getmembers(): _extract_tar(archive_path, dest_dir)
# Allow symlinks — macOS .app bundles require them (Framework layout)
if member.issym() or member.islnk():
link_target = member.linkname
# Reject symlinks that escape the dest dir
if os.path.isabs(link_target) or ".." in link_target.split("/"):
logger.warning("Skipping suspicious symlink: %s -> %s", member.name, link_target)
continue
else:
member_path = (dest_dir / member.name).resolve()
if not str(member_path).startswith(str(dest_dir.resolve())):
raise RuntimeError(f"Archive contains path traversal: {member.name}")
safe_members.append(member)
tar.extractall(dest_dir, members=safe_members) # If extracted into a single subdirectory, flatten it
# If tar extracted into a single subdirectory, flatten it
# (e.g. fingerprint-chromium-142-custom-v2/chrome → chrome) # (e.g. fingerprint-chromium-142-custom-v2/chrome → chrome)
# But never flatten .app bundles — macOS needs the bundle structure intact # But never flatten .app bundles — macOS needs the bundle structure intact
_flatten_single_subdir(dest_dir) _flatten_single_subdir(dest_dir)
@@ -303,6 +291,38 @@ def _extract_archive(
logger.info("Binary ready: %s", bp) logger.info("Binary ready: %s", bp)
def _extract_tar(archive_path: Path, dest_dir: Path) -> None:
"""Extract tar.gz archive with path traversal protection."""
with tarfile.open(archive_path, "r:gz") as tar:
safe_members = []
for member in tar.getmembers():
# Allow symlinks — macOS .app bundles require them (Framework layout)
if member.issym() or member.islnk():
link_target = member.linkname
if os.path.isabs(link_target) or ".." in link_target.split("/"):
logger.warning("Skipping suspicious symlink: %s -> %s", member.name, link_target)
continue
else:
member_path = (dest_dir / member.name).resolve()
if not str(member_path).startswith(str(dest_dir.resolve())):
raise RuntimeError(f"Archive contains path traversal: {member.name}")
safe_members.append(member)
tar.extractall(dest_dir, members=safe_members)
def _extract_zip(archive_path: Path, dest_dir: Path) -> None:
"""Extract zip archive with path traversal protection."""
import zipfile
with zipfile.ZipFile(archive_path, "r") as zf:
for info in zf.infolist():
member_path = (dest_dir / info.filename).resolve()
if not str(member_path).startswith(str(dest_dir.resolve())):
raise RuntimeError(f"Archive contains path traversal: {info.filename}")
zf.extractall(dest_dir)
def _flatten_single_subdir(dest_dir: Path) -> None: def _flatten_single_subdir(dest_dir: Path) -> None:
"""If extraction created a single subdirectory, move its contents up. """If extraction created a single subdirectory, move its contents up.
@@ -330,7 +350,9 @@ def _is_executable(path: Path) -> bool:
def _make_executable(path: Path) -> None: def _make_executable(path: Path) -> None:
"""Make a file executable (chmod +x).""" """Make a file executable (chmod +x). Skipped on Windows (no-op / AV lock risk)."""
if platform.system() == "Windows":
return
current = path.stat().st_mode current = path.stat().st_mode
path.chmod(current | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) path.chmod(current | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
@@ -433,7 +455,7 @@ def _get_latest_chromium_version() -> str | None:
GITHUB_API_URL, params={"per_page": 10}, timeout=10.0 GITHUB_API_URL, params={"per_page": 10}, timeout=10.0
) )
resp.raise_for_status() resp.raise_for_status()
platform_tarball = f"cloakbrowser-{get_platform_tag()}.tar.gz" platform_tarball = get_archive_name()
for release in resp.json(): for release in resp.json():
tag = release.get("tag_name", "") tag = release.get("tag_name", "")
if tag.startswith("chromium-v") and not release.get("draft"): if tag.startswith("chromium-v") and not release.get("draft"):
+29
View File
@@ -0,0 +1,29 @@
"""Persistent context example: cookies and localStorage survive across sessions."""
from cloakbrowser import launch_persistent_context
PROFILE_DIR = "./my-profile"
# Session 1 — set some state
print("=== Session 1: Setting state ===")
ctx = launch_persistent_context(PROFILE_DIR, headless=False)
page = ctx.new_page()
page.goto("https://example.com")
page.evaluate("document.cookie = 'session=abc123; path=/; max-age=3600'")
page.evaluate("localStorage.setItem('user', 'returning')")
print(f"Cookie: {page.evaluate('document.cookie')}")
ls_val = page.evaluate("localStorage.getItem('user')")
print(f"localStorage: {ls_val}")
ctx.close()
# Session 2 — state is restored
print("\n=== Session 2: Verifying persistence ===")
ctx = launch_persistent_context(PROFILE_DIR, headless=False)
page = ctx.new_page()
page.goto("https://example.com")
print(f"Cookie: {page.evaluate('document.cookie')}")
ls_val = page.evaluate("localStorage.getItem('user')")
print(f"localStorage: {ls_val}")
ctx.close()
print("\nDone!")
+7 -3
View File
@@ -143,11 +143,15 @@ def test_recaptcha(page):
"""recaptcha-demo.appspot.com — Google's official reCAPTCHA v3 score.""" """recaptcha-demo.appspot.com — Google's official reCAPTCHA v3 score."""
page.goto( page.goto(
"https://recaptcha-demo.appspot.com/recaptcha-v3-request-scores.php", "https://recaptcha-demo.appspot.com/recaptcha-v3-request-scores.php",
wait_until="networkidle", wait_until="domcontentloaded",
timeout=30000, timeout=30000,
) )
# Page auto-submits via grecaptcha.execute() — wait for backend response # Wait for backend response (step3 element appears when score arrives)
time.sleep(8) try:
page.wait_for_selector("li.step3", timeout=20000)
time.sleep(1)
except Exception:
time.sleep(10) # fallback
results = page.evaluate("""() => { results = page.evaluate("""() => {
const text = document.body.innerText; const text = document.body.innerText;
+14 -6
View File
@@ -11,7 +11,7 @@
Drop-in Playwright/Puppeteer replacement. Same API — just swap the import. Scores **0.9 on reCAPTCHA v3**, passes **Cloudflare Turnstile**, and clears **30/30** stealth detection tests. Drop-in Playwright/Puppeteer replacement. Same API — just swap the import. Scores **0.9 on reCAPTCHA v3**, passes **Cloudflare Turnstile**, and clears **30/30** stealth detection tests.
- 🔒 **25 source-level C++ patches** — not JS injection, not config flags - 🔒 **26 source-level C++ patches** — not JS injection, not config flags
- 🎯 **0.9 reCAPTCHA v3 score** — human-level, server-verified - 🎯 **0.9 reCAPTCHA v3 score** — human-level, server-verified
- ☁️ **Passes Cloudflare Turnstile**, FingerprintJS, BrowserScan — 30/30 tests - ☁️ **Passes Cloudflare Turnstile**, FingerprintJS, BrowserScan — 30/30 tests
- 🔄 **Drop-in replacement** — works with both Playwright and Puppeteer - 🔄 **Drop-in replacement** — works with both Playwright and Puppeteer
@@ -60,7 +60,7 @@ await browser.close();
### Options ### Options
```javascript ```javascript
import { launch, launchContext } from 'cloakbrowser'; import { launch, launchContext, launchPersistentContext } from 'cloakbrowser';
// With proxy // With proxy
const browser = await launch({ const browser = await launch({
@@ -72,7 +72,7 @@ const browser = await launch({ headless: false });
// Extra Chrome args // Extra Chrome args
const browser = await launch({ const browser = await launch({
args: ['--window-size=1920,1080'], args: ['--fingerprint=12345'],
}); });
// With timezone and locale (sets --fingerprint-timezone and --lang binary flags) // With timezone and locale (sets --fingerprint-timezone and --lang binary flags)
@@ -94,6 +94,16 @@ const context = await launchContext({
locale: 'en-US', locale: 'en-US',
timezoneId: 'America/New_York', timezoneId: 'America/New_York',
}); });
// Persistent profile — cookies/localStorage survive restarts, avoids incognito detection
const ctx = await launchPersistentContext({
userDataDir: './chrome-profile',
headless: false,
proxy: 'http://user:pass@proxy:8080',
});
const page = ctx.pages()[0] || await ctx.newPage();
await page.goto('https://example.com');
await ctx.close(); // profile saved — reuse same path to restore state
``` ```
### Auto Timezone/Locale from Proxy IP ### Auto Timezone/Locale from Proxy IP
@@ -176,9 +186,7 @@ const page = await browser.newPage();
| Linux x86_64 | ✅ Available | | Linux x86_64 | ✅ Available |
| macOS arm64 (Apple Silicon) | ✅ Available | | macOS arm64 (Apple Silicon) | ✅ Available |
| macOS x86_64 (Intel) | ✅ Available | | macOS x86_64 (Intel) | ✅ Available |
| Windows | Planned | | Windows x86_64 | ✅ Available |
**On Windows?** You can still use CloakBrowser via Docker or with your own Chromium binary by setting `CLOAKBROWSER_BINARY_PATH=/path/to/chrome`.
## Requirements ## Requirements
+40
View File
@@ -0,0 +1,40 @@
/**
* Persistent context example: cookies and localStorage survive across sessions.
*
* Usage:
* CLOAKBROWSER_BINARY_PATH=/path/to/chrome npx tsx examples/persistent-context.ts
*/
import { launchPersistentContext } from "../src/index.js";
const PROFILE_DIR = "./my-profile";
// Session 1 — set some state
console.log("=== Session 1: Setting state ===");
let ctx = await launchPersistentContext({
userDataDir: PROFILE_DIR,
headless: false,
});
let page = ctx.pages()[0] || (await ctx.newPage());
await page.goto("https://example.com");
await page.evaluate(() => {
document.cookie = "session=abc123; path=/; max-age=3600";
localStorage.setItem("user", "returning");
});
console.log(`Cookie: ${await page.evaluate(() => document.cookie)}`);
console.log(`localStorage: ${await page.evaluate(() => localStorage.getItem("user"))}`);
await ctx.close();
// Session 2 — state is restored
console.log("\n=== Session 2: Verifying persistence ===");
ctx = await launchPersistentContext({
userDataDir: PROFILE_DIR,
headless: false,
});
page = ctx.pages()[0] || (await ctx.newPage());
await page.goto("https://example.com");
console.log(`Cookie: ${await page.evaluate(() => document.cookie)}`);
console.log(`localStorage: ${await page.evaluate(() => localStorage.getItem("user"))}`);
await ctx.close();
console.log("\nDone!");
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "cloakbrowser", "name": "cloakbrowser",
"version": "0.3.1", "version": "0.3.5",
"description": "Stealth Chromium that passes every bot detection test. Drop-in Playwright/Puppeteer replacement with source-level fingerprint patches.", "description": "Stealth Chromium that passes every bot detection test. Drop-in Playwright/Puppeteer replacement with source-level fingerprint patches.",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
+32 -20
View File
@@ -23,17 +23,17 @@ export { WRAPPER_VERSION };
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Chromium version shipped with this release. // Chromium version shipped with this release.
// Different platforms may ship different versions (e.g. Linux gets v145 first, // Different platforms may ship different versions during transition periods.
// macOS stays on v142 until Mac builds are ready).
// CHROMIUM_VERSION is the latest across all platforms (for display/reference). // CHROMIUM_VERSION is the latest across all platforms (for display/reference).
// Use getChromiumVersion() for the current platform's actual version. // Use getChromiumVersion() for the current platform's actual version.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export const CHROMIUM_VERSION = "145.0.7632.109"; export const CHROMIUM_VERSION = "145.0.7632.109.2";
export const PLATFORM_CHROMIUM_VERSIONS: Record<string, string> = { export const PLATFORM_CHROMIUM_VERSIONS: Record<string, string> = {
"linux-x64": "145.0.7632.109", "linux-x64": "145.0.7632.109.2",
"darwin-arm64": "142.0.7444.175", "darwin-arm64": "145.0.7632.109.2",
"darwin-x64": "142.0.7444.175", "darwin-x64": "145.0.7632.109.2",
"windows-x64": "145.0.7632.109.2",
}; };
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -44,6 +44,7 @@ const SUPPORTED_PLATFORMS: Record<string, string> = {
"linux-arm64": "linux-arm64", "linux-arm64": "linux-arm64",
"darwin-arm64": "darwin-arm64", "darwin-arm64": "darwin-arm64",
"darwin-x64": "darwin-x64", "darwin-x64": "darwin-x64",
"win32-x64": "windows-x64",
}; };
// Platforms with pre-built binaries available for download (derived from version map). // Platforms with pre-built binaries available for download (derived from version map).
@@ -64,6 +65,7 @@ export function getPlatformTag(): string {
else if (platform === "linux" && arch === "arm64") key = "linux-arm64"; else if (platform === "linux" && arch === "arm64") key = "linux-arm64";
else if (platform === "darwin" && arch === "arm64") key = "darwin-arm64"; else if (platform === "darwin" && arch === "arm64") key = "darwin-arm64";
else if (platform === "darwin" && arch === "x64") key = "darwin-x64"; else if (platform === "darwin" && arch === "x64") key = "darwin-x64";
else if (platform === "win32" && arch === "x64") key = "win32-x64";
else { else {
const supported = Object.values(SUPPORTED_PLATFORMS).join(", "); const supported = Object.values(SUPPORTED_PLATFORMS).join(", ");
throw new Error( throw new Error(
@@ -92,6 +94,9 @@ export function getBinaryPath(version?: string): string {
if (process.platform === "darwin") { if (process.platform === "darwin") {
return path.join(binaryDir, "Chromium.app", "Contents", "MacOS", "Chromium"); return path.join(binaryDir, "Chromium.app", "Contents", "MacOS", "Chromium");
} }
if (process.platform === "win32") {
return path.join(binaryDir, "chrome.exe");
}
return path.join(binaryDir, "chrome"); return path.join(binaryDir, "chrome");
} }
@@ -102,9 +107,8 @@ export function checkPlatformAvailable(): void {
if (!AVAILABLE_PLATFORMS.has(tag)) { if (!AVAILABLE_PLATFORMS.has(tag)) {
const available = [...AVAILABLE_PLATFORMS].sort().join(", "); const available = [...AVAILABLE_PLATFORMS].sort().join(", ");
throw new Error( throw new Error(
`CloakBrowser — Pre-built binaries are currently only available for: ${available}.\n` + `CloakBrowser — Pre-built binaries are currently only available for: ${available}.\n\n` +
`Windows builds are coming soon.\n\n` + `To use CloakBrowser now, set CLOAKBROWSER_BINARY_PATH to a local Chromium binary.`
`To use CloakBrowser now, run in Docker (see README) or set CLOAKBROWSER_BINARY_PATH.`
); );
} }
} }
@@ -122,16 +126,22 @@ export const GITHUB_API_URL =
export const GITHUB_DOWNLOAD_BASE_URL = export const GITHUB_DOWNLOAD_BASE_URL =
"https://github.com/CloakHQ/cloakbrowser/releases/download"; "https://github.com/CloakHQ/cloakbrowser/releases/download";
export function getArchiveExt(): string {
return process.platform === "win32" ? ".zip" : ".tar.gz";
}
export function getArchiveName(tag?: string): string {
return `cloakbrowser-${tag || getPlatformTag()}${getArchiveExt()}`;
}
export function getDownloadUrl(version?: string): string { export function getDownloadUrl(version?: string): string {
const v = version || getChromiumVersion(); const v = version || getChromiumVersion();
const tag = getPlatformTag(); return `${DOWNLOAD_BASE_URL}/chromium-v${v}/${getArchiveName()}`;
return `${DOWNLOAD_BASE_URL}/chromium-v${v}/cloakbrowser-${tag}.tar.gz`;
} }
export function getFallbackDownloadUrl(version?: string): string { export function getFallbackDownloadUrl(version?: string): string {
const v = version || getChromiumVersion(); const v = version || getChromiumVersion();
const tag = getPlatformTag(); return `${GITHUB_DOWNLOAD_BASE_URL}/chromium-v${v}/${getArchiveName()}`;
return `${GITHUB_DOWNLOAD_BASE_URL}/chromium-v${v}/cloakbrowser-${tag}.tar.gz`;
} }
export function getEffectiveVersion(): string { export function getEffectiveVersion(): string {
@@ -198,19 +208,21 @@ export function getDefaultStealthArgs(): string[] {
if (isMac) { if (isMac) {
// macOS: run as native Mac browser — GPU/UA match natively // macOS: run as native Mac browser — GPU/UA match natively
return [...base, "--fingerprint-platform=macos"]; return [
...base,
"--fingerprint-platform=macos",
"--fingerprint-gpu-vendor=Google Inc. (Apple)",
"--fingerprint-gpu-renderer=ANGLE (Apple, ANGLE Metal Renderer: Apple M3, Unspecified Version)",
];
} }
// Linux: spoof as Windows // Linux/Windows: spoof as Windows desktop
// Hardware concurrency, device memory, screen, and window size are
// auto-generated by the binary from the seed (v14+).
return [ return [
...base, ...base,
"--fingerprint-platform=windows", "--fingerprint-platform=windows",
"--fingerprint-hardware-concurrency=8",
"--fingerprint-device-memory=8",
"--fingerprint-gpu-vendor=NVIDIA Corporation", "--fingerprint-gpu-vendor=NVIDIA Corporation",
"--fingerprint-gpu-renderer=NVIDIA GeForce RTX 3070", "--fingerprint-gpu-renderer=NVIDIA GeForce RTX 3070",
"--fingerprint-screen-width=1920",
"--fingerprint-screen-height=1080",
"--window-size=1920,1080",
]; ];
} }
+69 -30
View File
@@ -19,6 +19,8 @@ import {
GITHUB_DOWNLOAD_BASE_URL, GITHUB_DOWNLOAD_BASE_URL,
WRAPPER_VERSION, WRAPPER_VERSION,
checkPlatformAvailable, checkPlatformAvailable,
getArchiveExt,
getArchiveName,
getBinaryDir, getBinaryDir,
getBinaryPath, getBinaryPath,
getCacheDir, getCacheDir,
@@ -87,8 +89,8 @@ export async function ensureBinary(): Promise<string> {
if (!fs.existsSync(downloadedPath)) { if (!fs.existsSync(downloadedPath)) {
throw new Error( throw new Error(
`Download completed but binary not found at expected path: ${downloadedPath}. ` + `Download completed but binary not found at expected path: ${downloadedPath}. ` +
`This may indicate a packaging issue. Please report at ` + `This may indicate a packaging issue. Please report at ` +
`https://github.com/CloakHQ/cloakbrowser/issues` `https://github.com/CloakHQ/cloakbrowser/issues`
); );
} }
@@ -152,7 +154,7 @@ async function downloadAndExtract(version?: string): Promise<void> {
// Download to temp file (atomic — no partial downloads in cache) // Download to temp file (atomic — no partial downloads in cache)
const tmpPath = path.join( const tmpPath = path.join(
path.dirname(binaryDir), path.dirname(binaryDir),
`_download_${Date.now()}.tar.gz` `_download_${Date.now()}${getArchiveExt()}`
); );
try { try {
@@ -194,7 +196,7 @@ async function downloadAndExtract(version?: string): Promise<void> {
async function verifyDownloadChecksum(filePath: string, version?: string): Promise<void> { async function verifyDownloadChecksum(filePath: string, version?: string): Promise<void> {
const checksums = await fetchChecksums(version); const checksums = await fetchChecksums(version);
const tarballName = `cloakbrowser-${getPlatformTag()}.tar.gz`; const tarballName = getArchiveName();
if (!checksums) { if (!checksums) {
console.warn("[cloakbrowser] SHA256SUMS not available for this release — skipping checksum verification"); console.warn("[cloakbrowser] SHA256SUMS not available for this release — skipping checksum verification");
@@ -274,6 +276,9 @@ async function downloadFile(url: string, dest: string): Promise<void> {
const controller = new AbortController(); const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), DOWNLOAD_TIMEOUT_MS); const timeout = setTimeout(() => controller.abort(), DOWNLOAD_TIMEOUT_MS);
// Create file stream early so we can ensure cleanup on error
const fileStream = createWriteStream(dest);
try { try {
const response = await fetch(url, { const response = await fetch(url, {
signal: controller.signal, signal: controller.signal,
@@ -292,7 +297,6 @@ async function downloadFile(url: string, dest: string): Promise<void> {
let downloaded = 0; let downloaded = 0;
let lastLoggedPct = -1; let lastLoggedPct = -1;
const fileStream = createWriteStream(dest);
const reader = response.body.getReader(); const reader = response.body.getReader();
// Stream chunks to file with progress logging // Stream chunks to file with progress logging
@@ -316,19 +320,32 @@ async function downloadFile(url: string, dest: string): Promise<void> {
} }
} }
// Wait for file stream to finish // Wait for file stream to fully close (not just finish)
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
fileStream.end(() => resolve()); fileStream.end();
fileStream.on("close", () => resolve());
fileStream.on("error", reject); fileStream.on("error", reject);
}); });
const sizeMB = Math.floor(fs.statSync(dest).size / (1024 * 1024)); const sizeMB = Math.floor(fs.statSync(dest).size / (1024 * 1024));
console.log(`[cloakbrowser] Download complete: ${sizeMB} MB`); console.log(`[cloakbrowser] Download complete: ${sizeMB} MB`);
} catch (err) {
// Ensure file stream is destroyed on error to release the handle
if (!fileStream.destroyed) {
await new Promise<void>((resolve) => {
fileStream.destroy();
fileStream.on("close", () => resolve());
// Safety timeout in case close never fires
setTimeout(resolve, 2000);
});
}
throw err;
} finally { } finally {
clearTimeout(timeout); clearTimeout(timeout);
} }
} }
async function extractArchive( async function extractArchive(
archivePath: string, archivePath: string,
destDir: string, destDir: string,
@@ -342,30 +359,18 @@ async function extractArchive(
} }
fs.mkdirSync(destDir, { recursive: true }); fs.mkdirSync(destDir, { recursive: true });
// Extract with tar — the 'tar' package handles symlink/traversal safety if (archivePath.endsWith(".zip")) {
await tarExtract({ await extractZip(archivePath, destDir);
file: archivePath, } else {
cwd: destDir, await extractTar(archivePath, destDir);
// Security: strip leading path components and reject absolute paths }
strip: 0,
filter: (entryPath: string) => {
// Reject absolute paths and path traversal
if (path.isAbsolute(entryPath) || entryPath.includes("..")) {
console.warn(
`[cloakbrowser] Skipping suspicious archive entry: ${entryPath}`
);
return false;
}
return true;
},
});
// Flatten single subdirectory if needed // Flatten single subdirectory if needed
flattenSingleSubdir(destDir); flattenSingleSubdir(destDir);
// Make binary executable // Make binary executable (skip on Windows — no-op / AV lock risk)
const bp = binaryPath || getBinaryPath(); const bp = binaryPath || getBinaryPath();
if (fs.existsSync(bp)) { if (process.platform !== "win32" && fs.existsSync(bp)) {
fs.chmodSync(bp, 0o755); fs.chmodSync(bp, 0o755);
} }
@@ -379,6 +384,40 @@ async function extractArchive(
} }
} }
async function extractTar(archivePath: string, destDir: string): Promise<void> {
await tarExtract({
file: archivePath,
cwd: destDir,
strip: 0,
filter: (entryPath: string) => {
if (path.isAbsolute(entryPath) || entryPath.includes("..")) {
console.warn(
`[cloakbrowser] Skipping suspicious archive entry: ${entryPath}`
);
return false;
}
return true;
},
});
}
async function extractZip(archivePath: string, destDir: string): Promise<void> {
// Brief delay to ensure OS fully releases file handles (Windows)
await new Promise(resolve => setTimeout(resolve, 500));
if (process.platform === "win32") {
// PowerShell 5.1's Expand-Archive uses .NET FileStream which can conflict
// with recently-closed Node.js file handles. Use ZipFile API directly.
execFileSync("powershell", [
"-NoProfile", "-Command",
`Add-Type -AssemblyName System.IO.Compression.FileSystem; ` +
`[System.IO.Compression.ZipFile]::ExtractToDirectory('${archivePath}', '${destDir}')`,
], { timeout: 120_000 });
} else {
execFileSync("unzip", ["-o", archivePath, "-d", destDir], { timeout: 120_000 });
}
}
/** /**
* If extraction created a single subdirectory, move its contents up. * If extraction created a single subdirectory, move its contents up.
* Many tarballs wrap files in a top-level directory. * Many tarballs wrap files in a top-level directory.
@@ -452,7 +491,7 @@ export async function getLatestChromiumVersion(): Promise<string | null> {
draft: boolean; draft: boolean;
assets: Array<{ name: string }>; assets: Array<{ name: string }>;
}>; }>;
const platformTarball = `cloakbrowser-${getPlatformTag()}.tar.gz`; const platformTarball = getArchiveName();
for (const release of releases) { for (const release of releases) {
if (release.tag_name.startsWith("chromium-v") && !release.draft) { if (release.tag_name.startsWith("chromium-v") && !release.draft) {
const assetNames = new Set( const assetNames = new Set(
@@ -500,7 +539,7 @@ export async function checkWrapperUpdate(): Promise<void> {
if (data.version && versionNewer(data.version, WRAPPER_VERSION)) { if (data.version && versionNewer(data.version, WRAPPER_VERSION)) {
console.warn( console.warn(
`[cloakbrowser] Update available: ${WRAPPER_VERSION}${data.version}. ` + `[cloakbrowser] Update available: ${WRAPPER_VERSION}${data.version}. ` +
`Run: npm install cloakbrowser@latest` `Run: npm install cloakbrowser@latest`
); );
} }
} catch { } catch {
@@ -547,10 +586,10 @@ async function checkAndDownloadUpdate(): Promise<void> {
function maybeTriggerUpdateCheck(): void { function maybeTriggerUpdateCheck(): void {
// Wrapper update: once per process, not rate-limited // Wrapper update: once per process, not rate-limited
if (!wrapperUpdateChecked) { if (!wrapperUpdateChecked) {
checkWrapperUpdate().catch(() => {}); checkWrapperUpdate().catch(() => { });
} }
// Binary update: rate-limited to once per hour // Binary update: rate-limited to once per hour
if (!shouldCheckForUpdate()) return; if (!shouldCheckForUpdate()) return;
checkAndDownloadUpdate().catch(() => {}); checkAndDownloadUpdate().catch(() => { });
} }
+2 -2
View File
@@ -16,7 +16,7 @@
*/ */
// Launch functions (Playwright API) // Launch functions (Playwright API)
export { launch, launchContext } from "./playwright.js"; export { launch, launchContext, launchPersistentContext } from "./playwright.js";
// Binary management // Binary management
export { ensureBinary, clearCache, binaryInfo, checkForUpdate } from "./download.js"; export { ensureBinary, clearCache, binaryInfo, checkForUpdate } from "./download.js";
@@ -25,4 +25,4 @@ export { ensureBinary, clearCache, binaryInfo, checkForUpdate } from "./download
export { CHROMIUM_VERSION, getDefaultStealthArgs } from "./config.js"; export { CHROMIUM_VERSION, getDefaultStealthArgs } from "./config.js";
// Types // Types
export type { LaunchOptions, LaunchContextOptions, BinaryInfo } from "./types.js"; export type { LaunchOptions, LaunchContextOptions, LaunchPersistentContextOptions, BinaryInfo } from "./types.js";
+48 -1
View File
@@ -4,7 +4,7 @@
*/ */
import type { Browser, BrowserContext } from "playwright-core"; import type { Browser, BrowserContext } from "playwright-core";
import type { LaunchOptions, LaunchContextOptions } from "./types.js"; import type { LaunchOptions, LaunchContextOptions, LaunchPersistentContextOptions } from "./types.js";
import { DEFAULT_VIEWPORT, getDefaultStealthArgs } from "./config.js"; import { DEFAULT_VIEWPORT, getDefaultStealthArgs } from "./config.js";
import { ensureBinary } from "./download.js"; import { ensureBinary } from "./download.js";
import { parseProxyUrl } from "./proxy.js"; import { parseProxyUrl } from "./proxy.js";
@@ -88,6 +88,53 @@ export async function launchContext(
return context; return context;
} }
/**
* Launch stealth browser with a persistent user profile (non-incognito).
* Uses Playwright's chromium.launchPersistentContext() under the hood.
*
* This avoids incognito detection by services like BrowserScan (-10% penalty)
* and enables session persistence (cookies, localStorage) across launches.
*
* @example
* ```ts
* import { launchPersistentContext } from 'cloakbrowser';
* const context = await launchPersistentContext({
* userDataDir: './chrome-profile',
* headless: false,
* proxy: 'http://user:pass@host:port',
* geoip: true,
* });
* const page = context.pages()[0] || await context.newPage();
* await page.goto('https://example.com');
* await context.close();
* ```
*/
export async function launchPersistentContext(
options: LaunchPersistentContextOptions
): Promise<BrowserContext> {
const { chromium } = await import("playwright-core");
const binaryPath = process.env.CLOAKBROWSER_BINARY_PATH || (await ensureBinary());
const resolved = await maybeResolveGeoip(options);
const args = buildArgs({ ...options, ...resolved });
const context = await chromium.launchPersistentContext(options.userDataDir, {
executablePath: binaryPath,
headless: options.headless ?? true,
args,
ignoreDefaultArgs: ["--enable-automation"],
...(options.proxy ? { proxy: parseProxyUrl(options.proxy) } : {}),
...(options.userAgent ? { userAgent: options.userAgent } : {}),
viewport: options.viewport ?? DEFAULT_VIEWPORT,
...(resolved.locale ? { locale: resolved.locale } : {}),
...(resolved.timezone ? { timezoneId: resolved.timezone } : {}),
...(options.colorScheme ? { colorScheme: options.colorScheme } : {}),
...options.launchOptions,
});
return context;
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Internal // Internal
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
+5
View File
@@ -34,6 +34,11 @@ export interface LaunchContextOptions extends LaunchOptions {
colorScheme?: "light" | "dark" | "no-preference"; colorScheme?: "light" | "dark" | "no-preference";
} }
export interface LaunchPersistentContextOptions extends LaunchContextOptions {
/** Path to user data directory for persistent profile. */
userDataDir: string;
}
export interface BinaryInfo { export interface BinaryInfo {
version: string; version: string;
platform: string; platform: string;
+1 -1
View File
@@ -7,7 +7,7 @@ describe("binaryInfo", () => {
const info = binaryInfo(); const info = binaryInfo();
expect(info.version).toBe(getChromiumVersion()); expect(info.version).toBe(getChromiumVersion());
expect(info.platform).toMatch(/^(linux|darwin)-(x64|arm64)$/); expect(info.platform).toMatch(/^(linux|darwin|windows)-(x64|arm64)$/);
expect(info.binaryPath).toBeTruthy(); expect(info.binaryPath).toBeTruthy();
expect(typeof info.installed).toBe("boolean"); expect(typeof info.installed).toBe("boolean");
expect(info.cacheDir).toContain("cloakbrowser"); expect(info.cacheDir).toContain("cloakbrowser");
+4 -4
View File
@@ -100,7 +100,7 @@ describe("latest version (platform-aware)", () => {
{ {
tag_name: "chromium-v145.0.7718.0", tag_name: "chromium-v145.0.7718.0",
draft: false, draft: false,
assets: makeAssets(["linux-x64", "darwin-arm64", "darwin-x64"]), assets: makeAssets(["linux-x64", "darwin-arm64", "darwin-x64", "windows-x64"]),
}, },
]); ]);
expect(await getLatestChromiumVersion()).toBe("145.0.7718.0"); expect(await getLatestChromiumVersion()).toBe("145.0.7718.0");
@@ -116,7 +116,7 @@ describe("latest version (platform-aware)", () => {
{ {
tag_name: "chromium-v142.0.7444.175", tag_name: "chromium-v142.0.7444.175",
draft: false, draft: false,
assets: makeAssets(["linux-x64", "darwin-arm64", "darwin-x64"]), assets: makeAssets(["linux-x64", "darwin-arm64", "darwin-x64", "windows-x64"]),
}, },
]); ]);
const result = await getLatestChromiumVersion(); const result = await getLatestChromiumVersion();
@@ -133,14 +133,14 @@ describe("latest version (platform-aware)", () => {
{ {
tag_name: "chromium-v145.0.7718.0", tag_name: "chromium-v145.0.7718.0",
draft: false, draft: false,
assets: [{ name: "cloakbrowser-windows-x64.tar.gz" }], assets: [{ name: "cloakbrowser-freebsd-x64.tar.gz" }],
}, },
]); ]);
expect(await getLatestChromiumVersion()).toBeNull(); expect(await getLatestChromiumVersion()).toBeNull();
}); });
it("skips draft releases", async () => { it("skips draft releases", async () => {
const all = ["linux-x64", "darwin-arm64", "darwin-x64"]; const all = ["linux-x64", "darwin-arm64", "darwin-x64", "windows-x64"];
mockFetch([ mockFetch([
{ tag_name: "chromium-v999.0.0.0", draft: true, assets: makeAssets(all) }, { tag_name: "chromium-v999.0.0.0", draft: true, assets: makeAssets(all) },
{ tag_name: "chromium-v145.0.7718.0", draft: false, assets: makeAssets(all) }, { tag_name: "chromium-v145.0.7718.0", draft: false, assets: makeAssets(all) },
+5 -5
View File
@@ -166,7 +166,7 @@ class TestGetLatestVersion:
{ {
"tag_name": "chromium-v145.0.7718.0", "tag_name": "chromium-v145.0.7718.0",
"draft": False, "draft": False,
"assets": self._make_assets(["linux-x64", "darwin-arm64", "darwin-x64"]), "assets": self._make_assets(["linux-x64", "darwin-arm64", "darwin-x64", "windows-x64"]),
}, },
] ]
mock_response.raise_for_status = MagicMock() mock_response.raise_for_status = MagicMock()
@@ -187,7 +187,7 @@ class TestGetLatestVersion:
{ {
"tag_name": "chromium-v142.0.7444.175", "tag_name": "chromium-v142.0.7444.175",
"draft": False, "draft": False,
"assets": self._make_assets(["linux-x64", "darwin-arm64", "darwin-x64"]), "assets": self._make_assets(["linux-x64", "darwin-arm64", "darwin-x64", "windows-x64"]),
}, },
] ]
mock_response.raise_for_status = MagicMock() mock_response.raise_for_status = MagicMock()
@@ -202,7 +202,7 @@ class TestGetLatestVersion:
def test_skips_draft_releases(self): def test_skips_draft_releases(self):
mock_response = MagicMock() mock_response = MagicMock()
all_platforms = ["linux-x64", "darwin-arm64", "darwin-x64"] all_platforms = ["linux-x64", "darwin-arm64", "darwin-x64", "windows-x64"]
mock_response.json.return_value = [ mock_response.json.return_value = [
{"tag_name": "chromium-v999.0.0.0", "draft": True, "assets": self._make_assets(all_platforms)}, {"tag_name": "chromium-v999.0.0.0", "draft": True, "assets": self._make_assets(all_platforms)},
{"tag_name": "chromium-v145.0.7718.0", "draft": False, "assets": self._make_assets(all_platforms)}, {"tag_name": "chromium-v145.0.7718.0", "draft": False, "assets": self._make_assets(all_platforms)},
@@ -215,7 +215,7 @@ class TestGetLatestVersion:
def test_skips_non_chromium_tags(self): def test_skips_non_chromium_tags(self):
mock_response = MagicMock() mock_response = MagicMock()
all_platforms = ["linux-x64", "darwin-arm64", "darwin-x64"] all_platforms = ["linux-x64", "darwin-arm64", "darwin-x64", "windows-x64"]
mock_response.json.return_value = [ mock_response.json.return_value = [
{"tag_name": "v0.2.0", "draft": False, "assets": self._make_assets(all_platforms)}, {"tag_name": "v0.2.0", "draft": False, "assets": self._make_assets(all_platforms)},
{"tag_name": "chromium-v145.0.7718.0", "draft": False, "assets": self._make_assets(all_platforms)}, {"tag_name": "chromium-v145.0.7718.0", "draft": False, "assets": self._make_assets(all_platforms)},
@@ -233,7 +233,7 @@ class TestGetLatestVersion:
{ {
"tag_name": "chromium-v145.0.7718.0", "tag_name": "chromium-v145.0.7718.0",
"draft": False, "draft": False,
"assets": [{"name": "cloakbrowser-windows-x64.tar.gz"}], "assets": [{"name": "cloakbrowser-freebsd-x64.tar.gz"}],
}, },
] ]
mock_response.raise_for_status = MagicMock() mock_response.raise_for_status = MagicMock()