@ianmenethil/zp-devicefp 0.1.0-alpha.1 → 0.1.1
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 +235 -80
- package/docs/api-reference.md +472 -0
- package/docs/signals.md +383 -0
- package/package.json +75 -72
package/README.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# Browser Fingerprint Library
|
|
2
2
|
|
|
3
|
-
Async-first TypeScript browser and device fingerprinting library
|
|
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
|
-
##
|
|
21
|
+
## Quick start
|
|
16
22
|
|
|
17
|
-
```
|
|
18
|
-
@ianmenethil/zp-devicefp
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
36
|
+
---
|
|
36
37
|
|
|
37
|
-
|
|
38
|
+
## API overview
|
|
38
39
|
|
|
39
|
-
|
|
|
40
|
-
|
|
41
|
-
|
|
|
42
|
-
|
|
|
43
|
-
|
|
|
44
|
-
|
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
client.on('warning', (e) => {
|
|
56
|
+
// { signal?: 'webrtc', message: 'RTCPeerConnection not available' }
|
|
57
|
+
console.warn(e.message);
|
|
58
|
+
});
|
|
55
59
|
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
client.on('complete', (e) => {
|
|
61
|
+
// { result: FingerprintResult }
|
|
62
|
+
console.log('Done', e.result.thumbprint);
|
|
63
|
+
});
|
|
64
|
+
```
|
|
58
65
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
76
|
+
### Aborting
|
|
66
77
|
|
|
67
78
|
```ts
|
|
68
|
-
|
|
79
|
+
const controller = new AbortController();
|
|
69
80
|
|
|
70
|
-
const
|
|
71
|
-
timeoutMs: 1500,
|
|
72
|
-
extended: false,
|
|
73
|
-
});
|
|
81
|
+
const resultPromise = client.collect({ abortSignal: controller.signal });
|
|
74
82
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
90
|
+
---
|
|
81
91
|
|
|
82
|
-
|
|
92
|
+
## Options
|
|
83
93
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
105
|
+
---
|
|
90
106
|
|
|
91
|
-
|
|
107
|
+
## Result shape
|
|
92
108
|
|
|
93
|
-
|
|
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
|
-
|
|
123
|
+
### `AntiSpoofReport`
|
|
96
124
|
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
261
|
+
---
|
|
117
262
|
|
|
118
|
-
##
|
|
263
|
+
## Docs
|
|
119
264
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
-
|
|
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
|
-
|
|
273
|
+
---
|
|
125
274
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
+
```
|
package/docs/signals.md
ADDED
|
@@ -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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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.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"
|
|
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 'npm version \"${1:-minor}\" -m \"chore: release %s\" && bun publish --access public && rm -rf \"F:/_ZP-Main/apps/CDN-server/public/devicefp\" && mkdir -p \"F:/_ZP-Main/apps/CDN-server/public/devicefp\" && cp -r dist/cdn/* \"F:/_ZP-Main/apps/CDN-server/public/devicefp/\" && 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
|
}
|