@ianmenethil/zp-devicefp 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,472 +1,520 @@
1
- # API Reference
2
-
3
- Complete reference for `@ianmenethil/zp-devicefp` — every type, method, option, event, and export.
4
-
5
- ---
6
-
7
- ## Exports
8
-
9
- | Export | Kind | Description |
10
- |--------|------|-------------|
11
- | `createFingerprintClient` | function | Async factory that returns a configured `FingerprintClient` |
12
- | `LIBRARY_VERSION` | `string` | Semantic version of the library (e.g. `"0.1.0"`) |
13
- | `SCHEMA_VERSION` | `number` | Schema version for the result shape (integer, bumped on breaking changes) |
14
-
15
- ---
16
-
17
- ## `createFingerprintClient(options?)`
18
-
19
- Factory function. Returns `Promise<FingerprintClient>`.
20
-
21
- ```ts
22
- import { createFingerprintClient } from '@ianmenethil/zp-devicefp';
23
-
24
- const client = await createFingerprintClient({
25
- timeoutMs: 1500,
26
- extended: false,
27
- debug: false,
28
- });
29
- ```
30
-
31
- ### Parameters
32
-
33
- | Parameter | Type | Default | Description |
34
- |-----------|------|---------|-------------|
35
- | `options` | `CollectOptions` | `{}` | Configuration overrides applied to all subsequent `collect()` / `collectSync()` calls. Can be overridden per-call. |
36
-
37
- ---
38
-
39
- ## `CollectOptions`
40
-
41
- ```ts
42
- interface CollectOptions {
43
- include?: SignalName[];
44
- exclude?: SignalName[];
45
- timeoutMs?: number;
46
- abortSignal?: AbortSignal;
47
- extended?: boolean;
48
- debug?: boolean;
49
- }
50
- ```
51
-
52
- | Field | Type | Default | Description |
53
- |-------|------|---------|-------------|
54
- | `include` | `SignalName[]` | undefined | Whitelist of signals to collect. When set, only these signals run. |
55
- | `exclude` | `SignalName[]` | undefined | Blacklist of signals to skip. |
56
- | `timeoutMs` | `number` | `1500` | Per-signal timeout in milliseconds. Signals exceeding this return `status: 'timeout'`. |
57
- | `abortSignal` | `AbortSignal` | undefined | Pass an `AbortController.signal` to cancel in-flight collection. Aborted signals return `status: 'error'`. |
58
- | `extended` | `boolean` | `false` | When `true`, includes extended-tier signals in the collection. |
59
- | `debug` | `boolean` | `false` | Enable debug logging (passed to signal collectors via context). |
60
-
61
- ### Signal resolution order
62
-
63
- 1. Start with core signals (`ua`, `uaHints`, `locale`, `screen`, `hardware`, `storage`, `fonts`, `canvas`, `webgl`, `audio`).
64
- 2. If `extended: true`, add all 9 extended signals.
65
- 3. In `collectSync()` mode, remove signals without `supportsSync: true`.
66
- 4. If `include` is set, keep only signals present in the list.
67
- 5. Remove any signals listed in `exclude`.
68
-
69
- ---
70
-
71
- ## `FingerprintClient`
72
-
73
- The object returned by `createFingerprintClient()`.
74
-
75
- ```ts
76
- interface FingerprintClient {
77
- collect(options?: CollectOptions): Promise<FingerprintResult>;
78
- collectSync(options?: Omit<CollectOptions, 'timeoutMs' | 'abortSignal'>): FingerprintResult;
79
- upload(result: FingerprintResult, options: UploadOptions): Promise<Response>;
80
- on<K extends keyof EventPayloadMap>(event: K, cb: (payload: EventPayloadMap[K]) => void): () => void;
81
- }
82
- ```
83
-
84
- ### `client.collect(options?)`
85
-
86
- Collects signals asynchronously. All signals run in parallel with per-signal timeouts. Emits `progress`, `warning`, and `complete` events.
87
-
88
- ```ts
89
- const result = await client.collect({ extended: true, timeoutMs: 3000 });
90
- console.log(result.thumbprint);
91
- console.log(result.confidence);
92
- ```
93
-
94
- - **Returns**: `Promise<FingerprintResult>`
95
- - **Events emitted**: `progress` (per signal), `warning` (on non-fatal issues), `complete` (once, with final result)
96
-
97
- ### `client.collectSync(options?)`
98
-
99
- Collects signals synchronously. Only signals with `supportsSync: true` will run. Timeout and abort are not supported the `timeoutMs` and `abortSignal` fields are excluded from the options type.
100
-
101
- ```ts
102
- const result = client.collectSync();
103
- console.log(result.thumbprint);
104
- ```
105
-
106
- - **Returns**: `FingerprintResult`
107
- - **Sync-capable signals** (13): `ua`, `locale`, `screen`, `hardware`, `storage`, `fonts`, `canvas`, `webgl`, `frameInfo`, `networkInfo`, `paymentSupport`, `referrerInfo`, `navigationInfo`
108
- - **NOT sync-capable** (6): `uaHints`, `audio`, `mediaDevices`, `permissions`, `webrtc`, `riskSignals`
109
-
110
- ### `client.upload(result, options)`
111
-
112
- Posts a fingerprint result to a remote endpoint via HTTP POST.
113
-
114
- ```ts
115
- const response = await client.upload(result, {
116
- endpoint: 'https://api.example.com/fingerprint',
117
- headers: { 'Authorization': 'Bearer token' },
118
- bodyExtras: { sessionId: 'abc123' },
119
- });
120
- ```
121
-
122
- Payload shape:
123
- ```json
124
- {
125
- "fingerprint": { /* full FingerprintResult */ },
126
- "...bodyExtras keys..."
127
- }
128
- ```
129
-
130
- - **Returns**: `Promise<Response>` — native `fetch` Response
131
- - **Throws**: if `fetch` is not available in the runtime
132
-
133
- ### `client.on(event, callback)`
134
-
135
- Subscribe to lifecycle events during async collection.
136
-
137
- ```ts
138
- const unsubscribe = client.on('progress', (event) => {
139
- console.log(`${event.completed}/${event.total}: ${event.signal}`);
140
- });
141
-
142
- // Later, remove the listener:
143
- unsubscribe();
144
- ```
145
-
146
- - **Returns**: `() => void` call to unsubscribe
147
-
148
- ---
149
-
150
- ## Events
151
-
152
- | Event | Payload Type | When Emitted |
153
- |-------|-------------|--------------|
154
- | `progress` | `ProgressEvent` | After each signal completes during `collect()` |
155
- | `warning` | `WarningEvent` | When a non-fatal issue occurs (unsupported signal, blocked API, etc.) |
156
- | `complete` | `CompleteEvent` | After all signals finish in `collect()` |
157
-
158
- ### `ProgressEvent`
159
-
160
- ```ts
161
- interface ProgressEvent {
162
- completed: number; // signals completed so far
163
- total: number; // total signals requested
164
- signal: SignalName; // name of the signal that just completed
165
- result: SignalResult; // the signal's collection result
166
- }
167
- ```
168
-
169
- ### `WarningEvent`
170
-
171
- ```ts
172
- interface WarningEvent {
173
- signal?: SignalName; // which signal triggered the warning, if applicable
174
- message: string; // human-readable warning message
175
- }
176
- ```
177
-
178
- ### `CompleteEvent`
179
-
180
- ```ts
181
- interface CompleteEvent {
182
- result: FingerprintResult; // the completed fingerprint result
183
- }
184
- ```
185
-
186
- ---
187
-
188
- ## `FingerprintResult`
189
-
190
- The complete output of `collect()` or `collectSync()`.
191
-
192
- ```ts
193
- interface FingerprintResult {
194
- schemaVersion: number;
195
- libraryVersion: string;
196
- thumbprint: string;
197
- confidence: number;
198
- componentHashes: Partial<Record<SignalName, string>>;
199
- signals: Partial<Record<SignalName, SignalResult>>;
200
- antiSpoof: AntiSpoofReport;
201
- warnings: string[];
202
- diagnostics: CollectorDiagnostics;
203
- }
204
- ```
205
-
206
- | Field | Type | Description |
207
- |-------|------|-------------|
208
- | `schemaVersion` | `number` | Schema version (currently `1`). Bumped on breaking result shape changes. |
209
- | `libraryVersion` | `string` | Library version that produced this result (e.g. `"0.1.0"`). |
210
- | `thumbprint` | `string` | SHA-256 hash of all component hashes combined. The primary fingerprint identifier. |
211
- | `confidence` | `number` | 0–1 score. 0 = unreliable, 1 = high confidence. Weighted average of signal quality blended with anti-spoof score. |
212
- | `componentHashes` | `Partial<Record<SignalName, string>>` | Per-signal SHA-256 hashes of canonicalized signal values. Only present for signals that returned `value`. |
213
- | `signals` | `Partial<Record<SignalName, SignalResult>>` | Raw signal collection results, keyed by signal name. |
214
- | `antiSpoof` | `AntiSpoofReport` | Cross-signal coherence analysis and automation detection. |
215
- | `warnings` | `string[]` | Non-fatal warnings from the collection run. |
216
- | `diagnostics` | `CollectorDiagnostics` | Metadata about what ran and how long it took. |
217
-
218
- ### Thumbprint derivation
219
-
220
- 1. Each signal value is canonicalized (deterministic JSON serialization with sorted keys, normalized numbers, ISO dates).
221
- 2. Each canonicalized value is hashed with SHA-256 → `componentHashes`.
222
- 3. Component hashes are sorted by name, serialized as `[[name, hash], ...]`, canonicalized, and hashed once more → `thumbprint`.
223
-
224
- ---
225
-
226
- ## `SignalResult`
227
-
228
- The result of collecting a single signal.
229
-
230
- ```ts
231
- interface SignalResult<T = unknown> {
232
- status: 'ok' | 'unsupported' | 'blocked' | 'timeout' | 'error';
233
- value?: T;
234
- durationMs: number;
235
- error?: string;
236
- }
237
- ```
238
-
239
- | Field | Type | Description |
240
- |-------|------|-------------|
241
- | `status` | `SignalStatus` | Outcome of the collection attempt. |
242
- | `value` | `T` (optional) | The collected signal data. Only present when `status` is `'ok'`. |
243
- | `durationMs` | `number` | Wall-clock milliseconds spent collecting this signal. |
244
- | `error` | `string` (optional) | Human-readable error description. Present when status is not `'ok'`. |
245
-
246
- ### `SignalStatus`
247
-
248
- ```ts
249
- type SignalStatus = 'ok' | 'unsupported' | 'blocked' | 'timeout' | 'error';
250
- ```
251
-
252
- | Value | Meaning |
253
- |-------|---------|
254
- | `ok` | Signal collected successfully. `value` is populated. |
255
- | `unsupported` | The required browser API is not available. |
256
- | `blocked` | The API exists but access was denied (e.g. permissions, secure context). |
257
- | `timeout` | Collection exceeded the per-signal timeout. |
258
- | `error` | An unexpected error occurred during collection. |
259
-
260
- ---
261
-
262
- ## `AntiSpoofReport`
263
-
264
- Cross-signal coherence analysis.
265
-
266
- ```ts
267
- interface AntiSpoofReport {
268
- score: number;
269
- anomalies: string[];
270
- automationHints: string[];
271
- }
272
- ```
273
-
274
- | Field | Type | Description |
275
- |-------|------|-------------|
276
- | `score` | `number` | Normalized score 0–1. 0 = automated/bot, 1 = genuine user. Each anomaly deducts 0.12; each automation hint deducts 0.18. Floor at 0, ceiling at 1. |
277
- | `anomalies` | `string[]` | Specific inconsistencies found between signals. |
278
- | `automationHints` | `string[]` | Indicators of headless, automated, or bot environments. |
279
-
280
- ### Anomaly codes
281
-
282
- | Code | Trigger |
283
- |------|---------|
284
- | `ua_platform_mismatch` | `navigator.platform` contradicts UA Client Hints platform |
285
- | `touch_claim_without_touch_points` | iPhone UA but `maxTouchPoints` is 0 |
286
- | `mobile_ua_desktop_screen` | Android UA but screen width >= 1600px |
287
- | `windows_ua_mac_platform` | Windows UA string but platform reports macOS |
288
- | `implausible_device_memory` | `deviceMemory` < 0.25 or > 64 GB |
289
- | `tiny_screen_geometry` | Screen width and height both < 200px |
290
-
291
- ### Automation hint codes
292
-
293
- | Code | Trigger |
294
- |------|---------|
295
- | `navigator_webdriver` | `navigator.webdriver` is truthy |
296
- | `headless_user_agent` | UA string contains `"headless"` |
297
-
298
- ---
299
-
300
- ## `CollectorDiagnostics`
301
-
302
- ```ts
303
- interface CollectorDiagnostics {
304
- requestedSignals: SignalName[];
305
- completedSignals: SignalName[];
306
- elapsedMs: number;
307
- }
308
- ```
309
-
310
- | Field | Type | Description |
311
- |-------|------|-------------|
312
- | `requestedSignals` | `SignalName[]` | Signals requested for the run (after include/exclude/mode resolution). |
313
- | `completedSignals` | `SignalName[]` | Signals that produced a result (any status). Sorted alphabetically. |
314
- | `elapsedMs` | `number` | Total wall-clock milliseconds for the entire run (rounded to 3 decimal places). |
315
-
316
- ---
317
-
318
- ## `UploadOptions`
319
-
320
- ```ts
321
- interface UploadOptions {
322
- endpoint: string;
323
- headers?: Record<string, string>;
324
- bodyExtras?: Record<string, unknown>;
325
- }
326
- ```
327
-
328
- | Field | Type | Description |
329
- |-------|------|-------------|
330
- | `endpoint` | `string` | URL to POST the fingerprint payload to. |
331
- | `headers` | `Record<string, string>` | Additional HTTP headers merged with `content-type: application/json`. |
332
- | `bodyExtras` | `Record<string, unknown>` | Extra properties merged into the JSON body alongside `fingerprint`. |
333
-
334
- ---
335
-
336
- ## Confidence scoring
337
-
338
- Each signal has a fixed weight. The base confidence is the weighted ratio of earned points to available points.
339
-
340
- ### Signal weights
341
-
342
- | Signal | Weight | Signal | Weight |
343
- |--------|--------|--------|--------|
344
- | `canvas` | 1.0 | `audio` | 0.9 |
345
- | `webgl` | 1.0 | `fonts` | 0.9 |
346
- | `screen` | 0.8 | `webrtc` | 0.8 |
347
- | `uaHints` | 0.7 | `hardware` | 0.7 |
348
- | `riskSignals` | 0.7 | `mediaDevices` | 0.6 |
349
- | `networkInfo` | 0.6 | `ua` | 0.5 |
350
- | `locale` | 0.5 | `storage` | 0.4 |
351
- | `permissions` | 0.4 | `navigationInfo` | 0.4 |
352
- | `frameInfo` | 0.3 | `referrerInfo` | 0.3 |
353
- | `paymentSupport` | 0.2 | | |
354
-
355
- ### Points per status
356
-
357
- | Status | Points earned |
358
- |--------|---------------|
359
- | `ok` | Full weight |
360
- | `unsupported` | 35% of weight |
361
- | `blocked` | 35% of weight |
362
- | `timeout` | 15% of weight |
363
- | `error` | 0% |
364
-
365
- ### Final score
366
-
367
- ```
368
- base = earned / available
369
- final = base * (0.65 + antiSpoofScore * 0.35)
370
- ```
371
-
372
- The anti-spoof score blends 35% into the final confidence, meaning a low anti-spoof score drags confidence down proportionally.
373
-
374
- ---
375
-
376
- ## Internal constants
377
-
378
- These values are used internally to configure the library. They are not public exports but are documented here for reference.
379
-
380
- | Constant | Type | Value |
381
- |----------|------|-------|
382
- | `CORE_SIGNALS` | `SignalName[]` | `['ua', 'uaHints', 'locale', 'screen', 'hardware', 'storage', 'fonts', 'canvas', 'webgl', 'audio']` |
383
- | `EXTENDED_SIGNALS` | `SignalName[]` | `['mediaDevices', 'permissions', 'webrtc', 'frameInfo', 'networkInfo', 'paymentSupport', 'referrerInfo', 'navigationInfo', 'riskSignals']` |
384
- | `SYNC_SIGNALS` | `SignalName[]` | `['ua', 'locale', 'screen', 'hardware', 'storage', 'fonts', 'canvas', 'webgl', 'frameInfo', 'networkInfo', 'paymentSupport', 'referrerInfo', 'navigationInfo']` |
385
- | `DEFAULT_TIMEOUT_MS` | `number` | `1500` |
386
- | `DEFAULT_FONT_LIST` | `string[]` | `['Arial', 'Helvetica Neue', 'Times New Roman', 'Georgia', 'Courier New', 'Trebuchet MS', 'Verdana', 'Tahoma', 'Impact', 'Comic Sans MS']` |
387
- | `DEFAULT_PERMISSION_NAMES` | `readonly string[]` | `['geolocation', 'notifications', 'camera', 'microphone']` |
388
- | `DEFAULT_UA_HINTS` | `readonly string[]` | `['architecture', 'bitness', 'formFactors', 'fullVersionList', 'model', 'platform', 'platformVersion', 'wow64']` |
389
-
390
- ---
391
-
392
- ## CDN / IIFE API
393
-
394
- When loaded via `<script>` tag, the library attaches to `window.DeviceFP`.
395
-
396
- ```html
397
- <script src="https://cdn.jsdelivr.net/npm/@ianmenethil/zp-devicefp/dist/cdn/zp.dfp.min.js"></script>
398
- <script>
399
- const client = await window.DeviceFP.createFingerprintClient({ timeoutMs: 1500 });
400
- const result = await client.collect();
401
- console.log(window.DeviceFP.LIBRARY_VERSION);
402
- console.log(window.DeviceFP.SCHEMA_VERSION);
403
- </script>
404
- ```
405
-
406
- ### `window.DeviceFP` shape
407
-
408
- | Property | Type |
409
- |----------|------|
410
- | `createFingerprintClient` | `(options?: CollectOptions) => Promise<FingerprintClient>` |
411
- | `LIBRARY_VERSION` | `string` |
412
- | `SCHEMA_VERSION` | `number` |
413
-
414
- ### CDN files
415
-
416
- | File | Format | Description |
417
- |------|--------|-------------|
418
- | `zp.dfp.js` | IIFE | Unminified global bundle |
419
- | `zp.dfp.min.js` | IIFE | Minified global bundle |
420
- | `zp.dfp.obf.js` | IIFE | Minified + obfuscated global bundle |
421
- | `zp.dfp.esm.js` | ESM | Module for `import()` / `<script type="module">` |
422
- | `zp.dfp.manifest.json` | JSON | SRI integrity hashes for all CDN files |
423
-
424
- ### CDN sources
425
-
426
- | Source | Base URL |
427
- |--------|----------|
428
- | jsDelivr | `https://cdn.jsdelivr.net/npm/@ianmenethil/zp-devicefp/dist/cdn/` |
429
- | Self-hosted | `https://cdn.zenithpayments.support/devicefp/latest/` |
430
-
431
- ---
432
-
433
- ## NPM package layout
434
-
435
- ```text
436
- @ianmenethil/zp-devicefp/
437
- ├── dist/npm/
438
- │ ├── index.mjs # ESM entry (browser, ES2022)
439
- │ ├── index.cjs # CJS entry (Node 22+)
440
- │ └── index.d.ts # Public type declarations
441
- ├── dist/cdn/
442
- │ ├── zp.dfp.js # IIFE (unminified)
443
- │ ├── zp.dfp.min.js # IIFE (minified)
444
- │ ├── zp.dfp.obf.js # IIFE (minified + obfuscated)
445
- │ ├── zp.dfp.esm.js # ESM for import()
446
- │ └── zp.dfp.manifest.json
447
- ```
448
-
449
- ---
450
-
451
- ## NPM usage
452
-
453
- ```bash
454
- bun add @ianmenethil/zp-devicefp
455
- ```
456
-
457
- ```ts
458
- import {
459
- createFingerprintClient,
460
- LIBRARY_VERSION,
461
- SCHEMA_VERSION,
462
- } from '@ianmenethil/zp-devicefp';
463
-
464
- const client = await createFingerprintClient({ timeoutMs: 2000 });
465
- const result = await client.collect({ extended: true });
466
-
467
- // Upload to your backend
468
- await client.upload(result, {
469
- endpoint: '/api/fingerprint',
470
- bodyExtras: { userId: 'u_123' },
471
- });
472
- ```
1
+ # API Reference
2
+
3
+ Complete reference for `@ianmenethil/zp-devicefp` — every type, method, option, event, and export.
4
+
5
+ ---
6
+
7
+ ## Exports
8
+
9
+ ### Functions
10
+
11
+ | Export | Signature | Description |
12
+ |--------|-----------|-------------|
13
+ | `createFingerprintClient` | `(options?: CollectOptions) => Promise<FingerprintClient>` | Async factory that returns a configured `FingerprintClient` |
14
+
15
+ ### Constants
16
+
17
+ | Export | Type | Description |
18
+ |--------|------|-------------|
19
+ | `LIBRARY_VERSION` | `string` | Semantic version of the library (e.g. `"0.1.1"`) |
20
+ | `SCHEMA_VERSION` | `number` | Schema version for the result shape (integer, bumped on breaking changes) |
21
+ | `CORE_SIGNALS` | `SignalName[]` | The 10 signals collected by default |
22
+ | `EXTENDED_SIGNALS` | `SignalName[]` | The 11 signals requiring `extended: true` |
23
+ | `SYNC_SIGNALS` | `SignalName[]` | The 13 signals that support `collectSync()` |
24
+ | `DEFAULT_TIMEOUT_MS` | `number` | Default per-signal timeout: `1500` |
25
+ | `DEFAULT_FONT_LIST` | `string[]` | The 70 font faces probed during font detection (Fingerprint2 / Cardinal Commerce set) |
26
+ | `DEFAULT_PERMISSION_NAMES` | `readonly string[]` | The 4 permission names queried by the permissions signal |
27
+ | `DEFAULT_UA_HINTS` | `readonly string[]` | The 8 high-entropy UA-CH hints requested |
28
+
29
+ ---
30
+
31
+ ## `createFingerprintClient(options?)`
32
+
33
+ Factory function. Returns `Promise<FingerprintClient>`.
34
+
35
+ ```ts
36
+ import { createFingerprintClient } from '@ianmenethil/zp-devicefp';
37
+
38
+ const client = await createFingerprintClient({
39
+ timeoutMs: 1500,
40
+ extended: false,
41
+ debug: false,
42
+ });
43
+ ```
44
+
45
+ ### Parameters
46
+
47
+ | Parameter | Type | Default | Description |
48
+ |-----------|------|---------|-------------|
49
+ | `options` | `CollectOptions` | `{}` | Configuration overrides applied to all subsequent `collect()` / `collectSync()` calls. Can be overridden per-call. |
50
+
51
+ ---
52
+
53
+ ## `CollectOptions`
54
+
55
+ ```ts
56
+ interface CollectOptions {
57
+ include?: SignalName[];
58
+ exclude?: SignalName[];
59
+ timeoutMs?: number;
60
+ abortSignal?: AbortSignal;
61
+ extended?: boolean;
62
+ debug?: boolean;
63
+ }
64
+ ```
65
+
66
+ | Field | Type | Default | Description |
67
+ |-------|------|---------|-------------|
68
+ | `include` | `SignalName[]` | undefined | Whitelist of signals to collect. When set, only these signals run. |
69
+ | `exclude` | `SignalName[]` | undefined | Blacklist of signals to skip. |
70
+ | `timeoutMs` | `number` | `1500` | Per-signal timeout in milliseconds. Signals exceeding this return `status: 'timeout'`. |
71
+ | `abortSignal` | `AbortSignal` | undefined | Pass an `AbortController.signal` to cancel in-flight collection. Aborted signals return `status: 'error'`. |
72
+ | `extended` | `boolean` | `false` | When `true`, includes extended-tier signals in the collection. |
73
+ | `debug` | `boolean` | `false` | Enable debug logging (passed to signal collectors via context). |
74
+
75
+ ### Signal resolution order
76
+
77
+ 1. Start with core signals (`ua`, `uaHints`, `locale`, `screen`, `hardware`, `storage`, `fonts`, `canvas`, `webgl`, `audio`).
78
+ 2. If `extended: true`, add all 11 extended signals.
79
+ 3. In `collectSync()` mode, remove signals not in the `SYNC_SIGNALS` set.
80
+ 4. If `include` is set, keep only signals present in the list.
81
+ 5. Remove any signals listed in `exclude`.
82
+
83
+ ---
84
+
85
+ ## `FingerprintClient`
86
+
87
+ The object returned by `createFingerprintClient()`.
88
+
89
+ ```ts
90
+ interface FingerprintClient {
91
+ collect(options?: CollectOptions): Promise<FingerprintResult>;
92
+ collectSync(options?: Omit<CollectOptions, 'timeoutMs' | 'abortSignal'>): FingerprintResult;
93
+ on<K extends keyof EventPayloadMap>(event: K, cb: (payload: EventPayloadMap[K]) => void): () => void;
94
+ }
95
+ ```
96
+
97
+ ### `client.collect(options?)`
98
+
99
+ Collects signals asynchronously. All signals run in parallel with per-signal timeouts. Emits `progress`, `warning`, and `complete` events.
100
+
101
+ ```ts
102
+ const result = await client.collect({ extended: true, timeoutMs: 3000 });
103
+ console.log(result.thumbprint);
104
+ console.log(result.confidence);
105
+ ```
106
+
107
+ - **Returns**: `Promise<FingerprintResult>`
108
+ - **Events emitted**: `progress` (per signal), `warning` (on non-fatal issues), `complete` (once, with final result)
109
+
110
+ ### `client.collectSync(options?)`
111
+
112
+ Collects signals synchronously. Only signals in the `SYNC_SIGNALS` set will run. Timeout and abort are not supported — the `timeoutMs` and `abortSignal` fields are excluded from the options type.
113
+
114
+ ```ts
115
+ const result = client.collectSync();
116
+ console.log(result.thumbprint);
117
+ ```
118
+
119
+ - **Returns**: `FingerprintResult`
120
+ - **Sync-capable signals** (13): `ua`, `locale`, `screen`, `hardware`, `storage`, `fonts`, `canvas`, `webgl`, `frameInfo`, `networkInfo`, `paymentSupport`, `referrerInfo`, `navigationInfo`
121
+ - **NOT sync-capable** (8): `uaHints`, `audio`, `mediaDevices`, `permissions`, `webrtc`, `riskSignals`, `adblock`, `geolocation`
122
+
123
+ ### `client.on(event, callback)`
124
+
125
+ Subscribe to lifecycle events during async collection.
126
+
127
+ ```ts
128
+ const unsubscribe = client.on('progress', (event) => {
129
+ console.log(`${event.completed}/${event.total}: ${event.signal}`);
130
+ });
131
+
132
+ // Later, remove the listener:
133
+ unsubscribe();
134
+ ```
135
+
136
+ - **Returns**: `() => void` — call to unsubscribe
137
+
138
+ ---
139
+
140
+ ## Events
141
+
142
+ | Event | Payload Type | When Emitted |
143
+ |-------|-------------|--------------|
144
+ | `progress` | `ProgressEvent` | After each signal completes during `collect()` |
145
+ | `warning` | `WarningEvent` | When a non-fatal issue occurs (unsupported signal, blocked API, etc.) |
146
+ | `complete` | `CompleteEvent` | After all signals finish in `collect()` |
147
+
148
+ ### `ProgressEvent`
149
+
150
+ ```ts
151
+ interface ProgressEvent {
152
+ completed: number; // signals completed so far
153
+ total: number; // total signals requested
154
+ signal: SignalName; // name of the signal that just completed
155
+ result: SignalResult; // the signal's collection result
156
+ }
157
+ ```
158
+
159
+ ### `WarningEvent`
160
+
161
+ ```ts
162
+ interface WarningEvent {
163
+ signal?: SignalName; // which signal triggered the warning, if applicable
164
+ message: string; // human-readable warning message
165
+ }
166
+ ```
167
+
168
+ ### `CompleteEvent`
169
+
170
+ ```ts
171
+ interface CompleteEvent {
172
+ result: FingerprintResult; // the completed fingerprint result
173
+ }
174
+ ```
175
+
176
+ ---
177
+
178
+ ## `FingerprintResult`
179
+
180
+ The complete output of `collect()` or `collectSync()`.
181
+
182
+ ```ts
183
+ interface FingerprintResult {
184
+ schemaVersion: number;
185
+ libraryVersion: string;
186
+ thumbprint: string;
187
+ confidence: number;
188
+ componentHashes: Partial<Record<SignalName, string>>;
189
+ signals: Partial<Record<SignalName, SignalResult>>;
190
+ antiSpoof: AntiSpoofReport;
191
+ warnings: string[];
192
+ diagnostics: CollectorDiagnostics;
193
+ }
194
+ ```
195
+
196
+ | Field | Type | Description |
197
+ |-------|------|-------------|
198
+ | `schemaVersion` | `number` | Schema version (currently `2`). Bumped on breaking result shape changes. |
199
+ | `libraryVersion` | `string` | Library version that produced this result (e.g. `"0.1.1"`). |
200
+ | `thumbprint` | `string` | SHA-256 hash of all component hashes combined. The primary fingerprint identifier. |
201
+ | `confidence` | `number` | 0–1 score. 0 = unreliable, 1 = high confidence. Weighted average of signal quality blended with anti-spoof score. |
202
+ | `componentHashes` | `Partial<Record<SignalName, string>>` | Per-signal SHA-256 hashes of canonicalized signal values. Only present for signals that returned `value`. |
203
+ | `signals` | `Partial<Record<SignalName, SignalResult>>` | Raw signal collection results, keyed by signal name. |
204
+ | `antiSpoof` | `AntiSpoofReport` | Cross-signal coherence analysis and automation detection. |
205
+ | `warnings` | `string[]` | Non-fatal warnings from the collection run. |
206
+ | `diagnostics` | `CollectorDiagnostics` | Metadata about what ran and how long it took. |
207
+
208
+ ### Thumbprint derivation
209
+
210
+ 1. Each signal value is canonicalized (deterministic JSON serialization with sorted keys, normalized numbers, ISO dates).
211
+ 2. Each canonicalized value is hashed with SHA-256 `componentHashes`.
212
+ 3. Component hashes are sorted by name, serialized as `[[name, hash], ...]`, canonicalized, and hashed once more `thumbprint`.
213
+
214
+ ---
215
+
216
+ ## `SignalResult`
217
+
218
+ The result of collecting a single signal.
219
+
220
+ ```ts
221
+ interface SignalResult<T = unknown> {
222
+ status: 'ok' | 'unsupported' | 'blocked' | 'timeout' | 'error';
223
+ value?: T;
224
+ durationMs: number;
225
+ error?: string;
226
+ }
227
+ ```
228
+
229
+ | Field | Type | Description |
230
+ |-------|------|-------------|
231
+ | `status` | `SignalStatus` | Outcome of the collection attempt. |
232
+ | `value` | `T` (optional) | The collected signal data. Only present when `status` is `'ok'`. |
233
+ | `durationMs` | `number` | Wall-clock milliseconds spent collecting this signal. |
234
+ | `error` | `string` (optional) | Human-readable error description. Present when status is not `'ok'`. |
235
+
236
+ ### `SignalStatus`
237
+
238
+ ```ts
239
+ type SignalStatus = 'ok' | 'unsupported' | 'blocked' | 'timeout' | 'error';
240
+ ```
241
+
242
+ | Value | Meaning |
243
+ |-------|---------|
244
+ | `ok` | Signal collected successfully. `value` is populated. |
245
+ | `unsupported` | The required browser API is not available. |
246
+ | `blocked` | The API exists but access was denied (e.g. permissions, secure context). |
247
+ | `timeout` | Collection exceeded the per-signal timeout. |
248
+ | `error` | An unexpected error occurred during collection. |
249
+
250
+ ---
251
+
252
+ ## `AntiSpoofReport`
253
+
254
+ Cross-signal coherence analysis.
255
+
256
+ ```ts
257
+ interface AntiSpoofReport {
258
+ score: number;
259
+ anomalies: string[];
260
+ automationHints: string[];
261
+ fakedOS: boolean;
262
+ fakedBrowser: boolean;
263
+ }
264
+ ```
265
+
266
+ | Field | Type | Description |
267
+ |-------|------|-------------|
268
+ | `score` | `number` | Normalized score 0–1. 0 = automated/bot, 1 = genuine user. Each anomaly deducts 0.12; each automation hint deducts 0.18. Floor at 0, ceiling at 1. |
269
+ | `anomalies` | `string[]` | Specific inconsistencies found between signals. |
270
+ | `automationHints` | `string[]` | Indicators of headless, automated, or bot environments. |
271
+ | `fakedOS` | `boolean` | `true` when `ua_platform_mismatch` is in `anomalies` — the UA string's OS contradicts `navigator.platform`. |
272
+ | `fakedBrowser` | `boolean` | `true` when the UA claims a browser whose characteristic globals are missing (requires `riskSignals` to be collected). |
273
+
274
+ ### Anomaly codes
275
+
276
+ | Code | Trigger |
277
+ |------|---------|
278
+ | `ua_platform_mismatch` | `navigator.platform` contradicts UA Client Hints platform |
279
+ | `touch_claim_without_touch_points` | iPhone UA but `maxTouchPoints` is 0 |
280
+ | `mobile_ua_desktop_screen` | Android UA but screen width >= 1600px |
281
+ | `windows_ua_mac_platform` | Windows UA string but platform reports macOS |
282
+ | `implausible_device_memory` | `deviceMemory` < 0.25 or > 64 GB |
283
+ | `tiny_screen_geometry` | Screen width and height both < 200px |
284
+
285
+ ### Automation hint codes
286
+
287
+ | Code | Trigger |
288
+ |------|---------|
289
+ | `navigator_webdriver` | `navigator.webdriver` is truthy |
290
+ | `headless_user_agent` | UA string contains `"headless"` |
291
+
292
+ ---
293
+
294
+ ## `CollectorDiagnostics`
295
+
296
+ ```ts
297
+ interface CollectorDiagnostics {
298
+ requestedSignals: SignalName[];
299
+ completedSignals: SignalName[];
300
+ elapsedMs: number;
301
+ }
302
+ ```
303
+
304
+ | Field | Type | Description |
305
+ |-------|------|-------------|
306
+ | `requestedSignals` | `SignalName[]` | Signals requested for the run (after include/exclude/mode resolution). |
307
+ | `completedSignals` | `SignalName[]` | Signals that produced a result (any status). Sorted alphabetically. |
308
+ | `elapsedMs` | `number` | Total wall-clock milliseconds for the entire run (rounded to 3 decimal places). |
309
+
310
+ ---
311
+
312
+ ---
313
+
314
+ ## Confidence scoring
315
+
316
+ Each signal has a fixed weight. The base confidence is the weighted ratio of earned points to available points.
317
+
318
+ ### Signal weights
319
+
320
+ | Signal | Weight | Signal | Weight |
321
+ |--------|--------|--------|--------|
322
+ | `canvas` | 1.0 | `audio` | 0.9 |
323
+ | `webgl` | 1.0 | `fonts` | 0.9 |
324
+ | `screen` | 0.8 | `webrtc` | 0.8 |
325
+ | `uaHints` | 0.7 | `hardware` | 0.7 |
326
+ | `riskSignals` | 0.7 | `mediaDevices` | 0.6 |
327
+ | `networkInfo` | 0.6 | `ua` | 0.5 |
328
+ | `locale` | 0.5 | `storage` | 0.4 |
329
+ | `permissions` | 0.4 | `navigationInfo` | 0.4 |
330
+ | `frameInfo` | 0.3 | `referrerInfo` | 0.3 |
331
+ | `paymentSupport` | 0.2 | | |
332
+
333
+ ### Points per status
334
+
335
+ | Status | Points earned |
336
+ |--------|---------------|
337
+ | `ok` | Full weight |
338
+ | `unsupported` | 35% of weight |
339
+ | `blocked` | 35% of weight |
340
+ | `timeout` | 15% of weight |
341
+ | `error` | 0% |
342
+
343
+ ### Final score
344
+
345
+ ```
346
+ base = earned / available
347
+ final = base * (0.65 + antiSpoofScore * 0.35)
348
+ ```
349
+
350
+ The anti-spoof score blends 35% into the final confidence, meaning a low anti-spoof score drags confidence down proportionally.
351
+
352
+ ---
353
+
354
+ ## Source constants
355
+
356
+ These values are exported from `src/constants.ts` and re-exported from the package entry point (`src/index.ts`). Import them directly:
357
+
358
+ ```ts
359
+ import {
360
+ CORE_SIGNALS,
361
+ EXTENDED_SIGNALS,
362
+ SYNC_SIGNALS,
363
+ DEFAULT_TIMEOUT_MS,
364
+ DEFAULT_FONT_LIST,
365
+ DEFAULT_PERMISSION_NAMES,
366
+ DEFAULT_UA_HINTS,
367
+ } from '@ianmenethil/zp-devicefp';
368
+ ```
369
+
370
+ | Constant | Type | Value |
371
+ |----------|------|-------|
372
+ | `CORE_SIGNALS` | `SignalName[]` | `['ua', 'uaHints', 'locale', 'screen', 'hardware', 'storage', 'fonts', 'canvas', 'webgl', 'audio']` |
373
+ | `EXTENDED_SIGNALS` | `SignalName[]` | `['mediaDevices', 'permissions', 'webrtc', 'frameInfo', 'networkInfo', 'paymentSupport', 'referrerInfo', 'navigationInfo', 'riskSignals', 'adblock', 'geolocation']` |
374
+ | `SYNC_SIGNALS` | `SignalName[]` | `['ua', 'locale', 'screen', 'hardware', 'storage', 'fonts', 'canvas', 'webgl', 'frameInfo', 'networkInfo', 'paymentSupport', 'referrerInfo', 'navigationInfo']` |
375
+ | `DEFAULT_TIMEOUT_MS` | `number` | `1500` |
376
+ | `DEFAULT_FONT_LIST` | `string[]` | 70 font faces — Fingerprint2 / Cardinal Commerce set (Andale Mono, Arial, Baskerville, Calibri, Comic Sans MS, Consolas, Courier New, Georgia, Helvetica, Impact, Times New Roman, Verdana, Wingdings, and 57 more) |
377
+ | `DEFAULT_PERMISSION_NAMES` | `readonly string[]` | `['geolocation', 'notifications', 'camera', 'microphone']` |
378
+ | `DEFAULT_UA_HINTS` | `readonly string[]` | `['architecture', 'bitness', 'formFactors', 'fullVersionList', 'model', 'platform', 'platformVersion', 'wow64']` |
379
+
380
+ ---
381
+
382
+ ## CDN / IIFE API
383
+
384
+ When loaded via `<script>` tag, the library attaches to `window.DeviceFP`.
385
+
386
+ ```html
387
+ <script src="https://cdn.jsdelivr.net/npm/@ianmenethil/zp-devicefp/dist/cdn/zp.dfp.min.js"></script>
388
+ <script>
389
+ const client = await window.DeviceFP.createFingerprintClient({ timeoutMs: 1500 });
390
+ const result = await client.collect();
391
+ console.log(window.DeviceFP.LIBRARY_VERSION);
392
+ console.log(window.DeviceFP.SCHEMA_VERSION);
393
+ </script>
394
+ ```
395
+
396
+ ### `window.DeviceFP` shape
397
+
398
+ | Property | Type |
399
+ |----------|------|
400
+ | `createFingerprintClient` | `(options?: CollectOptions) => Promise<FingerprintClient>` |
401
+ | `LIBRARY_VERSION` | `string` |
402
+ | `SCHEMA_VERSION` | `number` |
403
+
404
+ ### CDN files
405
+
406
+ | File | Format | Description |
407
+ |------|--------|-------------|
408
+ | `zp.dfp.js` | IIFE | Unminified global bundle |
409
+ | `zp.dfp.min.js` | IIFE | Minified global bundle |
410
+ | `zp.dfp.obf.js` | IIFE | Minified + obfuscated global bundle |
411
+ | `zp.dfp.esm.js` | ESM | Module for `import()` / `<script type="module">` |
412
+ | `zp.dfp.manifest.json` | JSON | SRI integrity hashes for all CDN files |
413
+
414
+ ### CDN sources
415
+
416
+ | Source | Base URL |
417
+ |--------|----------|
418
+ | jsDelivr | `https://cdn.jsdelivr.net/npm/@ianmenethil/zp-devicefp/dist/cdn/` |
419
+ | Self-hosted | `https://cdn.zenithpayments.support/devicefp/` |
420
+
421
+ ---
422
+
423
+ ## Error handling
424
+
425
+ Every signal result includes an explicit `status` field. The library never swallows errors silently — each signal that fails reports its outcome and, where applicable, an error message.
426
+
427
+ ### Signal-level errors
428
+
429
+ Each `SignalResult.status` can be one of:
430
+
431
+ | Status | Meaning | `value` | `error` |
432
+ |--------|---------|---------|---------|
433
+ | `ok` | Collected successfully | Present | Absent |
434
+ | `unsupported` | Required browser API not available | Absent | Present |
435
+ | `blocked` | API exists but access denied (permissions, secure context) | Absent | Present |
436
+ | `timeout` | Collection exceeded `timeoutMs` | Absent | `"Signal collection timed out."` |
437
+ | `error` | Unexpected error during collection | Absent | Present |
438
+
439
+ ### Aborted collections
440
+
441
+ When an `AbortSignal` fires before or during collection:
442
+
443
+ - Signals not yet started return `status: 'error'` with `error: 'Collection aborted before signal execution started.'`.
444
+ - In-flight signals return `status: 'error'` with `error: 'Collection aborted.'`.
445
+
446
+ The `collect()` promise still resolves — the abort does not reject. Aborted/missing signals appear in `signals` with `status: 'error'` like any other failure.
447
+
448
+ ### Warnings
449
+
450
+ Non-fatal issues during collection (e.g., a signal API is unavailable, a signal has no collector registered) are reported via:
451
+
452
+ - The `warnings` array in `FingerprintResult` (accumulated across the run).
453
+ - The `warning` event on the event emitter (emitted in real-time during `collect()`).
454
+
455
+ ---
456
+
457
+ ## Extension and customization
458
+
459
+ The library does not currently expose a public extension API for registering custom signal collectors. The `SignalCollector` interface and `collectorMap` exist internally (`src/types/internal.ts`, `src/signals/index.ts`) but are not re-exported from the package entry point.
460
+
461
+ To extend the library, the supported approach is:
462
+
463
+ - **Include/exclude signals** via `CollectOptions.include` and `CollectOptions.exclude` to control which of the 21 built-in signals are collected.
464
+ - **Set `extended: true`** to add the 11 extended-tier signals to a collection run.
465
+ - **Adjust `timeoutMs`** per-run to balance thoroughness against latency.
466
+ - **Use `AbortSignal`** to cancel collection early from application code.
467
+ - **Use the event system** (`client.on('progress', ...)`, `client.on('warning', ...)`, `client.on('complete', ...)`) to observe and react to collection progress.
468
+
469
+ ---
470
+
471
+ ## Limitations and non-goals
472
+
473
+ - **Browser-side fingerprints are probabilistic**, not identity proofs. The thumbprint is a best-effort device identifier that can change across browser updates, privacy settings, and hardware changes.
474
+ - **Font detection is bounded** to 70 common fonts measured against 3 generic families. It does not scan the full system font catalog.
475
+ - **The anti-spoof report is heuristic** — combine with server-side context (IP reputation, behavioral analysis, request headers) for fraud or login-risk decisions.
476
+ - **No server-side identity resolution** — the library computes a client thumbprint only. Deriving authoritative server-side identities, clustering, and deduplication are the caller's responsibility.
477
+ - **No runtime dependencies** — this is intentional. The library bundles its own SHA-256 implementation and does not load external resources.
478
+ - **No battery, sensor, or installed-app probes** — these are excluded by design for privacy and stability reasons.
479
+
480
+ ---
481
+
482
+ ## NPM package layout
483
+
484
+ ```text
485
+ @ianmenethil/zp-devicefp/
486
+ ├── dist/npm/
487
+ │ ├── index.mjs # ESM entry (browser, ES2022)
488
+ │ ├── index.cjs # CJS entry (Node 22+)
489
+ │ └── index.d.ts # Public type declarations
490
+ ├── dist/cdn/
491
+ │ ├── zp.dfp.js # IIFE (unminified)
492
+ │ ├── zp.dfp.min.js # IIFE (minified)
493
+ │ ├── zp.dfp.obf.js # IIFE (minified + obfuscated)
494
+ │ ├── zp.dfp.esm.js # ESM for import()
495
+ │ └── zp.dfp.manifest.json
496
+ ```
497
+
498
+ ---
499
+
500
+ ## NPM usage
501
+
502
+ ```bash
503
+ bun add @ianmenethil/zp-devicefp
504
+ ```
505
+
506
+ ```ts
507
+ import {
508
+ createFingerprintClient,
509
+ LIBRARY_VERSION,
510
+ SCHEMA_VERSION,
511
+ CORE_SIGNALS,
512
+ EXTENDED_SIGNALS,
513
+ } from '@ianmenethil/zp-devicefp';
514
+
515
+ const client = await createFingerprintClient({ timeoutMs: 2000 });
516
+ const result = await client.collect({ extended: true });
517
+
518
+ console.log(result.thumbprint);
519
+ console.log(result.confidence);
520
+ ```