get-browser-fingerprint 5.1.0 → 5.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/README.md CHANGED
@@ -1,61 +1,130 @@
1
1
  # get-browser-fingerprint
2
2
 
3
- A single and fast (<50ms) asynchronous function returning a browser fingerprint, without requiring any permission to the user.
4
- Live [example here](https://damianobarbati.github.io/get-browser-fingerprint/).
3
+ A single asynchronous function that returns a browser fingerprint (8‑char hex) without requiring any user permission.
4
+
5
+ Live [example here](https://damianobarbati.github.io/get-browser-fingerprint/).
6
+
7
+ ## Return value
8
+
9
+ - **`fingerprint`** — MurmurHash3 (32-bit, hex) of the stable stringified payload.
10
+ - **`elapsedMs`** — Execution time in milliseconds.
11
+ - All collected signals are also spread on the returned object (e.g. `timezone`, `canvasID`, `webglID`, `languages`, …).
12
+
13
+ ## Collected signals
14
+
15
+ - **UA / platform:** `vendor`, `platform`, `userAgentHighEntropy` (UA Client Hints: architecture, bitness, platformVersion, model, fullVersionList).
16
+ - **Locale / Intl:** `languages`, `timezone`, `timezoneOffset`, `numberingSystem`, `intlCalendar`, `hourCycle`, `intlSupportedCalendars`.
17
+ - **Preferences:** `reducedMotion`, `colorScheme`, `prefersContrast`, `prefersReducedTransparency`, `forcedColors`.
18
+ - **Screen:** `devicePixelRatio`, `pixelDepth`, `colorDepth`, `screenOrientation`; on mobile: `width`, `height`, `aspectRatio`, `maxTouchPoints`.
19
+ - **Network:** `connectionType`, `effectiveType`, `saveData` (when available).
20
+ - **Device:** `hardwareConcurrency`, `deviceMemory`, `touchSupport`.
21
+ - **Media / codecs:** `mediaCodecs` (canPlayType for common MIME types), `canvasID`, `webglID`, `webgpuID`, `audioID`.
22
+ - **Other:** `fonts` (detected list), `pdfViewerEnabled`, `webdriver`, `permissionsState` (geolocation, notifications).
23
+
24
+ Signals that fail or are unavailable are set to `null` (no throw).
5
25
 
6
26
  ## Assumptions
7
27
 
8
- The function is targeting stock installations of the following browsers (no extensions or add-ons installed):
28
+ The function targets stock installations of:
29
+
9
30
  - Google Chrome (desktop and Android)
10
- - Mozilla Firefox (desktop, default tracking protection, no custom privacy flags enabled)
31
+ - Mozilla Firefox (desktop, default tracking protection)
11
32
  - Microsoft Edge (desktop and Android)
12
33
  - Apple Safari (macOS)
13
34
  - Safari on iOS / iPadOS
14
35
 
15
- Explicit assumptions:
16
- - No privacy-focused extensions or add-ons installed.
17
- - Browser privacy settings left at factory defaults (no manual changes to fingerprint resistance features).
18
- - No hardened, anti-detect or heavily modified browser variants.
36
+ Assumptions:
19
37
 
20
- Under these conditions the collected signals retain meaningful entropy in February 2026:
21
- - Canvas rendering (stable on Chrome, Edge, Firefox default; noisy or low-entropy on iOS Safari and partially on macOS Safari)
22
- - WebGL unmasked vendor and renderer (strong on desktop, more uniform on mobile)
23
- - Offline audio context (useful on Chrome, Edge, Firefox; heavily restricted on Safari)
24
- - WebGPU properties (if available; low but increasing entropy)
25
- - Locally installed fonts (valuable mainly on desktop Windows and macOS)
26
- - Passive signals (hardwareConcurrency, deviceMemory, screen, timezone, languages, etc.)
38
+ - No privacy-focused extensions or add-ons.
39
+ - Browser privacy settings at factory defaults.
40
+ - No hardened or anti-detect browser variants.
27
41
 
28
- On browsers with strong default fingerprint resistance (Safari iOS, certain Firefox modes) entropy and stability decrease significantly.
29
- The script does not attempt to bypass intentional randomization or hardening.
42
+ Under these conditions the signals keep useful entropy; on Safari iOS or strict Firefox modes entropy can be lower. The script does not try to bypass intentional randomization or hardening.
30
43
 
31
44
  ## Usage
32
45
 
33
- Get browser fingerprint:
34
46
  ```js
35
- import getBrowserFingerprint from 'get-browser-fingerprint';
36
- const fingerprint = await getBrowserFingerprint();
37
- console.log(fingerprint);
47
+ // Import the default async function
48
+ import getBrowserFingerprint from 'get-browser-fingerprint'
49
+
50
+ // Call once; result holds fingerprint, elapsedMs, and all collected signals
51
+ const result = await getBrowserFingerprint()
52
+ // Log the 8-char hex fingerprint
53
+ console.log(result.fingerprint)
54
+ // Log execution time in milliseconds
55
+ console.log(result.elapsedMs)
56
+
57
+ // Or destructure to get only the fingerprint
58
+ const { fingerprint } = await getBrowserFingerprint()
59
+ console.log(fingerprint)
38
60
  ```
39
61
 
40
- Options available:
41
- - `hardwareOnly` (default `true`): use only hardware info about device.
42
- - `debug` (default `false`): log data used to generate fingerprint to console and add canvas/webgl/audio elements to body.
62
+ Options:
63
+
64
+ - **`debug`** (default `false`): append canvas / WebGL / audio debug elements to `document.body` and include extra debug output.
65
+
66
+ ```js
67
+ const result = await getBrowserFingerprint({ debug: true })
68
+ ```
69
+
70
+ ### CDN
71
+
72
+ Load as ESM from a CDN:
73
+
74
+ ```html
75
+ <script type="module">
76
+ import getBrowserFingerprint from 'https://cdn.jsdelivr.net/npm/get-browser-fingerprint/+esm'
77
+ const { fingerprint, elapsedMs } = await getBrowserFingerprint()
78
+ console.log(fingerprint, elapsedMs)
79
+ </script>
80
+ ```
81
+
82
+ Or with a version pin:
83
+
84
+ ```html
85
+ <script type="module">
86
+ import getBrowserFingerprint from 'https://cdn.jsdelivr.net/npm/get-browser-fingerprint@latest/+esm'
87
+ const result = await getBrowserFingerprint()
88
+ console.log(result.fingerprint)
89
+ </script>
90
+ ```
43
91
 
44
92
  ## Development
45
93
 
46
- To test locally:
94
+ Install and run tests:
95
+
47
96
  ```sh
48
- fnm install # nodejs from .nvmrc
97
+ fnm install
49
98
  npm install -g corepack
50
- corepack enable # package manager from package.json
51
- corepack install # package manager from package.json
52
- pnpm install # install deps
99
+ corepack enable
100
+ corepack install
101
+ pnpm install
53
102
  pnpm exec playwright install chromium
54
103
  pnpm exec playwright install firefox
55
104
  pnpm test
56
105
  ```
57
106
 
58
- To run the example locally:
107
+ Run the demo locally (serve `src/`):
108
+
59
109
  ```sh
60
110
  pnpm serve -p 80 ./src
111
+ # or
112
+ npm run serve
61
113
  ```
114
+
115
+ Then open `http://localhost/index.html` (or port 80 if using `-p 80`).
116
+
117
+ ## References
118
+
119
+ - [Fingerprinting (MDN Glossary)](https://developer.mozilla.org/en-US/docs/Glossary/Fingerprinting)
120
+ - [HTTP Client hints (MDN)](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Client_hints)
121
+ - [Intl.DateTimeFormat.resolvedOptions() (MDN)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat/resolvedOptions)
122
+ - [Intl.supportedValuesOf() (MDN)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/supportedValuesOf)
123
+ - [Mitigating Browser Fingerprinting (W3C)](https://w3c.github.io/fingerprinting-guidance/)
124
+ - [Navigator (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/Navigator)
125
+ - [NavigatorUAData.getHighEntropyValues() (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/NavigatorUAData/getHighEntropyValues)
126
+ - [NetworkInformation (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/NetworkInformation)
127
+ - [Permissions API (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API)
128
+ - [Screen (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/Screen)
129
+ - [User-Agent Client Hints API (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/User-Agent_Client_Hints_API)
130
+ - [Window.devicePixelRatio (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "get-browser-fingerprint",
3
- "version": "5.1.0",
3
+ "version": "5.2.0",
4
4
  "author": "Damiano Barbati <damiano.barbati@gmail.com> (https://github.com/damianobarbati)",
5
5
  "repository": "https://github.com/damianobarbati/get-browser-fingerprint",
6
6
  "license": "MIT",
@@ -24,8 +24,8 @@
24
24
  "test": "node --test --test-reporter=spec --test-reporter-destination=stdout"
25
25
  },
26
26
  "devDependencies": {
27
- "@biomejs/biome": "^2.4.0",
27
+ "@biomejs/biome": "^2.4.8",
28
28
  "@playwright/test": "^1.58.2",
29
- "serve": "^14.2.5"
29
+ "serve": "^14.2.6"
30
30
  }
31
31
  }
package/src/index.d.ts CHANGED
@@ -4,7 +4,8 @@ export interface FingerprintOptions {
4
4
 
5
5
  export type FingerprintResult = {
6
6
  fingerprint: string;
7
- } & Record<string, string>;
7
+ elapsedMs: number;
8
+ } & Record<string, unknown>;
8
9
 
9
10
  export default function getBrowserFingerprint(options?: FingerprintOptions): Promise<FingerprintResult>;
10
11
 
package/src/index.js CHANGED
@@ -1,297 +1,104 @@
1
- const murmurHash3 = (key, seed = 0) => {
2
- let h = seed ^ key.length;
3
-
4
- let i = 0;
5
- const len = key.length;
6
-
7
- while (i + 3 < len) {
8
- let k = (key.charCodeAt(i) & 0xff) | ((key.charCodeAt(i + 1) & 0xff) << 8) | ((key.charCodeAt(i + 2) & 0xff) << 16) | ((key.charCodeAt(i + 3) & 0xff) << 24);
9
- k = Math.imul(k, 0xcc9e2d51);
10
- k = (k << 15) | (k >>> 17); // ROTL32(k, 15)
11
- k = Math.imul(k, 0x1b873593);
12
- h ^= k;
13
- h = (h << 13) | (h >>> 19); // ROTL32(h, 13)
14
- h = Math.imul(h, 5) + 0xe6546b64;
15
- i += 4;
16
- }
17
-
18
- // tail
19
- let k1 = 0;
20
- switch (len - i) {
21
- // biome-ignore lint/suspicious/noFallthroughSwitchClause: ignore
22
- case 3:
23
- k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16;
24
- // biome-ignore lint/suspicious/noFallthroughSwitchClause: ignore
25
- case 2:
26
- k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8;
27
- case 1:
28
- k1 ^= key.charCodeAt(i) & 0xff;
29
- k1 = Math.imul(k1, 0xcc9e2d51);
30
- k1 = (k1 << 15) | (k1 >>> 17);
31
- k1 = Math.imul(k1, 0x1b873593);
32
- h ^= k1;
33
- }
34
-
35
- h ^= h >>> 16;
36
- h = Math.imul(h, 0x85ebca6b);
37
- h ^= h >>> 13;
38
- h = Math.imul(h, 0xc2b2ae35);
39
- h ^= h >>> 16;
40
- h = h >>> 0; // force unsigned 32 bit
41
-
42
- const result = h.toString(16).padStart(8, '0');
43
- return result;
44
- };
45
-
46
- const safe = async (fn) => {
47
- try {
48
- return await fn();
49
- } catch {
50
- return null;
51
- }
52
- };
53
-
54
- const stableStringify = (obj) => {
55
- // handle null, string, number, boolean
56
- if (obj === null || typeof obj !== 'object') return JSON.stringify(obj);
57
- // handle arrays recursively
58
- if (Array.isArray(obj)) return `[${obj.map(stableStringify).join(',')}]`;
59
-
60
- const keys = Object.keys(obj).sort();
61
- const parts = [];
62
- for (const key of keys) {
63
- const value = obj[key];
64
- if (value !== undefined) parts.push(`${stableStringify(key)}:${stableStringify(value)}`);
65
- }
66
-
67
- return `{${parts.join(',')}}`;
68
- };
69
-
70
- const isMobile = () =>
71
- navigator.userAgentData?.mobile === true ||
72
- /Mobi|Android|iPhone|iPad|iPod|webOS|BlackBerry|Windows Phone/i.test(navigator.userAgent) ||
73
- (navigator.maxTouchPoints > 0 && matchMedia('(pointer:coarse)').matches && innerWidth <= 1024);
74
-
75
- /** @type {import('./index.d.ts').getBrowserFingerprint} */
76
- const getBrowserFingerprint = async ({ debug = false } = {}) => {
77
- // software
78
- const fonts = await safe(() => getFonts());
79
- const numberingSystem = await safe(() => new Intl.NumberFormat(window.navigator.languages?.[0] || 'en').resolvedOptions().numberingSystem);
80
- const languages = window.navigator.languages ? Array.from(window.navigator.languages).join(',') : window.navigator.language || null;
81
- const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
82
- const timezoneOffset = new Date().getTimezoneOffset();
83
- const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'reduce' : 'no-preference';
84
- const colorScheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
85
-
86
- // hardware
87
- const forcedColors = window.matchMedia('(forced-colors: active)').matches ? 'active' : null;
88
- const { pixelDepth, colorDepth } = window.screen;
89
- const touchSupport = 'ontouchstart' in window || window.navigator.maxTouchPoints > 0;
90
- const canvasID = await safe(() => getCanvasID(debug));
91
- const webglID = await safe(() => getWebglID());
92
- const webgpuID = await safe(() => getWebgpuID(debug));
93
- const audioID = await safe(() => getAudioID(debug));
94
- const aspectRatio = window.screen.width && window.screen.height ? (window.screen.width / window.screen.height).toFixed(4) : null;
95
-
96
- const data = {
97
- // SOFTWARE
98
- vendor: window.navigator.vendor || null, // vendor
99
- platform: window.navigator.platform || null, // os
100
- fonts, // stable default os setting
101
- numberingSystem, // stable default os setting
102
- languages, // stable user locale/setting
103
- timezone, // stable user locale/setting
104
- timezoneOffset, // stable user locale/setting
105
- reducedMotion, // stable user locale/setting
106
- colorScheme, // stable user locale/setting
107
- // HARDWARE
108
- hardwareConcurrency: window.navigator.hardwareConcurrency || null, // cpu
109
- deviceMemory: window.navigator.deviceMemory || null, // ram
110
- forcedColors, // display
111
- pixelDepth, // display
112
- colorDepth, // display
113
- touchSupport, // display
114
- canvasID, // gpu
115
- webglID, // gpu
116
- webgpuID, // gpu
117
- audioID, // audio
118
- };
119
-
120
- const mobileData = {
121
- aspectRatio,
122
- width: window.screen.width,
123
- height: window.screen.height,
124
- maxTouchPoints: window.navigator.maxTouchPoints,
125
- };
126
-
127
- if (isMobile()) Object.assign(data, mobileData);
128
-
129
- const payload = stableStringify(data);
130
- const fingerprint = murmurHash3(payload);
131
- return { fingerprint, ...data };
132
- };
133
-
134
- const getCanvasID = async (debug) => {
135
- const canvas = document.createElement('canvas');
136
- canvas.width = 280;
137
- canvas.height = 100;
138
- const ctx = canvas.getContext('2d');
139
-
140
- // background with gradient
141
- const grad = ctx.createLinearGradient(0, 0, 280, 100);
142
- grad.addColorStop(0, '#f60');
143
- grad.addColorStop(0.4, '#09f');
144
- grad.addColorStop(0.7, '#f09');
145
- grad.addColorStop(1, '#0f9');
146
- ctx.fillStyle = grad;
147
- ctx.fillRect(0, 0, 280, 100);
148
-
149
- // text with multiple fonts and emoji
150
- ctx.font = '18px "Arial","Helvetica","DejaVu Sans",sans-serif';
151
- ctx.fillStyle = '#000';
152
- ctx.textBaseline = 'alphabetic';
153
- ctx.fillText('Fingerprint ¼½¾™©®∆π∑€¶§', 12, 45);
154
-
155
- ctx.font = 'bold 22px "Georgia","Times New Roman",serif';
156
- ctx.fillStyle = '#222';
157
- ctx.fillText('🦊🐱🚀 2026', 12, 78);
158
-
159
- // content with antialiasing and compositing
160
- ctx.globalAlpha = 0.7;
161
- ctx.fillStyle = 'rgba(255, 80, 0, 0.35)';
162
- ctx.fillRect(180, 20, 80, 60);
163
-
164
- ctx.beginPath();
165
- ctx.arc(220, 50, 32, 0, Math.PI * 2);
166
- ctx.strokeStyle = '#fff';
167
- ctx.lineWidth = 4;
168
- ctx.stroke();
169
-
170
- ctx.beginPath();
171
- ctx.moveTo(30, 85);
172
- ctx.quadraticCurveTo(140, 20, 240, 85);
173
- ctx.strokeStyle = '#00f';
174
- ctx.lineWidth = 3;
175
- ctx.stroke();
176
-
177
- if (debug) {
178
- canvas.style.border = '1px solid #444';
179
- document.body.appendChild(canvas);
180
- }
181
-
182
- const dataUrl = canvas.toDataURL('image/png');
183
- const result = murmurHash3(dataUrl);
184
- return result;
185
- };
186
-
187
- const getWebglID = () => {
188
- const canvas = document.createElement('canvas');
189
- const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
190
- const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
191
- const vendor = debugInfo ? gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) : gl.getParameter(gl.VENDOR);
192
- const renderer = debugInfo ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER);
193
- const info = { vendor, renderer };
194
- const result = murmurHash3(stableStringify(info));
195
- return result;
196
- };
197
-
198
- const getWebgpuID = async (debug) => {
199
- const adapter = await navigator.gpu.requestAdapter({ powerPreference: 'high-performance' });
200
- const vendor = adapter.info?.vendor ?? '';
201
- const architecture = adapter.info?.architecture ?? '';
202
- const description = adapter.info?.description ?? '';
203
- const features = Array.from(adapter.features).sort();
204
- const limits = Array.from(adapter.limits).sort();
205
- const info = { vendor, architecture, description, features, limits };
206
-
207
- if (debug) {
208
- const debugDiv = document.createElement('pre');
209
- debugDiv.style.margin = '10px 0';
210
- debugDiv.style.padding = '10px';
211
- debugDiv.style.border = '1px solid #444';
212
- debugDiv.style.background = '#111';
213
- debugDiv.style.color = '#0f8';
214
- debugDiv.style.fontFamily = 'monospace';
215
- debugDiv.textContent = `WebGPU:\n${JSON.stringify(info, null, 2)}`;
216
- document.body.appendChild(debugDiv);
217
- }
218
-
219
- const result = murmurHash3(stableStringify(info));
220
- return result;
221
- };
222
-
223
- const getAudioID = async (debug) => {
1
+ const getAudioID = async (isDebug) => {
224
2
  const OfflineCtx = window.OfflineAudioContext || window.webkitOfflineAudioContext;
225
-
226
3
  const context = new OfflineCtx(1, 6000, 44100);
227
4
  const osc = context.createOscillator();
228
5
  osc.type = 'sawtooth';
229
6
  osc.frequency.value = 8800;
230
-
231
7
  const compressor = context.createDynamicsCompressor();
232
8
  compressor.threshold.value = -48;
233
9
  compressor.knee.value = 30;
234
10
  compressor.ratio.value = 14;
235
11
  compressor.attack.value = 0;
236
12
  compressor.release.value = 0.3;
237
-
238
13
  const gain = context.createGain();
239
14
  gain.gain.value = 0.4;
240
-
241
15
  osc.connect(compressor);
242
16
  compressor.connect(gain);
243
17
  gain.connect(context.destination);
244
-
245
18
  osc.start(0);
246
19
  const buffer = await context.startRendering();
247
20
  const channel = buffer.getChannelData(0);
248
-
249
21
  let hash = 0;
250
22
  let hash2 = 0;
251
23
  for (let i = 3000; i < 5800; i += 2) {
252
- const v = channel[i];
253
- hash = (hash * 31 + Math.floor((v + 1) * 100000)) | 0;
254
- hash2 = Math.imul(hash2 ^ Math.floor(v * 98765), 0x85ebca77);
24
+ const sampleValue = channel[i];
25
+ hash = (hash * 31 + Math.floor((sampleValue + 1) * 100000)) | 0;
26
+ hash2 = Math.imul(hash2 ^ Math.floor(sampleValue * 98765), 0x85ebca77);
255
27
  }
256
28
  const final = (hash >>> 0) ^ (hash2 >>> 0);
257
-
258
- if (debug) {
259
- const wc = document.createElement('canvas');
260
- wc.width = 500;
261
- wc.height = 120;
262
- wc.style.border = '1px solid #444';
263
- wc.style.margin = '10px 0';
264
- wc.title = 'Audio buffer snippet (4500–5000 samples)';
265
-
266
- const ctx = wc.getContext('2d');
29
+ if (isDebug) {
30
+ const waveformCanvas = document.createElement('canvas');
31
+ waveformCanvas.width = 500;
32
+ waveformCanvas.height = 120;
33
+ waveformCanvas.style.border = '1px solid #444';
34
+ waveformCanvas.style.margin = '10px 0';
35
+ waveformCanvas.title = 'Audio buffer snippet (4500–5000 samples)';
36
+ const ctx = waveformCanvas.getContext('2d');
267
37
  ctx.fillStyle = '#111';
268
- ctx.fillRect(0, 0, wc.width, wc.height);
269
-
38
+ ctx.fillRect(0, 0, waveformCanvas.width, waveformCanvas.height);
270
39
  ctx.font = '14px monospace';
271
40
  ctx.fillStyle = '#0f8';
272
41
  ctx.textAlign = 'left';
273
42
  ctx.fillText('AudioID', 10, 20);
274
-
275
43
  ctx.strokeStyle = '#0f8';
276
44
  ctx.lineWidth = 1.5;
277
45
  ctx.beginPath();
278
-
279
46
  const waveform = Array.from(channel.slice(4500, 5000));
280
- const step = wc.width / waveform.length;
281
-
47
+ const step = waveformCanvas.width / waveform.length;
282
48
  for (let i = 0; i < waveform.length; i++) {
283
49
  const x = i * step;
284
- const y = ((waveform[i] + 1) / 2) * wc.height;
285
- if (i === 0) ctx.moveTo(x, y);
286
- else ctx.lineTo(x, y);
50
+ const y = ((waveform[i] + 1) / 2) * waveformCanvas.height;
51
+ if (i === 0) {
52
+ ctx.moveTo(x, y);
53
+ } else {
54
+ ctx.lineTo(x, y);
55
+ }
287
56
  }
288
57
  ctx.stroke();
289
-
290
- document.body.appendChild(wc);
58
+ document.body.appendChild(waveformCanvas);
291
59
  }
60
+ return murmurHash3Hex(final.toString(16));
61
+ };
292
62
 
293
- const result = murmurHash3(final.toString(16));
294
- return result;
63
+ const getCanvasID = async (isDebug) => {
64
+ const canvas = document.createElement('canvas');
65
+ canvas.width = 280;
66
+ canvas.height = 100;
67
+ const ctx = canvas.getContext('2d');
68
+ const gradient = ctx.createLinearGradient(0, 0, 280, 100);
69
+ gradient.addColorStop(0, '#f60');
70
+ gradient.addColorStop(0.4, '#09f');
71
+ gradient.addColorStop(0.7, '#f09');
72
+ gradient.addColorStop(1, '#0f9');
73
+ ctx.fillStyle = gradient;
74
+ ctx.fillRect(0, 0, 280, 100);
75
+ ctx.font = '18px "Arial","Helvetica","DejaVu Sans",sans-serif';
76
+ ctx.fillStyle = '#000';
77
+ ctx.textBaseline = 'alphabetic';
78
+ ctx.fillText('Fingerprint ¼½¾™©®∆π∑€¶§', 12, 45);
79
+ ctx.font = 'bold 22px "Georgia","Times New Roman",serif';
80
+ ctx.fillStyle = '#222';
81
+ ctx.fillText('🦊🐱🚀 2026', 12, 78);
82
+ ctx.globalAlpha = 0.7;
83
+ ctx.fillStyle = 'rgba(255, 80, 0, 0.35)';
84
+ ctx.fillRect(180, 20, 80, 60);
85
+ ctx.beginPath();
86
+ ctx.arc(220, 50, 32, 0, Math.PI * 2);
87
+ ctx.strokeStyle = '#fff';
88
+ ctx.lineWidth = 4;
89
+ ctx.stroke();
90
+ ctx.beginPath();
91
+ ctx.moveTo(30, 85);
92
+ ctx.quadraticCurveTo(140, 20, 240, 85);
93
+ ctx.strokeStyle = '#00f';
94
+ ctx.lineWidth = 3;
95
+ ctx.stroke();
96
+ if (isDebug) {
97
+ canvas.style.border = '1px solid #444';
98
+ document.body.appendChild(canvas);
99
+ }
100
+ const dataUrl = canvas.toDataURL('image/png');
101
+ return murmurHash3Hex(dataUrl);
295
102
  };
296
103
 
297
104
  const getFonts = () => {
@@ -313,7 +120,6 @@ const getFonts = () => {
313
120
  '-apple-system',
314
121
  'BlinkMacSystemFont',
315
122
  ];
316
-
317
123
  const span = document.createElement('span');
318
124
  span.style.fontSize = '72px';
319
125
  span.style.position = 'absolute';
@@ -321,13 +127,11 @@ const getFonts = () => {
321
127
  span.style.whiteSpace = 'nowrap';
322
128
  span.textContent = 'mmmmmmmmmwwwwwww';
323
129
  document.body.appendChild(span);
324
-
325
130
  const defaults = {};
326
131
  for (const base of baseFonts) {
327
132
  span.style.fontFamily = base;
328
133
  defaults[base] = span.offsetWidth;
329
134
  }
330
-
331
135
  const detected = [];
332
136
  for (const font of testFonts) {
333
137
  let found = false;
@@ -346,10 +150,243 @@ const getFonts = () => {
346
150
  }
347
151
  }
348
152
  }
349
-
350
153
  document.body.removeChild(span);
351
- const info = detected.length ? detected.sort().join(',') : null;
352
- return info;
154
+ return detected.length ? detected.sort().join(',') : null;
155
+ };
156
+
157
+ const getMediaCodecs = () => {
158
+ const video = document.createElement('video');
159
+ const mimeList = ['video/webm', 'video/mp4', 'video/ogg', 'audio/mp4', 'audio/webm', 'audio/ogg', 'audio/mpeg'];
160
+ const codecSupportMap = {};
161
+ for (const mime of mimeList) {
162
+ const playbackSupport = video.canPlayType(mime);
163
+ codecSupportMap[mime] = playbackSupport === '' ? 'no' : playbackSupport;
164
+ }
165
+ return stableStringify(codecSupportMap);
166
+ };
167
+
168
+ const getWebglID = async () => {
169
+ const canvas = document.createElement('canvas');
170
+ const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
171
+ const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
172
+ const vendor = debugInfo ? gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) : gl.getParameter(gl.VENDOR);
173
+ const renderer = debugInfo ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER);
174
+ const webglInfo = { vendor, renderer };
175
+ return murmurHash3Hex(stableStringify(webglInfo));
176
+ };
177
+
178
+ const getWebgpuID = async (isDebug) => {
179
+ const adapter = await navigator.gpu.requestAdapter({ powerPreference: 'high-performance' });
180
+ const vendor = adapter.info?.vendor ?? '';
181
+ const architecture = adapter.info?.architecture ?? '';
182
+ const description = adapter.info?.description ?? '';
183
+ const features = Array.from(adapter.features).sort();
184
+ const limits = Array.from(adapter.limits).sort();
185
+ const webgpuInfo = { vendor, architecture, description, features, limits };
186
+ if (isDebug) {
187
+ const debugDiv = document.createElement('pre');
188
+ debugDiv.style.margin = '10px 0';
189
+ debugDiv.style.padding = '10px';
190
+ debugDiv.style.border = '1px solid #444';
191
+ debugDiv.style.background = '#111';
192
+ debugDiv.style.color = '#0f8';
193
+ debugDiv.style.fontFamily = 'monospace';
194
+ debugDiv.textContent = `WebGPU:\n${JSON.stringify(webgpuInfo, null, 2)}`;
195
+ document.body.appendChild(debugDiv);
196
+ }
197
+ return murmurHash3Hex(stableStringify(webgpuInfo));
198
+ };
199
+
200
+ const isMobile = () =>
201
+ navigator.userAgentData?.mobile === true ||
202
+ /Mobi|Android|iPhone|iPad|iPod|webOS|BlackBerry|Windows Phone/i.test(navigator.userAgent) ||
203
+ (navigator.maxTouchPoints > 0 && matchMedia('(pointer:coarse)').matches && innerWidth <= 1024);
204
+
205
+ const safe = async (asyncFn) => {
206
+ try {
207
+ return await asyncFn();
208
+ } catch {
209
+ return null;
210
+ }
211
+ };
212
+
213
+ const murmurHash3Hex = (inputString, seed = 1) => {
214
+ const data = new TextEncoder().encode(inputString);
215
+ const c1 = 0xcc9e2d51;
216
+ const c2 = 0x1b873593;
217
+ let h1 = seed >>> 0;
218
+
219
+ const blockCount = Math.floor(data.length / 4);
220
+ const dataView = new DataView(data.buffer, data.byteOffset, data.byteLength);
221
+ for (let i = 0; i < blockCount; i++) {
222
+ let k1 = dataView.getUint32(i * 4, true);
223
+ k1 = Math.imul(k1, c1);
224
+ k1 = (k1 << 15) | (k1 >>> 17);
225
+ k1 = Math.imul(k1, c2);
226
+ h1 ^= k1;
227
+ h1 = (h1 << 13) | (h1 >>> 19);
228
+ h1 = (Math.imul(h1, 5) + 0xe6546b64) >>> 0;
229
+ }
230
+
231
+ let k1 = 0;
232
+ const tailIndex = blockCount * 4;
233
+ const remainder = data.length & 3;
234
+ if (remainder === 3) {
235
+ k1 ^= data[tailIndex + 2] << 16;
236
+ }
237
+ if (remainder >= 2) {
238
+ k1 ^= data[tailIndex + 1] << 8;
239
+ }
240
+ if (remainder >= 1) {
241
+ k1 ^= data[tailIndex];
242
+ k1 = Math.imul(k1, c1);
243
+ k1 = (k1 << 15) | (k1 >>> 17);
244
+ k1 = Math.imul(k1, c2);
245
+ h1 ^= k1;
246
+ }
247
+
248
+ h1 ^= data.length;
249
+ h1 ^= h1 >>> 16;
250
+ h1 = Math.imul(h1, 0x85ebca6b) >>> 0;
251
+ h1 ^= h1 >>> 13;
252
+ h1 = Math.imul(h1, 0xc2b2ae35) >>> 0;
253
+ h1 ^= h1 >>> 16;
254
+ return h1.toString(16).padStart(8, '0');
255
+ };
256
+
257
+ const stableStringify = (value) => {
258
+ if (value === null || typeof value !== 'object') {
259
+ return JSON.stringify(value);
260
+ }
261
+ if (Array.isArray(value)) {
262
+ return `[${value.map(stableStringify).join(',')}]`;
263
+ }
264
+ const keys = Object.keys(value).sort();
265
+ const parts = [];
266
+ for (const key of keys) {
267
+ const item = value[key];
268
+ if (item !== undefined) {
269
+ parts.push(`${stableStringify(key)}:${stableStringify(item)}`);
270
+ }
271
+ }
272
+ return `{${parts.join(',')}}`;
273
+ };
274
+
275
+ const getBrowserFingerprint = async ({ debug: isDebug = false } = {}) => {
276
+ const startTime = performance.now();
277
+ const userAgentHighEntropy = await safe(async () => {
278
+ if (!window.navigator.userAgentData || typeof window.navigator.userAgentData.getHighEntropyValues !== 'function') {
279
+ return null;
280
+ }
281
+ const highEntropyValues = await window.navigator.userAgentData.getHighEntropyValues(['architecture', 'bitness', 'platformVersion', 'model', 'fullVersionList']);
282
+ return stableStringify(highEntropyValues);
283
+ });
284
+ const mediaCodecs = await safe(() => getMediaCodecs());
285
+ const fonts = await safe(() => getFonts());
286
+ const numberingSystem = await safe(() => new Intl.NumberFormat(window.navigator.languages?.[0] || 'en').resolvedOptions().numberingSystem);
287
+ const languages = window.navigator.languages ? Array.from(window.navigator.languages).join(',') : window.navigator.language || null;
288
+ const intlResolved = Intl.DateTimeFormat().resolvedOptions();
289
+ const timezone = intlResolved.timeZone;
290
+ const timezoneOffset = new Date().getTimezoneOffset();
291
+ const intlCalendar = intlResolved.calendar || null;
292
+ const hourCycle = intlResolved.hour12 !== undefined ? (intlResolved.hour12 ? 'h12' : 'h23') : intlResolved.hourCycle || null;
293
+ const reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'reduce' : 'no-preference';
294
+ const colorScheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
295
+ const prefersContrast = window.matchMedia('(prefers-contrast: more)').matches
296
+ ? 'more'
297
+ : window.matchMedia('(prefers-contrast: less)').matches
298
+ ? 'less'
299
+ : window.matchMedia('(prefers-contrast: custom)').matches
300
+ ? 'custom'
301
+ : 'no-preference';
302
+ const prefersReducedTransparency = window.matchMedia('(prefers-reduced-transparency: reduce)').matches ? 'reduce' : 'no-preference';
303
+ const forcedColors = window.matchMedia('(forced-colors: active)').matches ? 'active' : null;
304
+ const devicePixelRatio = typeof window.devicePixelRatio === 'number' ? Math.round(window.devicePixelRatio * 100) / 100 : null;
305
+ const { pixelDepth, colorDepth } = window.screen;
306
+ const touchSupport = 'ontouchstart' in window || window.navigator.maxTouchPoints > 0;
307
+ const canvasID = await safe(() => getCanvasID(isDebug));
308
+ const webglID = await safe(() => getWebglID());
309
+ const webgpuID = await safe(() => getWebgpuID(isDebug));
310
+ const audioID = await safe(() => getAudioID(isDebug));
311
+ const aspectRatio = window.screen.width && window.screen.height ? (window.screen.width / window.screen.height).toFixed(4) : null;
312
+ const screenOrientation = await safe(() => window.screen.orientation?.type || null);
313
+ const networkConnection = window.navigator.connection || window.navigator.mozConnection || window.navigator.webkitConnection;
314
+ const connectionType = networkConnection ? networkConnection.type || null : null;
315
+ const effectiveType = networkConnection?.effectiveType || null;
316
+ const saveData = networkConnection?.saveData === true;
317
+ const pdfViewerEnabled = typeof window.navigator.pdfViewerEnabled === 'boolean' ? window.navigator.pdfViewerEnabled : null;
318
+ const webdriver = typeof window.navigator.webdriver === 'boolean' ? window.navigator.webdriver : null;
319
+ const intlSupportedCalendars = await safe(() => {
320
+ if (typeof Intl.supportedValuesOf !== 'function') {
321
+ return null;
322
+ }
323
+ return Intl.supportedValuesOf('calendar').join(',');
324
+ });
325
+ const permissionsState = await safe(async () => {
326
+ if (!window.navigator.permissions || typeof window.navigator.permissions.query !== 'function') {
327
+ return null;
328
+ }
329
+ const permissionNames = ['geolocation', 'notifications'];
330
+ const permissionStateMap = {};
331
+ for (const name of permissionNames) {
332
+ try {
333
+ const permissionResult = await window.navigator.permissions.query({ name });
334
+ permissionStateMap[name] = permissionResult.state;
335
+ } catch {
336
+ permissionStateMap[name] = 'unsupported';
337
+ }
338
+ }
339
+ return stableStringify(permissionStateMap);
340
+ });
341
+ const data = {
342
+ vendor: window.navigator.vendor || null,
343
+ platform: window.navigator.platform || null,
344
+ userAgentHighEntropy: userAgentHighEntropy || null,
345
+ mediaCodecs: mediaCodecs || null,
346
+ fonts,
347
+ numberingSystem,
348
+ languages,
349
+ timezone,
350
+ timezoneOffset,
351
+ intlCalendar,
352
+ hourCycle,
353
+ reducedMotion,
354
+ colorScheme,
355
+ prefersContrast,
356
+ prefersReducedTransparency,
357
+ pdfViewerEnabled,
358
+ webdriver,
359
+ intlSupportedCalendars: intlSupportedCalendars || null,
360
+ permissionsState: permissionsState || null,
361
+ hardwareConcurrency: window.navigator.hardwareConcurrency || null,
362
+ deviceMemory: window.navigator.deviceMemory || null,
363
+ forcedColors,
364
+ devicePixelRatio,
365
+ pixelDepth,
366
+ colorDepth,
367
+ touchSupport,
368
+ screenOrientation: screenOrientation || null,
369
+ connectionType,
370
+ effectiveType,
371
+ saveData,
372
+ canvasID,
373
+ webglID,
374
+ webgpuID,
375
+ audioID,
376
+ };
377
+ const mobileData = {
378
+ aspectRatio,
379
+ width: window.screen.width,
380
+ height: window.screen.height,
381
+ maxTouchPoints: window.navigator.maxTouchPoints,
382
+ };
383
+ if (isMobile()) {
384
+ Object.assign(data, mobileData);
385
+ }
386
+ const payload = stableStringify(data);
387
+ const fingerprint = murmurHash3Hex(payload);
388
+ const elapsedMs = Math.round(performance.now() - startTime);
389
+ return { fingerprint, ...data, elapsedMs };
353
390
  };
354
391
 
355
392
  if (typeof window !== 'undefined') {