https://github.com/okasi/detect-bot-client
TypeScript bot detection: catch WebDriver, headless Chrome, Playwright/Puppeteer, robotic mouse/typing, datacenter IPs, JA3 TLS mismatch & timezone spoofing — browser + Node.
https://github.com/okasi/detect-bot-client
anti-bot automation-detection behavioral-analysis bot-detection browser-fingerprinting cybersecurity fraud-protection geoip mouse-movement security tls-fingerprint
Last synced: about 5 hours ago
JSON representation
TypeScript bot detection: catch WebDriver, headless Chrome, Playwright/Puppeteer, robotic mouse/typing, datacenter IPs, JA3 TLS mismatch & timezone spoofing — browser + Node.
- Host: GitHub
- URL: https://github.com/okasi/detect-bot-client
- Owner: okasi
- License: mit
- Created: 2026-06-18T09:49:24.000Z (12 days ago)
- Default Branch: main
- Last Pushed: 2026-06-22T07:26:09.000Z (8 days ago)
- Last Synced: 2026-06-22T07:26:16.670Z (8 days ago)
- Topics: anti-bot, automation-detection, behavioral-analysis, bot-detection, browser-fingerprinting, cybersecurity, fraud-protection, geoip, mouse-movement, security, tls-fingerprint
- Language: TypeScript
- Homepage: https://okasi.github.io/detect-bot-client/
- Size: 1.94 MB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 2
-
Metadata Files:
- Readme: README.md
- License: LICENSE
- Agents: AGENTS.md
Awesome Lists containing this project
README
# detect-bot-client
**Detect bots, headless browsers, and automation — in the browser and on the server.**
One library. Three layers of defense. Zero external API keys.
[](https://www.npmjs.com/package/detect-bot-client)
[](LICENSE)
[](https://nodejs.org)
[](https://github.com/okasi/detect-bot-client/actions/workflows/ci.yml)
[](https://github.com/okasi/detect-bot-client/actions/workflows/update-ip-data.yml)
[Quick start](#quick-start) · [Detection modes](#detection-modes) · [Signals](#signals) · [API](#api) · [Examples](#examples) · [FAQ](#faq)
---
## Why this library?
Most bot-detection snippets are copy-pasted checks that rot quickly. **detect-bot-client** gives you a maintained, typed, testable toolkit that covers the full stack:
**[Live demo](https://okasi.github.io/detect-bot-client/)** — run instant and behavioral checks in your browser.
| Layer | Runs where | Catches |
|-------|------------|---------|
| **Instant** | Browser (sync) | WebDriver, Selenium, Playwright, headless Chrome, bad WebGL/WebGPU |
| **Behavioral** | Browser (over time) | Robotic mouse/scroll/typing, synthetic events |
| **Server** | Node / edge | Datacenter IPs, AbuseIPDB, TLS fingerprint mismatch, timezone spoofing |
- **No API keys** — GeoIP and IP blocklists are bundled and updated weekly
- **TypeScript-first** — full types, ESM + CJS
- **Composable** — use one layer or combine all three
- **Explainable** — every flag has a name, weight, and confidence level
---
## Quick start
```bash
npm install detect-bot-client
```
### Browser — block automation on page load
```ts
import { detectInstantClient } from "detect-bot-client";
const result = detectInstantClient(window);
if (!result.isLegitClient) {
window.location.href = "/blocked";
}
```
### Server — score a request in one call
```ts
import { detectServerClientAsync } from "detect-bot-client";
const result = await detectServerClientAsync({
clientIp: req.ip,
clientTimezone: req.headers["x-timezone"],
userAgent: req.headers["user-agent"],
tlsFingerprint: req.headers["x-ja3-hash"],
});
if (!result.isLegitClient) {
return res.status(403).json({ signals: result.signals });
}
```
### Behavioral — catch scripted interaction
```ts
import { createBehavioralClientDetector } from "detect-bot-client";
const result = await createBehavioralClientDetector({ context: window }).observe(10_000);
if (!result.isLegitClient) {
console.warn("Robotic behavior", result.suspicionScore);
}
```
---
## Detection modes
```mermaid
flowchart LR
subgraph Browser
A[Instant] --> B{Pass?}
B -->|yes| C[Behavioral]
B -->|no| X[Block]
C --> D{Pass?}
D -->|yes| E[Allow]
D -->|no| X
end
subgraph Server
S[detectServerClientAsync] --> T{Pass?}
T -->|yes| E
T -->|no| X
end
Browser -->|beacon + headers| Server
```
| Mode | API | Speed | Environment |
|------|-----|-------|-------------|
| **Instant** | `detectInstantClient` | Immediate | Browser |
| **Instant+** | `detectInstantClientAsync` | ~50ms | Browser (adds WebGPU check) |
| **Behavioral** | `createBehavioralClientDetector` | 5–30s | Browser |
| **Server** | `detectServerClientAsync` | ~1–5ms per IP | Node, edge |
### Instant
Runs synchronously against `window`. Async variant adds WebGPU `shader-f16` validation on Chromium.
```ts
const sync = detectInstantClient(window);
const async = await detectInstantClientAsync(window);
```
### Behavioral
Observes mouse, scroll, and keyboard events. Score: `1 - Π(1 - weight)` across triggered signals.
```ts
const detector = createBehavioralClientDetector({
context: window,
scoreThreshold: 0.55,
onUpdate: (r) => console.log(r.suspicionScore),
});
await detector.observe(8_000);
```
### Server
Pass `clientIp` to auto-run GeoIP lookup, datacenter range check, AbuseIPDB blocklist, iCloud Private Relay check, TLS validation, and timezone comparison.
```ts
const result = await detectServerClientAsync({
clientIp: req.ip,
clientTimezone: req.headers["x-timezone"],
tlsFingerprint: req.headers["x-ja3-hash"],
userAgent: req.headers["user-agent"],
});
```
Bundled IP data is refreshed weekly. Run locally: `npm run update:ip-data`.
---
## Signals
### Instant (boolean flags)
Any suspicious flag fails `isLegitClient`.
| Flag | Triggers when |
|------|---------------|
| `isWebDriver` | `navigator.webdriver === true` |
| `isPhantomJS` | PhantomJS globals present |
| `isNightmare` | Nightmare.js marker |
| `isSelenium` | Selenium document markers |
| `isDomAutomation` | Chrome DOM automation globals |
| `isHeadless` | WebDriver or HeadlessChrome UA |
| `isSuspiciousResolution` | Screen < 136×170 |
| `isUserAgentValid` | UA does not start with `Mozilla/5.0 (` |
| `isWebGLSupported` | No WebGL context |
| `isModern` | Below Chrome 121 / Firefox 128 / Safari 16.4 |
| `isMissingChromeObject` | Chromium without `chrome.runtime` |
| `isSoftwareRenderer` | SwiftShader / llvmpipe WebGL |
| `isSuspiciousWindowDimensions` | No browser chrome + origin placement |
| `isEmptyPlugins` | Zero plugins on Chromium |
| `isAutomationArtifacts` | ChromeDriver / Playwright markers |
| `isSuspiciousWebDriverDescriptor` | Patched `navigator.webdriver` |
| `isShaderF16Supported` | Async — missing WebGPU `shader-f16` on Chromium |
### Behavioral (weighted)
| ID | Weight | Confidence | Description |
|----|--------|------------|-------------|
| `no-mouse-activity` | 0.20 | low | Clicks without mouse moves |
| `click-without-mouse-movement` | 0.35 | high | Click with no recent mouse path |
| `linear-mouse-movement` | 0.25 | medium | Straight path, uniform speed |
| `teleport-mouse` | 0.40 | high | Implausible cursor jumps |
| `linear-scroll` | 0.30 | medium | Uniform scroll deltas/timing |
| `linear-typing` | 0.35 | high | Robotic or superhuman intervals |
| `synthetic-events` | 0.50 | high | `isTrusted === false` |
### Server (weighted)
| ID | Weight | Confidence | Description |
|----|--------|------------|-------------|
| `timezone-mismatch` | 0.50 | high | Client TZ ≠ GeoIP TZ |
| `known-suspicious-tls` | 0.55 | high | JA3 matches Python/curl/Go/Java |
| `tls-user-agent-mismatch` | 0.50 | high | JA3 conflicts with User-Agent |
| `missing-tls-fingerprint` | 0.25 | medium | Browser UA without JA3 |
| `accept-language-geo-mismatch` | 0.20 | low | Accept-Language missing GeoIP country |
| `datacenter-browser-mismatch` | 0.35 | medium | Datacenter IP + browser UA |
| `abuse-listed-ip` | 0.60 | high | AbuseIPDB 30-day blocklist |
| `icloud-private-relay` | 0.15 | low | iCloud Private Relay egress |
**Bundled IP data:** `data/datacenter_ip_ranges.csv` (ipcat), `data/abuse_ip_db_30d_ips.csv` (AbuseIPDB), `data/icloud_private_relay_ip_ranges.csv` (Apple).
---
## API
```ts
import {
// Browser — instant
detectInstantClient,
detectInstantClientAsync,
// Browser — behavioral
createBehavioralClientDetector,
analyzeBehavioralSamples,
// Server
detectServerClient,
detectServerClientAsync,
enrichServerContext,
lookupClientIpGeo,
createIpListChecker,
// Standalone checks
isAutomationArtifacts,
isSoftwareRenderer,
isTimezoneMismatch,
isTlsUserAgentMismatch,
KNOWN_SUSPICIOUS_TLS_FINGERPRINTS,
} from "detect-bot-client";
```
### Server options
```ts
detectServerClientAsync(context, {
dataDir: "./custom-data",
lookupGeo: true,
checkIpLists: true,
timezoneToleranceMinutes: 60,
scoreThreshold: 0.5,
requireTlsFingerprint: false,
suspiciousTlsFingerprints: [],
});
```
### Behavioral options
```ts
createBehavioralClientDetector({
context: window,
minObservationMs: 3_000,
scoreThreshold: 0.55,
pollIntervalMs: 1_000,
onUpdate: (result) => {},
});
```
---
## Examples
### Defense in depth
```ts
const instant = detectInstantClient(window);
if (!instant.isLegitClient) block();
fetch("/api/beacon", {
headers: { "X-Timezone": Intl.DateTimeFormat().resolvedOptions().timeZone },
});
const behavioral = await createBehavioralClientDetector({ context: window }).observe(10_000);
if (!behavioral.isLegitClient) challenge();
const server = await detectServerClientAsync({ clientIp: req.ip /* ... */ });
if (!server.isLegitClient) return res.status(403).end();
```
### Express middleware
```ts
import { detectServerClientAsync } from "detect-bot-client";
app.use(async (req, res, next) => {
const result = await detectServerClientAsync({
clientIp: req.ip,
clientTimezone: req.headers["x-timezone"],
userAgent: req.headers["user-agent"],
tlsFingerprint: req.headers["x-ja3-hash"],
});
if (!result.isLegitClient) {
return res.status(403).json({ signals: result.signals });
}
next();
});
```
### Next.js client guard
```tsx
"use client";
import { useEffect } from "react";
import { detectInstantClient } from "detect-bot-client";
export function BotGuard({ children }) {
useEffect(() => {
if (!detectInstantClient(window).isLegitClient) {
window.location.href = "/blocked";
}
}, []);
return children;
}
```
---
## FAQ
**Can client-side checks be bypassed?**
Yes. Use instant + behavioral for friction; server detection for authoritative decisions.
**False positives?**
Possible with privacy browsers, VPNs, iCloud Relay, corporate proxies, VMs. Tune `scoreThreshold`.
**How often is IP data updated?**
Weekly (Mondays 04:00 UTC). Run `npm run update:ip-data` locally anytime.
**Works without bundlers?**
Yes — ESM + CJS + types. Use with Vite, Webpack, Next.js, or `esm.sh`.
---
## Development
```bash
git clone https://github.com/okasi/detect-bot-client.git
cd detect-bot-client
npm install
npx patchright install chromium # once, for browser tests
npm test # unit tests
npm run test:patchright # real Chromium via patchright
npm run build
npm run build:site # copy browser bundle into docs/ for GitHub Pages
```
Live demo: https://okasi.github.io/detect-bot-client/ (deployed from `docs/` on push to `main`).
**GitHub Pages setup (one time):** Settings → Pages → Build and deployment → **Deploy from a branch** → Branch: `gh-pages` / `/ (root)`.
### Publish to npm
npm package: **`detect-bot-client`** (verified available; distinct from older [`detect-bot`](https://www.npmjs.com/package/detect-bot)).
#### Step 1 — First publish (once, from your computer)
```bash
git clone https://github.com/okasi/detect-bot-client.git
cd detect-bot-client
npm install
npm run test
npm run build
npm login
npm publish --access public
```
#### Step 2 — Enable Trusted Publishing (for GitHub Actions)
1. https://www.npmjs.com/package/detect-bot-client → **Settings** → **Trusted publishing**
2. **GitHub Actions** → user `okasi`, repo `detect-bot-client`, workflow `publish.yml`
3. Save
#### Step 3 — Future releases via Actions
```bash
npm version patch
git push origin main --follow-tags
```
Or re-run **Actions → Publish npm → Run workflow**.
See [AGENTS.md](AGENTS.md) for architecture and contributor guidance.
## License
[MIT](LICENSE) © [okasi](https://github.com/okasi)
---
**If this saved you time, consider starring the repo.**
[](https://github.com/okasi/detect-bot-client)