{"_id":"@atol-sh/fingerprint","_rev":"2-dd2a6cd8b99a0b75be29ff1a67698f4b","name":"@atol-sh/fingerprint","dist-tags":{"latest":"0.2.0"},"versions":{"0.1.0":{"name":"@atol-sh/fingerprint","version":"0.1.0","keywords":["atol","fingerprint","device-fingerprinting","device-intelligence","bot-detection","fraud-detection","browser-signals"],"author":{"name":"Atol"},"license":"Apache-2.0","_id":"@atol-sh/fingerprint@0.1.0","maintainers":[{"name":"remiphilippe","email":"remi@aiku.fr"}],"homepage":"https://atol.sh","bugs":{"url":"https://github.com/atol-sh/atol-fingerprint-js/issues"},"dist":{"shasum":"5e203108648ebc33d3e7984edc633bdce3ad8888","tarball":"https://registry.npmjs.org/@atol-sh/fingerprint/-/fingerprint-0.1.0.tgz","fileCount":11,"integrity":"sha512-qM6pVg15e60gufIIvWiLaha6Tv8XxW1uKdsJ9Xs9qD25D+yMDdQ4PccrfOZj9q+z0k9MT77BVYiaGaLLvKtt/w==","signatures":[{"sig":"MEUCIEEE7NKx8rOB33jewpBqz706zz/uwTke+TCXFEXn8iT1AiEAlDDCdUevyKxs7mEVv9DU1h/Kx8ZidVTcYJdDe5KXwes=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":118996},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"default":"./dist/index.js","require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"gitHead":"890cfd23d2232eacb8b4aaf8054d650148adc3aa","scripts":{"dev":"tsup --watch","test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","verify:pack":"publint && attw --pack . --profile node16","prepublishOnly":"npm run typecheck && npm test && npm run build"},"_npmUser":{"name":"remiphilippe","email":"remi@aiku.fr"},"repository":{"url":"git+https://github.com/atol-sh/atol-fingerprint-js.git","type":"git"},"_npmVersion":"11.12.1","description":"Browser device-signal collection for Atol Device Intelligence","directories":{},"sideEffects":false,"_nodeVersion":"26.0.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.0.0","vitest":"^4.1.8","publint":"^0.3.21","happy-dom":"^20.10.2","typescript":"^5.7.0","@arethetypeswrong/cli":"^0.18.4"},"_npmOperationalInternal":{"tmp":"tmp/fingerprint_0.1.0_1783397712981_0.7233397244527724","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@atol-sh/fingerprint","version":"0.2.0","description":"Browser device-signal collection for Atol Device Intelligence","type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"},"default":"./dist/index.js"},"./package.json":"./package.json"},"sideEffects":false,"engines":{"node":">=18"},"author":{"name":"Atol"},"scripts":{"build":"tsup","dev":"tsup --watch","typecheck":"tsc --noEmit && tsc --project tsconfig.tests.json","test":"vitest run","prepublishOnly":"npm run typecheck && npm test && npm run build","verify:pack":"publint && attw --pack . --profile node16"},"repository":{"type":"git","url":"git+https://github.com/atol-sh/atol-fingerprint-js.git"},"homepage":"https://atol.sh","keywords":["atol","fingerprint","device-fingerprinting","device-intelligence","bot-detection","fraud-detection","browser-signals"],"publishConfig":{"access":"public"},"devDependencies":{"@arethetypeswrong/cli":"^0.18.4","happy-dom":"^20.10.2","publint":"^0.3.21","tsup":"^8.0.0","typescript":"^5.7.0","vitest":"^4.1.8"},"license":"Apache-2.0","_id":"@atol-sh/fingerprint@0.2.0","bugs":{"url":"https://github.com/atol-sh/atol-fingerprint-js/issues"},"_integrity":"sha512-OiFKSsYf/7hsdpjNL9oNtCVLP6p52XXsFze1PsleavFz6LBEGV/6SqSce14GB1lyFQrcSxCWJUgOiajdQPz8oQ==","_resolved":"/home/runner/work/atol-fingerprint-js/atol-fingerprint-js/release-artifact/atol-sh-fingerprint-0.2.0.tgz","_from":"file:/home/runner/work/atol-fingerprint-js/atol-fingerprint-js/release-artifact/atol-sh-fingerprint-0.2.0.tgz","_nodeVersion":"22.23.1","_npmVersion":"11.18.0","dist":{"integrity":"sha512-OiFKSsYf/7hsdpjNL9oNtCVLP6p52XXsFze1PsleavFz6LBEGV/6SqSce14GB1lyFQrcSxCWJUgOiajdQPz8oQ==","shasum":"e5ba2d03296ed4305fdf193582104ebe9e9c0429","tarball":"https://registry.npmjs.org/@atol-sh/fingerprint/-/fingerprint-0.2.0.tgz","fileCount":11,"unpackedSize":147339,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@atol-sh%2ffingerprint@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIHDitcdG751A1/RJ4zgiXPcmO8FXcA3Bk/P/+LCYo8l9AiEAuXjngs+J3HOv2aBQgBMIqK8LM3M8uzAfnTM72nYozbw="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:ddac8871-a2b3-4a16-80eb-bf9cbe053a6c"}},"directories":{},"maintainers":[{"name":"remiphilippe","email":"remi@aiku.fr"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/fingerprint_0.2.0_1786380720470_0.29647991371282356"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-07T04:15:12.780Z","modified":"2026-08-10T16:52:00.997Z","0.1.0":"2026-07-07T04:15:13.117Z","0.2.0":"2026-08-10T16:52:00.653Z"},"bugs":{"url":"https://github.com/atol-sh/atol-fingerprint-js/issues"},"author":{"name":"Atol"},"license":"Apache-2.0","homepage":"https://atol.sh","keywords":["atol","fingerprint","device-fingerprinting","device-intelligence","bot-detection","fraud-detection","browser-signals"],"repository":{"type":"git","url":"git+https://github.com/atol-sh/atol-fingerprint-js.git"},"description":"Browser device-signal collection for Atol Device Intelligence","maintainers":[{"name":"remiphilippe","email":"remi@aiku.fr"}],"readme":"# @atol-sh/fingerprint\n\n[![npm version](https://img.shields.io/npm/v/@atol-sh/fingerprint.svg)](https://www.npmjs.com/package/@atol-sh/fingerprint)\n[![license](https://img.shields.io/npm/l/@atol-sh/fingerprint.svg)](./LICENSE)\n[![CI](https://github.com/atol-sh/atol-fingerprint-js/actions/workflows/ci.yml/badge.svg)](https://github.com/atol-sh/atol-fingerprint-js/actions/workflows/ci.yml)\n\nBrowser device-signal collection for the [Atol](https://atol.sh) Device\nIntelligence Engine. Collects raw browser signals, submits them to the Atol\ncontrol plane, and returns a server-assigned device ID plus server-computed\nsmart signals (bot, VPN, tamper, anomaly score). All analysis happens\nserver-side; the browser never makes trust decisions.\n\n- Zero production dependencies. Pure TypeScript + DOM APIs.\n- ESM + CJS + type declarations.\n- Apache-2.0.\n\nThis package is a standalone SDK, but it's also an optional companion to\n[`@atol-sh/js`](https://www.npmjs.com/package/@atol-sh/js) and\n[`@atol-sh/react`](https://www.npmjs.com/package/@atol-sh/react) - install it\nalongside either to enable device intelligence (bot/VPN/tamper signals) in\nthose SDKs.\n\n## Install\n\n```bash\nnpm install @atol-sh/fingerprint\n```\n\n## Quick start\n\n```ts\nimport { AtolFingerprint, isCollectionDisabled } from \"@atol-sh/fingerprint\";\n\n// Collect signals once at load time.\nconst fp = await AtolFingerprint.load({\n  endpoint: \"https://api.atol.sh\", // default\n});\n\n// Submit to the control plane.\nconst result = await fp.identify({\n  authorize: async ({ method, url }) => {\n    const { token, proof } = await acquireTenantAuthorization(method, url);\n    return proof\n      ? { scheme: \"DPoP\", accessToken: token, dpopProof: proof }\n      : { scheme: \"Bearer\", accessToken: token };\n  },\n});\n\nif (isCollectionDisabled(result)) {\n  // User opted out (config) or browser sent Global Privacy Control (gpc).\n  console.log(\"collection disabled:\", result.reason);\n} else {\n  console.log(result.device_id, result.confidence, result.signals?.anomaly_score);\n}\n\n// Inspect raw signals without submitting:\nconst signals = fp.getSignals();\n```\n\n`identify({ authorize })` calls the credential owner for the exact POST URL\nimmediately before dispatch. The callback returns either a Bearer token or an\ninseparable DPoP token/proof pair. Neither the callback nor its result is\nstored by the collector. The control plane derives the organization, user,\nand session from that verified authorization.\n\n## Server contract\n\n`identify()` performs `POST {endpoint}/api/v1/devices/identify` with body:\n\n```json\n{\n  \"client_platform\": \"browser\",\n  \"client_signals\": { /* ClientSignals, see below */ }\n}\n```\n\nThe control plane handler (`atol` repo, `internal/api/device_handler.go`) is\nthe canonical contract. Its response, typed as `IdentifyResult`:\n\n```ts\ninterface IdentifyResult {\n  device_id: string;     // server-assigned; never generated client-side\n  known: boolean;\n  confidence: number;\n  new_device: boolean;\n  platform: string;\n  browser: string;\n  os_version: string;\n  signals: SmartSignals;\n}\n\ninterface SmartSignals {\n  bot: boolean;\n  vpn: boolean;\n  proxy: boolean;\n  tor: boolean;\n  incognito: boolean;\n  tampered: boolean;\n  emulator: boolean;\n  rooted: boolean;\n  geo_mismatch: boolean;\n  device_mismatch: boolean;\n  device_shared: boolean;\n  shared_user_count: number;\n  anomaly_score: number; // [0, 1]; 0 = clean, 1 = highly suspicious\n}\n```\n\nFailures make `identify()` throw `IdentifyRequestError`, with a closed `code`\nand an HTTP `status` when a response was received. The SDK validates the exact\nresponse shape and does not coerce missing fields or return unknown members.\nRaw response bodies, status text, request URLs, and caught browser values are\nnever retained on the error. Identify requests omit ambient credentials and\nreferrers, bypass browser caching, and reject redirects. There is no silent\nfallback.\n\nAll types (`ClientSignals`, `SmartSignals`, `IdentifyResult`,\n`IdentifyOptions`, `FingerprintConfig`, `CollectionDisabled`) and the\n`isCollectionDisabled` type guard are exported from the package root for downstream SDKs.\n`IdentifyRequestError` and `IdentifyRequestErrorCode` are exported there too.\n\n## What is collected\n\nEverything below is read once at `AtolFingerprint.load()` and only leaves the\nbrowser when you call `identify()`.\n\n| Signal | Detail |\n|--------|--------|\n| Canvas rendering | A small test scene (text, gradient, arcs) rendered to an offscreen canvas, exported as a data URL. Hashed server-side. |\n| WebGL | Unmasked renderer and vendor strings (`WEBGL_debug_renderer_info`), supported extension names. |\n| Audio | A short numeric hash of an `OfflineAudioContext` oscillator+compressor render. Raw audio never leaves the device. |\n| Fonts | Presence/absence of a fixed list of 30 common fonts (width-measurement technique). Names only. |\n| Screen metrics | `screen.width`, `screen.height`, `devicePixelRatio`, `colorDepth`. |\n| Hardware | `navigator.hardwareConcurrency`, `navigator.deviceMemory`. |\n| Locale | `navigator.languages`, `navigator.platform`, IANA timezone. |\n| UA Client Hints | High-entropy values: `architecture`, `bitness`, `fullVersionList`, `model`, `platformVersion`, `uaFullVersion`, `wow64` (falls back to `brands`/`mobile`/`platform`, then to the plain user-agent string). |\n| Touch | `navigator.maxTouchPoints`. |\n| Codecs | Which of 10 fixed codec strings `MediaSource.isTypeSupported` accepts. |\n| Media devices | The COUNT of devices from `enumerateDevices()`. Never labels or device IDs. |\n| CSS preferences | `prefers-color-scheme`, `prefers-reduced-motion`, `prefers-contrast`, `pointer`, `hover`, `prefers-reduced-transparency`, `forced-colors`. |\n| Math fingerprint | Results of 22 `Math` functions (engine/libm variations). |\n| API availability | Booleans for `bluetooth`, `usb`, `webgpu`, `speechSynthesis`, `webxr`, `serial`, `hid`, `credentials`, `PaymentRequest`. |\n| Incognito heuristic | Boolean derived from `navigator.storage.estimate()` quota (< 500 MB suggests private browsing). |\n| Automation markers | Booleans: `navigator.webdriver`, PhantomJS/Nightmare/Selenium/Playwright/Puppeteer/CDP injected globals, `domAutomation`, and headless indicators (zero plugins, zero outer window size, `connection.rtt === 0`, notification permission inconsistency, missing languages). |\n\nThe mobile-only fields in the wire format (`mobile_id`, `app_attest`,\n`play_integrity`, `sensor_data`, `build_props`) are always empty strings from\nthis browser SDK.\n\n## What is NOT collected\n\n- No geolocation (no Geolocation API; any geo analysis is server-side from IP).\n- No persistent client-side identifiers: the device ID is assigned by the\n  server. Nothing is written to cookies, localStorage, IndexedDB, or any\n  other client storage.\n- No media device labels or device IDs - only the count.\n- No keystrokes, form contents, page contents, or browsing history.\n- No raw audio or microphone/camera access.\n\n## Data flow\n\n1. `load()` reads the signals above in the browser. No network traffic.\n2. `identify()` POSTs them to `https://api.atol.sh/api/v1/devices/identify`\n   (or your configured `endpoint`) with an operation-scoped Bearer token or\n   DPoP token/proof pair bound to that exact POST URL.\n3. The control plane hashes the signals, matches them against known devices\n   for your tenant, evaluates smart signals, and returns `IdentifyResult`.\n4. The returned `device_id` and `signals` feed OPA policies\n   (`input.device.*`) for step-up auth and fraud decisions.\n\nThe loaded agent retains only its configured endpoint and collected signal\nsnapshot in memory. It never retains authorization material and writes no\ndurable browser state. Every durable device record - the profile, its\nfingerprint history, and the session binding - lives server-side in the Atol\ncontrol plane, retained per Atol's device-data retention policy.\n\n## Privacy & compliance\n\nDevice fingerprinting reads information from the user's terminal equipment.\nUnder GDPR and the ePrivacy Directive this generally requires prior user\ndisclosure and, depending on your legal basis (e.g. consent vs. legitimate\ninterest for fraud prevention), user consent. You are responsible for\ndisclosing fingerprinting in your privacy policy and obtaining any required\nconsent before calling `AtolFingerprint.load()`.\n\nSee the Atol privacy documentation: https://atol.sh/docs/privacy (placeholder).\n\n### Consent and opt-out API\n\n```ts\n// Explicit opt-out: collection never runs, nothing is read or sent.\nconst fp = await AtolFingerprint.load({ disabled: !userHasConsented });\n\nconst result = await fp.identify();\n// result = { collection_disabled: true, reason: \"config\" }\n```\n\nWhen collection is disabled:\n\n- `load()` reads no browser APIs at all.\n- `identify()` performs no network request and resolves to\n  `{ collection_disabled: true, reason: \"config\" | \"gpc\" }`.\n- `getSignals()` returns the same `CollectionDisabled` object.\n- No fake or placeholder data is ever produced.\n\nUse the exported `isCollectionDisabled(result)` type guard to narrow results,\nor check `fp.disabled`.\n\n### Global Privacy Control\n\nIf the browser advertises [Global Privacy Control](https://globalprivacycontrol.org/)\n(`navigator.globalPrivacyControl === true`), the SDK disables collection by\ndefault and behaves exactly as if `disabled: true` was passed (with\n`reason: \"gpc\"`). If you have an independent legal basis to ignore GPC (e.g.\nfraud prevention required to provide the service), you may override:\n\n```ts\nconst fp = await AtolFingerprint.load({ respectGPC: false });\n```\n\n## Troubleshooting\n\n- **`identify()` throws with a 401/403** - the authorization is expired,\n  mismatched to the target tenant, or missing its DPoP proof. A missing or\n  malformed authorization owner is rejected locally before dispatch. Acquire\n  the token and proof together via `identify({ authorize })`.\n- **`identify()` throws `IdentifyRequestError`** - inspect its closed `code`\n  and optional numeric `status`. The SDK deliberately does not expose raw\n  server or browser diagnostics.\n- **`getSignals()` / `identify()` return `{ collection_disabled: true }`\n  unexpectedly** - check `reason`. `\"gpc\"` means the browser sent Global\n  Privacy Control; pass `respectGPC: false` if you have an independent legal\n  basis to collect anyway. `\"config\"` means `disabled: true` was passed to\n  `load()`.\n- **Signals look incomplete in a headless/automated browser** - expected.\n  Several collectors (WebGL, fonts, media devices) return sparse data or\n  automation markers under headless Chrome/Playwright/Puppeteer by design.\n\n## Development\n\n```bash\nnpm ci\nnpm run typecheck\nnpm test          # vitest, happy-dom environment\nnpm run build     # tsup -> dist/ (ESM + CJS + d.ts)\nnpm run verify:pack # publint + are-the-types-wrong\n```\n\n## License\n\nApache-2.0. Copyright 2026 Atol.\n","readmeFilename":"README.md"}