mirror of
https://github.com/CloakHQ/CloakBrowser.git
synced 2026-06-23 11:41:46 +02:00
feat: add JavaScript/TypeScript wrapper with Playwright + Puppeteer support
Adds js/ package mirroring the Python wrapper architecture: - Dual API: import from 'cloakbrowser' (Playwright) or 'cloakbrowser/puppeteer' - TypeScript with full type definitions - Same binary download/cache logic, same stealth args, same env vars - Optional peer deps: users install only the runtime they need - Full 6-site stealth test suite (sannysoft, incolumitas, BrowserScan, deviceandbrowserinfo, FingerprintJS, reCAPTCHA v3) - Published to npm as cloakbrowser@0.1.2
This commit is contained in:
@@ -37,6 +37,10 @@ htmlcov/
|
|||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
.claude/
|
.claude/
|
||||||
|
|
||||||
|
# JavaScript / Node.js
|
||||||
|
js/node_modules/
|
||||||
|
js/dist/
|
||||||
|
|
||||||
# Distribution
|
# Distribution
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
*.whl
|
*.whl
|
||||||
@@ -48,4 +52,5 @@ docs/
|
|||||||
|
|
||||||
# Release scripts
|
# Release scripts
|
||||||
publish.sh
|
publish.sh
|
||||||
|
deploy.sh
|
||||||
.env
|
.env
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
# CloakBrowser
|
# CloakBrowser
|
||||||
|
|
||||||
[](https://pypi.org/project/cloakbrowser/)
|
[](https://pypi.org/project/cloakbrowser/)
|
||||||
|
[](https://www.npmjs.com/package/cloakbrowser)
|
||||||
[](https://pypi.org/project/cloakbrowser/)
|
[](https://pypi.org/project/cloakbrowser/)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
[](https://github.com/CloakHQ/CloakBrowser)
|
[](https://github.com/CloakHQ/CloakBrowser)
|
||||||
@@ -12,15 +13,16 @@
|
|||||||
|
|
||||||
**Stealth Chromium that passes every bot detection test.**
|
**Stealth Chromium that passes every bot detection test.**
|
||||||
|
|
||||||
Drop-in Playwright replacement. Same API, same code — just swap the import. Your browser now scores **0.9 on reCAPTCHA v3**, passes **Cloudflare Turnstile**, and clears **14 out of 14** stealth detection tests.
|
Drop-in Playwright/Puppeteer replacement for Python and JavaScript. Same API, same code — just swap the import. Your browser now scores **0.9 on reCAPTCHA v3**, passes **Cloudflare Turnstile**, and clears **14 out of 14** stealth detection tests.
|
||||||
|
|
||||||
- 🔒 **16 source-level C++ patches** — not JS injection, not config flags
|
- 🔒 **16 source-level C++ patches** — not JS injection, not config flags
|
||||||
- 🎯 **0.9 reCAPTCHA v3 score** — human-level, server-verified
|
- 🎯 **0.9 reCAPTCHA v3 score** — human-level, server-verified
|
||||||
- ☁️ **Passes Cloudflare Turnstile**, FingerprintJS, BrowserScan — 14/14 tests
|
- ☁️ **Passes Cloudflare Turnstile**, FingerprintJS, BrowserScan — 14/14 tests
|
||||||
- 🔄 **Drop-in Playwright replacement** — same API, swap one import
|
- 🔄 **Drop-in replacement** — works with Playwright (Python & JS) and Puppeteer (JS)
|
||||||
- 📦 **`pip install cloakbrowser`** — binary auto-downloads, zero config
|
- 📦 **`pip install cloakbrowser`** or **`npm install cloakbrowser`** — binary auto-downloads, zero config
|
||||||
- 🦊 **Fills the Camoufox vacuum** — Chromium-based, actively maintained
|
- 🦊 **Fills the Camoufox vacuum** — Chromium-based, actively maintained
|
||||||
|
|
||||||
|
**Python:**
|
||||||
```python
|
```python
|
||||||
from cloakbrowser import launch
|
from cloakbrowser import launch
|
||||||
|
|
||||||
@@ -30,12 +32,42 @@ page.goto("https://protected-site.com") # no more blocks
|
|||||||
browser.close()
|
browser.close()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**JavaScript (Playwright):**
|
||||||
|
```javascript
|
||||||
|
import { launch } from 'cloakbrowser';
|
||||||
|
|
||||||
|
const browser = await launch();
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.goto('https://protected-site.com');
|
||||||
|
await browser.close();
|
||||||
|
```
|
||||||
|
|
||||||
|
**JavaScript (Puppeteer):**
|
||||||
|
```javascript
|
||||||
|
import { launch } from 'cloakbrowser/puppeteer';
|
||||||
|
|
||||||
|
const browser = await launch();
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.goto('https://protected-site.com');
|
||||||
|
await browser.close();
|
||||||
|
```
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
|
**Python:**
|
||||||
```bash
|
```bash
|
||||||
pip install cloakbrowser
|
pip install cloakbrowser
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**JavaScript / Node.js:**
|
||||||
|
```bash
|
||||||
|
# With Playwright
|
||||||
|
npm install cloakbrowser playwright-core
|
||||||
|
|
||||||
|
# With Puppeteer
|
||||||
|
npm install cloakbrowser puppeteer-core
|
||||||
|
```
|
||||||
|
|
||||||
On first run, the stealth Chromium binary is automatically downloaded (~200MB, cached locally).
|
On first run, the stealth Chromium binary is automatically downloaded (~200MB, cached locally).
|
||||||
|
|
||||||
## Why CloakBrowser?
|
## Why CloakBrowser?
|
||||||
@@ -91,12 +123,12 @@ All tests verified against live detection services. Last tested: Feb 2026 (Chrom
|
|||||||
|
|
||||||
## How It Works
|
## How It Works
|
||||||
|
|
||||||
CloakBrowser is a thin Python wrapper 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`
|
1. **You install** → `pip install cloakbrowser` or `npm install cloakbrowser`
|
||||||
2. **First launch** → binary auto-downloads for your platform (Linux x64 / macOS arm64)
|
2. **First launch** → binary auto-downloads for your platform (Linux x64 / macOS arm64)
|
||||||
3. **Every launch** → Playwright starts with our binary + stealth args
|
3. **Every launch** → Playwright or Puppeteer starts with our binary + stealth args
|
||||||
4. **You write code** → standard Playwright API, nothing new to learn
|
4. **You write code** → standard Playwright/Puppeteer API, nothing new to learn
|
||||||
|
|
||||||
The binary includes 16 source-level patches that modify:
|
The binary includes 16 source-level patches that modify:
|
||||||
- Canvas fingerprint generation
|
- Canvas fingerprint generation
|
||||||
@@ -185,6 +217,63 @@ clear_cache()
|
|||||||
ensure_binary()
|
ensure_binary()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## JavaScript / Node.js API
|
||||||
|
|
||||||
|
CloakBrowser ships a TypeScript package with full type definitions. Choose Playwright or Puppeteer — same stealth binary underneath.
|
||||||
|
|
||||||
|
### Playwright (default)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { launch, launchContext } from 'cloakbrowser';
|
||||||
|
|
||||||
|
// Basic
|
||||||
|
const browser = await launch();
|
||||||
|
|
||||||
|
// With options
|
||||||
|
const browser = await launch({
|
||||||
|
headless: false,
|
||||||
|
proxy: 'http://user:pass@proxy:8080',
|
||||||
|
args: ['--window-size=1920,1080'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convenience: browser + context in one call
|
||||||
|
const context = await launchContext({
|
||||||
|
userAgent: 'Custom UA',
|
||||||
|
viewport: { width: 1920, height: 1080 },
|
||||||
|
locale: 'en-US',
|
||||||
|
timezoneId: 'America/New_York',
|
||||||
|
});
|
||||||
|
const page = await context.newPage();
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note:** Each example above is standalone — not meant to run as one block.
|
||||||
|
|
||||||
|
### Puppeteer
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { launch } from 'cloakbrowser/puppeteer';
|
||||||
|
|
||||||
|
const browser = await launch({ headless: true });
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.goto('https://example.com');
|
||||||
|
await browser.close();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Utility Functions (JS)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { ensureBinary, clearCache, binaryInfo } from 'cloakbrowser';
|
||||||
|
|
||||||
|
// Pre-download binary (e.g., during Docker build)
|
||||||
|
await ensureBinary();
|
||||||
|
|
||||||
|
// Check installation status
|
||||||
|
console.log(binaryInfo());
|
||||||
|
|
||||||
|
// Force re-download
|
||||||
|
clearCache();
|
||||||
|
```
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
| Env Variable | Default | Description |
|
| Env Variable | Default | Description |
|
||||||
@@ -232,11 +321,16 @@ page.goto("https://example.com")
|
|||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
See the [`examples/`](examples/) directory:
|
**Python** — see [`examples/`](examples/):
|
||||||
- [`basic.py`](examples/basic.py) — Launch and load a page
|
- [`basic.py`](examples/basic.py) — Launch and load a page
|
||||||
- [`recaptcha_score.py`](examples/recaptcha_score.py) — Check your reCAPTCHA v3 score
|
- [`recaptcha_score.py`](examples/recaptcha_score.py) — Check your reCAPTCHA v3 score
|
||||||
- [`stealth_test.py`](examples/stealth_test.py) — Run against all detection services
|
- [`stealth_test.py`](examples/stealth_test.py) — Run against all detection services
|
||||||
|
|
||||||
|
**JavaScript** — see [`js/examples/`](js/examples/):
|
||||||
|
- [`basic-playwright.ts`](js/examples/basic-playwright.ts) — Playwright launch and load
|
||||||
|
- [`basic-puppeteer.ts`](js/examples/basic-puppeteer.ts) — Puppeteer launch and load
|
||||||
|
- [`stealth-test.ts`](js/examples/stealth-test.ts) — Full 6-site detection test suite
|
||||||
|
|
||||||
## Roadmap
|
## Roadmap
|
||||||
|
|
||||||
| Feature | Status |
|
| Feature | Status |
|
||||||
@@ -244,7 +338,7 @@ See the [`examples/`](examples/) directory:
|
|||||||
| Linux x64 binary | ✅ Released |
|
| Linux x64 binary | ✅ Released |
|
||||||
| macOS arm64 (Apple Silicon) | 🔜 In progress |
|
| macOS arm64 (Apple Silicon) | 🔜 In progress |
|
||||||
| Chromium 145 build | 🔜 In progress |
|
| Chromium 145 build | 🔜 In progress |
|
||||||
| JavaScript/Puppeteer support (`cloakbrowser-js`) | 📋 Planned |
|
| JavaScript/Puppeteer + Playwright support | ✅ Released |
|
||||||
| Fingerprint rotation per session | 📋 Planned |
|
| Fingerprint rotation per session | 📋 Planned |
|
||||||
| Built-in proxy rotation | 📋 Planned |
|
| Built-in proxy rotation | 📋 Planned |
|
||||||
| Windows support | 📋 Planned |
|
| Windows support | 📋 Planned |
|
||||||
|
|||||||
+153
@@ -0,0 +1,153 @@
|
|||||||
|
<p align="center">
|
||||||
|
<img src="https://raw.githubusercontent.com/CloakHQ/CloakBrowser/main/images/logo.png" width="500" alt="CloakBrowser">
|
||||||
|
</p>
|
||||||
|
|
||||||
|
# CloakBrowser
|
||||||
|
|
||||||
|
[](https://www.npmjs.com/package/cloakbrowser)
|
||||||
|
[](https://github.com/CloakHQ/CloakBrowser/blob/main/LICENSE)
|
||||||
|
|
||||||
|
**Stealth Chromium that passes every bot detection test.**
|
||||||
|
|
||||||
|
Drop-in Playwright/Puppeteer replacement. Same API — just swap the import. Scores **0.9 on reCAPTCHA v3**, passes **Cloudflare Turnstile**, and clears **14/14** stealth detection tests.
|
||||||
|
|
||||||
|
- 🔒 **16 source-level C++ patches** — not JS injection, not config flags
|
||||||
|
- 🎯 **0.9 reCAPTCHA v3 score** — human-level, server-verified
|
||||||
|
- ☁️ **Passes Cloudflare Turnstile**, FingerprintJS, BrowserScan — 14/14 tests
|
||||||
|
- 🔄 **Drop-in replacement** — works with both Playwright and Puppeteer
|
||||||
|
- 📦 **`npm install cloakbrowser`** — binary auto-downloads, zero config
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# With Playwright
|
||||||
|
npm install cloakbrowser playwright-core
|
||||||
|
|
||||||
|
# With Puppeteer
|
||||||
|
npm install cloakbrowser puppeteer-core
|
||||||
|
```
|
||||||
|
|
||||||
|
On first launch, the stealth Chromium binary auto-downloads (~200MB, cached at `~/.cloakbrowser/`).
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Playwright (default)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { launch } from 'cloakbrowser';
|
||||||
|
|
||||||
|
const browser = await launch();
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.goto('https://protected-site.com');
|
||||||
|
console.log(await page.title());
|
||||||
|
await browser.close();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Puppeteer
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { launch } from 'cloakbrowser/puppeteer';
|
||||||
|
|
||||||
|
const browser = await launch();
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.goto('https://protected-site.com');
|
||||||
|
console.log(await page.title());
|
||||||
|
await browser.close();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { launch, launchContext } from 'cloakbrowser';
|
||||||
|
|
||||||
|
// With proxy
|
||||||
|
const browser = await launch({
|
||||||
|
proxy: 'http://user:pass@proxy:8080',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Headed mode (visible browser window)
|
||||||
|
const browser = await launch({ headless: false });
|
||||||
|
|
||||||
|
// Extra Chrome args
|
||||||
|
const browser = await launch({
|
||||||
|
args: ['--window-size=1920,1080'],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Browser + context in one call
|
||||||
|
const context = await launchContext({
|
||||||
|
userAgent: 'Custom UA',
|
||||||
|
viewport: { width: 1920, height: 1080 },
|
||||||
|
locale: 'en-US',
|
||||||
|
timezoneId: 'America/New_York',
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Utilities
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { ensureBinary, clearCache, binaryInfo } from 'cloakbrowser';
|
||||||
|
|
||||||
|
// Pre-download binary (e.g., during Docker build)
|
||||||
|
await ensureBinary();
|
||||||
|
|
||||||
|
// Check installation
|
||||||
|
console.log(binaryInfo());
|
||||||
|
|
||||||
|
// Force re-download
|
||||||
|
clearCache();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Results
|
||||||
|
|
||||||
|
| Detection Service | Stock Browser | CloakBrowser |
|
||||||
|
|---|---|---|
|
||||||
|
| **reCAPTCHA v3** | 0.1 (bot) | **0.9** (human) |
|
||||||
|
| **Cloudflare Turnstile** | FAIL | **PASS** |
|
||||||
|
| **FingerprintJS** | DETECTED | **PASS** |
|
||||||
|
| **BrowserScan** | DETECTED | **NORMAL** (4/4) |
|
||||||
|
| **bot.incolumitas.com** | 13 fails | **1 fail** |
|
||||||
|
| `navigator.webdriver` | `true` | **`false`** |
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
| Env Variable | Default | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `CLOAKBROWSER_BINARY_PATH` | — | Skip download, use a local Chromium binary |
|
||||||
|
| `CLOAKBROWSER_CACHE_DIR` | `~/.cloakbrowser` | Binary cache directory |
|
||||||
|
| `CLOAKBROWSER_DOWNLOAD_URL` | GitHub Releases | Custom download URL |
|
||||||
|
|
||||||
|
## Migrate From Playwright
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- import { chromium } from 'playwright';
|
||||||
|
- const browser = await chromium.launch();
|
||||||
|
+ import { launch } from 'cloakbrowser';
|
||||||
|
+ const browser = await launch();
|
||||||
|
|
||||||
|
const page = await browser.newPage();
|
||||||
|
// ... rest of your code works unchanged
|
||||||
|
```
|
||||||
|
|
||||||
|
## Platforms
|
||||||
|
|
||||||
|
| Platform | Status |
|
||||||
|
|---|---|
|
||||||
|
| Linux x86_64 | ✅ Supported |
|
||||||
|
| macOS arm64 (Apple Silicon) | Coming soon |
|
||||||
|
| macOS x86_64 (Intel) | Coming soon |
|
||||||
|
| Windows | Planned |
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Node.js >= 18
|
||||||
|
- One of: `playwright-core` >= 1.40 or `puppeteer-core` >= 21
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT — see [LICENSE](https://github.com/CloakHQ/CloakBrowser/blob/main/LICENSE).
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- [GitHub](https://github.com/CloakHQ/CloakBrowser)
|
||||||
|
- [PyPI (Python package)](https://pypi.org/project/cloakbrowser/)
|
||||||
|
- [Full documentation](https://github.com/CloakHQ/CloakBrowser#readme)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* Basic CloakBrowser example using Playwright API.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* CLOAKBROWSER_BINARY_PATH=/path/to/chrome npx tsx examples/basic-playwright.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { launch } from "../src/index.js";
|
||||||
|
|
||||||
|
const browser = await launch({ headless: true });
|
||||||
|
const page = await browser.newPage();
|
||||||
|
|
||||||
|
await page.goto("https://example.com");
|
||||||
|
console.log(`Title: ${await page.title()}`);
|
||||||
|
console.log(`URL: ${page.url()}`);
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
console.log("Done.");
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* Basic CloakBrowser example using Puppeteer API.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* CLOAKBROWSER_BINARY_PATH=/path/to/chrome npx tsx examples/basic-puppeteer.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { launch } from "../src/puppeteer.js";
|
||||||
|
|
||||||
|
const browser = await launch({ headless: true });
|
||||||
|
const page = await browser.newPage();
|
||||||
|
|
||||||
|
await page.goto("https://example.com");
|
||||||
|
console.log(`Title: ${await page.title()}`);
|
||||||
|
console.log(`URL: ${page.url()}`);
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
console.log("Done.");
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
/**
|
||||||
|
* Full stealth test suite — validates CloakBrowser against live detection services.
|
||||||
|
* Mirrors Python examples/stealth_test.py.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* CLOAKBROWSER_BINARY_PATH=/path/to/chrome npx tsx examples/stealth-test.ts
|
||||||
|
* CLOAKBROWSER_BINARY_PATH=/path/to/chrome npx tsx examples/stealth-test.ts --proxy http://10.50.96.5:8888
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { launch } from "../src/index.js";
|
||||||
|
|
||||||
|
const PROXY = process.argv.includes("--proxy")
|
||||||
|
? process.argv[process.argv.indexOf("--proxy") + 1]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
interface TestResult {
|
||||||
|
name: string;
|
||||||
|
status: "PASS" | "FAIL" | "ERROR";
|
||||||
|
verdict: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const results: TestResult[] = [];
|
||||||
|
|
||||||
|
console.log("=".repeat(60));
|
||||||
|
console.log("CloakBrowser JS — Stealth Test Suite");
|
||||||
|
console.log("=".repeat(60));
|
||||||
|
console.log(`Proxy: ${PROXY || "none"}\n`);
|
||||||
|
|
||||||
|
const browser = await launch({ headless: true, proxy: PROXY });
|
||||||
|
const page = await browser.newPage();
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Test 1: bot.sannysoft.com
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
async function testSannysoft() {
|
||||||
|
console.log("--- bot.sannysoft.com ---");
|
||||||
|
await page.goto("https://bot.sannysoft.com", {
|
||||||
|
waitUntil: "networkidle",
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
await page.waitForTimeout(3000);
|
||||||
|
|
||||||
|
const result = await page.evaluate(() => {
|
||||||
|
const rows = document.querySelectorAll("table tr");
|
||||||
|
let passed = 0;
|
||||||
|
let total = 0;
|
||||||
|
const failed: string[] = [];
|
||||||
|
rows.forEach((r) => {
|
||||||
|
const cells = r.querySelectorAll("td");
|
||||||
|
if (cells.length >= 2) {
|
||||||
|
total++;
|
||||||
|
const key = cells[0]!.innerText.trim();
|
||||||
|
const cls = cells[1]!.className || "";
|
||||||
|
if (cls.includes("failed")) {
|
||||||
|
failed.push(key);
|
||||||
|
} else {
|
||||||
|
passed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { passed, total, failed };
|
||||||
|
});
|
||||||
|
|
||||||
|
const verdict =
|
||||||
|
result.failed.length === 0
|
||||||
|
? `${result.passed}/${result.total} — ALL GREEN`
|
||||||
|
: `${result.passed}/${result.total} (FAILED: ${result.failed.join(", ")})`;
|
||||||
|
const status = result.failed.length === 0 ? "PASS" : "FAIL";
|
||||||
|
console.log(`Result: [${status}] ${verdict}\n`);
|
||||||
|
results.push({ name: "bot.sannysoft.com", status, verdict });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Test 2: bot.incolumitas.com
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
async function testIncolumitas() {
|
||||||
|
console.log("--- bot.incolumitas.com ---");
|
||||||
|
await page.goto("https://bot.incolumitas.com", {
|
||||||
|
waitUntil: "networkidle",
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
await page.waitForTimeout(12000); // needs time for all detection tests
|
||||||
|
|
||||||
|
const result = await page.evaluate(() => {
|
||||||
|
const text = document.body.innerText;
|
||||||
|
const okMatches = text.match(/"(\w+)":\s*"OK"/g) || [];
|
||||||
|
const failMatches = text.match(/"(\w+)":\s*"FAIL"/g) || [];
|
||||||
|
const failedTests = failMatches.map((m) => {
|
||||||
|
const match = m.match(/"(\w+)"/);
|
||||||
|
return match ? match[1] : m;
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
passed: okMatches.length,
|
||||||
|
failed: failMatches.length,
|
||||||
|
failedTests,
|
||||||
|
total: okMatches.length + failMatches.length,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const verdict =
|
||||||
|
result.failed === 0
|
||||||
|
? `${result.passed}/${result.total} — ALL GREEN`
|
||||||
|
: `${result.passed}/${result.total} (FAILED: ${result.failedTests.join(", ")})`;
|
||||||
|
// WEBDRIVER false positive is expected
|
||||||
|
const status = result.failed <= 1 ? "PASS" : "FAIL";
|
||||||
|
console.log(`Result: [${status}] ${verdict}\n`);
|
||||||
|
results.push({ name: "bot.incolumitas.com", status, verdict });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Test 3: BrowserScan
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
async function testBrowserScan() {
|
||||||
|
console.log("--- BrowserScan ---");
|
||||||
|
await page.goto("https://www.browserscan.net/bot-detection", {
|
||||||
|
waitUntil: "networkidle",
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
|
||||||
|
const result = await page.evaluate(() => {
|
||||||
|
const text = document.body.innerText;
|
||||||
|
const normalMatches = text.match(/Normal/g);
|
||||||
|
const abnormalMatches = text.match(/Abnormal/g);
|
||||||
|
return {
|
||||||
|
normal: normalMatches ? normalMatches.length : 0,
|
||||||
|
abnormal: abnormalMatches ? abnormalMatches.length : 0,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const verdict = `Normal: ${result.normal}, Abnormal: ${result.abnormal}`;
|
||||||
|
const status = result.abnormal === 0 ? "PASS" : "FAIL";
|
||||||
|
console.log(`Result: [${status}] ${verdict}\n`);
|
||||||
|
results.push({ name: "BrowserScan", status, verdict });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Test 4: deviceandbrowserinfo.com
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
async function testDeviceAndBrowserInfo() {
|
||||||
|
console.log("--- deviceandbrowserinfo.com ---");
|
||||||
|
await page.goto("https://deviceandbrowserinfo.com/are_you_a_bot", {
|
||||||
|
waitUntil: "domcontentloaded",
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
await page.waitForTimeout(8000);
|
||||||
|
|
||||||
|
const result = await page.evaluate(() => {
|
||||||
|
const text = document.body.innerText;
|
||||||
|
const botMatch = text.match(/"isBot":\s*(true|false)/);
|
||||||
|
const isBot = botMatch ? botMatch[1] === "true" : null;
|
||||||
|
const checks: Record<string, boolean> = {};
|
||||||
|
const patterns = [
|
||||||
|
"isBot",
|
||||||
|
"hasBotUserAgent",
|
||||||
|
"hasWebdriverTrue",
|
||||||
|
"isHeadlessChrome",
|
||||||
|
"isAutomatedWithCDP",
|
||||||
|
"hasSuspiciousWeakSignals",
|
||||||
|
"isPlaywright",
|
||||||
|
"hasInconsistentChromeObject",
|
||||||
|
];
|
||||||
|
patterns.forEach((p) => {
|
||||||
|
const match = text.match(new RegExp('"' + p + '":\\s*(true|false)'));
|
||||||
|
if (match) checks[p] = match[1] === "true";
|
||||||
|
});
|
||||||
|
return { isBot, checks };
|
||||||
|
});
|
||||||
|
|
||||||
|
const trueFlags = Object.entries(result.checks)
|
||||||
|
.filter(([, v]) => v)
|
||||||
|
.map(([k]) => k);
|
||||||
|
const verdict =
|
||||||
|
`isBot: ${result.isBot}` +
|
||||||
|
(trueFlags.length > 0 ? ` (flagged: ${trueFlags.join(", ")})` : " — all clear");
|
||||||
|
const status = !result.isBot ? "PASS" : "FAIL";
|
||||||
|
console.log(`Result: [${status}] ${verdict}\n`);
|
||||||
|
results.push({ name: "deviceandbrowserinfo.com", status, verdict });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Test 5: FingerprintJS
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
async function testFingerprintJS() {
|
||||||
|
console.log("--- FingerprintJS ---");
|
||||||
|
await page.goto("https://demo.fingerprint.com/web-scraping", {
|
||||||
|
waitUntil: "networkidle",
|
||||||
|
timeout: 30000,
|
||||||
|
});
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await page.click("button:has-text('Search')", { timeout: 5000 });
|
||||||
|
await page.waitForTimeout(5000);
|
||||||
|
} catch {
|
||||||
|
// Search button may not be present
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await page.evaluate(() => {
|
||||||
|
const text = document.body.innerText;
|
||||||
|
const hasFlights =
|
||||||
|
text.includes("Price per adult") || text.includes("$");
|
||||||
|
const isBlocked =
|
||||||
|
text.includes("request was blocked") ||
|
||||||
|
text.includes("bot visit detected");
|
||||||
|
return { passed: hasFlights && !isBlocked, isBlocked, hasFlights };
|
||||||
|
});
|
||||||
|
|
||||||
|
const verdict = result.passed
|
||||||
|
? "PASSED (flights shown)"
|
||||||
|
: result.isBlocked
|
||||||
|
? "BLOCKED"
|
||||||
|
: "NO FLIGHTS";
|
||||||
|
const status = result.passed ? "PASS" : "FAIL";
|
||||||
|
console.log(`Result: [${status}] ${verdict}\n`);
|
||||||
|
results.push({ name: "FingerprintJS", status, verdict });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Test 6: reCAPTCHA v3
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
async function testRecaptcha() {
|
||||||
|
console.log("--- reCAPTCHA v3 (Google) ---");
|
||||||
|
await page.goto(
|
||||||
|
"https://recaptcha-demo.appspot.com/recaptcha-v3-request-scores.php",
|
||||||
|
{ waitUntil: "networkidle", timeout: 30000 }
|
||||||
|
);
|
||||||
|
await page.waitForTimeout(8000);
|
||||||
|
|
||||||
|
const result = await page.evaluate(() => {
|
||||||
|
const text = document.body.innerText;
|
||||||
|
const scoreMatch = text.match(/"score":\s*(\d+\.\d+)/);
|
||||||
|
return {
|
||||||
|
score: scoreMatch ? parseFloat(scoreMatch[1]) : null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const verdict = `Score: ${result.score ?? "N/A"}`;
|
||||||
|
const status = (result.score ?? 0) >= 0.7 ? "PASS" : "FAIL";
|
||||||
|
console.log(`Result: [${status}] ${verdict}\n`);
|
||||||
|
results.push({ name: "reCAPTCHA v3", status, verdict });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Run all tests
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const tests = [
|
||||||
|
testSannysoft,
|
||||||
|
testIncolumitas,
|
||||||
|
testBrowserScan,
|
||||||
|
testDeviceAndBrowserInfo,
|
||||||
|
testFingerprintJS,
|
||||||
|
testRecaptcha,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const test of tests) {
|
||||||
|
try {
|
||||||
|
await test();
|
||||||
|
} catch (err) {
|
||||||
|
const name = test.name.replace("test", "");
|
||||||
|
console.log(`Error: ${err}\n`);
|
||||||
|
results.push({ name, status: "ERROR", verdict: String(err) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
console.log("=".repeat(60));
|
||||||
|
console.log("RESULTS SUMMARY");
|
||||||
|
console.log("=".repeat(60));
|
||||||
|
for (const r of results) {
|
||||||
|
const icon = { PASS: "+", FAIL: "!", ERROR: "x" }[r.status];
|
||||||
|
console.log(` [${icon}] ${r.name}: ${r.verdict}`);
|
||||||
|
}
|
||||||
|
const passedCount = results.filter((r) => r.status === "PASS").length;
|
||||||
|
console.log(`\n ${passedCount}/${results.length} tests passed`);
|
||||||
|
console.log("=".repeat(60));
|
||||||
|
|
||||||
|
process.exit(passedCount === results.length ? 0 : 1);
|
||||||
Generated
+2924
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,72 @@
|
|||||||
|
{
|
||||||
|
"name": "cloakbrowser",
|
||||||
|
"version": "0.1.2",
|
||||||
|
"description": "Stealth Chromium that passes every bot detection test. Drop-in Playwright/Puppeteer replacement with source-level fingerprint patches.",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"types": "dist/index.d.ts",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js"
|
||||||
|
},
|
||||||
|
"./puppeteer": {
|
||||||
|
"types": "./dist/puppeteer.d.ts",
|
||||||
|
"import": "./dist/puppeteer.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"keywords": [
|
||||||
|
"stealth",
|
||||||
|
"browser",
|
||||||
|
"chromium",
|
||||||
|
"playwright",
|
||||||
|
"puppeteer",
|
||||||
|
"scraping",
|
||||||
|
"anti-detect",
|
||||||
|
"bot-detection",
|
||||||
|
"fingerprint",
|
||||||
|
"recaptcha",
|
||||||
|
"cloudflare",
|
||||||
|
"datadome"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/CloakHQ/cloakbrowser",
|
||||||
|
"directory": "js"
|
||||||
|
},
|
||||||
|
"homepage": "https://github.com/CloakHQ/cloakbrowser#javascript--nodejs",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"playwright-core": ">=1.40.0",
|
||||||
|
"puppeteer-core": ">=21.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"playwright-core": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"puppeteer-core": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"tar": "^7.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.10.0",
|
||||||
|
"playwright-core": "^1.40.0",
|
||||||
|
"puppeteer-core": "^21.0.0",
|
||||||
|
"typescript": "^5.3.0",
|
||||||
|
"vitest": "^1.0.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"test": "vitest run"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* Stealth configuration and platform detection for cloakbrowser.
|
||||||
|
* Mirrors Python cloakbrowser/config.py.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Chromium version shipped with this release
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
export const CHROMIUM_VERSION = "142.0.7444.175";
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Platform detection
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const SUPPORTED_PLATFORMS: Record<string, string> = {
|
||||||
|
"linux-x64": "linux-x64",
|
||||||
|
"linux-arm64": "linux-arm64",
|
||||||
|
"darwin-arm64": "darwin-arm64",
|
||||||
|
"darwin-x64": "darwin-x64",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getPlatformTag(): string {
|
||||||
|
const platform = process.platform;
|
||||||
|
const arch = process.arch;
|
||||||
|
|
||||||
|
// Map Node.js platform/arch to our tag format
|
||||||
|
let key: string;
|
||||||
|
if (platform === "linux" && arch === "x64") key = "linux-x64";
|
||||||
|
else if (platform === "linux" && arch === "arm64") key = "linux-arm64";
|
||||||
|
else if (platform === "darwin" && arch === "arm64") key = "darwin-arm64";
|
||||||
|
else if (platform === "darwin" && arch === "x64") key = "darwin-x64";
|
||||||
|
else {
|
||||||
|
const supported = Object.values(SUPPORTED_PLATFORMS).join(", ");
|
||||||
|
throw new Error(
|
||||||
|
`Unsupported platform: ${platform} ${arch}. Supported: ${supported}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SUPPORTED_PLATFORMS[key]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Binary cache paths
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
export function getCacheDir(): string {
|
||||||
|
const custom = process.env.CLOAKBROWSER_CACHE_DIR;
|
||||||
|
if (custom) return custom;
|
||||||
|
return path.join(os.homedir(), ".cloakbrowser");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBinaryDir(): string {
|
||||||
|
return path.join(getCacheDir(), `chromium-${CHROMIUM_VERSION}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getBinaryPath(): string {
|
||||||
|
const binaryDir = getBinaryDir();
|
||||||
|
if (process.platform === "darwin") {
|
||||||
|
return path.join(binaryDir, "Chromium.app", "Contents", "MacOS", "Chromium");
|
||||||
|
}
|
||||||
|
return path.join(binaryDir, "chrome");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Download URL
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
const DOWNLOAD_BASE_URL =
|
||||||
|
process.env.CLOAKBROWSER_DOWNLOAD_URL ||
|
||||||
|
"https://github.com/CloakHQ/chromium-stealth-builds/releases/download";
|
||||||
|
|
||||||
|
export function getDownloadUrl(): string {
|
||||||
|
const tag = getPlatformTag();
|
||||||
|
return `${DOWNLOAD_BASE_URL}/v${CHROMIUM_VERSION}/cloakbrowser-${tag}.tar.gz`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Local binary override
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
export function getLocalBinaryOverride(): string | undefined {
|
||||||
|
return process.env.CLOAKBROWSER_BINARY_PATH || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Default stealth arguments
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
export function getDefaultStealthArgs(): string[] {
|
||||||
|
const seed = Math.floor(Math.random() * 90000) + 10000; // 10000-99999
|
||||||
|
return [
|
||||||
|
"--no-sandbox",
|
||||||
|
"--disable-blink-features=AutomationControlled",
|
||||||
|
`--fingerprint=${seed}`,
|
||||||
|
"--fingerprint-platform=windows",
|
||||||
|
"--fingerprint-hardware-concurrency=8",
|
||||||
|
"--fingerprint-gpu-vendor=NVIDIA Corporation",
|
||||||
|
"--fingerprint-gpu-renderer=NVIDIA GeForce RTX 3070",
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,250 @@
|
|||||||
|
/**
|
||||||
|
* Binary download and cache management for cloakbrowser.
|
||||||
|
* Downloads the patched Chromium binary on first use, caches it locally.
|
||||||
|
* Mirrors Python cloakbrowser/download.py.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { pipeline } from "node:stream/promises";
|
||||||
|
import { createWriteStream } from "node:fs";
|
||||||
|
import { extract as tarExtract } from "tar";
|
||||||
|
|
||||||
|
import type { BinaryInfo } from "./types.js";
|
||||||
|
import {
|
||||||
|
CHROMIUM_VERSION,
|
||||||
|
getBinaryDir,
|
||||||
|
getBinaryPath,
|
||||||
|
getDownloadUrl,
|
||||||
|
getLocalBinaryOverride,
|
||||||
|
getPlatformTag,
|
||||||
|
getCacheDir,
|
||||||
|
} from "./config.js";
|
||||||
|
|
||||||
|
const DOWNLOAD_TIMEOUT_MS = 600_000; // 10 minutes
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Public API
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the stealth Chromium binary is available. Download if needed.
|
||||||
|
* Returns the path to the chrome executable.
|
||||||
|
*/
|
||||||
|
export async function ensureBinary(): Promise<string> {
|
||||||
|
// Check for local override
|
||||||
|
const localOverride = getLocalBinaryOverride();
|
||||||
|
if (localOverride) {
|
||||||
|
if (!fs.existsSync(localOverride)) {
|
||||||
|
throw new Error(
|
||||||
|
`CLOAKBROWSER_BINARY_PATH set to '${localOverride}' but file does not exist`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log(`[cloakbrowser] Using local binary override: ${localOverride}`);
|
||||||
|
return localOverride;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if binary is cached
|
||||||
|
const binaryPath = getBinaryPath();
|
||||||
|
if (fs.existsSync(binaryPath) && isExecutable(binaryPath)) {
|
||||||
|
return binaryPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download
|
||||||
|
console.log(
|
||||||
|
`[cloakbrowser] Stealth Chromium ${CHROMIUM_VERSION} not found. Downloading for ${getPlatformTag()}...`
|
||||||
|
);
|
||||||
|
await downloadAndExtract();
|
||||||
|
|
||||||
|
if (!fs.existsSync(binaryPath)) {
|
||||||
|
throw new Error(
|
||||||
|
`Download completed but binary not found at expected path: ${binaryPath}. ` +
|
||||||
|
`This may indicate a packaging issue. Please report at ` +
|
||||||
|
`https://github.com/CloakHQ/cloakbrowser/issues`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return binaryPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Remove all cached binaries. Forces re-download on next launch. */
|
||||||
|
export function clearCache(): void {
|
||||||
|
const cacheDir = getCacheDir();
|
||||||
|
if (fs.existsSync(cacheDir)) {
|
||||||
|
fs.rmSync(cacheDir, { recursive: true, force: true });
|
||||||
|
console.log(`[cloakbrowser] Cache cleared: ${cacheDir}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Return info about the current binary installation. */
|
||||||
|
export function binaryInfo(): BinaryInfo {
|
||||||
|
const binaryPath = getBinaryPath();
|
||||||
|
return {
|
||||||
|
version: CHROMIUM_VERSION,
|
||||||
|
platform: getPlatformTag(),
|
||||||
|
binaryPath,
|
||||||
|
installed: fs.existsSync(binaryPath),
|
||||||
|
cacheDir: getBinaryDir(),
|
||||||
|
downloadUrl: getDownloadUrl(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Internal helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
async function downloadAndExtract(): Promise<void> {
|
||||||
|
const url = getDownloadUrl();
|
||||||
|
const binaryDir = getBinaryDir();
|
||||||
|
|
||||||
|
// Create cache dir
|
||||||
|
fs.mkdirSync(path.dirname(binaryDir), { recursive: true });
|
||||||
|
|
||||||
|
// Download to temp file (atomic — no partial downloads in cache)
|
||||||
|
const tmpPath = path.join(
|
||||||
|
path.dirname(binaryDir),
|
||||||
|
`_download_${Date.now()}.tar.gz`
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await downloadFile(url, tmpPath);
|
||||||
|
await extractArchive(tmpPath, binaryDir);
|
||||||
|
} finally {
|
||||||
|
// Clean up temp file
|
||||||
|
if (fs.existsSync(tmpPath)) {
|
||||||
|
fs.unlinkSync(tmpPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadFile(url: string, dest: string): Promise<void> {
|
||||||
|
console.log(`[cloakbrowser] Downloading from ${url}`);
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeout = setTimeout(() => controller.abort(), DOWNLOAD_TIMEOUT_MS);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
signal: controller.signal,
|
||||||
|
redirect: "follow",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Download failed: HTTP ${response.status} ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.body) {
|
||||||
|
throw new Error("Download failed: empty response body");
|
||||||
|
}
|
||||||
|
|
||||||
|
const total = Number(response.headers.get("content-length") || 0);
|
||||||
|
let downloaded = 0;
|
||||||
|
let lastLoggedPct = -1;
|
||||||
|
|
||||||
|
const fileStream = createWriteStream(dest);
|
||||||
|
const reader = response.body.getReader();
|
||||||
|
|
||||||
|
// Stream chunks to file with progress logging
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
|
||||||
|
fileStream.write(value);
|
||||||
|
downloaded += value.length;
|
||||||
|
|
||||||
|
if (total > 0) {
|
||||||
|
const pct = Math.floor((downloaded / total) * 100);
|
||||||
|
if (pct >= lastLoggedPct + 10) {
|
||||||
|
lastLoggedPct = pct;
|
||||||
|
const dlMB = Math.floor(downloaded / (1024 * 1024));
|
||||||
|
const totalMB = Math.floor(total / (1024 * 1024));
|
||||||
|
console.log(
|
||||||
|
`[cloakbrowser] Download progress: ${pct}% (${dlMB}/${totalMB} MB)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for file stream to finish
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
fileStream.end(() => resolve());
|
||||||
|
fileStream.on("error", reject);
|
||||||
|
});
|
||||||
|
|
||||||
|
const sizeMB = Math.floor(fs.statSync(dest).size / (1024 * 1024));
|
||||||
|
console.log(`[cloakbrowser] Download complete: ${sizeMB} MB`);
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function extractArchive(
|
||||||
|
archivePath: string,
|
||||||
|
destDir: string
|
||||||
|
): Promise<void> {
|
||||||
|
console.log(`[cloakbrowser] Extracting to ${destDir}`);
|
||||||
|
|
||||||
|
// Clean existing dir if partial download existed
|
||||||
|
if (fs.existsSync(destDir)) {
|
||||||
|
fs.rmSync(destDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
fs.mkdirSync(destDir, { recursive: true });
|
||||||
|
|
||||||
|
// Extract with tar — the 'tar' package handles symlink/traversal safety
|
||||||
|
await tarExtract({
|
||||||
|
file: archivePath,
|
||||||
|
cwd: destDir,
|
||||||
|
// Security: strip leading path components and reject absolute paths
|
||||||
|
strip: 0,
|
||||||
|
filter: (entryPath: string) => {
|
||||||
|
// Reject absolute paths and path traversal
|
||||||
|
if (path.isAbsolute(entryPath) || entryPath.includes("..")) {
|
||||||
|
console.warn(
|
||||||
|
`[cloakbrowser] Skipping suspicious archive entry: ${entryPath}`
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Flatten single subdirectory if needed
|
||||||
|
flattenSingleSubdir(destDir);
|
||||||
|
|
||||||
|
// Make binary executable
|
||||||
|
const binaryPath = getBinaryPath();
|
||||||
|
if (fs.existsSync(binaryPath)) {
|
||||||
|
fs.chmodSync(binaryPath, 0o755);
|
||||||
|
console.log(`[cloakbrowser] Binary ready: ${binaryPath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If extraction created a single subdirectory, move its contents up.
|
||||||
|
* Many tarballs wrap files in a top-level directory.
|
||||||
|
*/
|
||||||
|
function flattenSingleSubdir(destDir: string): void {
|
||||||
|
const entries = fs.readdirSync(destDir);
|
||||||
|
if (entries.length === 1) {
|
||||||
|
const subdir = path.join(destDir, entries[0]!);
|
||||||
|
if (fs.statSync(subdir).isDirectory()) {
|
||||||
|
const children = fs.readdirSync(subdir);
|
||||||
|
for (const child of children) {
|
||||||
|
fs.renameSync(
|
||||||
|
path.join(subdir, child),
|
||||||
|
path.join(destDir, child)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
fs.rmdirSync(subdir);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isExecutable(filePath: string): boolean {
|
||||||
|
try {
|
||||||
|
fs.accessSync(filePath, fs.constants.X_OK);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
/**
|
||||||
|
* CloakBrowser — Stealth Chromium for Node.js
|
||||||
|
*
|
||||||
|
* Default export uses Playwright. For Puppeteer, import from 'cloakbrowser/puppeteer'.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* // Playwright (default)
|
||||||
|
* import { launch } from 'cloakbrowser';
|
||||||
|
* const browser = await launch();
|
||||||
|
*
|
||||||
|
* // Puppeteer
|
||||||
|
* import { launch } from 'cloakbrowser/puppeteer';
|
||||||
|
* const browser = await launch();
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Launch functions (Playwright API)
|
||||||
|
export { launch, launchContext } from "./playwright.js";
|
||||||
|
|
||||||
|
// Binary management
|
||||||
|
export { ensureBinary, clearCache, binaryInfo } from "./download.js";
|
||||||
|
|
||||||
|
// Config
|
||||||
|
export { CHROMIUM_VERSION, getDefaultStealthArgs } from "./config.js";
|
||||||
|
|
||||||
|
// Types
|
||||||
|
export type { LaunchOptions, LaunchContextOptions, BinaryInfo } from "./types.js";
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
/**
|
||||||
|
* Playwright launch wrapper for cloakbrowser.
|
||||||
|
* Mirrors Python cloakbrowser/browser.py.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Browser, BrowserContext } from "playwright-core";
|
||||||
|
import type { LaunchOptions, LaunchContextOptions } from "./types.js";
|
||||||
|
import { getDefaultStealthArgs } from "./config.js";
|
||||||
|
import { ensureBinary } from "./download.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launch stealth Chromium browser via Playwright.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* import { launch } from 'cloakbrowser';
|
||||||
|
* const browser = await launch();
|
||||||
|
* const page = await browser.newPage();
|
||||||
|
* await page.goto('https://bot.incolumitas.com');
|
||||||
|
* console.log(await page.title());
|
||||||
|
* await browser.close();
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export async function launch(options: LaunchOptions = {}): Promise<Browser> {
|
||||||
|
const { chromium } = await import("playwright-core");
|
||||||
|
|
||||||
|
const binaryPath = process.env.CLOAKBROWSER_BINARY_PATH || (await ensureBinary());
|
||||||
|
const args = buildArgs(options);
|
||||||
|
|
||||||
|
const browser = await chromium.launch({
|
||||||
|
executablePath: binaryPath,
|
||||||
|
headless: options.headless ?? true,
|
||||||
|
args,
|
||||||
|
ignoreDefaultArgs: ["--enable-automation"],
|
||||||
|
...(options.proxy ? { proxy: { server: options.proxy } } : {}),
|
||||||
|
...options.launchOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
return browser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launch stealth browser and return a BrowserContext with common options pre-set.
|
||||||
|
* Closing the context also closes the browser.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* import { launchContext } from 'cloakbrowser';
|
||||||
|
* const context = await launchContext({
|
||||||
|
* userAgent: 'Mozilla/5.0...',
|
||||||
|
* viewport: { width: 1920, height: 1080 },
|
||||||
|
* });
|
||||||
|
* const page = await context.newPage();
|
||||||
|
* await page.goto('https://example.com');
|
||||||
|
* await context.close(); // also closes browser
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export async function launchContext(
|
||||||
|
options: LaunchContextOptions = {}
|
||||||
|
): Promise<BrowserContext> {
|
||||||
|
const browser = await launch(options);
|
||||||
|
|
||||||
|
let context: BrowserContext;
|
||||||
|
try {
|
||||||
|
context = await browser.newContext({
|
||||||
|
...(options.userAgent ? { userAgent: options.userAgent } : {}),
|
||||||
|
...(options.viewport ? { viewport: options.viewport } : {}),
|
||||||
|
...(options.locale ? { locale: options.locale } : {}),
|
||||||
|
...(options.timezoneId ? { timezoneId: options.timezoneId } : {}),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
await browser.close();
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Patch close() to also close the browser
|
||||||
|
const origClose = context.close.bind(context);
|
||||||
|
context.close = async () => {
|
||||||
|
await origClose();
|
||||||
|
await browser.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Internal
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function buildArgs(options: LaunchOptions): string[] {
|
||||||
|
const args: string[] = [];
|
||||||
|
if (options.stealthArgs !== false) {
|
||||||
|
args.push(...getDefaultStealthArgs());
|
||||||
|
}
|
||||||
|
if (options.args) {
|
||||||
|
args.push(...options.args);
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/**
|
||||||
|
* Puppeteer launch wrapper for cloakbrowser.
|
||||||
|
* Alternative to the Playwright wrapper for users who prefer Puppeteer.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Browser } from "puppeteer-core";
|
||||||
|
import type { LaunchOptions } from "./types.js";
|
||||||
|
import { getDefaultStealthArgs } from "./config.js";
|
||||||
|
import { ensureBinary } from "./download.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Launch stealth Chromium browser via Puppeteer.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```ts
|
||||||
|
* import { launch } from 'cloakbrowser/puppeteer';
|
||||||
|
* const browser = await launch();
|
||||||
|
* const page = await browser.newPage();
|
||||||
|
* await page.goto('https://bot.incolumitas.com');
|
||||||
|
* console.log(await page.title());
|
||||||
|
* await browser.close();
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
export async function launch(options: LaunchOptions = {}): Promise<Browser> {
|
||||||
|
const puppeteer = await import("puppeteer-core");
|
||||||
|
|
||||||
|
const binaryPath = process.env.CLOAKBROWSER_BINARY_PATH || (await ensureBinary());
|
||||||
|
const args = buildArgs(options);
|
||||||
|
|
||||||
|
// Puppeteer handles proxy via CLI args, not a separate option
|
||||||
|
if (options.proxy) {
|
||||||
|
args.push(`--proxy-server=${options.proxy}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const browser = await puppeteer.default.launch({
|
||||||
|
executablePath: binaryPath,
|
||||||
|
headless: options.headless ?? true,
|
||||||
|
args,
|
||||||
|
ignoreDefaultArgs: ["--enable-automation"],
|
||||||
|
...options.launchOptions,
|
||||||
|
});
|
||||||
|
|
||||||
|
return browser;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Internal
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function buildArgs(options: LaunchOptions): string[] {
|
||||||
|
const args: string[] = [];
|
||||||
|
if (options.stealthArgs !== false) {
|
||||||
|
args.push(...getDefaultStealthArgs());
|
||||||
|
}
|
||||||
|
if (options.args) {
|
||||||
|
args.push(...options.args);
|
||||||
|
}
|
||||||
|
return args;
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Shared types for cloakbrowser launch wrappers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface LaunchOptions {
|
||||||
|
/** Run in headless mode (default: true). */
|
||||||
|
headless?: boolean;
|
||||||
|
/** Proxy server URL, e.g. 'http://proxy:8080' or 'socks5://proxy:1080'. */
|
||||||
|
proxy?: string;
|
||||||
|
/** Additional Chromium CLI arguments. */
|
||||||
|
args?: string[];
|
||||||
|
/** Include default stealth fingerprint args (default: true). Set false to use custom --fingerprint flags. */
|
||||||
|
stealthArgs?: boolean;
|
||||||
|
/** Raw options passed directly to playwright/puppeteer launch(). */
|
||||||
|
launchOptions?: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LaunchContextOptions extends LaunchOptions {
|
||||||
|
/** Custom user agent string. */
|
||||||
|
userAgent?: string;
|
||||||
|
/** Viewport size. */
|
||||||
|
viewport?: { width: number; height: number };
|
||||||
|
/** Browser locale, e.g. "en-US". */
|
||||||
|
locale?: string;
|
||||||
|
/** Timezone, e.g. "America/New_York". */
|
||||||
|
timezoneId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BinaryInfo {
|
||||||
|
version: string;
|
||||||
|
platform: string;
|
||||||
|
binaryPath: string;
|
||||||
|
installed: boolean;
|
||||||
|
cacheDir: string;
|
||||||
|
downloadUrl: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import {
|
||||||
|
CHROMIUM_VERSION,
|
||||||
|
getDefaultStealthArgs,
|
||||||
|
getCacheDir,
|
||||||
|
getBinaryDir,
|
||||||
|
getDownloadUrl,
|
||||||
|
} from "../src/config.js";
|
||||||
|
|
||||||
|
describe("config", () => {
|
||||||
|
it("CHROMIUM_VERSION matches expected format", () => {
|
||||||
|
expect(CHROMIUM_VERSION).toMatch(/^\d+\.\d+\.\d+\.\d+$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getDefaultStealthArgs returns expected flags", () => {
|
||||||
|
const args = getDefaultStealthArgs();
|
||||||
|
|
||||||
|
expect(args).toContain("--no-sandbox");
|
||||||
|
expect(args).toContain("--disable-blink-features=AutomationControlled");
|
||||||
|
expect(args).toContain("--fingerprint-platform=windows");
|
||||||
|
expect(args).toContain("--fingerprint-hardware-concurrency=8");
|
||||||
|
|
||||||
|
// Should have a random fingerprint seed
|
||||||
|
const fingerprintArg = args.find((a) => a.startsWith("--fingerprint="));
|
||||||
|
expect(fingerprintArg).toBeDefined();
|
||||||
|
const seed = Number(fingerprintArg!.split("=")[1]);
|
||||||
|
expect(seed).toBeGreaterThanOrEqual(10000);
|
||||||
|
expect(seed).toBeLessThanOrEqual(99999);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getDefaultStealthArgs generates different seeds", () => {
|
||||||
|
const seeds = new Set<string>();
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
const args = getDefaultStealthArgs();
|
||||||
|
const fp = args.find((a) => a.startsWith("--fingerprint="))!;
|
||||||
|
seeds.add(fp);
|
||||||
|
}
|
||||||
|
// With 90k possible seeds, 10 calls should produce at least 2 unique
|
||||||
|
expect(seeds.size).toBeGreaterThan(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getCacheDir returns ~/.cloakbrowser by default", () => {
|
||||||
|
const dir = getCacheDir();
|
||||||
|
expect(dir).toContain(".cloakbrowser");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getBinaryDir includes version", () => {
|
||||||
|
const dir = getBinaryDir();
|
||||||
|
expect(dir).toContain(`chromium-${CHROMIUM_VERSION}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("getDownloadUrl contains version and platform tag", () => {
|
||||||
|
const url = getDownloadUrl();
|
||||||
|
expect(url).toContain(CHROMIUM_VERSION);
|
||||||
|
expect(url).toContain("cloakbrowser-");
|
||||||
|
expect(url).toContain(".tar.gz");
|
||||||
|
expect(url).toContain("github.com/CloakHQ/");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { binaryInfo } from "../src/download.js";
|
||||||
|
import { CHROMIUM_VERSION } from "../src/config.js";
|
||||||
|
|
||||||
|
describe("binaryInfo", () => {
|
||||||
|
it("returns correct structure", () => {
|
||||||
|
const info = binaryInfo();
|
||||||
|
|
||||||
|
expect(info.version).toBe(CHROMIUM_VERSION);
|
||||||
|
expect(info.platform).toMatch(/^(linux|darwin)-(x64|arm64)$/);
|
||||||
|
expect(info.binaryPath).toBeTruthy();
|
||||||
|
expect(typeof info.installed).toBe("boolean");
|
||||||
|
expect(info.cacheDir).toContain("cloakbrowser");
|
||||||
|
expect(info.downloadUrl).toContain(".tar.gz");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Integration tests require the binary — run with:
|
||||||
|
// CLOAKBROWSER_BINARY_PATH=/path/to/chrome npm test
|
||||||
|
describe.skipIf(!process.env.CLOAKBROWSER_BINARY_PATH)(
|
||||||
|
"launch (integration)",
|
||||||
|
() => {
|
||||||
|
it("launches browser and checks stealth", async () => {
|
||||||
|
const { launch } = await import("../src/playwright.js");
|
||||||
|
|
||||||
|
const browser = await launch({ headless: true });
|
||||||
|
const page = await browser.newPage();
|
||||||
|
await page.goto("about:blank");
|
||||||
|
|
||||||
|
const webdriver = await page.evaluate(() => navigator.webdriver);
|
||||||
|
expect(webdriver).toBeFalsy();
|
||||||
|
|
||||||
|
const plugins = await page.evaluate(() => navigator.plugins.length);
|
||||||
|
expect(plugins).toBeGreaterThan(0);
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
}, 30_000);
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["dist", "node_modules", "tests", "examples"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user