get-browser-fingerprint 4.1.1 → 5.0.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 CHANGED
@@ -1,10 +1,35 @@
1
1
  # get-browser-fingerprint
2
2
 
3
- Zero dependencies package exporting a single and fast (<50ms) asynchronous function returning a browser fingerprint, without requiring any permission to the user.
3
+ A single and fast (<50ms) asynchronous function returning a browser fingerprint, without requiring any permission to the user.
4
+
5
+ ## Assumptions
6
+
7
+ The function is targeting stock installations of the following browsers (no extensions or add-ons installed):
8
+ - Google Chrome (desktop and Android)
9
+ - Mozilla Firefox (desktop, default tracking protection, no custom privacy flags enabled)
10
+ - Microsoft Edge (desktop and Android)
11
+ - Apple Safari (macOS)
12
+ - Safari on iOS / iPadOS
13
+
14
+ Explicit assumptions:
15
+ - No privacy-focused extensions or add-ons installed.
16
+ - Browser privacy settings left at factory defaults (no manual changes to fingerprint resistance features).
17
+ - No hardened, anti-detect or heavily modified browser variants.
18
+
19
+ Under these conditions the collected signals retain meaningful entropy in February 2026:
20
+ - Canvas rendering (stable on Chrome, Edge, Firefox default; noisy or low-entropy on iOS Safari and partially on macOS Safari)
21
+ - WebGL unmasked vendor and renderer (strong on desktop, more uniform on mobile)
22
+ - Offline audio context (useful on Chrome, Edge, Firefox; heavily restricted on Safari)
23
+ - WebGPU properties (if available; low but increasing entropy)
24
+ - Locally installed fonts (valuable mainly on desktop Windows and macOS)
25
+ - Passive signals (hardwareConcurrency, deviceMemory, screen, timezone, languages, etc.)
26
+
27
+ On browsers with strong default fingerprint resistance (Safari iOS, certain Firefox modes) entropy and stability decrease significantly.
28
+ The script does not attempt to bypass intentional randomization or hardening.
4
29
 
5
30
  ## Usage
6
31
 
7
- Get browser fingerprint:
32
+ Get browser fingerprint:
8
33
  ```js
9
34
  import getBrowserFingerprint from 'get-browser-fingerprint';
10
35
  const fingerprint = await getBrowserFingerprint();
@@ -19,9 +44,13 @@ Options available:
19
44
 
20
45
  To test locally:
21
46
  ```sh
22
- fnm install
23
- pnpm install
47
+ fnm install # nodejs from .nvmrc
48
+ npm install -g corepack
49
+ corepack enable # package manager from package.json
50
+ corepack install # package manager from package.json
51
+ pnpm install # install deps
24
52
  pnpm exec playwright install chromium
53
+ pnpm exec playwright install firefox
25
54
  pnpm test
26
55
  ```
27
56
 
package/package.json CHANGED
@@ -1,11 +1,16 @@
1
1
  {
2
2
  "name": "get-browser-fingerprint",
3
- "version": "4.1.1",
3
+ "version": "5.0.1",
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",
7
- "main": "src/index.js",
7
+ "packageManager": "pnpm@10.28.2",
8
+ "engines": {
9
+ "node": ">=25.6",
10
+ "pnpm": ">=10.28"
11
+ },
8
12
  "type": "module",
13
+ "main": "src/index.js",
9
14
  "files": [
10
15
  "README.md",
11
16
  "package.json",
@@ -14,13 +19,13 @@
14
19
  ],
15
20
  "types": "./src/index.d.ts",
16
21
  "scripts": {
22
+ "postinstall": "pnpm audit --audit-level critical",
17
23
  "lint": "biome check --write",
18
- "test": "vitest run"
24
+ "test": "node --test --test-reporter=spec --test-reporter-destination=stdout"
19
25
  },
20
26
  "devDependencies": {
21
- "@biomejs/biome": "^2.2.2",
22
- "@playwright/test": "^1.55.0",
23
- "serve": "^14.2.4",
24
- "vitest": "^3.2.4"
27
+ "@biomejs/biome": "^2.4.0",
28
+ "@playwright/test": "^1.58.2",
29
+ "serve": "^14.2.5"
25
30
  }
26
31
  }
package/src/index.d.ts CHANGED
@@ -1,9 +1,12 @@
1
1
  export interface FingerprintOptions {
2
- hardwareOnly?: boolean;
3
2
  debug?: boolean;
4
3
  }
5
4
 
6
- export default function getBrowserFingerprint(options?: FingerprintOptions): Promise<number>;
5
+ export type FingerprintResult = {
6
+ fingerprint: string;
7
+ } & Record<string, string>;
8
+
9
+ export default function getBrowserFingerprint(options?: FingerprintOptions): Promise<FingerprintResult>;
7
10
 
8
11
  declare global {
9
12
  interface Window {
package/src/index.js CHANGED
@@ -1,339 +1,358 @@
1
- /** @type {import('./index.d.ts').getBrowserFingerprint} */
2
- const getBrowserFingerprint = async ({ hardwareOnly = true, debug = false } = {}) => {
3
- const { cookieEnabled, deviceMemory, doNotTrack, hardwareConcurrency, language, languages, maxTouchPoints, platform, userAgent, vendor } = window.navigator;
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
+ }
4
34
 
5
- // we use screen info only on mobile, because on desktop the user may use multiple monitors
6
- const enableScreen = /Mobi|Android|iPhone|iPad|iPod/i.test(navigator.userAgent);
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
7
41
 
8
- const { width, height, colorDepth, pixelDepth } = enableScreen ? window.screen : {}; // undefined will remove this from the stringify down here
9
- const timezoneOffset = new Date().getTimezoneOffset();
10
- const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
11
- const touchSupport = "ontouchstart" in window;
12
- const devicePixelRatio = window.devicePixelRatio;
13
-
14
- const canvas = getCanvasID(debug);
15
- const audio = await getAudioID(debug);
16
- const audioInfo = getAudioInfo();
17
- const webgl = getWebglID(debug);
18
- const webglInfo = getWebglInfo();
19
-
20
- const data = hardwareOnly
21
- ? {
22
- audioInfo,
23
- audio,
24
- canvas,
25
- colorDepth,
26
- deviceMemory,
27
- devicePixelRatio,
28
- hardwareConcurrency,
29
- height,
30
- maxTouchPoints,
31
- pixelDepth,
32
- platform,
33
- touchSupport,
34
- webgl,
35
- webglInfo,
36
- width,
37
- }
38
- : {
39
- audioInfo,
40
- audio,
41
- canvas,
42
- colorDepth,
43
- cookieEnabled,
44
- deviceMemory,
45
- devicePixelRatio,
46
- doNotTrack,
47
- hardwareConcurrency,
48
- height,
49
- language,
50
- languages,
51
- maxTouchPoints,
52
- pixelDepth,
53
- platform,
54
- timezone,
55
- timezoneOffset,
56
- touchSupport,
57
- userAgent,
58
- vendor,
59
- webgl,
60
- webglInfo,
61
- width,
62
- };
63
-
64
- if (debug) console.log("Fingerprint data:", JSON.stringify(data, null, 2));
65
-
66
- const payload = JSON.stringify(data, null, 2);
67
- const result = murmurhash3_32_gc(payload);
42
+ const result = h.toString(16).padStart(8, '0');
68
43
  return result;
69
44
  };
70
45
 
71
- const getCanvasID = (debug) => {
46
+ const safe = async (fn) => {
72
47
  try {
73
- const canvas = document.createElement("canvas");
74
- const ctx = canvas.getContext("2d");
75
- const text = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ`~1!2@3#4$5%6^7&8*9(0)-_=+[{]}|;:',<.>/?";
76
- ctx.textBaseline = "top";
77
- ctx.font = "14px 'Arial'";
78
- ctx.textBaseline = "alphabetic";
79
- ctx.fillStyle = "#f60";
80
- ctx.fillRect(125, 1, 62, 20);
81
- ctx.fillStyle = "#069";
82
- ctx.fillText(text, 2, 15);
83
- ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
84
- ctx.fillText(text, 4, 17);
85
-
86
- const result = canvas.toDataURL();
87
-
88
- if (debug) {
89
- document.body.appendChild(canvas);
90
- } else {
91
- ctx.clearRect(0, 0, canvas.width, canvas.height);
92
- }
93
-
94
- return murmurhash3_32_gc(result);
48
+ return await fn();
95
49
  } catch {
96
50
  return null;
97
51
  }
98
52
  };
99
53
 
100
- const getWebglID = (debug) => {
101
- try {
102
- const canvas = document.createElement("canvas");
103
- const ctx = canvas.getContext("webgl");
104
- canvas.width = 256;
105
- canvas.height = 128;
106
-
107
- const f =
108
- "attribute vec2 attrVertex;varying vec2 varyinTexCoordinate;uniform vec2 uniformOffset;void main(){varyinTexCoordinate=attrVertex+uniformOffset;gl_Position=vec4(attrVertex,0,1);}";
109
- const g = "precision mediump float;varying vec2 varyinTexCoordinate;void main() {gl_FragColor=vec4(varyinTexCoordinate,0,1);}";
110
- const h = ctx.createBuffer();
111
-
112
- ctx.bindBuffer(ctx.ARRAY_BUFFER, h);
113
-
114
- const i = new Float32Array([-0.2, -0.9, 0, 0.4, -0.26, 0, 0, 0.7321, 0]);
115
-
116
- ctx.bufferData(ctx.ARRAY_BUFFER, i, ctx.STATIC_DRAW);
117
- h.itemSize = 3;
118
- h.numItems = 3;
119
-
120
- const j = ctx.createProgram();
121
- const k = ctx.createShader(ctx.VERTEX_SHADER);
122
-
123
- ctx.shaderSource(k, f);
124
- ctx.compileShader(k);
125
-
126
- const l = ctx.createShader(ctx.FRAGMENT_SHADER);
127
-
128
- ctx.shaderSource(l, g);
129
- ctx.compileShader(l);
130
- ctx.attachShader(j, k);
131
- ctx.attachShader(j, l);
132
- ctx.linkProgram(j);
133
- ctx.useProgram(j);
134
-
135
- j.vertexPosAttrib = ctx.getAttribLocation(j, "attrVertex");
136
- j.offsetUniform = ctx.getUniformLocation(j, "uniformOffset");
137
-
138
- ctx.enableVertexAttribArray(j.vertexPosArray);
139
- ctx.vertexAttribPointer(j.vertexPosAttrib, h.itemSize, ctx.FLOAT, !1, 0, 0);
140
- ctx.uniform2f(j.offsetUniform, 1, 1);
141
- ctx.drawArrays(ctx.TRIANGLE_STRIP, 0, h.numItems);
142
-
143
- const n = new Uint8Array(canvas.width * canvas.height * 4);
144
- ctx.readPixels(0, 0, canvas.width, canvas.height, ctx.RGBA, ctx.UNSIGNED_BYTE, n);
145
-
146
- const result = JSON.stringify(n).replace(/,?"[0-9]+":/g, "");
147
-
148
- if (debug) {
149
- document.body.appendChild(canvas);
150
- } else {
151
- ctx.clear(ctx.COLOR_BUFFER_BIT | ctx.DEPTH_BUFFER_BIT | ctx.STENCIL_BUFFER_BIT);
152
- }
153
-
154
- return murmurhash3_32_gc(result);
155
- } catch {
156
- return null;
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)}`);
157
65
  }
158
- };
159
66
 
160
- const getWebglInfo = () => {
161
- try {
162
- const ctx = document.createElement("canvas").getContext("webgl");
67
+ return `{${parts.join(',')}}`;
68
+ };
163
69
 
164
- const result = {
165
- VERSION: String(ctx.getParameter(ctx.VERSION)),
166
- SHADING_LANGUAGE_VERSION: String(ctx.getParameter(ctx.SHADING_LANGUAGE_VERSION)),
167
- VENDOR: String(ctx.getParameter(ctx.VENDOR)),
168
- SUPPORTED_EXTENSIONS: String(ctx.getSupportedExtensions()),
169
- };
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);
170
74
 
171
- return result;
172
- } catch {
173
- return null;
174
- }
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 };
175
132
  };
176
133
 
177
- const getAudioInfo = () => {
178
- try {
179
- const OfflineAudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext;
180
- const length = 44100;
181
- const sampleRate = 44100;
182
- const context = new OfflineAudioContext(1, length, sampleRate);
183
-
184
- const result = {
185
- sampleRate: context.sampleRate,
186
- channelCount: context.destination.maxChannelCount,
187
- outputLatency: context.outputLatency,
188
- state: context.state,
189
- baseLatency: context.baseLatency,
190
- };
191
-
192
- return result;
193
- } catch {
194
- return null;
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);
195
180
  }
196
- };
197
181
 
198
- const getAudioID = async (debug) => {
199
- try {
200
- const OfflineAudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext;
201
- const sampleRate = 44100;
202
- const length = 44100; // Number of samples (1 second of audio)
203
- const context = new OfflineAudioContext(1, length, sampleRate);
204
-
205
- // Create an oscillator to generate sound
206
- const oscillator = context.createOscillator();
207
- oscillator.type = "sine";
208
- oscillator.frequency.value = 440;
209
-
210
- oscillator.connect(context.destination);
211
- oscillator.start();
212
-
213
- // Render the audio into a buffer
214
- const renderedBuffer = await context.startRendering();
215
- const channelData = renderedBuffer.getChannelData(0);
216
-
217
- // Generate fingerprint by summing the absolute values of the audio data
218
- const result = channelData.reduce((acc, val) => acc + Math.abs(val), 0).toString();
219
-
220
- if (debug) {
221
- const wavBlob = bufferToWav(renderedBuffer);
222
- const audioURL = URL.createObjectURL(wavBlob);
223
-
224
- const audioElement = document.createElement("audio");
225
- audioElement.controls = true;
226
- audioElement.src = audioURL;
227
- document.body.appendChild(audioElement);
228
- }
182
+ const dataUrl = canvas.toDataURL('image/png');
183
+ const result = murmurHash3(dataUrl);
184
+ return result;
185
+ };
229
186
 
230
- return murmurhash3_32_gc(result);
231
- } catch {
232
- return null;
233
- }
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;
234
196
  };
235
197
 
236
- const bufferToWav = (buffer) => {
237
- const numOfChannels = buffer.numberOfChannels;
238
- const length = buffer.length * numOfChannels * 2 + 44; // Buffer size in bytes
239
- const wavBuffer = new ArrayBuffer(length);
240
- const view = new DataView(wavBuffer);
241
-
242
- // Write WAV file header
243
- writeString(view, 0, "RIFF");
244
- view.setUint32(4, length - 8, true);
245
- writeString(view, 8, "WAVE");
246
- writeString(view, 12, "fmt ");
247
- view.setUint32(16, 16, true);
248
- view.setUint16(20, 1, true);
249
- view.setUint16(22, numOfChannels, true);
250
- view.setUint32(24, buffer.sampleRate, true);
251
- view.setUint32(28, buffer.sampleRate * numOfChannels * 2, true);
252
- view.setUint16(32, numOfChannels * 2, true);
253
- view.setUint16(34, 16, true);
254
- writeString(view, 36, "data");
255
- view.setUint32(40, length - 44, true);
256
-
257
- // Write interleaved audio data
258
- let offset = 44;
259
- for (let i = 0; i < buffer.length; i++) {
260
- for (let channel = 0; channel < numOfChannels; channel++) {
261
- const sample = buffer.getChannelData(channel)[i];
262
- const intSample = Math.max(-1, Math.min(1, sample)) * 32767;
263
- view.setInt16(offset, intSample, true);
264
- offset += 2;
265
- }
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);
266
217
  }
267
218
 
268
- return new Blob([view], { type: "audio/wav" });
219
+ const result = murmurHash3(stableStringify(info));
220
+ return result;
269
221
  };
270
222
 
271
- const writeString = (view, offset, string) => {
272
- for (let i = 0; i < string.length; i++) {
273
- view.setUint8(offset + i, string.charCodeAt(i));
223
+ const getAudioID = async (debug) => {
224
+ const OfflineCtx = window.OfflineAudioContext || window.webkitOfflineAudioContext;
225
+
226
+ const context = new OfflineCtx(1, 6000, 44100);
227
+ const osc = context.createOscillator();
228
+ osc.type = 'sawtooth';
229
+ osc.frequency.value = 8800;
230
+
231
+ const compressor = context.createDynamicsCompressor();
232
+ compressor.threshold.value = -48;
233
+ compressor.knee.value = 30;
234
+ compressor.ratio.value = 14;
235
+ compressor.attack.value = 0;
236
+ compressor.release.value = 0.3;
237
+
238
+ const gain = context.createGain();
239
+ gain.gain.value = 0.4;
240
+
241
+ osc.connect(compressor);
242
+ compressor.connect(gain);
243
+ gain.connect(context.destination);
244
+
245
+ osc.start(0);
246
+ const buffer = await context.startRendering();
247
+ const channel = buffer.getChannelData(0);
248
+
249
+ let hash = 0;
250
+ let hash2 = 0;
251
+ 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);
274
255
  }
275
- };
276
-
277
- const murmurhash3_32_gc = (key) => {
278
- const remainder = key.length & 3; // key.length % 4
279
- const bytes = key.length - remainder;
280
- const c1 = 0xcc9e2d51;
281
- const c2 = 0x1b873593;
282
-
283
- let h1;
284
- let h1b;
285
- let k1;
286
-
287
- for (let i = 0; i < bytes; i++) {
288
- k1 = (key.charCodeAt(i) & 0xff) | ((key.charCodeAt(++i) & 0xff) << 8) | ((key.charCodeAt(++i) & 0xff) << 16) | ((key.charCodeAt(++i) & 0xff) << 24);
289
- ++i;
290
-
291
- k1 = ((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff;
292
- k1 = (k1 << 15) | (k1 >>> 17);
293
- k1 = ((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff;
256
+ 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');
267
+ ctx.fillStyle = '#111';
268
+ ctx.fillRect(0, 0, wc.width, wc.height);
269
+
270
+ ctx.font = '14px monospace';
271
+ ctx.fillStyle = '#0f8';
272
+ ctx.textAlign = 'left';
273
+ ctx.fillText('AudioID', 10, 20);
274
+
275
+ ctx.strokeStyle = '#0f8';
276
+ ctx.lineWidth = 1.5;
277
+ ctx.beginPath();
278
+
279
+ const waveform = Array.from(channel.slice(4500, 5000));
280
+ const step = wc.width / waveform.length;
281
+
282
+ for (let i = 0; i < waveform.length; i++) {
283
+ 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);
287
+ }
288
+ ctx.stroke();
294
289
 
295
- h1 ^= k1;
296
- h1 = (h1 << 13) | (h1 >>> 19);
297
- h1b = ((h1 & 0xffff) * 5 + ((((h1 >>> 16) * 5) & 0xffff) << 16)) & 0xffffffff;
298
- h1 = (h1b & 0xffff) + 0x6b64 + ((((h1b >>> 16) + 0xe654) & 0xffff) << 16);
290
+ document.body.appendChild(wc);
299
291
  }
300
292
 
301
- const i = bytes - 1;
293
+ const result = murmurHash3(final.toString(16));
294
+ return result;
295
+ };
302
296
 
303
- k1 = 0;
297
+ const getFonts = () => {
298
+ const baseFonts = ['monospace', 'serif', 'sans-serif'];
299
+ const testFonts = [
300
+ 'Arial',
301
+ 'Helvetica',
302
+ 'Verdana',
303
+ 'Trebuchet MS',
304
+ 'Comic Sans MS',
305
+ 'Georgia',
306
+ 'Times New Roman',
307
+ 'Courier New',
308
+ 'Segoe UI',
309
+ 'Roboto',
310
+ 'Open Sans',
311
+ 'Noto Sans',
312
+ 'system-ui',
313
+ '-apple-system',
314
+ 'BlinkMacSystemFont',
315
+ ];
316
+
317
+ const span = document.createElement('span');
318
+ span.style.fontSize = '72px';
319
+ span.style.position = 'absolute';
320
+ span.style.visibility = 'hidden';
321
+ span.style.whiteSpace = 'nowrap';
322
+ span.textContent = 'mmmmmmmmmwwwwwww';
323
+ document.body.appendChild(span);
324
+
325
+ const defaults = {};
326
+ for (const base of baseFonts) {
327
+ span.style.fontFamily = base;
328
+ defaults[base] = span.offsetWidth;
329
+ }
304
330
 
305
- switch (remainder) {
306
- case 3: {
307
- k1 ^= (key.charCodeAt(i + 2) & 0xff) << 16;
308
- break;
309
- }
310
- case 2: {
311
- k1 ^= (key.charCodeAt(i + 1) & 0xff) << 8;
312
- break;
331
+ const detected = [];
332
+ for (const font of testFonts) {
333
+ let found = false;
334
+ for (const base of baseFonts) {
335
+ span.style.fontFamily = `"${font}", ${base}`;
336
+ if (span.offsetWidth !== defaults[base]) {
337
+ detected.push(font);
338
+ found = true;
339
+ break;
340
+ }
313
341
  }
314
- case 1: {
315
- k1 ^= key.charCodeAt(i) & 0xff;
316
- break;
342
+ if (!found) {
343
+ span.style.fontFamily = `"${font}"`;
344
+ if (span.offsetWidth !== defaults.monospace && span.offsetWidth !== defaults.serif) {
345
+ detected.push(font);
346
+ }
317
347
  }
318
348
  }
319
349
 
320
- k1 = ((k1 & 0xffff) * c1 + ((((k1 >>> 16) * c1) & 0xffff) << 16)) & 0xffffffff;
321
- k1 = (k1 << 15) | (k1 >>> 17);
322
- k1 = ((k1 & 0xffff) * c2 + ((((k1 >>> 16) * c2) & 0xffff) << 16)) & 0xffffffff;
323
- h1 ^= k1;
324
-
325
- h1 ^= key.length;
326
-
327
- h1 ^= h1 >>> 16;
328
- h1 = ((h1 & 0xffff) * 0x85ebca6b + ((((h1 >>> 16) * 0x85ebca6b) & 0xffff) << 16)) & 0xffffffff;
329
- h1 ^= h1 >>> 13;
330
- h1 = ((h1 & 0xffff) * 0xc2b2ae35 + ((((h1 >>> 16) * 0xc2b2ae35) & 0xffff) << 16)) & 0xffffffff;
331
- h1 ^= h1 >>> 16;
332
-
333
- return h1 >>> 0;
350
+ document.body.removeChild(span);
351
+ const info = detected.length ? detected.sort().join(',') : null;
352
+ return info;
334
353
  };
335
354
 
336
- if (typeof window !== "undefined") {
355
+ if (typeof window !== 'undefined') {
337
356
  window.getBrowserFingerprint = getBrowserFingerprint;
338
357
  }
339
358