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

device-signal-kit

v0.1.1

Published

Lightweight, dependency-free TypeScript library for browser device fingerprinting, composite risk scoring, and bot/automation detection.

Readme

device-signal-kit

A lightweight, zero-runtime-dependency TypeScript library for browser-side device fingerprinting, composite risk scoring, and bot/automation detection — the kind of signal collection done by tools like FingerprintJS, rebuilt from first principles with a strict, explicitly-SOLID architecture.

What this is: a portfolio / demonstration project showing how a real client-side signal pipeline is structured. What this is not: a production-grade anti-fraud system. The heuristics here are deliberately simple, well-documented, and evadable — see Limitations.


What it does

  1. Collects a device fingerprint from native browser APIs — canvas rendering, WebGL renderer/vendor, an audio-context signal, and navigator/screen/timezone properties.
  2. Combines signals into a 0–100 risk score based on their consistency and completeness.
  3. Flags likely bot/automation sessions with small, explainable heuristics (every verdict comes with reasons, never a black-box boolean).
  4. Ships as ESM + UMD bundles with .d.ts types and no third-party runtime dependencies.
  5. Is architected to scale — new signals and heuristics are added without touching existing code.

Install & usage

npm install device-signal-kit
import { collect } from 'device-signal-kit';

const result = await collect();

console.log(result.fingerprint); // e.g. "42910ff0"
console.log(result.riskScore);   // 0–100
console.log(result.bot);         // { isLikelyBot, reasons[], checks[] }
console.log(result.signals);     // per-signal breakdown

collect() returns:

interface ScanResult {
  fingerprint: string;            // stable device ID (FNV-1a hash of signals)
  riskScore: number;              // 0–100, higher = more suspicious
  signals: SignalResult[];        // one entry per collector
  bot: {
    isLikelyBot: boolean;
    reasons: string[];            // human-readable, only the triggered ones
    checks: BotCheckResult[];     // every heuristic's full result
  };
}

📘 Full SDK usage guide → docs/USAGE.md — install, module formats (ESM/UMD/CDN), the complete API & result reference, enabling the opt-in interaction heuristic, extending with custom collectors/heuristics/scorers, framework examples, and the full TypeScript export list.

Live dashboard

A full demo dashboard lives in ui/. Run it with:

npm install
npm run dev          # opens http://localhost:5174

It shows a radial risk gauge (green < 30, amber 30–70, red > 70), the copy-to-clipboard fingerprint, a per-signal breakdown table with collected / inconsistent / blocked status, and a "Likely human / Likely automated" badge with the list of triggered reasons. It supports light/dark via prefers-color-scheme and is responsive to mobile width.

The dashboard (ui/app.ts) contains no fingerprinting/scoring/bot logic — it only calls the library's public collect() and registry, keeping the UI layer honestly separated from the core.


Risk-scoring approach

EntropyRiskScorer (src/scoring/entropy-risk-scorer.ts) is a heuristic, not a trained model, and is documented as such. The intuition:

  • A genuine browser exposes a full set of high-entropy signals that are mutually consistent.
  • Privacy tools and automation frameworks tend to block APIs (canvas refuses to render, no audio context) or spoof them (software WebGL renderer, contradictory navigator.languages).

So the score is built up like this:

| Condition | Effect | | --- | --- | | Baseline | start at 5 | | Each blocked signal | +2 (less identifiable, mildly suspicious) | | Each inconsistent signal | +3 (active spoofing — a stronger tell) |

The total penalty is normalised against the worst case (every signal inconsistent) so the output stays a stable 0–100 regardless of how many collectors are registered. A clean, fully-consistent browser scores ~5; a browser with every signal spoofed scores 100.


Bot heuristics

Each heuristic checks exactly one condition and returns { name, triggered, reason } so results are explainable.

| Heuristic | File | Checks | | --- | --- | --- | | webdriver | webdriver-heuristic.ts | navigator.webdriver === true | | headless-plugins | headless-plugin-heuristic.ts | Chrome UA but empty navigator.plugins/mimeTypes, or missing window.chrome | | headless-ua | headless-ua-heuristic.ts | UA reveals an automation/embedded-runtime marker (HeadlessChrome, Electron, Puppeteer, …) | | permissions-mismatch | permissions-heuristic.ts | Notification.permission === 'denied' while permissions.query returns 'prompt' (classic headless tell) | | devtools-cdp (experimental) | devtools-cdp-heuristic.ts | Probes for an attached inspector/CDP session by logging an object with a getter and seeing if it gets read | | interaction (opt-in) | interaction-heuristic.ts | Click with no preceding mousemove; perfectly linear cursor paths |

The dashboard also renders a "Heuristic checks" panel (every check, pass or fail) and a raw "Environment" panel exposing the exact values the heuristics read (navigator.webdriver, plugin count, window.chrome, UA, …), so any verdict is fully auditable rather than a black box.

Note on headless-ua: the Electron token it matches also appears in legitimate desktop apps (Slack, VS Code, agent/desktop shells), so it will false-positive on those — it's a deliberately weak, illustrative signal, not a reliable one.

Why a sophisticated agent can still read as "human"

A capable agent driving a real, non-headless browser through an extension or profile (rather than WebDriver/headless mode) typically:

  • has navigator.webdriver === false,
  • exposes real navigator.plugins and a normal window.chrome,
  • carries a stock Chrome user-agent, and
  • produces mouse input indistinguishable from a person's.

In that case most client-side heuristics here correctly report "human" — and that's the right answer for what client-side detection can actually observe. Catching that class of automation requires server-side signals (request timing/rate, TLS/network fingerprinting, IP reputation, behavioural analysis over a session, challenge–response), which are out of scope for a browser-only library. This boundary is the whole point of the demo.

The one client-side angle that sometimes reaches it is the experimental devtools-cdp heuristic: tooling that drives a real browser often attaches an inspector/CDP session, which the probe can occasionally detect. It is deliberately weak — it depends on Chrome version and CDP preview behaviour, it can be evaded by avoiding console previews, and it false-positives whenever a human has DevTools open. So a devtools-cdp trigger means "an inspector appears attached", which is weaker than "this is a bot" — read it alongside the other checks, never alone.

Why interaction is opt-in

Unlike the others, the interaction heuristic must attach mousemove/click listeners and observe the user interacting with the page. That carries UX and privacy implications the passive checks don't, so it is never enabled by default. A caller turns it on explicitly:

import { collect, heuristicRegistry, InteractionHeuristic } from 'device-signal-kit';

heuristicRegistry.register(new InteractionHeuristic({ enabled: true }));
// ... later, after the user has had a chance to interact ...
const result = await collect();

Limitations & honesty

These heuristics are demonstrations. Every one can be evaded — webdriver can be patched out, plugins can be faked, and human-like mouse movement can be synthesised. A real anti-bot system layers server-side signals, behavioural models, TLS/network fingerprinting, and continuous evaluation. Do not rely on this library as your only line of defence.


Architecture (SOLID, mapped to the code)

This project implements SOLID literally, not "in spirit". Where each principle shows up:

  • S — Single Responsibility. Every collector (src/signals/) collects exactly one signal; every heuristic (src/bot/) checks exactly one condition. Scoring, data collection, fingerprint hashing, and UI rendering each live in their own module and never mix.
  • O — Open/Closed. SignalOrchestrator is never edited to add a feature. A new signal/heuristic is a new file plus one register() call. The orchestrator contains zero references to canvas/WebGL/audio/navigator.
  • L — Liskov Substitution. Any ISignalCollector is swappable for any other; same for IBotHeuristic and IRiskScorer. This is enforced by shared contract tests (test/contracts/) that run the same assertions against every implementation.
  • I — Interface Segregation. The contracts in src/core/interfaces.ts are tiny, single-method interfaces (collect(), check(), score()) — no bloated god-interface.
  • D — Dependency Inversion. The orchestrator depends only on those interfaces, injected via its constructor. src/index.ts is the only place concrete classes are instantiated and wired (the composition root). Plain constructor injection — no DI framework, consistent with the zero-dependency goal.

Concurrency & scalability

Signal collection runs with Promise.allSettled, so adding more signals does not increase scan time linearly and one failing collector can never break the others. The Registry allows third parties to register new collectors/heuristics at runtime, so the library is extensible without forking it.


Adding a new signal

Create one file implementing ISignalCollector, then register it. No existing file is modified (Open/Closed):

// my-fonts-signal.ts
import type { ISignalCollector, SignalResult } from 'device-signal-kit';

export class FontsSignalCollector implements ISignalCollector {
  readonly name = 'fonts';

  async collect(): Promise<SignalResult> {
    try {
      const available = ['Arial', 'Courier', 'Times'].filter((f) =>
        document.fonts.check(`12px "${f}"`),
      );
      return {
        name: this.name,
        value: available.join(','),
        collected: true,
        status: 'collected',
      };
    } catch {
      return { name: this.name, value: '', collected: false, status: 'blocked' };
    }
  }
}
import { collectorRegistry } from 'device-signal-kit';
import { FontsSignalCollector } from './my-fonts-signal';

collectorRegistry.register(new FontsSignalCollector()); // ← the one line
// collect() now includes the new signal automatically.

Adding a new bot heuristic

Identical pattern with IBotHeuristic:

// timezone-mismatch-heuristic.ts
import type { BotCheckResult, IBotHeuristic } from 'device-signal-kit';

export class TimezoneMismatchHeuristic implements IBotHeuristic {
  readonly name = 'timezone-mismatch';

  async check(): Promise<BotCheckResult> {
    const offsetTz = new Date().getTimezoneOffset();
    const intlTz = Intl.DateTimeFormat().resolvedOptions().timeZone;
    const suspicious = offsetTz === 0 && intlTz === 'UTC'; // common headless default
    return {
      name: this.name,
      triggered: suspicious,
      reason: suspicious
        ? 'UTC timezone with zero offset (common headless default)'
        : 'timezone consistent',
    };
  }
}
import { heuristicRegistry } from 'device-signal-kit';
heuristicRegistry.register(new TimezoneMismatchHeuristic()); // ← the one line

Ethics & responsible use

This library collects device/browser characteristics only — canvas rendering, the WebGL renderer string, an audio-context fingerprint, and navigator/screen properties. It collects no personally identifiable information and is not tied to any real person's identity.

Even so, device fingerprinting can be used to track users across sites without their knowledge. If you use this:

  • Get consent and disclose what you collect and why.
  • Don't use it for covert cross-site tracking.
  • Respect privacy regulations (GDPR, ePrivacy, CCPA, etc.) in your jurisdiction.
  • Treat the risk score and bot verdict as signals to a human or a larger system, not as automated grounds to deny someone access.

Project layout

src/
  core/        interfaces, orchestrator, registry  (depend only on contracts)
  signals/     canvas, webgl, audio, navigator collectors
  scoring/     entropy risk scorer
  bot/         webdriver, headless-plugin, interaction heuristics
  fingerprint.ts   pure FNV-1a hash of signals
  index.ts     composition root — the only place concretes are wired
test/
  contracts/   shared LSP contract suites (run against every impl)
  signals/  bot/  scoring/  + fingerprint, orchestrator, registry tests
ui/            vanilla-TS dashboard (no framework, dev-only)

Scripts

| Command | Description | | --- | --- | | npm run build | Type-check, then build ESM + UMD bundles and .d.ts | | npm test | Run the Vitest suite (jsdom) | | npm run dev | Start the UI dashboard dev server | | npm run typecheck | tsc --noEmit |

License

MIT