@zaplier/sdk 1.8.1 → 1.8.2

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.
Files changed (34) hide show
  1. package/dist/core-ultra.min.js +1 -1
  2. package/dist/index.cjs +1615 -34
  3. package/dist/index.d.ts +11 -7
  4. package/dist/index.esm.js +1615 -34
  5. package/dist/v2/chunks/browser-apis-AyU2utpF.min.js +6 -0
  6. package/dist/v2/chunks/confidence-BslwbUCt.min.js +6 -0
  7. package/dist/v2/chunks/device-signals-2L_62qNZ.min.js +6 -0
  8. package/dist/v2/chunks/dom-blockers-C467-IRd.min.js +6 -0
  9. package/dist/v2/chunks/fingerprint-FfUEEIAd.min.js +6 -0
  10. package/dist/v2/chunks/hardware-9ikfSEs-.min.js +6 -0
  11. package/dist/v2/chunks/incognito-CkKAdE8Z.min.js +6 -0
  12. package/dist/v2/chunks/math-Q4s6nkVD.min.js +6 -0
  13. package/dist/v2/chunks/plugins-enhanced-mUjU1EXe.min.js +6 -0
  14. package/dist/v2/chunks/session-replay-C5Tp0d16.min.js +6 -0
  15. package/dist/v2/chunks/storage-Bl_8oytT.min.js +6 -0
  16. package/dist/v2/chunks/system-DTjxyOZF.min.js +6 -0
  17. package/dist/v2/core.d.ts +7 -0
  18. package/dist/v2/core.min.js +2 -2
  19. package/dist/v2/modules/anti-adblock.js +1 -1
  20. package/dist/v2/modules/browser-apis-DzzjRXFN.js +6 -0
  21. package/dist/v2/modules/confidence-CLylpqVh.js +6 -0
  22. package/dist/v2/modules/device-signals-D-VQg-o6.js +6 -0
  23. package/dist/v2/modules/dom-blockers-D9M2aO9M.js +6 -0
  24. package/dist/v2/modules/fingerprint-Ddq30bun.js +6 -0
  25. package/dist/v2/modules/fingerprint.js +2 -2
  26. package/dist/v2/modules/hardware-BxWqOjae.js +6 -0
  27. package/dist/v2/modules/heatmap.js +1 -1
  28. package/dist/v2/modules/incognito-DpuYoC8S.js +6 -0
  29. package/dist/v2/modules/math-B13vt1ND.js +6 -0
  30. package/dist/v2/modules/plugins-enhanced-D5ft0k0e.js +6 -0
  31. package/dist/v2/modules/replay.js +1 -1
  32. package/dist/v2/modules/storage-D8dcMojB.js +6 -0
  33. package/dist/v2/modules/system-ZMflVbka.js +6 -0
  34. package/package.json +13 -13
package/dist/index.cjs CHANGED
@@ -1,7 +1,7 @@
1
1
  /*!
2
2
  * Zaplier SDK v1.0.0
3
3
  * Advanced privacy-first tracking with fingerprinting and security detection
4
- * (c) 2025 RabbitTracker Team
4
+ * (c) 2026 RabbitTracker Team
5
5
  * https://zaplier.com
6
6
  */
7
7
  'use strict';
@@ -14,6 +14,53 @@ Object.defineProperty(exports, '__esModule', { value: true });
14
14
  *
15
15
  * Provides 128-bit hash output using the x64 variant of MurmurHash3
16
16
  */
17
+ /**
18
+ * Stable JSON stringify — sorts object keys at every level so the resulting
19
+ * string is identical regardless of engine-specific key ordering.
20
+ * Based on fast-json-stable-stringify (ThumbmarkJS adaptation).
21
+ */
22
+ function stableStringify(data) {
23
+ const seen = [];
24
+ function stringify(node) {
25
+ if (node && typeof node.toJSON === "function") {
26
+ node = node.toJSON();
27
+ }
28
+ if (node === undefined)
29
+ return undefined;
30
+ if (typeof node === "number")
31
+ return Number.isFinite(node) ? String(node) : "null";
32
+ if (typeof node !== "object")
33
+ return JSON.stringify(node);
34
+ if (Array.isArray(node)) {
35
+ let out = "[";
36
+ for (let i = 0; i < node.length; i++) {
37
+ if (i)
38
+ out += ",";
39
+ out += stringify(node[i]) ?? "null";
40
+ }
41
+ return out + "]";
42
+ }
43
+ if (node === null)
44
+ return "null";
45
+ if (seen.indexOf(node) !== -1) {
46
+ throw new TypeError("Converting circular structure to JSON");
47
+ }
48
+ const idx = seen.push(node) - 1;
49
+ const keys = Object.keys(node).sort();
50
+ let out = "";
51
+ for (const key of keys) {
52
+ const val = stringify(node[key]);
53
+ if (val === undefined)
54
+ continue;
55
+ if (out)
56
+ out += ",";
57
+ out += JSON.stringify(key) + ":" + val;
58
+ }
59
+ seen.splice(idx, 1);
60
+ return "{" + out + "}";
61
+ }
62
+ return stringify(data) ?? "";
63
+ }
17
64
  /**
18
65
  * Adds two 64-bit numbers
19
66
  */
@@ -185,7 +232,7 @@ function hashFingerprint(components, debug = false) {
185
232
  // CRITICAL: Do NOT use JSON.stringify(obj, keys) as it filters out nested properties!
186
233
  // Use canonicalizeStable instead which handles deep sorting and normalization.
187
234
  const canonicalized = canonicalizeStable(components);
188
- const canonical = JSON.stringify(canonicalized);
235
+ const canonical = stableStringify(canonicalized);
189
236
  if (debug) {
190
237
  console.log("[RabbitTracker] hashFingerprint debug:", {
191
238
  originalKeys: Object.keys(components),
@@ -298,7 +345,7 @@ function canonicalizeStable(input) {
298
345
  */
299
346
  function hashStableCore(coreVector, debug = false) {
300
347
  const canonicalized = canonicalizeStable(coreVector);
301
- const canonical = JSON.stringify(canonicalized);
348
+ const canonical = stableStringify(canonicalized);
302
349
  if (debug) {
303
350
  console.log("[RabbitTracker] hashStableCore debug:", {
304
351
  originalKeys: Object.keys(coreVector),
@@ -608,6 +655,14 @@ function getAudioContextCharacteristics() {
608
655
  if ('baseLatency' in testContext) {
609
656
  characteristics.baseLatency = testContext.baseLatency;
610
657
  }
658
+ // Channel count mode of AudioBufferSourceNode
659
+ try {
660
+ const bufferSource = testContext.createBufferSource();
661
+ characteristics.channelCountMode = bufferSource.channelCountMode;
662
+ }
663
+ catch {
664
+ characteristics.channelCountMode = 'max';
665
+ }
611
666
  // OfflineAudioContext doesn't have close method - it's automatically disposed after rendering
612
667
  if ('close' in testContext && typeof testContext.close === 'function') {
613
668
  testContext.close();
@@ -705,7 +760,8 @@ async function getAudioFingerprint() {
705
760
  contextCharacteristics: contextCharacteristics,
706
761
  nodeCapabilities: nodeCapabilities,
707
762
  audioStackHash: audioStackHash,
708
- supportedSampleRates: contextCharacteristics.supportedSampleRates || []
763
+ supportedSampleRates: contextCharacteristics.supportedSampleRates || [],
764
+ channelCountMode: contextCharacteristics.channelCountMode || 'max'
709
765
  };
710
766
  return {
711
767
  value: result,
@@ -724,7 +780,8 @@ async function getAudioFingerprint() {
724
780
  contextCharacteristics: {},
725
781
  nodeCapabilities: {},
726
782
  audioStackHash: "error",
727
- supportedSampleRates: []
783
+ supportedSampleRates: [],
784
+ channelCountMode: 'max'
728
785
  },
729
786
  duration: performance.now() - startTime,
730
787
  error: error instanceof Error ? error.message : "Audio fingerprinting failed",
@@ -1393,6 +1450,28 @@ async function getBrowserFingerprint() {
1393
1450
  if (hardwareInfo.deviceMemory !== undefined) {
1394
1451
  result.deviceMemory = hardwareInfo.deviceMemory;
1395
1452
  }
1453
+ // productSub: "20030107" = Chrome/Safari, "20100101" = Firefox
1454
+ const productSub = navigator.productSub;
1455
+ if (typeof productSub === 'string' && productSub) {
1456
+ result.productSub = productSub;
1457
+ }
1458
+ // ApplePay version — exclusive to Safari + HTTPS + Apple device
1459
+ result.applePayVersion = (() => {
1460
+ try {
1461
+ if (typeof globalThis.ApplePaySession === 'undefined')
1462
+ return 0;
1463
+ if (typeof globalThis.ApplePaySession.supportsVersion !== 'function')
1464
+ return 0;
1465
+ for (let v = 15; v >= 1; v--) {
1466
+ if (globalThis.ApplePaySession.supportsVersion(v))
1467
+ return v;
1468
+ }
1469
+ return 0;
1470
+ }
1471
+ catch {
1472
+ return 0;
1473
+ }
1474
+ })();
1396
1475
  return {
1397
1476
  value: result,
1398
1477
  duration: endTime - startTime
@@ -1430,6 +1509,56 @@ function isBrowserFingerprintingAvailable() {
1430
1509
  }
1431
1510
  }
1432
1511
 
1512
+ /**
1513
+ * Common Pixels Anti-Randomization Utility
1514
+ * Based on ThumbmarkJS commonPixels implementation
1515
+ *
1516
+ * Browsers like Brave and Firefox (private mode) inject per-pixel random noise
1517
+ * into canvas/WebGL reads to prevent fingerprinting. This utility runs the same
1518
+ * render N times and selects the most frequent value per pixel channel, producing
1519
+ * a consensus ImageData that is stable across noise injections.
1520
+ */
1521
+ /**
1522
+ * Returns the most frequent value in an array of numbers.
1523
+ */
1524
+ function getMostFrequent(arr) {
1525
+ if (arr.length === 0)
1526
+ return 0;
1527
+ const freq = {};
1528
+ for (const n of arr) {
1529
+ freq[n] = (freq[n] ?? 0) + 1;
1530
+ }
1531
+ let best = arr[0];
1532
+ for (const key in freq) {
1533
+ const n = parseInt(key, 10);
1534
+ if (freq[n] > freq[best]) {
1535
+ best = n;
1536
+ }
1537
+ }
1538
+ return best;
1539
+ }
1540
+ /**
1541
+ * Given N ImageData objects from the same canvas/scene, returns a new ImageData
1542
+ * where each byte is the most frequent value across all renders.
1543
+ * This cancels out per-pixel random noise injected by privacy-hardened browsers.
1544
+ *
1545
+ * @param images - Array of ImageData objects (must all have the same dimensions)
1546
+ * @param width - Canvas width in pixels
1547
+ * @param height - Canvas height in pixels
1548
+ */
1549
+ function getCommonPixels(images, width, height) {
1550
+ const len = images[0].data.length;
1551
+ const finalData = new Uint8ClampedArray(len);
1552
+ for (let i = 0; i < len; i++) {
1553
+ const samples = [];
1554
+ for (const img of images) {
1555
+ samples.push(img.data[i]);
1556
+ }
1557
+ finalData[i] = getMostFrequent(samples);
1558
+ }
1559
+ return new ImageData(finalData, width, height);
1560
+ }
1561
+
1433
1562
  /**
1434
1563
  * Canvas Fingerprinting
1435
1564
  * Based on FingerprintJS canvas component with incognito detection
@@ -1729,6 +1858,36 @@ function detectAdvancedInconsistencies(textHash, geometryHash, subPixelData, ctx
1729
1858
  };
1730
1859
  }
1731
1860
  }
1861
+ /**
1862
+ * Render a scene function onto a canvas N times and return the consensus ImageData.
1863
+ * Used to cancel out per-pixel noise injected by Brave/Firefox privacy mode.
1864
+ */
1865
+ function renderConsensus(width, height, renderFn, runs = 3) {
1866
+ const images = [];
1867
+ for (let i = 0; i < runs; i++) {
1868
+ const c = document.createElement("canvas");
1869
+ c.width = width;
1870
+ c.height = height;
1871
+ const ctx = c.getContext("2d", { willReadFrequently: true });
1872
+ if (!ctx)
1873
+ continue;
1874
+ renderFn(ctx);
1875
+ try {
1876
+ images.push(ctx.getImageData(0, 0, width, height));
1877
+ }
1878
+ catch {
1879
+ // getImageData blocked — skip
1880
+ }
1881
+ }
1882
+ if (images.length === 0)
1883
+ return "consensus-blocked";
1884
+ // Single render — no noise to cancel; hash directly
1885
+ if (images.length === 1) {
1886
+ return hash32(Array.from(images[0].data).join(","));
1887
+ }
1888
+ const consensus = getCommonPixels(images, width, height);
1889
+ return hash32(Array.from(consensus.data).join(","));
1890
+ }
1732
1891
  /**
1733
1892
  * Generate enhanced canvas fingerprint with multiple primitives and sub-pixel analysis
1734
1893
  */
@@ -1765,15 +1924,29 @@ async function getCanvasFingerprint() {
1765
1924
  const subPixelAnalysis = analyzeSubPixels(ctx, canvas);
1766
1925
  // ENHANCED: Advanced inconsistency detection
1767
1926
  const inconsistencyAnalysis = detectAdvancedInconsistencies(textHash, geometryHash, subPixelAnalysis, ctx);
1768
- // Create composite canvas with both text and geometry for additional hash
1769
- ctx.clearRect(0, 0, canvas.width, canvas.height);
1770
- // Draw both text and geometry together
1771
- drawAdvancedText(ctx);
1772
- ctx.globalCompositeOperation = "multiply";
1773
- drawAdvancedGeometry(ctx);
1774
- ctx.globalCompositeOperation = "source-over";
1775
- const compositeData = canvas.toDataURL("image/png");
1776
- const compositeHash = hash32(compositeData);
1927
+ // ANTI-RANDOMIZATION: If browser injects canvas noise (Brave/Firefox/Samsung),
1928
+ // run the scene 3× and take the per-pixel consensus to cancel the noise out.
1929
+ const needsConsensus = inconsistencyAnalysis.isInconsistent || isBrave() || isSamsungInternet$1();
1930
+ let compositeHash;
1931
+ if (needsConsensus) {
1932
+ const w = canvas.width;
1933
+ const h = canvas.height;
1934
+ compositeHash = renderConsensus(w, h, (c) => {
1935
+ drawAdvancedText(c);
1936
+ c.globalCompositeOperation = "multiply";
1937
+ drawAdvancedGeometry(c);
1938
+ c.globalCompositeOperation = "source-over";
1939
+ });
1940
+ }
1941
+ else {
1942
+ // Fast path: single composite render
1943
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1944
+ drawAdvancedText(ctx);
1945
+ ctx.globalCompositeOperation = "multiply";
1946
+ drawAdvancedGeometry(ctx);
1947
+ ctx.globalCompositeOperation = "source-over";
1948
+ compositeHash = hash32(canvas.toDataURL("image/png"));
1949
+ }
1777
1950
  const endTime = performance.now();
1778
1951
  const result = {
1779
1952
  text: textHash,
@@ -1782,7 +1955,7 @@ async function getCanvasFingerprint() {
1782
1955
  isInconsistent: inconsistencyAnalysis.isInconsistent,
1783
1956
  // NEW FIELDS
1784
1957
  subPixelAnalysis, // Sub-pixel characteristics for GPU differentiation
1785
- compositeHash, // Combined text+geometry hash
1958
+ compositeHash, // Combined text+geometry hash (consensus if noisy browser)
1786
1959
  inconsistencyConfidence: inconsistencyAnalysis.confidence,
1787
1960
  blockingReasons: inconsistencyAnalysis.reasons,
1788
1961
  };
@@ -2666,6 +2839,28 @@ function createProgram(gl, vertexShader, fragmentShader) {
2666
2839
  }
2667
2840
  return program;
2668
2841
  }
2842
+ /**
2843
+ * Read pixels from a WebGL context and return them as a fresh ImageData-like array.
2844
+ */
2845
+ function readWebGLPixels(gl) {
2846
+ const pixels = new Uint8Array(gl.canvas.width * gl.canvas.height * 4);
2847
+ gl.readPixels(0, 0, gl.canvas.width, gl.canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
2848
+ return pixels;
2849
+ }
2850
+ /**
2851
+ * Hash a Uint8Array of pixel data into a hex string.
2852
+ */
2853
+ function hashPixels(pixels) {
2854
+ let hash = 0;
2855
+ for (let i = 0; i < pixels.length; i += 4) {
2856
+ const r = (pixels[i] ?? 0);
2857
+ const g = (pixels[i + 1] ?? 0) * 256;
2858
+ const b = (pixels[i + 2] ?? 0) * 65536;
2859
+ const a = (pixels[i + 3] ?? 0) * 16777216;
2860
+ hash = (hash * 33 + r + g + b + a) >>> 0;
2861
+ }
2862
+ return hash.toString(16);
2863
+ }
2669
2864
  /**
2670
2865
  * Render 3D scene to generate GPU-specific hash
2671
2866
  * Based on FingerprintJS methodology for maximum differentiation
@@ -2757,26 +2952,67 @@ function render3DScene(gl) {
2757
2952
  gl.clear(gl.COLOR_BUFFER_BIT);
2758
2953
  // Draw triangles
2759
2954
  gl.drawArrays(gl.TRIANGLES, 0, 6);
2760
- // Read pixels to generate hash
2761
- const pixels = new Uint8Array(gl.canvas.width * gl.canvas.height * 4);
2762
- gl.readPixels(0, 0, gl.canvas.width, gl.canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
2763
- // Generate hash from rendered pixels
2764
- let hash = 0;
2765
- for (let i = 0; i < pixels.length; i += 4) {
2766
- // Combine RGBA values with different weights (with null checks)
2767
- const r = (pixels[i] ?? 0) * 1;
2768
- const g = (pixels[i + 1] ?? 0) * 256;
2769
- const b = (pixels[i + 2] ?? 0) * 65536;
2770
- const a = (pixels[i + 3] ?? 0) * 16777216;
2771
- const pixelValue = r + g + b + a;
2772
- hash = (hash * 33 + pixelValue) >>> 0; // Use unsigned 32-bit arithmetic
2955
+ // ANTI-RANDOMIZATION: If browser injects per-pixel noise (Brave/Samsung),
2956
+ // render on separate canvases and take per-channel consensus before hashing.
2957
+ let renderHash;
2958
+ const needsConsensus = isBrave() || isSamsungInternet$1();
2959
+ if (needsConsensus) {
2960
+ const w = gl.canvas.width;
2961
+ const h = gl.canvas.height;
2962
+ const pixelsArr = [readWebGLPixels(gl)];
2963
+ // Render 2 more times on separate canvases (each needs its own GL resources)
2964
+ for (let run = 0; run < 2; run++) {
2965
+ const c = document.createElement("canvas");
2966
+ c.width = w;
2967
+ c.height = h;
2968
+ const g = c.getContext("webgl");
2969
+ if (!g)
2970
+ continue;
2971
+ const vSrc = vertexShaderSource;
2972
+ const fSrc = fragmentShaderSource;
2973
+ const vs = createShader(g, g.VERTEX_SHADER, vSrc);
2974
+ const fs = createShader(g, g.FRAGMENT_SHADER, fSrc);
2975
+ if (!vs || !fs)
2976
+ continue;
2977
+ const prog = createProgram(g, vs, fs);
2978
+ if (!prog)
2979
+ continue;
2980
+ g.useProgram(prog);
2981
+ const buf = g.createBuffer();
2982
+ g.bindBuffer(g.ARRAY_BUFFER, buf);
2983
+ g.bufferData(g.ARRAY_BUFFER, vertices, g.STATIC_DRAW);
2984
+ const posLoc = g.getAttribLocation(prog, "a_position");
2985
+ const colLoc = g.getAttribLocation(prog, "a_color");
2986
+ const timeLoc = g.getUniformLocation(prog, "u_time");
2987
+ g.enableVertexAttribArray(posLoc);
2988
+ g.enableVertexAttribArray(colLoc);
2989
+ g.vertexAttribPointer(posLoc, 2, g.FLOAT, false, 20, 0);
2990
+ g.vertexAttribPointer(colLoc, 3, g.FLOAT, false, 20, 8);
2991
+ g.uniform1f(timeLoc, 1.23456789);
2992
+ g.enable(g.BLEND);
2993
+ g.blendFunc(g.SRC_ALPHA, g.ONE_MINUS_SRC_ALPHA);
2994
+ g.clearColor(0.1, 0.2, 0.3, 1.0);
2995
+ g.clear(g.COLOR_BUFFER_BIT);
2996
+ g.drawArrays(g.TRIANGLES, 0, 6);
2997
+ pixelsArr.push(readWebGLPixels(g));
2998
+ g.deleteProgram(prog);
2999
+ g.deleteShader(vs);
3000
+ g.deleteShader(fs);
3001
+ g.deleteBuffer(buf);
3002
+ }
3003
+ const images = pixelsArr.map((p) => new ImageData(new Uint8ClampedArray(p), w, h));
3004
+ const consensus = getCommonPixels(images, w, h);
3005
+ renderHash = hashPixels(new Uint8Array(consensus.data));
3006
+ }
3007
+ else {
3008
+ renderHash = hashPixels(readWebGLPixels(gl));
2773
3009
  }
2774
3010
  // Cleanup
2775
3011
  gl.deleteProgram(program);
2776
3012
  gl.deleteShader(vertexShader);
2777
3013
  gl.deleteShader(fragmentShader);
2778
3014
  gl.deleteBuffer(buffer);
2779
- return hash.toString(16);
3015
+ return renderHash;
2780
3016
  }
2781
3017
  catch (error) {
2782
3018
  // Fallback to basic parameters hash if rendering fails
@@ -3368,6 +3604,30 @@ function getColorScheme() {
3368
3604
  return 'unknown';
3369
3605
  }
3370
3606
  }
3607
+ function getScriptingSupport() {
3608
+ try {
3609
+ for (const v of ['enabled', 'initial-only', 'none']) {
3610
+ if (safeMatchMedia(`(scripting: ${v})`)?.matches)
3611
+ return v;
3612
+ }
3613
+ return 'unknown';
3614
+ }
3615
+ catch {
3616
+ return 'unknown';
3617
+ }
3618
+ }
3619
+ function getUpdateFrequency() {
3620
+ try {
3621
+ for (const v of ['fast', 'slow', 'none']) {
3622
+ if (safeMatchMedia(`(update: ${v})`)?.matches)
3623
+ return v;
3624
+ }
3625
+ return 'unknown';
3626
+ }
3627
+ catch {
3628
+ return 'unknown';
3629
+ }
3630
+ }
3371
3631
  function getAccessibilityFingerprint() {
3372
3632
  // Original properties
3373
3633
  const colorGamut = getColorGamut$1();
@@ -3388,6 +3648,9 @@ function getAccessibilityFingerprint() {
3388
3648
  const anyHover = getAnyHover();
3389
3649
  const anyPointer = getAnyPointer();
3390
3650
  const colorScheme = getColorScheme();
3651
+ // ThumbmarkJS-inspired additions
3652
+ const scripting = getScriptingSupport();
3653
+ const updateFrequency = getUpdateFrequency();
3391
3654
  // Determine if user has accessibility features enabled
3392
3655
  const hasAccessibilityFeatures = Boolean(forcedColors ||
3393
3656
  reducedMotion ||
@@ -3434,6 +3697,9 @@ function getAccessibilityFingerprint() {
3434
3697
  anyHover,
3435
3698
  anyPointer,
3436
3699
  colorScheme,
3700
+ // ThumbmarkJS-inspired additions
3701
+ scripting,
3702
+ updateFrequency,
3437
3703
  // Computed properties
3438
3704
  hasAccessibilityFeatures,
3439
3705
  displayCapabilities,
@@ -3526,6 +3792,8 @@ var accessibility = /*#__PURE__*/Object.freeze({
3526
3792
  getReducedData: getReducedData,
3527
3793
  getReducedMotion: getReducedMotion,
3528
3794
  getReducedTransparency: getReducedTransparency,
3795
+ getScriptingSupport: getScriptingSupport,
3796
+ getUpdateFrequency: getUpdateFrequency,
3529
3797
  isAccessibilityDetectionAvailable: isAccessibilityDetectionAvailable
3530
3798
  });
3531
3799
 
@@ -4813,6 +5081,15 @@ function getEnhancedFontFingerprintHash(fingerprint) {
4813
5081
  * @see https://bugzilla.mozilla.org/show_bug.cgi?id=531915
4814
5082
  */
4815
5083
  const M = Math; // Minification optimization
5084
+ // Midpoint-rule numerical integrator — same as ThumbmarkJS
5085
+ function integrate(f, a, b, n) {
5086
+ const h = (b - a) / n;
5087
+ let sum = 0;
5088
+ for (let i = 0; i < n; i++) {
5089
+ sum += f(a + (i + 0.5) * h);
5090
+ }
5091
+ return sum * h;
5092
+ }
4816
5093
  const fallbackFn = () => 0;
4817
5094
  /**
4818
5095
  * Enhanced math fingerprinting with additional precision tests
@@ -4914,7 +5191,16 @@ function getMathFingerprint$1() {
4914
5191
  float64: testFloat64(),
4915
5192
  bigNumbers: testBigNumbers(),
4916
5193
  smallNumbers: testSmallNumbers(),
4917
- }
5194
+ },
5195
+ // Large-number edge cases — vary by engine (V8 vs SpiderMonkey vs JavaScriptCore)
5196
+ largeCos: M.cos(1e20),
5197
+ largeSin: M.sin(1e20),
5198
+ largeTan: M.tan(1e20),
5199
+ // Accumulated precision differences over 97 midpoints
5200
+ integratedAsin: integrate(M.asin, -1, 1, 97),
5201
+ integratedCos: integrate(M.cos, 0, M.PI, 97),
5202
+ integratedSin: integrate(M.sin, -M.PI, M.PI, 97),
5203
+ integratedTan: integrate(M.tan, 0, 2 * M.PI, 97),
4918
5204
  };
4919
5205
  }
4920
5206
  /**
@@ -4983,6 +5269,1161 @@ function analyzeMathFingerprint(mathData) {
4983
5269
  };
4984
5270
  }
4985
5271
 
5272
+ /**
5273
+ * WebRTC Fingerprinting
5274
+ * Based on ThumbmarkJS WebRTC component
5275
+ *
5276
+ * WebRTC is one of the richest sources of entropy: the SDP offer exposes
5277
+ * codec lists, RTP extensions and ICE candidate type, which vary by browser
5278
+ * version, OS and hardware in a stable, non-randomizable way.
5279
+ */
5280
+ /**
5281
+ * Extract codec descriptions from a WebRTC SDP string for a given media type.
5282
+ */
5283
+ function parseCodecs(sdp, mediaType) {
5284
+ const match = sdp.match(new RegExp(`m=${mediaType} [^\\s]+ [^\\s]+ ([^\\n\\r]+)`));
5285
+ const descriptors = match ? match[1].split(" ") : [];
5286
+ return descriptors
5287
+ .map((descriptor) => {
5288
+ const matcher = new RegExp(`(rtpmap|fmtp|rtcp-fb):${descriptor} (.+)`, "g");
5289
+ const matches = [...sdp.matchAll(matcher)];
5290
+ if (!matches.length)
5291
+ return null;
5292
+ const desc = {};
5293
+ for (const m of matches) {
5294
+ const type = m[1];
5295
+ const data = m[2];
5296
+ const parts = data.split("/");
5297
+ if (type === "rtpmap") {
5298
+ desc.mimeType = `${mediaType}/${parts[0]}`;
5299
+ desc.clockRate = Number(parts[1]);
5300
+ if (mediaType === "audio")
5301
+ desc.channels = Number(parts[2]) || 1;
5302
+ }
5303
+ else if (type === "rtcp-fb") {
5304
+ desc.feedbackSupport ??
5305
+ (desc.feedbackSupport = []);
5306
+ desc.feedbackSupport.push(data);
5307
+ }
5308
+ else if (type === "fmtp") {
5309
+ desc.sdpFmtpLine = data;
5310
+ }
5311
+ }
5312
+ return desc;
5313
+ })
5314
+ .filter(Boolean);
5315
+ }
5316
+ /**
5317
+ * Collect WebRTC fingerprint by creating an offer and analysing its SDP.
5318
+ * Timeout defaults to 90% of global fingerprint timeout (≈4500 ms).
5319
+ */
5320
+ async function getWebRTCFingerprint(timeout = 4500) {
5321
+ const startTime = performance.now();
5322
+ return new Promise((resolve) => {
5323
+ try {
5324
+ const RTC = window.RTCPeerConnection ||
5325
+ window.webkitRTCPeerConnection ||
5326
+ window.mozRTCPeerConnection;
5327
+ if (!RTC) {
5328
+ resolve({
5329
+ value: {
5330
+ supported: false,
5331
+ extensionsHash: "unsupported",
5332
+ audio: { count: 0, hash: "unsupported" },
5333
+ video: { count: 0, hash: "unsupported" },
5334
+ candidateType: null,
5335
+ },
5336
+ duration: performance.now() - startTime,
5337
+ });
5338
+ return;
5339
+ }
5340
+ const pc = new RTC({
5341
+ iceCandidatePoolSize: 1,
5342
+ iceServers: [],
5343
+ });
5344
+ pc.createDataChannel("");
5345
+ (async () => {
5346
+ try {
5347
+ const offer = await pc.createOffer({
5348
+ offerToReceiveAudio: true,
5349
+ offerToReceiveVideo: true,
5350
+ });
5351
+ await pc.setLocalDescription(offer);
5352
+ const sdp = offer.sdp ?? "";
5353
+ // RTP extensions — sorted for stability
5354
+ const extensions = [
5355
+ ...new Set((sdp.match(/extmap:\d+ [^\n\r]+/g) ?? []).map((x) => x.replace(/extmap:\d+ /, ""))),
5356
+ ].sort();
5357
+ const audioCodecs = parseCodecs(sdp, "audio");
5358
+ const videoCodecs = parseCodecs(sdp, "video");
5359
+ const partialResult = {
5360
+ extensionsHash: hash32(stableStringify(extensions)),
5361
+ audio: {
5362
+ count: audioCodecs.length,
5363
+ hash: hash32(stableStringify(audioCodecs)),
5364
+ },
5365
+ video: {
5366
+ count: videoCodecs.length,
5367
+ hash: hash32(stableStringify(videoCodecs)),
5368
+ },
5369
+ };
5370
+ // Wait for first ICE candidate (reveals candidateType)
5371
+ const iceTimeout = Math.floor(timeout * 0.9);
5372
+ const candidateType = await new Promise((resolveIce) => {
5373
+ const t = setTimeout(() => {
5374
+ pc.removeEventListener("icecandidate", onIce);
5375
+ pc.close();
5376
+ resolveIce(null);
5377
+ }, iceTimeout);
5378
+ const onIce = (evt) => {
5379
+ if (!evt.candidate?.candidate)
5380
+ return;
5381
+ clearTimeout(t);
5382
+ pc.removeEventListener("icecandidate", onIce);
5383
+ pc.close();
5384
+ resolveIce(evt.candidate.type ?? null);
5385
+ };
5386
+ pc.addEventListener("icecandidate", onIce);
5387
+ });
5388
+ resolve({
5389
+ value: {
5390
+ supported: true,
5391
+ ...partialResult,
5392
+ candidateType,
5393
+ timedOut: candidateType === null,
5394
+ },
5395
+ duration: performance.now() - startTime,
5396
+ });
5397
+ }
5398
+ catch (err) {
5399
+ pc.close();
5400
+ resolve({
5401
+ value: {
5402
+ supported: true,
5403
+ extensionsHash: "offer-error",
5404
+ audio: { count: 0, hash: "offer-error" },
5405
+ video: { count: 0, hash: "offer-error" },
5406
+ candidateType: null,
5407
+ },
5408
+ duration: performance.now() - startTime,
5409
+ error: err instanceof Error ? err.message : "WebRTC offer failed",
5410
+ });
5411
+ }
5412
+ })();
5413
+ }
5414
+ catch (err) {
5415
+ resolve({
5416
+ value: {
5417
+ supported: false,
5418
+ extensionsHash: "error",
5419
+ audio: { count: 0, hash: "error" },
5420
+ video: { count: 0, hash: "error" },
5421
+ candidateType: null,
5422
+ },
5423
+ duration: performance.now() - startTime,
5424
+ error: err instanceof Error ? err.message : "WebRTC error",
5425
+ });
5426
+ }
5427
+ });
5428
+ }
5429
+ /**
5430
+ * Check if WebRTC fingerprinting is available in this environment.
5431
+ */
5432
+ function isWebRTCAvailable() {
5433
+ try {
5434
+ return !!(window.RTCPeerConnection ||
5435
+ window.webkitRTCPeerConnection ||
5436
+ window.mozRTCPeerConnection);
5437
+ }
5438
+ catch {
5439
+ return false;
5440
+ }
5441
+ }
5442
+
5443
+ /**
5444
+ * Permissions API Fingerprinting
5445
+ * Based on ThumbmarkJS permissions component
5446
+ *
5447
+ * Permission states (granted / denied / prompt) vary by browser, OS version,
5448
+ * site origin and user profile in a stable way — 25 signals for low cost.
5449
+ * To cancel out race conditions the query is run 3× in parallel and the
5450
+ * most-frequent value per permission is selected.
5451
+ */
5452
+ // 25-item list from ThumbmarkJS
5453
+ const PERMISSION_KEYS = [
5454
+ "accelerometer",
5455
+ "accessibility",
5456
+ "accessibility-events",
5457
+ "ambient-light-sensor",
5458
+ "background-fetch",
5459
+ "background-sync",
5460
+ "bluetooth",
5461
+ "camera",
5462
+ "clipboard-read",
5463
+ "clipboard-write",
5464
+ "device-info",
5465
+ "display-capture",
5466
+ "geolocation",
5467
+ "gyroscope",
5468
+ "local-fonts",
5469
+ "magnetometer",
5470
+ "microphone",
5471
+ "midi",
5472
+ "nfc",
5473
+ "notifications",
5474
+ "payment-handler",
5475
+ "persistent-storage",
5476
+ "push",
5477
+ "speaker",
5478
+ "storage-access",
5479
+ "top-level-storage-access",
5480
+ "window-management",
5481
+ ];
5482
+ /**
5483
+ * Query all permissions once and return the result map.
5484
+ */
5485
+ async function queryPermissionsOnce() {
5486
+ const result = {};
5487
+ await Promise.allSettled(PERMISSION_KEYS.map(async (key) => {
5488
+ try {
5489
+ const status = await navigator.permissions.query({
5490
+ name: key,
5491
+ });
5492
+ result[key] = status.state;
5493
+ }
5494
+ catch {
5495
+ // Unsupported permission — omit
5496
+ }
5497
+ }));
5498
+ return result;
5499
+ }
5500
+ /**
5501
+ * Return the most-frequent string value across an array.
5502
+ */
5503
+ function mostFrequent(values) {
5504
+ if (values.length === 0)
5505
+ return "unknown";
5506
+ const freq = {};
5507
+ for (const v of values)
5508
+ freq[v] = (freq[v] ?? 0) + 1;
5509
+ let best = values[0];
5510
+ for (const k in freq) {
5511
+ if (freq[k] > freq[best])
5512
+ best = k;
5513
+ }
5514
+ return best;
5515
+ }
5516
+ /**
5517
+ * Collect Permissions fingerprint.
5518
+ * Runs 3 parallel queries and returns the per-permission consensus to
5519
+ * eliminate any transient state fluctuations.
5520
+ */
5521
+ async function getPermissionsFingerprint() {
5522
+ const startTime = performance.now();
5523
+ try {
5524
+ if (!navigator?.permissions?.query) {
5525
+ return {
5526
+ value: {},
5527
+ duration: performance.now() - startTime,
5528
+ error: "Permissions API not available",
5529
+ };
5530
+ }
5531
+ // Run 3 times in parallel for consensus
5532
+ const runs = await Promise.all([
5533
+ queryPermissionsOnce(),
5534
+ queryPermissionsOnce(),
5535
+ queryPermissionsOnce(),
5536
+ ]);
5537
+ // Merge: pick most frequent value per key
5538
+ const merged = {};
5539
+ for (const key of PERMISSION_KEYS) {
5540
+ const values = runs
5541
+ .map((r) => r[key])
5542
+ .filter((v) => v !== undefined);
5543
+ if (values.length > 0) {
5544
+ merged[key] = mostFrequent(values);
5545
+ }
5546
+ }
5547
+ return {
5548
+ value: merged,
5549
+ duration: performance.now() - startTime,
5550
+ };
5551
+ }
5552
+ catch (err) {
5553
+ return {
5554
+ value: {},
5555
+ duration: performance.now() - startTime,
5556
+ error: err instanceof Error ? err.message : "Permissions fingerprinting failed",
5557
+ };
5558
+ }
5559
+ }
5560
+ /**
5561
+ * Check if Permissions API fingerprinting is available.
5562
+ */
5563
+ function isPermissionsAvailable() {
5564
+ try {
5565
+ return (typeof navigator !== "undefined" &&
5566
+ typeof navigator.permissions?.query === "function");
5567
+ }
5568
+ catch {
5569
+ return false;
5570
+ }
5571
+ }
5572
+
5573
+ /**
5574
+ * Speech Synthesis Fingerprinting
5575
+ * Based on ThumbmarkJS speech component
5576
+ *
5577
+ * The list of system voices (Speech Synthesis API) is stable per OS/version/locale
5578
+ * and provides extra entropy at zero user-visible cost.
5579
+ * Voices may load asynchronously, so we listen for `voiceschanged` with an 800 ms fallback.
5580
+ */
5581
+ const VOICE_LOAD_TIMEOUT_MS = 800;
5582
+ /**
5583
+ * Escape commas and backslashes inside a voice property string.
5584
+ */
5585
+ function escapeValue(v) {
5586
+ return v.replace(/\\/g, "\\\\").replace(/,/g, "\\,");
5587
+ }
5588
+ /**
5589
+ * Build a canonical sorted signature array from a list of voices.
5590
+ */
5591
+ function buildSignatures(voices) {
5592
+ return voices
5593
+ .map((v) => [
5594
+ escapeValue(v.voiceURI ?? ""),
5595
+ escapeValue(v.name ?? ""),
5596
+ escapeValue(v.lang ?? ""),
5597
+ v.localService ? "1" : "0",
5598
+ v.default ? "1" : "0",
5599
+ ].join(","))
5600
+ .sort();
5601
+ }
5602
+ /**
5603
+ * Collect Speech Synthesis fingerprint.
5604
+ * Experimental: not included in `stableCoreHash` by default.
5605
+ */
5606
+ async function getSpeechFingerprint() {
5607
+ const startTime = performance.now();
5608
+ return new Promise((resolve) => {
5609
+ try {
5610
+ if (typeof window === "undefined" ||
5611
+ !window.speechSynthesis ||
5612
+ typeof window.speechSynthesis.getVoices !== "function") {
5613
+ resolve({
5614
+ value: { supported: false, voiceCount: 0, voicesHash: "unsupported" },
5615
+ duration: performance.now() - startTime,
5616
+ error: "Speech Synthesis API not supported",
5617
+ });
5618
+ return;
5619
+ }
5620
+ let resolved = false;
5621
+ let timeoutHandle = null;
5622
+ const processVoices = (voices) => {
5623
+ if (resolved)
5624
+ return;
5625
+ resolved = true;
5626
+ if (timeoutHandle)
5627
+ clearTimeout(timeoutHandle);
5628
+ try {
5629
+ const signatures = buildSignatures(voices);
5630
+ resolve({
5631
+ value: {
5632
+ supported: true,
5633
+ voiceCount: voices.length,
5634
+ voicesHash: hash32(stableStringify(signatures)),
5635
+ },
5636
+ duration: performance.now() - startTime,
5637
+ });
5638
+ }
5639
+ catch (err) {
5640
+ resolve({
5641
+ value: { supported: true, voiceCount: 0, voicesHash: "error" },
5642
+ duration: performance.now() - startTime,
5643
+ error: err instanceof Error ? err.message : "Voice processing failed",
5644
+ });
5645
+ }
5646
+ };
5647
+ // Immediate attempt (works in Chrome)
5648
+ const voices = window.speechSynthesis.getVoices();
5649
+ if (voices.length > 0) {
5650
+ processVoices(voices);
5651
+ return;
5652
+ }
5653
+ // Fallback timeout (Firefox/Safari load voices asynchronously)
5654
+ timeoutHandle = setTimeout(() => {
5655
+ processVoices(window.speechSynthesis.getVoices());
5656
+ }, VOICE_LOAD_TIMEOUT_MS);
5657
+ // voiceschanged event (most browsers fire this when voices are ready)
5658
+ const onChanged = () => {
5659
+ window.speechSynthesis.removeEventListener("voiceschanged", onChanged);
5660
+ processVoices(window.speechSynthesis.getVoices());
5661
+ };
5662
+ window.speechSynthesis.addEventListener("voiceschanged", onChanged);
5663
+ }
5664
+ catch (err) {
5665
+ resolve({
5666
+ value: { supported: false, voiceCount: 0, voicesHash: "error" },
5667
+ duration: performance.now() - startTime,
5668
+ error: err instanceof Error ? err.message : "Speech synthesis error",
5669
+ });
5670
+ }
5671
+ });
5672
+ }
5673
+ /**
5674
+ * Check if Speech Synthesis fingerprinting is available.
5675
+ */
5676
+ function isSpeechAvailable() {
5677
+ try {
5678
+ return (typeof window !== "undefined" &&
5679
+ typeof window.speechSynthesis?.getVoices === "function");
5680
+ }
5681
+ catch {
5682
+ return false;
5683
+ }
5684
+ }
5685
+
5686
+ /**
5687
+ * Stabilization Rules for Fingerprint Components
5688
+ * Based on ThumbmarkJS stabilizationExclusionRules
5689
+ *
5690
+ * Certain components produce noisy / randomized output in specific browser
5691
+ * contexts (private mode, iframes, always-noisy browsers). This module defines
5692
+ * which components to exclude per context to maximize hash stability.
5693
+ */
5694
+ /** Port of ThumbmarkJS stabilizationExclusionRules. */
5695
+ const stabilizationRules = {
5696
+ private: [
5697
+ { exclude: ["canvas"], browsers: ["firefox", "safari", "brave"] },
5698
+ { exclude: ["audio"], browsers: ["safari", "samsung"] },
5699
+ { exclude: ["fonts"], browsers: ["firefox"] },
5700
+ {
5701
+ exclude: ["hardware", "pluginsEnhanced"],
5702
+ browsers: ["brave"],
5703
+ },
5704
+ ],
5705
+ iframe: [
5706
+ { exclude: ["system"], browsers: ["safari"] },
5707
+ { exclude: ["permissions"] }, // All browsers
5708
+ ],
5709
+ always: [
5710
+ { exclude: ["speech"], browsers: ["brave", "firefox"] },
5711
+ ],
5712
+ };
5713
+ /**
5714
+ * Detect the current browser name for rule matching.
5715
+ */
5716
+ function getCurrentBrowser() {
5717
+ try {
5718
+ if (isBrave())
5719
+ return "brave";
5720
+ if (isFirefox())
5721
+ return "firefox";
5722
+ if (isSafari())
5723
+ return "safari";
5724
+ if (isSamsungInternet$1())
5725
+ return "samsung";
5726
+ // Edge / Chrome fall through as "other" — rules that target them can be added later
5727
+ return "other";
5728
+ }
5729
+ catch {
5730
+ return "other";
5731
+ }
5732
+ }
5733
+ /**
5734
+ * Detect whether the page is running inside an iframe.
5735
+ */
5736
+ function isInIframe() {
5737
+ try {
5738
+ return window !== window.top;
5739
+ }
5740
+ catch {
5741
+ return true; // Cross-origin frame access throws — treat as iframe
5742
+ }
5743
+ }
5744
+ /**
5745
+ * Given the incognito likelihood (0-1) from `incognitoDetection`, decide
5746
+ * whether to treat the session as private mode.
5747
+ */
5748
+ function isLikelyPrivate(incognitoLikelihood) {
5749
+ // Threshold: probability ≥ 0.6 → apply private-mode exclusions
5750
+ return (incognitoLikelihood ?? 0) >= 0.6;
5751
+ }
5752
+ /**
5753
+ * Returns the set of component keys that should be nulled out / excluded from
5754
+ * hashing given the current context.
5755
+ *
5756
+ * @param incognitoLikelihood - Output of incognito detection (0-1). Pass 0 if unavailable.
5757
+ */
5758
+ function getExcludedComponents(incognitoLikelihood) {
5759
+ const browser = getCurrentBrowser();
5760
+ const excluded = new Set();
5761
+ const applyRules = (rules) => {
5762
+ for (const rule of rules) {
5763
+ if (!rule.browsers ||
5764
+ rule.browsers.includes(browser)) {
5765
+ for (const key of rule.exclude) {
5766
+ excluded.add(key);
5767
+ }
5768
+ }
5769
+ }
5770
+ };
5771
+ // Always-active exclusions
5772
+ applyRules(stabilizationRules.always);
5773
+ // Iframe exclusions
5774
+ if (isInIframe()) {
5775
+ applyRules(stabilizationRules.iframe);
5776
+ }
5777
+ // Private-mode exclusions
5778
+ if (isLikelyPrivate(incognitoLikelihood)) {
5779
+ applyRules(stabilizationRules.private);
5780
+ }
5781
+ return excluded;
5782
+ }
5783
+
5784
+ /**
5785
+ * CSS @supports Feature Vector Fingerprinting
5786
+ *
5787
+ * Tests ~40 modern CSS features via CSS.supports() to produce a stable
5788
+ * boolean vector (~50 bits of entropy). Síncrono, zero custo, estável por
5789
+ * versão de browser. Different browsers differ on ~10-15 features, making
5790
+ * this a strong discriminator for browser family + version.
5791
+ */
5792
+ /**
5793
+ * CSS features to test — [key, CSS declaration or condition string]
5794
+ * For selector() and condition-text forms we use the single-argument overload.
5795
+ * For @layer we pass the conditionText form directly.
5796
+ */
5797
+ const CSS_FEATURES = [
5798
+ // Layout moderno
5799
+ ["aspect-ratio", "aspect-ratio: 1"],
5800
+ ["container-type", "container-type: inline-size"],
5801
+ ["content-visibility", "content-visibility: auto"],
5802
+ ["field-sizing", "field-sizing: content"],
5803
+ // CSS Grid avançado
5804
+ ["subgrid", "grid-template-columns: subgrid"],
5805
+ ["masonry", "grid-template-rows: masonry"],
5806
+ // Seletores
5807
+ ["has-selector", "selector(:has(*))"],
5808
+ ["is-selector", "selector(:is(*))"],
5809
+ ["where-selector", "selector(:where(*))"],
5810
+ ["focus-visible", "selector(:focus-visible)"],
5811
+ // Cores modernas
5812
+ ["color-mix", "color: color-mix(in srgb, red, blue)"],
5813
+ ["oklch", "color: oklch(0.5 0.2 200)"],
5814
+ ["relative-color", "color: hsl(from red h s l)"],
5815
+ // Visual
5816
+ ["backdrop-filter", "backdrop-filter: blur(10px)"],
5817
+ ["text-wrap-balance", "text-wrap: balance"],
5818
+ ["text-wrap-pretty", "text-wrap: pretty"],
5819
+ ["overscroll-behavior", "overscroll-behavior: none"],
5820
+ ["scroll-behavior", "scroll-behavior: smooth"],
5821
+ // View transitions
5822
+ ["view-transition", "view-transition-name: auto"],
5823
+ // Animações modernas
5824
+ ["animation-timeline", "animation-timeline: scroll()"],
5825
+ ["animation-range", "animation-range: 10% 90%"],
5826
+ // Scroll-driven
5827
+ ["scroll-timeline", "scroll-timeline: --t block"],
5828
+ // Tipografia
5829
+ ["font-variant-numeric", "font-variant-numeric: oldstyle-nums"],
5830
+ ["hyphenate-character", "hyphenate-character: auto"],
5831
+ ["text-decoration-skip-ink", "text-decoration-skip-ink: auto"],
5832
+ ["hanging-punctuation", "hanging-punctuation: first"],
5833
+ // Transforms
5834
+ ["individual-transform", "translate: 10px"],
5835
+ ["rotate-property", "rotate: 45deg"],
5836
+ // Logical properties
5837
+ ["margin-inline", "margin-inline: auto"],
5838
+ ["inset", "inset: 0"],
5839
+ // Containment
5840
+ ["contain", "contain: layout"],
5841
+ ["contain-intrinsic-size", "contain-intrinsic-size: auto"],
5842
+ // Cascade
5843
+ ["cascade-layers", "@layer"],
5844
+ ["revert-layer", "color: revert-layer"],
5845
+ // Scrollbar
5846
+ ["scrollbar-width", "scrollbar-width: thin"],
5847
+ ["scrollbar-color", "scrollbar-color: red blue"],
5848
+ // Vendor webkit
5849
+ ["webkit-text-stroke", "-webkit-text-stroke: 1px black"],
5850
+ ["webkit-line-clamp", "-webkit-line-clamp: 2"],
5851
+ // Nesting CSS
5852
+ ["css-nesting", "selector(& .child)"],
5853
+ // Forced colors
5854
+ ["forced-colors", "(forced-colors: active)"],
5855
+ ];
5856
+ /**
5857
+ * Test a single CSS feature via CSS.supports().
5858
+ * Returns false on any exception (API absent or invalid syntax).
5859
+ */
5860
+ function testFeature(declaration) {
5861
+ try {
5862
+ // selector(...) and condition-text forms use the single-arg overload
5863
+ if (declaration.startsWith("selector(") ||
5864
+ declaration.startsWith("(") ||
5865
+ declaration.startsWith("@")) {
5866
+ return CSS.supports(declaration);
5867
+ }
5868
+ return CSS.supports(declaration);
5869
+ }
5870
+ catch {
5871
+ return false;
5872
+ }
5873
+ }
5874
+ /**
5875
+ * Produce a simple hex hash from the boolean feature vector.
5876
+ * Uses the feature keys + their boolean values for determinism.
5877
+ */
5878
+ function hashFeatures(features) {
5879
+ const str = stableStringify(features);
5880
+ let h = 0x811c9dc5;
5881
+ for (let i = 0; i < str.length; i++) {
5882
+ h ^= str.charCodeAt(i);
5883
+ h = (Math.imul(h, 0x01000193) | 0) >>> 0;
5884
+ }
5885
+ return h.toString(16).padStart(8, "0");
5886
+ }
5887
+ /**
5888
+ * Collect CSS @supports feature vector fingerprint.
5889
+ */
5890
+ function getCSSFeaturesFingerprint() {
5891
+ const startTime = performance.now();
5892
+ try {
5893
+ if (!isCSSFeaturesAvailable()) {
5894
+ return {
5895
+ value: { features: {}, supportedCount: 0, hash: "" },
5896
+ duration: performance.now() - startTime,
5897
+ error: "CSS.supports not available",
5898
+ };
5899
+ }
5900
+ const features = {};
5901
+ for (const [key, declaration] of CSS_FEATURES) {
5902
+ features[key] = testFeature(declaration);
5903
+ }
5904
+ const supportedCount = Object.values(features).filter(Boolean).length;
5905
+ const hash = hashFeatures(features);
5906
+ return {
5907
+ value: { features, supportedCount, hash },
5908
+ duration: performance.now() - startTime,
5909
+ };
5910
+ }
5911
+ catch (err) {
5912
+ return {
5913
+ value: { features: {}, supportedCount: 0, hash: "" },
5914
+ duration: performance.now() - startTime,
5915
+ error: err instanceof Error ? err.message : "CSS features fingerprinting failed",
5916
+ };
5917
+ }
5918
+ }
5919
+ /**
5920
+ * Check if CSS @supports fingerprinting is available.
5921
+ */
5922
+ function isCSSFeaturesAvailable() {
5923
+ try {
5924
+ return (typeof CSS !== "undefined" && typeof CSS.supports === "function");
5925
+ }
5926
+ catch {
5927
+ return false;
5928
+ }
5929
+ }
5930
+
5931
+ /**
5932
+ * Media Codec Support Matrix Fingerprinting
5933
+ *
5934
+ * Tests video/audio codec support via HTMLMediaElement.canPlayType() and
5935
+ * MediaRecorder.isTypeSupported(). ~20 bits of entropy, stable per OS +
5936
+ * hardware (hardware decoders vary), synchronous, zero cost.
5937
+ */
5938
+ // canPlayType — vídeo
5939
+ const VIDEO_TYPES = [
5940
+ 'video/mp4; codecs="avc1.42E01E"', // H.264 Baseline
5941
+ 'video/mp4; codecs="avc1.4D001E"', // H.264 Main
5942
+ 'video/mp4; codecs="hev1.1.6.L93.B0"', // H.265/HEVC
5943
+ 'video/mp4; codecs="vp09.00.10.08"', // VP9 in MP4
5944
+ 'video/mp4; codecs="av01.0.01M.08"', // AV1 in MP4
5945
+ 'video/webm; codecs="vp8"',
5946
+ 'video/webm; codecs="vp9"',
5947
+ 'video/webm; codecs="av01.0.01M.08"', // AV1 in WebM
5948
+ 'video/ogg; codecs="theora"',
5949
+ ];
5950
+ // canPlayType — áudio
5951
+ const AUDIO_TYPES = [
5952
+ 'audio/mp4; codecs="mp4a.40.2"', // AAC-LC
5953
+ 'audio/mp4; codecs="mp4a.40.5"', // HE-AAC
5954
+ 'audio/mpeg', // MP3
5955
+ 'audio/ogg; codecs="vorbis"',
5956
+ 'audio/ogg; codecs="opus"',
5957
+ 'audio/webm; codecs="opus"',
5958
+ 'audio/wav; codecs="1"', // PCM
5959
+ 'audio/flac',
5960
+ 'audio/aac',
5961
+ ];
5962
+ // MediaRecorder.isTypeSupported
5963
+ const RECORDER_TYPES = [
5964
+ 'video/webm; codecs="vp8"',
5965
+ 'video/webm; codecs="vp9"',
5966
+ 'video/webm; codecs="av1"',
5967
+ 'video/webm; codecs="vp9,opus"',
5968
+ 'audio/webm; codecs="opus"',
5969
+ 'audio/ogg; codecs="opus"',
5970
+ 'video/mp4; codecs="avc1"',
5971
+ ];
5972
+ /**
5973
+ * Produce a simple FNV-1a hex hash from the codec matrix.
5974
+ */
5975
+ function hashCodecs(video, audio, recorder) {
5976
+ const str = stableStringify({ video, audio, recorder });
5977
+ let h = 0x811c9dc5;
5978
+ for (let i = 0; i < str.length; i++) {
5979
+ h ^= str.charCodeAt(i);
5980
+ h = (Math.imul(h, 0x01000193) | 0) >>> 0;
5981
+ }
5982
+ return h.toString(16).padStart(8, "0");
5983
+ }
5984
+ /**
5985
+ * Collect media codec support fingerprint.
5986
+ */
5987
+ function getMediaCodecsFingerprint() {
5988
+ const startTime = performance.now();
5989
+ try {
5990
+ if (!isMediaCodecsAvailable()) {
5991
+ return {
5992
+ value: { video: {}, audio: {}, recorder: {}, hash: "" },
5993
+ duration: performance.now() - startTime,
5994
+ error: "document not available",
5995
+ };
5996
+ }
5997
+ // Test video codecs
5998
+ const videoEl = document.createElement("video");
5999
+ const video = {};
6000
+ for (const mime of VIDEO_TYPES) {
6001
+ try {
6002
+ video[mime] = videoEl.canPlayType(mime);
6003
+ }
6004
+ catch {
6005
+ video[mime] = "";
6006
+ }
6007
+ }
6008
+ // Test audio codecs
6009
+ const audioEl = document.createElement("audio");
6010
+ const audio = {};
6011
+ for (const mime of AUDIO_TYPES) {
6012
+ try {
6013
+ audio[mime] = audioEl.canPlayType(mime);
6014
+ }
6015
+ catch {
6016
+ audio[mime] = "";
6017
+ }
6018
+ }
6019
+ // Test MediaRecorder support
6020
+ const recorder = {};
6021
+ const hasRecorder = typeof MediaRecorder !== "undefined" &&
6022
+ typeof MediaRecorder.isTypeSupported === "function";
6023
+ for (const mime of RECORDER_TYPES) {
6024
+ try {
6025
+ recorder[mime] = hasRecorder ? MediaRecorder.isTypeSupported(mime) : false;
6026
+ }
6027
+ catch {
6028
+ recorder[mime] = false;
6029
+ }
6030
+ }
6031
+ const hash = hashCodecs(video, audio, recorder);
6032
+ return {
6033
+ value: { video, audio, recorder, hash },
6034
+ duration: performance.now() - startTime,
6035
+ };
6036
+ }
6037
+ catch (err) {
6038
+ return {
6039
+ value: { video: {}, audio: {}, recorder: {}, hash: "" },
6040
+ duration: performance.now() - startTime,
6041
+ error: err instanceof Error ? err.message : "Media codecs fingerprinting failed",
6042
+ };
6043
+ }
6044
+ }
6045
+ /**
6046
+ * Check if media codec fingerprinting is available.
6047
+ */
6048
+ function isMediaCodecsAvailable() {
6049
+ try {
6050
+ return typeof document !== "undefined";
6051
+ }
6052
+ catch {
6053
+ return false;
6054
+ }
6055
+ }
6056
+
6057
+ /**
6058
+ * WebAssembly Feature Detection Fingerprinting
6059
+ *
6060
+ * Detects ~6–8 WebAssembly proposal flags via WebAssembly.validate() (sync,
6061
+ * does not execute the module). Stable per engine version. ~6 bits of entropy.
6062
+ */
6063
+ // ─── Minimal WASM modules for feature detection ────────────────────────────
6064
+ // Each is the smallest valid module that uses the target feature/opcode.
6065
+ // WebAssembly.validate() returns false (instead of throwing) if invalid,
6066
+ // so any exception = feature not supported.
6067
+ // SIMD: v128.const opcode (0xFD 0x0F)
6068
+ // Module: one function returning v128 constant (all zeros)
6069
+ const SIMD_BYTES = new Uint8Array([
6070
+ 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, // magic + version
6071
+ 0x01, 0x05, 0x01, 0x60, 0x00, 0x01, 0x7b, // type section: () -> v128
6072
+ 0x03, 0x02, 0x01, 0x00, // function section
6073
+ 0x0a, 0x0a, 0x01, 0x08, 0x00, // code section
6074
+ 0xfd, 0x0f, // v128.const
6075
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 16 bytes of immediate
6076
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
6077
+ 0x0b, // end
6078
+ ]);
6079
+ // Bulk memory: memory.copy (0xFC 0x0A)
6080
+ // Module: one function calling memory.copy(0, 0, n)
6081
+ const BULK_MEMORY_BYTES = new Uint8Array([
6082
+ 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, // magic + version
6083
+ 0x01, 0x04, 0x01, 0x60, 0x00, 0x00, // type: () -> ()
6084
+ 0x03, 0x02, 0x01, 0x00, // function section
6085
+ 0x05, 0x03, 0x01, 0x00, 0x00, // memory section (1 page min 0)
6086
+ 0x0a, 0x0c, 0x01, 0x0a, 0x00, // code section
6087
+ 0x41, 0x00, // i32.const 0
6088
+ 0x41, 0x00, // i32.const 0
6089
+ 0x41, 0x00, // i32.const 0
6090
+ 0xfc, 0x0a, 0x00, 0x00, // memory.copy mem_idx=0 mem_idx=0
6091
+ 0x0b, // end
6092
+ ]);
6093
+ // Exception handling: tag section (section id 0x0d)
6094
+ // Module: just a tag section — engines without exception handling won't parse 0x0d
6095
+ const EXCEPTIONS_BYTES = new Uint8Array([
6096
+ 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, // magic + version
6097
+ 0x01, 0x04, 0x01, 0x60, 0x00, 0x00, // type section: () -> ()
6098
+ 0x0d, 0x03, 0x01, 0x00, 0x00, // tag section: 1 tag of type 0
6099
+ ]);
6100
+ // GC: rec type section (0x4E inside type section) — very new, Chrome 119+, FF 120+
6101
+ // Module: minimal module with an empty rec group
6102
+ const GC_BYTES = new Uint8Array([
6103
+ 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, // magic + version
6104
+ 0x01, 0x03, // type section, 3 bytes body
6105
+ 0x01, // 1 entry
6106
+ 0x4e, 0x00, // rec, 0 types in group
6107
+ ]);
6108
+ /**
6109
+ * Safe wrapper around WebAssembly.validate — returns false on any exception.
6110
+ */
6111
+ function validate(bytes) {
6112
+ try {
6113
+ return WebAssembly.validate(bytes);
6114
+ }
6115
+ catch {
6116
+ return false;
6117
+ }
6118
+ }
6119
+ /**
6120
+ * Collect WebAssembly feature flags.
6121
+ */
6122
+ function getWasmFeaturesFingerprint() {
6123
+ const startTime = performance.now();
6124
+ try {
6125
+ const basic = typeof WebAssembly !== "undefined";
6126
+ if (!basic) {
6127
+ const value = {
6128
+ basic: false,
6129
+ validate: false,
6130
+ compile: false,
6131
+ simd: false,
6132
+ threads: false,
6133
+ bulkMemory: false,
6134
+ exceptions: false,
6135
+ gc: false,
6136
+ };
6137
+ return { value, duration: performance.now() - startTime };
6138
+ }
6139
+ const validateAvailable = typeof WebAssembly.validate === "function";
6140
+ const value = {
6141
+ basic: true,
6142
+ validate: validateAvailable,
6143
+ compile: typeof WebAssembly.compile === "function",
6144
+ simd: validateAvailable ? validate(SIMD_BYTES) : false,
6145
+ threads: typeof SharedArrayBuffer !== "undefined",
6146
+ bulkMemory: validateAvailable ? validate(BULK_MEMORY_BYTES) : false,
6147
+ exceptions: validateAvailable ? validate(EXCEPTIONS_BYTES) : false,
6148
+ gc: validateAvailable ? validate(GC_BYTES) : false,
6149
+ };
6150
+ return { value, duration: performance.now() - startTime };
6151
+ }
6152
+ catch (err) {
6153
+ return {
6154
+ value: {
6155
+ basic: false,
6156
+ validate: false,
6157
+ compile: false,
6158
+ simd: false,
6159
+ threads: false,
6160
+ bulkMemory: false,
6161
+ exceptions: false,
6162
+ gc: false,
6163
+ },
6164
+ duration: performance.now() - startTime,
6165
+ error: err instanceof Error ? err.message : "Wasm features fingerprinting failed",
6166
+ };
6167
+ }
6168
+ }
6169
+ /**
6170
+ * Check if WebAssembly feature detection is available.
6171
+ */
6172
+ function isWasmAvailable() {
6173
+ try {
6174
+ return (typeof WebAssembly !== "undefined" &&
6175
+ typeof WebAssembly.validate === "function");
6176
+ }
6177
+ catch {
6178
+ return false;
6179
+ }
6180
+ }
6181
+
6182
+ /**
6183
+ * MathML Rendering Fingerprint
6184
+ *
6185
+ * Measures bounding box dimensions of rendered MathML structures to extract
6186
+ * font-metric entropy (~15 bits). Varies by OS (macOS vs Windows vs Linux),
6187
+ * browser engine (Gecko vs WebKit vs Blink), and system font version.
6188
+ *
6189
+ * Approach: inject hidden <math> elements into document.body, measure, remove.
6190
+ * No iframe needed — simpler than ThumbmarkJS approach.
6191
+ */
6192
+ // Blackboard bold symbols × Greek sub/superscript pairs — from ThumbmarkJS
6193
+ const BB_SYMBOLS = ['𝔸', '𝔹', 'ℂ', '𝔻', '𝔼', '𝔽', '𝔾', 'ℍ', '𝕀', '𝕁', '𝕂', '𝕃'];
6194
+ const GREEK_PAIRS = [
6195
+ ['β', 'ψ'], ['λ', 'ε'], ['ζ', 'α'], ['ξ', 'μ'], ['ρ', 'φ'], ['κ', 'τ'],
6196
+ ['η', 'σ'], ['ι', 'ω'], ['γ', 'ν'], ['χ', 'δ'], ['θ', 'π'], ['υ', 'ο'],
6197
+ ];
6198
+ // Complex structure 4: ∏ munderover wrapping all 12 BB×Greek mmultiscripts
6199
+ const _bbMrow = BB_SYMBOLS.map((b, i) => {
6200
+ const [g1, g2] = GREEK_PAIRS[i];
6201
+ return `<mmultiscripts><mi>${b}</mi><mi>${g1}</mi><mi>${g2}</mi></mmultiscripts>`;
6202
+ }).join('');
6203
+ // MathML structures to render — 16 total, inspired by ThumbmarkJS
6204
+ const MATHML_STRUCTURES = [
6205
+ // 1. Integral with fraction
6206
+ `<math><msubsup><mo>∫</mo><mi>a</mi><mi>b</mi></msubsup><mfrac><mrow><mi>f</mi><mo>(</mo><mi>x</mi><mo>)</mo></mrow><mrow><mi>d</mi><mi>x</mi></mrow></mfrac></math>`,
6207
+ // 2. Fraction with pi and r squared
6208
+ `<math><mfrac><mrow><mi>π</mi><mo>×</mo><msup><mi>r</mi><mn>2</mn></msup></mrow><mrow><mn>4</mn></mrow></mfrac></math>`,
6209
+ // 3. Matrix with Greek letters
6210
+ `<math><mo>[</mo><mtable><mtr><mtd><mi>α</mi></mtd><mtd><mi>β</mi></mtd></mtr><mtr><mtd><mi>γ</mi></mtd><mtd><mi>δ</mi></mtd></mtr></mtable><mo>]</mo></math>`,
6211
+ // 4. ∏ munderover with 12 blackboard bold × Greek mmultiscripts (replaces simple ∑)
6212
+ `<math><munderover><mo>∏</mo><mrow><mi>i</mi><mo>=</mo><mn>1</mn></mrow><mi>n</mi></munderover><mrow>${_bbMrow}</mrow></math>`,
6213
+ // 5–16. Individual blackboard bold symbol with Greek sub/superscript (one per symbol)
6214
+ ...BB_SYMBOLS.map((b, i) => {
6215
+ const [g1, g2] = GREEK_PAIRS[i];
6216
+ return `<math><mmultiscripts><mi>${b}</mi><mi>${g1}</mi><mi>${g2}</mi></mmultiscripts></math>`;
6217
+ }),
6218
+ ];
6219
+ // Font style properties to capture from the root <math> element
6220
+ const FONT_STYLE_PROPS = [
6221
+ "fontFamily",
6222
+ "fontSize",
6223
+ "fontWeight",
6224
+ "fontStyle",
6225
+ "lineHeight",
6226
+ "fontVariant",
6227
+ "fontStretch",
6228
+ "fontKerning",
6229
+ "fontFeatureSettings",
6230
+ "fontVariantNumeric",
6231
+ ];
6232
+ /**
6233
+ * FNV-1a hash over a JSON string.
6234
+ */
6235
+ function fnv1a(str) {
6236
+ let h = 0x811c9dc5;
6237
+ for (let i = 0; i < str.length; i++) {
6238
+ h ^= str.charCodeAt(i);
6239
+ h = (Math.imul(h, 0x01000193) | 0) >>> 0;
6240
+ }
6241
+ return h.toString(16).padStart(8, "0");
6242
+ }
6243
+ /**
6244
+ * Check if the browser renders MathML natively by measuring a trivial element.
6245
+ */
6246
+ function isMathMLRendered() {
6247
+ try {
6248
+ const container = document.createElement("div");
6249
+ container.style.cssText = "position:absolute;visibility:hidden;top:-9999px;left:-9999px";
6250
+ container.innerHTML = "<math><mrow><mi>x</mi></mrow></math>";
6251
+ document.body.appendChild(container);
6252
+ const math = container.querySelector("math");
6253
+ const rect = math?.getBoundingClientRect();
6254
+ document.body.removeChild(container);
6255
+ return !!(rect && rect.width > 0 && rect.height > 0);
6256
+ }
6257
+ catch {
6258
+ return false;
6259
+ }
6260
+ }
6261
+ /**
6262
+ * Render a MathML string in a hidden container, measure dimensions, then remove.
6263
+ */
6264
+ function measureMathML(mathHtml) {
6265
+ const container = document.createElement("div");
6266
+ container.style.cssText =
6267
+ "position:absolute;visibility:hidden;top:-9999px;left:-9999px;font-size:16px";
6268
+ container.innerHTML = mathHtml;
6269
+ document.body.appendChild(container);
6270
+ const math = container.querySelector("math");
6271
+ const rect = math?.getBoundingClientRect() ?? { width: 0, height: 0 };
6272
+ document.body.removeChild(container);
6273
+ return {
6274
+ width: Math.round(rect.width * 100) / 100,
6275
+ height: Math.round(rect.height * 100) / 100,
6276
+ };
6277
+ }
6278
+ /**
6279
+ * Capture computedStyle font properties of the <math> element.
6280
+ */
6281
+ function captureFontStyle() {
6282
+ const container = document.createElement("div");
6283
+ container.style.cssText =
6284
+ "position:absolute;visibility:hidden;top:-9999px;left:-9999px;font-size:16px";
6285
+ container.innerHTML = "<math><mi>x</mi></math>";
6286
+ document.body.appendChild(container);
6287
+ const math = container.querySelector("math");
6288
+ const style = math ? window.getComputedStyle(math) : null;
6289
+ const result = {};
6290
+ for (const prop of FONT_STYLE_PROPS) {
6291
+ result[prop] = style ? style[prop] ?? "" : "";
6292
+ }
6293
+ document.body.removeChild(container);
6294
+ return result;
6295
+ }
6296
+ /**
6297
+ * Collect MathML rendering fingerprint.
6298
+ * Returns `supported: false` (without error) in browsers that don't render MathML.
6299
+ */
6300
+ async function getMathMLFingerprint() {
6301
+ const startTime = performance.now();
6302
+ try {
6303
+ if (!isMathMLAvailable()) {
6304
+ return {
6305
+ value: { supported: false, dimensionsHash: "", fontStyleHash: "" },
6306
+ duration: performance.now() - startTime,
6307
+ error: "document not available",
6308
+ };
6309
+ }
6310
+ if (!isMathMLRendered()) {
6311
+ return {
6312
+ value: { supported: false, dimensionsHash: "", fontStyleHash: "" },
6313
+ duration: performance.now() - startTime,
6314
+ };
6315
+ }
6316
+ // Measure all structures
6317
+ const dimensions = MATHML_STRUCTURES.map((html) => measureMathML(html));
6318
+ const dimensionsHash = fnv1a(stableStringify(dimensions));
6319
+ // Capture font style info
6320
+ const fontStyle = captureFontStyle();
6321
+ const fontStyleHash = fnv1a(stableStringify(fontStyle));
6322
+ return {
6323
+ value: { supported: true, dimensionsHash, fontStyleHash },
6324
+ duration: performance.now() - startTime,
6325
+ };
6326
+ }
6327
+ catch (err) {
6328
+ return {
6329
+ value: { supported: false, dimensionsHash: "", fontStyleHash: "" },
6330
+ duration: performance.now() - startTime,
6331
+ error: err instanceof Error ? err.message : "MathML fingerprinting failed",
6332
+ };
6333
+ }
6334
+ }
6335
+ /**
6336
+ * Check if MathML fingerprinting is available (requires DOM).
6337
+ */
6338
+ function isMathMLAvailable() {
6339
+ try {
6340
+ return (typeof document !== "undefined" &&
6341
+ typeof document.createElement === "function" &&
6342
+ typeof document.body !== "undefined" &&
6343
+ document.body !== null);
6344
+ }
6345
+ catch {
6346
+ return false;
6347
+ }
6348
+ }
6349
+
6350
+ /**
6351
+ * User-Agent Client Hints Fingerprinting
6352
+ *
6353
+ * Uses navigator.userAgentData.getHighEntropyValues() to obtain precise
6354
+ * platform details (~30 bits in Chromium). Gracefully degrades to
6355
+ * `available: false` in Firefox and Safari which don't expose the API.
6356
+ *
6357
+ * Note: platformVersion can change with OS/browser updates — this component
6358
+ * contributes to fingerprintHash but NOT to stableCoreHash (see orchestrator).
6359
+ */
6360
+ /**
6361
+ * Collect User-Agent Client Hints.
6362
+ */
6363
+ async function getClientHintsFingerprint() {
6364
+ const startTime = performance.now();
6365
+ try {
6366
+ if (!isClientHintsAvailable()) {
6367
+ return {
6368
+ value: { available: false },
6369
+ duration: performance.now() - startTime,
6370
+ };
6371
+ }
6372
+ const uaData = navigator.userAgentData;
6373
+ const highEntropy = await uaData.getHighEntropyValues([
6374
+ "architecture",
6375
+ "bitness",
6376
+ "fullVersionList",
6377
+ "mobile",
6378
+ "model",
6379
+ "platformVersion",
6380
+ "formFactor",
6381
+ "uaFullVersion",
6382
+ ]);
6383
+ // Normalize fullVersionList → brands
6384
+ const brands = highEntropy.fullVersionList?.map((b) => ({
6385
+ brand: b.brand ?? "",
6386
+ version: b.version ?? "",
6387
+ }));
6388
+ return {
6389
+ value: {
6390
+ available: true,
6391
+ brands,
6392
+ mobile: highEntropy.mobile,
6393
+ platform: uaData.platform,
6394
+ platformVersion: highEntropy.platformVersion,
6395
+ architecture: highEntropy.architecture,
6396
+ bitness: highEntropy.bitness,
6397
+ model: highEntropy.model,
6398
+ uaFullVersion: highEntropy.uaFullVersion,
6399
+ formFactor: highEntropy.formFactor,
6400
+ },
6401
+ duration: performance.now() - startTime,
6402
+ };
6403
+ }
6404
+ catch (err) {
6405
+ return {
6406
+ value: { available: false },
6407
+ duration: performance.now() - startTime,
6408
+ error: err instanceof Error ? err.message : "Client Hints fingerprinting failed",
6409
+ };
6410
+ }
6411
+ }
6412
+ /**
6413
+ * Check if User-Agent Client Hints API is available (Chromium-based browsers).
6414
+ */
6415
+ function isClientHintsAvailable() {
6416
+ try {
6417
+ return (typeof navigator !== "undefined" &&
6418
+ "userAgentData" in navigator &&
6419
+ typeof navigator.userAgentData?.getHighEntropyValues ===
6420
+ "function");
6421
+ }
6422
+ catch {
6423
+ return false;
6424
+ }
6425
+ }
6426
+
4986
6427
  /**
4987
6428
  * Fingerprint Orchestrator
4988
6429
  * Main fingerprinting engine that coordinates all components
@@ -5024,16 +6465,24 @@ function shouldIncludeComponent(component, options) {
5024
6465
  return false;
5025
6466
  }
5026
6467
  }
5027
- // New FingerprintJS components are enabled by default in non-GDPR mode
5028
- const fingerprintJSComponents = [
6468
+ // New FingerprintJS / ThumbmarkJS components are enabled by default
6469
+ const extendedComponents = [
5029
6470
  "mathFingerprint",
5030
6471
  "dateTimeLocale",
5031
6472
  "accessibilityEnhanced",
5032
6473
  "fontPreferences",
5033
6474
  "enhancedFonts",
6475
+ "webrtc",
6476
+ "permissions",
6477
+ "speech",
6478
+ // High-entropy new components
6479
+ "cssFeatures",
6480
+ "mediaCodecs",
6481
+ "wasmFeatures",
6482
+ "mathml",
6483
+ "clientHints",
5034
6484
  ];
5035
- // Always allow FingerprintJS components unless specifically excluded
5036
- if (fingerprintJSComponents.includes(component)) {
6485
+ if (extendedComponents.includes(component)) {
5037
6486
  return true;
5038
6487
  }
5039
6488
  return true;
@@ -5183,6 +6632,46 @@ async function collectFingerprint(options = {}) {
5183
6632
  result,
5184
6633
  })));
5185
6634
  }
6635
+ // WebRTC Fingerprint (ThumbmarkJS-inspired)
6636
+ if (shouldIncludeComponent("webrtc", opts) && isWebRTCAvailable()) {
6637
+ parallelTasks.push(collectComponent("webrtc", () => getWebRTCFingerprint(opts.timeout), opts).then((result) => ({ component: "webrtc", result })));
6638
+ }
6639
+ // Permissions Fingerprint (ThumbmarkJS-inspired)
6640
+ if (shouldIncludeComponent("permissions", opts) && isPermissionsAvailable()) {
6641
+ parallelTasks.push(collectComponent("permissions", getPermissionsFingerprint, opts).then((result) => ({ component: "permissions", result })));
6642
+ }
6643
+ // Speech Synthesis Fingerprint (ThumbmarkJS-inspired, experimental)
6644
+ if (!opts.gdprMode && shouldIncludeComponent("speech", opts) && isSpeechAvailable()) {
6645
+ parallelTasks.push(collectComponent("speech", getSpeechFingerprint, opts).then((result) => ({
6646
+ component: "speech",
6647
+ result,
6648
+ })));
6649
+ }
6650
+ // CSS @supports Feature Vector (high entropy, synchronous)
6651
+ if (shouldIncludeComponent("cssFeatures", opts) && isCSSFeaturesAvailable()) {
6652
+ parallelTasks.push(collectComponent("cssFeatures", async () => getCSSFeaturesFingerprint(), opts).then((result) => ({ component: "cssFeatures", result })));
6653
+ }
6654
+ // Media Codec Support Matrix (synchronous, OS+hardware signal)
6655
+ if (shouldIncludeComponent("mediaCodecs", opts) && isMediaCodecsAvailable()) {
6656
+ parallelTasks.push(collectComponent("mediaCodecs", async () => getMediaCodecsFingerprint(), opts).then((result) => ({ component: "mediaCodecs", result })));
6657
+ }
6658
+ // WebAssembly Feature Flags (synchronous, engine signal)
6659
+ if (shouldIncludeComponent("wasmFeatures", opts) && isWasmAvailable()) {
6660
+ parallelTasks.push(collectComponent("wasmFeatures", async () => getWasmFeaturesFingerprint(), opts).then((result) => ({ component: "wasmFeatures", result })));
6661
+ }
6662
+ // MathML Rendering Fingerprint (async DOM, font-metric signal)
6663
+ if (shouldIncludeComponent("mathml", opts) && isMathMLAvailable()) {
6664
+ parallelTasks.push(collectComponent("mathml", getMathMLFingerprint, opts).then((result) => ({
6665
+ component: "mathml",
6666
+ result,
6667
+ })));
6668
+ }
6669
+ // Client Hints API (async, Chromium-only, precise platform data)
6670
+ if (!opts.gdprMode &&
6671
+ shouldIncludeComponent("clientHints", opts) &&
6672
+ isClientHintsAvailable()) {
6673
+ parallelTasks.push(collectComponent("clientHints", getClientHintsFingerprint, opts).then((result) => ({ component: "clientHints", result })));
6674
+ }
5186
6675
  // Execute all parallel tasks and handle results
5187
6676
  const parallelResults = await Promise.allSettled(parallelTasks);
5188
6677
  parallelResults.forEach((promiseResult, index) => {
@@ -5563,6 +7052,28 @@ async function collectFingerprint(options = {}) {
5563
7052
  });
5564
7053
  }
5565
7054
  }
7055
+ // STABILIZATION: Determine which components are noisy in the current context
7056
+ // and null them out before hashing to avoid hash instability.
7057
+ {
7058
+ const incognitoLikelihood = fingerprintData.incognitoDetection?.likelihood ?? 0;
7059
+ const excluded = getExcludedComponents(incognitoLikelihood);
7060
+ if (excluded.size > 0) {
7061
+ for (const key of excluded) {
7062
+ if (fingerprintData[key] !== undefined) {
7063
+ fingerprintData[key] = null;
7064
+ // Move from collected to failed list
7065
+ const idx = collectedComponents.indexOf(key);
7066
+ if (idx !== -1) {
7067
+ collectedComponents.splice(idx, 1);
7068
+ failedComponents.push({
7069
+ component: key,
7070
+ error: `excluded by stabilization rules`,
7071
+ });
7072
+ }
7073
+ }
7074
+ }
7075
+ }
7076
+ }
5566
7077
  // Generate hash from collected components
5567
7078
  const componentValues = {};
5568
7079
  collectedComponents.forEach((component) => {
@@ -6000,6 +7511,8 @@ async function collectFingerprint(options = {}) {
6000
7511
  "accessibilityEnhanced", // Locale/accessibility (can change)
6001
7512
  "domBlockers",
6002
7513
  "pluginsEnhanced", // Privacy tools detection (can change)
7514
+ "speech", // Experimental — OS-specific, excluded from stableCoreHash
7515
+ "clientHints", // platformVersion changes with OS/browser updates
6003
7516
  ];
6004
7517
  const stableComponentValues = {};
6005
7518
  for (const [key, value] of Object.entries(componentValues)) {
@@ -6117,6 +7630,12 @@ function getAvailableComponents() {
6117
7630
  available.push("screen");
6118
7631
  if (isBrowserFingerprintingAvailable())
6119
7632
  available.push("browser");
7633
+ if (isWebRTCAvailable())
7634
+ available.push("webrtc");
7635
+ if (isPermissionsAvailable())
7636
+ available.push("permissions");
7637
+ if (isSpeechAvailable())
7638
+ available.push("speech");
6120
7639
  return available;
6121
7640
  }
6122
7641
 
@@ -24847,6 +26366,18 @@ function getHardwareFingerprint() {
24847
26366
  architecture: getArchitecture$1(),
24848
26367
  maxTouchPoints: getMaxTouchPoints(),
24849
26368
  platform: navigator.platform || 'unknown',
26369
+ jsHeapSizeLimit: performance.memory?.jsHeapSizeLimit ?? 0,
26370
+ architectureFloat32: (() => {
26371
+ try {
26372
+ const f = new Float32Array(1);
26373
+ f[0] = Infinity;
26374
+ f[0] -= f[0]; // NaN
26375
+ return new Uint8Array(f.buffer)[3] ?? -1;
26376
+ }
26377
+ catch {
26378
+ return -1;
26379
+ }
26380
+ })(),
24850
26381
  };
24851
26382
  }
24852
26383
 
@@ -26502,6 +28033,56 @@ const COMPONENT_CHARACTERISTICS = {
26502
28033
  uniqueness: 0.82, // Good uniqueness across systems
26503
28034
  spoofResistance: 0.7, // Moderate spoof resistance
26504
28035
  },
28036
+ // ThumbmarkJS-inspired new components
28037
+ webrtc: {
28038
+ stability: 0.92, // Codec/extension lists are stable per browser version
28039
+ entropy: 0.85, // Rich source of entropy (codecs + RTP extensions)
28040
+ uniqueness: 0.8, // Good differentiation across browser+OS combos
28041
+ spoofResistance: 0.9, // Very hard to fake SDP offer
28042
+ },
28043
+ permissions: {
28044
+ stability: 0.88, // Permission states are stable per profile
28045
+ entropy: 0.75, // 25 signals provide good entropy
28046
+ uniqueness: 0.72, // Decent uniqueness across configurations
28047
+ spoofResistance: 0.8, // Hard to predict all 25 states
28048
+ },
28049
+ speech: {
28050
+ stability: 0.9, // Voice list is stable per OS/locale
28051
+ entropy: 0.7, // Varies by OS version and language
28052
+ uniqueness: 0.65, // Less unique but still informative
28053
+ spoofResistance: 0.85, // Hard to fake voice list
28054
+ },
28055
+ // High-entropy new components
28056
+ cssFeatures: {
28057
+ stability: 0.97, // CSS support flags are very stable per browser version
28058
+ entropy: 0.88, // ~50 bits from 40 boolean features
28059
+ uniqueness: 0.85, // Good differentiation across browser+version combos
28060
+ spoofResistance: 0.95, // Hard to fake CSS.supports() results
28061
+ },
28062
+ mediaCodecs: {
28063
+ stability: 0.95, // Codec support is stable per OS + hardware
28064
+ entropy: 0.82, // ~20 bits, varies by OS (HEVC on macOS, etc.)
28065
+ uniqueness: 0.78, // Good differentiation across OS/browser combos
28066
+ spoofResistance: 0.92, // Requires actual codec support to fake
28067
+ },
28068
+ wasmFeatures: {
28069
+ stability: 0.98, // Wasm proposal flags are stable per engine version
28070
+ entropy: 0.70, // ~6-8 bits from proposal flags
28071
+ uniqueness: 0.72, // Distinguishes engine generations well
28072
+ spoofResistance: 0.97, // validate() is nearly impossible to fake
28073
+ },
28074
+ mathml: {
28075
+ stability: 0.90, // Font rendering can change with OS/font updates
28076
+ entropy: 0.80, // ~15 bits from font metric variations
28077
+ uniqueness: 0.75, // Varies by OS, engine, and font stack
28078
+ spoofResistance: 0.88, // Pixel-level measurements are hard to fake
28079
+ },
28080
+ clientHints: {
28081
+ stability: 0.93, // platformVersion can change with OS updates
28082
+ entropy: 0.92, // ~30 bits in Chromium (platform, arch, model, etc.)
28083
+ uniqueness: 0.90, // Very precise in Chromium-based browsers
28084
+ spoofResistance: 0.85, // API can be overridden but requires effort
28085
+ },
26505
28086
  };
26506
28087
  /**
26507
28088
  * New advanced components characteristics