datalex-fingerprint
v1.1.1
Published
A TypeScript fingerprinting library for browser and device identification
Downloads
62
Maintainers
Readme
datalex-fingerprint 
A lightweight TypeScript library for browser and device fingerprinting. The SDK owns collecting, caching, and talking to the backend — you don't hand-write that glue. Ships three ways:
- npm package, core:
import { init } from 'datalex-fingerprint' - npm package, React:
import { useFingerprint } from 'datalex-fingerprint/react' - plain
<script>tag, no build step, no npm:window.DatalexFingerprint
Table of contents
- Install
- Quick start
- Core API reference
- React API reference
- Low-level / advanced API
- How collection works
- Development
- Changelog
Install
npm package
npm install datalex-fingerprintReact is an optional peer dependency — only needed if you import datalex-fingerprint/react.
Plain/vanilla/other-framework usage has zero extra dependencies.
# Only if you're using the React subpath:
npm install react react-domScript tag (no install)
Nothing to install — point a <script> tag at the hosted build (see
As a plain script tag below).
Quick start
As an npm package (vanilla / any framework)
import { init, ready } from 'datalex-fingerprint';
// Call once, anywhere at bootstrap. Pass the backend URL to send fingerprint data to — collection
// starts automatically once the page is idle, and the result is sent to that backend for you.
init({ apiUrl: 'https://api.yourbackend.com' });
// Read the result whenever you need it — resolves whether the hash came from cache or a fresh
// compute, and never rejects (a failure surfaces as `result.error`).
const { hash } = await ready();That's the whole surface: the SDK gives you a hash and stores fingerprint data on the backend.
It has no concept of sessions, users, or any other application identity — if you need to link a
fingerprint to a user or session on your backend, correlate it there using the hash.
As an npm package (React)
import { TouchpointProvider, useFingerprint } from 'datalex-fingerprint/react';
function App() {
return (
<TouchpointProvider config={{ apiUrl: 'https://api.yourbackend.com' }}>
<Checkout />
</TouchpointProvider>
);
}
function Checkout() {
const { hash } = useFingerprint();
async function onSubmit() {
// hash is whatever init() has produced so far — null if collection hasn't settled yet.
// Submit immediately either way; never block checkout on the fingerprint.
// ...submit the order with hash (may be null; handle that case server-side)
}
return <button onClick={onSubmit}>Pay now</button>;
}<TouchpointProvider> is a convenience, not a requirement — useFingerprint() works as soon as
init() has run anywhere (app bootstrap, a <script> tag, another component). If neither has run
yet, the hook reports status: 'uninitialized' instead of throwing.
As a plain script tag (CDN, no build step)
For a page with no bundler at all — a static checkout page, a CMS, a third-party embed. Everything
is bundled into one file and exposed as window.DatalexFingerprint, with the same API as the npm
core package.
<!-- Pin integrity="sha384-..." for the exact version you're loading, generated with:
openssl dgst -sha384 -binary touchpoint.global.js | openssl base64 -A -->
<script
src="https://cdn.datalex.solutions/sdk/v1/touchpoint.global.js"
integrity="sha384-..."
crossorigin="anonymous"
></script>
<script>
window.DatalexFingerprint.init({ apiUrl: 'https://api.yourbackend.com' });
window.DatalexFingerprint.ready().then(({ hash }) => {
console.log('fingerprint hash:', hash);
});
</script>No React build is published for the script-tag path — datalex-fingerprint/react is an npm-only
subpath. A vanilla page can still read window.DatalexFingerprint.ready()/subscribe() directly
from a <script> of its own.
Core API reference
Available from datalex-fingerprint (npm) and as window.DatalexFingerprint.* (script tag) —
identical API either way.
init(config)
Configures the SDK and (by default) starts collecting once the page is idle. Safe to call more
than once: later calls merge new config without restarting an in-progress or already-settled
collection. Returns a handle ({ ready, refresh, getResult, subscribe }) equivalent to the
standalone functions below.
apiUrl is required on every call — the SDK ships with no built-in backend URL and no
fallback. Omitting it (or passing an empty string) throws immediately, including from a plain
<script> tag with no TypeScript to catch it at compile time.
| Option | Type | Default | Description |
|---------------|-------------------------------|---------------------------|--------------|
| apiUrl | string | (required, no default) | Backend URL to send fingerprint data to. |
| autoCollect | boolean | true | Start collecting once idle. Set false to collect only on demand via ready(). |
| cache | boolean \| CacheOptions | true | Reuse the persisted hash across visits. Pass a CacheOptions object to customize the storage key/drivers/cookie domain. |
| timeout | number | 10000 | Hard ceiling (ms) on the whole collect. ready() always resolves by this point, never hangs. |
| onReady | (result: TouchpointResult) => void | — | Sugar over subscribe() for a fire-once callback. |
| debug | boolean | false | Logs a notice when a redundant init() call is ignored. |
ready(): Promise<TouchpointResult>
Resolves with the current (or in-flight, or freshly started) fingerprint result. Never rejects
— failures surface as result.error with hash: ''. Starts collection immediately if it hasn't
already, regardless of init()'s idle deferral — an explicit reader is never made to wait for idle.
interface TouchpointResult {
hash: string;
cached: boolean; // true if this result came from persisted storage, not a fresh compute
source?: 'cookie' | 'localStorage' | 'indexedDB' | 'memory'; // which layer served the cache hit
error?: { code: string; message: string; details: string[] };
}getResult(): TouchpointResult | null
Synchronous, no-op peek at the current settled result — null if collection hasn't settled yet.
Use ready() to actually wait for a result.
subscribe(callback): () => void
Registers a listener for the next settle (and any later refresh() update). Returns an unsubscribe
function. Does not replay the current value — call getResult() too if you need it immediately.
const unsubscribe = subscribe((result) => console.log('fingerprint updated:', result));
// later: unsubscribe();refresh(): Promise<TouchpointResult>
Forces a new collect cycle, discarding the in-memory settled result. Still reads/writes through the
persisted cache the same way init()'s auto-collect always does — this re-evaluates, it does not
bypass storage the way clearCache() would.
React API reference
Available from datalex-fingerprint/react. Requires react/react-dom ≥ 18 (peer dependencies).
<TouchpointProvider config autoInit? children>
Optional convenience that calls the core's init(config) from a declarative spot in the tree —
config (with its required apiUrl) is forwarded straight through. The provider itself isn't
required — useFingerprint() works with no provider as long as init() ran anywhere. Set
autoInit={false} to skip calling init() (e.g. it's already called elsewhere).
useFingerprint(): UseFingerprintResult
interface UseFingerprintResult {
status: 'uninitialized' | 'loading' | 'ready' | 'error';
hash: string | null;
data: TouchpointResult | null;
error: Error | null;
isLoading: boolean; // status === 'loading'
isReady: boolean; // status === 'ready'
refresh(): Promise<void>;
}SSR-safe by construction: on the server it returns a stable placeholder (status: 'uninitialized')
and the first client render matches it exactly, so there's no hydration mismatch — the real
fingerprint arrives on a subsequent update, never during the initial render.
useFingerprintValue(): string | null
Convenience wrapper for callers who only want the hash string (or null until ready / on error).
Low-level / advanced API
For cases where the init()-owned flow doesn't fit — full manual control over configuration,
collection, and storage:
| Export | Purpose |
|---|---|
| getFingerprint({ cache?, cacheOptions? }) | Compute (or read the cached) hash directly, without the init() singleton/auto-collect machinery. Talks to the same backend as everything else — call setApiBaseUrl() first (see below). |
| generateAndStore({ cache?, cacheOptions? }) | Compute the fingerprint and POST it to the backend in one call. |
| generateFingerprint(options?) | Hash of just the user agent string — lighter-weight, no device collectors. |
| getUserAgent(), getMediaDevices(), getRawMediaDevices(), getWebGLFingerprint(), getAudioFingerprint() | Individual collectors, if you want to assemble your own fingerprint. |
| hashFingerprint(data, options?), createFingerprint(userAgent, additionalData?), isValidFingerprint(value) | Hashing primitives. |
| clearCache(options?), peekCache(options?), isPersistenceAvailable(options?), getCachedFingerprint(compute, options?) | Direct access to the persisted-hash cache (cookie → localStorage → IndexedDB → memory fallback). |
| storeToBackend(request), findSimilarFingerprints(hash, threshold?, limit?) | Direct backend calls — will fail with an API_URL_NOT_CONFIGURED error until setApiBaseUrl() has been called. |
| setApiBaseUrl(apiUrl), getApiBaseUrl() | Required if you're using the low-level API without init(): there's no built-in backend URL, so getApiBaseUrl() returns null until you call setApiBaseUrl(apiUrl). |
How collection works
- One hash definition.
init()'s auto-collect,ready(),getFingerprint(), andgenerateAndStore()all derive the hash from the same collector pipeline, so they never disagree on what "the fingerprint" is for a given device. - Environment metadata doesn't affect the hash. Screen resolution, timezone, language, and platform are sent to the backend as context but excluded from the hash itself — a monitor change or a flight doesn't make the same device look like a new one.
- Collectors run in parallel, each with its own timeout. A slow or stuck component (e.g. audio fingerprinting in a backgrounded tab) degrades to that component's own "unavailable" value instead of blocking the rest of collection or hanging indefinitely.
- Concurrent callers share one run. Two components calling
ready()at once, or React StrictMode's double-invoke, trigger exactly one collection cycle and one set of backend calls. - Caching persists across cookie, localStorage, and IndexedDB, self-healing any layer that was missing, so a returning visitor's hash survives clearing any single storage mechanism.
Development
# Install dependencies
npm install
# Typecheck
npm run typecheck
# Run tests
npm test
# Build all artifacts (core esm/cjs, react esm/cjs, CDN global, type declarations)
npm run buildBuild outputs in dist/:
| File | What it's for |
|---|---|
| touchpoint.esm.js / touchpoint.cjs.js | npm core package (. export) |
| react/index.esm.js / react/index.cjs.js | npm React subpath (./react export) |
| touchpoint.global.js | Script-tag / CDN build, exposes window.DatalexFingerprint |
| types/ | TypeScript declarations for both entry points |
Changelog
See CHANGELOG.md for a detailed history of changes.
