@hyvmind/cdp-stealth
v0.1.0
Published
Zero-dependency CDP-based browser stealth library for anti-bot evasion
Downloads
198
Maintainers
Readme
cdp-stealth
Zero-dependency CDP-based browser stealth library for anti-bot evasion. Works with any Puppeteer variant.
Features
- 12 stealth evasions covering all major bot detection vectors
- Zero runtime dependencies — ships only compiled TypeScript
- CDP-first — uses Chrome DevTools Protocol directly for User Agent + Client Hints (falls back to page API)
- Challenge detection — detects and waits for Cloudflare, Akamai, and DataDome challenge pages
- Fully configurable — toggle individual evasions, override UA, viewport, WebGL, hardware specs
- Works everywhere —
puppeteer,puppeteer-core,@cloudflare/puppeteer
Installation
npm install @hyvmind/cdp-stealthQuick Start
import puppeteer from "puppeteer";
import { applyStealthEvasions } from "@hyvmind/cdp-stealth";
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
const result = await applyStealthEvasions(page);
// => { evasionsApplied: 12, appliedNames: [...], skippedNames: [], usedCDP: true }
await page.goto("https://bot.sannysoft.com");All 12 evasions are applied by default. No configuration required.
Cloudflare Workers
cdp-stealth was built for use with @cloudflare/puppeteer in Durable Objects:
import puppeteer from "@cloudflare/puppeteer";
import { applyStealthEvasions, detectChallenge, waitForChallenge } from "@hyvmind/cdp-stealth";
export class Browser {
private browser: puppeteer.Browser | null = null;
async fetch(request: Request, env: Env): Promise<Response> {
this.browser ??= await puppeteer.launch(env.MYBROWSER);
const page = await this.browser.newPage();
await applyStealthEvasions(page);
await page.goto("https://example.com", { waitUntil: "networkidle0" });
// Handle challenge pages
if (await detectChallenge(page)) {
const { resolved } = await waitForChallenge(page, { timeout: 15000 });
if (!resolved) return new Response("Challenge not resolved", { status: 403 });
}
const content = await page.content();
await page.close();
return new Response(content);
}
}Configuration
Pass a StealthConfig object to override defaults:
import { applyStealthEvasions } from "@hyvmind/cdp-stealth";
await applyStealthEvasions(page, {
// User agent (default: Chrome 131 on macOS)
userAgent: {
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ...",
platform: "Win32",
brands: [
{ brand: "Google Chrome", version: "131" },
{ brand: "Chromium", version: "131" },
{ brand: "Not_A Brand", version: "24" },
],
},
// Viewport (default: 1920x1080)
viewport: { width: 1280, height: 720, deviceScaleFactor: 2 },
// WebGL fingerprint (default: Intel Iris OpenGL Engine)
webgl: { vendor: "NVIDIA Corporation", renderer: "NVIDIA GeForce GTX 1080" },
// Hardware (defaults: 8 cores, 8 GB)
hardwareConcurrency: 4,
deviceMemory: 4,
// Languages (default: ['en-US', 'en'])
languages: ["fr-FR", "fr", "en"],
// Locale and timezone
locale: "fr-FR",
timezone: "Europe/Paris",
// Disable specific evasions
evasions: {
iframe: false,
mediaCodecs: false,
},
});All fields are optional. Unset fields use sensible defaults (Chrome 131 on macOS 15).
Challenge Detection
Detect and wait for bot challenge pages (Cloudflare Turnstile, Akamai, DataDome):
import { detectChallenge, waitForChallenge } from "@hyvmind/cdp-stealth";
await page.goto("https://protected-site.com");
const isChallenge = await detectChallenge(page);
if (isChallenge) {
const result = await waitForChallenge(page, {
timeout: 15000, // Max wait time (default: 15s)
pollInterval: 500, // Check interval (default: 500ms)
});
if (result.resolved) {
// Challenge passed, page has navigated to real content
} else {
// Timeout reached, challenge still showing
}
}Also available as a subpath import:
import { detectChallenge, waitForChallenge } from "@hyvmind/cdp-stealth/challenge";Evasion Reference
| # | Name | What it does | Config |
| --- | --------------------- | ------------------------------------------------------------------------------------ | --------------------- |
| 1 | webdriver | Removes navigator.webdriver (returns undefined, not false) | — |
| 2 | userAgent | Full UA override via CDP Emulation.setUserAgentOverride with Client Hints metadata | userAgent |
| 3 | chromeRuntime | Injects window.chrome stubs (runtime, app, csi, loadTimes) | — |
| 4 | plugins | Realistic navigator.plugins array with proper PluginArray/MimeType prototypes | — |
| 5 | languages | navigator.languages + navigator.language consistency | languages |
| 6 | permissions | navigator.permissions.query() override for notifications | — |
| 7 | webglVendor | WebGL UNMASKED_VENDOR/RENDERER spoofing on WebGL + WebGL2 | webgl |
| 8 | outerDimensions | Fixes window.outerWidth/outerHeight/screenX/screenY (headless returns 0) | viewport |
| 9 | hardwareConcurrency | navigator.hardwareConcurrency override | hardwareConcurrency |
| 10 | deviceMemory | navigator.deviceMemory override | deviceMemory |
| 11 | iframe | HTMLIFrameElement.contentWindow getter fix via Proxy | — |
| 12 | sourceUrl | Strips //# sourceURL=pptr: from Error stack traces | — |
All evasions are enabled by default. Disable individually via evasions: { name: false }.
Builder API
StealthConfigurator provides a chainable builder for complex configurations:
import { StealthConfigurator } from "@hyvmind/cdp-stealth";
const stealth = new StealthConfigurator()
.setUserAgent({ platform: "Win32" })
.setViewport({ width: 1280, height: 720 })
.setLocale("en-GB")
.setTimezone("Europe/London")
.setWebGL({ vendor: "NVIDIA Corporation", renderer: "NVIDIA GeForce RTX 3080" })
.setEvasions({ iframe: false });
// Apply to multiple pages
const page1 = await browser.newPage();
await stealth.apply(page1);
const page2 = await browser.newPage();
await stealth.apply(page2);Individual Evasions
For fine-grained control, import and apply evasions individually:
import { evasions } from "@hyvmind/cdp-stealth";
// Apply only what you need
await evasions.webdriver.apply(page);
await evasions.userAgent.apply(page, { userAgent: "Custom UA", platform: "Win32" });
await evasions.chromeRuntime.apply(page);
await evasions.plugins.apply(page);
await evasions.languages.apply(page, ["pt-BR", "pt", "en"]);
await evasions.webgl.apply(page, { vendor: "Apple", renderer: "Apple M1" });
await evasions.hardware.apply(page, 16, 32); // cores, memory GBOr import the evasions module directly:
import * as webdriver from "@hyvmind/cdp-stealth/evasions";API Reference
applyStealthEvasions(page, config?)
Applies all enabled stealth evasions to a page. Call once after browser.newPage().
function applyStealthEvasions(page: StealthPage, config?: StealthConfig): Promise<StealthResult>;Returns StealthResult:
| Field | Type | Description |
| ----------------- | ---------- | ------------------------------------------------ |
| evasionsApplied | number | Count of evasions applied |
| appliedNames | string[] | Names of applied evasions |
| skippedNames | string[] | Names of skipped evasions |
| usedCDP | boolean | Whether CDP session was used for UA/Client Hints |
detectChallenge(page)
Detects Cloudflare, Akamai, DataDome, and generic bot challenge pages.
function detectChallenge(page: StealthPage): Promise<boolean>;waitForChallenge(page, options?)
Polls detectChallenge until the challenge resolves or timeout is reached.
function waitForChallenge(page: StealthPage, options?: WaitOptions): Promise<ChallengeResult>;WaitOptions: { timeout?: number, pollInterval?: number }
ChallengeResult: { resolved: boolean, timeoutReached: boolean }
StealthConfigurator
Chainable builder. Methods: setUserAgent(), setViewport(), setLocale(), setTimezone(), setWebGL(), setEvasions(), getConfig(), apply(page).
Exported Types
import type {
StealthPage,
CDPSession,
StealthConfig,
StealthResult,
UserAgentConfig,
ViewportConfig,
WebGLConfig,
EvasionFlags,
ChallengeResult,
WaitOptions,
} from "@hyvmind/cdp-stealth";How It Works
cdp-stealth uses two mechanisms to apply evasions:
CDP session (
page.createCDPSession()) — Used forEmulation.setUserAgentOverridewhich is the only way to properly set Client Hints headers (Sec-CH-UA-*). Falls back topage.setUserAgent()if CDP is unavailable.evaluateOnNewDocument()— Injects JavaScript that runs before any page scripts. This is where browser-context overrides happen (navigator properties, window dimensions, WebGL parameters, etc.). Scripts are injected once and apply to all subsequent navigations.
Each evasion module exports a NAME constant and an apply(page, ...args) function, making them composable and testable in isolation.
Compatibility
| Runtime | Package | CDP Support |
| ------------------ | ----------------------- | ----------- |
| Node.js | puppeteer | Full |
| Node.js | puppeteer-core | Full |
| Cloudflare Workers | @cloudflare/puppeteer | Full |
The library defines minimal StealthPage and CDPSession interfaces — any object matching these shapes will work, no Puppeteer type dependency required.
License
MIT
