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

doorman-benny

v1.25.0

Published

High-entropy device fingerprint library using browser API signals

Readme

Benny the Doorman

High-entropy device fingerprinting that measures what the hardware does, not just what the browser reports.

A production-grade browser fingerprinting library for fraud detection, bot detection, MFA, and cross-browser device identification. Zero runtime dependencies, ~52 KB minified, TypeScript-native.

Upgrading from 1.2.x? The canvas signal hash changes in 1.3.0 because the collector now does a 3-run mode-merge on raw RGBA bytes (was: single-render PNG bytes). On a stable browser the new hash is deterministic and stable across calls, but it does not equal the 1.2.x value. hardwareFingerprint is unaffected. If you store full fingerprint values server-side, treat this as a one-time migration.


Features

  • Two-Level Fingerprinting — browser-instance fingerprint and cross-browser hardware fingerprint
  • Hardware Binding — identifies the same physical device across different browsers
  • Anti-Detect Detection — flags spoofed / virtualized / anti-fingerprint browsers
  • Automation Detection — flags Puppeteer, Playwright, Selenium, stealth-plugin drivers
  • Incognito Detection — informational private-mode signal
  • Cross-Browser Stable — same hardware hash across Chrome, Safari, Firefox, Brave
  • Performance Optimised — parallel collection, configurable timeouts and tiering
  • Zero Runtime Dependencies — single self-contained bundle
  • TypeScript Native — full type definitions included

Installation

npm install doorman-benny

Quick Start

The library has two top-level entry points. Pick based on what you need:

import { getDeviceId, getFingerprint } from 'doorman-benny';

// Cross-browser DEVICE identity (~200–300ms, stable across browsers / incognito).
// Use for: MFA, device-bound auth, cross-browser linking, license enforcement.
const { id, confidence, consistency, incognito } = await getDeviceId();
console.log(id);          // "7c2e9a4b8f1d6e3a" — same on Chrome / Brave / Safari for the same device
console.log(confidence);  // 0.0–1.0 — collection quality

// Full BROWSER-INSTANCE fingerprint (~1–1.5s, differentiates same-device sessions).
// Use for: session tracking, fraud detection, bot detection.
const result = await getFingerprint();
console.log(result.fingerprint);          // "a3f8e2d1c9b4f6a8" — per-browser-instance
console.log(result.hardwareFingerprint);  // "7c2e9a4b8f1d6e3a" — cross-browser device hash
console.log(result.consistency);          // Anti-detect browser detection
console.log(result.incognito);            // Private-mode detection
console.log(result.automation);           // Puppeteer / Playwright / Selenium / stealth detection
console.log(result.crossBrowser);         // Cross-browser confidence score

Rule of thumb:

  • Identifying a devicegetDeviceId
  • Identifying a browser instancegetFingerprint

Background Collection

import { createCollector } from 'doorman-benny';

const collector = createCollector();
collector.start();                       // Begin early in page load

// ... user interaction, page load, etc. ...

const result = await collector.getResult();

Comparing Fingerprints

import { getFingerprint, compareFingerprints } from 'doorman-benny';

const fpA = await getFingerprint();
// ... later, or on different browser ...
const fpB = await getFingerprint();

const comparison = compareFingerprints(fpA, fpB);
console.log(comparison.match);            // true/false
console.log(comparison.matchScore);       // 0-100 percentage
console.log(comparison.hardwareMatch);    // Same device?
console.log(comparison.similarity);       // 0.0-1.0 weighted score

Configuration

interface FingerprintOptions {
  tiers?: number[];                       // Which tiers to collect. Default: [1]
  timeout?: number;                       // Global ms ceiling. Default: 5000
  componentTimeout?: number;              // Per-signal ms cap. Default: undefined (no cap)
  exclude?: string[];                     // Signal names to skip. Default: []
  debug?: boolean;                        // Include raw values. Default: false
  stabilize?: ('private' | 'iframe')[];   // Opt-in farble stabilization. Default: undefined
}

Three tiers are available:

  • Tier 1 — fast, high-reliability signals (default)
  • Tier 2 — broader coverage, slightly slower
  • Tier 3 — optional, opt-in (tiers: [1, 2, 3])

A user-configurable global timeout caps total collection time. Set componentTimeout to also cap each individual signal independently — useful when you'd rather drop one slow signal than miss the entire collection.

stabilize: ['private'] drops signals known to be farbled or noised by the detected browser (canvas / audio / speech voices on Brave, Firefox, Safari 17+, Samsung Internet) so the resulting hash stays stable across calls on those browsers, at the cost of entropy on the affected platform. The setting is opt-in — leaving it unset preserves the default behaviour from prior versions.


Result Structure

interface FingerprintResult {
  fingerprint: string;              // Full hash (16-char hex)
  hardwareFingerprint: string;      // Hardware-only hash (16-char hex)
  version: string;                  // Library version
  collectionTimeMs: number;
  signals: Record<string, SignalResult>;
  consistency: ConsistencyResult;   // Anti-detect browser detection
  crossBrowser: CrossBrowserScore;  // Cross-browser confidence
  incognito: IncognitoResult;       // Private/incognito mode detection
  automation: AutomationResult;     // Automation framework detection
  errors?: FingerprintError[];      // Collector/pipeline errors (present only if non-empty)
}

interface SignalResult {
  value: unknown;             // Raw value (debug mode only)
  hash: string;               // 16-char hex hash
  confidence: 'normal' | 'degraded' | 'absent' | 'stabilized';
  binding: 'hardware' | 'engine';
  timeMs: number;
  errorMessage?: string;      // Diagnostic when this signal failed (capped at ~200 chars)
  sentinel?: 'unsupported' | 'threw' | 'timeout' | 'randomized';  // Diagnostic tag (1.17.0+)
}

`sentinel` is absent on healthy `normal` collections. `'unsupported'` fires when the required browser API does not exist. `'threw'` fires when the collector raised an exception (hash is `'__absent__'`). `'timeout'` fires when the collector did not finish within the global deadline (hash is `'__absent__'`). `'randomized'` fires when noise mitigation detected per-call randomisation — hash carries the real mode-merged value rather than `'__absent__'`, making it usable for best-effort identification while the sentinel flags the instability for analytics.

interface FingerprintError {
  type: 'collector_threw' | 'collector_timeout' | 'fatal';
  component?: string;
  message: string;
}

interface ConsistencyResult {
  score: number;                                 // 0.0 – 1.0
  flags: string[];
  spoofLikelihood: 'low' | 'medium' | 'high';
}

interface IncognitoResult {
  score: number;                                  // 0.0 – 1.0
  flags: string[];
  incognitoLikelihood: 'low' | 'medium' | 'high';
}

interface AutomationResult {
  score: number;                                   // 0.0 – 1.0
  flags: string[];
  automationLikelihood: 'low' | 'medium' | 'high';
}

interface ComparisonResult {
  matchScore: number;         // 0-100 percentage
  match: boolean;             // true if matchScore >= threshold (default 85)
  hardwareMatch: boolean;
  similarity: number;         // 0.0-1.0
  hardwareSimilarity: number;
  constraintViolations: string[];
  signalComparison: Record<string, SignalComparisonDetail>;
  diffVector: number[];       // ML feature vector
}

spoofLikelihood, incognitoLikelihood, and automationLikelihood are derived from internal flag counts. Treat medium as suggestive, high as conclusive.


Comparison Modes

compareFingerprints supports six modes for different use cases:

const comparison = compareFingerprints(fpA, fpB, {
  mode: 'cross-browser'    // 'exact' | 'cross-browser' | 'hardware-only'
                           // 'engine-only' | 'strict' | 'lenient'
});

| Mode | Use For | |------|---------| | cross-browser | Recommended for device matching across browsers | | hardware-only | Strictest cross-browser device verification | | engine-only | Detecting browser changes on the same device | | exact | Default — all signals with fuzzy matching | | strict | No fuzzy matching — flag any change | | lenient | Maximum tolerance |

interface ComparisonOptions {
  mode?: 'exact' | 'cross-browser' | 'hardware-only' | 'engine-only' | 'strict' | 'lenient';
  includeSignals?: string[];
  excludeSignals?: string[];
  fuzzyMatching?: boolean;     // Default: true
  matchThreshold?: number;     // 0-100, default: 85
  checkConstraints?: boolean;  // Default: true
  fuzzyThresholds?: FuzzyThresholds;
}

Fuzzy matching gracefully handles browser resize, font additions, timezone changes, minor signal variations, and DPR rounding.


Common Use Cases

Multi-Factor Authentication

const currentFp = await getFingerprint();
const savedFp  = await loadFromServer();

const comparison = compareFingerprints(currentFp, savedFp);
if (!comparison.hardwareMatch) {
  requireAdditionalVerification('Different device detected');
}

Cross-Browser Device Matching

const fp = await getFingerprint();
sendToServer({ deviceId: fp.hardwareFingerprint });

Fraud Detection

const fp = await getFingerprint();
if (fp.consistency.spoofLikelihood === 'high') {
  flagForReview('Anti-detect browser likely');
}

Bot Detection

const fp = await getFingerprint();
if (fp.automation.automationLikelihood === 'high') {
  blockRequest('Automation framework detected');
} else if (fp.automation.automationLikelihood === 'medium') {
  requireCaptcha();
}

Submit to Benny the Doorman server

import { getFingerprint, submitToBenny, SubmitToBennyError } from 'doorman-benny';

const fingerprintResult = await getFingerprint();
try {
  const response = await submitToBenny(fingerprintResult, {
    endpoint: 'https://eu.api.bennythedoorman.com/v1/identify',
    apiKey: 'pk_live_…',
    subjectId: currentUserId,  // strongly recommended for GDPR DSAR
  });
  console.log(response.visitorId, response.matchConfidence);
} catch (err) {
  if (err instanceof SubmitToBennyError) {
    console.error('identify failed', err.status, err.requestId, err.errors);
  }
}

Performance

| Browser | Typical Tier 1 Collection | |---------|---------------------------| | Chrome / Edge | ~1200 ms | | Firefox | ~1400 ms | | Safari | ~1600 ms |

For best UX, use the background collector (createCollector()) so collection overlaps with page load.


Browser Compatibility

| Browser | Support | |---------|---------| | Chrome 90+ | Excellent | | Edge 90+ | Excellent | | Firefox 88+ | Good | | Safari 14+ | Good | | Mobile Chrome | Good | | Mobile Safari | Good |

Signals unsupported by a given browser are marked as absent rather than failing the run.


Native SDKs (preview)

Native iOS and Android ports of the same fingerprinting pipeline live alongside the web SDK in this repo. Both produce byte-identical hashes to the web SDK for any signal that has a cross-platform equivalent — so a single hardwareFingerprint value can identify the same device across Safari, your iOS app, and your Android app.

| Platform | Location | Status | |---|---|---| | iOS / macCatalyst | packages/ios/ — Swift Package | Preview — core (xxHash64, fusion, hardware hash) + 2 of 26 signals implemented; rest are typed stubs | | Android | packages/android/ — Gradle library module | Preview — same scope as iOS |

The cross-language contract (xxHash64 primes, SIGNAL_ORDER, HARDWARE_SIGNALS, __absent__ sentinel, confidence/sentinel matrix, test vectors) is documented at docs/kb/shared/contract.md. The platform-specific signal maps and the cross-surface matching guide live alongside it under docs/kb/shared/.


API Reference

getDeviceId(options?): Promise<DeviceIdResult>

Cross-browser device identifier. Skips engine-bound collectors entirely — fast and stable across browsers / incognito.

Returns: DeviceIdResult with id, confidence, stableSignals, unstableSignals, consistency, incognito, automation, collectionTimeMs, version, signals.

Use when: cross-browser device matching, MFA, account-device binding, license enforcement.

getFingerprint(options?): Promise<FingerprintResult>

Full browser-instance fingerprint (hardware + engine signals).

Use when: maximum entropy for fraud / bot detection, distinguishing two users on the same physical device.

createCollector(options?): CollectorHandle

Background collector. start() begins collection; getResult() returns the result (auto-starts if needed).

compareFingerprints(a, b, options?): ComparisonResult

Compares two fingerprints with mode-aware fuzzy matching.

submitToBenny(result, options): Promise<IdentifyResponse>

Posts a FingerprintResult to a Benny the Doorman server's POST /v1/identify endpoint and returns the parsed IdentifyResponse. Stamps issuedAt: Date.now() automatically. Throws SubmitToBennyError on any failure (network error, abort, timeout, non-2xx HTTP, JSON parse error).

Required options: endpoint: string (full URL, e.g. https://eu.api.bennythedoorman.com/v1/identify), apiKey: string (pk_live_… or pk_test_…).

Notable optional options: subjectId?: stringstrongly recommended for GDPR DSAR fulfilment; without it, erasing a data subject requires a full table scan rather than an indexed lookup. linkedId?: string, tag?: Record<string, unknown>, clientVersion?: string, tiers?: readonly number[], origin?: stringrequired for Node.js callers (browsers stamp Origin automatically; Node's native fetch does not, so omitting this causes a 403 from the server's Origin allowlist). signal?: AbortSignal (cancellation), timeoutMs?: number (auto-aborts after N ms — implemented via setTimeout + a private AbortController, with any caller-supplied signal merged in via a one-shot listener; deliberately avoids AbortSignal.any for Node 18 compatibility), fetch?: typeof fetch (injectable for testing).

Response shape: IdentifyResponse — see v0_spec §8.3 for the full field list (requestId, visitorId, isNew, firstSeen, lastSeen, matchConfidence, linkedId, tag, errors).

SubmitToBennyError

Thrown by submitToBenny on any failure path. Extends Error.

Properties: status: number — HTTP status code on a non-2xx server response; sentinel 0 when no HTTP exchange happened (network error, abort, timeout, body-serialization failure). requestId: string | null — echoed from the server error envelope when present; use this to pivot against reqId in the server logs. errors: ReadonlyArray<IdentifyResponseError> — the server's errors[] envelope on a non-2xx, or a single synthetic entry on transport failures (NETWORK_ERROR, TIMEOUT, ABORTED, PARSE_ERROR, BODY_SERIALIZATION_ERROR, FETCH_UNAVAILABLE, INVALID_ENDPOINT, INVALID_API_KEY_INPUT, HTTP_ERROR). Always at least one entry.

try {
  const response = await submitToBenny(fingerprintResult, { endpoint, apiKey });
  console.log(response.visitorId);
} catch (err) {
  if (err instanceof SubmitToBennyError) {
    console.error(err.status, err.requestId, err.errors);
  }
}

VERSION: string

Library version constant.


Privacy Considerations

Benny the Doorman is a fingerprinting library intended for legitimate security and fraud-prevention use cases:

  • Fraud detection
  • Bot detection
  • Account security (MFA)
  • Analytics (device counting)

Best practices:

  • Obtain user consent where required by law
  • Comply with GDPR / CCPA / equivalent regulations
  • Combine with other authentication methods
  • Don't rely solely on fingerprints for critical decisions
  • Implement server-side verification

License

Source-Available Non-Redistribution License — free to use, run, and modify for personal, internal, commercial, or non-commercial purposes, but you may not redistribute, resell, sublicense, or host the Software as a paid offering. See LICENSE for full terms.

For redistribution rights, OEM bundling, or any usage outside the granted license, contact the copyright holder for a commercial agreement.


Support & Commercial Licensing

For feature requests, integration support, or commercial licensing inquiries, contact the copyright holder.


Built for security teams, fraud-detection engineers, and platforms that need robust device identification.