@ianmenethil/zp-devicefp
v0.2.0
Published
Async-first browser and device fingerprinting library with explainable signals and anomaly scoring.
Maintainers
Readme
Browser Fingerprint Library
Async-first TypeScript browser and device fingerprinting library. Collects 21 tiered browser signals, computes per-component SHA-256 hashes, returns a composite thumbprint, and includes an anti-spoof anomaly report with confidence scoring.
- Zero runtime dependencies — no CDN loads, no npm deps at runtime.
- ESM, CJS, and IIFE — single TypeScript source tree, three distribution formats.
- 92 tests across 24 files, 94.9% line coverage, 82.3% function coverage.
Install
bun add @ianmenethil/zp-devicefpimport { createFingerprintClient } from '@ianmenethil/zp-devicefp';Quick start
import { createFingerprintClient } from '@ianmenethil/zp-devicefp';
const client = await createFingerprintClient({ timeoutMs: 1500 });
// Async collection — all signals, parallel with timeouts
const result = await client.collect();
console.log(result.thumbprint); // "a1b2c3d4..." (64-char SHA-256 hex)
console.log(result.confidence); // 0.87
console.log(result.antiSpoof.score); // 0.94Named exports
npm / ESM / CJS (@ianmenethil/zp-devicefp)
Functions
| Export | Signature | Description |
| ------------------------- | ---------------------------------------------------------- | ----------------------------------------------- |
| createFingerprintClient | (options?: CollectOptions) => Promise<FingerprintClient> | Factory — creates a configured client instance. |
Constants
| Export | Type | Description |
| ------------------------- | --------------------------------------------- | ---------------------------------------------------------------- |
| LIBRARY_VERSION | string | Semver string of the installed library (e.g. "0.1.1"). |
| SCHEMA_VERSION | number | Integer schema version; bumped on breaking result-shape changes. |
| CORE_SIGNALS | SignalName[] | The 10 signals collected by default. |
| EXTENDED_SIGNALS | SignalName[] | The 11 signals requiring extended: true. |
| SYNC_SIGNALS | SignalName[] | The 13 signals that support collectSync(). |
| DEFAULT_TIMEOUT_MS | number | Default per-signal timeout: 1500. |
| DEFAULT_FONT_LIST | string[] | The 70 font faces probed during font detection (Fingerprint2 / Cardinal Commerce set). |
| DEFAULT_PERMISSION_NAMES| readonly [...] | The 4 permission names queried by the permissions signal. |
| DEFAULT_UA_HINTS | readonly [...] | The 8 high-entropy UA-CH hints requested. |
TypeScript types — all declared in the package root and importable directly:
import type {
// Client
FingerprintClient,
// Core result
FingerprintResult,
SignalResult, // generic: SignalResult<T = unknown>
SignalName,
SignalStatus,
AntiSpoofReport,
CollectorDiagnostics,
// Options
CollectOptions,
// Events
EventPayloadMap,
ProgressEvent,
WarningEvent,
CompleteEvent,
} from '@ianmenethil/zp-devicefp';CDN / IIFE (window.DeviceFP)
The IIFE bundle exposes only the three runtime values — TypeScript types are compile-time only and are not present on the global:
| Property | Type | Description |
| ------------------------- | ---------------------------------------------------------- | ----------------------------------------------- |
| createFingerprintClient | (options?: CollectOptions) => Promise<FingerprintClient> | Factory — creates a configured client instance. |
| LIBRARY_VERSION | string | Semver string of the running bundle. |
| SCHEMA_VERSION | number | Integer schema version of the result shape. |
API overview
| Method | Returns | Description |
| -------------------------------- | ---------------------------- | ------------------------------------------------------------------------------------- |
| client.collect(options?) | Promise<FingerprintResult> | Async collection. All signals run in parallel with per-signal timeouts. Emits events. |
| client.collectSync(options?) | FingerprintResult | Sync collection. Only 13 of 21 signals support sync mode. No timeout/abort. |
| client.on(event, callback) | () => void | Subscribe to progress, warning, or complete events during async collection. |
Events
client.on('progress', e => {
// { completed: 3, total: 10, signal: 'canvas', result: SignalResult }
console.log(`${e.completed}/${e.total}: ${e.signal}`);
});
client.on('warning', e => {
// { signal?: 'webrtc', message: 'RTCPeerConnection not available' }
console.warn(e.message);
});
client.on('complete', e => {
// { result: FingerprintResult }
console.log('Done', e.result.thumbprint);
});Aborting
const controller = new AbortController();
const resultPromise = client.collect({ abortSignal: controller.signal });
// Cancel mid-flight
controller.abort();
const result = await resultPromise;
// Aborted signals appear as status: 'error' with error: 'Collection aborted.'Options
interface CollectOptions {
include?: SignalName[]; // whitelist: only collect these signals
exclude?: SignalName[]; // blacklist: skip these signals
timeoutMs?: number; // per-signal timeout (default: 1500)
abortSignal?: AbortSignal; // cancel in-flight collection
extended?: boolean; // include extended-tier signals (default: false)
debug?: boolean; // enable debug logging (default: false)
}Result shape
interface FingerprintResult {
schemaVersion: number; // schema version (integer, bumped on breaking changes)
libraryVersion: string; // library semver string
thumbprint: string; // SHA-256 of all component hashes combined
confidence: number; // 0–1 quality score
componentHashes: Partial<Record<SignalName, string>>; // per-signal SHA-256 digests
signals: Partial<Record<SignalName, SignalResult>>; // raw signal results
antiSpoof: AntiSpoofReport; // coherence analysis and automation hints
warnings: string[]; // non-fatal collection warnings
diagnostics: CollectorDiagnostics; // run metadata
}AntiSpoofReport
{
score: number; // 0 = automated/bot, 1 = genuine user
anomalies: string[]; // signal inconsistencies (6 anomaly types)
automationHints: string[]; // headless/bot indicators (2 hint types)
fakedOS: boolean; // UA's OS contradicts navigator.platform
fakedBrowser: boolean; // UA claims Chrome but window.chrome is absent
}SignalResult
{
status: 'ok' | 'unsupported' | 'blocked' | 'timeout' | 'error';
value?: unknown; // signal-specific data (only when status === 'ok')
durationMs: number; // wall-clock collection time
error?: string; // error description (when status !== 'ok')
}Full API reference with all types, methods, events, options, confidence scoring, anti-spoof heuristics, and constants: docs/api-reference.md
Signals (21 total)
Core tier (10 signals, enabled by default)
| Signal | Sync | Description |
| ---------- | ---- | ----------------------------------------------------------------------------------------------------------------------- |
| ua | yes | navigator.userAgent, platform, webdriver, maxTouchPoints, cookieEnabled, vendor, doNotTrack (normalized), plugins (MIME format) |
| uaHints | no | User-Agent Client Hints: brands, platform, high-entropy values (architecture, bitness, model, etc.) |
| locale | yes | Language, languages, locale, calendar, numbering system, timeZone, formatted samples |
| screen | yes | Screen width/height, colorDepth, orientation, DPR, fakedResolution (FP2 has_lied_resolution), ccaScreenSize (Cardinal bucket), media queries |
| hardware | yes | navigator.hardwareConcurrency, deviceMemory, platform, maxTouchPoints, touchEventCreationSuccessful, onTouchStartAvailable |
| storage | yes | localStorage, sessionStorage, IndexedDB, Web SQL availability (write-read-delete probe) |
| fonts | yes | Installed font detection via hidden DOM span measurement (70 fonts, 3 generic families) |
| canvas | yes | Canvas 2D rendering pattern → toDataURL() (GPU+driver fingerprint) |
| webgl | yes | WebGL renderer/vendor (unmasked), version, extensions, max texture/viewport |
| audio | no | OfflineAudioContext oscillator+compressor numeric signature |
Extended tier (11 signals, requires extended: true)
| Signal | Sync | Description |
| ---------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| mediaDevices | no | Media device counts by kind (audioinput, audiooutput, videoinput) — no labels/IDs |
| permissions | no | Permissions API state for geolocation, notifications, camera, microphone |
| webrtc | no | RTCPeerConnection SDP codec/extmap capability hash |
| frameInfo | yes | Iframe count, source domains, top-level window check |
| networkInfo | yes | NetworkInformation API: effectiveType, downlink, RTT, saveData |
| paymentSupport | yes | window.PaymentRequest availability |
| referrerInfo | yes | document.referrer |
| navigationInfo | yes | PerformanceNavigationTiming.type (navigate/reload/back_forward/prerender) |
| riskSignals | no | Bot/headless detection: webdriver, headless UA, missing chrome object, zero dimensions, empty plugins, empty languages, UA-CH consistency, notification permission, permissions API headless hint, time to capture |
| adblock | no | Ad blocker detection — appends div.adsbox to body, checks offsetHeight === 0 after 100 ms |
| geolocation | no | Geographic coordinates (latitude, longitude, accuracy) via Geolocation API — prompts user permission |
Complete per-signal documentation with value shapes, APIs used, and edge cases: docs/signals.md
Anti-spoof heuristics (6 anomalies + 2 automation hints)
Anomalies (coherence violations, –0.12 each)
| Code | What it detects |
| ---------------------------------- | ----------------------------------------------- |
| ua_platform_mismatch | navigator.platform contradicts UA-CH platform |
| touch_claim_without_touch_points | iPhone UA but maxTouchPoints is 0 |
| mobile_ua_desktop_screen | Android UA with screen width >= 1600px |
| windows_ua_mac_platform | Windows UA but platform reports macOS |
| implausible_device_memory | deviceMemory < 0.25 or > 64 GB |
| tiny_screen_geometry | Screen width and height both < 200px |
Automation hints (bot/headless indicators, –0.18 each)
| Code | What it detects |
| --------------------- | ------------------------------- |
| navigator_webdriver | navigator.webdriver is truthy |
| headless_user_agent | UA contains "headless" |
Named booleans on AntiSpoofReport
| Field | Derived from | Meaning |
| ------------- | ----------------------------- | ---------------------------------------------------- |
| fakedOS | ua_platform_mismatch anomaly | UA's OS contradicts navigator.platform |
| fakedBrowser| missing-chrome-object hint | UA claims Chrome but window.chrome is absent |
Confidence scoring
Each signal has a fixed weight (0.2–1.0). Canvas, WebGL, fonts, and audio carry the highest weight. Status affects points earned: ok = full weight, unsupported/blocked = 35%, timeout = 15%, error = 0.
The base ratio is scaled by an anti-spoof factor: base × (0.65 + antiSpoofScore × 0.35). A perfect anti-spoof score (1.0) leaves the base unchanged; a zero score reduces it to 65%. Full details in docs/api-reference.md.
CDN
CDN files are available from jsDelivr (automatic from npm publish) and self-hosted.
| File | Format |
| ---------------------- | ---------------------------- |
| zp.dfp.js | IIFE (unminified) |
| zp.dfp.min.js | IIFE (minified) |
| zp.dfp.obf.js | IIFE (minified + obfuscated) |
| zp.dfp.esm.js | ESM for import() |
| zp.dfp.manifest.json | SRI integrity hashes |
<!-- jsDelivr -->
<script src="https://cdn.jsdelivr.net/npm/@ianmenethil/zp-devicefp/dist/cdn/zp.dfp.min.js"></script>
<!-- Self-hosted -->
<script src="https://cdn.zenithpayments.support/devicefp/zp.dfp.min.js"></script>
<script>
const client = await window.DeviceFP.createFingerprintClient({ timeoutMs: 1500 });
const result = await client.collect();
console.log(window.DeviceFP.LIBRARY_VERSION); // "0.1.1"
console.log(window.DeviceFP.SCHEMA_VERSION); // 2
</script>Package layout
dist/
├── npm/
│ ├── index.mjs # ESM entry (browser, ES2022)
│ ├── index.cjs # CJS entry (Node 22+)
│ └── index.d.ts # public types
└── cdn/
├── zp.dfp.js # IIFE (unminified)
├── zp.dfp.min.js # IIFE (minified)
├── zp.dfp.obf.js # IIFE (minified + obfuscated)
├── zp.dfp.esm.js # ESM for import()
└── zp.dfp.manifest.jsonBuild and test
bun install
bun run build # lint → typecheck → knip → jscpd → tests + coverage → esbuild → CDN deploy
bun test # 92 tests, 94.9% line coverage
bun run smoke # validates all dist artifactsDocs
| Document | Covers | | ---------------------------------------------- | ----------------------------------------------------------------------------------------- | | docs/api-reference.md | Complete API: types, methods, events, options, constants, confidence, anti-spoof, CDN | | docs/signals.md | All 21 signals: value shapes, APIs used, sync support, edge cases | | docs/architecture.md | Architecture, module map, Mermaid diagrams, data flow, confidence scoring | | docs/privacy.md | Privacy-by-design notes | | docs/testing.md | Test strategy and coverage intent |
Design choices
- Async first — high-value collectors (audio, UA hints, media devices, permissions, WebRTC, risk signals) run in
collect(). - Explainable output — every signal includes a status and duration. No silent failures.
- No dangerous defaults — no battery, sensors, arbitrary installed-app probes, or local IP harvesting.
- Anti-spoofing focuses on coherence checks and automation hints, not "secret" client tricks.
Limitations
- Browser-side fingerprints are probabilistic, not identity proofs.
- Font detection probes 70 common fonts (Fingerprint2 / Cardinal Commerce set) and relies on span-measurement heuristics.
- The anti-spoof report is heuristic — combine with server-side context for fraud or login-risk decisions.
