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