@kaidn/fp
v1.1.0
Published
Kaidn device fingerprint — thin browser client. Computes a stable device_id + automation signals (headless/UA/JA4 beacon) to pass to the Kaidn fraud-scoring API. Open client, closed engine.
Maintainers
Readme
@kaidn/fp
Browser device-fingerprint client for Kaidn. Computes a stable
device_id plus automation signals (headless / UA-spoofing) and beacons them to the
Kaidn edge so the connection's JA4 TLS fingerprint is captured against that device.
Browser-only, and safe there. This package holds no secret — the beacon uses a publishable key (
pk_live_…) that is domain-locked in your Kaidn dashboard. Your server then scores with the samedevice_idusing your secret API key — via@kaidn/sdk(Node),kaidn(Python), or a plain HTTP POST from any other language. Never put your API key in the browser.
device_idis an input, not an identity. It is a raw attribute hash and it collides across unrelated people (2.30 per fingerprint on iOS Safari in production data), because a default iPhone is identical to another default iPhone./v1/scoreresolves it server-side into adevice.resolved_idand returns the measuredcollision_riskalongside it. Link visits on that, never on this hash. See the device identity docs.
Install
npm install @kaidn/fpOr drop the pre-built tag in — no build step, window.Kaidn is set for you:
<script src="https://api.kaidn.io/fp/pk_live_xxx.js" defer></script>Usage (plain JS, no build step)
The tag sets window.Kaidn. Configure it, then call init():
<script src="https://api.kaidn.io/fp/pk_live_xxx.js" defer></script>
<script>
document.addEventListener("DOMContentLoaded", function () {
// optional: attach your own variables to the beacon
Kaidn.store("user_id", "u_1024");
// fingerprint on submit, then let the form through.
// A hidden `kaidn_device_id` field is appended for your backend to read.
Kaidn.trigger("#signup-form");
// optional: see what was collected (this is NOT a score — see below)
Kaidn.afterResult(function (r) { console.log(r.device_id); });
// optional: an ad-blocker or a blocked script lands here
Kaidn.afterFailure(function (reason) { console.warn("kaidn fp failed", reason); });
Kaidn.init();
});
</script>⚠️ Wrap the config in DOMContentLoaded (or drop defer from the tag). A
deferred script runs after the document is parsed, while a plain inline script
runs as it is parsed — so inline code placed directly after the tag executes
first and throws Kaidn is not defined.
Your backend then reads the posted kaidn_device_id and passes it to /v1/score:
$device_id = $_POST['kaidn_device_id'] ?? null;window.Kaidn
| Method | What it does |
|---|---|
| init() | Start. Fingerprints immediately unless pause() was called. |
| trigger(selector, before?) | Bind a form or element. On submit/click it fingerprints first, appends kaidn_device_id (plus any store() vars) as hidden fields, then proceeds. before runs on the raw event. |
| store(key, value) | Attach a custom variable (user_id, transaction_id, …) to the beacon and to the appended fields. |
| afterResult(fn) | Runs after a successful collect, with { device_id, device, attributes, anomalies, vars }. |
| afterFailure(fn) | Runs when collection or the beacon fails: blocked script, ad-blocker, timeout. |
| pause() / resume() | Hold the page-load fingerprint so you can store() data that is not ready yet. |
| watch(options?) | Session heartbeat: re-beacons the same device_id about every 60s and on tab refocus. Returns { stop() }. |
afterResult gives you the collection, never a verdict. Scoring is
server-side by design, so nothing the browser receives can be tampered with to
change an outcome.
trigger() will not hang a form. If collection stalls (a blocked script, a slow
device) it gives up after ~2.5s and submits anyway, so the worst case is a
signup with no device_id rather than a signup that never happens.
The beacon posts to the origin the script was served from, so the JA4 is
captured against your end user's own TLS connection. Override with
data-endpoint on the script tag if you proxy it.
Usage (bundler / SPA)
import { beacon } from "@kaidn/fp";
// on your signup / login / checkout page, from the END USER's browser:
const fp = await beacon("https://api.kaidn.io/v1/fp", "pk_live_xxx");
// submit fp.device_id alongside your form; your backend passes it to /v1/score
form.elements.namedItem("device_id").value = fp.device_id;collect() computes the fingerprint without any network call:
import { collect } from "@kaidn/fp";
const { device_id, device, attributes, anomalies } = await collect();Why the browser call matters
JA4 is the fingerprint of whoever opens the TLS connection. Only a direct
browser→edge request (this beacon) captures the real end user's TLS stack; a
server-to-server call would capture your own backend's. The beacon associates the
JA4 with device_id, and your later /v1/score lookup inherits it.
Exports
collect(options?)— compute{ device_id, device, attributes, anomalies }(no network)beacon(endpoint, pk, options?)—collect()+ best-effort POST to/v1/fpwatch(endpoint, pk, options?)— session heartbeat: fingerprints once, then re-beacons the samedevice_idevery ~60s (and on tab refocus) so Kaidn sees the connection's IP over time. Becausedevice_id+ JA4 stay constant across a VPN change, a beacon whose IP flips connection type mid-session (a dropped VPN leaking the real home IP, or a device that starts cloaking) is caught by scoring. Returns{ stop() }.createTracker(deps)— the testable core behind thewindow.Kaidndrop-in tagdetectAutomation,checkUaConsistency,parseUserAgent,flattenComponents,pickWebglRenderer— the pure signal helpers
The verdict never comes back to the browser (by design) — scoring stays server-side.
Server side
Whatever your backend is, it does the same thing: take the kaidn_device_id
this package put on the form, and send it to /v1/score with your secret key.
The verdict comes back there, never to the browser.
Node / TypeScript — @kaidn/sdk
import { Kaidn } from "@kaidn/sdk";
const kaidn = new Kaidn({ apiKey: process.env.KAIDN_API_KEY! });
const { verdict } = await kaidn.score({ event: "signup", device_id, ip, email });Python — kaidn
from kaidn import KaidnClient
client = KaidnClient() # reads $KAIDN_API_KEY
r = client.score(event="signup", ip=ip, email=email,
device_id=form.get("kaidn_device_id"))
if r.blocked:
...PHP, Ruby, Go, Java, anything else — there is no SDK to wait for. /v1/score
is one JSON POST with an x-api-key header:
$ch = curl_init('https://api.kaidn.io/v1/score');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['content-type: application/json',
'x-api-key: ' . getenv('KAIDN_API_KEY')],
CURLOPT_POSTFIELDS => json_encode([
'event' => 'signup',
'ip' => $_SERVER['REMOTE_ADDR'],
'email' => $email,
'device_id' => $_POST['kaidn_device_id'] ?? null, // from @kaidn/fp
]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 2, // this sits on your signup path
]);
$res = json_decode(curl_exec($ch), true); // ['verdict' => 'allow'|'review'|'block', ...]Two rules worth keeping wherever you call it from: give it a hard timeout, and fail open. A fraud check that is slow or down must never become a signup that is slow or down.
