fingerprint-platform-sdk
v0.1.5
Published
Browser SDK for the self-hosted Fingerprint Platform — collects device signals, encrypts the payload (X25519+AES-GCM), and ships it to the collector.
Maintainers
Readme
fingerprint-platform-sdk
Browser SDK for the self-hosted Fingerprint Platform. Collects 50+ device signals (canvas/webgl/audio hashes, fonts, timezone, hardware, behavioral aggregates, lie-detection), encrypts the payload with X25519 + AES-256-GCM, and ships it to your collector.
The collector pairs each event with server-side signals it observes
directly from the request (real IP, JA4 TLS fingerprint, header order,
HTTP/2 frame fingerprint) and returns a suspectScore 0–100 plus a list
of risk flags.
Install
npm install fingerprint-platform-sdk
# or
pnpm add fingerprint-platform-sdk
# or
yarn add fingerprint-platform-sdkUse it (ESM / bundlers)
import { init } from 'fingerprint-platform-sdk';
const fp = init({
collectorUrl: 'https://fp.your-domain.com',
// The api_key issued for your tenant when the project was provisioned.
// Sent on every ingest as the `X-Project-Key` header. NOT the
// human-readable project id — that's only used in the dashboard URL.
// Without an api_key, every request returns 401 unknown_project.
projectKey: 'Id6MOAy8K3pQpwAMc9yqQjuxmupb2Wvo4GpOBw2C6hw',
// optional: pin a known server pubkey to skip the /v1/pk round-trip
// serverPublicKeyB64: '...',
});
// On a meaningful action (login, signup, checkout, …)
const result = await fp.identify({
event: 'login',
linkedId: 'user-12345',
});
console.log(result.eventId); // "1700000000000.abc12X"
console.log(result.visitorId); // "cD6f7B607A518A37aB38" (mixed-case alphanumeric,
// 16–64 chars — sync mode is default; pass
// { sync: false } for fire-and-forget)Use it (script tag — no bundler)
<script src="https://unpkg.com/fingerprint-platform-sdk/dist/fp.js" async></script>
<script>
window.addEventListener('load', async () => {
window.FP.init({
collectorUrl: 'https://fp.your-domain.com',
projectKey: 'YOUR_API_KEY', // see «projectKey» in the table below
});
const r = await window.FP.identify({ event: 'pageview' });
console.log(r);
});
</script>Auto-init via global config
<script>
window.__FP_CONFIG__ = { collectorUrl: 'https://fp.your-domain.com' };
</script>
<script src="https://unpkg.com/fingerprint-platform-sdk/dist/fp.js" async></script>init is called automatically as soon as the SDK loads.
API
init(config: FpConfig): FpSdk
| Field | Required | Description |
| -------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| collectorUrl | ✅ | Origin where the platform's collector is reachable, e.g. https://fp.your-domain.com. The SDK appends /v1/pk and the (path-randomised) ingest endpoint. |
| projectKey | ✅* | The api_key issued for your tenant — sent on every ingest as the X-Project-Key header. This is the random opaque string the platform gave you when the project was provisioned (not the human-readable project id used in dashboard URLs). Omit only if your collector deployment exposes a permissive default project; otherwise every request returns 401 unknown_project. |
| serverPublicKeyB64 | | Pre-shared public key (base64). Skips the bootstrap /v1/pk round-trip. Useful when you control deploys end-to-end. |
| sdkVersion | | Defaults to the package version. Override for A/B-testing custom builds. |
identify(opts?: IdentifyOptions): Promise<IngestResponse>
| Field | Type | Description |
| -------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| event | 'pageview' \| 'login' \| 'signup' \| 'action' \| 'heartbeat' | Defaults to 'pageview'. |
| linkedId | string | Optional. Integrator-supplied identifier for the user — typically your authenticated user id, an order number, or a session token. Lets the platform tie a device fingerprint (visitorId) back to your account model. The userId parameter is a deprecated alias kept for pre-0.1.4 callers. |
| sync | boolean | Default true. The collector blocks the response until scoring finishes (~50–150 ms). Pass false for passive page-view tracking — see «Sync vs async» below. |
| preferBeacon | boolean | When true, prefers navigator.sendBeacon (fire-and-forget, fast). Implies sync: false. Default false. |
Sync vs async
fp.identify() defaults to synchronous scoring: the HTTP response
body carries the real visitorId and suspectScore once the scoring
worker has finished. This is the right default for click-driven flows
(login submit, checkout) where the integrator wants to act on the
verdict inline.
For passive page-views or analytics where you don't need the verdict on the wire, opt out:
fp.identify({ event: 'pageview', sync: false });
// → returns { visitorId: 'pending', suspectScore: -1 } immediately;
// real verdict is delivered via your webhook or pulled later via
// GET /v1/result/<eventId>.If the scoring worker doesn't publish a result within
SYNC_TIMEOUT_MS (default 2s on the collector), sync calls fall back
to the same pending response — the request is never lost, you just
don't see the verdict on this RPC.
Returns the IngestResponse:
interface IngestResponse {
v: string;
eventId: string; // 6-char id, returned immediately
result: 'ok' | 'retry' | 'reject';
visitorId: string; // 16–64 char alphanumeric device id, or "pending" while scoring is async
userId?: string; // echoed
suspectScore: number; // 0–100, or -1 while pending
external: {
flags?: string[];
linkedVisitors?: string[];
};
}What gets sent
The SDK collects, then encrypts, the following before posting:
- Hardware: User-Agent, Client Hints, screen, hardware concurrency, device memory.
- Locale: timezone, language, languages list.
- Rendering hashes: canvas, WebGL, audio (each rendered 3× and the majority hash kept for stability).
- Fonts & plugins.
- Permissions map (notifications/geolocation/camera/…).
- WebRTC local IP (for VPN detection on the server).
- Behavioral aggregates: mouse velocity, keyboard hold/gap timings, scroll inertia.
- Lie detection:
navigator.webdriver,Function.prototype.toStringintegrity, iframe / worker comparisons.
The full schema lives in @fp/shared-types.
What gets received from the request
Outside what the SDK sends, the collector also reads (browser cannot fake):
- Real source IP (or
X-Forwarded-Forbehind nginx / Caddy). - TLS JA4 fingerprint when the request goes through the platform's TLS edge.
- HTTP/2 SETTINGS frame order (Akamai-style).
- HTTP header order — distinguishes browsers from
python-requests/axios/Go net/http.
Bundle size
| Format | Size |
| ----------------------------------------------- | ------------------------ |
| dist/fp.js (IIFE, terser) | ~60 KB / ~22 KB gzipped |
| dist/fp.obf.js (IIFE + javascript-obfuscator) | ~300 KB / ~80 KB gzipped |
| dist/index.mjs (ESM, for bundler consumers) | ~60 KB / ~22 KB gzipped |
Building from source (for platform maintainers)
This package is published from the Fingerprint Platform monorepo. Two CI-controlled knobs harden the production bundle:
| Env var | Effect |
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| SDK_DOMAIN_LOCK="host1,host2" | Bakes a hostname allow-list into fp.obf.js. Loaded from any other origin → SDK self-redirects to about:blank. |
| SDK_HARDEN=true | Enables selfDefending, debugProtection (DevTools-detection debugger loop), and disableConsoleOutput. |
SDK_DOMAIN_LOCK="fp.example.com" SDK_HARDEN=true \
pnpm --filter fingerprint-platform-sdk buildTo publish:
cd apps/sdk-browser
pnpm version patch # bump (or minor / major)
pnpm publish --dry-run # inspect tarball — verifies no .tsbuildinfo /
# .d.ts.map / source files leak
pnpm publish --access publicUse pnpm publish (not npm publish) — the workspace devDependencies
are written as workspace:* in package.json. pnpm rewrites those to
real published versions on publish; npm would publish the literal
workspace:* and the package would be uninstallable.
prepublishOnly regenerates dist/ (ESM + CJS + IIFE + types) before
each publish; nothing leaks the workspace's @fp/* internals — vite
bundles them inline (see vite.config.ts → external: []).
License
MIT — see LICENSE.
