Compare commits

..
Author SHA1 Message Date
CloakHQ 5b2981c4c1 release: v0.3.23 — Puppeteer humanize, CDP humanize export, cloakserve locale fix 2026-04-09 20:56:16 +02:00
lilosandGitHub 7afe59435e feat: Add Puppeteer humanize support and fix Playwright humanize gaps (#129)
- Add full Puppeteer humanize implementation (page, frame, element handle patching)
- Fix critical Playwright gaps: page.pressSequentially, page.tap, page.clear
- Fix frame-level patching: frame.pressSequentially, frame.tap
- Add comprehensive stealth tests for Puppeteer
- Update SLOW test suite to use correct humanize: true API
- Add 4 new tests validating fixed Playwright methods
2026-04-09 20:49:20 +02:00
CloakHQ 1cef71133d fix(test): clear CLOAKBROWSER_BINARY_PATH in puppeteer mock tests
Env var override takes precedence over ensureBinary mock, causing
test to fail in Docker where the var is always set.
2026-04-09 20:45:16 +02:00
CloakHQ 7a0937cc54 feat(js): expose humanize module for CDP-connected browsers (#126)
Add ./human export path to package.json so users can import patchBrowser,
patchPage, and resolveConfig to humanize CDP-connected Playwright instances.
2026-04-09 18:06:29 +02:00
CloakHQ 5b00ff0325 fix(cloakserve): route locale/timezone/seed CLI args through build_args()
CLI args like --fingerprint-locale were passed as raw passthrough args
to Chrome, missing the companion --lang flag that build_args() normally
adds. Caused Intl API to default to en-US while navigator.language
showed the correct locale — a detectable mismatch.

Fixes #130
2026-04-09 17:58:47 +02:00
CloakHQ 5dd44298ee ci: use Node 24 for npm publish (Node 22.22.2 has broken npm)
Node 22.22.2's bundled npm 10.9.7 is missing promise-retry, breaking
npm install -g. Node 24 ships npm 11.11.0 with native OIDC support.

Ref: nodejs/node#62425, actions/runner-images#13883
2026-04-09 04:52:12 +02:00
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
CloakHQ 216a7d6a6a fix(examples): enable geoip in stealth test to fix FingerprintJS detection 2026-04-06 02:38:26 +02:00
CloakHQ 02359f69c8 ci: remove npm self-upgrade step — Node 22 ships with compatible npm 2026-04-06 02:08:44 +02:00
26 changed files with 3744 additions and 99 deletions
+1 -3
View File
@@ -83,10 +83,8 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with: with:
node-version: 22 node-version: 24 # npm 11.11.0 native — no upgrade needed (Node 22.22.2 has broken npm)
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- name: Upgrade npm
run: npm install -g npm@latest
- 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
+21
View File
@@ -6,6 +6,27 @@ Changes are tagged: **[wrapper]** for Python/JS wrapper, **[binary]** for Chromi
--- ---
## [0.3.23] — 2026-04-09
- **[wrapper]** Add full Puppeteer humanize support — human-like mouse, keyboard, and scroll behavior for `puppeteer-core` users (thanks [@evelaa123](https://github.com/evelaa123), #129)
- **[wrapper]** Fix Playwright humanize gaps — `pressSequentially`, `tap`, `clear` on pages and frames now use human-like behavior (#129)
- **[wrapper]** Expose humanize module for CDP-connected browsers — `import from 'cloakbrowser/human'` for manual patching of external Playwright instances (#126)
- **[docker]** Fix `cloakserve` locale/timezone mismatch — CLI args now route through `build_args()` so the companion `--lang` flag is added automatically (#130)
- **[meta]** Use Node 24 in CI publish workflow to work around broken npm in Node 22.22.2
## [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/
+36 -28
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()
@@ -451,7 +452,7 @@ clearCache();
## Human Behavior ## Human Behavior
Pass `humanize=True` to make all mouse, keyboard, and scroll interactions indistinguishable from real users. All Playwright calls `page.click()`, `page.fill()`, `page.type()`, `page.mouse.*`, `page.keyboard.*`, and the full Locator API are automatically replaced with human-like equivalents. No code changes needed. Pass `humanize=True` to make all mouse, keyboard, and scroll interactions indistinguishable from real users. All Playwright calls (`page.click()`, `page.fill()`, `page.type()`, `page.mouse.*`, `page.keyboard.*`, Locator API) and Puppeteer calls (`page.click()`, `page.type()`, `page.mouse.*`, `page.keyboard.*`, ElementHandle API) are automatically replaced with human-like equivalents. No code changes needed.
```python ```python
browser = launch(humanize=True) browser = launch(humanize=True)
@@ -462,6 +463,14 @@ page.locator("button[type=submit]").click() # Bézier curve, realistic aim
``` ```
```javascript ```javascript
// Playwright
import { launch } from 'cloakbrowser';
const browser = await launch({ humanize: true });
```
```javascript
// Puppeteer
import { launch } from 'cloakbrowser/puppeteer';
const browser = await launch({ humanize: true }); const browser = await launch({ humanize: true });
``` ```
@@ -510,9 +519,11 @@ const browser = await launch({
Access the original un-patched Playwright page at `page._original` if you need raw speed for a specific call. Access the original un-patched Playwright page at `page._original` if you need raw speed for a specific call.
> **Note:** Always use `page.click(selector)`, `page.type(selector, text)`, `page.hover(selector)`, or `page.locator(selector).*` — these go through the full humanize pipeline. Avoid `page.query_selector()` — `ElementHandle` objects bypass all patches, so mouse movement teleports, keyboard events fire without timing, and scroll has no human curve. > **Note (Playwright):** Always use `page.click(selector)`, `page.type(selector, text)`, `page.hover(selector)`, or `page.locator(selector).*` — these go through the full humanize pipeline. Avoid `page.query_selector()` — `ElementHandle` objects bypass all patches, so mouse movement teleports, keyboard events fire without timing, and scroll has no human curve.
>
> **Note (Puppeteer):** Both selector-based methods (`page.click()`, `page.type()`) and ElementHandle methods (`el.click()`, `el.type()`) are fully humanized. `page.$()`, `page.$$()`, and `page.waitForSelector()` return patched handles automatically.
> Contributed by [@evelaa123](https://github.com/evelaa123) — full Playwright API coverage. > Contributed by [@evelaa123](https://github.com/evelaa123) — full Playwright and Puppeteer API coverage.
## Configuration ## Configuration
@@ -554,14 +565,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
@@ -569,6 +576,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) |
@@ -598,11 +607,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",
@@ -656,11 +663,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.
@@ -921,9 +928,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.
@@ -1019,7 +1026,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 |
@@ -1043,7 +1050,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
@@ -1068,3 +1075,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
+34 -1
View File
@@ -88,11 +88,17 @@ class ChromePool:
global_args: list[str], global_args: list[str],
headless: bool, headless: bool,
data_dir: str = "/tmp/cloakserve", data_dir: str = "/tmp/cloakserve",
default_seed: str | None = None,
default_locale: str | None = None,
default_timezone: str | None = None,
): ):
self._binary = binary self._binary = binary
self._global_args = global_args self._global_args = global_args
self._headless = headless self._headless = headless
self._data_dir = data_dir self._data_dir = data_dir
self._default_seed = default_seed
self._default_locale = default_locale
self._default_timezone = default_timezone
self._processes: dict[str, ChromeProcess] = {} self._processes: dict[str, ChromeProcess] = {}
self._default: ChromeProcess | None = None self._default: ChromeProcess | None = None
self._locks: dict[str, asyncio.Lock] = {} self._locks: dict[str, asyncio.Lock] = {}
@@ -140,6 +146,14 @@ class ChromePool:
geoip: bool = False, geoip: bool = False,
) -> ChromeProcess: ) -> ChromeProcess:
"""Get existing or launch new Chrome process for a seed.""" """Get existing or launch new Chrome process for a seed."""
# Apply CLI defaults when query params don't provide values
if seed is None and self._default_seed:
seed = self._default_seed
if locale is None:
locale = self._default_locale
if timezone is None:
timezone = self._default_timezone
# No seed = default shared process # No seed = default shared process
if seed is None: if seed is None:
seed_key = "__default__" seed_key = "__default__"
@@ -552,11 +566,20 @@ def _default_data_dir() -> str:
def parse_cli_args(argv: list[str]) -> tuple[dict, list[str]]: def parse_cli_args(argv: list[str]) -> tuple[dict, list[str]]:
"""Parse cloakserve-specific args, return (config, passthrough_args).""" """Parse cloakserve-specific args, return (config, passthrough_args).
--fingerprint, --fingerprint-locale, and --fingerprint-timezone are
extracted into config defaults so they route through build_args()
(e.g. locale needs both --lang and --fingerprint-locale).
Query-string params override these defaults per-connection.
"""
config: dict = { config: dict = {
"port": 9222, "port": 9222,
"headless": True, "headless": True,
"data_dir": None, "data_dir": None,
"default_seed": None,
"default_locale": None,
"default_timezone": None,
} }
passthrough = [] passthrough = []
# Flags consumed by cloakserve (not passed to Chrome) # Flags consumed by cloakserve (not passed to Chrome)
@@ -577,6 +600,13 @@ def parse_cli_args(argv: list[str]) -> tuple[dict, list[str]]:
passthrough.append(arg) passthrough.append(arg)
elif arg.startswith(consumed_prefixes): elif arg.startswith(consumed_prefixes):
pass # Strip these silently pass # Strip these silently
# Route through build_args() so companion flags are set correctly
elif arg.startswith("--fingerprint-locale="):
config["default_locale"] = arg.split("=", 1)[1]
elif arg.startswith("--fingerprint-timezone="):
config["default_timezone"] = arg.split("=", 1)[1]
elif arg.startswith("--fingerprint="):
config["default_seed"] = arg.split("=", 1)[1]
else: else:
passthrough.append(arg) passthrough.append(arg)
@@ -599,6 +629,9 @@ def main() -> None:
global_args=global_args, global_args=global_args,
headless=config["headless"], headless=config["headless"],
data_dir=config["data_dir"], data_dir=config["data_dir"],
default_seed=config["default_seed"],
default_locale=config["default_locale"],
default_timezone=config["default_timezone"],
) )
app = web.Application() app = web.Application()
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.3.20" __version__ = "0.3.23"
+27 -6
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."""
@@ -237,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,
@@ -264,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'.
@@ -310,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)
@@ -354,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,
@@ -380,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'.
@@ -429,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)
@@ -472,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,
@@ -495,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'.
@@ -527,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)
+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)",
]
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
+1 -1
View File
@@ -228,7 +228,7 @@ def main():
print() print()
print("Launching stealth browser...", flush=True) print("Launching stealth browser...", flush=True)
browser = launch(headless=not HEADED, proxy=PROXY) browser = launch(headless=not HEADED, proxy=PROXY, geoip=True)
page = browser.new_page() page = browser.new_page()
# Show browser fingerprint details # Show browser fingerprint details
+3 -3
View File
@@ -203,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
+5 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "cloakbrowser", "name": "cloakbrowser",
"version": "0.3.20", "version": "0.3.23",
"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",
@@ -13,6 +13,10 @@
"./puppeteer": { "./puppeteer": {
"types": "./dist/puppeteer.d.ts", "types": "./dist/puppeteer.d.ts",
"import": "./dist/puppeteer.js" "import": "./dist/puppeteer.js"
},
"./human": {
"types": "./dist/human/index.d.ts",
"import": "./dist/human/index.js"
} }
}, },
"bin": { "bin": {
+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)",
];
} }
+913
View File
@@ -0,0 +1,913 @@
/**
* Human-like behavioral layer for cloakbrowser Puppeteer edition.
*
* Mirrors Playwright humanize architecture, adapted for Puppeteer API.
*
* Patches ALL native Puppeteer interaction surfaces:
*
* PAGE-LEVEL:
* click (with clickCount support for dblclick), hover, type,
* select, focus, tap, goto
*
* MOUSE:
* move, click (with clickCount support for dblclick), wheel,
* dragAndDrop
*
* KEYBOARD:
* type, down, up, press, sendCharacter
*
* FRAME-LEVEL:
* click, hover, type, select, focus, tap
* + $, $$, waitForSelector (return patched ElementHandles)
*
* ELEMENTHANDLE-LEVEL (Puppeteer-specific, no Playwright equivalent):
* click (with clickCount), hover, type, press, tap, select,
* focus, drop, dragAndDrop
* + $, $$, waitForSelector (nested elements are also patched)
*
* BROWSER-LEVEL:
* newPage, createBrowserContext / createIncognitoBrowserContext,
* targetcreated event
*
* Stealth-aware:
* - isInputElement / isSelectorFocused use CDP Isolated Worlds
* - Shift symbol typing uses CDP Input.dispatchKeyEvent (isTrusted=true)
* - ElementHandle isInput check uses CDP DOM.describeNode (no JS execution)
* - Falls back to page.evaluate only when CDP session is unavailable
*
* Puppeteer-specific adaptations:
* - page.createCDPSession() instead of context.newCDPSession(page)
* - page.viewport() instead of page.viewportSize()
* - page.$(selector) instead of page.locator(selector)
* - keyboard.sendCharacter() mapped via RawKeyboard.insertText
* - mouse.wheel({deltaX, deltaY}) object form adapted to (dx, dy)
* - page.select() instead of page.selectOption()
* - ElementHandle prototype patching (Puppeteer-only)
* - No page.dblclick() Puppeteer uses click({clickCount:2})
*/
import type { Browser, Page, Frame, CDPSession, ElementHandle, BrowserContext } from 'puppeteer-core';
import type { HumanConfig } from '../human/config.js';
import { resolveConfig, rand, randRange, sleep } from '../human/config.js';
import { RawMouse, RawKeyboard, humanMove, humanClick, clickTarget, humanIdle } from '../human/mouse.js';
import { humanType } from './keyboard.js';
import { scrollToElement, smoothWheel } from './scroll.js';
export type { HumanConfig } from '../human/config.js';
export { resolveConfig } from '../human/config.js';
export { humanMove, humanClick, clickTarget, humanIdle } from '../human/mouse.js';
export { humanType } from './keyboard.js';
export { scrollToElement } from './scroll.js';
// ============================================================================
// CDP Isolated World — stealth DOM evaluation (Puppeteer version)
// ============================================================================
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.createCDPSession();
}
return this.cdp;
}
private async createWorld(): Promise<number> {
const cdp = await this.ensureCdp();
const tree = await cdp.send('Page.getFrameTree');
const frameId = (tree as any).frameTree.frame.id;
const result = await cdp.send('Page.createIsolatedWorld', {
frameId,
worldName: '',
grantUniveralAccess: true,
});
const ctxId = (result as any).executionContextId;
this.contextId = ctxId;
return ctxId;
}
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 as any).exceptionDetails) {
if (attempt === 0) {
await this.createWorld();
continue;
}
return undefined;
}
return (result as any).result?.value;
} catch {
if (attempt === 0) {
this.contextId = null;
try { await this.createWorld(); } catch { return undefined; }
continue;
}
return undefined;
}
}
return undefined;
}
invalidate(): void {
this.contextId = null;
}
async getCdpSession(): Promise<CDPSession> {
return this.ensureCdp();
}
}
// ============================================================================
// Cursor state
// ============================================================================
class CursorState {
x = 0;
y = 0;
initialized = false;
}
// ============================================================================
// Stealth DOM queries
// ============================================================================
async function isInputElement(
stealth: StealthEval | null,
page: Page,
selector: string,
): Promise<boolean> {
if (stealth) {
try {
const escaped = JSON.stringify(selector);
const result = await stealth.evaluate(`
(() => {
const el = document.querySelector(${escaped});
if (!el) return false;
const tag = el.tagName.toLowerCase();
return tag === 'input' || tag === 'textarea'
|| el.getAttribute('contenteditable') === 'true';
})()
`);
return !!result;
} catch { /* fallthrough */ }
}
return page.evaluate((sel: string) => {
const el = document.querySelector(sel);
if (!el) return false;
const tag = el.tagName.toLowerCase();
return tag === 'input' || tag === 'textarea'
|| el.getAttribute('contenteditable') === 'true';
}, selector).catch(() => false);
}
async function isSelectorFocused(
stealth: StealthEval | null,
page: Page,
selector: string,
): Promise<boolean> {
if (stealth) {
try {
const escaped = JSON.stringify(selector);
const result = await stealth.evaluate(`
(() => {
const el = document.querySelector(${escaped});
return el === document.activeElement;
})()
`);
return !!result;
} catch { /* fallthrough */ }
}
return page.evaluate((sel: string) => {
const el = document.querySelector(sel);
return el === document.activeElement;
}, selector).catch(() => false);
}
// ============================================================================
// Stealth ElementHandle input check — uses CDP DOM.describeNode
// instead of el.evaluate() to avoid main-world JS execution.
// ============================================================================
async function isInputElementHandle(
stealth: StealthEval | null,
el: ElementHandle,
): Promise<boolean> {
if (stealth) {
try {
const cdp = await stealth.getCdpSession();
const remoteObject = (el as any).remoteObject?.();
if (remoteObject?.objectId) {
const { node } = await cdp.send('DOM.describeNode', {
objectId: remoteObject.objectId,
}) as any;
const tag = (node?.nodeName || '').toLowerCase();
if (tag === 'input' || tag === 'textarea') return true;
const attrs: string[] = node?.attributes || [];
for (let i = 0; i < attrs.length; i += 2) {
if (attrs[i] === 'contenteditable' && attrs[i + 1] === 'true') {
return true;
}
}
return false;
}
} catch { /* fallthrough to el.evaluate */ }
}
return el.evaluate((node: any) => {
const tag = node.tagName?.toLowerCase();
return tag === 'input' || tag === 'textarea'
|| node.getAttribute?.('contenteditable') === 'true';
}).catch(() => false);
}
// ============================================================================
// Page-level patching
// ============================================================================
function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
const originals = {
click: page.click.bind(page),
hover: page.hover.bind(page),
type: page.type.bind(page),
select: page.select.bind(page),
focus: page.focus.bind(page),
goto: page.goto.bind(page),
tap: page.tap.bind(page),
mouseMove: page.mouse.move.bind(page.mouse),
mouseClick: page.mouse.click.bind(page.mouse),
mouseDown: page.mouse.down.bind(page.mouse),
mouseUp: page.mouse.up.bind(page.mouse),
mouseWheel: (page.mouse as any).wheel?.bind(page.mouse),
mouseDragAndDrop: (page.mouse as any).dragAndDrop?.bind(page.mouse),
keyboardType: page.keyboard.type.bind(page.keyboard),
keyboardDown: page.keyboard.down.bind(page.keyboard) as (key: string) => Promise<void>,
keyboardUp: page.keyboard.up.bind(page.keyboard) as (key: string) => Promise<void>,
keyboardPress: page.keyboard.press.bind(page.keyboard),
keyboardSendCharacter: page.keyboard.sendCharacter.bind(page.keyboard),
};
(page as any)._original = originals;
(page as any)._humanCfg = cfg;
const stealth = new StealthEval(page);
(page as any)._stealth = stealth;
let cdpSession: CDPSession | null = null;
const ensureCdp = async (): Promise<CDPSession | null> => {
if (!cdpSession) {
try { cdpSession = await stealth.getCdpSession(); } catch {}
}
return cdpSession;
};
const raw: RawMouse = {
move: originals.mouseMove,
down: originals.mouseDown,
up: originals.mouseUp,
wheel: async (deltaX: number, deltaY: number) => {
if (originals.mouseWheel) {
await originals.mouseWheel({ deltaX, deltaY });
}
},
};
const rawKb: RawKeyboard = {
down: originals.keyboardDown,
up: originals.keyboardUp,
type: originals.keyboardType,
insertText: originals.keyboardSendCharacter,
};
async function ensureCursorInit(): Promise<void> {
if (!cursor.initialized) {
cursor.x = rand(cfg.initial_cursor_x[0], cfg.initial_cursor_x[1]);
cursor.y = rand(cfg.initial_cursor_y[0], cfg.initial_cursor_y[1]);
await originals.mouseMove(cursor.x, cursor.y);
cursor.initialized = true;
}
}
// ==== goto ====
const humanGoto = async (url: string, options?: any) => {
const response = await originals.goto(url, options);
stealth.invalidate();
patchFrames(page, cfg, cursor, raw, rawKb, originals, stealth);
return response;
};
// ==== click (with clickCount support for dblclick) ====
const humanClickFn = async (selector: string, options?: any) => {
await ensureCursorInit();
if (cfg.idle_between_actions) {
await humanIdle(raw, rand(cfg.idle_between_duration[0], cfg.idle_between_duration[1]), cursor.x, cursor.y, cfg);
}
const { box, cursorX, cursorY } = await scrollToElement(page, raw, selector, cursor.x, cursor.y, cfg);
cursor.x = cursorX;
cursor.y = cursorY;
const isInput = await isInputElement(stealth, page, selector);
const target = clickTarget(box, isInput, cfg);
await humanMove(raw, cursor.x, cursor.y, target.x, target.y, cfg);
cursor.x = target.x;
cursor.y = target.y;
const clickCount = options?.clickCount ?? options?.count ?? 1;
if (clickCount >= 2) {
await humanClick(raw, isInput, cfg);
await sleep(rand(40, 90));
await raw.down({ clickCount: 2 });
await sleep(rand(30, 60));
await raw.up({ clickCount: 2 });
} else {
await humanClick(raw, isInput, cfg);
}
};
// ==== hover ====
const humanHoverFn = async (selector: string, options?: any) => {
await ensureCursorInit();
if (cfg.idle_between_actions) {
await humanIdle(raw, rand(cfg.idle_between_duration[0], cfg.idle_between_duration[1]), cursor.x, cursor.y, cfg);
}
const { box, cursorX, cursorY } = await scrollToElement(page, raw, selector, cursor.x, cursor.y, cfg);
cursor.x = cursorX;
cursor.y = cursorY;
const target = clickTarget(box, false, cfg);
await humanMove(raw, cursor.x, cursor.y, target.x, target.y, cfg);
cursor.x = target.x;
cursor.y = target.y;
};
// ==== type ====
const humanTypeFn = async (selector: string, text: string, options?: any) => {
await sleep(randRange(cfg.field_switch_delay));
await humanClickFn(selector);
await sleep(rand(100, 250));
const cdp = await ensureCdp();
await humanType(page, rawKb, text, cfg, cdp);
};
// ==== select ====
const humanSelectFn = async (selector: string, ...values: string[]) => {
await humanHoverFn(selector);
await sleep(rand(100, 300));
return originals.select(selector, ...values);
};
// ==== focus ====
const humanFocusFn = async (selector: string) => {
if (!await isSelectorFocused(stealth, page, selector)) {
await humanClickFn(selector);
}
};
// ==== tap ====
const humanTapFn = async (selector: string, options?: any) => {
await humanClickFn(selector, options);
};
// ============================================================
// Assign page-level patches
// ============================================================
(page as any).goto = humanGoto;
(page as any).click = humanClickFn;
(page as any).hover = humanHoverFn;
(page as any).type = humanTypeFn;
(page as any).select = humanSelectFn;
(page as any).focus = humanFocusFn;
(page as any).tap = humanTapFn;
// ============================================================
// Mouse patches
// ============================================================
page.mouse.move = async (x: number, y: number, options?: any) => {
await ensureCursorInit();
await humanMove(raw, cursor.x, cursor.y, x, y, cfg);
cursor.x = x;
cursor.y = y;
};
page.mouse.click = async (x: number, y: number, options?: any) => {
await ensureCursorInit();
await humanMove(raw, cursor.x, cursor.y, x, y, cfg);
cursor.x = x;
cursor.y = y;
const clickCount = options?.clickCount ?? options?.count ?? 1;
if (clickCount >= 2) {
await humanClick(raw, false, cfg);
await sleep(rand(40, 90));
await raw.down({ clickCount: 2 });
await sleep(rand(30, 60));
await raw.up({ clickCount: 2 });
} else {
await humanClick(raw, false, cfg);
}
};
if (originals.mouseWheel) {
(page.mouse as any).wheel = async (options?: { deltaX?: number; deltaY?: number }) => {
const dx = options?.deltaX ?? 0;
const dy = options?.deltaY ?? 0;
if (Math.abs(dy) > 0) {
await smoothWheel(raw, dy, cfg, 'y');
}
if (Math.abs(dx) > 0) {
await smoothWheel(raw, dx, cfg, 'x');
}
};
}
if (originals.mouseDragAndDrop) {
(page.mouse as any).dragAndDrop = async (
start: { x: number; y: number },
target: { x: number; y: number },
options?: any,
) => {
await ensureCursorInit();
await humanMove(raw, cursor.x, cursor.y, start.x, start.y, cfg);
cursor.x = start.x;
cursor.y = start.y;
await sleep(rand(100, 200));
await originals.mouseDown();
await sleep(rand(80, 150));
await humanMove(raw, cursor.x, cursor.y, target.x, target.y, cfg);
cursor.x = target.x;
cursor.y = target.y;
await sleep(rand(80, 150));
await originals.mouseUp();
};
}
// ============================================================
// Keyboard patches
// ============================================================
page.keyboard.type = async (text: string, options?: any) => {
const cdp = await ensureCdp();
await humanType(page, rawKb, text, cfg, cdp);
};
page.keyboard.press = async (key: any, options?: any) => {
await sleep(rand(20, 60));
await originals.keyboardDown(key as any);
await sleep(randRange(cfg.key_hold));
await originals.keyboardUp(key as any);
};
page.keyboard.down = async (key: any) => {
await sleep(rand(10, 30));
await originals.keyboardDown(key as any);
};
page.keyboard.up = async (key: any) => {
await sleep(rand(10, 30));
await originals.keyboardUp(key as any);
};
// ============================================================
// Store helpers for frame/element patching
// ============================================================
(page as any)._humanCursor = cursor;
(page as any)._humanRaw = raw;
(page as any)._humanRawKb = rawKb;
(page as any)._ensureCursorInit = ensureCursorInit;
// Initialize cursor
cursor.x = rand(cfg.initial_cursor_x[0], cfg.initial_cursor_x[1]);
cursor.y = rand(cfg.initial_cursor_y[0], cfg.initial_cursor_y[1]);
originals.mouseMove(cursor.x, cursor.y).then(() => {
cursor.initialized = true;
}).catch(() => {});
// Patch frames
patchFrames(page, cfg, cursor, raw, rawKb, originals, stealth);
// Patch ElementHandle selectors
patchElementHandle(page, cfg, cursor, raw, rawKb, originals, stealth);
}
// ============================================================================
// ElementHandle patching — PUPPETEER-SPECIFIC
// ============================================================================
function patchElementHandle(
page: Page,
cfg: HumanConfig,
cursor: CursorState,
raw: RawMouse,
rawKb: RawKeyboard,
originals: any,
stealth: StealthEval,
): void {
const orig$ = page.$.bind(page);
const orig$$ = page.$$.bind(page);
const origWaitForSelector = page.waitForSelector.bind(page);
(page as any).$ = async (selector: string) => {
const el = await orig$(selector);
if (el) patchSingleElementHandle(el, page, cfg, cursor, raw, rawKb, originals, stealth);
return el;
};
(page as any).$$ = async (selector: string) => {
const els = await orig$$(selector);
for (const el of els) {
patchSingleElementHandle(el, page, cfg, cursor, raw, rawKb, originals, stealth);
}
return els;
};
(page as any).waitForSelector = async (selector: string, options?: any) => {
const el = await origWaitForSelector(selector, options);
if (el) patchSingleElementHandle(el, page, cfg, cursor, raw, rawKb, originals, stealth);
return el;
};
}
function patchSingleElementHandle(
el: ElementHandle,
page: Page,
cfg: HumanConfig,
cursor: CursorState,
raw: RawMouse,
rawKb: RawKeyboard,
originals: any,
stealth: StealthEval,
): void {
if ((el as any)._humanPatched) return;
(el as any)._humanPatched = true;
const origElClick = el.click.bind(el);
const origElHover = el.hover.bind(el);
const origElType = el.type.bind(el);
const origElPress = (el as any).press?.bind(el);
const origElTap = (el as any).tap?.bind(el);
const origElFocus = (el as any).focus?.bind(el);
const origElDragAndDrop = (el as any).dragAndDrop?.bind(el);
const origElSelect = (el as any).select?.bind(el);
const origElDrop = (el as any).drop?.bind(el);
// --- Nested selectors ---
const origEl$ = el.$.bind(el);
const origEl$$ = el.$$.bind(el);
const origElWaitForSelector = el.waitForSelector.bind(el);
(el as any).$ = async (selector: string) => {
const child = await origEl$(selector);
if (child) patchSingleElementHandle(child, page, cfg, cursor, raw, rawKb, originals, stealth);
return child;
};
(el as any).$$ = async (selector: string) => {
const children = await origEl$$(selector);
for (const child of children) {
patchSingleElementHandle(child, page, cfg, cursor, raw, rawKb, originals, stealth);
}
return children;
};
(el as any).waitForSelector = async (selector: string, options?: any) => {
const child = await origElWaitForSelector(selector, options);
if (child) patchSingleElementHandle(child, page, cfg, cursor, raw, rawKb, originals, stealth);
return child;
};
// --- Helper: get box and move cursor ---
const moveToElement = async () => {
await (page as any)._ensureCursorInit();
const box = await el.boundingBox();
if (!box) return null;
const isInp = await isInputElementHandle(stealth, el);
const target = clickTarget(box, isInp, cfg);
if (cfg.idle_between_actions) {
await humanIdle(raw, rand(cfg.idle_between_duration[0], cfg.idle_between_duration[1]), cursor.x, cursor.y, cfg);
}
await humanMove(raw, cursor.x, cursor.y, target.x, target.y, cfg);
cursor.x = target.x;
cursor.y = target.y;
return { box, isInp };
};
// --- el.click() ---
(el as any).click = async (options?: any) => {
const info = await moveToElement();
if (!info) return origElClick(options);
const clickCount = options?.clickCount ?? options?.count ?? 1;
if (clickCount >= 2) {
await humanClick(raw, info.isInp, cfg);
await sleep(rand(40, 90));
await raw.down({ clickCount: 2 });
await sleep(rand(30, 60));
await raw.up({ clickCount: 2 });
} else {
await humanClick(raw, info.isInp, cfg);
}
};
// --- el.hover() ---
(el as any).hover = async () => {
const info = await moveToElement();
if (!info) return origElHover();
};
// --- el.type() ---
(el as any).type = async (text: string, options?: any) => {
const info = await moveToElement();
if (!info) return origElType(text, options);
await humanClick(raw, info.isInp, cfg);
await sleep(rand(100, 250));
const cdp = await stealth.getCdpSession().catch(() => null);
await humanType(page, rawKb, text, cfg, cdp);
};
// --- el.press() ---
if (origElPress) {
(el as any).press = async (key: string, options?: any) => {
await sleep(rand(20, 60));
await originals.keyboardDown(key as any);
await sleep(randRange(cfg.key_hold));
await originals.keyboardUp(key as any);
};
}
// --- el.tap() ---
if (origElTap) {
(el as any).tap = async () => {
const info = await moveToElement();
if (!info) return origElTap();
await humanClick(raw, info.isInp, cfg);
};
}
// --- el.focus() ---
if (origElFocus) {
(el as any).focus = async () => {
const info = await moveToElement();
if (!info) return origElFocus();
await humanClick(raw, info.isInp, cfg);
};
}
// --- el.select() ---
if (origElSelect) {
(el as any).select = async (...values: string[]) => {
const info = await moveToElement();
if (!info) return origElSelect(...values);
await humanClick(raw, false, cfg);
await sleep(rand(100, 300));
return origElSelect(...values);
};
}
// --- el.drop() ---
if (origElDrop) {
(el as any).drop = async (draggable: ElementHandle, options?: any) => {
const srcBox = await draggable.boundingBox();
const tgtBox = await el.boundingBox();
if (srcBox && tgtBox) {
const sx = srcBox.x + srcBox.width / 2;
const sy = srcBox.y + srcBox.height / 2;
const tx = tgtBox.x + tgtBox.width / 2;
const ty = tgtBox.y + tgtBox.height / 2;
await (page as any)._ensureCursorInit();
await humanMove(raw, cursor.x, cursor.y, sx, sy, cfg);
cursor.x = sx;
cursor.y = sy;
await sleep(rand(100, 200));
await originals.mouseDown();
await sleep(rand(80, 150));
await humanMove(raw, cursor.x, cursor.y, tx, ty, cfg);
cursor.x = tx;
cursor.y = ty;
await sleep(rand(80, 150));
await originals.mouseUp();
} else {
return origElDrop(draggable, options);
}
};
}
// --- el.dragAndDrop() ---
if (origElDragAndDrop) {
(el as any).dragAndDrop = async (targetEl: ElementHandle, options?: any) => {
const srcBox = await el.boundingBox();
const tgtBox = await targetEl.boundingBox();
if (srcBox && tgtBox) {
const sx = srcBox.x + srcBox.width / 2;
const sy = srcBox.y + srcBox.height / 2;
const tx = tgtBox.x + tgtBox.width / 2;
const ty = tgtBox.y + tgtBox.height / 2;
await (page as any)._ensureCursorInit();
await humanMove(raw, cursor.x, cursor.y, sx, sy, cfg);
cursor.x = sx;
cursor.y = sy;
await sleep(rand(100, 200));
await originals.mouseDown();
await sleep(rand(80, 150));
await humanMove(raw, cursor.x, cursor.y, tx, ty, cfg);
cursor.x = tx;
cursor.y = ty;
await sleep(rand(80, 150));
await originals.mouseUp();
} else {
return origElDragAndDrop(targetEl, options);
}
};
}
}
// ============================================================================
// Frame-level patching — native Puppeteer Frame methods only
// Puppeteer Frame has: click, hover, type, select, focus, tap
// ============================================================================
function patchFrames(
page: Page,
cfg: HumanConfig,
cursor: CursorState,
raw: RawMouse,
rawKb: RawKeyboard,
originals: any,
stealth: StealthEval,
): void {
for (const frame of iterFrames(page)) {
patchSingleFrame(frame, page, cfg, cursor, raw, rawKb, originals, stealth);
}
}
function patchSingleFrame(
frame: Frame,
page: Page,
cfg: HumanConfig,
cursor: CursorState,
raw: RawMouse,
rawKb: RawKeyboard,
originals: any,
stealth: StealthEval,
): void {
if ((frame as any)._humanPatched) return;
(frame as any)._humanPatched = true;
const origFrameSelect = frame.select.bind(frame);
(frame as any).click = async (selector: string, options?: any) => {
await (page as any).click(selector, options);
};
(frame as any).hover = async (selector: string, options?: any) => {
await (page as any).hover(selector, options);
};
(frame as any).type = async (selector: string, text: string, options?: any) => {
await (page as any).type(selector, text, options);
};
(frame as any).select = async (selector: string, ...values: string[]) => {
await (page as any).hover(selector);
await sleep(rand(100, 300));
return origFrameSelect(selector, ...values);
};
(frame as any).focus = async (selector: string) => {
await (page as any).focus(selector);
};
(frame as any).tap = async (selector: string, options?: any) => {
await (page as any).click(selector, options);
};
// Patch frame.$() to return patched ElementHandles
const origFrame$ = frame.$.bind(frame);
const origFrame$$ = frame.$$.bind(frame);
const origFrameWaitForSelector = frame.waitForSelector.bind(frame);
(frame as any).$ = async (selector: string) => {
const el = await origFrame$(selector);
if (el) patchSingleElementHandle(el, page, cfg, cursor, raw, rawKb, originals, stealth);
return el;
};
(frame as any).$$ = async (selector: string) => {
const els = await origFrame$$(selector);
for (const el of els) {
patchSingleElementHandle(el, page, cfg, cursor, raw, rawKb, originals, stealth);
}
return els;
};
(frame as any).waitForSelector = async (selector: string, options?: any) => {
const el = await origFrameWaitForSelector(selector, options);
if (el) patchSingleElementHandle(el, page, cfg, cursor, raw, rawKb, originals, stealth);
return el;
};
}
function* iterFrames(page: Page): Generator<Frame> {
try {
const mainFrame = page.mainFrame();
yield mainFrame;
for (const child of mainFrame.childFrames()) {
yield child;
}
} catch {}
}
// ============================================================================
// Browser-level patching
// ============================================================================
export function patchBrowser(browser: Browser, cfg: HumanConfig): void {
browser.pages().then(pages => {
for (const page of pages) {
if (!(page as any)._original) {
patchPage(page, cfg, new CursorState());
}
}
}).catch(() => {});
const origNewPage = browser.newPage.bind(browser);
(browser as any).newPage = async () => {
const page = await origNewPage();
if (!(page as any)._original) {
patchPage(page, cfg, new CursorState());
}
return page;
};
// v21: createIncognitoBrowserContext
// v22+: createBrowserContext (renamed in puppeteer/puppeteer#11834)
for (const methodName of ['createBrowserContext', 'createIncognitoBrowserContext'] as const) {
if (typeof (browser as any)[methodName] === 'function') {
const origCreateContext = (browser as any)[methodName].bind(browser);
(browser as any)[methodName] = async (options?: any) => {
const context: BrowserContext = await origCreateContext(options);
const origCtxNewPage = context.newPage.bind(context);
(context as any).newPage = async () => {
const page = await origCtxNewPage();
if (!(page as any)._original) {
patchPage(page, cfg, new CursorState());
}
return page;
};
return context;
};
}
}
browser.on('targetcreated', async (target: any) => {
try {
if (target.type() === 'page') {
const page = await target.page();
if (page && !(page as any)._original) {
patchPage(page, cfg, new CursorState());
}
}
} catch {}
});
}
export { patchPage };
+187
View File
@@ -0,0 +1,187 @@
/**
* cloakbrowser-human Human-like keyboard input.
* Adapted for Puppeteer API.
*
* Changes from Playwright version:
* - Uses puppeteer-core Page/CDPSession types
* - keyboard.sendCharacter() mapped via RawKeyboard.insertText adapter
* - CDPSession obtained via page.createCDPSession()
*
* Stealth-aware: shift symbols use CDP Input.dispatchKeyEvent (isTrusted=true).
*/
import type { Page, CDPSession } from 'puppeteer-core';
import { RawKeyboard } from '../human/mouse.js';
import type { HumanConfig } from '../human/config.js';
import { rand, randRange, sleep } from '../human/config.js';
const SHIFT_SYMBOLS = new Set([
'@', '#', '!', '$', '%', '^', '&', '*', '(', ')',
'_', '+', '{', '}', '|', ':', '"', '<', '>', '?', '~',
]);
const NEARBY_KEYS: Record<string, string> = {
a: 'sqwz', b: 'vghn', c: 'xdfv', d: 'sfecx', e: 'wrsdf',
f: 'dgrtcv', g: 'fhtyb', h: 'gjybn', i: 'ujko', j: 'hkunm',
k: 'jloi', l: 'kop', m: 'njk', n: 'bhjm', o: 'iklp',
p: 'ol', q: 'wa', r: 'edft', s: 'awedxz', t: 'rfgy',
u: 'yhji', v: 'cfgb', w: 'qase', x: 'zsdc', y: 'tghu',
z: 'asx',
'1': '2q', '2': '13qw', '3': '24we', '4': '35er', '5': '46rt',
'6': '57ty', '7': '68yu', '8': '79ui', '9': '80io', '0': '9p',
};
const SHIFT_SYMBOL_CODES: Record<string, string> = {
'!': 'Digit1', '@': 'Digit2', '#': 'Digit3', '$': 'Digit4',
'%': 'Digit5', '^': 'Digit6', '&': 'Digit7', '*': 'Digit8',
'(': 'Digit9', ')': 'Digit0', '_': 'Minus', '+': 'Equal',
'{': 'BracketLeft', '}': 'BracketRight', '|': 'Backslash',
':': 'Semicolon', '"': 'Quote', '<': 'Comma', '>': 'Period',
'?': 'Slash', '~': 'Backquote',
};
const SHIFT_SYMBOL_KEYCODES: Record<string, number> = {
'!': 49, '@': 50, '#': 51, '$': 52, '%': 53,
'^': 54, '&': 55, '*': 56, '(': 57, ')': 48,
'_': 189, '+': 187, '{': 219, '}': 221, '|': 220,
':': 186, '"': 222, '<': 188, '>': 190, '?': 191,
'~': 192,
};
function isAscii(ch: string): boolean {
const code = ch.codePointAt(0);
return code !== undefined && code < 128;
}
function getNearbyKey(ch: string): string {
const lower = ch.toLowerCase();
if (lower in NEARBY_KEYS) {
const neighbors = NEARBY_KEYS[lower];
const wrong = neighbors[Math.floor(Math.random() * neighbors.length)];
return ch === ch.toUpperCase() && ch !== ch.toLowerCase() ? wrong.toUpperCase() : wrong;
}
return ch;
}
function isUpperCase(ch: string): boolean {
return ch.length === 1 && ch >= 'A' && ch <= 'Z';
}
export async function humanType(
page: Page,
raw: RawKeyboard,
text: string,
cfg: HumanConfig,
cdpSession?: CDPSession | null,
): Promise<void> {
const chars = [...text];
for (let i = 0; i < chars.length; i++) {
const ch = chars[i];
// Non-ASCII → sendCharacter via insertText adapter
if (!isAscii(ch)) {
await sleep(randRange(cfg.key_hold));
await raw.insertText(ch);
if (i < chars.length - 1) await interCharDelay(cfg);
continue;
}
// Mistype
if (Math.random() < cfg.mistype_chance && /^[a-zA-Z0-9]$/.test(ch)) {
const wrong = getNearbyKey(ch);
await typeNormalChar(raw, wrong, cfg);
await sleep(randRange(cfg.mistype_delay_notice));
await raw.down('Backspace');
await sleep(randRange(cfg.key_hold));
await raw.up('Backspace');
await sleep(randRange(cfg.mistype_delay_correct));
}
if (isUpperCase(ch)) {
await typeShiftedChar(raw, ch, cfg);
} else if (SHIFT_SYMBOLS.has(ch)) {
await typeShiftSymbol(page, raw, ch, cfg, cdpSession);
} else {
await typeNormalChar(raw, ch, cfg);
}
if (i < chars.length - 1) await interCharDelay(cfg);
}
}
async function typeNormalChar(raw: RawKeyboard, ch: string, cfg: HumanConfig): Promise<void> {
await raw.down(ch);
await sleep(randRange(cfg.key_hold));
await raw.up(ch);
}
async function typeShiftedChar(raw: RawKeyboard, ch: string, cfg: HumanConfig): Promise<void> {
await raw.down('Shift');
await sleep(randRange(cfg.shift_down_delay));
await raw.down(ch);
await sleep(randRange(cfg.key_hold));
await raw.up(ch);
await sleep(randRange(cfg.shift_up_delay));
await raw.up('Shift');
}
async function typeShiftSymbol(
page: Page,
raw: RawKeyboard,
ch: string,
cfg: HumanConfig,
cdpSession?: CDPSession | null,
): Promise<void> {
if (cdpSession) {
const code = SHIFT_SYMBOL_CODES[ch] || '';
const keyCode = SHIFT_SYMBOL_KEYCODES[ch] || 0;
await raw.down('Shift');
await sleep(randRange(cfg.shift_down_delay));
await cdpSession.send('Input.dispatchKeyEvent', {
type: 'keyDown',
modifiers: 8,
key: ch,
code,
windowsVirtualKeyCode: keyCode,
text: ch,
unmodifiedText: ch,
});
await sleep(randRange(cfg.key_hold));
await cdpSession.send('Input.dispatchKeyEvent', {
type: 'keyUp',
modifiers: 8,
key: ch,
code,
windowsVirtualKeyCode: keyCode,
});
await sleep(randRange(cfg.shift_up_delay));
await raw.up('Shift');
} else {
await raw.down('Shift');
await sleep(randRange(cfg.shift_down_delay));
await raw.insertText(ch);
await page.evaluate((key: string) => {
const el = document.activeElement;
if (el) {
el.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }));
el.dispatchEvent(new KeyboardEvent('keyup', { key, bubbles: true }));
}
}, ch);
await sleep(randRange(cfg.shift_up_delay));
await raw.up('Shift');
}
}
async function interCharDelay(cfg: HumanConfig): Promise<void> {
if (Math.random() < cfg.typing_pause_chance) {
await sleep(randRange(cfg.typing_pause_range));
} else {
const delay = cfg.typing_delay + (Math.random() - 0.5) * 2 * cfg.typing_delay_spread;
await sleep(Math.max(10, delay));
}
}
+166
View File
@@ -0,0 +1,166 @@
/**
* cloakbrowser-human Human-like scrolling via mouse wheel events.
* Adapted for Puppeteer API.
*
* Changes from Playwright version:
* - page.viewport() instead of page.viewportSize()
* - page.$(selector) + el.boundingBox() instead of page.locator().boundingBox()
* - No timeout parameter on boundingBox()
*/
import type { Page } from 'puppeteer-core';
import type { HumanConfig } from '../human/config.js';
import { rand, randRange, randIntRange, sleep } from '../human/config.js';
import { RawMouse, humanMove } from '../human/mouse.js';
interface ElementBounds {
x: number;
y: number;
width: number;
height: number;
}
function isInViewport(
bounds: ElementBounds,
viewportHeight: number,
cfg: HumanConfig,
): boolean {
const topEdge = bounds.y;
const bottomEdge = bounds.y + bounds.height;
const zoneTop = viewportHeight * cfg.scroll_target_zone[0];
const zoneBottom = viewportHeight * cfg.scroll_target_zone[1];
return topEdge >= zoneTop && bottomEdge <= zoneBottom;
}
export async function smoothWheel(
raw: RawMouse,
delta: number,
cfg: HumanConfig,
axis: 'x' | 'y' = 'y',
): Promise<void> {
const absD = Math.abs(delta);
const sign = delta > 0 ? 1 : -1;
let sent = 0;
while (sent < absD) {
const stepSize = rand(20, 40);
const chunk = Math.min(stepSize, absD - sent);
const d = Math.round(chunk) * sign;
if (axis === 'x') {
await raw.wheel(d, 0);
} else {
await raw.wheel(0, d);
}
sent += chunk;
await sleep(rand(8, 20));
}
}
async function getElementBox(page: Page, selector: string): Promise<ElementBounds | null> {
try {
const el = await page.$(selector);
if (!el) return null;
const box = await el.boundingBox();
if (!box) return null;
return { x: box.x, y: box.y, width: box.width, height: box.height };
} catch {
return null;
}
}
export async function scrollToElement(
page: Page,
raw: RawMouse,
selector: string,
cursorX: number,
cursorY: number,
cfg: HumanConfig,
): Promise<{ box: ElementBounds; cursorX: number; cursorY: number }> {
const viewport = page.viewport();
if (!viewport) throw new Error('Viewport size not available');
let box = await getElementBox(page, selector);
if (!box) {
await sleep(200);
box = await getElementBox(page, selector);
if (!box) throw new Error(`Element not found: ${selector}`);
}
if (isInViewport(box, viewport.height, cfg)) {
return { box, cursorX, cursorY };
}
// Move cursor into scroll area
const scrollAreaX = Math.round(viewport.width * rand(0.3, 0.7));
const scrollAreaY = Math.round(viewport.height * rand(0.3, 0.7));
await humanMove(raw, cursorX, cursorY, scrollAreaX, scrollAreaY, cfg);
cursorX = scrollAreaX;
cursorY = scrollAreaY;
await sleep(randRange(cfg.scroll_pre_move_delay));
// Calculate scroll distance
const targetY = viewport.height * rand(cfg.scroll_target_zone[0], cfg.scroll_target_zone[1]);
const elementCenter = box.y + box.height / 2;
const distanceToScroll = elementCenter - targetY;
const direction = distanceToScroll > 0 ? 1 : -1;
const absDistance = Math.abs(distanceToScroll);
const avgDelta = (cfg.scroll_delta_base[0] + cfg.scroll_delta_base[1]) / 2;
const totalClicks = Math.max(3, Math.ceil(absDistance / avgDelta));
const accelSteps = randIntRange(cfg.scroll_accel_steps);
const decelSteps = randIntRange(cfg.scroll_decel_steps);
let scrolled = 0;
for (let i = 0; i < totalClicks; i++) {
let delta: number;
let pause: number;
if (i < accelSteps) {
delta = rand(80, 100);
pause = randRange(cfg.scroll_pause_slow);
} else if (i >= totalClicks - decelSteps) {
delta = rand(60, 90);
pause = randRange(cfg.scroll_pause_slow);
} else {
delta = randRange(cfg.scroll_delta_base);
pause = randRange(cfg.scroll_pause_fast);
}
delta *= 1 + (Math.random() - 0.5) * 2 * cfg.scroll_delta_variance;
delta = Math.round(delta) * direction;
await smoothWheel(raw, delta, cfg);
scrolled += Math.abs(delta);
await sleep(pause);
if (i % 3 === 2 || i === totalClicks - 1) {
box = await getElementBox(page, selector);
if (box && isInViewport(box, viewport.height, cfg)) {
break;
}
}
if (scrolled >= absDistance * 1.1) break;
}
// Optional overshoot + correction
if (Math.random() < cfg.scroll_overshoot_chance) {
const overshootPx = Math.round(randRange(cfg.scroll_overshoot_px)) * direction;
await smoothWheel(raw, overshootPx, cfg);
await sleep(randRange(cfg.scroll_settle_delay));
const corrections = randIntRange([1, 2]);
for (let c = 0; c < corrections; c++) {
const corrDelta = Math.round(rand(40, 80)) * -direction;
await smoothWheel(raw, corrDelta, cfg);
await sleep(rand(100, 250));
}
}
await sleep(randRange(cfg.scroll_settle_delay));
box = await getElementBox(page, selector);
if (!box) throw new Error(`Element lost after scrolling: ${selector}`);
return { box, cursorX, cursorY };
}
+13 -2
View File
@@ -440,6 +440,9 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
(page as any).uncheck = humanUncheckFn; (page as any).uncheck = humanUncheckFn;
(page as any).selectOption = humanSelectOptionFn; (page as any).selectOption = humanSelectOptionFn;
(page as any).press = humanPressFn; (page as any).press = humanPressFn;
(page as any).pressSequentially = humanPressSequentiallyFn;
(page as any).tap = humanTapFn;
(page as any).clear = humanClearFn;
// --- mouse patches --- // --- mouse patches ---
page.mouse.move = async (x: number, y: number, options?: any) => { page.mouse.move = async (x: number, y: number, options?: any) => {
@@ -494,8 +497,8 @@ function patchPage(page: Page, cfg: HumanConfig, cursor: CursorState): void {
/** /**
* Patch Frame methods so Locator-based calls go through humanization. * Patch Frame methods so Locator-based calls go through humanization.
* All 11 methods patched: click, dblclick, hover, type, fill, check, uncheck, * All 13 methods patched: click, dblclick, hover, type, fill, check, uncheck,
* selectOption, press, clear, dragAndDrop. * selectOption, press, pressSequentially, tap, clear, dragAndDrop.
*/ */
function patchFrames( function patchFrames(
page: Page, page: Page,
@@ -563,6 +566,14 @@ function patchSingleFrame(
await (page as any).press(selector, key, options); await (page as any).press(selector, key, options);
}; };
(frame as any).pressSequentially = async (selector: string, text: string, options?: any) => {
await (page as any).pressSequentially(selector, text, options);
};
(frame as any).tap = async (selector: string, options?: any) => {
await (page as any).tap(selector, options);
};
(frame as any).clear = async (selector: string, options?: any) => { (frame as any).clear = async (selector: string, options?: any) => {
if (!await isSelectorFocused(stealth, page, selector)) { if (!await isSelectorFocused(stealth, page, selector)) {
await (page as any).click(selector); await (page as any).click(selector);
+2 -2
View File
@@ -106,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) {
@@ -181,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,
}); });
+21 -10
View File
@@ -1,6 +1,7 @@
/** /**
* Puppeteer launch wrapper for cloakbrowser. * Puppeteer launch wrapper for cloakbrowser.
* Alternative to the Playwright wrapper for users who prefer Puppeteer. * NOW WITH HUMANIZE SUPPORT humanize: true enables human-like
* mouse curves, keyboard timing, and scroll patterns (same as Playwright).
*/ */
import type { Browser } from "puppeteer-core"; import type { Browser } from "puppeteer-core";
@@ -17,11 +18,12 @@ import { maybeResolveGeoip, resolveWebrtcArgs } from "./geoip.js";
* @example * @example
* ```ts * ```ts
* import { launch } from 'cloakbrowser/puppeteer'; * import { launch } from 'cloakbrowser/puppeteer';
* const browser = await launch(); * * // With humanize — human-like mouse, keyboard, scroll
* const browser = await launch({ humanize: true });
* const page = await browser.newPage(); * const page = await browser.newPage();
* await page.goto('https://bot.incolumitas.com'); * await page.goto('[https://example.com](https://example.com)');
* console.log(await page.title()); * await page.click('#login'); // Bézier curve mouse movement
* await browser.close(); * await page.type('#email', 'user@example.com'); // Per-character timing
* ``` * ```
*/ */
export async function launch(options: LaunchOptions = {}): Promise<Browser> { export async function launch(options: LaunchOptions = {}): Promise<Browser> {
@@ -30,6 +32,7 @@ export async function launch(options: LaunchOptions = {}): Promise<Browser> {
const binaryPath = process.env.CLOAKBROWSER_BINARY_PATH || (await ensureBinary()); const binaryPath = process.env.CLOAKBROWSER_BINARY_PATH || (await ensureBinary());
const { exitIp, ...resolved } = (await maybeResolveGeoip(options)) ?? {}; const { exitIp, ...resolved } = (await maybeResolveGeoip(options)) ?? {};
let resolvedArgs = (await resolveWebrtcArgs(options)) ?? options.args; let resolvedArgs = (await resolveWebrtcArgs(options)) ?? options.args;
if (exitIp && !(resolvedArgs ?? []).some(a => a.startsWith("--fingerprint-webrtc-ip"))) { if (exitIp && !(resolvedArgs ?? []).some(a => a.startsWith("--fingerprint-webrtc-ip"))) {
resolvedArgs = [...(resolvedArgs ?? []), `--fingerprint-webrtc-ip=${exitIp}`]; resolvedArgs = [...(resolvedArgs ?? []), `--fingerprint-webrtc-ip=${exitIp}`];
} }
@@ -82,10 +85,18 @@ export async function launch(options: LaunchOptions = {}): Promise<Browser> {
}; };
} }
// Human-like behavioral patching — FULL coverage, same as Playwright.
// This enables Bézier mouse movements, organic typing rhythms, and
// natural scrolling to bypass advanced anti-bot detection.
if (options.humanize) {
const { patchBrowser } = await import('./human-puppeteer/index.js');
const { resolveConfig } = await import('./human/config.js');
const cfg = resolveConfig(
(options.humanPreset as any) ?? 'default',
options.humanConfig as any,
);
patchBrowser(browser, cfg);
}
return browser; return browser;
} }
// ---------------------------------------------------------------------------
// Internal
// ---------------------------------------------------------------------------
+1 -1
View File
@@ -36,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();
+83
View File
@@ -451,6 +451,89 @@ describe("module exports", () => {
}); });
}); });
// =========================================================================
// patchBrowser on CDP-connected browser (issue #126)
// =========================================================================
describe("patchBrowser CDP-connected workflow", () => {
it("patches existing pages on a browser with pre-existing contexts", async () => {
const { patchBrowser, resolveConfig } = await import("../src/human/index.js");
// Simulate a CDP-connected browser: it already has contexts and pages
const page = buildMockPage();
const context: any = {
pages: vi.fn(() => [page]),
on: vi.fn(),
newPage: vi.fn(async () => buildMockPage()),
addInitScript: vi.fn(async () => {}),
};
const browser: any = {
contexts: vi.fn(() => [context]),
newContext: vi.fn(async () => context),
newPage: vi.fn(async () => page),
};
const cfg = resolveConfig("default");
patchBrowser(browser, cfg);
// page should now have _original (proof it was patched)
expect((page as any)._original).toBeDefined();
expect((page as any)._original.click).toBeTypeOf("function");
expect((page as any)._original.fill).toBeTypeOf("function");
expect((page as any)._humanCfg).toBe(cfg);
});
it("patched click calls mouse.down (humanized path, not original)", async () => {
const { patchBrowser, resolveConfig } = await import("../src/human/index.js");
let downCalled = false;
const page = buildMockPage();
page.mouse.down = vi.fn(async () => { downCalled = true; });
const context: any = {
pages: vi.fn(() => [page]),
on: vi.fn(),
newPage: vi.fn(async () => buildMockPage()),
addInitScript: vi.fn(async () => {}),
};
const browser: any = {
contexts: vi.fn(() => [context]),
newContext: vi.fn(async () => context),
newPage: vi.fn(async () => page),
};
patchBrowser(browser, resolveConfig("default"));
// Click through the patched method — should go through humanize path
try { await (page as any).click("button"); } catch (_) {}
expect(downCalled).toBe(true);
}, 30000);
it("new contexts created after patchBrowser are also patched", async () => {
const { patchBrowser, resolveConfig } = await import("../src/human/index.js");
const newPage = buildMockPage();
const newContext: any = {
pages: vi.fn(() => [newPage]),
on: vi.fn(),
newPage: vi.fn(async () => buildMockPage()),
addInitScript: vi.fn(async () => {}),
};
const browser: any = {
contexts: vi.fn(() => []),
newContext: vi.fn(async () => newContext),
newPage: vi.fn(async () => newPage),
};
patchBrowser(browser, resolveConfig("default"));
// Create a new context via the patched newContext
const ctx = await browser.newContext();
// Pages in the new context should be patched
expect((newPage as any)._original).toBeDefined();
});
});
// ========================================================================= // =========================================================================
// Test helpers // Test helpers
// ========================================================================= // =========================================================================
+1
View File
@@ -22,6 +22,7 @@ describe("puppeteer launch", () => {
let mockBrowser: any; let mockBrowser: any;
beforeEach(async () => { beforeEach(async () => {
delete process.env.CLOAKBROWSER_BINARY_PATH;
puppeteerMock = await import("puppeteer-core"); puppeteerMock = await import("puppeteer-core");
mockBrowser = { mockBrowser = {
newPage: vi.fn().mockResolvedValue({ newPage: vi.fn().mockResolvedValue({
File diff suppressed because it is too large Load Diff
+103 -4
View File
@@ -831,6 +831,101 @@ describe("frame patching with stealth", () => {
}); });
// =========================================================================
// Page-level: pressSequentially, tap, clear are patched
// =========================================================================
describe("page-level pressSequentially, tap, clear patches", () => {
it("page.pressSequentially is replaced after patchPage", async () => {
const { patchPage } = await import("../src/human/index.js");
const page = buildMockPage();
const originalPressSeq = page.pressSequentially ?? (() => {});
const cfg = resolveConfig("default");
const cursor = { x: 0, y: 0, initialized: false };
patchPage(page as any, cfg, cursor as any);
expect(typeof (page as any).pressSequentially).toBe("function");
expect((page as any).pressSequentially).not.toBe(originalPressSeq);
});
it("page.tap is replaced after patchPage", async () => {
const { patchPage } = await import("../src/human/index.js");
const page = buildMockPage();
const originalTap = page.tap ?? (() => {});
const cfg = resolveConfig("default");
const cursor = { x: 0, y: 0, initialized: false };
patchPage(page as any, cfg, cursor as any);
expect(typeof (page as any).tap).toBe("function");
expect((page as any).tap).not.toBe(originalTap);
});
it("page.clear is replaced after patchPage", async () => {
const { patchPage } = await import("../src/human/index.js");
const page = buildMockPage();
const originalClear = page.clear ?? (() => {});
const cfg = resolveConfig("default");
const cursor = { x: 0, y: 0, initialized: false };
patchPage(page as any, cfg, cursor as any);
expect(typeof (page as any).clear).toBe("function");
expect((page as any).clear).not.toBe(originalClear);
});
});
// =========================================================================
// Frame-level: pressSequentially, tap are patched
// =========================================================================
describe("frame-level pressSequentially, tap patches", () => {
it("child frame has pressSequentially patched", async () => {
const { patchPage } = await import("../src/human/index.js");
const childFrame: any = {
click: vi.fn(async () => {}),
dblclick: vi.fn(async () => {}),
hover: vi.fn(async () => {}),
type: vi.fn(async () => {}),
fill: vi.fn(async () => {}),
check: vi.fn(async () => {}),
uncheck: vi.fn(async () => {}),
selectOption: vi.fn(async () => {}),
press: vi.fn(async () => {}),
pressSequentially: vi.fn(async () => {}),
tap: vi.fn(async () => {}),
clear: vi.fn(async () => {}),
dragAndDrop: vi.fn(async () => {}),
locator: vi.fn(() => ({
boundingBox: vi.fn(async () => ({ x: 0, y: 0, width: 100, height: 30 })),
})),
childFrames: vi.fn(() => []),
};
const origPressSeq = childFrame.pressSequentially;
const origTap = childFrame.tap;
const mainFrame = {
...childFrame,
childFrames: vi.fn(() => [childFrame]),
};
const page = buildMockPage({ mainFrameReturn: mainFrame });
const cfg = resolveConfig("default");
const cursor = { x: 0, y: 0, initialized: false };
patchPage(page as any, cfg, cursor as any);
expect((childFrame as any)._humanPatched).toBe(true);
// pressSequentially and tap should be replaced with humanized versions
expect(childFrame.pressSequentially).not.toBe(origPressSeq);
expect(childFrame.tap).not.toBe(origTap);
expect(typeof childFrame.pressSequentially).toBe("function");
expect(typeof childFrame.tap).toBe("function");
});
});
// ========================================================================= // =========================================================================
// Non-ASCII text does NOT go through CDP shift symbol path // Non-ASCII text does NOT go through CDP shift symbol path
// ========================================================================= // =========================================================================
@@ -898,7 +993,8 @@ describeIfSlow("stealth browser: no evaluate leak on click", () => {
it("click() does not trigger querySelector from evaluate context", async () => { it("click() does not trigger querySelector from evaluate context", async () => {
const { launch } = await import("../src/index.js"); const { launch } = await import("../src/index.js");
const browser = await launch({ headless: true, args: ['--humanize'] }); const browser = await launch({ headless: true, humanize: true });
const page = await browser.newPage(); const page = await browser.newPage();
await page.goto('https://www.wikipedia.org', { waitUntil: 'domcontentloaded' }); await page.goto('https://www.wikipedia.org', { waitUntil: 'domcontentloaded' });
@@ -932,7 +1028,8 @@ describeIfSlow("stealth browser: shift symbols isTrusted=true", () => {
it("'!' produces isTrusted=true keydown, not isTrusted=false", async () => { it("'!' produces isTrusted=true keydown, not isTrusted=false", async () => {
const { launch } = await import("../src/index.js"); const { launch } = await import("../src/index.js");
const browser = await launch({ headless: true, args: ['--humanize'] }); const browser = await launch({ headless: true, humanize: true });
const page = await browser.newPage(); const page = await browser.newPage();
await page.goto('https://www.wikipedia.org', { waitUntil: 'domcontentloaded' }); await page.goto('https://www.wikipedia.org', { waitUntil: 'domcontentloaded' });
@@ -972,7 +1069,8 @@ describeIfSlow("stealth browser: navigation invalidation", () => {
it("click works after navigation (isolated world re-created)", async () => { it("click works after navigation (isolated world re-created)", async () => {
const { launch } = await import("../src/index.js"); const { launch } = await import("../src/index.js");
const browser = await launch({ headless: true, args: ['--humanize'] }); const browser = await launch({ headless: true, humanize: true });
const page = await browser.newPage(); const page = await browser.newPage();
expect((page as any)._stealth).toBeDefined(); expect((page as any)._stealth).toBeDefined();
@@ -1003,7 +1101,8 @@ describeIfSlow("stealth browser: full form no evaluate leak", () => {
it("form with shift symbols has zero evaluate leaks and zero untrusted events", async () => { it("form with shift symbols has zero evaluate leaks and zero untrusted events", async () => {
const { launch } = await import("../src/index.js"); const { launch } = await import("../src/index.js");
const browser = await launch({ headless: true, args: ['--humanize'] }); const browser = await launch({ headless: true, humanize: true });
const page = await browser.newPage(); const page = await browser.newPage();
await page.goto( await page.goto(
+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)