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:
+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