@ianmenethil/zp-devicefp 0.1.0-alpha.1 → 0.1.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.
package/README.md CHANGED
@@ -1,6 +1,12 @@
1
1
  # Browser Fingerprint Library
2
2
 
3
- Async-first TypeScript browser and device fingerprinting library built from the supplied design brief. It collects a tiered set of browser signals, computes per-component hashes, returns a composite thumbprint, and includes a lightweight anomaly report instead of pretending a client-only fingerprint is unforgeable.
3
+ Async-first TypeScript browser and device fingerprinting library. Collects 19 tiered browser signals, computes per-component SHA-256 hashes, returns a composite thumbprint, and includes an anti-spoof anomaly report with confidence scoring.
4
+
5
+ - **Zero runtime dependencies** — no CDN loads, no npm deps at runtime.
6
+ - **ESM, CJS, and IIFE** — single TypeScript source tree, three distribution formats.
7
+ - **84 tests, 95% line coverage.**
8
+
9
+ ---
4
10
 
5
11
  ## Install
6
12
 
@@ -12,97 +18,236 @@ bun add @ianmenethil/zp-devicefp
12
18
  import { createFingerprintClient } from '@ianmenethil/zp-devicefp';
13
19
  ```
14
20
 
15
- ## Package contents
21
+ ## Quick start
16
22
 
17
- ```text
18
- @ianmenethil/zp-devicefp/
19
- ├── dist/
20
- │ ├── npm/
21
- │ │ ├── index.mjs ← ESM entry (browser, es2022)
22
- │ │ ├── index.cjs ← CJS entry (Node 22+)
23
- │ │ └── index.d.ts ← public types
24
- │ └── cdn/
25
- │ ├── zp.dfp.js ← IIFE (unminified)
26
- │ ├── zp.dfp.min.js ← IIFE (minified)
27
- │ ├── zp.dfp.obf.js ← IIFE (minified + obfuscated)
28
- │ ├── zp.dfp.esm.js ← ESM for import()
29
- │ └── zp.dfp.manifest.json ← SRI integrity hashes
30
- ├── README.md
31
- ├── LICENSE
32
- └── docs/
23
+ ```ts
24
+ import { createFingerprintClient } from '@ianmenethil/zp-devicefp';
25
+
26
+ const client = await createFingerprintClient({ timeoutMs: 1500 });
27
+
28
+ // Async collection all signals, parallel with timeouts
29
+ const result = await client.collect();
30
+
31
+ console.log(result.thumbprint); // "a1b2c3d4..." (64-char SHA-256 hex)
32
+ console.log(result.confidence); // 0.87
33
+ console.log(result.antiSpoof.score); // 0.94
33
34
  ```
34
35
 
35
- ## CDN
36
+ ---
36
37
 
37
- CDN files are available from two sources — jsDelivr (automatic from each npm publish) and the self-hosted CDN at `cdn.zenithpayments.support` (deployed on every build).
38
+ ## API overview
38
39
 
39
- | File | jsDelivr | Self-hosted |
40
- |------|----------|-------------|
41
- | IIFE (minified) | `@ianmenethil/zp-devicefp/dist/cdn/zp.dfp.min.js` | `devicefp/latest/zp.dfp.min.js` |
42
- | IIFE (unminified) | `@ianmenethil/zp-devicefp/dist/cdn/zp.dfp.js` | `devicefp/latest/zp.dfp.js` |
43
- | IIFE (obfuscated) | `@ianmenethil/zp-devicefp/dist/cdn/zp.dfp.obf.js` | `devicefp/latest/zp.dfp.obf.js` |
44
- | ESM | `@ianmenethil/zp-devicefp/dist/cdn/zp.dfp.esm.js` | `devicefp/latest/zp.dfp.esm.js` |
45
- | Manifest | `@ianmenethil/zp-devicefp/dist/cdn/zp.dfp.manifest.json` | `devicefp/latest/zp.dfp.manifest.json` |
40
+ | Method | Returns | Description |
41
+ |--------|---------|-------------|
42
+ | `client.collect(options?)` | `Promise<FingerprintResult>` | Async collection. All signals run in parallel with per-signal timeouts. Emits events. |
43
+ | `client.collectSync(options?)` | `FingerprintResult` | Sync collection. Only 13 of 19 signals support sync mode. No timeout/abort. |
44
+ | `client.upload(result, options)` | `Promise<Response>` | POST the fingerprint to a backend endpoint. |
45
+ | `client.on(event, callback)` | `() => void` | Subscribe to `progress`, `warning`, or `complete` events during async collection. |
46
46
 
47
- - **jsDelivr base:** `https://cdn.jsdelivr.net/npm/` — drop the version tag for latest, or pin with `@2.1` or `@2`.
48
- - **Self-hosted base:** `https://cdn.zenithpayments.support/` — `latest/` always points to the most recent build; versioned folders (`2.1.1/`) are created on `bun run release`.
47
+ ### Events
49
48
 
50
- ### CDN usage
49
+ ```ts
50
+ client.on('progress', (e) => {
51
+ // { completed: 3, total: 10, signal: 'canvas', result: SignalResult }
52
+ console.log(`${e.completed}/${e.total}: ${e.signal}`);
53
+ });
51
54
 
52
- ```html
53
- <!-- jsDelivr -->
54
- <script src="https://cdn.jsdelivr.net/npm/@ianmenethil/zp-devicefp/dist/cdn/zp.dfp.min.js"></script>
55
+ client.on('warning', (e) => {
56
+ // { signal?: 'webrtc', message: 'RTCPeerConnection not available' }
57
+ console.warn(e.message);
58
+ });
55
59
 
56
- <!-- Self-hosted -->
57
- <script src="https://cdn.zenithpayments.support/devicefp/latest/zp.dfp.min.js"></script>
60
+ client.on('complete', (e) => {
61
+ // { result: FingerprintResult }
62
+ console.log('Done', e.result.thumbprint);
63
+ });
64
+ ```
58
65
 
59
- <script>
60
- const client = await window.DeviceFP.createFingerprintClient({ timeoutMs: 1500 });
61
- const result = await client.collect();
62
- </script>
66
+ ### Upload
67
+
68
+ ```ts
69
+ await client.upload(result, {
70
+ endpoint: 'https://api.example.com/fingerprint',
71
+ headers: { 'Authorization': 'Bearer token' },
72
+ bodyExtras: { sessionId: 'abc123' },
73
+ });
63
74
  ```
64
75
 
65
- ## Public API
76
+ ### Aborting
66
77
 
67
78
  ```ts
68
- import { createFingerprintClient } from '@ianmenethil/zp-devicefp';
79
+ const controller = new AbortController();
69
80
 
70
- const client = await createFingerprintClient({
71
- timeoutMs: 1500,
72
- extended: false,
73
- });
81
+ const resultPromise = client.collect({ abortSignal: controller.signal });
74
82
 
75
- const result = await client.collect();
76
- console.log(result.thumbprint, result.antiSpoof);
77
- console.log(result.diagnostics);
83
+ // Cancel mid-flight
84
+ controller.abort();
85
+
86
+ const result = await resultPromise;
87
+ // Aborted signals appear as status: 'error' with error: 'Collection aborted.'
78
88
  ```
79
89
 
80
- `collectSync()` intentionally returns a partial fingerprint that only uses sync-safe collectors.
90
+ ---
81
91
 
82
- Each result also includes:
92
+ ## Options
83
93
 
84
- - `componentHashes`: deterministic per-signal digests
85
- - `antiSpoof`: anomaly and automation hints
86
- - `diagnostics`: requested signals, completed signals, and elapsed runtime
87
- - `warnings`: collection-time warnings that are scoped to that run only
94
+ ```ts
95
+ interface CollectOptions {
96
+ include?: SignalName[]; // whitelist: only collect these signals
97
+ exclude?: SignalName[]; // blacklist: skip these signals
98
+ timeoutMs?: number; // per-signal timeout (default: 1500)
99
+ abortSignal?: AbortSignal; // cancel in-flight collection
100
+ extended?: boolean; // include extended-tier signals (default: false)
101
+ debug?: boolean; // enable debug logging (default: false)
102
+ }
103
+ ```
88
104
 
89
- ## Signals
105
+ ---
90
106
 
91
- Core tier:
107
+ ## Result shape
92
108
 
93
- `ua` · `uaHints` · `locale` · `screen` · `hardware` · `storage` · `fonts` · `canvas` · `webgl` · `audio`
109
+ ```ts
110
+ interface FingerprintResult {
111
+ schemaVersion: number; // schema version (integer, bumped on breaking changes)
112
+ libraryVersion: string; // library semver string
113
+ thumbprint: string; // SHA-256 of all component hashes combined
114
+ confidence: number; // 0–1 quality score
115
+ componentHashes: Partial<Record<SignalName, string>>; // per-signal SHA-256 digests
116
+ signals: Partial<Record<SignalName, SignalResult>>; // raw signal results
117
+ antiSpoof: AntiSpoofReport; // coherence analysis and automation hints
118
+ warnings: string[]; // non-fatal collection warnings
119
+ diagnostics: CollectorDiagnostics; // run metadata
120
+ }
121
+ ```
94
122
 
95
- Extended tier:
123
+ ### `AntiSpoofReport`
96
124
 
97
- `mediaDevices` · `permissions` · `webrtc` · `frameInfo` · `networkInfo` · `paymentSupport` · `referrerInfo` · `navigationInfo` · `riskSignals`
125
+ ```ts
126
+ {
127
+ score: number; // 0 = automated/bot, 1 = genuine user
128
+ anomalies: string[]; // signal inconsistencies (6 anomaly types)
129
+ automationHints: string[]; // headless/bot indicators (2 hint types)
130
+ }
131
+ ```
98
132
 
99
- ## Design choices
133
+ ### `SignalResult`
134
+
135
+ ```ts
136
+ {
137
+ status: 'ok' | 'unsupported' | 'blocked' | 'timeout' | 'error';
138
+ value?: unknown; // signal-specific data (only when status === 'ok')
139
+ durationMs: number; // wall-clock collection time
140
+ error?: string; // error description (when status !== 'ok')
141
+ }
142
+ ```
143
+
144
+ Full API reference with all types, methods, events, options, confidence scoring, anti-spoof heuristics, and constants: **[docs/api-reference.md](docs/api-reference.md)**
145
+
146
+ ---
147
+
148
+ ## Signals (19 total)
149
+
150
+ ### Core tier (10 signals, enabled by default)
100
151
 
101
- - Async first: high-value collectors like audio, UA hints, media devices, permissions, and WebRTC run in `collect()`.
102
- - Explainable output: every signal includes a status and duration.
103
- - No dangerous defaults: no battery, sensors, arbitrary installed-app probes, or local IP harvesting.
104
- - Anti-spoofing focuses on coherence checks and automation hints.
105
- - Upload is backend-neutral and optional.
152
+ | Signal | Sync | Description |
153
+ |--------|------|-------------|
154
+ | `ua` | yes | `navigator.userAgent`, platform, webdriver, maxTouchPoints, cookieEnabled, vendor |
155
+ | `uaHints` | no | User-Agent Client Hints: brands, platform, high-entropy values (architecture, bitness, model, etc.) |
156
+ | `locale` | yes | Language, languages, locale, calendar, numbering system, timeZone, formatted samples |
157
+ | `screen` | yes | Screen width/height, colorDepth, orientation, DPR, media queries (color-gamut, reduced-motion, contrast, forced-colors) |
158
+ | `hardware` | yes | `navigator.hardwareConcurrency`, `deviceMemory`, platform, maxTouchPoints |
159
+ | `storage` | yes | localStorage, sessionStorage, IndexedDB, Web SQL availability (write-read-delete probe) |
160
+ | `fonts` | yes | Installed font detection via hidden DOM span measurement (10 fonts, 3 generic families) |
161
+ | `canvas` | yes | Canvas 2D rendering pattern → `toDataURL()` (GPU+driver fingerprint) |
162
+ | `webgl` | yes | WebGL renderer/vendor (unmasked), version, extensions, max texture/viewport |
163
+ | `audio` | no | OfflineAudioContext oscillator+compressor numeric signature |
164
+
165
+ ### Extended tier (9 signals, requires `extended: true`)
166
+
167
+ | Signal | Sync | Description |
168
+ |--------|------|-------------|
169
+ | `mediaDevices` | no | Media device counts by kind (audioinput, audiooutput, videoinput) — no labels/IDs |
170
+ | `permissions` | no | Permissions API state for geolocation, notifications, camera, microphone |
171
+ | `webrtc` | no | RTCPeerConnection SDP codec/extmap capability hash |
172
+ | `frameInfo` | yes | Iframe count, source domains, top-level window check |
173
+ | `networkInfo` | yes | NetworkInformation API: effectiveType, downlink, RTT, saveData |
174
+ | `paymentSupport` | yes | `window.PaymentRequest` availability |
175
+ | `referrerInfo` | yes | `document.referrer` |
176
+ | `navigationInfo` | yes | PerformanceNavigationTiming.type (navigate/reload/back_forward/prerender) |
177
+ | `riskSignals` | no | Bot/headless detection: webdriver, headless UA, missing chrome object, zero dimensions, empty plugins, UA-CH consistency, notification permission, time to capture |
178
+
179
+ Complete per-signal documentation with value shapes, APIs used, and edge cases: **[docs/signals.md](docs/signals.md)**
180
+
181
+ ---
182
+
183
+ ## Anti-spoof heuristics (6 anomalies + 2 automation hints)
184
+
185
+ | Code | What it detects |
186
+ |------|----------------|
187
+ | `navigator_webdriver` | `navigator.webdriver` is truthy |
188
+ | `headless_user_agent` | UA contains `"headless"` |
189
+ | `ua_platform_mismatch` | `navigator.platform` contradicts UA-CH platform |
190
+ | `touch_claim_without_touch_points` | iPhone UA but `maxTouchPoints` is 0 |
191
+ | `mobile_ua_desktop_screen` | Android UA with screen width >= 1600px |
192
+ | `windows_ua_mac_platform` | Windows UA but platform reports macOS |
193
+ | `implausible_device_memory` | `deviceMemory` < 0.25 or > 64 GB |
194
+ | `tiny_screen_geometry` | Screen width and height both < 200px |
195
+
196
+ ---
197
+
198
+ ## Confidence scoring
199
+
200
+ Each signal has a fixed weight (0.2–1.0). Canvas, WebGL, fonts, and audio carry the highest weight. Status affects points earned: `ok` = full weight, `unsupported`/`blocked` = 35%, `timeout` = 15%, `error` = 0.
201
+
202
+ The base ratio is blended with the anti-spoof score (65% base / 35% anti-spoof). Full details in [docs/api-reference.md](docs/api-reference.md).
203
+
204
+ ---
205
+
206
+ ## CDN
207
+
208
+ CDN files are available from jsDelivr (automatic from npm publish) and self-hosted.
209
+
210
+ | File | Format |
211
+ |------|--------|
212
+ | `zp.dfp.js` | IIFE (unminified) |
213
+ | `zp.dfp.min.js` | IIFE (minified) |
214
+ | `zp.dfp.obf.js` | IIFE (minified + obfuscated) |
215
+ | `zp.dfp.esm.js` | ESM for `import()` |
216
+ | `zp.dfp.manifest.json` | SRI integrity hashes |
217
+
218
+ ```html
219
+ <!-- jsDelivr -->
220
+ <script src="https://cdn.jsdelivr.net/npm/@ianmenethil/zp-devicefp/dist/cdn/zp.dfp.min.js"></script>
221
+
222
+ <!-- Self-hosted -->
223
+ <script src="https://cdn.zenithpayments.support/devicefp/latest/zp.dfp.min.js"></script>
224
+
225
+ <script>
226
+ const client = await window.DeviceFP.createFingerprintClient({ timeoutMs: 1500 });
227
+ const result = await client.collect();
228
+ console.log(window.DeviceFP.LIBRARY_VERSION); // "0.1.0"
229
+ </script>
230
+ ```
231
+
232
+ ---
233
+
234
+ ## Package layout
235
+
236
+ ```text
237
+ dist/
238
+ ├── npm/
239
+ │ ├── index.mjs # ESM entry (browser, ES2022)
240
+ │ ├── index.cjs # CJS entry (Node 22+)
241
+ │ └── index.d.ts # public types
242
+ └── cdn/
243
+ ├── zp.dfp.js # IIFE (unminified)
244
+ ├── zp.dfp.min.js # IIFE (minified)
245
+ ├── zp.dfp.obf.js # IIFE (minified + obfuscated)
246
+ ├── zp.dfp.esm.js # ESM for import()
247
+ └── zp.dfp.manifest.json
248
+ ```
249
+
250
+ ---
106
251
 
107
252
  ## Build and test
108
253
 
@@ -113,22 +258,32 @@ bun test # 84 tests, 95% line coverage
113
258
  bun run smoke # validates all dist artifacts
114
259
  ```
115
260
 
116
- The build pipeline runs esbuild to produce `dist/npm/` and `dist/cdn/`, then copies CDN files to the local CDN server directory for self-hosting.
261
+ ---
117
262
 
118
- ## Limitations
263
+ ## Docs
119
264
 
120
- - Browser-side fingerprints are probabilistic, not identity proofs.
121
- - The default font probe is intentionally bounded and conservative.
122
- - The anti-spoof report is heuristic and should be combined with server-side context for real fraud or login-risk decisions.
265
+ | Document | Covers |
266
+ |----------|--------|
267
+ | [docs/api-reference.md](docs/api-reference.md) | Complete API: types, methods, events, options, constants, confidence, anti-spoof, upload, CDN |
268
+ | [docs/signals.md](docs/signals.md) | All 19 signals: value shapes, APIs used, sync support, edge cases |
269
+ | [docs/architecture.md](docs/architecture.md) | High-level flow and module map |
270
+ | [docs/privacy.md](docs/privacy.md) | Privacy-by-design notes |
271
+ | [docs/testing.md](docs/testing.md) | Test strategy and coverage intent |
123
272
 
124
- ## Layout
273
+ ---
125
274
 
126
- ```text
127
- zp-devicefp/
128
- ├── src/ # 19 signal collectors, core modules
129
- ├── tests/ # bun test (unit + integration)
130
- ├── docs/ # architecture, privacy, testing docs
131
- ├── .scripts/ # esbuild build pipeline
132
- ├── scripts/ # smoke tests
133
- └── dist/ # generated (npm/ + cdn/)
134
- ```
275
+ ## Design choices
276
+
277
+ - **Async first** high-value collectors (audio, UA hints, media devices, permissions, WebRTC, risk signals) run in `collect()`.
278
+ - **Explainable output** every signal includes a status and duration. No silent failures.
279
+ - **No dangerous defaults** — no battery, sensors, arbitrary installed-app probes, or local IP harvesting.
280
+ - **Anti-spoofing** focuses on coherence checks and automation hints, not "secret" client tricks.
281
+ - **Upload is backend-neutral and optional** — you control where (if anywhere) fingerprints are sent.
282
+
283
+ ---
284
+
285
+ ## Limitations
286
+
287
+ - Browser-side fingerprints are probabilistic, not identity proofs.
288
+ - Font detection is bounded to 10 common fonts and is intentionally conservative.
289
+ - The anti-spoof report is heuristic — combine with server-side context for fraud or login-risk decisions.
@@ -0,0 +1,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
+ | 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
+ ```
@@ -0,0 +1,383 @@
1
+ # Signal Reference
2
+
3
+ Every signal collected by the library, including its name, tier, sync support, description, and the shape of its `value` when `status` is `'ok'`.
4
+
5
+ ---
6
+
7
+ ## Signal overview
8
+
9
+ | # | Signal | Tier | Sync | Description |
10
+ |---|--------|------|------|-------------|
11
+ | 1 | `ua` | core | yes | Legacy `navigator.userAgent` and related properties |
12
+ | 2 | `uaHints` | core | no | User-Agent Client Hints (brands, platform, high-entropy values) |
13
+ | 3 | `locale` | core | yes | Language, locale, calendar, time zone, number/date formatting |
14
+ | 4 | `screen` | core | yes | Screen dimensions, color depth, orientation, DPR, media queries |
15
+ | 5 | `hardware` | core | yes | Hardware concurrency, device memory, platform |
16
+ | 6 | `storage` | core | yes | localStorage, sessionStorage, IndexedDB, Web SQL availability |
17
+ | 7 | `fonts` | core | yes | Installed font detection via DOM measurement |
18
+ | 8 | `canvas` | core | yes | Canvas 2D rendering fingerprint (GPU+driver signature) |
19
+ | 9 | `webgl` | core | yes | WebGL renderer, vendor, extensions, GPU capabilities |
20
+ | 10 | `audio` | core | no | OfflineAudioContext oscillator+compressor signature |
21
+ | 11 | `mediaDevices` | extended | no | Media device kind enumeration (counts only, no labels) |
22
+ | 12 | `permissions` | extended | no | Permissions API state for geolocation, notifications, camera, microphone |
23
+ | 13 | `webrtc` | extended | no | RTCPeerConnection codec and extmap capability hash |
24
+ | 14 | `frameInfo` | extended | yes | Iframe context: count, domains, top-level check |
25
+ | 15 | `networkInfo` | extended | yes | NetworkInformation API: effective type, downlink, RTT, saveData |
26
+ | 16 | `paymentSupport` | extended | yes | PaymentRequest API availability |
27
+ | 17 | `referrerInfo` | extended | yes | `document.referrer` |
28
+ | 18 | `navigationInfo` | extended | yes | PerformanceNavigationTiming type (navigate/reload/back_forward/prerender) |
29
+ | 19 | `riskSignals` | extended | no | Bot/headless detection: 9 heuristics |
30
+
31
+ ---
32
+
33
+ ## Core signals
34
+
35
+ ### `ua` — User-Agent string
36
+
37
+ **Tier:** core | **Sync:** yes | **APIs used:** `navigator`
38
+
39
+ ```ts
40
+ // value shape:
41
+ {
42
+ userAgent: string; // navigator.userAgent
43
+ appVersion: string; // navigator.appVersion
44
+ vendor: string; // navigator.vendor
45
+ platform: string; // navigator.platform
46
+ webdriver: boolean; // navigator.webdriver
47
+ maxTouchPoints: number; // navigator.maxTouchPoints
48
+ cookieEnabled: boolean; // navigator.cookieEnabled
49
+ vendorSub: string; // navigator.vendorSub
50
+ }
51
+ ```
52
+
53
+ Returns `'unsupported'` if `navigator` is not available.
54
+
55
+ ### `uaHints` — User-Agent Client Hints
56
+
57
+ **Tier:** core | **Sync:** no | **APIs used:** `navigator.userAgentData`
58
+
59
+ ```ts
60
+ // value shape:
61
+ {
62
+ brands: { brand: string; version: string }[]; // low-entropy brands
63
+ mobile: boolean; // userAgentData.mobile
64
+ platform: string; // userAgentData.platform
65
+ architecture?: string; // high-entropy: CPU architecture
66
+ bitness?: string; // high-entropy: 32/64
67
+ formFactors?: string[]; // high-entropy: device form factors
68
+ fullVersionList?: { brand: string; version: string }[]; // high-entropy: full versions
69
+ model?: string; // high-entropy: device model
70
+ platformVersion?: string; // high-entropy: OS version
71
+ wow64?: boolean; // high-entropy: 32-bit on 64-bit
72
+ }
73
+ ```
74
+
75
+ Returns `'unsupported'` if `navigator.userAgentData` is not available. Returns `'blocked'` if `getHighEntropyValues` throws.
76
+
77
+ ### `locale` — Language and internationalization
78
+
79
+ **Tier:** core | **Sync:** yes | **APIs used:** `navigator`, `Intl`
80
+
81
+ ```ts
82
+ // value shape:
83
+ {
84
+ language: string; // navigator.language
85
+ languages: readonly string[]; // navigator.languages
86
+ locale: string; // Intl.DateTimeFormat resolved locale
87
+ calendar: string; // e.g. "gregory", "iso8601"
88
+ numberingSystem: string; // e.g. "latn", "arab"
89
+ timeZone: string; // IANA timezone e.g. "America/New_York"
90
+ hourCycle: string; // e.g. "h12", "h23"
91
+ timeZoneOffsetMinutes: number; // UTC offset in minutes
92
+ formattedNumber: string; // Intl.NumberFormat sample: "123,456.789"
93
+ formattedDate: string; // Intl.DateTimeFormat sample for Jan 2, 2024
94
+ formattedRelativeDay: string | undefined; // Intl.RelativeTimeFormat: "yesterday" etc.
95
+ }
96
+ ```
97
+
98
+ Returns `'unsupported'` if `navigator` or `Intl` is not available.
99
+
100
+ ### `screen` — Screen and display
101
+
102
+ **Tier:** core | **Sync:** yes | **APIs used:** `screen`, `navigator`, `matchMedia`, `devicePixelRatio`
103
+
104
+ ```ts
105
+ // value shape:
106
+ {
107
+ width: number; // screen.width
108
+ height: number; // screen.height
109
+ availWidth: number; // screen.availWidth
110
+ availHeight: number; // screen.availHeight
111
+ colorDepth: number; // screen.colorDepth
112
+ pixelDepth: number; // screen.pixelDepth
113
+ orientationType: string; // screen.orientation.type
114
+ orientationAngle: number; // screen.orientation.angle
115
+ maxTouchPoints: number; // navigator.maxTouchPoints (0 if missing)
116
+ devicePixelRatio: number | undefined; // window.devicePixelRatio
117
+ colorGamutP3: boolean | 'unsupported'; // (color-gamut: p3) media query
118
+ prefersReducedMotion: boolean | 'unsupported'; // (prefers-reduced-motion: reduce)
119
+ prefersContrastMore: boolean | 'unsupported'; // (prefers-contrast: more)
120
+ forcedColorsActive: boolean | 'unsupported'; // (forced-colors: active)
121
+ }
122
+ ```
123
+
124
+ Returns `'unsupported'` if `screen` is not available.
125
+
126
+ ### `hardware` — Hardware properties
127
+
128
+ **Tier:** core | **Sync:** yes | **APIs used:** `navigator`
129
+
130
+ ```ts
131
+ // value shape:
132
+ {
133
+ hardwareConcurrency: number; // navigator.hardwareConcurrency (logical CPU cores)
134
+ deviceMemory: number | undefined; // navigator.deviceMemory (GB, may be undefined)
135
+ platform: string; // navigator.platform
136
+ maxTouchPoints: number; // navigator.maxTouchPoints
137
+ }
138
+ ```
139
+
140
+ Returns `'unsupported'` if `navigator` is not available.
141
+
142
+ ### `storage` — Storage API availability
143
+
144
+ **Tier:** core | **Sync:** yes | **APIs used:** `localStorage`, `sessionStorage`, `indexedDB`, `openDatabase`, `navigator`
145
+
146
+ ```ts
147
+ // value shape:
148
+ {
149
+ cookiesEnabled: boolean; // navigator.cookieEnabled
150
+ localStorage: 'available' | 'blocked' | 'unsupported'; // write-read-delete probe
151
+ sessionStorage: 'available' | 'blocked' | 'unsupported'; // write-read-delete probe
152
+ indexedDb: 'available' | 'unsupported'; // typeof indexedDB check
153
+ openDatabase: 'available' | 'unsupported'; // typeof openDatabase check (Web SQL)
154
+ pdfViewerEnabled: boolean; // navigator.pdfViewerEnabled
155
+ }
156
+ ```
157
+
158
+ ### `fonts` — Font detection
159
+
160
+ **Tier:** core | **Sync:** yes | **APIs used:** DOM (`document.createElement`, `offsetWidth`)
161
+
162
+ ```ts
163
+ // value shape:
164
+ {
165
+ detectedFonts: string[]; // sorted list of detected font family names
166
+ fontCount: number; // count of detected fonts
167
+ }
168
+ ```
169
+
170
+ Tests 10 fonts against 3 generic families (monospace, sans-serif, serif) by rendering hidden `<span>` elements and measuring width differences. Returns `'unsupported'` if the DOM is not available.
171
+
172
+ **Fonts probed:** `Arial`, `Helvetica Neue`, `Times New Roman`, `Georgia`, `Courier New`, `Trebuchet MS`, `Verdana`, `Tahoma`, `Impact`, `Comic Sans MS`
173
+
174
+ ### `canvas` — Canvas 2D fingerprint
175
+
176
+ **Tier:** core | **Sync:** yes | **APIs used:** Canvas 2D (`getContext('2d')`)
177
+
178
+ ```ts
179
+ // value shape:
180
+ {
181
+ dataUrl: string; // canvas.toDataURL() — base64 PNG of rendered pattern
182
+ winding: boolean; // ctx.isPointInPath(1, 1, 'evenodd')
183
+ }
184
+ ```
185
+
186
+ Renders a 280×80 canvas with a filled rectangle (#f60), text ("Browser FP"), and an arc. Differences in GPU, driver, and browser anti-aliasing produce distinct fingerprints.
187
+
188
+ Returns `'unsupported'` if the canvas 2D context is unavailable. Returns `'error'` if rendering throws.
189
+
190
+ ### `webgl` — WebGL fingerprint
191
+
192
+ **Tier:** core | **Sync:** yes | **APIs used:** WebGL (`getContext('webgl')`, `WEBGL_debug_renderer_info`)
193
+
194
+ ```ts
195
+ // value shape:
196
+ {
197
+ vendor: string; // UNMASKED_VENDOR_WEBGL or VENDOR
198
+ renderer: string; // UNMASKED_RENDERER_WEBGL or RENDERER
199
+ version: string; // WebGL version string
200
+ shadingLanguageVersion: string; // GLSL version string
201
+ maxTextureSize: number; // MAX_TEXTURE_SIZE
202
+ maxViewportDims: Int32Array; // MAX_VIEWPORT_DIMS
203
+ extensions: string[]; // getSupportedExtensions() sorted
204
+ }
205
+ ```
206
+
207
+ Attempts `webgl` context first, falls back to `experimental-webgl`. Uses `WEBGL_debug_renderer_info` for unmasked GPU strings when available.
208
+
209
+ Returns `'unsupported'` if canvas/WebGL is not available.
210
+
211
+ ### `audio` — AudioContext fingerprint
212
+
213
+ **Tier:** core | **Sync:** no | **APIs used:** `OfflineAudioContext`, `OscillatorNode`, `DynamicsCompressorNode`
214
+
215
+ ```ts
216
+ // value shape:
217
+ {
218
+ sampleRate: number; // rendered.sampleRate
219
+ length: number; // rendered.length (number of sample frames)
220
+ signalValue: number; // sum of absolute channel data values (6 decimal places)
221
+ }
222
+ ```
223
+
224
+ Creates an OfflineAudioContext (1 channel, 5000 samples, 44100 Hz), plays a 10 kHz triangle wave through a DynamicsCompressor with tuned parameters, then sums the absolute values of the rendered output. Different audio hardware/drivers produce distinct values.
225
+
226
+ Returns `'unsupported'` if `OfflineAudioContext` (or `webkitOfflineAudioContext`) is not available. Returns `'blocked'` if rendering throws.
227
+
228
+ ---
229
+
230
+ ## Extended signals
231
+
232
+ ### `mediaDevices` — Media device enumeration
233
+
234
+ **Tier:** extended | **Sync:** no | **APIs used:** `navigator.mediaDevices.enumerateDevices`
235
+
236
+ ```ts
237
+ // value shape — an object keyed by MediaDeviceKind:
238
+ {
239
+ audioinput: number; // count of audio input devices
240
+ audiooutput: number; // count of audio output devices
241
+ videoinput: number; // count of video input devices
242
+ }
243
+ ```
244
+
245
+ Only device counts and kinds are collected — no device labels or IDs. Requires a secure context (HTTPS) and a visible document.
246
+
247
+ Returns `'blocked'` if not in a secure context, if document is not visible, or if `enumerateDevices` throws. Returns `'unsupported'` if `mediaDevices` is not available.
248
+
249
+ ### `permissions` — Permissions API state
250
+
251
+ **Tier:** extended | **Sync:** no | **APIs used:** `navigator.permissions.query`
252
+
253
+ ```ts
254
+ // value shape:
255
+ {
256
+ geolocation: 'granted' | 'denied' | 'prompt' | 'unsupported';
257
+ notifications: 'granted' | 'denied' | 'prompt' | 'unsupported';
258
+ camera: 'granted' | 'denied' | 'prompt' | 'unsupported';
259
+ microphone: 'granted' | 'denied' | 'prompt' | 'unsupported';
260
+ }
261
+ ```
262
+
263
+ Queries 4 permission names in parallel. Each individual query that fails returns `'unsupported'` for that key.
264
+
265
+ Returns `'unsupported'` if `navigator.permissions` is not available. Returns `'blocked'` if all queries fail together.
266
+
267
+ ### `webrtc` — WebRTC codec capability
268
+
269
+ **Tier:** extended | **Sync:** no | **APIs used:** `RTCPeerConnection`
270
+
271
+ ```ts
272
+ // value shape:
273
+ {
274
+ codecs: string[]; // sorted audio/video codec names from SDP rtpmap lines
275
+ extmaps: string[]; // sorted extmap IDs from SDP
276
+ }
277
+ ```
278
+
279
+ Creates an RTCPeerConnection, opens a data channel, generates an SDP offer, and parses supported codecs and extmap entries. The peer connection is closed after collection.
280
+
281
+ Returns `'unsupported'` if `RTCPeerConnection` (or `webkitRTCPeerConnection`) is not available. Returns `'blocked'` if SDP negotiation throws.
282
+
283
+ ### `frameInfo` — Iframe context
284
+
285
+ **Tier:** extended | **Sync:** yes | **APIs used:** DOM, `window.top`
286
+
287
+ ```ts
288
+ // value shape:
289
+ {
290
+ iframesCount: number; // count of <iframe> elements in the document
291
+ isTopLevel: boolean; // whether window === window.top
292
+ iframeDomains: string[]; // hostnames extracted from iframe src URLs
293
+ }
294
+ ```
295
+
296
+ Returns `'unsupported'` if `document` is not available. Returns `'error'` if enumeration throws.
297
+
298
+ ### `networkInfo` — Network Information
299
+
300
+ **Tier:** extended | **Sync:** yes | **APIs used:** `navigator.connection` (vendor-prefixed)
301
+
302
+ ```ts
303
+ // value shape:
304
+ {
305
+ effectiveType: string | null; // 'slow-2g' | '2g' | '3g' | '4g' | null
306
+ downlink: number | null; // estimated downlink speed in Mbps
307
+ rtt: number | null; // estimated round-trip time in ms
308
+ saveData: boolean | null; // data-saver mode
309
+ }
310
+ ```
311
+
312
+ Checks `navigator.connection`, `navigator.mozConnection`, and `navigator.webkitConnection` for the NetworkInformation API (Chrome, Firefox, older WebKit).
313
+
314
+ Returns `'unsupported'` if `navigator` or the connection API is not available.
315
+
316
+ ### `paymentSupport` — PaymentRequest API
317
+
318
+ **Tier:** extended | **Sync:** yes | **APIs used:** `window.PaymentRequest`
319
+
320
+ ```ts
321
+ // value shape:
322
+ {
323
+ paymentRequest: boolean; // whether PaymentRequest constructor exists
324
+ }
325
+ ```
326
+
327
+ ### `referrerInfo` — Document referrer
328
+
329
+ **Tier:** extended | **Sync:** yes | **APIs used:** `document.referrer`
330
+
331
+ ```ts
332
+ // value shape:
333
+ {
334
+ referrer: string; // document.referrer (empty string if no referrer)
335
+ }
336
+ ```
337
+
338
+ Returns `'unsupported'` if `document` is not available.
339
+
340
+ ### `navigationInfo` — Navigation type
341
+
342
+ **Tier:** extended | **Sync:** yes | **APIs used:** `PerformanceNavigationTiming`
343
+
344
+ ```ts
345
+ // value shape:
346
+ {
347
+ navigationType: string | number | null;
348
+ // 'navigate' | 'reload' | 'back_forward' | 'prerender' (modern)
349
+ // 0 | 1 | 2 | 255 (legacy performance.navigation.type)
350
+ }
351
+ ```
352
+
353
+ Prefers `PerformanceNavigationTiming.type`. Falls back to deprecated `performance.navigation.type` in older browsers.
354
+
355
+ Returns `'unsupported'` if `performance` is not available. Returns `'error'` if probing throws.
356
+
357
+ ### `riskSignals` — Bot and headless detection
358
+
359
+ **Tier:** extended | **Sync:** no | **APIs used:** `navigator`, `Notification`, `Permissions`, `Performance`
360
+
361
+ ```ts
362
+ // value shape:
363
+ {
364
+ webdriver: boolean | null; // navigator.webdriver flag
365
+ headlessHints: string[]; // detected headless indicators
366
+ uaConsistency: boolean | null; // UA platform vs UA-CH consistency
367
+ notificationPermission: string | null; // Notification.permission
368
+ permissionsHeadlessHint: boolean | null; // denied+default mismatch
369
+ timeToCaptureMs: number | null; // ms since navigation start
370
+ }
371
+ ```
372
+
373
+ #### Headless hints checked
374
+
375
+ | Hint code | Trigger |
376
+ |-----------|---------|
377
+ | `headless-ua` | UA contains `"HeadlessChrome"` |
378
+ | `missing-chrome-object` | Chrome UA but `window.chrome` is not an object |
379
+ | `zero-outer-dimensions` | `outerWidth === 0` or `outerHeight === 0` |
380
+ | `no-plugins-on-desktop-chrome` | Chrome UA (non-mobile) but `navigator.plugins` is empty |
381
+ | `empty-languages` | `navigator.languages` is an empty array |
382
+
383
+ Returns `'unsupported'` if `navigator` is not available. Returns `'blocked'` if collection throws.
package/package.json CHANGED
@@ -1,75 +1,78 @@
1
1
  {
2
- "name": "@ianmenethil/zp-devicefp",
3
- "version": "0.1.0-alpha.1",
4
- "description": "Async-first browser and device fingerprinting library with explainable signals and anomaly scoring.",
5
- "type": "module",
6
- "main": "./dist/npm/index.cjs",
7
- "module": "./dist/npm/index.mjs",
8
- "types": "./dist/npm/index.d.ts",
9
- "sideEffects": false,
10
- "exports": {
11
- ".": {
12
- "types": "./dist/npm/index.d.ts",
13
- "import": "./dist/npm/index.mjs",
14
- "require": "./dist/npm/index.cjs",
15
- "default": "./dist/npm/index.mjs"
16
- }
17
- },
18
- "files": [
19
- "dist",
20
- "README.md",
21
- "LICENSE",
22
- "docs"
23
- ],
24
- "packageManager": "bun@1.3.9",
25
- "engines": {
26
- "bun": ">=1.3.0",
27
- "node": ">=22"
28
- },
29
- "prepublishOnly": "bun run build",
30
- "scripts": {
31
- "build": "bun run check && bun run test:coverage && bun -e \"await import('./.scripts/build.ts')\"",
32
- "test": "bun test",
33
- "test:coverage": "bun test --coverage",
34
- "release": "sh -c 'V=$(npm version \"${1:-minor}\" -m \"chore: release %s\" | tail -1 | sed s/v//) && bun publish --access public && mkdir -p \"F:/_ZP-Main/apps/CDN-server/public/devicefp/$V\" && cp -r dist/cdn/* \"F:/_ZP-Main/apps/CDN-server/public/devicefp/$V/\" && mkdir -p \"F:/_ZP-Main/apps/CDN-server/public/devicefp/latest\" && cp -r dist/cdn/* \"F:/_ZP-Main/apps/CDN-server/public/devicefp/latest/\" && git push --follow-tags' --",
35
- "smoke": "bun run scripts/smoke.mjs",
36
- "check": "bun run lint && bun run typecheck && bun run knip && bun run jscpd",
37
- "lint": "eslint . --config eslint.config.js",
38
- "typecheck": "tsc -p tsconfig.json --noEmit",
39
- "knip": "knip --config knip.json",
40
- "jscpd": "jscpd --config .jscpd.json",
41
- "format": "prettier --write .",
42
- "format:check": "prettier --check ."
43
- },
44
- "keywords": [
45
- "browser",
46
- "device",
47
- "fingerprint",
48
- "typescript",
49
- "fraud",
50
- "risk"
51
- ],
52
- "author": "Ian / Zenith Payments",
53
- "license": "MIT",
54
- "homepage": "https://github.com/ianmenethil/zp-devicefp#readme",
55
- "repository": {
56
- "type": "git",
57
- "url": "git+https://github.com/ianmenethil/zp-devicefp.git"
58
- },
59
- "bugs": {
60
- "url": "https://github.com/ianmenethil/zp-devicefp/issues"
61
- },
62
- "devDependencies": {
63
- "@eslint/js": "^10.0.1",
64
- "esbuild": "^0.28.0",
65
- "eslint": "^10.3.0",
66
- "eslint-plugin-tsdoc": "^0.5.2",
67
- "javascript-obfuscator": "^5.4.2",
68
- "jscpd": "^4.2.0",
69
- "knip": "^6.13.1",
70
- "lefthook": "^2.1.6",
71
- "prettier": "^3.8.3",
72
- "typescript": "~6.0.3",
73
- "typescript-eslint": "^8.59.3"
2
+ "name": "@ianmenethil/zp-devicefp",
3
+ "version": "0.1.0",
4
+ "description": "Async-first browser and device fingerprinting library with explainable signals and anomaly scoring.",
5
+ "type": "module",
6
+ "main": "./dist/npm/index.cjs",
7
+ "module": "./dist/npm/index.mjs",
8
+ "types": "./dist/npm/index.d.ts",
9
+ "sideEffects": false,
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/npm/index.d.ts",
13
+ "import": "./dist/npm/index.mjs",
14
+ "require": "./dist/npm/index.cjs",
15
+ "default": "./dist/npm/index.mjs"
74
16
  }
17
+ },
18
+ "files": [
19
+ "dist",
20
+ "README.md",
21
+ "LICENSE",
22
+ "docs"
23
+ ],
24
+ "packageManager": "bun@1.3.9",
25
+ "engines": {
26
+ "bun": ">=1.3.0",
27
+ "node": ">=22"
28
+ },
29
+ "prepublishOnly": "bun run build",
30
+ "scripts": {
31
+ "build": "bun run check && bun run test:coverage && bun -e \"await import('./.scripts/build.ts')\"",
32
+ "test": "bun test",
33
+ "test:coverage": "bun test --coverage",
34
+ "release": "sh -c 'V=$(npm version \"${1:-minor}\" -m \"chore: release %s\" | tail -1 | sed s/v//) && bun publish --access public && rm -rf \"F:/_ZP-Main/apps/CDN-server/public/devicefp\" && mkdir -p \"F:/_ZP-Main/apps/CDN-server/public/devicefp/$V\" && cp -r dist/cdn/* \"F:/_ZP-Main/apps/CDN-server/public/devicefp/$V/\" && mkdir -p \"F:/_ZP-Main/apps/CDN-server/public/devicefp/latest\" && cp -r dist/cdn/* \"F:/_ZP-Main/apps/CDN-server/public/devicefp/latest/\" && git push --follow-tags' --",
35
+ "release:minor": "bun run release minor",
36
+ "release:patch": "bun run release patch",
37
+ "release:major": "bun run release major",
38
+ "smoke": "bun run scripts/smoke.mjs",
39
+ "check": "bun run lint && bun run typecheck && bun run knip && bun run jscpd",
40
+ "lint": "eslint . --config eslint.config.js",
41
+ "typecheck": "tsc -p tsconfig.json --noEmit",
42
+ "knip": "knip --config knip.json",
43
+ "jscpd": "jscpd --config .jscpd.json",
44
+ "format": "prettier --write .",
45
+ "format:check": "prettier --check ."
46
+ },
47
+ "keywords": [
48
+ "browser",
49
+ "device",
50
+ "fingerprint",
51
+ "typescript",
52
+ "fraud",
53
+ "risk"
54
+ ],
55
+ "author": "Ian / Zenith Payments",
56
+ "license": "MIT",
57
+ "homepage": "https://github.com/ianmenethil/zp-devicefp#readme",
58
+ "repository": {
59
+ "type": "git",
60
+ "url": "git+https://github.com/ianmenethil/zp-devicefp.git"
61
+ },
62
+ "bugs": {
63
+ "url": "https://github.com/ianmenethil/zp-devicefp/issues"
64
+ },
65
+ "devDependencies": {
66
+ "@eslint/js": "^10.0.1",
67
+ "esbuild": "^0.28.0",
68
+ "eslint": "^10.3.0",
69
+ "eslint-plugin-tsdoc": "^0.5.2",
70
+ "javascript-obfuscator": "^5.4.2",
71
+ "jscpd": "^4.2.0",
72
+ "knip": "^6.13.1",
73
+ "lefthook": "^2.1.6",
74
+ "prettier": "^3.8.3",
75
+ "typescript": "~6.0.3",
76
+ "typescript-eslint": "^8.59.3"
77
+ }
75
78
  }