Compare commits

..
Author SHA1 Message Date
CloakHQ 54d8442f20 release: v0.3.22 — Chromium 146 upgrade (linux-x64) 2026-04-09 04:36:42 +02:00
CloakHQ a01adbe26c ci: restore npm upgrade for OIDC publishing (pin to npm@11)
Node 22 ships npm v10 which lacks OIDC support. The upgrade step was
removed in 02359f6 but is required for provenance-based publishing.
Pin to npm@11 instead of @latest to avoid future breakage.
2026-04-07 07:31:08 +02:00
CloakHQ 06d77e7261 refactor: remove dead stealth args, let binary handle GPU diversity
Remove --disable-blink-features=AutomationControlled (dead, binary handles
navigator.webdriver at source level) and hardcoded GPU vendor/renderer flags.
Binary auto-generates diverse GPU profiles from fingerprint seed. Improves
fingerprint diversity -- previously every user shared the same GPU string.

Bump to v0.3.21.
2026-04-07 07:16:22 +02:00
CloakHQ 211bd93d3e fix(docker): install geoip2 in Docker image
geoip=True raised ImportError inside the container because geoip2
was not installed. Added [geoip] extra to pip install.
2026-04-07 06:37:14 +02:00
CloakHQandkitiho 1060772734 fix: allow null viewport in Python wrapper (mirrors #107)
viewport=None now disables viewport emulation via Playwright's
no_viewport=True, matching the JS wrapper's viewport: null behavior.
Uses a sentinel to distinguish "not provided" from explicit None.

Co-authored-by: kitiho <51785099+kitiho@users.noreply.github.com>
2026-04-07 05:14:01 +02:00
kitihoandGitHub 8eb2e4b905 fix: allow null viewport to disable viewport emulation (#107)
fix: allow null viewport to disable viewport emulation
2026-04-07 05:11:44 +02:00
21 changed files with 361 additions and 714 deletions
+2
View File
@@ -85,6 +85,8 @@ jobs:
with: with:
node-version: 22 node-version: 22
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- name: Upgrade npm for OIDC support
run: npm install -g npm@11
- name: Build - name: Build
run: cd js && npm ci && npm run build run: cd js && npm ci && npm run build
- name: Publish to npm - name: Publish to npm
+5
View File
@@ -67,3 +67,8 @@ debug
publish-docker.sh publish-docker.sh
captures captures
20[0-9][0-9]-[0-9][0-9]-[0-9][0-9]-*.txt 20[0-9][0-9]-[0-9][0-9]-[0-9][0-9]-*.txt
# Beads / Dolt files (added by bd init)
.dolt/
*.db
.beads-credential-key
+13
View File
@@ -6,6 +6,19 @@ Changes are tagged: **[wrapper]** for Python/JS wrapper, **[binary]** for Chromi
--- ---
## [0.3.22] — 2026-04-09
- **[binary]** Upgrade Linux x64 build to Chromium 146.0.7680.177.1 — 49 source-level C++ patches (up from 48), rebased from 145.0.7632.x
## [0.3.21] — 2026-04-07
- **[wrapper]** Remove dead `--disable-blink-features=AutomationControlled` flag -- binary patch 009 already handles `navigator.webdriver` at source level
- **[wrapper]** Remove hardcoded GPU vendor/renderer flags -- binary auto-generates diverse, realistic GPU profiles from the fingerprint seed. Each seed gets a unique GPU instead of every user sharing the same one
- **[wrapper]** Allow `viewport=None` to disable viewport emulation in both Python and JS wrappers (thanks [@kitiho](https://github.com/kitiho), #107)
- **[wrapper]** Enable `geoip=True` in stealth test example to fix FingerprintJS detection
- **[meta]** Remove npm self-upgrade step in CI -- Node 22 ships with compatible npm
- **[docker]** Install `geoip2` in Docker image for GeoIP auto-detection support
## [0.3.20] — 2026-04-06 ## [0.3.20] — 2026-04-06
- **[binary]** Upgrade Linux x64 build to 145.0.7632.159.9 — 48 source-level C++ patches (up from 42) - **[binary]** Upgrade Linux x64 build to 145.0.7632.159.9 — 48 source-level C++ patches (up from 42)
+1 -1
View File
@@ -20,7 +20,7 @@ WORKDIR /app
# Python wrapper # Python wrapper
COPY pyproject.toml README.md LICENSE BINARY-LICENSE.md CHANGELOG.md ./ COPY pyproject.toml README.md LICENSE BINARY-LICENSE.md CHANGELOG.md ./
COPY cloakbrowser/ cloakbrowser/ COPY cloakbrowser/ cloakbrowser/
RUN pip install --no-cache-dir ".[serve]" RUN pip install --no-cache-dir ".[serve,geoip]"
# JS wrapper # JS wrapper
COPY js/ js/ COPY js/ js/
+23 -52
View File
@@ -40,7 +40,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>
- **48 source-level C++ patches** — canvas, WebGL, audio, fonts, GPU, screen, WebRTC, network timing, automation signals, CDP input behavior - **49 source-level C++ patches** — canvas, WebGL, audio, fonts, GPU, screen, WebRTC, network timing, automation signals, CDP input behavior
- **`humanize=True`** — human-like mouse curves, keyboard timing, and scroll patterns. One flag, behavioral detection passes - **`humanize=True`** — human-like mouse curves, keyboard timing, and scroll patterns. One flag, behavioral detection passes
- **0.9 reCAPTCHA v3 score** — human-level, server-verified - **0.9 reCAPTCHA v3 score** — human-level, server-verified
- **Passes Cloudflare Turnstile**, FingerprintJS, BrowserScan — tested against 30+ detection sites - **Passes Cloudflare Turnstile**, FingerprintJS, BrowserScan — tested against 30+ detection sites
@@ -128,9 +128,10 @@ Open [http://localhost:8080](http://localhost:8080). Create a profile. Click **L
--- ---
## Latest: v0.3.20 (Chromium 145.0.7632.159.9) ## Latest: v0.3.22 (Chromium 146.0.7680.177.1)
- **48 fingerprint patches** (Linux x64) — 6 new patches covering WebRTC IP spoofing, proxy signal removal, and network timing normalization - **Chromium 146 upgrade** — rebased all patches from 145.0.7632.x to 146.0.7680.177
- **49 fingerprint patches** (Linux x64) — 1 new patch, all existing patches carried forward
- **WebRTC IP spoofing** — `--fingerprint-webrtc-ip=auto` resolves your proxy's exit IP and spoofs WebRTC ICE candidates. Auto-injected when using `geoip=True` (no extra network call) - **WebRTC IP spoofing** — `--fingerprint-webrtc-ip=auto` resolves your proxy's exit IP and spoofs WebRTC ICE candidates. Auto-injected when using `geoip=True` (no extra network call)
- **Proxy signal removal** — DNS/connect/SSL timing zeroed, proxy cache headers stripped, Proxy-Connection header leak removed - **Proxy signal removal** — DNS/connect/SSL timing zeroed, proxy cache headers stripped, Proxy-Connection header leak removed
- **`cloakserve` CDP multiplexer** — rewritten as a multi-connection CDP proxy with per-connection fingerprint seeds - **`cloakserve` CDP multiplexer** — rewritten as a multi-connection CDP proxy with per-connection fingerprint seeds
@@ -154,7 +155,7 @@ CloakBrowser doesn't solve CAPTCHAs — it prevents them from appearing. No CAPT
## Test Results ## Test Results
All tests verified against live detection services. Last tested: Mar 2026 (Chromium 145). All tests verified against live detection services. Last tested: Apr 2026 (Chromium 146).
| Detection Service | Stock Playwright | CloakBrowser | Notes | | Detection Service | Stock Playwright | CloakBrowser | Notes |
|---|---|---|---| |---|---|---|---|
@@ -169,7 +170,7 @@ All tests verified against live detection services. Last tested: Mar 2026 (Chrom
| `navigator.webdriver` | `true` | **`false`** | Source-level patch | | `navigator.webdriver` | `true` | **`false`** | Source-level patch |
| `navigator.plugins.length` | 0 | **5** | Real plugin list | | `navigator.plugins.length` | 0 | **5** | Real plugin list |
| `window.chrome` | `undefined` | **`object`** | Present like real Chrome | | `window.chrome` | `undefined` | **`object`** | Present like real Chrome |
| UA string | `HeadlessChrome` | **`Chrome/145.0.0.0`** | No headless leak | | UA string | `HeadlessChrome` | **`Chrome/146.0.0.0`** | No headless leak |
| CDP detection | Detected | **Not detected** | `isAutomatedWithCDP: false` | | CDP detection | Detected | **Not detected** | `isAutomatedWithCDP: false` |
| TLS fingerprint | Mismatch | **Identical to Chrome** | ja3n/ja4/akamai match | | TLS fingerprint | Mismatch | **Identical to Chrome** | ja3n/ja4/akamai match |
| | | **Tested against 30+ detection sites** | | | | | **Tested against 30+ detection sites** | |
@@ -218,11 +219,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 (Chromium 145) 2. **First launch** → binary auto-downloads for your platform (Chromium 146)
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 48 source-level patches covering canvas, WebGL, audio, fonts, GPU, screen properties, WebRTC, network timing, hardware reporting, automation signal removal, and CDP input behavior mimicking. The binary includes 49 source-level patches covering canvas, WebGL, audio, fonts, GPU, screen properties, WebRTC, network timing, hardware reporting, automation signal removal, and CDP input behavior mimicking.
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.
@@ -369,7 +370,7 @@ from cloakbrowser import binary_info, clear_cache, ensure_binary
# Check binary installation status # Check binary installation status
print(binary_info()) print(binary_info())
# {'version': '145.0.7632.159.2', 'platform': 'linux-x64', 'installed': True, ...} # {'version': '146.0.7680.177.1', 'platform': 'linux-x64', 'installed': True, ...}
# Force re-download # Force re-download
clear_cache() clear_cache()
@@ -514,33 +515,6 @@ Access the original un-patched Playwright page at `page._original` if you need r
> Contributed by [@evelaa123](https://github.com/evelaa123) — full Playwright API coverage. > Contributed by [@evelaa123](https://github.com/evelaa123) — full Playwright API coverage.
## Stealth Evaluate
`page.stealth_evaluate(expression)` runs JavaScript in a CDP isolated world instead of Playwright's main-world `evaluate()`. This produces clean `Error.stack` traces and full variable isolation from page JS — useful when a site's anti-bot scripts inspect execution context.
```python
browser = launch()
page = browser.new_page()
page.goto("https://example.com")
# Stealth — clean stack trace, invisible to page JS
title = page.stealth_evaluate("document.title")
rect = page.stealth_evaluate("document.querySelector('#btn').getBoundingClientRect().toJSON()")
# Regular evaluate — unchanged, use for DOM writes
page.evaluate("document.body.style.display = 'none'")
```
```javascript
const browser = await launch();
const page = await browser.newPage();
await page.goto('https://example.com');
const title = await page.stealthEvaluate('document.title');
```
Always available on every page — no flag needed. Returns JSON-serializable values only. The isolated world context auto-recreates after navigation.
## Configuration ## Configuration
| Env Variable | Default | Description | | Env Variable | Default | Description |
@@ -581,14 +555,10 @@ Every `launch()` call sets these automatically. The **wrapper** applies platform
|------|--------------|---------------|----------| |------|--------------|---------------|----------|
| `--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-gpu-vendor` | `NVIDIA Corporation` | `Google Inc. (Apple)` | WebGL `UNMASKED_VENDOR_WEBGL` |
| `--fingerprint-gpu-renderer` | `NVIDIA GeForce RTX 3070` | `ANGLE (Apple, ANGLE Metal Renderer: Apple M3, Unspecified Version)` | WebGL `UNMASKED_RENDERER_WEBGL` |
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. The binary auto-generates everything else from the seed: GPU, hardware concurrency, device memory, and screen dimensions. Each seed produces a unique, consistent fingerprint. 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. > **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
@@ -596,6 +566,8 @@ Supported by the binary but **not set by default** — pass via `args` to custom
| Flag | Controls | | Flag | Controls |
|------|----------| |------|----------|
| `--fingerprint-gpu-vendor` | WebGL `UNMASKED_VENDOR_WEBGL` (auto-generated from seed + platform) |
| `--fingerprint-gpu-renderer` | WebGL `UNMASKED_RENDERER_WEBGL` (auto-generated from seed + platform) |
| `--fingerprint-hardware-concurrency` | `navigator.hardwareConcurrency` (auto-generated: `8`) | | `--fingerprint-hardware-concurrency` | `navigator.hardwareConcurrency` (auto-generated: `8`) |
| `--fingerprint-device-memory` | `navigator.deviceMemory` in GB (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-width` | Screen width (auto-generated: `1920` Win/Linux, `1440` macOS) |
@@ -625,11 +597,9 @@ 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-gpu-vendor=NVIDIA Corporation",
"--fingerprint-gpu-renderer=NVIDIA GeForce RTX 3070",
]) ])
# Override GPU to look like a different machine # Override GPU to look like a specific machine
browser = launch(args=[ browser = launch(args=[
"--fingerprint-gpu-vendor=Intel Inc.", "--fingerprint-gpu-vendor=Intel Inc.",
"--fingerprint-gpu-renderer=Intel Iris OpenGL Engine", "--fingerprint-gpu-renderer=Intel Iris OpenGL Engine",
@@ -683,11 +653,11 @@ browser = await launch_async(args=["--remote-debugging-port=9242"])
| Platform | Chromium | Patches | Status | | Platform | Chromium | Patches | Status |
|---|---|---|---| |---|---|---|---|
| Linux x86_64 | 145 | 42 | ✅ Latest | | Linux x86_64 | 146 | 49 | ✅ Latest |
| Linux arm64 (RPi, Graviton) | 145 | 33 | ✅ | | Linux arm64 (RPi, Graviton) | 145 | 48 | ✅ |
| macOS arm64 (Apple Silicon) | 145 | 26 | ✅ | | macOS arm64 (Apple Silicon) | 145 | 26 | ✅ |
| macOS x86_64 (Intel) | 145 | 26 | ✅ | | macOS x86_64 (Intel) | 145 | 26 | ✅ |
| Windows x86_64 | 145 | 33 | ✅ | | Windows x86_64 | 145 | 48 | ✅ |
The wrapper auto-downloads the correct binary for your platform. The wrapper auto-downloads the correct binary for your platform.
@@ -948,9 +918,9 @@ export CLOAKBROWSER_BINARY_PATH=/path/to/your/chrome
Install a specific wrapper version to downgrade both the wrapper and the binary it downloads: Install a specific wrapper version to downgrade both the wrapper and the binary it downloads:
```bash ```bash
pip install cloakbrowser==0.3.11 # Python pip install cloakbrowser==0.3.21 # Python
npm install cloakbrowser@0.3.11 # JavaScript npm install cloakbrowser@0.3.21 # JavaScript
docker pull cloakhq/cloakbrowser:0.3.11 # Docker docker pull cloakhq/cloakbrowser:0.3.21 # Docker
``` ```
Each wrapper version pins its own binary version, so downgrading the wrapper automatically gets you the matching binary on next launch. Each wrapper version pins its own binary version, so downgrading the wrapper automatically gets you the matching binary on next launch.
@@ -1046,7 +1016,7 @@ A: Yes. Pass `proxy="http://user:pass@host:port"` to `launch()`.
| Feature | Status | | Feature | Status |
|---------|--------| |---------|--------|
| Linux x64 — Chromium 145 (48 patches) | ✅ Released | | Linux x64 — Chromium 146 (49 patches) | ✅ Released |
| macOS arm64/x64 — Chromium 145 (26 patches) | ✅ Released | | macOS arm64/x64 — Chromium 145 (26 patches) | ✅ Released |
| Windows x64 — Chromium 145 (33 patches) | ✅ Released | | Windows x64 — Chromium 145 (33 patches) | ✅ Released |
| JavaScript/Puppeteer + Playwright support | ✅ Released | | JavaScript/Puppeteer + Playwright support | ✅ Released |
@@ -1070,7 +1040,7 @@ All releases are signed for supply chain verification.
```bash ```bash
# Verify GPG signature (binary release tag) # Verify GPG signature (binary release tag)
gpg --keyserver keyserver.ubuntu.com --recv-keys C60C0DDC9D0DE2DD gpg --keyserver keyserver.ubuntu.com --recv-keys C60C0DDC9D0DE2DD
git verify-tag chromium-v145.0.7632.159.9 git verify-tag chromium-v146.0.7680.177.1
# Verify GitHub binary attestation (Sigstore) # Verify GitHub binary attestation (Sigstore)
gh attestation verify cloakbrowser-linux-x64.tar.gz --repo CloakHQ/cloakbrowser gh attestation verify cloakbrowser-linux-x64.tar.gz --repo CloakHQ/cloakbrowser
@@ -1095,3 +1065,4 @@ Issues and PRs welcome. If something isn't working, [open an issue](https://gith
- [@evelaa123](https://github.com/evelaa123) — humanize behavior, persistent contexts, Windows fix - [@evelaa123](https://github.com/evelaa123) — humanize behavior, persistent contexts, Windows fix
- [@yahooguntu](https://github.com/yahooguntu) — persistent contexts - [@yahooguntu](https://github.com/yahooguntu) — persistent contexts
- [@kitiho](https://github.com/kitiho) — null viewport fix
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.3.20" __version__ = "0.3.22"
+27 -26
View File
@@ -24,6 +24,9 @@ from .download import ensure_binary
logger = logging.getLogger("cloakbrowser") logger = logging.getLogger("cloakbrowser")
# Sentinel to distinguish "viewport not provided" from "viewport=None" (disable emulation)
_VIEWPORT_UNSET = object()
def _resolve_timezone(timezone: str | None, kwargs: dict[str, Any]) -> str | None: def _resolve_timezone(timezone: str | None, kwargs: dict[str, Any]) -> str | None:
"""Accept both timezone and timezone_id — either works, no warning.""" """Accept both timezone and timezone_id — either works, no warning."""
@@ -138,10 +141,6 @@ def launch(
cfg = resolve_config(human_preset, human_config) cfg = resolve_config(human_preset, human_config)
patch_browser(browser, cfg) patch_browser(browser, cfg)
# Stealth evaluate — always attached
from .stealth_eval import patch_browser_stealth_eval
patch_browser_stealth_eval(browser, is_async=False)
return browser return browser
@@ -231,10 +230,6 @@ async def launch_async( # noqa: C901
cfg = resolve_config(human_preset, human_config) cfg = resolve_config(human_preset, human_config)
patch_browser_async(browser, cfg) patch_browser_async(browser, cfg)
# Stealth evaluate — always attached
from .stealth_eval import patch_browser_stealth_eval
patch_browser_stealth_eval(browser, is_async=True)
return browser return browser
@@ -245,7 +240,7 @@ def launch_persistent_context(
args: list[str] | None = None, args: list[str] | None = None,
stealth_args: bool = True, stealth_args: bool = True,
user_agent: str | None = None, user_agent: str | None = None,
viewport: dict | None = None, viewport: dict | None = _VIEWPORT_UNSET,
locale: str | None = None, locale: str | None = None,
timezone: str | None = None, timezone: str | None = None,
color_scheme: Literal["light", "dark", "no-preference"] | None = None, color_scheme: Literal["light", "dark", "no-preference"] | None = None,
@@ -272,6 +267,7 @@ def launch_persistent_context(
stealth_args: Include default stealth fingerprint args (default True). stealth_args: Include default stealth fingerprint args (default True).
user_agent: Custom user agent string. user_agent: Custom user agent string.
viewport: Viewport size dict, e.g. {"width": 1920, "height": 1080}. viewport: Viewport size dict, e.g. {"width": 1920, "height": 1080}.
Pass None to disable viewport emulation (use OS window size).
locale: Browser locale, e.g. "en-US". locale: Browser locale, e.g. "en-US".
timezone: IANA timezone (e.g. 'America/New_York'). timezone: IANA timezone (e.g. 'America/New_York').
color_scheme: Color scheme preference — 'light', 'dark', or 'no-preference'. color_scheme: Color scheme preference — 'light', 'dark', or 'no-preference'.
@@ -318,7 +314,12 @@ def launch_persistent_context(
context_kwargs: dict[str, Any] = {} context_kwargs: dict[str, Any] = {}
if user_agent: if user_agent:
context_kwargs["user_agent"] = user_agent context_kwargs["user_agent"] = user_agent
context_kwargs["viewport"] = viewport or DEFAULT_VIEWPORT if viewport is _VIEWPORT_UNSET:
context_kwargs["viewport"] = DEFAULT_VIEWPORT
elif viewport is None:
context_kwargs["no_viewport"] = True
else:
context_kwargs["viewport"] = viewport
if color_scheme: if color_scheme:
context_kwargs["color_scheme"] = color_scheme context_kwargs["color_scheme"] = color_scheme
context_kwargs.update(kwargs) context_kwargs.update(kwargs)
@@ -352,10 +353,6 @@ def launch_persistent_context(
cfg = resolve_config(human_preset, human_config) cfg = resolve_config(human_preset, human_config)
patch_context(context, cfg) patch_context(context, cfg)
# Stealth evaluate — always attached
from .stealth_eval import patch_context_stealth_eval
patch_context_stealth_eval(context, is_async=False)
return context return context
@@ -366,7 +363,7 @@ async def launch_persistent_context_async(
args: list[str] | None = None, args: list[str] | None = None,
stealth_args: bool = True, stealth_args: bool = True,
user_agent: str | None = None, user_agent: str | None = None,
viewport: dict | None = None, viewport: dict | None = _VIEWPORT_UNSET,
locale: str | None = None, locale: str | None = None,
timezone: str | None = None, timezone: str | None = None,
color_scheme: Literal["light", "dark", "no-preference"] | None = None, color_scheme: Literal["light", "dark", "no-preference"] | None = None,
@@ -392,6 +389,7 @@ async def launch_persistent_context_async(
stealth_args: Include default stealth fingerprint args (default True). stealth_args: Include default stealth fingerprint args (default True).
user_agent: Custom user agent string. user_agent: Custom user agent string.
viewport: Viewport size dict, e.g. {"width": 1920, "height": 1080}. viewport: Viewport size dict, e.g. {"width": 1920, "height": 1080}.
Pass None to disable viewport emulation (use OS window size).
locale: Browser locale, e.g. "en-US". locale: Browser locale, e.g. "en-US".
timezone: IANA timezone (e.g. 'America/New_York'). timezone: IANA timezone (e.g. 'America/New_York').
color_scheme: Color scheme preference — 'light', 'dark', or 'no-preference'. color_scheme: Color scheme preference — 'light', 'dark', or 'no-preference'.
@@ -441,7 +439,12 @@ async def launch_persistent_context_async(
context_kwargs: dict[str, Any] = {} context_kwargs: dict[str, Any] = {}
if user_agent: if user_agent:
context_kwargs["user_agent"] = user_agent context_kwargs["user_agent"] = user_agent
context_kwargs["viewport"] = viewport or DEFAULT_VIEWPORT if viewport is _VIEWPORT_UNSET:
context_kwargs["viewport"] = DEFAULT_VIEWPORT
elif viewport is None:
context_kwargs["no_viewport"] = True
else:
context_kwargs["viewport"] = viewport
if color_scheme: if color_scheme:
context_kwargs["color_scheme"] = color_scheme context_kwargs["color_scheme"] = color_scheme
context_kwargs.update(kwargs) context_kwargs.update(kwargs)
@@ -475,10 +478,6 @@ async def launch_persistent_context_async(
cfg = resolve_config(human_preset, human_config) cfg = resolve_config(human_preset, human_config)
patch_context_async(context, cfg) patch_context_async(context, cfg)
# Stealth evaluate — always attached
from .stealth_eval import patch_context_stealth_eval
patch_context_stealth_eval(context, is_async=True)
return context return context
@@ -488,7 +487,7 @@ def launch_context(
args: list[str] | None = None, args: list[str] | None = None,
stealth_args: bool = True, stealth_args: bool = True,
user_agent: str | None = None, user_agent: str | None = None,
viewport: dict | None = None, viewport: dict | None = _VIEWPORT_UNSET,
locale: str | None = None, locale: str | None = None,
timezone: str | None = None, timezone: str | None = None,
color_scheme: Literal["light", "dark", "no-preference"] | None = None, color_scheme: Literal["light", "dark", "no-preference"] | None = None,
@@ -511,6 +510,7 @@ def launch_context(
stealth_args: Include default stealth fingerprint args (default True). stealth_args: Include default stealth fingerprint args (default True).
user_agent: Custom user agent string. user_agent: Custom user agent string.
viewport: Viewport size dict, e.g. {"width": 1920, "height": 1080}. viewport: Viewport size dict, e.g. {"width": 1920, "height": 1080}.
Pass None to disable viewport emulation (use OS window size).
locale: Browser locale, e.g. "en-US". locale: Browser locale, e.g. "en-US".
timezone: IANA timezone (e.g. 'America/New_York'). timezone: IANA timezone (e.g. 'America/New_York').
color_scheme: Color scheme preference — 'light', 'dark', or 'no-preference'. color_scheme: Color scheme preference — 'light', 'dark', or 'no-preference'.
@@ -543,7 +543,12 @@ def launch_context(
context_kwargs: dict[str, Any] = {} context_kwargs: dict[str, Any] = {}
if user_agent: if user_agent:
context_kwargs["user_agent"] = user_agent context_kwargs["user_agent"] = user_agent
context_kwargs["viewport"] = viewport or DEFAULT_VIEWPORT if viewport is _VIEWPORT_UNSET:
context_kwargs["viewport"] = DEFAULT_VIEWPORT
elif viewport is None:
context_kwargs["no_viewport"] = True
else:
context_kwargs["viewport"] = viewport
if color_scheme: if color_scheme:
context_kwargs["color_scheme"] = color_scheme context_kwargs["color_scheme"] = color_scheme
context_kwargs.update(kwargs) context_kwargs.update(kwargs)
@@ -572,10 +577,6 @@ def launch_context(
cfg = resolve_config(human_preset, human_config) cfg = resolve_config(human_preset, human_config)
patch_context(context, cfg) patch_context(context, cfg)
# Stealth evaluate — always attached
from .stealth_eval import patch_context_stealth_eval
patch_context_stealth_eval(context, is_async=False)
return context return context
+5 -14
View File
@@ -15,10 +15,10 @@ from ._version import __version__
# 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.159.9" CHROMIUM_VERSION = "146.0.7680.177.1"
PLATFORM_CHROMIUM_VERSIONS: dict[str, str] = { PLATFORM_CHROMIUM_VERSIONS: dict[str, str] = {
"linux-x64": "145.0.7632.159.9", "linux-x64": "146.0.7680.177.1",
"linux-arm64": "145.0.7632.159.7", "linux-arm64": "145.0.7632.159.7",
"darwin-arm64": "145.0.7632.109.2", "darwin-arm64": "145.0.7632.109.2",
"darwin-x64": "145.0.7632.109.2", "darwin-x64": "145.0.7632.109.2",
@@ -48,26 +48,17 @@ def get_default_stealth_args() -> list[str]:
base = [ base = [
"--no-sandbox", "--no-sandbox",
"--disable-blink-features=AutomationControlled",
f"--fingerprint={seed}", f"--fingerprint={seed}",
] ]
if system == "Darwin": if system == "Darwin":
# 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/Windows: Windows fingerprint profile # Linux/Windows: Windows fingerprint profile
# Hardware concurrency, device memory, screen, and window size are # Hardware concurrency, device memory, screen, window size, and GPU are
# auto-generated by the binary from the seed (v14+). # auto-generated by the binary from the seed (v14+).
return base + [ return base + ["--fingerprint-platform=windows"]
"--fingerprint-platform=windows",
"--fingerprint-gpu-vendor=Google Inc. (NVIDIA)",
"--fingerprint-gpu-renderer=ANGLE (NVIDIA, NVIDIA GeForce RTX 3070 (0x00002484) Direct3D11 vs_5_0 ps_5_0, D3D11)",
]
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
+137 -9
View File
@@ -26,7 +26,6 @@ from .scroll import scroll_to_element
from .mouse_async import AsyncRawMouse, async_human_move, async_human_click, async_human_idle from .mouse_async import AsyncRawMouse, async_human_move, async_human_click, async_human_idle
from .keyboard_async import AsyncRawKeyboard, async_human_type from .keyboard_async import AsyncRawKeyboard, async_human_type
from .scroll_async import async_scroll_to_element from .scroll_async import async_scroll_to_element
from ..stealth_eval import _SyncIsolatedWorld, _AsyncIsolatedWorld
_SELECT_ALL = "Meta+a" if sys.platform == "darwin" else "Control+a" _SELECT_ALL = "Meta+a" if sys.platform == "darwin" else "Control+a"
@@ -45,9 +44,142 @@ logger = logging.getLogger("cloakbrowser.human")
# CDP Isolated World — stealth DOM evaluation # CDP Isolated World — stealth DOM evaluation
# ============================================================================ # ============================================================================
class _SyncIsolatedWorld:
"""Manages a CDP isolated execution context for DOM reads (sync).
# _SyncIsolatedWorld and _AsyncIsolatedWorld are defined in Produces clean Error.stack traces (no 'eval at evaluate :302:')
# cloakbrowser.stealth_eval and imported at the top of this file. and is invisible to querySelector monkey-patches in the main world.
Context ID is invalidated on navigation and auto-recreated on next call.
"""
__slots__ = ("_page", "_cdp", "_context_id")
def __init__(self, page: Any):
self._page = page
self._cdp: Any = None
self._context_id: Optional[int] = None
def _ensure_cdp(self) -> Any:
if self._cdp is None:
self._cdp = self._page.context.new_cdp_session(self._page)
return self._cdp
def _create_world(self) -> int:
cdp = self._ensure_cdp()
tree = cdp.send("Page.getFrameTree")
frame_id = tree["frameTree"]["frame"]["id"]
result = cdp.send("Page.createIsolatedWorld", {
"frameId": frame_id,
"worldName": "",
"grantUniveralAccess": True,
})
self._context_id = result["executionContextId"]
return self._context_id
def evaluate(self, expression: str) -> Any:
"""Evaluate JS in isolated world. Auto-recreates on stale context."""
if self._context_id is None:
self._create_world()
for attempt in range(2):
try:
result = self._cdp.send("Runtime.evaluate", {
"expression": expression,
"contextId": self._context_id,
"returnByValue": True,
})
if "exceptionDetails" in result:
if attempt == 0:
self._create_world()
continue
return None
return result.get("result", {}).get("value")
except Exception:
if attempt == 0:
self._context_id = None
try:
self._create_world()
except Exception:
return None
continue
return None
return None
def invalidate(self) -> None:
"""Mark context as stale — call after navigation."""
self._context_id = None
def get_cdp_session(self) -> Any:
"""Get the underlying CDP session (reused for Input.dispatchKeyEvent)."""
return self._ensure_cdp()
class _AsyncIsolatedWorld:
"""Manages a CDP isolated execution context for DOM reads (async).
Same as _SyncIsolatedWorld but uses await for all CDP calls.
"""
__slots__ = ("_page", "_cdp", "_context_id")
def __init__(self, page: Any):
self._page = page
self._cdp: Any = None
self._context_id: Optional[int] = None
async def _ensure_cdp(self) -> Any:
if self._cdp is None:
self._cdp = await self._page.context.new_cdp_session(self._page)
return self._cdp
async def _create_world(self) -> int:
cdp = await self._ensure_cdp()
tree = await cdp.send("Page.getFrameTree")
frame_id = tree["frameTree"]["frame"]["id"]
result = await cdp.send("Page.createIsolatedWorld", {
"frameId": frame_id,
"worldName": "",
"grantUniveralAccess": True,
})
self._context_id = result["executionContextId"]
return self._context_id
async def evaluate(self, expression: str) -> Any:
"""Evaluate JS in isolated world. Auto-recreates on stale context."""
if self._context_id is None:
await self._create_world()
for attempt in range(2):
try:
result = await self._cdp.send("Runtime.evaluate", {
"expression": expression,
"contextId": self._context_id,
"returnByValue": True,
})
if "exceptionDetails" in result:
if attempt == 0:
await self._create_world()
continue
return None
return result.get("result", {}).get("value")
except Exception:
if attempt == 0:
self._context_id = None
try:
await self._create_world()
except Exception:
return None
continue
return None
return None
def invalidate(self) -> None:
"""Mark context as stale — call after navigation."""
self._context_id = None
async def get_cdp_session(self) -> Any:
"""Get the underlying CDP session (reused for Input.dispatchKeyEvent)."""
return await self._ensure_cdp()
# ============================================================================ # ============================================================================
@@ -607,10 +739,8 @@ def patch_page(page: Any, cfg: HumanConfig, cursor: _CursorState) -> None:
page._original = originals page._original = originals
page._human_cfg = cfg page._human_cfg = cfg
# --- Stealth infrastructure (reuse if already attached by stealth_eval) --- # --- Stealth infrastructure ---
try: try:
stealth = getattr(page, '_stealth_world', None)
if not isinstance(stealth, _SyncIsolatedWorld):
stealth = _SyncIsolatedWorld(page) stealth = _SyncIsolatedWorld(page)
page._stealth_world = stealth page._stealth_world = stealth
cdp_session = stealth.get_cdp_session() cdp_session = stealth.get_cdp_session()
@@ -974,9 +1104,7 @@ def patch_page_async(page: Any, cfg: HumanConfig, cursor: _CursorState) -> None:
page._original = originals page._original = originals
page._human_cfg = cfg page._human_cfg = cfg
# --- Stealth infrastructure (reuse if already attached by stealth_eval) --- # --- Stealth infrastructure (lazy-initialized, async) ---
stealth = getattr(page, '_stealth_world', None)
if not isinstance(stealth, _AsyncIsolatedWorld):
stealth = _AsyncIsolatedWorld(page) stealth = _AsyncIsolatedWorld(page)
page._stealth_world = stealth page._stealth_world = stealth
cdp_session_holder: list[Any] = [None] # mutable container for closure cdp_session_holder: list[Any] = [None] # mutable container for closure
-271
View File
@@ -1,271 +0,0 @@
"""Stealth evaluate — run JS in a CDP isolated world.
Provides page.stealth_evaluate(expression) on every page returned by
cloakbrowser launch functions. Produces clean Error.stack traces (no
``eval at evaluate :302:`` leak) and full variable isolation from main
world JS. Context auto-recreates after navigation.
The same isolated world instances are reused by the humanize layer
(human/__init__.py) for stealth DOM queries.
"""
from __future__ import annotations
import logging
from typing import Any, Optional
logger = logging.getLogger("cloakbrowser.stealth_eval")
# ============================================================================
# Isolated world classes
# ============================================================================
class _SyncIsolatedWorld:
"""CDP isolated execution context for DOM reads (sync).
Produces clean Error.stack traces and is invisible to
querySelector monkey-patches in the main world.
Context ID is invalidated on navigation and auto-recreated.
"""
__slots__ = ("_page", "_cdp", "_context_id")
def __init__(self, page: Any):
self._page = page
self._cdp: Any = None
self._context_id: Optional[int] = None
def _ensure_cdp(self) -> Any:
if self._cdp is None:
self._cdp = self._page.context.new_cdp_session(self._page)
return self._cdp
def _create_world(self) -> int:
cdp = self._ensure_cdp()
tree = cdp.send("Page.getFrameTree")
frame_id = tree["frameTree"]["frame"]["id"]
result = cdp.send("Page.createIsolatedWorld", {
"frameId": frame_id,
"worldName": "",
"grantUniveralAccess": True,
})
self._context_id = result["executionContextId"]
return self._context_id
def evaluate(self, expression: str) -> Any:
"""Evaluate JS in isolated world. Auto-recreates on stale context."""
if self._context_id is None:
try:
self._create_world()
except Exception:
logger.debug("stealth_evaluate: failed to create isolated world")
return None
for attempt in range(2):
try:
result = self._cdp.send("Runtime.evaluate", {
"expression": expression,
"contextId": self._context_id,
"returnByValue": True,
})
if "exceptionDetails" in result:
if attempt == 0:
self._create_world()
continue
logger.debug("stealth_evaluate: JS exception: %s",
result["exceptionDetails"].get("text", "unknown"))
return None
return result.get("result", {}).get("value")
except Exception:
if attempt == 0:
self._context_id = None
try:
self._create_world()
except Exception:
logger.debug("stealth_evaluate: failed to recreate isolated world")
return None
continue
logger.debug("stealth_evaluate: CDP evaluate failed after retry")
return None
return None
def invalidate(self) -> None:
"""Mark context as stale — call after navigation."""
self._context_id = None
def get_cdp_session(self) -> Any:
"""Get the underlying CDP session (reused for Input.dispatchKeyEvent)."""
return self._ensure_cdp()
class _AsyncIsolatedWorld:
"""CDP isolated execution context for DOM reads (async).
Same as _SyncIsolatedWorld but uses await for all CDP calls.
"""
__slots__ = ("_page", "_cdp", "_context_id")
def __init__(self, page: Any):
self._page = page
self._cdp: Any = None
self._context_id: Optional[int] = None
async def _ensure_cdp(self) -> Any:
if self._cdp is None:
self._cdp = await self._page.context.new_cdp_session(self._page)
return self._cdp
async def _create_world(self) -> int:
cdp = await self._ensure_cdp()
tree = await cdp.send("Page.getFrameTree")
frame_id = tree["frameTree"]["frame"]["id"]
result = await cdp.send("Page.createIsolatedWorld", {
"frameId": frame_id,
"worldName": "",
"grantUniveralAccess": True,
})
self._context_id = result["executionContextId"]
return self._context_id
async def evaluate(self, expression: str) -> Any:
"""Evaluate JS in isolated world. Auto-recreates on stale context."""
if self._context_id is None:
try:
await self._create_world()
except Exception:
logger.debug("stealth_evaluate: failed to create isolated world")
return None
for attempt in range(2):
try:
result = await self._cdp.send("Runtime.evaluate", {
"expression": expression,
"contextId": self._context_id,
"returnByValue": True,
})
if "exceptionDetails" in result:
if attempt == 0:
await self._create_world()
continue
logger.debug("stealth_evaluate: JS exception: %s",
result["exceptionDetails"].get("text", "unknown"))
return None
return result.get("result", {}).get("value")
except Exception:
if attempt == 0:
self._context_id = None
try:
await self._create_world()
except Exception:
logger.debug("stealth_evaluate: failed to recreate isolated world")
return None
continue
logger.debug("stealth_evaluate: CDP evaluate failed after retry")
return None
return None
def invalidate(self) -> None:
"""Mark context as stale — call after navigation."""
self._context_id = None
async def get_cdp_session(self) -> Any:
"""Get the underlying CDP session (reused for Input.dispatchKeyEvent)."""
return await self._ensure_cdp()
# ============================================================================
# Page / context / browser patching
# ============================================================================
def _patch_page_sync(page: Any) -> None:
"""Attach page.stealth_evaluate() using a sync isolated world."""
if hasattr(page, "stealth_evaluate"):
return
existing = getattr(page, "_stealth_world", None)
if isinstance(existing, _SyncIsolatedWorld):
world = existing
else:
world = _SyncIsolatedWorld(page)
page._stealth_world = world
page.stealth_evaluate = world.evaluate
def _patch_page_async(page: Any) -> None:
"""Attach page.stealth_evaluate() using an async isolated world."""
if hasattr(page, "stealth_evaluate"):
return
existing = getattr(page, "_stealth_world", None)
if isinstance(existing, _AsyncIsolatedWorld):
world = existing
else:
world = _AsyncIsolatedWorld(page)
page._stealth_world = world
page.stealth_evaluate = world.evaluate
def patch_context_stealth_eval(context: Any, *, is_async: bool = False) -> None:
"""Patch existing pages + hook new_page() for stealth_evaluate."""
if getattr(context, "_stealth_eval_patched", False):
return
context._stealth_eval_patched = True
patch_fn = _patch_page_async if is_async else _patch_page_sync
for p in context.pages:
patch_fn(p)
orig_new_page = context.new_page
if is_async:
async def _patched_new_page(*args: Any, **kwargs: Any) -> Any:
page = await orig_new_page(*args, **kwargs)
patch_fn(page)
return page
else:
def _patched_new_page(*args: Any, **kwargs: Any) -> Any:
page = orig_new_page(*args, **kwargs)
patch_fn(page)
return page
context.new_page = _patched_new_page
context.on("page", lambda p: patch_fn(p))
def patch_browser_stealth_eval(browser: Any, *, is_async: bool = False) -> None:
"""Patch browser factory methods for stealth_evaluate."""
patch_fn = _patch_page_async if is_async else _patch_page_sync
# Hook new_context()
orig_new_context = browser.new_context
if is_async:
async def _patched_new_context(*args: Any, **kwargs: Any) -> Any:
ctx = await orig_new_context(*args, **kwargs)
patch_context_stealth_eval(ctx, is_async=True)
return ctx
else:
def _patched_new_context(*args: Any, **kwargs: Any) -> Any:
ctx = orig_new_context(*args, **kwargs)
patch_context_stealth_eval(ctx, is_async=False)
return ctx
browser.new_context = _patched_new_context
# Hook new_page()
orig_new_page = browser.new_page
if is_async:
async def _patched_new_page(*args: Any, **kwargs: Any) -> Any:
page = await orig_new_page(*args, **kwargs)
patch_context_stealth_eval(page.context, is_async=True)
patch_fn(page)
return page
else:
def _patched_new_page(*args: Any, **kwargs: Any) -> Any:
page = orig_new_page(*args, **kwargs)
patch_context_stealth_eval(page.context, is_async=False)
patch_fn(page)
return page
browser.new_page = _patched_new_page
+3 -21
View File
@@ -177,24 +177,6 @@ if (newVersion) console.log(`Updated to ${newVersion}`);
| TLS fingerprint | Mismatch | **Identical to Chrome** | | TLS fingerprint | Mismatch | **Identical to Chrome** |
| | | **Tested against 30+ detection sites** | | | | **Tested against 30+ detection sites** |
## Stealth Evaluate
`page.stealthEvaluate(expression)` runs JavaScript in a CDP isolated world instead of Playwright's main-world `evaluate()`. This produces clean `Error.stack` traces and full variable isolation from page JS.
```typescript
const browser = await launch();
const page = await browser.newPage();
await page.goto('https://example.com');
// Stealth — clean stack trace, invisible to page JS
const title = await page.stealthEvaluate('document.title');
// Regular evaluate — unchanged, use for DOM writes
await page.evaluate(() => { document.body.style.display = 'none'; });
```
Always available on every page — no flag needed. Returns JSON-serializable values only. The isolated world context auto-recreates after navigation.
## Configuration ## Configuration
| Env Variable | Default | Description | | Env Variable | Default | Description |
@@ -221,11 +203,11 @@ const page = await browser.newPage();
| Platform | Chromium | Patches | Status | | Platform | Chromium | Patches | Status |
|---|---|---|---| |---|---|---|---|
| Linux x86_64 | 145 | 33 | ✅ Latest | | Linux x86_64 | 145 | 48 | ✅ Latest |
| Linux arm64 (RPi, Graviton) | 145 | 33 | ✅ Latest | | Linux arm64 (RPi, Graviton) | 145 | 48 | ✅ Latest |
| macOS arm64 (Apple Silicon) | 145 | 26 | ✅ Latest | | macOS arm64 (Apple Silicon) | 145 | 26 | ✅ Latest |
| macOS x86_64 (Intel) | 145 | 26 | ✅ Latest | | macOS x86_64 (Intel) | 145 | 26 | ✅ Latest |
| Windows x86_64 | 145 | 33 | ✅ Latest | | Windows x86_64 | 145 | 48 | ✅ Latest |
## Requirements ## Requirements
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "cloakbrowser", "name": "cloakbrowser",
"version": "0.3.20", "version": "0.3.22",
"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",
+5 -16
View File
@@ -27,10 +27,10 @@ export { WRAPPER_VERSION };
// 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.159.9"; export const CHROMIUM_VERSION = "146.0.7680.177.1";
export const PLATFORM_CHROMIUM_VERSIONS: Record<string, string> = { export const PLATFORM_CHROMIUM_VERSIONS: Record<string, string> = {
"linux-x64": "145.0.7632.159.9", "linux-x64": "146.0.7680.177.1",
"linux-arm64": "145.0.7632.159.7", "linux-arm64": "145.0.7632.159.7",
"darwin-arm64": "145.0.7632.109.2", "darwin-arm64": "145.0.7632.109.2",
"darwin-x64": "145.0.7632.109.2", "darwin-x64": "145.0.7632.109.2",
@@ -211,27 +211,16 @@ export function getDefaultStealthArgs(): string[] {
const base = [ const base = [
"--no-sandbox", "--no-sandbox",
"--disable-blink-features=AutomationControlled",
`--fingerprint=${seed}`, `--fingerprint=${seed}`,
]; ];
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 [ return [...base, "--fingerprint-platform=macos"];
...base,
"--fingerprint-platform=macos",
"--fingerprint-gpu-vendor=Google Inc. (Apple)",
"--fingerprint-gpu-renderer=ANGLE (Apple, ANGLE Metal Renderer: Apple M3, Unspecified Version)",
];
} }
// Linux/Windows: spoof as Windows desktop // Linux/Windows: spoof as Windows desktop
// Hardware concurrency, device memory, screen, and window size are // Hardware concurrency, device memory, screen, window size, and GPU are
// auto-generated by the binary from the seed (v14+). // auto-generated by the binary from the seed (v14+).
return [ return [...base, "--fingerprint-platform=windows"];
...base,
"--fingerprint-platform=windows",
"--fingerprint-gpu-vendor=Google Inc. (NVIDIA)",
"--fingerprint-gpu-renderer=ANGLE (NVIDIA, NVIDIA GeForce RTX 3070 (0x00002484) Direct3D11 vs_5_0 ps_5_0, D3D11)",
];
} }
+101 -9
View File
@@ -19,7 +19,6 @@ import { HumanConfig, resolveConfig, rand, randRange, sleep } from './config.js'
import { RawMouse, RawKeyboard, humanMove, humanClick, clickTarget, humanIdle } from './mouse.js'; import { RawMouse, RawKeyboard, humanMove, humanClick, clickTarget, humanIdle } from './mouse.js';
import { humanType } from './keyboard.js'; import { humanType } from './keyboard.js';
import { scrollToElement } from './scroll.js'; import { scrollToElement } from './scroll.js';
import { StealthEval } from '../stealth-eval.js';
export { HumanConfig, resolveConfig } from './config.js'; export { HumanConfig, resolveConfig } from './config.js';
export { humanMove, humanClick, clickTarget, humanIdle } from './mouse.js'; export { humanMove, humanClick, clickTarget, humanIdle } from './mouse.js';
@@ -30,7 +29,102 @@ export { scrollToElement } from './scroll.js';
const SELECT_ALL = process.platform === 'darwin' ? 'Meta+a' : 'Control+a'; const SELECT_ALL = process.platform === 'darwin' ? 'Meta+a' : 'Control+a';
// StealthEval is defined in stealth-eval.ts and imported at the top of this file. // ============================================================================
// CDP Isolated World — stealth DOM evaluation
// ============================================================================
/**
* Manages a CDP isolated execution context for DOM reads.
* Produces clean Error.stack traces (no 'eval at evaluate :302:')
* and is invisible to querySelector monkey-patches in the main world.
*
* Context ID is invalidated on navigation and auto-recreated on next call.
*/
class StealthEval {
private cdp: CDPSession | null = null;
private contextId: number | null = null;
private page: Page;
constructor(page: Page) {
this.page = page;
}
private async ensureCdp(): Promise<CDPSession> {
if (!this.cdp) {
this.cdp = await this.page.context().newCDPSession(this.page);
}
return this.cdp;
}
private async createWorld(): Promise<number> {
const cdp = await this.ensureCdp();
const tree = await cdp.send('Page.getFrameTree');
const frameId = tree.frameTree.frame.id;
const result = await cdp.send('Page.createIsolatedWorld', {
frameId,
worldName: '',
grantUniveralAccess: true,
});
const ctxId = result.executionContextId;
this.contextId = ctxId;
return ctxId;
}
/**
* Evaluate a JS expression in the isolated world.
* Auto-recreates the world if the context was invalidated (navigation).
* Returns the result value, or undefined on failure.
*/
async evaluate(expression: string): Promise<any> {
if (this.contextId === null) {
await this.createWorld();
}
for (let attempt = 0; attempt < 2; attempt++) {
try {
const cdp = await this.ensureCdp();
const result = await cdp.send('Runtime.evaluate', {
expression,
contextId: this.contextId!,
returnByValue: true,
});
if (result.exceptionDetails) {
// Context was likely invalidated by navigation
if (attempt === 0) {
await this.createWorld();
continue;
}
return undefined;
}
return result.result?.value;
} catch {
if (attempt === 0) {
this.contextId = null;
try {
await this.createWorld();
} catch {
return undefined;
}
continue;
}
return undefined;
}
}
return undefined;
}
/** Mark context as stale — call after navigation. */
invalidate(): void {
this.contextId = null;
}
/** Get the underlying CDP session (reused for Input.dispatchKeyEvent etc.). */
async getCdpSession(): Promise<CDPSession> {
return this.ensureCdp();
}
}
// ============================================================================ // ============================================================================
@@ -69,8 +163,7 @@ async function isInputElement(
|| el.getAttribute('contenteditable') === 'true'; || el.getAttribute('contenteditable') === 'true';
})() })()
`); `);
if (result !== undefined && result !== null) return !!result; return !!result;
// undefined/null = CDP failed, fall through to page.evaluate
} catch { } catch {
// Fall through to page.evaluate // Fall through to page.evaluate
} }
@@ -104,8 +197,7 @@ async function isSelectorFocused(
return el === document.activeElement; return el === document.activeElement;
})() })()
`); `);
if (result !== undefined && result !== null) return !!result; return !!result;
// undefined/null = CDP failed, fall through to page.evaluate
} catch { } catch {
// Fall through to page.evaluate // Fall through to page.evaluate
} }
@@ -154,9 +246,9 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
(page as any)._original = originals; (page as any)._original = originals;
(page as any)._humanCfg = cfg; (page as any)._humanCfg = cfg;
// --- Stealth infrastructure (reuse if already attached by stealth-eval) --- // --- Stealth infrastructure ---
const stealth = (page as any)._stealthWorld ?? new StealthEval(page); const stealth = new StealthEval(page);
(page as any)._stealthWorld = stealth; (page as any)._stealth = stealth;
// CDP session for shift symbol typing (lazy-initialized, reuses stealth's session) // CDP session for shift symbol typing (lazy-initialized, reuses stealth's session)
let cdpSession: CDPSession | null = null; let cdpSession: CDPSession | null = null;
+2 -14
View File
@@ -67,10 +67,6 @@ export async function launch(options: LaunchOptions = {}): Promise<Browser> {
patchBrowser(browser, cfg); patchBrowser(browser, cfg);
} }
// Stealth evaluate — always attached
const { patchBrowser: patchStealthEval } = await import('./stealth-eval.js');
patchStealthEval(browser);
return browser; return browser;
} }
@@ -110,7 +106,7 @@ export async function launchContext(
try { try {
context = await browser.newContext({ context = await browser.newContext({
...(options.userAgent ? { userAgent: options.userAgent } : {}), ...(options.userAgent ? { userAgent: options.userAgent } : {}),
viewport: options.viewport ?? DEFAULT_VIEWPORT, viewport: options.viewport === undefined ? DEFAULT_VIEWPORT : options.viewport,
...(options.colorScheme ? { colorScheme: options.colorScheme } : {}), ...(options.colorScheme ? { colorScheme: options.colorScheme } : {}),
}); });
} catch (err) { } catch (err) {
@@ -136,10 +132,6 @@ export async function launchContext(
patchContext(context, cfg); patchContext(context, cfg);
} }
// Stealth evaluate — always attached
const { patchContext: patchStealthEvalCtx } = await import('./stealth-eval.js');
patchStealthEvalCtx(context);
return context; return context;
} }
@@ -189,7 +181,7 @@ export async function launchPersistentContext(
? { proxy: typeof options.proxy === "string" ? parseProxyUrl(options.proxy) : options.proxy } ? { proxy: typeof options.proxy === "string" ? parseProxyUrl(options.proxy) : options.proxy }
: {}), : {}),
...(options.userAgent ? { userAgent: options.userAgent } : {}), ...(options.userAgent ? { userAgent: options.userAgent } : {}),
viewport: options.viewport ?? DEFAULT_VIEWPORT, viewport: options.viewport === undefined ? DEFAULT_VIEWPORT : options.viewport,
...(options.colorScheme ? { colorScheme: options.colorScheme } : {}), ...(options.colorScheme ? { colorScheme: options.colorScheme } : {}),
...options.launchOptions, ...options.launchOptions,
}); });
@@ -205,10 +197,6 @@ export async function launchPersistentContext(
patchContext(context, cfg); patchContext(context, cfg);
} }
// Stealth evaluate — always attached
const { patchContext: patchStealthEvalCtx2 } = await import('./stealth-eval.js');
patchStealthEvalCtx2(context);
return context; return context;
} }
-161
View File
@@ -1,161 +0,0 @@
/**
* Stealth evaluate run JS in a CDP isolated world.
*
* Provides page.stealthEvaluate(expression) on every page returned by
* cloakbrowser launch functions. Produces clean Error.stack traces (no
* `eval at evaluate :302:` leak) and full variable isolation from main
* world JS. Context auto-recreates after navigation.
*
* The same StealthEval instances are reused by the humanize layer
* (human/index.ts) for stealth DOM queries.
*/
import type { Browser, BrowserContext, Page, CDPSession } from 'playwright-core';
// ============================================================================
// Isolated world class
// ============================================================================
/**
* Manages a CDP isolated execution context for DOM reads.
* Produces clean Error.stack traces (no 'eval at evaluate :302:')
* and is invisible to querySelector monkey-patches in the main world.
*
* Context ID is invalidated on navigation and auto-recreated on next call.
*/
export class StealthEval {
private cdp: CDPSession | null = null;
private contextId: number | null = null;
private page: Page;
constructor(page: Page) {
this.page = page;
}
private async ensureCdp(): Promise<CDPSession> {
if (!this.cdp) {
this.cdp = await this.page.context().newCDPSession(this.page);
}
return this.cdp;
}
private async createWorld(): Promise<number> {
const cdp = await this.ensureCdp();
const tree = await cdp.send('Page.getFrameTree');
const frameId = tree.frameTree.frame.id;
const result = await cdp.send('Page.createIsolatedWorld', {
frameId,
worldName: '',
grantUniveralAccess: true,
});
const ctxId = result.executionContextId;
this.contextId = ctxId;
return ctxId;
}
/**
* Evaluate a JS expression in the isolated world.
* Auto-recreates the world if the context was invalidated (navigation).
* Returns the result value, or undefined on failure.
*/
async evaluate(expression: string): Promise<any> {
if (this.contextId === null) {
try {
await this.createWorld();
} catch {
return undefined;
}
}
for (let attempt = 0; attempt < 2; attempt++) {
try {
const cdp = await this.ensureCdp();
const result = await cdp.send('Runtime.evaluate', {
expression,
contextId: this.contextId!,
returnByValue: true,
});
if (result.exceptionDetails) {
if (attempt === 0) {
await this.createWorld();
continue;
}
return undefined;
}
return result.result?.value;
} catch {
if (attempt === 0) {
this.contextId = null;
try {
await this.createWorld();
} catch {
return undefined;
}
continue;
}
return undefined;
}
}
return undefined;
}
/** Mark context as stale — call after navigation. */
invalidate(): void {
this.contextId = null;
}
/** Get the underlying CDP session (reused for Input.dispatchKeyEvent etc.). */
async getCdpSession(): Promise<CDPSession> {
return this.ensureCdp();
}
}
// ============================================================================
// Page / context / browser patching
// ============================================================================
function patchPage(page: Page): void {
if ((page as any).stealthEvaluate) return;
const existing = (page as any)._stealthWorld;
const stealth = existing instanceof StealthEval ? existing : new StealthEval(page);
(page as any)._stealthWorld = stealth;
(page as any).stealthEvaluate = stealth.evaluate.bind(stealth);
}
export function patchContext(context: BrowserContext): void {
if ((context as any)._stealthEvalPatched) return;
(context as any)._stealthEvalPatched = true;
for (const p of context.pages()) {
patchPage(p);
}
const origNewPage = context.newPage.bind(context);
context.newPage = async (...args: Parameters<BrowserContext['newPage']>) => {
const page = await origNewPage(...args);
patchPage(page);
return page;
};
context.on('page', (page: Page) => patchPage(page));
}
export function patchBrowser(browser: Browser): void {
const origNewContext = browser.newContext.bind(browser);
browser.newContext = async (...args: Parameters<Browser['newContext']>) => {
const ctx = await origNewContext(...args);
patchContext(ctx);
return ctx;
};
const origNewPage = browser.newPage.bind(browser);
browser.newPage = async (...args: Parameters<Browser['newPage']>) => {
const page = await origNewPage(...args);
patchContext(page.context());
patchPage(page);
return page;
};
}
+1 -8
View File
@@ -2,13 +2,6 @@
* Shared types for cloakbrowser launch wrappers. * Shared types for cloakbrowser launch wrappers.
*/ */
declare module 'playwright-core' {
interface Page {
/** Evaluate JS in a CDP isolated world — clean stack traces, invisible to main-world monkey-patches. */
stealthEvaluate(expression: string): Promise<any>;
}
}
export interface LaunchOptions { export interface LaunchOptions {
/** Run in headless mode (default: true). */ /** Run in headless mode (default: true). */
headless?: boolean; headless?: boolean;
@@ -43,7 +36,7 @@ export interface LaunchContextOptions extends LaunchOptions {
/** Custom user agent string. */ /** Custom user agent string. */
userAgent?: string; userAgent?: string;
/** Viewport size. */ /** Viewport size. */
viewport?: { width: number; height: number }; viewport?: { width: number; height: number } | null;
/** Browser locale, e.g. "en-US". */ /** Browser locale, e.g. "en-US". */
locale?: string; locale?: string;
/** IANA timezone — alias for `timezone`. Either works. */ /** IANA timezone — alias for `timezone`. Either works. */
+4 -3
View File
@@ -21,16 +21,17 @@ describe("config", () => {
const isMac = process.platform === "darwin"; const isMac = process.platform === "darwin";
expect(args).toContain("--no-sandbox"); expect(args).toContain("--no-sandbox");
expect(args).toContain("--disable-blink-features=AutomationControlled");
if (isMac) { if (isMac) {
expect(args).toContain("--fingerprint-platform=macos"); expect(args).toContain("--fingerprint-platform=macos");
// macOS: no hardware-concurrency or GPU spoofing (uses native values)
expect(args.some((a) => a.includes("hardware-concurrency"))).toBe(false);
} else { } else {
expect(args).toContain("--fingerprint-platform=windows"); expect(args).toContain("--fingerprint-platform=windows");
} }
// GPU flags removed — binary auto-generates from seed + platform
expect(args.some((a) => a.includes("fingerprint-gpu-vendor"))).toBe(false);
expect(args.some((a) => a.includes("fingerprint-gpu-renderer"))).toBe(false);
// Should have a random fingerprint seed // Should have a random fingerprint seed
const fingerprintArg = args.find((a) => a.startsWith("--fingerprint=")); const fingerprintArg = args.find((a) => a.startsWith("--fingerprint="));
expect(fingerprintArg).toBeDefined(); expect(fingerprintArg).toBeDefined();
+8 -89
View File
@@ -46,17 +46,14 @@ describe("launchContext (unit)", () => {
let mockContext: any; let mockContext: any;
let mockBrowser: any; let mockBrowser: any;
let mockChromium: any; let mockChromium: any;
let origNewContext: any;
const origEnv = process.env.CLOAKBROWSER_BINARY_PATH; const origEnv = process.env.CLOAKBROWSER_BINARY_PATH;
beforeEach(() => { beforeEach(() => {
process.env.CLOAKBROWSER_BINARY_PATH = "/fake/chrome"; process.env.CLOAKBROWSER_BINARY_PATH = "/fake/chrome";
const origClose = vi.fn(); const origClose = vi.fn();
mockContext = { close: origClose, _origClose: origClose, newPage: vi.fn(), on: vi.fn(), pages: vi.fn().mockReturnValue([]) }; mockContext = { close: origClose, _origClose: origClose };
origNewContext = vi.fn().mockResolvedValue(mockContext);
mockBrowser = { mockBrowser = {
newContext: origNewContext, newContext: vi.fn().mockResolvedValue(mockContext),
newPage: vi.fn(),
close: vi.fn(), close: vi.fn(),
}; };
mockChromium = { launch: vi.fn().mockResolvedValue(mockBrowser) }; mockChromium = { launch: vi.fn().mockResolvedValue(mockBrowser) };
@@ -78,7 +75,7 @@ describe("launchContext (unit)", () => {
const { launchContext } = await import("../src/playwright.js"); const { launchContext } = await import("../src/playwright.js");
await launchContext(); await launchContext();
const ctxArgs = origNewContext.mock.calls[0][0]; const ctxArgs = mockBrowser.newContext.mock.calls[0][0];
expect(ctxArgs.viewport).toEqual(DEFAULT_VIEWPORT); expect(ctxArgs.viewport).toEqual(DEFAULT_VIEWPORT);
}); });
@@ -87,7 +84,7 @@ describe("launchContext (unit)", () => {
const custom = { width: 1280, height: 720 }; const custom = { width: 1280, height: 720 };
await launchContext({ viewport: custom }); await launchContext({ viewport: custom });
const ctxArgs = origNewContext.mock.calls[0][0]; const ctxArgs = mockBrowser.newContext.mock.calls[0][0];
expect(ctxArgs.viewport).toEqual(custom); expect(ctxArgs.viewport).toEqual(custom);
}); });
@@ -95,7 +92,7 @@ describe("launchContext (unit)", () => {
const { launchContext } = await import("../src/playwright.js"); const { launchContext } = await import("../src/playwright.js");
await launchContext({ userAgent: "Custom/1.0" }); await launchContext({ userAgent: "Custom/1.0" });
const ctxArgs = origNewContext.mock.calls[0][0]; const ctxArgs = mockBrowser.newContext.mock.calls[0][0];
expect(ctxArgs.userAgent).toBe("Custom/1.0"); expect(ctxArgs.userAgent).toBe("Custom/1.0");
}); });
@@ -111,7 +108,7 @@ describe("launchContext (unit)", () => {
expect(hasTimezoneFlag).toBe(true); expect(hasTimezoneFlag).toBe(true);
// NOT in newContext() — no CDP emulation // NOT in newContext() — no CDP emulation
const ctxArgs = origNewContext.mock.calls[0][0]; const ctxArgs = mockBrowser.newContext.mock.calls[0][0];
expect(ctxArgs.timezoneId).toBeUndefined(); expect(ctxArgs.timezoneId).toBeUndefined();
}); });
@@ -119,7 +116,7 @@ describe("launchContext (unit)", () => {
const { launchContext } = await import("../src/playwright.js"); const { launchContext } = await import("../src/playwright.js");
await launchContext({ colorScheme: "dark" }); await launchContext({ colorScheme: "dark" });
const ctxArgs = origNewContext.mock.calls[0][0]; const ctxArgs = mockBrowser.newContext.mock.calls[0][0];
expect(ctxArgs.colorScheme).toBe("dark"); expect(ctxArgs.colorScheme).toBe("dark");
}); });
@@ -135,84 +132,6 @@ describe("launchContext (unit)", () => {
}); });
}); });
// ---------------------------------------------------------------------------
// stealth_evaluate patching unit tests
// ---------------------------------------------------------------------------
describe("stealthEvaluate patching (unit)", () => {
const origEnv = process.env.CLOAKBROWSER_BINARY_PATH;
let mockPage: any;
let mockContext: any;
let mockBrowser: any;
beforeEach(() => {
process.env.CLOAKBROWSER_BINARY_PATH = "/fake/chrome";
// Page mock with context() returning the implicit context
mockPage = {
context: vi.fn(),
};
// Implicit context created by browser.newPage()
mockContext = {
pages: vi.fn().mockReturnValue([]),
newPage: vi.fn(),
on: vi.fn(),
};
mockPage.context.mockReturnValue(mockContext);
mockBrowser = {
newContext: vi.fn().mockResolvedValue(mockContext),
newPage: vi.fn().mockResolvedValue(mockPage),
close: vi.fn(),
};
const mockChromium = { launch: vi.fn().mockResolvedValue(mockBrowser) };
vi.doMock("playwright-core", () => ({ chromium: mockChromium }));
});
afterEach(() => {
vi.restoreAllMocks();
vi.resetModules();
if (origEnv) {
process.env.CLOAKBROWSER_BINARY_PATH = origEnv;
} else {
delete process.env.CLOAKBROWSER_BINARY_PATH;
}
});
it("page.stealthEvaluate exists after launch + browser.newPage", async () => {
const { launch } = await import("../src/playwright.js");
const browser = await launch({ headless: true });
const page = await browser.newPage();
expect(typeof (page as any).stealthEvaluate).toBe("function");
});
it("implicit context from browser.newPage is patched for future pages", async () => {
const { launch } = await import("../src/playwright.js");
const browser = await launch({ headless: true });
await browser.newPage();
// The 'page' event listener should be registered on the implicit context
expect(mockContext.on).toHaveBeenCalledWith("page", expect.any(Function));
// The context should be marked as patched
expect((mockContext as any)._stealthEvalPatched).toBe(true);
});
it("context from browser.newContext patches pages with stealthEvaluate", async () => {
const { launch } = await import("../src/playwright.js");
const browser = await launch({ headless: true });
const mockPage2: any = { context: vi.fn().mockReturnValue(mockContext) };
mockContext.newPage.mockResolvedValue(mockPage2);
mockContext.pages.mockReturnValue([]);
const ctx = await browser.newContext();
const page = await ctx.newPage();
expect(typeof (page as any).stealthEvaluate).toBe("function");
});
});
describe("launchPersistentContext (unit)", () => { describe("launchPersistentContext (unit)", () => {
let mockContext: any; let mockContext: any;
let mockChromium: any; let mockChromium: any;
@@ -220,7 +139,7 @@ describe("launchPersistentContext (unit)", () => {
beforeEach(() => { beforeEach(() => {
process.env.CLOAKBROWSER_BINARY_PATH = "/fake/chrome"; process.env.CLOAKBROWSER_BINARY_PATH = "/fake/chrome";
mockContext = { close: vi.fn(), pages: vi.fn().mockReturnValue([]), newPage: vi.fn(), on: vi.fn() }; mockContext = { close: vi.fn(), pages: vi.fn().mockReturnValue([]) };
mockChromium = { mockChromium = {
launchPersistentContext: vi.fn().mockResolvedValue(mockContext), launchPersistentContext: vi.fn().mockResolvedValue(mockContext),
}; };
+12 -12
View File
@@ -470,7 +470,7 @@ describe("humanType mixed text with CDP", () => {
// patchPage stealth wiring // patchPage stealth wiring
// ========================================================================= // =========================================================================
describe("patchPage stealth infrastructure", () => { describe("patchPage stealth infrastructure", () => {
it("page._stealthWorld is a StealthEval instance after patching", async () => { it("page._stealth is a StealthEval instance after patching", async () => {
const { patchPage } = await import("../src/human/index.js"); const { patchPage } = await import("../src/human/index.js");
const page = buildMockPage(); const page = buildMockPage();
@@ -478,10 +478,10 @@ describe("patchPage stealth infrastructure", () => {
const cursor = { x: 0, y: 0, initialized: false }; const cursor = { x: 0, y: 0, initialized: false };
patchPage(page as any, cfg, cursor as any); patchPage(page as any, cfg, cursor as any);
expect((page as any)._stealthWorld).toBeDefined(); expect((page as any)._stealth).toBeDefined();
expect(typeof (page as any)._stealthWorld.evaluate).toBe("function"); expect(typeof (page as any)._stealth.evaluate).toBe("function");
expect(typeof (page as any)._stealthWorld.invalidate).toBe("function"); expect(typeof (page as any)._stealth.invalidate).toBe("function");
expect(typeof (page as any)._stealthWorld.getCdpSession).toBe("function"); expect(typeof (page as any)._stealth.getCdpSession).toBe("function");
}); });
it("page._original and page._humanCfg are set", async () => { it("page._original and page._humanCfg are set", async () => {
@@ -504,7 +504,7 @@ describe("patchPage stealth infrastructure", () => {
const cursor = { x: 0, y: 0, initialized: false }; const cursor = { x: 0, y: 0, initialized: false };
patchPage(page as any, cfg, cursor as any); patchPage(page as any, cfg, cursor as any);
const stealth = (page as any)._stealthWorld; const stealth = (page as any)._stealth;
const invalidateSpy = vi.spyOn(stealth, "invalidate"); const invalidateSpy = vi.spyOn(stealth, "invalidate");
await page.goto("https://example.com"); await page.goto("https://example.com");
@@ -540,7 +540,7 @@ describe("StealthEval lifecycle", () => {
const cursor = { x: 0, y: 0, initialized: false }; const cursor = { x: 0, y: 0, initialized: false };
patchPage(page as any, cfg, cursor as any); patchPage(page as any, cfg, cursor as any);
const stealth = (page as any)._stealthWorld; const stealth = (page as any)._stealth;
expect(() => stealth.invalidate()).not.toThrow(); expect(() => stealth.invalidate()).not.toThrow();
}); });
@@ -552,7 +552,7 @@ describe("StealthEval lifecycle", () => {
const cursor = { x: 0, y: 0, initialized: false }; const cursor = { x: 0, y: 0, initialized: false };
patchPage(page as any, cfg, cursor as any); patchPage(page as any, cfg, cursor as any);
const stealth = (page as any)._stealthWorld; const stealth = (page as any)._stealth;
const session = await stealth.getCdpSession(); const session = await stealth.getCdpSession();
expect(session).toBeDefined(); expect(session).toBeDefined();
expect(typeof session.send).toBe("function"); expect(typeof session.send).toBe("function");
@@ -587,7 +587,7 @@ describe("StealthEval lifecycle", () => {
const cursor = { x: 0, y: 0, initialized: false }; const cursor = { x: 0, y: 0, initialized: false };
patchPage(page as any, cfg, cursor as any); patchPage(page as any, cfg, cursor as any);
const stealth = (page as any)._stealthWorld; const stealth = (page as any)._stealth;
const result = await stealth.evaluate("1 + 1"); const result = await stealth.evaluate("1 + 1");
expect(result).toBe(true); expect(result).toBe(true);
}); });
@@ -626,7 +626,7 @@ describe("StealthEval lifecycle", () => {
const cursor = { x: 0, y: 0, initialized: false }; const cursor = { x: 0, y: 0, initialized: false };
patchPage(page as any, cfg, cursor as any); patchPage(page as any, cfg, cursor as any);
const stealth = (page as any)._stealthWorld; const stealth = (page as any)._stealth;
const result = await stealth.evaluate("test"); const result = await stealth.evaluate("test");
expect(result).toBe("recovered"); expect(result).toBe("recovered");
}); });
@@ -660,7 +660,7 @@ describe("StealthEval lifecycle", () => {
const cursor = { x: 0, y: 0, initialized: false }; const cursor = { x: 0, y: 0, initialized: false };
patchPage(page as any, cfg, cursor as any); patchPage(page as any, cfg, cursor as any);
const stealth = (page as any)._stealthWorld; const stealth = (page as any)._stealth;
const result = await stealth.evaluate("broken"); const result = await stealth.evaluate("broken");
expect(result).toBeUndefined(); expect(result).toBeUndefined();
}); });
@@ -975,7 +975,7 @@ describeIfSlow("stealth browser: navigation invalidation", () => {
const browser = await launch({ headless: true, args: ['--humanize'] }); const browser = await launch({ headless: true, args: ['--humanize'] });
const page = await browser.newPage(); const page = await browser.newPage();
expect((page as any)._stealthWorld).toBeDefined(); expect((page as any)._stealth).toBeDefined();
await page.goto('https://www.wikipedia.org', { waitUntil: 'domcontentloaded' }); await page.goto('https://www.wikipedia.org', { waitUntil: 'domcontentloaded' });
await sleep(1000); await sleep(1000);
+6 -2
View File
@@ -133,10 +133,14 @@ class TestStealthArgs:
with patch("cloakbrowser.config.platform.system", return_value="Darwin"): with patch("cloakbrowser.config.platform.system", return_value="Darwin"):
args = get_default_stealth_args() args = get_default_stealth_args()
assert "--fingerprint-platform=macos" in args assert "--fingerprint-platform=macos" in args
assert any("Apple" in a for a in args) # GPU flags removed — binary auto-generates from seed + platform
assert not any("fingerprint-gpu-vendor" in a for a in args)
assert not any("fingerprint-gpu-renderer" in a for a in args)
def test_linux_windows_profile(self): def test_linux_windows_profile(self):
with patch("cloakbrowser.config.platform.system", return_value="Linux"): with patch("cloakbrowser.config.platform.system", return_value="Linux"):
args = get_default_stealth_args() args = get_default_stealth_args()
assert "--fingerprint-platform=windows" in args assert "--fingerprint-platform=windows" in args
assert any("NVIDIA" in a for a in args) # GPU flags removed — binary auto-generates from seed + platform
assert not any("fingerprint-gpu-vendor" in a for a in args)
assert not any("fingerprint-gpu-renderer" in a for a in args)