Library / SDK
okasi/bot-signal avatar
okasi/bot-signal

bot-signal weights its flags so ambiguous ones only block in combination

TypeScript bot detection: catch WebDriver, headless Chrome, Playwright/Puppeteer, robotic mouse/typing, datacenter IPs, JA3 TLS mismatch & timezone spoofing — browser + Node.

541 stars4 forksTypeScriptMIT

At a glance

What is it?
A MIT TypeScript bot detection library that scores three independent layers, instant browser artifacts, behavioral input over time, and server-side IP, TLS and timezone checks, with no API key and no data leaving your infrastructure. The scoring model, not the check list, is the interesting part.
Who is it for?
bot-signal suits a team that has been maintaining its own pile of copy-pasted bot checks and wants the weighting handled rather than the detection list, because a library that rates a check as definitive or ambiguous and combines them probabilistically is a different shape from a snippet that returns true.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 3 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 8, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Three layers, and only one of them needs a server

The library scores three independent layers rather than returning a single boolean from one place, and the quick start is one install:

bash
npm install bot-signal

Instant runs in the browser, synchronously against `window`, and looks for automation artifacts, native tampering, and inconsistencies between realms, user agent and GPU, plus Chrome DevTools Protocol traces. Behavioral runs in the browser over time, watching mouse movement, clicks, touch gestures, scrolling and keyboard events for robotic patterns and synthetic events. Server runs on Node 22 or newer and covers things the client cannot be trusted to report about itself: datacenter IP ranges, the AbuseIPDB blocklist, TLS fingerprint mismatch and timezone spoofing. The stated entry point is a single `isHuman()` call, and the claim is that nothing leaves your infrastructure, with no API keys and no external service in the path. You can also take any layer alone, which is what makes it composable rather than all or nothing.

The score is one minus the product of the non-weights

The scoring model is a noisy-OR and it is the part worth understanding before you tune anything. Across triggered signals the suspicion score is computed as one minus the product of one minus each weight, so two independent weak signals combine into something stronger than either alone without either having to be near certainty. A definitive automation marker carries a weight of 1.0, which makes it decisive on its own. An ambiguous check carries between 0.25 and 0.45, and the reason for that ceiling is specificity rather than caution in the abstract: checks in that band also fire on real clients, with in-app browsers, F11 fullscreen and GPU-less virtual machines named as the cases that produce them. `isLegitClient` is then simply the score being below a threshold, 0.5 by default, which you are expected to change. Passing a lower threshold, 0.3 for example, turns blocking on any single soft signal into the default behaviour.

The async variant adds the checks that cost a round trip

Instant mode is synchronous and free of latency, which is why it can run on page load before anything renders. The async variant trades that for a deeper picture and is documented as usually under 500 milliseconds. What it adds is specific: WebGPU capability probing including the `shader-f16` feature, CDP serialization detection, consistency between the Notification API and the Permissions API, high-entropy User-Agent Client Hints, and validation of the worker realm. Each of those targets a way an automation stack leaves a trace that the synchronous path cannot see in time. The result object is the same shape either way, exposing the suspicion score, a confidence value, the list of signals, the legitimacy flag, and an `automation` block carrying a detected kind with its own confidence, evidence and alternatives, which is useful when you want to log why something was flagged rather than only that it was.

Server mode takes headers you are already logging

The server layer is the one that actually stops traffic, and it is close to a signature check plus a reputation lookup. Passing `clientIp` is enough to trigger a GeoIP lookup, a datacenter range test, the AbuseIPDB blocklist and an iCloud Private Relay check. Everything else enriches the comparison. You pass a client timezone header, a TLS fingerprint from the JA3 hash header, the user agent, and optionally the values the browser reported to your own backend, meaning the user agent, language, language list and platform from the request body. It also reads the client hints and fetch metadata headers: `sec-ch-ua`, `sec-ch-ua-platform`, `sec-ch-ua-mobile`, `sec-fetch-site` and `sec-fetch-mode`. The documented cost is roughly one to five milliseconds per IP, which is why it can sit in front of an endpoint rather than behind a queue. The mismatch it looks for is a claimed identity that disagrees with what the connection and the address actually are.

The bundle is large because blocklists are committed

The size note is in the same section as the no-API-key claim, and the two are related. GeoIP data and the IP blocklists are bundled rather than fetched, and the update workflow runs weekly, so the package carries roughly 1.8 MB as a tarball and about 16 MB unpacked, almost entirely blocklist data rather than code. That is the deliberate trade: nothing calls out at runtime, and you pay the cost once at install. IPv4 and IPv6 are both handled, including IPv6 ranges and IPv4-mapped addresses, and matching is done with binary search at about a microsecond per lookup rather than a linear scan. The dependency count is one, described as the offline GeoIP database. So the practical concern is your bundler rather than your threat model: a client-side bundle carrying megabytes of address data will be noticed by anyone measuring build output, which is part of why the entry point table separates the layers.

Exports resolve by environment, with explicit subpaths

The package is TypeScript-first with full types, ships both ESM and CommonJS, and is marked `sideEffects: false` so a bundler can tree-shake it. The export map is the part that saves you a bug. The root import resolves to a browser-only build when a browser bundler asks for it, so `import { isHuman } from "bot-signal"` in client code cannot accidentally pull in the Node layer. When you want to be precise instead of relying on that, two subpaths exist: `bot-signal/browser` for instant plus behavioral only, and `bot-signal/server` for server detection only, the latter requiring Node 22 or newer. Type declarations are published for both subpaths, so autocompletion follows the same split. For environments with no bundler at all, a global build is served from a CDN, where the global object is named `BotSignal` rather than matching the package name, which is one of those details that costs an hour if you miss it.

A patchright vitest config means the detector is tested against automation

One file in the repository root tells you how this library is validated. Alongside the standard `vitest.config.ts` there is `vitest.patchright.config.ts`, and patchright is a fork of Playwright, so there is a second test configuration built around driving a real automated browser rather than a mocked `window`. That matters because most of the instant checks are exactly the kind of thing a fake test object gets wrong. A separate `tsconfig.types-test.json` alongside the main `tsconfig.json` implies the published type surface is tested too, which matters for a package whose whole value is that consumers can read every signal field. The rest of the layout is conventional: `src/`, `test/`, `docs/`, `data/` for the bundled data, `scripts/` for the weekly update job, plus `AGENTS.md`, `CONTRIBUTING.md` and `SECURITY.md`. The package itself is at version 2.0.17 and the repository publishes no GitHub releases, so there is no tag to pin and the manifest is the version of record.

Editorial conclusion

bot-signal suits a team that has been maintaining its own pile of copy-pasted bot checks and wants the weighting handled rather than the detection list, because a library that rates a check as definitive or ambiguous and combines them probabilistically is a different shape from a snippet that returns true. It is not a complete answer on its own, and the documentation does not claim it is: instant mode is a client-side signal any determined client can work around, and the server layer is what actually stops traffic. Before you deploy it, read the score formula rather than the pass or fail result, because the default threshold of 0.5 means your false-positive rate depends entirely on which checks fire on your legitimate users. Also check the packaging cost, since the bundle is large for a client-side library and the reason is committed blocklist data rather than code.

Frequently asked questions

How does bot-signal combine multiple detections into one score?

Each triggered signal carries a weight and the suspicion score is one minus the product of one minus each weight, a noisy-OR. Definitive automation markers weigh 1.0 and are decisive alone, while ambiguous checks that also fire on real clients weigh between 0.25 and 0.45 so they only block when combined with something else. isLegitClient is the score below a threshold, 0.5 by default.

Does bot-signal call any external service or need an API key?

No. GeoIP data and the IP blocklists are bundled and updated weekly, and the package has one dependency, the offline GeoIP database. The trade is package size, around 1.8 MB as a tarball and about 16 MB unpacked, almost all of it blocklist data.

What does the bot-signal server layer check?

Given a client IP it auto-runs a GeoIP lookup, a datacenter range check, the AbuseIPDB blocklist and an iCloud Private Relay check, then adds TLS fingerprint validation from a JA3 hash header, timezone comparison, user agent and client-reported browser values, and the client hints and fetch metadata headers. The documented cost is roughly one to five milliseconds per IP on Node 22 or newer.

How do I import bot-signal in browser and Node code?

The root import resolves to a browser build in browser bundlers and to the full build elsewhere, with isHuman available at the root. For explicit control use bot-signal/browser for instant plus behavioral only, or bot-signal/server for server detection only on Node 22 or newer. Without a bundler, a CDN global build is available and the global object is named BotSignal rather than matching the package name.

Official sources

  1. Issues
  2. License: MIT
  3. okasi/bot-signal on GitHub
  4. Project website
  5. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/okasi-bot-signal.svg)](https://hysenlabs.com/projects/okasi-bot-signal)