npm package: @hyvmind/cdp-stealth
Description: Zero-dependency CDP-based browser stealth library for anti-bot evasion
Author: HyvMind
Latest version: 0.1.0

# cdp-stealth

[![TypeScript](https://img.shields.io/badge/TypeScript-5.9+-blue?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
[![Zero Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)](package.json)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

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

```bash
npm install @hyvmind/cdp-stealth
```

## Quick Start

```typescript
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:

```typescript
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:

```typescript
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):

```typescript
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:

```typescript
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:

```typescript
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:

```typescript
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 GB
```

Or import the evasions module directly:

```typescript
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()`.

```typescript
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.

```typescript
function detectChallenge(page: StealthPage): Promise<boolean>;
```

### `waitForChallenge(page, options?)`

Polls `detectChallenge` until the challenge resolves or timeout is reached.

```typescript
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

```typescript
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:

1. **CDP session** (`page.createCDPSession()`) — Used for `Emulation.setUserAgentOverride` which is the only way to properly set Client Hints headers (`Sec-CH-UA-*`). Falls back to `page.setUserAgent()` if CDP is unavailable.

2. **`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