npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ja3lab

v0.2.2

Published

What does your HTTP client's TLS fingerprint actually look like — and is it coherent with your User-Agent? JA3/JA4/HTTP2 fingerprint check, diff, and classification for anti-bot work.

Readme

ja3lab

What does your HTTP client's TLS fingerprint actually look like — and does it match the story your User-Agent tells?

$ ja3lab compare --via "curl_chrome150"

── ja3lab compare ──

                 this node client                          curl_chrome150
────────────────────────────────────────────────────────────────────────────────────────────────
User-Agent       node                                  !=  Mozilla/5.0 (...) Chrome/150.0.0.0
JA3 hash         d67b094811e5145139d7cea5f014309f      !=  8817cca1f070ba0a1b31f7c76e1c1ed1
JA4              t13d5212h1_b262b3658495_8e6e362c5e    !=  t13d1516h2_8daaf6152771_806a8c22fd
HTTP/2 (h2)      — (HTTP/1.x only)                     !=  52d84b11737d980aef856699f885ca86
────────────────────────────────────────────────────────────────────────────────────────────────
The two clients present DIFFERENT TLS fingerprints. Anti-bot reads the handshake, not the UA.

Why

Anti-bot doesn't read your User-Agent or your TLS fingerprint. It reads both and asks whether they tell the same story. A Chrome UA over a Node/OpenSSL handshake — 59 cipher suites where a real browser offers 15 — is the single most common scraper tell, and it's invisible unless you look at the handshake.

ja3lab makes the handshake visible:

  1. check — what do I look like right now, to a WAF?
  2. compare — my client vs a real browser (via curl-impersonate), side by side.
  3. known — a reference table of verified fingerprints, captured live, not copied from blogs.

Install

npm

npm i -g ja3lab

Or from source:

git clone https://github.com/Oussama-Rahmouni/ja3lab.git
cd ja3lab
npm install
npm run build
npm link   # puts `ja3lab` on your PATH

Usage

ja3lab check                              # fingerprint this Node client
ja3lab check --via "curl_chrome150"       # fingerprint any command-line client
ja3lab compare --via "curl_chrome150"     # side-by-side diff
ja3lab known                              # the verified reference table
ja3lab check --json                       # machine-readable
ja3lab check --reflector https://my-reflector/api/all   # self-hosted echo endpoint

Exit codes: 0 coherent · 2 UA and TLS disagree · 1 error. Drops into CI: fail the build when your scraper's fingerprint drifts off the browser it claims to be.

What the verdict means

INCOHERENT — UA claims chrome116, the handshake disagrees:
  · UA claims chrome but the handshake offers 59 cipher suites — a real browser offers ~15–17
  · UA claims chrome but ALPN is h1 — modern browsers negotiate h2 on TLS 1.3
  • browser-shaped / library-shaped — structural classification from the JA4 shape: cipher count, extension count, ALPN. Browsers are minimal and deliberate; TLS libraries ship the full cipher list.
  • COHERENT / INCOHERENT — whether the UA claim and the handshake agree. Incoherence is what gets you flagged before any behavioral analysis runs.

The reference table

Captured live against tls.peet.ws (2026-09-16, expanded 2026-09-24 and 2026-10-10):

| label | client | JA4 | the tell | |---|---|---|---| | chrome-150 | Chrome 150 via curl-impersonate 2.x (BoringSSL) | t13d1516h2_8daaf6152771_… | same JA4_a/b as chrome-116 — only the extension set moved in 34 versions | | chrome-116 | Chrome 116 via curl-impersonate (BoringSSL) | t13d1516h2_… | 15 ciphers, 16 extensions, h2 | | node-undici | Node fetch / undici (18–20) | t13d5911h1_… | 59 ciphers, h1 | | node-undici-22 | Node 22 fetch / undici | t13d5212h1_… | 52 ciphers — same family, new shape | | curl-openssl | curl 8.5 (OpenSSL) | t13d3112h2_e8f1e7e78f70_… | OpenSSL default list | | python-requests | Python requests 2.31 (urllib3) | t13d3112h1_e8f1e7e78f70_… | byte-identical JA3 to curl — only ALPN differs | | python-urllib | Python 3.12 urllib (OpenSSL) | t13d3113h1_e8f1e7e78f70_… | same JA4_a cipher hash as curl | | wget-gnutls | wget 1.21 (GnuTLS) | t13d7511_… | 75 ciphers, no ALPN at all | | go-nethttp | Go net/http (crypto/tls) | t13d1411h2_… | 14 ciphers, h2 — passes the browser-shape heuristic |

The lesson rows: curl, requests, and urllib differ in everything except the TLS library — and the TLS library is what the WAF sees. And the Go row is the warning: cipher-count heuristics alone can't separate a minimal TLS library from a browser.

Library

import { fingerprintSelf, fingerprintVia, classify, checkCoherence, renderCompare } from 'ja3lab';

const me = await fingerprintSelf();
const browser = await fingerprintVia('curl_chrome116');

classify(me);            // exact match + browser/library shape + reasons
checkCoherence(me);      // does the UA story match the handshake?
renderCompare(me, browser);

What it is not

Not a bypass tool. It shows you the fingerprint; fixing it is a separate craft (curl-impersonate, uTLS, browser clients). Diagnosis first.

License

MIT · Oussama Rahmouni