mirror of
https://github.com/CloakHQ/CloakBrowser.git
synced 2026-06-23 11:41:46 +02:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a32d7c4eae |
@@ -85,8 +85,6 @@ 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
|
||||||
|
|||||||
@@ -67,8 +67,3 @@ 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
|
|
||||||
|
|||||||
@@ -6,19 +6,6 @@ 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
@@ -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,geoip]"
|
RUN pip install --no-cache-dir ".[serve]"
|
||||||
|
|
||||||
# JS wrapper
|
# JS wrapper
|
||||||
COPY js/ js/
|
COPY js/ js/
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
- **49 source-level C++ patches** — canvas, WebGL, audio, fonts, GPU, screen, WebRTC, network timing, automation signals, CDP input behavior
|
- **48 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,10 +128,9 @@ Open [http://localhost:8080](http://localhost:8080). Create a profile. Click **L
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Latest: v0.3.22 (Chromium 146.0.7680.177.1)
|
## Latest: v0.3.20 (Chromium 145.0.7632.159.9)
|
||||||
|
|
||||||
- **Chromium 146 upgrade** — rebased all patches from 145.0.7632.x to 146.0.7680.177
|
- **48 fingerprint patches** (Linux x64) — 6 new patches covering WebRTC IP spoofing, proxy signal removal, and network timing normalization
|
||||||
- **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
|
||||||
@@ -155,7 +154,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: Apr 2026 (Chromium 146).
|
All tests verified against live detection services. Last tested: Mar 2026 (Chromium 145).
|
||||||
|
|
||||||
| Detection Service | Stock Playwright | CloakBrowser | Notes |
|
| Detection Service | Stock Playwright | CloakBrowser | Notes |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
@@ -170,7 +169,7 @@ All tests verified against live detection services. Last tested: Apr 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/146.0.0.0`** | No headless leak |
|
| UA string | `HeadlessChrome` | **`Chrome/145.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** | |
|
||||||
@@ -219,11 +218,11 @@ All tests verified against live detection services. Last tested: Apr 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 146)
|
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 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.
|
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.
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
@@ -370,7 +369,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': '146.0.7680.177.1', 'platform': 'linux-x64', 'installed': True, ...}
|
# {'version': '145.0.7632.159.2', 'platform': 'linux-x64', 'installed': True, ...}
|
||||||
|
|
||||||
# Force re-download
|
# Force re-download
|
||||||
clear_cache()
|
clear_cache()
|
||||||
@@ -515,6 +514,33 @@ 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 |
|
||||||
@@ -555,10 +581,14 @@ Every `launch()` call sets these automatically. The **wrapper** applies platform
|
|||||||
|------|--------------|---------------|----------|
|
|------|--------------|---------------|----------|
|
||||||
| `--fingerprint` | Random (10000–99999) | Random (10000–99999) | Master seed for canvas, WebGL, audio, fonts, client rects |
|
| `--fingerprint` | Random (10000–99999) | Random (10000–99999) | 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 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.
|
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.
|
> **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
|
||||||
|
|
||||||
@@ -566,8 +596,6 @@ 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) |
|
||||||
@@ -597,9 +625,11 @@ 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 specific machine
|
# Override GPU to look like a different 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",
|
||||||
@@ -653,11 +683,11 @@ browser = await launch_async(args=["--remote-debugging-port=9242"])
|
|||||||
|
|
||||||
| Platform | Chromium | Patches | Status |
|
| Platform | Chromium | Patches | Status |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Linux x86_64 | 146 | 49 | ✅ Latest |
|
| Linux x86_64 | 145 | 42 | ✅ Latest |
|
||||||
| Linux arm64 (RPi, Graviton) | 145 | 48 | ✅ |
|
| Linux arm64 (RPi, Graviton) | 145 | 33 | ✅ |
|
||||||
| 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 | 48 | ✅ |
|
| Windows x86_64 | 145 | 33 | ✅ |
|
||||||
|
|
||||||
The wrapper auto-downloads the correct binary for your platform.
|
The wrapper auto-downloads the correct binary for your platform.
|
||||||
|
|
||||||
@@ -918,9 +948,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.21 # Python
|
pip install cloakbrowser==0.3.11 # Python
|
||||||
npm install cloakbrowser@0.3.21 # JavaScript
|
npm install cloakbrowser@0.3.11 # JavaScript
|
||||||
docker pull cloakhq/cloakbrowser:0.3.21 # Docker
|
docker pull cloakhq/cloakbrowser:0.3.11 # 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.
|
||||||
|
|
||||||
@@ -1016,7 +1046,7 @@ A: Yes. Pass `proxy="http://user:pass@host:port"` to `launch()`.
|
|||||||
|
|
||||||
| Feature | Status |
|
| Feature | Status |
|
||||||
|---------|--------|
|
|---------|--------|
|
||||||
| Linux x64 — Chromium 146 (49 patches) | ✅ Released |
|
| Linux x64 — Chromium 145 (48 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 |
|
||||||
@@ -1040,7 +1070,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-v146.0.7680.177.1
|
git verify-tag chromium-v145.0.7632.159.9
|
||||||
|
|
||||||
# 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
|
||||||
@@ -1065,4 +1095,3 @@ 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 @@
|
|||||||
__version__ = "0.3.22"
|
__version__ = "0.3.20"
|
||||||
|
|||||||
+26
-27
@@ -24,9 +24,6 @@ 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."""
|
||||||
@@ -141,6 +138,10 @@ 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
|
||||||
|
|
||||||
|
|
||||||
@@ -230,6 +231,10 @@ 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
|
||||||
|
|
||||||
|
|
||||||
@@ -240,7 +245,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 = _VIEWPORT_UNSET,
|
viewport: dict | None = None,
|
||||||
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,
|
||||||
@@ -267,7 +272,6 @@ 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'.
|
||||||
@@ -314,12 +318,7 @@ 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
|
||||||
if viewport is _VIEWPORT_UNSET:
|
context_kwargs["viewport"] = viewport or DEFAULT_VIEWPORT
|
||||||
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)
|
||||||
@@ -353,6 +352,10 @@ 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
|
||||||
|
|
||||||
|
|
||||||
@@ -363,7 +366,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 = _VIEWPORT_UNSET,
|
viewport: dict | None = None,
|
||||||
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,
|
||||||
@@ -389,7 +392,6 @@ 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'.
|
||||||
@@ -439,12 +441,7 @@ 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
|
||||||
if viewport is _VIEWPORT_UNSET:
|
context_kwargs["viewport"] = viewport or DEFAULT_VIEWPORT
|
||||||
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)
|
||||||
@@ -478,6 +475,10 @@ 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
|
||||||
|
|
||||||
|
|
||||||
@@ -487,7 +488,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 = _VIEWPORT_UNSET,
|
viewport: dict | None = None,
|
||||||
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,
|
||||||
@@ -510,7 +511,6 @@ 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,12 +543,7 @@ 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
|
||||||
if viewport is _VIEWPORT_UNSET:
|
context_kwargs["viewport"] = viewport or DEFAULT_VIEWPORT
|
||||||
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)
|
||||||
@@ -577,6 +572,10 @@ 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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+14
-5
@@ -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 = "146.0.7680.177.1"
|
CHROMIUM_VERSION = "145.0.7632.159.9"
|
||||||
|
|
||||||
PLATFORM_CHROMIUM_VERSIONS: dict[str, str] = {
|
PLATFORM_CHROMIUM_VERSIONS: dict[str, str] = {
|
||||||
"linux-x64": "146.0.7680.177.1",
|
"linux-x64": "145.0.7632.159.9",
|
||||||
"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,17 +48,26 @@ 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 + ["--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/Windows: Windows fingerprint profile
|
# Linux/Windows: Windows fingerprint profile
|
||||||
# Hardware concurrency, device memory, screen, window size, and GPU are
|
# Hardware concurrency, device memory, screen, and window size are
|
||||||
# auto-generated by the binary from the seed (v14+).
|
# auto-generated by the binary from the seed (v14+).
|
||||||
return base + ["--fingerprint-platform=windows"]
|
return 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)",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ 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"
|
||||||
|
|
||||||
@@ -44,142 +45,9 @@ 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).
|
|
||||||
|
|
||||||
Produces clean Error.stack traces (no 'eval at evaluate :302:')
|
# _SyncIsolatedWorld and _AsyncIsolatedWorld are defined in
|
||||||
and is invisible to querySelector monkey-patches in the main world.
|
# cloakbrowser.stealth_eval and imported at the top of this file.
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -739,8 +607,10 @@ 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 ---
|
# --- Stealth infrastructure (reuse if already attached by stealth_eval) ---
|
||||||
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()
|
||||||
@@ -1104,7 +974,9 @@ 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 (lazy-initialized, async) ---
|
# --- Stealth infrastructure (reuse if already attached by stealth_eval) ---
|
||||||
|
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
|
||||||
|
|||||||
@@ -0,0 +1,271 @@
|
|||||||
|
"""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
|
||||||
+21
-3
@@ -177,6 +177,24 @@ 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 |
|
||||||
@@ -203,11 +221,11 @@ const page = await browser.newPage();
|
|||||||
|
|
||||||
| Platform | Chromium | Patches | Status |
|
| Platform | Chromium | Patches | Status |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Linux x86_64 | 145 | 48 | ✅ Latest |
|
| Linux x86_64 | 145 | 33 | ✅ Latest |
|
||||||
| Linux arm64 (RPi, Graviton) | 145 | 48 | ✅ Latest |
|
| Linux arm64 (RPi, Graviton) | 145 | 33 | ✅ 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 | 48 | ✅ Latest |
|
| Windows x86_64 | 145 | 33 | ✅ Latest |
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cloakbrowser",
|
"name": "cloakbrowser",
|
||||||
"version": "0.3.22",
|
"version": "0.3.20",
|
||||||
"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",
|
||||||
|
|||||||
+16
-5
@@ -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 = "146.0.7680.177.1";
|
export const CHROMIUM_VERSION = "145.0.7632.159.9";
|
||||||
|
|
||||||
export const PLATFORM_CHROMIUM_VERSIONS: Record<string, string> = {
|
export const PLATFORM_CHROMIUM_VERSIONS: Record<string, string> = {
|
||||||
"linux-x64": "146.0.7680.177.1",
|
"linux-x64": "145.0.7632.159.9",
|
||||||
"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,16 +211,27 @@ 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 [...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/Windows: spoof as Windows desktop
|
// Linux/Windows: spoof as Windows desktop
|
||||||
// Hardware concurrency, device memory, screen, window size, and GPU are
|
// Hardware concurrency, device memory, screen, and window size are
|
||||||
// auto-generated by the binary from the seed (v14+).
|
// auto-generated by the binary from the seed (v14+).
|
||||||
return [...base, "--fingerprint-platform=windows"];
|
return [
|
||||||
|
...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)",
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-101
@@ -19,6 +19,7 @@ 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';
|
||||||
@@ -29,102 +30,7 @@ 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -163,7 +69,8 @@ async function isInputElement(
|
|||||||
|| el.getAttribute('contenteditable') === 'true';
|
|| el.getAttribute('contenteditable') === 'true';
|
||||||
})()
|
})()
|
||||||
`);
|
`);
|
||||||
return !!result;
|
if (result !== undefined && result !== null) return !!result;
|
||||||
|
// undefined/null = CDP failed, fall through to page.evaluate
|
||||||
} catch {
|
} catch {
|
||||||
// Fall through to page.evaluate
|
// Fall through to page.evaluate
|
||||||
}
|
}
|
||||||
@@ -197,7 +104,8 @@ async function isSelectorFocused(
|
|||||||
return el === document.activeElement;
|
return el === document.activeElement;
|
||||||
})()
|
})()
|
||||||
`);
|
`);
|
||||||
return !!result;
|
if (result !== undefined && result !== null) return !!result;
|
||||||
|
// undefined/null = CDP failed, fall through to page.evaluate
|
||||||
} catch {
|
} catch {
|
||||||
// Fall through to page.evaluate
|
// Fall through to page.evaluate
|
||||||
}
|
}
|
||||||
@@ -246,9 +154,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 ---
|
// --- Stealth infrastructure (reuse if already attached by stealth-eval) ---
|
||||||
const stealth = new StealthEval(page);
|
const stealth = (page as any)._stealthWorld ?? new StealthEval(page);
|
||||||
(page as any)._stealth = stealth;
|
(page as any)._stealthWorld = 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;
|
||||||
|
|||||||
+14
-2
@@ -67,6 +67,10 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,7 +110,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 === undefined ? DEFAULT_VIEWPORT : options.viewport,
|
viewport: options.viewport ?? DEFAULT_VIEWPORT,
|
||||||
...(options.colorScheme ? { colorScheme: options.colorScheme } : {}),
|
...(options.colorScheme ? { colorScheme: options.colorScheme } : {}),
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -132,6 +136,10 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +189,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 === undefined ? DEFAULT_VIEWPORT : options.viewport,
|
viewport: options.viewport ?? DEFAULT_VIEWPORT,
|
||||||
...(options.colorScheme ? { colorScheme: options.colorScheme } : {}),
|
...(options.colorScheme ? { colorScheme: options.colorScheme } : {}),
|
||||||
...options.launchOptions,
|
...options.launchOptions,
|
||||||
});
|
});
|
||||||
@@ -197,6 +205,10 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
};
|
||||||
|
}
|
||||||
+8
-1
@@ -2,6 +2,13 @@
|
|||||||
* 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;
|
||||||
@@ -36,7 +43,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 } | null;
|
viewport?: { width: number; height: number };
|
||||||
/** 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. */
|
||||||
|
|||||||
@@ -21,17 +21,16 @@ 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();
|
||||||
|
|||||||
+89
-8
@@ -46,14 +46,17 @@ 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 };
|
mockContext = { close: origClose, _origClose: origClose, newPage: vi.fn(), on: vi.fn(), pages: vi.fn().mockReturnValue([]) };
|
||||||
|
origNewContext = vi.fn().mockResolvedValue(mockContext);
|
||||||
mockBrowser = {
|
mockBrowser = {
|
||||||
newContext: vi.fn().mockResolvedValue(mockContext),
|
newContext: origNewContext,
|
||||||
|
newPage: vi.fn(),
|
||||||
close: vi.fn(),
|
close: vi.fn(),
|
||||||
};
|
};
|
||||||
mockChromium = { launch: vi.fn().mockResolvedValue(mockBrowser) };
|
mockChromium = { launch: vi.fn().mockResolvedValue(mockBrowser) };
|
||||||
@@ -75,7 +78,7 @@ describe("launchContext (unit)", () => {
|
|||||||
const { launchContext } = await import("../src/playwright.js");
|
const { launchContext } = await import("../src/playwright.js");
|
||||||
await launchContext();
|
await launchContext();
|
||||||
|
|
||||||
const ctxArgs = mockBrowser.newContext.mock.calls[0][0];
|
const ctxArgs = origNewContext.mock.calls[0][0];
|
||||||
expect(ctxArgs.viewport).toEqual(DEFAULT_VIEWPORT);
|
expect(ctxArgs.viewport).toEqual(DEFAULT_VIEWPORT);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -84,7 +87,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 = mockBrowser.newContext.mock.calls[0][0];
|
const ctxArgs = origNewContext.mock.calls[0][0];
|
||||||
expect(ctxArgs.viewport).toEqual(custom);
|
expect(ctxArgs.viewport).toEqual(custom);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -92,7 +95,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 = mockBrowser.newContext.mock.calls[0][0];
|
const ctxArgs = origNewContext.mock.calls[0][0];
|
||||||
expect(ctxArgs.userAgent).toBe("Custom/1.0");
|
expect(ctxArgs.userAgent).toBe("Custom/1.0");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -108,7 +111,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 = mockBrowser.newContext.mock.calls[0][0];
|
const ctxArgs = origNewContext.mock.calls[0][0];
|
||||||
expect(ctxArgs.timezoneId).toBeUndefined();
|
expect(ctxArgs.timezoneId).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -116,7 +119,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 = mockBrowser.newContext.mock.calls[0][0];
|
const ctxArgs = origNewContext.mock.calls[0][0];
|
||||||
expect(ctxArgs.colorScheme).toBe("dark");
|
expect(ctxArgs.colorScheme).toBe("dark");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -132,6 +135,84 @@ 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;
|
||||||
@@ -139,7 +220,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([]) };
|
mockContext = { close: vi.fn(), pages: vi.fn().mockReturnValue([]), newPage: vi.fn(), on: vi.fn() };
|
||||||
mockChromium = {
|
mockChromium = {
|
||||||
launchPersistentContext: vi.fn().mockResolvedValue(mockContext),
|
launchPersistentContext: vi.fn().mockResolvedValue(mockContext),
|
||||||
};
|
};
|
||||||
|
|||||||
+12
-12
@@ -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._stealth is a StealthEval instance after patching", async () => {
|
it("page._stealthWorld 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)._stealth).toBeDefined();
|
expect((page as any)._stealthWorld).toBeDefined();
|
||||||
expect(typeof (page as any)._stealth.evaluate).toBe("function");
|
expect(typeof (page as any)._stealthWorld.evaluate).toBe("function");
|
||||||
expect(typeof (page as any)._stealth.invalidate).toBe("function");
|
expect(typeof (page as any)._stealthWorld.invalidate).toBe("function");
|
||||||
expect(typeof (page as any)._stealth.getCdpSession).toBe("function");
|
expect(typeof (page as any)._stealthWorld.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)._stealth;
|
const stealth = (page as any)._stealthWorld;
|
||||||
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)._stealth;
|
const stealth = (page as any)._stealthWorld;
|
||||||
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)._stealth;
|
const stealth = (page as any)._stealthWorld;
|
||||||
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)._stealth;
|
const stealth = (page as any)._stealthWorld;
|
||||||
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)._stealth;
|
const stealth = (page as any)._stealthWorld;
|
||||||
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)._stealth;
|
const stealth = (page as any)._stealthWorld;
|
||||||
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)._stealth).toBeDefined();
|
expect((page as any)._stealthWorld).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);
|
||||||
|
|||||||
@@ -133,14 +133,10 @@ 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
|
||||||
# GPU flags removed — binary auto-generates from seed + platform
|
assert any("Apple" in a for a in args)
|
||||||
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
|
||||||
# GPU flags removed — binary auto-generates from seed + platform
|
assert any("NVIDIA" in a for a in args)
|
||||||
assert not any("fingerprint-gpu-vendor" in a for a in args)
|
|
||||||
assert not any("fingerprint-gpu-renderer" in a for a in args)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user