@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.
package/docs/signals.md CHANGED
@@ -1,383 +1,425 @@
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.
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
+ | 20 | `adblock` | extended | no | Ad blocker detection via DOM bait element |
31
+ | 21 | `geolocation` | extended | no | Geographic coordinates via Geolocation API |
32
+
33
+ ---
34
+
35
+ ## Core signals
36
+
37
+ ### `ua` User-Agent string
38
+
39
+ **Tier:** core | **Sync:** yes | **APIs used:** `navigator`
40
+
41
+ ```ts
42
+ // value shape:
43
+ {
44
+ userAgent: string; // navigator.userAgent
45
+ appVersion: string; // navigator.appVersion
46
+ vendor: string; // navigator.vendor
47
+ platform: string; // navigator.platform
48
+ webdriver: boolean; // navigator.webdriver
49
+ maxTouchPoints: number; // navigator.maxTouchPoints
50
+ cookieEnabled: boolean; // navigator.cookieEnabled
51
+ vendorSub: string; // navigator.vendorSub
52
+ doNotTrack: string | null; // navigator.doNotTrack ("1", "0", "unspecified", or null)
53
+ plugins: string[]; // navigator.plugins mapped to "name::description::filename"
54
+ }
55
+ ```
56
+
57
+ Returns `'unsupported'` if `navigator` is not available.
58
+
59
+ ### `uaHints` — User-Agent Client Hints
60
+
61
+ **Tier:** core | **Sync:** no | **APIs used:** `navigator.userAgentData`
62
+
63
+ ```ts
64
+ // value shape:
65
+ {
66
+ brands: { brand: string; version: string }[]; // low-entropy brands
67
+ mobile: boolean; // userAgentData.mobile
68
+ platform: string; // userAgentData.platform
69
+ architecture?: string; // high-entropy: CPU architecture
70
+ bitness?: string; // high-entropy: 32/64
71
+ formFactors?: string[]; // high-entropy: device form factors
72
+ fullVersionList?: { brand: string; version: string }[]; // high-entropy: full versions
73
+ model?: string; // high-entropy: device model
74
+ platformVersion?: string; // high-entropy: OS version
75
+ wow64?: boolean; // high-entropy: 32-bit on 64-bit
76
+ }
77
+ ```
78
+
79
+ Returns `'unsupported'` if `navigator.userAgentData` is not available. Returns `'blocked'` if `getHighEntropyValues` throws.
80
+
81
+ ### `locale` — Language and internationalization
82
+
83
+ **Tier:** core | **Sync:** yes | **APIs used:** `navigator`, `Intl`
84
+
85
+ ```ts
86
+ // value shape:
87
+ {
88
+ language: string; // navigator.language
89
+ languages: readonly string[]; // navigator.languages
90
+ locale: string; // Intl.DateTimeFormat resolved locale
91
+ calendar: string; // e.g. "gregory", "iso8601"
92
+ numberingSystem: string; // e.g. "latn", "arab"
93
+ timeZone: string; // IANA timezone e.g. "America/New_York"
94
+ hourCycle: string; // e.g. "h12", "h23"
95
+ timeZoneOffsetMinutes: number; // UTC offset in minutes
96
+ formattedNumber: string; // Intl.NumberFormat sample: "123,456.789"
97
+ formattedDate: string; // Intl.DateTimeFormat sample for Jan 2, 2024
98
+ formattedRelativeDay: string | undefined; // Intl.RelativeTimeFormat: "yesterday" etc.
99
+ }
100
+ ```
101
+
102
+ Returns `'unsupported'` if `navigator` or `Intl` is not available.
103
+
104
+ ### `screen` — Screen and display
105
+
106
+ **Tier:** core | **Sync:** yes | **APIs used:** `screen`, `navigator`, `matchMedia`, `devicePixelRatio`
107
+
108
+ ```ts
109
+ // value shape:
110
+ {
111
+ width: number; // screen.width
112
+ height: number; // screen.height
113
+ availWidth: number; // screen.availWidth
114
+ availHeight: number; // screen.availHeight
115
+ colorDepth: number; // screen.colorDepth
116
+ pixelDepth: number; // screen.pixelDepth
117
+ orientationType: string; // screen.orientation.type
118
+ orientationAngle: number; // screen.orientation.angle
119
+ maxTouchPoints: number; // navigator.maxTouchPoints (0 if missing)
120
+ devicePixelRatio: number | undefined; // window.devicePixelRatio
121
+ fakedResolution: boolean; // true if availWidth/Height exceeds screen.width/height (FP2 has_lied_resolution)
122
+ ccaScreenSize: string; // Cardinal Commerce outerWidth/outerHeight bucket ('01'–'04')
123
+ colorGamutP3: boolean | 'unsupported'; // (color-gamut: p3) media query
124
+ prefersReducedMotion: boolean | 'unsupported'; // (prefers-reduced-motion: reduce)
125
+ prefersContrastMore: boolean | 'unsupported'; // (prefers-contrast: more)
126
+ forcedColorsActive: boolean | 'unsupported'; // (forced-colors: active)
127
+ }
128
+ ```
129
+
130
+ Returns `'unsupported'` if `screen` is not available.
131
+
132
+ ### `hardware` — Hardware properties
133
+
134
+ **Tier:** core | **Sync:** yes | **APIs used:** `navigator`
135
+
136
+ ```ts
137
+ // value shape:
138
+ {
139
+ hardwareConcurrency: number; // navigator.hardwareConcurrency (logical CPU cores)
140
+ deviceMemory: number | undefined; // navigator.deviceMemory (GB, may be undefined)
141
+ platform: string; // navigator.platform
142
+ maxTouchPoints: number; // navigator.maxTouchPoints
143
+ touchEventCreationSuccessful: boolean; // document.createEvent('TouchEvent') succeeded
144
+ onTouchStartAvailable: boolean; // 'ontouchstart' in window
145
+ }
146
+ ```
147
+
148
+ Returns `'unsupported'` if `navigator` is not available.
149
+
150
+ ### `storage` Storage API availability
151
+
152
+ **Tier:** core | **Sync:** yes | **APIs used:** `localStorage`, `sessionStorage`, `indexedDB`, `openDatabase`, `navigator`
153
+
154
+ ```ts
155
+ // value shape:
156
+ {
157
+ cookiesEnabled: boolean; // navigator.cookieEnabled
158
+ localStorage: 'available' | 'blocked' | 'unsupported'; // write-read-delete probe
159
+ sessionStorage: 'available' | 'blocked' | 'unsupported'; // write-read-delete probe
160
+ indexedDb: 'available' | 'unsupported'; // typeof indexedDB check
161
+ openDatabase: 'available' | 'unsupported'; // typeof openDatabase check (Web SQL)
162
+ pdfViewerEnabled: boolean; // navigator.pdfViewerEnabled
163
+ }
164
+ ```
165
+
166
+ ### `fonts` Font detection
167
+
168
+ **Tier:** core | **Sync:** yes | **APIs used:** DOM (`document.createElement`, `offsetWidth`)
169
+
170
+ ```ts
171
+ // value shape:
172
+ {
173
+ detectedFonts: string[]; // sorted list of detected font family names
174
+ fontCount: number; // count of detected fonts
175
+ }
176
+ ```
177
+
178
+ Tests 70 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.
179
+
180
+ **Font set:** matches the Fingerprint2 / Cardinal Commerce canonical font list (Andale Mono, Arial, Baskerville, Calibri, Comic Sans MS, Consolas, Courier New, Franklin Gothic Medium, Garamond, Georgia, Helvetica, Impact, Lucida Console, Meiryo, Monaco, MS Gothic, Palatino, Segoe UI, Symbol, Tahoma, Times New Roman, Trebuchet MS, Verdana, Wingdings, and 46 more).
181
+
182
+ ### `canvas` Canvas 2D fingerprint
183
+
184
+ **Tier:** core | **Sync:** yes | **APIs used:** Canvas 2D (`getContext('2d')`)
185
+
186
+ ```ts
187
+ // value shape:
188
+ {
189
+ dataUrl: string; // canvas.toDataURL() — base64 PNG of rendered pattern
190
+ winding: boolean; // ctx.isPointInPath(1, 1, 'evenodd')
191
+ }
192
+ ```
193
+
194
+ 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.
195
+
196
+ Returns `'unsupported'` if the canvas 2D context is unavailable. Returns `'error'` if rendering throws.
197
+
198
+ ### `webgl` WebGL fingerprint
199
+
200
+ **Tier:** core | **Sync:** yes | **APIs used:** WebGL (`getContext('webgl')`, `WEBGL_debug_renderer_info`)
201
+
202
+ ```ts
203
+ // value shape:
204
+ {
205
+ vendor: string; // UNMASKED_VENDOR_WEBGL or VENDOR
206
+ renderer: string; // UNMASKED_RENDERER_WEBGL or RENDERER
207
+ version: string; // WebGL version string
208
+ shadingLanguageVersion: string; // GLSL version string
209
+ maxTextureSize: number; // MAX_TEXTURE_SIZE
210
+ maxViewportDims: Int32Array; // MAX_VIEWPORT_DIMS
211
+ extensions: string[]; // getSupportedExtensions() sorted
212
+ }
213
+ ```
214
+
215
+ Attempts `webgl` context first, falls back to `experimental-webgl`. Uses `WEBGL_debug_renderer_info` for unmasked GPU strings when available.
216
+
217
+ Returns `'unsupported'` if canvas/WebGL is not available.
218
+
219
+ ### `audio` AudioContext fingerprint
220
+
221
+ **Tier:** core | **Sync:** no | **APIs used:** `OfflineAudioContext`, `OscillatorNode`, `DynamicsCompressorNode`
222
+
223
+ ```ts
224
+ // value shape:
225
+ {
226
+ sampleRate: number; // rendered.sampleRate
227
+ length: number; // rendered.length (number of sample frames)
228
+ signalValue: number; // sum of absolute channel data values (6 decimal places)
229
+ }
230
+ ```
231
+
232
+ 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.
233
+
234
+ Returns `'unsupported'` if `OfflineAudioContext` (or `webkitOfflineAudioContext`) is not available. Returns `'blocked'` if rendering throws.
235
+
236
+ ---
237
+
238
+ ## Extended signals
239
+
240
+ ### `mediaDevices` Media device enumeration
241
+
242
+ **Tier:** extended | **Sync:** no | **APIs used:** `navigator.mediaDevices.enumerateDevices`
243
+
244
+ ```ts
245
+ // value shapean object keyed by MediaDeviceKind:
246
+ {
247
+ audioinput: number; // count of audio input devices
248
+ audiooutput: number; // count of audio output devices
249
+ videoinput: number; // count of video input devices
250
+ }
251
+ ```
252
+
253
+ Only device counts and kinds are collected — no device labels or IDs. Requires a secure context (HTTPS) and a visible document.
254
+
255
+ Returns `'blocked'` if not in a secure context, if document is not visible, or if `enumerateDevices` throws. Returns `'unsupported'` if `mediaDevices` is not available.
256
+
257
+ ### `permissions` Permissions API state
258
+
259
+ **Tier:** extended | **Sync:** no | **APIs used:** `navigator.permissions.query`
260
+
261
+ ```ts
262
+ // value shape:
263
+ {
264
+ geolocation: 'granted' | 'denied' | 'prompt' | 'unsupported';
265
+ notifications: 'granted' | 'denied' | 'prompt' | 'unsupported';
266
+ camera: 'granted' | 'denied' | 'prompt' | 'unsupported';
267
+ microphone: 'granted' | 'denied' | 'prompt' | 'unsupported';
268
+ }
269
+ ```
270
+
271
+ Queries 4 permission names in parallel. Each individual query that fails returns `'unsupported'` for that key.
272
+
273
+ Returns `'unsupported'` if `navigator.permissions` is not available. Returns `'blocked'` if all queries fail together.
274
+
275
+ ### `webrtc` WebRTC codec capability
276
+
277
+ **Tier:** extended | **Sync:** no | **APIs used:** `RTCPeerConnection`
278
+
279
+ ```ts
280
+ // value shape:
281
+ {
282
+ codecs: string[]; // sorted audio/video codec names from SDP rtpmap lines
283
+ extmaps: string[]; // sorted extmap IDs from SDP
284
+ }
285
+ ```
286
+
287
+ 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.
288
+
289
+ Returns `'unsupported'` if `RTCPeerConnection` (or `webkitRTCPeerConnection`) is not available. Returns `'blocked'` if SDP negotiation throws.
290
+
291
+ ### `frameInfo` Iframe context
292
+
293
+ **Tier:** extended | **Sync:** yes | **APIs used:** DOM, `window.top`
294
+
295
+ ```ts
296
+ // value shape:
297
+ {
298
+ iframesCount: number; // count of <iframe> elements in the document
299
+ isTopLevel: boolean; // whether window === window.top
300
+ iframeDomains: string[]; // hostnames extracted from iframe src URLs
301
+ }
302
+ ```
303
+
304
+ Returns `'unsupported'` if `document` is not available. Returns `'error'` if enumeration throws.
305
+
306
+ ### `networkInfo` Network Information
307
+
308
+ **Tier:** extended | **Sync:** yes | **APIs used:** `navigator.connection` (vendor-prefixed)
309
+
310
+ ```ts
311
+ // value shape:
312
+ {
313
+ effectiveType: string | null; // 'slow-2g' | '2g' | '3g' | '4g' | null
314
+ downlink: number | null; // estimated downlink speed in Mbps
315
+ rtt: number | null; // estimated round-trip time in ms
316
+ saveData: boolean | null; // data-saver mode
317
+ }
318
+ ```
319
+
320
+ Checks `navigator.connection`, `navigator.mozConnection`, and `navigator.webkitConnection` for the NetworkInformation API (Chrome, Firefox, older WebKit).
321
+
322
+ Returns `'unsupported'` if `navigator` or the connection API is not available.
323
+
324
+ ### `paymentSupport` — PaymentRequest API
325
+
326
+ **Tier:** extended | **Sync:** yes | **APIs used:** `window.PaymentRequest`
327
+
328
+ ```ts
329
+ // value shape:
330
+ {
331
+ paymentRequest: boolean; // whether PaymentRequest constructor exists
332
+ }
333
+ ```
334
+
335
+ ### `referrerInfo` — Document referrer
336
+
337
+ **Tier:** extended | **Sync:** yes | **APIs used:** `document.referrer`
338
+
339
+ ```ts
340
+ // value shape:
341
+ {
342
+ referrer: string; // document.referrer (empty string if no referrer)
343
+ }
344
+ ```
345
+
346
+ Returns `'unsupported'` if `document` is not available.
347
+
348
+ ### `navigationInfo` Navigation type
349
+
350
+ **Tier:** extended | **Sync:** yes | **APIs used:** `PerformanceNavigationTiming`
351
+
352
+ ```ts
353
+ // value shape:
354
+ {
355
+ navigationType: string | number | null;
356
+ // 'navigate' | 'reload' | 'back_forward' | 'prerender' (modern)
357
+ // 0 | 1 | 2 | 255 (legacy performance.navigation.type)
358
+ }
359
+ ```
360
+
361
+ Prefers `PerformanceNavigationTiming.type`. Falls back to deprecated `performance.navigation.type` in older browsers.
362
+
363
+ Returns `'unsupported'` if `performance` is not available. Returns `'error'` if probing throws.
364
+
365
+ ### `riskSignals` Bot and headless detection
366
+
367
+ **Tier:** extended | **Sync:** no | **APIs used:** `navigator`, `Notification`, `Permissions`, `Performance`
368
+
369
+ ```ts
370
+ // value shape:
371
+ {
372
+ webdriver: boolean | null; // navigator.webdriver flag
373
+ headlessHints: string[]; // detected headless indicators
374
+ uaConsistency: boolean | null; // UA platform vs UA-CH consistency
375
+ notificationPermission: string | null; // Notification.permission
376
+ permissionsHeadlessHint: boolean | null; // denied+default mismatch
377
+ timeToCaptureMs: number | null; // ms since navigation start
378
+ }
379
+ ```
380
+
381
+ #### Headless hints checked
382
+
383
+ | Hint code | Trigger |
384
+ |-----------|---------|
385
+ | `headless-ua` | UA contains `"HeadlessChrome"` |
386
+ | `missing-chrome-object` | Chrome UA but `window.chrome` is not an object |
387
+ | `zero-outer-dimensions` | `outerWidth === 0` or `outerHeight === 0` |
388
+ | `no-plugins-on-desktop-chrome` | Chrome UA (non-mobile) but `navigator.plugins` is empty |
389
+ | `empty-languages` | `navigator.languages` is an empty array |
390
+
391
+ Returns `'unsupported'` if `navigator` is not available. Returns `'blocked'` if collection throws.
392
+
393
+ ### `adblock` — Ad blocker detection
394
+
395
+ **Tier:** extended | **Sync:** no | **APIs used:** DOM (`document.createElement`, `offsetHeight`)
396
+
397
+ ```ts
398
+ // value shape:
399
+ {
400
+ adblockEnabled: boolean; // true if a bait element was hidden/collapsed by an ad blocker
401
+ }
402
+ ```
403
+
404
+ Appends a `div` with ad-blocker-targeted class names (e.g. `adsbox`, `pub_300x250`) to the document body, waits 100 ms, then checks whether the element's dimensions collapsed to zero. The element is removed immediately after measurement.
405
+
406
+ Returns `'unsupported'` if `document` is not available or the DOM is not accessible.
407
+
408
+ ### `geolocation` — Geographic coordinates
409
+
410
+ **Tier:** extended | **Sync:** no | **APIs used:** `navigator.geolocation`
411
+
412
+ ```ts
413
+ // value shape:
414
+ {
415
+ latitude: number; // decimal degrees
416
+ longitude: number; // decimal degrees
417
+ accuracy: number; // radius in metres
418
+ }
419
+ ```
420
+
421
+ Calls `navigator.geolocation.getCurrentPosition()` with `timeout: timeoutMs` and `maximumAge: 60000`. The browser will prompt the user for permission on first use.
422
+
423
+ Returns `'unsupported'` if `navigator.geolocation` is absent (HTTP context or non-browser runtime).
424
+ Returns `'blocked'` when the user denies the permission prompt (`PERMISSION_DENIED`, error code 1).
425
+ Returns `'error'` for other geolocation failures (position unavailable, timeout from the Geolocation API itself).