@zaplier/sdk 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.esm.js CHANGED
@@ -173,8 +173,10 @@ function hash32(input) {
173
173
  * Hash fingerprint components into a stable identifier
174
174
  */
175
175
  function hashFingerprint(components) {
176
- // Convert components to canonical string representation
177
- const canonical = JSON.stringify(components, Object.keys(components).sort());
176
+ // Convert components to canonical string representation using deep sorting
177
+ // CRITICAL: Do NOT use JSON.stringify(obj, keys) as it filters out nested properties!
178
+ // Use canonicalizeStable instead which handles deep sorting and normalization.
179
+ const canonical = JSON.stringify(canonicalizeStable(components));
178
180
  // Use MurmurHash3 x64 for consistent hashing
179
181
  return x64hash128(canonical);
180
182
  }
@@ -231,17 +233,21 @@ function hashStableCore(coreVector) {
231
233
  * Based on FingerprintJS audio component using AudioContext
232
234
  */
233
235
  /**
234
- * Audio configuration for fingerprinting
236
+ * Enhanced audio configuration for maximum differentiation
235
237
  */
236
238
  const AUDIO_CONFIG = {
237
239
  SAMPLE_RATE: 44100,
238
240
  DURATION: 0.1, // 100ms
239
- FREQUENCY: 1000, // 1kHz tone
241
+ FREQUENCIES: [440, 1000, 1760, 3520], // Multiple frequencies for better differentiation
240
242
  COMPRESSOR_THRESHOLD: -50,
241
243
  COMPRESSOR_KNEE: 40,
242
244
  COMPRESSOR_RATIO: 12,
243
245
  COMPRESSOR_ATTACK: 0.003,
244
246
  COMPRESSOR_RELEASE: 0.25,
247
+ // NEW: Additional audio processing parameters
248
+ FILTER_FREQUENCY: 2000,
249
+ GAIN_VALUE: 0.5,
250
+ DELAY_TIME: 0.02,
245
251
  };
246
252
  /**
247
253
  * Create offline audio context (doesn't require user gesture)
@@ -303,40 +309,144 @@ function getDefaultSampleRate() {
303
309
  return 44100; // Default fallback (standard sample rate)
304
310
  }
305
311
  /**
306
- * Generate oscillator fingerprint using OfflineAudioContext (no user gesture required)
307
- */
308
- async function generateOscillatorFingerprint(offlineContext) {
309
- try {
310
- const osc = offlineContext.createOscillator();
311
- const comp = offlineContext.createDynamicsCompressor();
312
- // Configure oscillator
313
- osc.type = "triangle";
314
- osc.frequency.setValueAtTime(AUDIO_CONFIG.FREQUENCY, offlineContext.currentTime);
315
- // Configure compressor
316
- comp.threshold.setValueAtTime(AUDIO_CONFIG.COMPRESSOR_THRESHOLD, offlineContext.currentTime);
317
- comp.knee.setValueAtTime(AUDIO_CONFIG.COMPRESSOR_KNEE, offlineContext.currentTime);
318
- comp.ratio.setValueAtTime(AUDIO_CONFIG.COMPRESSOR_RATIO, offlineContext.currentTime);
319
- comp.attack.setValueAtTime(AUDIO_CONFIG.COMPRESSOR_ATTACK, offlineContext.currentTime);
320
- comp.release.setValueAtTime(AUDIO_CONFIG.COMPRESSOR_RELEASE, offlineContext.currentTime);
321
- // Connect within offline graph (no audible output, no user gesture needed)
322
- osc.connect(comp);
323
- comp.connect(offlineContext.destination);
324
- osc.start();
325
- osc.stop(AUDIO_CONFIG.DURATION);
326
- const buf = await offlineContext.startRendering();
327
- const ch = buf.getChannelData(0);
328
- // Sample a few deterministic points for fingerprinting
329
- const pts = [
330
- ch[0] || 0,
331
- ch[Math.floor(ch.length * 0.33)] || 0,
332
- ch[Math.floor(ch.length * 0.66)] || 0,
333
- ch[ch.length - 1] || 0,
312
+ * Generate advanced multiple oscillator fingerprint for maximum DAC/hardware differentiation
313
+ */
314
+ async function generateMultiOscillatorFingerprint(offlineContext) {
315
+ try {
316
+ // Create multiple oscillators with different waveforms and frequencies
317
+ const oscillators = AUDIO_CONFIG.FREQUENCIES.map((freq, index) => {
318
+ const osc = offlineContext.createOscillator();
319
+ const waveforms = ['sine', 'square', 'sawtooth', 'triangle'];
320
+ const waveformIndex = index % waveforms.length;
321
+ const selectedWaveform = waveforms[waveformIndex];
322
+ // Type guard to ensure we have a valid waveform
323
+ if (selectedWaveform) {
324
+ osc.type = selectedWaveform;
325
+ }
326
+ else {
327
+ osc.type = 'sine'; // Fallback to sine wave
328
+ }
329
+ osc.frequency.setValueAtTime(freq, offlineContext.currentTime);
330
+ return osc;
331
+ });
332
+ // Create advanced audio processing chain
333
+ const compressor = offlineContext.createDynamicsCompressor();
334
+ const biquadFilter = offlineContext.createBiquadFilter();
335
+ const gainNode = offlineContext.createGain();
336
+ const delayNode = offlineContext.createDelay();
337
+ const analyserNode = offlineContext.createAnalyser();
338
+ // Configure compressor with specific settings
339
+ compressor.threshold.setValueAtTime(AUDIO_CONFIG.COMPRESSOR_THRESHOLD, offlineContext.currentTime);
340
+ compressor.knee.setValueAtTime(AUDIO_CONFIG.COMPRESSOR_KNEE, offlineContext.currentTime);
341
+ compressor.ratio.setValueAtTime(AUDIO_CONFIG.COMPRESSOR_RATIO, offlineContext.currentTime);
342
+ compressor.attack.setValueAtTime(AUDIO_CONFIG.COMPRESSOR_ATTACK, offlineContext.currentTime);
343
+ compressor.release.setValueAtTime(AUDIO_CONFIG.COMPRESSOR_RELEASE, offlineContext.currentTime);
344
+ // Configure biquad filter for frequency response differences
345
+ biquadFilter.type = 'bandpass';
346
+ biquadFilter.frequency.setValueAtTime(AUDIO_CONFIG.FILTER_FREQUENCY, offlineContext.currentTime);
347
+ biquadFilter.Q.setValueAtTime(0.7, offlineContext.currentTime);
348
+ // Configure gain node
349
+ gainNode.gain.setValueAtTime(AUDIO_CONFIG.GAIN_VALUE, offlineContext.currentTime);
350
+ // Configure delay node for additional processing
351
+ delayNode.delayTime.setValueAtTime(AUDIO_CONFIG.DELAY_TIME, offlineContext.currentTime);
352
+ // Create a mixer node to combine oscillators
353
+ const mixerGain = offlineContext.createGain();
354
+ mixerGain.gain.setValueAtTime(0.25, offlineContext.currentTime); // Reduce volume when mixing
355
+ // Connect oscillators to mixer
356
+ oscillators.forEach(osc => {
357
+ osc.connect(mixerGain);
358
+ });
359
+ // Create complex audio processing chain:
360
+ // Mixed Oscillators -> Filter -> Compressor -> Gain -> Delay -> Analyser -> Destination
361
+ mixerGain.connect(biquadFilter);
362
+ biquadFilter.connect(compressor);
363
+ compressor.connect(gainNode);
364
+ gainNode.connect(delayNode);
365
+ delayNode.connect(analyserNode);
366
+ analyserNode.connect(offlineContext.destination);
367
+ // Start all oscillators
368
+ oscillators.forEach((osc, index) => {
369
+ osc.start(index * 0.01); // Slightly staggered start times
370
+ osc.stop(AUDIO_CONFIG.DURATION);
371
+ });
372
+ const buffer = await offlineContext.startRendering();
373
+ const channelData = buffer.getChannelData(0);
374
+ // Enhanced sampling for better differentiation
375
+ const samplePoints = [];
376
+ for (let i = 0; i < 32; i++) {
377
+ const index = Math.floor((channelData.length / 32) * i);
378
+ samplePoints.push(channelData[index] || 0);
379
+ }
380
+ // Calculate additional characteristics
381
+ const rms = Math.sqrt(samplePoints.reduce((sum, val) => sum + val * val, 0) / samplePoints.length);
382
+ const maxAmplitude = Math.max(...samplePoints.map(Math.abs));
383
+ const averageAmplitude = samplePoints.reduce((sum, val) => sum + Math.abs(val), 0) / samplePoints.length;
384
+ // Frequency domain analysis
385
+ let spectralCentroid = 0;
386
+ for (let i = 0; i < Math.min(samplePoints.length, 16); i++) {
387
+ const sample = samplePoints[i];
388
+ if (sample !== undefined) {
389
+ spectralCentroid += i * Math.abs(sample);
390
+ }
391
+ }
392
+ const characteristics = [
393
+ ...samplePoints,
394
+ rms,
395
+ maxAmplitude,
396
+ averageAmplitude,
397
+ spectralCentroid
334
398
  ];
335
- return hash32(pts.join(","));
399
+ return hash32(characteristics.join(","));
336
400
  }
337
401
  catch (error) {
338
- // If OfflineAudioContext fails, return fallback hash
339
- return hash32("oscillator_error");
402
+ return hash32("multi_oscillator_error");
403
+ }
404
+ }
405
+ /**
406
+ * Generate frequency response fingerprint to detect DAC characteristics
407
+ */
408
+ async function generateFrequencyResponseFingerprint(offlineContext) {
409
+ try {
410
+ // Test multiple frequencies to detect DAC/audio hardware frequency response
411
+ const testFrequencies = [100, 440, 1000, 2000, 4000, 8000, 12000, 16000];
412
+ const responses = [];
413
+ for (const frequency of testFrequencies) {
414
+ const osc = offlineContext.createOscillator();
415
+ const analyser = offlineContext.createAnalyser();
416
+ osc.type = 'sine';
417
+ osc.frequency.setValueAtTime(frequency, offlineContext.currentTime);
418
+ // Configure analyser for frequency analysis
419
+ analyser.fftSize = 256;
420
+ analyser.smoothingTimeConstant = 0;
421
+ osc.connect(analyser);
422
+ analyser.connect(offlineContext.destination);
423
+ osc.start();
424
+ osc.stop(0.05); // Short test for each frequency
425
+ // Create a new offline context for each frequency test
426
+ const testContext = createOfflineAudioContext();
427
+ if (testContext) {
428
+ const testOsc = testContext.createOscillator();
429
+ testOsc.type = 'sine';
430
+ testOsc.frequency.setValueAtTime(frequency, testContext.currentTime);
431
+ testOsc.connect(testContext.destination);
432
+ testOsc.start();
433
+ testOsc.stop(0.05);
434
+ try {
435
+ const testBuffer = await testContext.startRendering();
436
+ const testData = testBuffer.getChannelData(0);
437
+ // Calculate RMS for this frequency
438
+ const rms = Math.sqrt(testData.reduce((sum, val) => sum + val * val, 0) / testData.length);
439
+ responses.push(rms);
440
+ }
441
+ catch {
442
+ responses.push(0);
443
+ }
444
+ }
445
+ }
446
+ return hash32(responses.join(","));
447
+ }
448
+ catch (error) {
449
+ return hash32("frequency_response_error");
340
450
  }
341
451
  }
342
452
  /**
@@ -348,7 +458,8 @@ async function generateCompressorFingerprint(offlineContext) {
348
458
  const offlineCompressor = offlineContext.createDynamicsCompressor();
349
459
  // Configure nodes
350
460
  offlineOscillator.type = "triangle";
351
- offlineOscillator.frequency.setValueAtTime(AUDIO_CONFIG.FREQUENCY, offlineContext.currentTime);
461
+ offlineOscillator.frequency.setValueAtTime(AUDIO_CONFIG.FREQUENCIES[0], // Use first frequency from the array
462
+ offlineContext.currentTime);
352
463
  offlineCompressor.threshold.setValueAtTime(AUDIO_CONFIG.COMPRESSOR_THRESHOLD, offlineContext.currentTime);
353
464
  offlineCompressor.knee.setValueAtTime(AUDIO_CONFIG.COMPRESSOR_KNEE, offlineContext.currentTime);
354
465
  offlineCompressor.ratio.setValueAtTime(AUDIO_CONFIG.COMPRESSOR_RATIO, offlineContext.currentTime);
@@ -377,35 +488,138 @@ async function generateCompressorFingerprint(offlineContext) {
377
488
  }
378
489
  }
379
490
  /**
380
- * Generate audio fingerprint using OfflineAudioContext (no user gesture required)
381
- * This is the recommended approach for fingerprinting - no audio output, no user interaction needed
491
+ * Get detailed audio context characteristics
492
+ */
493
+ function getAudioContextCharacteristics() {
494
+ const characteristics = {};
495
+ try {
496
+ // Try to get characteristics from OfflineAudioContext
497
+ const testContext = createOfflineAudioContext();
498
+ if (testContext) {
499
+ characteristics.sampleRate = testContext.sampleRate;
500
+ characteristics.length = testContext.length;
501
+ characteristics.state = testContext.state;
502
+ // Test supported sample rates
503
+ const supportedRates = [];
504
+ const testRates = [8000, 22050, 44100, 48000, 96000];
505
+ for (const rate of testRates) {
506
+ try {
507
+ const testCtx = new (window.OfflineAudioContext || window.webkitOfflineAudioContext)(1, 1, rate);
508
+ if (testCtx.sampleRate === rate) {
509
+ supportedRates.push(rate);
510
+ }
511
+ // OfflineAudioContext doesn't have close method - it's automatically disposed after rendering
512
+ if ('close' in testCtx && typeof testCtx.close === 'function') {
513
+ testCtx.close();
514
+ }
515
+ }
516
+ catch {
517
+ // Rate not supported
518
+ }
519
+ }
520
+ characteristics.supportedSampleRates = supportedRates;
521
+ // Test base latency (if available)
522
+ if ('baseLatency' in testContext) {
523
+ characteristics.baseLatency = testContext.baseLatency;
524
+ }
525
+ // OfflineAudioContext doesn't have close method - it's automatically disposed after rendering
526
+ if ('close' in testContext && typeof testContext.close === 'function') {
527
+ testContext.close();
528
+ }
529
+ }
530
+ }
531
+ catch (error) {
532
+ characteristics.error = 'context_characteristics_error';
533
+ }
534
+ return characteristics;
535
+ }
536
+ /**
537
+ * Generate enhanced audio fingerprint with multiple analysis methods
382
538
  */
383
539
  async function getAudioFingerprint() {
384
540
  const startTime = performance.now();
385
541
  try {
386
- // Get sample rate from default context (for metadata only)
542
+ // Get sample rate and context characteristics
387
543
  const sampleRate = getDefaultSampleRate();
388
- // Create offline contexts for fingerprinting (no user gesture needed)
389
- const offlineContextOsc = createOfflineAudioContext();
390
- if (!offlineContextOsc) {
544
+ const contextCharacteristics = getAudioContextCharacteristics();
545
+ // Create multiple offline contexts for different analysis methods
546
+ const contexts = {
547
+ multiOsc: createOfflineAudioContext(),
548
+ compressor: createOfflineAudioContext(),
549
+ frequencyResponse: createOfflineAudioContext(),
550
+ };
551
+ // Verify all contexts are available
552
+ if (!contexts.multiOsc || !contexts.compressor || !contexts.frequencyResponse) {
391
553
  throw new Error("OfflineAudioContext not available");
392
554
  }
393
- // Create another offline context for compressor fingerprint
394
- const offlineContextComp = createOfflineAudioContext();
395
- if (!offlineContextComp) {
396
- throw new Error("OfflineAudioContext not available for compressor");
397
- }
398
- // Generate fingerprints in parallel (both use OfflineAudioContext, no user gesture needed)
399
- const [oscillatorHash, compressorHash] = await Promise.all([
400
- generateOscillatorFingerprint(offlineContextOsc).catch(() => "oscillator_error"),
401
- generateCompressorFingerprint(offlineContextComp).catch(() => "compressor_error"),
555
+ // Generate multiple fingerprints in parallel for enhanced differentiation
556
+ const [multiOscillatorHash, compressorHash, frequencyResponseHash] = await Promise.all([
557
+ generateMultiOscillatorFingerprint(contexts.multiOsc).catch(() => "multi_oscillator_error"),
558
+ generateCompressorFingerprint(contexts.compressor).catch(() => "compressor_error"),
559
+ generateFrequencyResponseFingerprint(contexts.frequencyResponse).catch(() => "frequency_response_error"),
402
560
  ]);
561
+ // Test audio node creation capabilities
562
+ const nodeCapabilities = {};
563
+ try {
564
+ const testContext = createOfflineAudioContext();
565
+ if (testContext) {
566
+ const nodeTypes = [
567
+ 'createOscillator',
568
+ 'createAnalyser',
569
+ 'createBiquadFilter',
570
+ 'createConvolver',
571
+ 'createDelay',
572
+ 'createDynamicsCompressor',
573
+ 'createGain',
574
+ 'createWaveShaper',
575
+ 'createStereoPanner',
576
+ 'createChannelSplitter',
577
+ 'createChannelMerger'
578
+ ];
579
+ for (const nodeType of nodeTypes) {
580
+ try {
581
+ if (typeof testContext[nodeType] === 'function') {
582
+ testContext[nodeType]();
583
+ nodeCapabilities[nodeType] = true;
584
+ }
585
+ }
586
+ catch {
587
+ nodeCapabilities[nodeType] = false;
588
+ }
589
+ }
590
+ // OfflineAudioContext doesn't have close method - it's automatically disposed after rendering
591
+ if ('close' in testContext && typeof testContext.close === 'function') {
592
+ testContext.close();
593
+ }
594
+ }
595
+ }
596
+ catch {
597
+ // Node capability testing failed
598
+ }
599
+ // Calculate audio stack fingerprint
600
+ const audioStackComponents = [
601
+ multiOscillatorHash,
602
+ compressorHash,
603
+ frequencyResponseHash,
604
+ sampleRate.toString(),
605
+ JSON.stringify(contextCharacteristics),
606
+ JSON.stringify(nodeCapabilities)
607
+ ];
608
+ const audioStackHash = hash32(audioStackComponents.join("|"));
403
609
  const endTime = performance.now();
404
610
  const result = {
405
- oscillator: oscillatorHash,
611
+ // Legacy fields for compatibility
612
+ oscillator: multiOscillatorHash,
406
613
  compressor: compressorHash,
407
614
  sampleRate: sampleRate,
408
- maxChannelCount: 2, // Default stereo, doesn't affect fingerprint quality
615
+ maxChannelCount: contextCharacteristics.supportedSampleRates?.length || 2,
616
+ // NEW: Enhanced audio analysis
617
+ multiOscillatorFingerprint: multiOscillatorHash,
618
+ frequencyResponseFingerprint: frequencyResponseHash,
619
+ contextCharacteristics: contextCharacteristics,
620
+ nodeCapabilities: nodeCapabilities,
621
+ audioStackHash: audioStackHash,
622
+ supportedSampleRates: contextCharacteristics.supportedSampleRates || []
409
623
  };
410
624
  return {
411
625
  value: result,
@@ -419,6 +633,12 @@ async function getAudioFingerprint() {
419
633
  compressor: "error",
420
634
  sampleRate: 0,
421
635
  maxChannelCount: 0,
636
+ multiOscillatorFingerprint: "error",
637
+ frequencyResponseFingerprint: "error",
638
+ contextCharacteristics: {},
639
+ nodeCapabilities: {},
640
+ audioStackHash: "error",
641
+ supportedSampleRates: []
422
642
  },
423
643
  duration: performance.now() - startTime,
424
644
  error: error instanceof Error ? error.message : "Audio fingerprinting failed",
@@ -445,6 +665,417 @@ function isAudioAvailable() {
445
665
  }
446
666
  }
447
667
 
668
+ /**
669
+ * Browser Detection Utilities
670
+ * Based on FingerprintJS browser detection patterns
671
+ * Uses feature detection instead of user-agent parsing for reliability
672
+ */
673
+ /**
674
+ * Detects if the browser is WebKit-based (Safari, mobile Safari)
675
+ */
676
+ function isWebKit$1() {
677
+ try {
678
+ return ("WebKitAppearance" in document.documentElement.style ||
679
+ "webkitRequestFileSystem" in window ||
680
+ "webkitResolveLocalFileSystemURL" in window ||
681
+ Boolean(window.safari));
682
+ }
683
+ catch {
684
+ return false;
685
+ }
686
+ }
687
+ /**
688
+ * Detects if the browser is running on Android
689
+ */
690
+ function isAndroid$1() {
691
+ try {
692
+ return ("ontouchstart" in window &&
693
+ ("orientation" in window || "onorientationchange" in window) &&
694
+ /android/i.test(navigator.userAgent));
695
+ }
696
+ catch {
697
+ return false;
698
+ }
699
+ }
700
+ /**
701
+ * Detects if the browser is Brave
702
+ * Brave masquerades as Chrome but has specific APIs
703
+ */
704
+ function isBrave() {
705
+ try {
706
+ // Brave has navigator.brave API (most reliable method)
707
+ if ("brave" in navigator && navigator.brave) {
708
+ return true;
709
+ }
710
+ // Secondary check: Brave blocks certain APIs that Chrome doesn't
711
+ // Brave has specific user agent string patterns (less reliable)
712
+ const ua = navigator.userAgent;
713
+ if (ua.includes("Brave")) {
714
+ return true;
715
+ }
716
+ // Brave detection through missing APIs that Chrome has
717
+ if (window.chrome &&
718
+ window.chrome.runtime &&
719
+ !window.chrome.webstore &&
720
+ !window.navigator.getBattery && // Brave removes some APIs for privacy
721
+ /Chrome/.test(ua)) {
722
+ return true;
723
+ }
724
+ return false;
725
+ }
726
+ catch {
727
+ return false;
728
+ }
729
+ }
730
+ /**
731
+ * Detects if the browser is Arc
732
+ * Arc browser is Chromium-based but has unique characteristics
733
+ */
734
+ function isArc() {
735
+ try {
736
+ const ua = navigator.userAgent;
737
+ // Arc specific detection
738
+ if (ua.includes("Arc/")) {
739
+ return true;
740
+ }
741
+ // Arc may have specific window properties
742
+ if (window.arc || window.Arc) {
743
+ return true;
744
+ }
745
+ // Arc modifies some Chrome APIs
746
+ if (window.chrome &&
747
+ window.chrome.runtime &&
748
+ /Chrome/.test(ua) &&
749
+ // Arc specific user agent patterns or missing features
750
+ (ua.includes("ArcBrowser") || window.location.protocol === "arc:")) {
751
+ return true;
752
+ }
753
+ return false;
754
+ }
755
+ catch {
756
+ return false;
757
+ }
758
+ }
759
+ /**
760
+ * Detects if the browser is Opera (modern Chromium-based)
761
+ */
762
+ function isOpera() {
763
+ try {
764
+ // Opera has specific properties
765
+ if (window.opr || window.opera) {
766
+ return true;
767
+ }
768
+ const ua = navigator.userAgent;
769
+ if (ua.includes("OPR/") || ua.includes("Opera/")) {
770
+ return true;
771
+ }
772
+ return false;
773
+ }
774
+ catch {
775
+ return false;
776
+ }
777
+ }
778
+ /**
779
+ * Detects if the browser is Vivaldi
780
+ */
781
+ function isVivaldi() {
782
+ try {
783
+ const ua = navigator.userAgent;
784
+ if (ua.includes("Vivaldi/")) {
785
+ return true;
786
+ }
787
+ // Vivaldi has specific window properties
788
+ if (window.vivaldi) {
789
+ return true;
790
+ }
791
+ return false;
792
+ }
793
+ catch {
794
+ return false;
795
+ }
796
+ }
797
+ /**
798
+ * Detects if the browser is Samsung Internet
799
+ */
800
+ function isSamsungInternet$1() {
801
+ try {
802
+ const ua = navigator.userAgent;
803
+ return ua.includes("SamsungBrowser/") || ua.includes("Samsung Internet");
804
+ }
805
+ catch {
806
+ return false;
807
+ }
808
+ }
809
+ /**
810
+ * Detects if the browser is Chrome/Chromium-based (pure Chrome, not derivatives)
811
+ */
812
+ function isChrome() {
813
+ try {
814
+ // First, exclude known Chromium derivatives
815
+ if (isBrave() ||
816
+ isArc() ||
817
+ isOpera() ||
818
+ isVivaldi() ||
819
+ isEdge() ||
820
+ isSamsungInternet$1()) {
821
+ return false;
822
+ }
823
+ return Boolean(window.chrome &&
824
+ (window.chrome.webstore || window.chrome.runtime) &&
825
+ /Chrome/.test(navigator.userAgent) &&
826
+ !/Edg|OPR|Opera|Vivaldi|SamsungBrowser|Arc|Brave/.test(navigator.userAgent));
827
+ }
828
+ catch {
829
+ return false;
830
+ }
831
+ }
832
+ /**
833
+ * Detects if the browser is Firefox
834
+ */
835
+ function isFirefox() {
836
+ try {
837
+ return ("InstallTrigger" in window ||
838
+ "mozInnerScreenX" in window ||
839
+ "mozPaintCount" in window ||
840
+ Boolean(navigator.mozApps));
841
+ }
842
+ catch {
843
+ return false;
844
+ }
845
+ }
846
+ /**
847
+ * Detects if the browser is Edge (legacy or Chromium)
848
+ */
849
+ function isEdge() {
850
+ try {
851
+ return ("msCredentials" in navigator ||
852
+ Boolean(window.StyleMedia) ||
853
+ (isChrome() && /edg/i.test(navigator.userAgent)));
854
+ }
855
+ catch {
856
+ return false;
857
+ }
858
+ }
859
+ /**
860
+ * Detects if the browser is Safari (not just WebKit)
861
+ */
862
+ function isSafari() {
863
+ try {
864
+ return (isWebKit$1() &&
865
+ !isChrome() &&
866
+ !isEdge() &&
867
+ Boolean(window.safari) &&
868
+ /safari/i.test(navigator.userAgent));
869
+ }
870
+ catch {
871
+ return false;
872
+ }
873
+ }
874
+ /**
875
+ * Detects if the browser is in a secure context
876
+ */
877
+ function isSecureContext() {
878
+ try {
879
+ return window.isSecureContext || location.protocol === "https:";
880
+ }
881
+ catch {
882
+ return false;
883
+ }
884
+ }
885
+ /**
886
+ * Detects if the browser is likely in private/incognito mode
887
+ * This is a best-effort detection and not 100% reliable
888
+ */
889
+ function isLikelyPrivateMode() {
890
+ try {
891
+ // Quick checks for obvious private mode indicators
892
+ if ("webkitTemporaryStorage" in navigator) {
893
+ return false; // Likely not private
894
+ }
895
+ // Check for reduced storage quotas (common in private mode)
896
+ if (navigator.storage && navigator.storage.estimate) {
897
+ navigator.storage.estimate().then((estimate) => {
898
+ const quota = estimate.quota || 0;
899
+ return quota < 1024 * 1024 * 100; // Less than 100MB likely indicates private mode
900
+ });
901
+ }
902
+ return false; // Default to not private if we can't determine
903
+ }
904
+ catch {
905
+ return false;
906
+ }
907
+ }
908
+ /**
909
+ * Gets the exact browser name with proper Chromium-based browser detection
910
+ */
911
+ function getBrowserName() {
912
+ try {
913
+ // Check specific Chromium-based browsers first (order matters!)
914
+ if (isBrave())
915
+ return "Brave";
916
+ if (isArc())
917
+ return "Arc";
918
+ if (isOpera())
919
+ return "Opera";
920
+ if (isVivaldi())
921
+ return "Vivaldi";
922
+ if (isSamsungInternet$1())
923
+ return "Samsung Internet";
924
+ if (isEdge())
925
+ return "Edge";
926
+ // Check non-Chromium browsers
927
+ if (isFirefox())
928
+ return "Firefox";
929
+ if (isSafari())
930
+ return "Safari";
931
+ // Finally check pure Chrome (after excluding derivatives)
932
+ if (isChrome())
933
+ return "Chrome";
934
+ // Fallback: try to parse from user agent
935
+ const ua = navigator.userAgent;
936
+ if (/Firefox/i.test(ua) && !isChrome())
937
+ return "Firefox";
938
+ if (/Safari/i.test(ua) && !isChrome())
939
+ return "Safari";
940
+ if (/Chrome/i.test(ua))
941
+ return "Chrome"; // Last resort fallback
942
+ return "Unknown";
943
+ }
944
+ catch {
945
+ return "Unknown";
946
+ }
947
+ }
948
+ /**
949
+ * Gets browser engine name for debugging
950
+ */
951
+ function getBrowserEngine$1() {
952
+ if (isBrave() ||
953
+ isArc() ||
954
+ isOpera() ||
955
+ isVivaldi() ||
956
+ isSamsungInternet$1() ||
957
+ isChrome())
958
+ return "Blink";
959
+ if (isEdge())
960
+ return "EdgeHTML/Blink";
961
+ if (isFirefox())
962
+ return "Gecko";
963
+ if (isWebKit$1() || isSafari())
964
+ return "WebKit";
965
+ return "Unknown";
966
+ }
967
+ /**
968
+ * Gets detailed browser information with accurate Chromium-based detection
969
+ */
970
+ function getBrowserInfo() {
971
+ const name = getBrowserName();
972
+ const engine = getBrowserEngine$1();
973
+ const chromiumBased = [
974
+ "Brave",
975
+ "Arc",
976
+ "Opera",
977
+ "Vivaldi",
978
+ "Samsung Internet",
979
+ "Edge",
980
+ "Chrome",
981
+ ].includes(name);
982
+ // Browser family for fingerprinting stability
983
+ let family = name.toLowerCase();
984
+ if (chromiumBased && name !== "Chrome") {
985
+ family = `${name.toLowerCase()}-chromium`; // e.g., "brave-chromium"
986
+ }
987
+ return {
988
+ name,
989
+ engine,
990
+ isChromiumBased: chromiumBased,
991
+ family,
992
+ };
993
+ }
994
+ /**
995
+ * Checks if the current environment supports DOM manipulation
996
+ */
997
+ function supportsDOMManipulation() {
998
+ try {
999
+ return (typeof document !== "undefined" &&
1000
+ typeof document.createElement === "function" &&
1001
+ typeof document.body !== "undefined");
1002
+ }
1003
+ catch {
1004
+ return false;
1005
+ }
1006
+ }
1007
+ /**
1008
+ * Checks if the current environment supports media queries
1009
+ */
1010
+ function supportsMediaQueries() {
1011
+ try {
1012
+ return (typeof window !== "undefined" && typeof window.matchMedia === "function");
1013
+ }
1014
+ catch {
1015
+ return false;
1016
+ }
1017
+ }
1018
+ /**
1019
+ * Checks if WebGL is likely to be available and stable
1020
+ */
1021
+ function supportsWebGL() {
1022
+ try {
1023
+ if (!supportsDOMManipulation())
1024
+ return false;
1025
+ const canvas = document.createElement("canvas");
1026
+ const gl = canvas.getContext("webgl") || canvas.getContext("experimental-webgl");
1027
+ if (!gl)
1028
+ return false;
1029
+ // Quick stability check
1030
+ const webglContext = gl;
1031
+ const renderer = webglContext.getParameter(webglContext.RENDERER);
1032
+ const isSupported = Boolean(renderer && typeof renderer === "string");
1033
+ // Cleanup context to avoid "Too many active WebGL contexts" warning
1034
+ const ext = webglContext.getExtension("WEBGL_lose_context");
1035
+ if (ext) {
1036
+ ext.loseContext();
1037
+ }
1038
+ return isSupported;
1039
+ }
1040
+ catch {
1041
+ return false;
1042
+ }
1043
+ }
1044
+ /**
1045
+ * Checks if audio context is available and not suspended
1046
+ */
1047
+ function supportsAudioContext() {
1048
+ try {
1049
+ const AudioContext = window.AudioContext || window.webkitAudioContext;
1050
+ if (!AudioContext)
1051
+ return false;
1052
+ // Don't create context here, just check availability
1053
+ return true;
1054
+ }
1055
+ catch {
1056
+ return false;
1057
+ }
1058
+ }
1059
+ function getBrowserCapabilities$1() {
1060
+ const supportsDOM = supportsDOMManipulation();
1061
+ const supportsMedia = supportsMediaQueries();
1062
+ return {
1063
+ engine: getBrowserEngine$1(),
1064
+ supportsDOM,
1065
+ supportsMediaQueries: supportsMedia,
1066
+ supportsWebGL: supportsWebGL(),
1067
+ supportsAudio: supportsAudioContext(),
1068
+ isSecure: isSecureContext(),
1069
+ isPrivateMode: isLikelyPrivateMode(),
1070
+ // DOM blockers work best on WebKit and Android
1071
+ canRunDOMBlockers: (isWebKit$1() || isAndroid$1()) && supportsDOM,
1072
+ // Accessibility requires media queries
1073
+ canRunAccessibility: supportsMedia,
1074
+ // Browser APIs need secure context for many features
1075
+ canRunBrowserAPIs: supportsDOM && typeof navigator !== "undefined",
1076
+ };
1077
+ }
1078
+
448
1079
  /**
449
1080
  * Browser and System Information Fingerprinting
450
1081
  * Based on FingerprintJS browser component with enhanced detection
@@ -591,10 +1222,27 @@ function getPlatformInfo() {
591
1222
  return { platform: '', userAgent: '' };
592
1223
  }
593
1224
  }
1225
+ /**
1226
+ * Get precise browser information with Chromium-based browser detection
1227
+ */
1228
+ function getEnhancedBrowserInfo() {
1229
+ try {
1230
+ return getBrowserInfo();
1231
+ }
1232
+ catch {
1233
+ // Fallback if detection fails
1234
+ return {
1235
+ name: 'Unknown',
1236
+ engine: 'Unknown',
1237
+ isChromiumBased: false,
1238
+ family: 'unknown'
1239
+ };
1240
+ }
1241
+ }
594
1242
  /**
595
1243
  * Get browser capabilities
596
1244
  */
597
- function getBrowserCapabilities$1() {
1245
+ function getBrowserCapabilities() {
598
1246
  try {
599
1247
  const cookieEnabled = navigator.cookieEnabled !== false;
600
1248
  // Do Not Track
@@ -625,9 +1273,10 @@ async function getBrowserFingerprint() {
625
1273
  const timezoneInfo = getTimezoneInfo();
626
1274
  const platformInfo = getPlatformInfo();
627
1275
  const hardwareInfo = getHardwareInfo();
628
- const capabilities = getBrowserCapabilities$1();
1276
+ const capabilities = getBrowserCapabilities();
629
1277
  const pluginInfo = getPluginInfo();
630
1278
  const touchSupport = getTouchSupport$1();
1279
+ const enhancedBrowserInfo = getEnhancedBrowserInfo();
631
1280
  const endTime = performance.now();
632
1281
  const result = {
633
1282
  // Language and locale
@@ -639,6 +1288,8 @@ async function getBrowserFingerprint() {
639
1288
  // Platform information
640
1289
  platform: platformInfo.platform,
641
1290
  userAgent: platformInfo.userAgent,
1291
+ // Enhanced browser identification (solves Chromium masquerading!)
1292
+ browserInfo: enhancedBrowserInfo,
642
1293
  // Hardware information
643
1294
  hardwareConcurrency: hardwareInfo.hardwareConcurrency,
644
1295
  // Browser capabilities
@@ -695,133 +1346,361 @@ function isBrowserFingerprintingAvailable() {
695
1346
  * Based on FingerprintJS canvas component with incognito detection
696
1347
  */
697
1348
  /**
698
- * Text to render for canvas fingerprinting
1349
+ * Text to render for canvas fingerprinting with maximum differentiation
699
1350
  */
700
- const CANVAS_TEXT = 'RabbitTracker Canvas 🎨 🔒 2024';
1351
+ const CANVAS_TEXTS = [
1352
+ "Zap Canvas 🎨🔒2024",
1353
+ "Żółć gęślą jaźń €$¢£¥",
1354
+ "αβγδεζηθικλμνξο",
1355
+ "中文测试字体渲染",
1356
+ "🌟🎯🚀💎🌊🎨",
1357
+ ];
701
1358
  /**
702
- * Geometric shapes for canvas fingerprinting
703
- */
704
- function drawGeometry(ctx) {
705
- // Set up styles
706
- ctx.fillStyle = 'rgb(102, 204, 0)';
707
- ctx.fillRect(10, 10, 50, 50);
708
- ctx.fillStyle = '#f60';
709
- ctx.fillRect(70, 10, 50, 50);
710
- // Draw circle
1359
+ * Enhanced geometric shapes for canvas fingerprinting with sub-pixel precision
1360
+ */
1361
+ function drawAdvancedGeometry(ctx) {
1362
+ // Enable high-quality rendering for sub-pixel differences
1363
+ ctx.imageSmoothingEnabled = true;
1364
+ ctx.imageSmoothingQuality = "high";
1365
+ // Complex gradient with multiple stops
1366
+ const radialGradient = ctx.createRadialGradient(75, 75, 0, 75, 75, 50);
1367
+ radialGradient.addColorStop(0, "rgba(255, 0, 0, 0.8)");
1368
+ radialGradient.addColorStop(0.3, "rgba(0, 255, 0, 0.6)");
1369
+ radialGradient.addColorStop(0.7, "rgba(0, 0, 255, 0.4)");
1370
+ radialGradient.addColorStop(1, "rgba(255, 255, 0, 0.2)");
1371
+ ctx.fillStyle = radialGradient;
1372
+ ctx.fillRect(10, 10, 130, 100);
1373
+ // Sub-pixel positioned shapes for GPU/anti-aliasing differentiation
1374
+ ctx.fillStyle = "rgba(102, 204, 0, 0.7)";
1375
+ ctx.fillRect(15.5, 15.3, 49.7, 49.2);
1376
+ ctx.fillStyle = "#f60";
1377
+ ctx.fillRect(70.3, 10.7, 50.1, 50.9);
1378
+ // Complex bezier curves that stress different rendering engines
711
1379
  ctx.beginPath();
712
- ctx.arc(50, 80, 20, 0, Math.PI * 2, true);
1380
+ ctx.moveTo(25.2, 120.1);
1381
+ ctx.bezierCurveTo(25.2, 120.1, 75.8, 90.3, 125.5, 120.7);
1382
+ ctx.bezierCurveTo(125.5, 120.7, 90.1, 150.2, 60.8, 140.9);
713
1383
  ctx.closePath();
1384
+ ctx.fillStyle = "rgba(200, 100, 50, 0.6)";
714
1385
  ctx.fill();
715
- // Draw triangle
1386
+ // Overlapping circles with complex blending
1387
+ ctx.globalCompositeOperation = "multiply";
716
1388
  ctx.beginPath();
717
- ctx.moveTo(100, 80);
718
- ctx.lineTo(120, 120);
719
- ctx.lineTo(80, 120);
720
- ctx.closePath();
721
- ctx.stroke();
722
- // Add gradient
723
- const gradient = ctx.createLinearGradient(0, 0, 150, 150);
724
- gradient.addColorStop(0, 'red');
725
- gradient.addColorStop(1, 'blue');
726
- ctx.fillStyle = gradient;
727
- ctx.fillRect(130, 10, 50, 50);
728
- }
729
- /**
730
- * Draw text with various styles
731
- */
732
- function drawText(ctx) {
733
- // Text with emoji and special characters
734
- ctx.textBaseline = 'top';
735
- ctx.font = '14px Arial, sans-serif';
736
- ctx.fillStyle = '#000';
737
- ctx.fillText(CANVAS_TEXT, 4, 140);
738
- // Different font
739
- ctx.font = '12px Georgia, serif';
740
- ctx.fillStyle = '#666';
741
- ctx.fillText('Georgia Font Test', 4, 160);
742
- // Bold text
743
- ctx.font = 'bold 16px Helvetica';
744
- ctx.fillStyle = '#333';
745
- ctx.fillText('Bold Helvetica', 4, 180);
746
- // Apply transformations
1389
+ ctx.arc(50.7, 80.3, 20.1, 0, Math.PI * 2);
1390
+ ctx.fillStyle = "rgba(255, 0, 100, 0.5)";
1391
+ ctx.fill();
1392
+ ctx.beginPath();
1393
+ ctx.arc(70.3, 80.7, 20.9, 0, Math.PI * 2);
1394
+ ctx.fillStyle = "rgba(0, 255, 100, 0.5)";
1395
+ ctx.fill();
1396
+ ctx.globalCompositeOperation = "source-over";
1397
+ // Thin lines to test anti-aliasing
1398
+ ctx.strokeStyle = "rgba(50, 50, 50, 0.8)";
1399
+ ctx.lineWidth = 0.5;
1400
+ ctx.setLineDash([2.3, 1.7]);
1401
+ for (let i = 0; i < 10; i++) {
1402
+ ctx.beginPath();
1403
+ ctx.moveTo(10 + i * 13.7, 160);
1404
+ ctx.lineTo(50 + i * 11.3, 200);
1405
+ ctx.stroke();
1406
+ }
1407
+ // Reset line dash
1408
+ ctx.setLineDash([]);
1409
+ // Pattern with complex repeating elements
1410
+ const patternCanvas = document.createElement("canvas");
1411
+ patternCanvas.width = 20;
1412
+ patternCanvas.height = 20;
1413
+ const patternCtx = patternCanvas.getContext("2d");
1414
+ if (patternCtx) {
1415
+ patternCtx.fillStyle = "rgba(150, 75, 200, 0.3)";
1416
+ patternCtx.fillRect(0, 0, 10, 10);
1417
+ patternCtx.fillRect(10, 10, 10, 10);
1418
+ const pattern = ctx.createPattern(patternCanvas, "repeat");
1419
+ if (pattern) {
1420
+ ctx.fillStyle = pattern;
1421
+ ctx.fillRect(150, 120, 80, 60);
1422
+ }
1423
+ }
1424
+ // Shadow effects for additional GPU differentiation
1425
+ ctx.shadowColor = "rgba(0, 0, 0, 0.5)";
1426
+ ctx.shadowBlur = 3.2;
1427
+ ctx.shadowOffsetX = 2.1;
1428
+ ctx.shadowOffsetY = 2.7;
1429
+ ctx.fillStyle = "rgba(100, 200, 150, 0.8)";
1430
+ ctx.fillRect(160, 30, 60, 40);
1431
+ // Reset shadow
1432
+ ctx.shadowColor = "transparent";
1433
+ ctx.shadowBlur = 0;
1434
+ ctx.shadowOffsetX = 0;
1435
+ ctx.shadowOffsetY = 0;
1436
+ }
1437
+ /**
1438
+ * Draw multiple text samples with different fonts and effects for maximum differentiation
1439
+ */
1440
+ function drawAdvancedText(ctx) {
1441
+ const fonts = [
1442
+ "14px Arial, sans-serif",
1443
+ '13px "Times New Roman", serif',
1444
+ "12px Georgia, serif",
1445
+ "15px Helvetica, Arial, sans-serif",
1446
+ '11px "Courier New", monospace',
1447
+ "13px Verdana, sans-serif",
1448
+ "16px Impact, fantasy",
1449
+ '12px "Comic Sans MS", cursive',
1450
+ ];
1451
+ const colors = [
1452
+ "#000",
1453
+ "#333",
1454
+ "#666",
1455
+ "#999",
1456
+ "rgba(255, 0, 0, 0.8)",
1457
+ "rgba(0, 255, 0, 0.7)",
1458
+ "rgba(0, 0, 255, 0.6)",
1459
+ "rgba(128, 64, 192, 0.9)",
1460
+ ];
1461
+ // Test different text baselines and alignments
1462
+ const baselines = [
1463
+ "top",
1464
+ "hanging",
1465
+ "middle",
1466
+ "alphabetic",
1467
+ "bottom",
1468
+ ];
1469
+ const alignments = ["left", "center", "right"];
1470
+ let y = 250;
1471
+ CANVAS_TEXTS.forEach((text, textIndex) => {
1472
+ fonts.forEach((font, fontIndex) => {
1473
+ const colorIndex = (textIndex + fontIndex) % colors.length;
1474
+ const baselineIndex = fontIndex % baselines.length;
1475
+ const alignIndex = fontIndex % alignments.length;
1476
+ ctx.font = font;
1477
+ ctx.fillStyle = colors[colorIndex] || colors[0];
1478
+ ctx.textBaseline = baselines[baselineIndex] || "top";
1479
+ ctx.textAlign = alignments[alignIndex] || "left";
1480
+ // Sub-pixel positioning for enhanced differentiation
1481
+ const x = 10 + ((fontIndex * 0.7) % 200);
1482
+ const yPos = y + ((fontIndex * 18.3) % 100);
1483
+ ctx.fillText(text, x, yPos);
1484
+ // Add stroke text for additional GPU stress testing
1485
+ if (fontIndex % 2 === 0) {
1486
+ ctx.strokeStyle = `rgba(${100 + fontIndex * 20}, ${50 + fontIndex * 15}, ${150 + fontIndex * 10}, 0.5)`;
1487
+ ctx.lineWidth = 0.3;
1488
+ ctx.strokeText(text, x + 0.5, yPos + 0.5);
1489
+ }
1490
+ });
1491
+ y += 120;
1492
+ });
1493
+ // Test text with transformations that stress different rendering engines
747
1494
  ctx.save();
1495
+ ctx.translate(100, 400);
1496
+ ctx.rotate(Math.PI / 6);
748
1497
  ctx.scale(1.2, 0.8);
749
- ctx.font = '10px Courier New';
750
- ctx.fillStyle = '#999';
751
- ctx.fillText('Transformed Text', 4, 220);
1498
+ ctx.font = "italic 14px Arial";
1499
+ ctx.fillStyle = "rgba(200, 100, 50, 0.8)";
1500
+ ctx.fillText("Transformed & Rotated Text", 0, 0);
752
1501
  ctx.restore();
1502
+ // Test very small and very large text
1503
+ ctx.font = "6px Arial";
1504
+ ctx.fillStyle = "#000";
1505
+ ctx.fillText("Tiny text rendering test", 10, 500);
1506
+ ctx.font = "32px Arial";
1507
+ ctx.fillStyle = "rgba(255, 0, 0, 0.5)";
1508
+ ctx.fillText("Large", 10, 540);
1509
+ // Reset text properties
1510
+ ctx.textAlign = "left";
1511
+ ctx.textBaseline = "top";
1512
+ }
1513
+ /**
1514
+ * Analyze sub-pixel differences for enhanced GPU/anti-aliasing detection
1515
+ */
1516
+ function analyzeSubPixels(ctx, canvas) {
1517
+ try {
1518
+ // Create test pattern for sub-pixel analysis
1519
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1520
+ // Draw shapes at sub-pixel coordinates to stress anti-aliasing
1521
+ ctx.fillStyle = "rgb(100, 150, 200)";
1522
+ ctx.fillRect(10.3, 10.7, 20.1, 20.9);
1523
+ ctx.strokeStyle = "rgb(200, 100, 50)";
1524
+ ctx.lineWidth = 1.3;
1525
+ ctx.beginPath();
1526
+ ctx.moveTo(15.2, 35.8);
1527
+ ctx.lineTo(25.7, 45.1);
1528
+ ctx.stroke();
1529
+ // Get pixel data for analysis
1530
+ const imageData = ctx.getImageData(10, 10, 30, 40);
1531
+ const pixels = imageData.data;
1532
+ // Calculate sub-pixel characteristics
1533
+ let redSum = 0, greenSum = 0, blueSum = 0;
1534
+ let edgeVariance = 0;
1535
+ for (let i = 0; i < pixels.length; i += 4) {
1536
+ redSum += pixels[i] || 0;
1537
+ greenSum += pixels[i + 1] || 0;
1538
+ blueSum += pixels[i + 2] || 0;
1539
+ // Calculate edge variance for anti-aliasing detection
1540
+ if (i > 0) {
1541
+ const prevR = pixels[i - 4] || 0;
1542
+ const currR = pixels[i] || 0;
1543
+ edgeVariance += Math.abs(currR - prevR);
1544
+ }
1545
+ }
1546
+ const pixelCount = pixels.length / 4;
1547
+ const avgRed = Math.round(redSum / pixelCount);
1548
+ const avgGreen = Math.round(greenSum / pixelCount);
1549
+ const avgBlue = Math.round(blueSum / pixelCount);
1550
+ const avgVariance = Math.round(edgeVariance / pixelCount);
1551
+ return `${avgRed}-${avgGreen}-${avgBlue}-${avgVariance}`;
1552
+ }
1553
+ catch (error) {
1554
+ return "subpixel-error";
1555
+ }
753
1556
  }
754
1557
  /**
755
- * Detect potential incognito mode through canvas inconsistencies
1558
+ * Detect potential incognito mode and canvas blocking through multiple methods
756
1559
  */
757
- function detectInconsistencies(textData, geometryData, ctx) {
1560
+ function detectAdvancedInconsistencies(textHash, geometryHash, subPixelData, ctx) {
1561
+ const reasons = [];
1562
+ let suspiciousSignals = 0;
758
1563
  try {
759
- // Test if getImageData works consistently
1564
+ // Test 1: Check if getImageData works consistently
760
1565
  const imageData = ctx.getImageData(0, 0, 1, 1);
761
- // In some browsers in incognito mode, canvas operations might be slightly different
762
- // This is a heuristic check
763
- if (textData.length < 100 || geometryData.length < 100) {
764
- return true; // Suspiciously short data
765
- }
766
- // Check for specific patterns that might indicate canvas blocking
767
- const hasExpectedPatterns = textData.includes('data:image/png') &&
768
- geometryData.includes('data:image/png');
769
- return !hasExpectedPatterns;
1566
+ if (!imageData || imageData.data.length === 0) {
1567
+ reasons.push("getImageData-blocked");
1568
+ suspiciousSignals += 3;
1569
+ }
1570
+ // Test 2: Check hash lengths (blocked canvas often returns default values)
1571
+ if (textHash.length < 8 || geometryHash.length < 8) {
1572
+ reasons.push("short-hash");
1573
+ suspiciousSignals += 2;
1574
+ }
1575
+ // Test 3: Check for known blocked patterns
1576
+ const knownBlockedHashes = [
1577
+ "error",
1578
+ "blocked",
1579
+ "disabled",
1580
+ "00000000",
1581
+ "ffffffff",
1582
+ "12345678",
1583
+ ];
1584
+ if (knownBlockedHashes.includes(textHash) ||
1585
+ knownBlockedHashes.includes(geometryHash)) {
1586
+ reasons.push("known-blocked-hash");
1587
+ suspiciousSignals += 3;
1588
+ }
1589
+ // Test 4: Sub-pixel analysis indicates blocking
1590
+ if (subPixelData === "subpixel-error" || subPixelData.includes("0-0-0")) {
1591
+ reasons.push("subpixel-anomaly");
1592
+ suspiciousSignals += 2;
1593
+ }
1594
+ // Test 5: Same hash for different operations (indicates fake canvas)
1595
+ if (textHash === geometryHash && textHash !== "error") {
1596
+ reasons.push("identical-hashes");
1597
+ suspiciousSignals += 2;
1598
+ }
1599
+ // Test 6: toDataURL performance (some privacy tools slow it down)
1600
+ const canvas = document.createElement("canvas");
1601
+ canvas.width = 10;
1602
+ canvas.height = 10;
1603
+ const testCtx = canvas.getContext("2d");
1604
+ if (testCtx) {
1605
+ const start = performance.now();
1606
+ testCtx.fillStyle = "red";
1607
+ testCtx.fillRect(0, 0, 10, 10);
1608
+ canvas.toDataURL();
1609
+ const duration = performance.now() - start;
1610
+ // Suspiciously slow canvas operations
1611
+ if (duration > 50) {
1612
+ reasons.push("slow-canvas");
1613
+ suspiciousSignals += 1;
1614
+ }
1615
+ }
1616
+ const confidence = Math.min(suspiciousSignals / 5, 1); // Normalize to 0-1
1617
+ const isInconsistent = suspiciousSignals >= 2; // Threshold for inconsistency
1618
+ return { isInconsistent, confidence, reasons };
770
1619
  }
771
1620
  catch (error) {
772
- // If getImageData fails, it might indicate blocking
773
- return true;
1621
+ return {
1622
+ isInconsistent: true,
1623
+ confidence: 1,
1624
+ reasons: ["detection-error"],
1625
+ };
774
1626
  }
775
1627
  }
776
1628
  /**
777
- * Generate canvas fingerprint
1629
+ * Generate enhanced canvas fingerprint with multiple primitives and sub-pixel analysis
778
1630
  */
779
1631
  async function getCanvasFingerprint() {
780
1632
  const startTime = performance.now();
781
1633
  try {
782
- // Create canvas element
783
- const canvas = document.createElement('canvas');
784
- canvas.width = 200;
785
- canvas.height = 250;
786
- const ctx = canvas.getContext('2d');
1634
+ // Create larger canvas for more detailed fingerprinting
1635
+ const canvas = document.createElement("canvas");
1636
+ canvas.width = 300;
1637
+ canvas.height = 600;
1638
+ const ctx = canvas.getContext("2d", {
1639
+ alpha: true,
1640
+ desynchronized: false,
1641
+ colorSpace: "srgb",
1642
+ willReadFrequently: true,
1643
+ });
787
1644
  if (!ctx) {
788
- throw new Error('Canvas 2D context not available');
1645
+ throw new Error("Canvas 2D context not available");
789
1646
  }
790
- // Test canvas winding (different behavior across browsers)
1647
+ // Test canvas winding (different behavior across browsers/GPUs)
791
1648
  ctx.rect(0, 0, 10, 10);
792
1649
  ctx.rect(2, 2, 6, 6);
793
- const isClockwise = ctx.isPointInPath(5, 5, 'evenodd');
794
- // Draw text
795
- drawText(ctx);
796
- const textData = canvas.toDataURL();
797
- // Clear and draw geometry
1650
+ const isClockwise = ctx.isPointInPath(5, 5, "evenodd");
1651
+ // ENHANCED: Draw advanced text with multiple fonts and effects
1652
+ drawAdvancedText(ctx);
1653
+ const textData = canvas.toDataURL("image/png");
1654
+ const textHash = hash32(textData);
1655
+ // Clear and draw advanced geometry
798
1656
  ctx.clearRect(0, 0, canvas.width, canvas.height);
799
- drawGeometry(ctx);
800
- const geometryData = canvas.toDataURL();
801
- // Check for inconsistencies that might indicate incognito mode
802
- const isInconsistent = detectInconsistencies(textData, geometryData, ctx);
1657
+ drawAdvancedGeometry(ctx);
1658
+ const geometryData = canvas.toDataURL("image/png");
1659
+ const geometryHash = hash32(geometryData);
1660
+ // ENHANCED: Analyze sub-pixels for GPU/anti-aliasing differentiation
1661
+ const subPixelAnalysis = analyzeSubPixels(ctx, canvas);
1662
+ // ENHANCED: Advanced inconsistency detection
1663
+ const inconsistencyAnalysis = detectAdvancedInconsistencies(textHash, geometryHash, subPixelAnalysis, ctx);
1664
+ // Create composite canvas with both text and geometry for additional hash
1665
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
1666
+ // Draw both text and geometry together
1667
+ drawAdvancedText(ctx);
1668
+ ctx.globalCompositeOperation = "multiply";
1669
+ drawAdvancedGeometry(ctx);
1670
+ ctx.globalCompositeOperation = "source-over";
1671
+ const compositeData = canvas.toDataURL("image/png");
1672
+ const compositeHash = hash32(compositeData);
803
1673
  const endTime = performance.now();
804
1674
  const result = {
805
- text: hash32(textData),
806
- geometry: hash32(geometryData),
1675
+ text: textHash,
1676
+ geometry: geometryHash,
807
1677
  winding: isClockwise,
808
- isInconsistent
1678
+ isInconsistent: inconsistencyAnalysis.isInconsistent,
1679
+ // NEW FIELDS
1680
+ subPixelAnalysis, // Sub-pixel characteristics for GPU differentiation
1681
+ compositeHash, // Combined text+geometry hash
1682
+ inconsistencyConfidence: inconsistencyAnalysis.confidence,
1683
+ blockingReasons: inconsistencyAnalysis.reasons,
809
1684
  };
810
1685
  return {
811
1686
  value: result,
812
- duration: endTime - startTime
1687
+ duration: endTime - startTime,
813
1688
  };
814
1689
  }
815
1690
  catch (error) {
816
1691
  return {
817
1692
  value: {
818
- text: 'error',
819
- geometry: 'error',
1693
+ text: "error",
1694
+ geometry: "error",
820
1695
  winding: false,
821
- isInconsistent: true
1696
+ isInconsistent: true,
1697
+ subPixelAnalysis: "error",
1698
+ compositeHash: "error",
1699
+ inconsistencyConfidence: 1,
1700
+ blockingReasons: ["canvas-error"],
822
1701
  },
823
1702
  duration: performance.now() - startTime,
824
- error: error instanceof Error ? error.message : 'Canvas fingerprinting failed'
1703
+ error: error instanceof Error ? error.message : "Canvas fingerprinting failed",
825
1704
  };
826
1705
  }
827
1706
  }
@@ -830,9 +1709,9 @@ async function getCanvasFingerprint() {
830
1709
  */
831
1710
  function isCanvasAvailable$1() {
832
1711
  try {
833
- const canvas = document.createElement('canvas');
834
- const ctx = canvas.getContext('2d');
835
- return ctx !== null && typeof ctx.fillText === 'function';
1712
+ const canvas = document.createElement("canvas");
1713
+ const ctx = canvas.getContext("2d");
1714
+ return ctx !== null && typeof ctx.fillText === "function";
836
1715
  }
837
1716
  catch {
838
1717
  return false;
@@ -1652,347 +2531,370 @@ function collectWebGLParameters(gl) {
1652
2531
  return parameters;
1653
2532
  }
1654
2533
  /**
1655
- * Generate WebGL fingerprint (enhanced with caching)
1656
- */
1657
- async function getWebGLFingerprint() {
1658
- const startTime = performance.now();
1659
- try {
1660
- // Check if WebGL is available for this browser
1661
- if (!isWebGLAvailable()) {
1662
- throw new Error('WebGL not supported in this browser');
1663
- }
1664
- const gl = getCachedWebGLContext();
1665
- if (!gl) {
1666
- throw new Error('WebGL context not available');
1667
- }
1668
- // Verify context is still valid
1669
- if (!isWebGLContextValid()) {
1670
- throw new Error('WebGL context lost');
1671
- }
1672
- // Get basic WebGL information using cached parameters
1673
- const cachedParams = getWebGLParameters();
1674
- const vendor = cachedParams.VENDOR || 'unknown';
1675
- const renderer = cachedParams.RENDERER || 'unknown';
1676
- const version = cachedParams.VERSION || 'unknown';
1677
- // Get unmasked vendor/renderer if available (more specific GPU info)
1678
- let unmaskedVendor = vendor;
1679
- let unmaskedRenderer = renderer;
1680
- try {
1681
- const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
1682
- if (debugInfo) {
1683
- unmaskedVendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) || vendor;
1684
- unmaskedRenderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) || renderer;
1685
- }
1686
- }
1687
- catch {
1688
- // Debug renderer info blocked or not available
1689
- }
1690
- // Collect extensions (cached)
1691
- const extensions = getWebGLExtensions$1();
1692
- // Collect parameters (cached)
1693
- const parameters = collectWebGLParameters(gl);
1694
- // Get shader precision
1695
- const shaderPrecision = getShaderPrecision(gl);
1696
- const endTime = performance.now();
1697
- const result = {
1698
- vendor: unmaskedVendor,
1699
- renderer: unmaskedRenderer,
1700
- version,
1701
- extensions,
1702
- parameters,
1703
- shaderPrecision
1704
- };
1705
- return {
1706
- value: result,
1707
- duration: endTime - startTime
1708
- };
1709
- }
1710
- catch (error) {
1711
- return {
1712
- value: {
1713
- vendor: 'unknown',
1714
- renderer: 'unknown',
1715
- version: 'unknown',
1716
- extensions: [],
1717
- parameters: {},
1718
- shaderPrecision: { vertex: '', fragment: '' }
1719
- },
1720
- duration: performance.now() - startTime,
1721
- error: error instanceof Error ? error.message : 'WebGL fingerprinting failed'
1722
- };
1723
- }
1724
- }
1725
- /**
1726
- * Singleton instance for WebGL availability check
1727
- */
1728
- let webglAvailabilityCache = null;
1729
- /**
1730
- * Check if WebGL fingerprinting is available (with proper cleanup)
1731
- */
1732
- function isWebGLAvailable() {
1733
- // Use cached result if available
1734
- if (webglAvailabilityCache !== null) {
1735
- return webglAvailabilityCache;
1736
- }
1737
- try {
1738
- const canvas = document.createElement('canvas');
1739
- canvas.width = 1;
1740
- canvas.height = 1;
1741
- const gl = canvas.getContext('webgl', {
1742
- failIfMajorPerformanceCaveat: true,
1743
- antialias: false,
1744
- alpha: false,
1745
- depth: false,
1746
- stencil: false,
1747
- preserveDrawingBuffer: false
1748
- }) || canvas.getContext('experimental-webgl', {
1749
- failIfMajorPerformanceCaveat: true,
1750
- antialias: false,
1751
- alpha: false,
1752
- depth: false,
1753
- stencil: false,
1754
- preserveDrawingBuffer: false
1755
- });
1756
- const isAvailable = gl !== null;
1757
- // Proper cleanup to prevent context leak
1758
- if (gl && 'getExtension' in gl) {
1759
- try {
1760
- const webglContext = gl;
1761
- const ext = webglContext.getExtension('WEBGL_lose_context');
1762
- if (ext) {
1763
- ext.loseContext();
1764
- }
1765
- }
1766
- catch (e) {
1767
- // Ignore cleanup errors
1768
- }
1769
- }
1770
- // Clean up canvas
1771
- canvas.width = 0;
1772
- canvas.height = 0;
1773
- // Cache the result
1774
- webglAvailabilityCache = isAvailable;
1775
- return isAvailable;
1776
- }
1777
- catch {
1778
- webglAvailabilityCache = false;
1779
- return false;
1780
- }
1781
- }
1782
-
1783
- /**
1784
- * Browser Detection Utilities
1785
- * Based on FingerprintJS browser detection patterns
1786
- * Uses feature detection instead of user-agent parsing for reliability
1787
- */
1788
- /**
1789
- * Detects if the browser is WebKit-based (Safari, mobile Safari)
1790
- */
1791
- function isWebKit$1() {
1792
- try {
1793
- return ('WebKitAppearance' in document.documentElement.style ||
1794
- 'webkitRequestFileSystem' in window ||
1795
- 'webkitResolveLocalFileSystemURL' in window ||
1796
- Boolean(window.safari));
1797
- }
1798
- catch {
1799
- return false;
1800
- }
1801
- }
1802
- /**
1803
- * Detects if the browser is running on Android
1804
- */
1805
- function isAndroid$1() {
1806
- try {
1807
- return ('ontouchstart' in window &&
1808
- ('orientation' in window || 'onorientationchange' in window) &&
1809
- /android/i.test(navigator.userAgent));
1810
- }
1811
- catch {
1812
- return false;
1813
- }
1814
- }
1815
- /**
1816
- * Detects if the browser is Chrome/Chromium-based
1817
- */
1818
- function isChrome() {
1819
- try {
1820
- return Boolean(window.chrome &&
1821
- (window.chrome.webstore || window.chrome.runtime));
1822
- }
1823
- catch {
1824
- return false;
1825
- }
1826
- }
1827
- /**
1828
- * Detects if the browser is Firefox
1829
- */
1830
- function isFirefox() {
1831
- try {
1832
- return ('InstallTrigger' in window ||
1833
- 'mozInnerScreenX' in window ||
1834
- 'mozPaintCount' in window ||
1835
- Boolean(navigator.mozApps));
1836
- }
1837
- catch {
1838
- return false;
1839
- }
1840
- }
1841
- /**
1842
- * Detects if the browser is Edge (legacy or Chromium)
1843
- */
1844
- function isEdge() {
1845
- try {
1846
- return ('msCredentials' in navigator ||
1847
- Boolean(window.StyleMedia) ||
1848
- (isChrome() && /edg/i.test(navigator.userAgent)));
1849
- }
1850
- catch {
1851
- return false;
1852
- }
1853
- }
1854
- /**
1855
- * Detects if the browser is Safari (not just WebKit)
2534
+ * Create and compile a shader
1856
2535
  */
1857
- function isSafari() {
1858
- try {
1859
- return (isWebKit$1() &&
1860
- !isChrome() &&
1861
- !isEdge() &&
1862
- Boolean(window.safari) &&
1863
- /safari/i.test(navigator.userAgent));
1864
- }
1865
- catch {
1866
- return false;
2536
+ function createShader(gl, type, source) {
2537
+ const shader = gl.createShader(type);
2538
+ if (!shader)
2539
+ return null;
2540
+ gl.shaderSource(shader, source);
2541
+ gl.compileShader(shader);
2542
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
2543
+ gl.deleteShader(shader);
2544
+ return null;
1867
2545
  }
2546
+ return shader;
1868
2547
  }
1869
2548
  /**
1870
- * Detects if the browser is in a secure context
2549
+ * Create and link a shader program
1871
2550
  */
1872
- function isSecureContext() {
1873
- try {
1874
- return window.isSecureContext || location.protocol === 'https:';
1875
- }
1876
- catch {
1877
- return false;
2551
+ function createProgram(gl, vertexShader, fragmentShader) {
2552
+ const program = gl.createProgram();
2553
+ if (!program)
2554
+ return null;
2555
+ gl.attachShader(program, vertexShader);
2556
+ gl.attachShader(program, fragmentShader);
2557
+ gl.linkProgram(program);
2558
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
2559
+ gl.deleteProgram(program);
2560
+ return null;
1878
2561
  }
1879
- }
1880
- /**
1881
- * Detects if the browser is likely in private/incognito mode
1882
- * This is a best-effort detection and not 100% reliable
1883
- */
1884
- function isLikelyPrivateMode() {
1885
- try {
1886
- // Quick checks for obvious private mode indicators
1887
- if ('webkitTemporaryStorage' in navigator) {
1888
- return false; // Likely not private
1889
- }
1890
- // Check for reduced storage quotas (common in private mode)
1891
- if (navigator.storage && navigator.storage.estimate) {
1892
- navigator.storage.estimate().then(estimate => {
1893
- const quota = estimate.quota || 0;
1894
- return quota < 1024 * 1024 * 100; // Less than 100MB likely indicates private mode
1895
- });
2562
+ return program;
2563
+ }
2564
+ /**
2565
+ * Render 3D scene to generate GPU-specific hash
2566
+ * Based on FingerprintJS methodology for maximum differentiation
2567
+ */
2568
+ function render3DScene(gl) {
2569
+ try {
2570
+ // Vertex shader with complex calculations for GPU differentiation
2571
+ const vertexShaderSource = `
2572
+ attribute vec2 a_position;
2573
+ attribute vec3 a_color;
2574
+ varying vec3 v_color;
2575
+ uniform float u_time;
2576
+
2577
+ void main() {
2578
+ // Complex mathematical operations to differentiate GPUs
2579
+ float wave = sin(a_position.x * 10.0 + u_time) * 0.1;
2580
+ vec2 position = a_position + vec2(wave, cos(a_position.y * 8.0) * 0.1);
2581
+
2582
+ // GPU-specific floating point calculations
2583
+ float precision = sin(position.x * 123.456) + cos(position.y * 789.012);
2584
+ position += vec2(precision * 0.001);
2585
+
2586
+ gl_Position = vec4(position, 0.0, 1.0);
2587
+ v_color = a_color;
2588
+ }
2589
+ `;
2590
+ // Fragment shader with GPU-specific precision differences
2591
+ const fragmentShaderSource = `
2592
+ precision mediump float;
2593
+ varying vec3 v_color;
2594
+ uniform float u_time;
2595
+
2596
+ void main() {
2597
+ // Complex color calculations that vary between GPU vendors
2598
+ vec3 color = v_color;
2599
+ color.r += sin(gl_FragCoord.x * 0.1 + u_time) * 0.1;
2600
+ color.g += cos(gl_FragCoord.y * 0.1 + u_time) * 0.1;
2601
+ color.b += sin((gl_FragCoord.x + gl_FragCoord.y) * 0.05) * 0.1;
2602
+
2603
+ // GPU-specific precision in color calculations
2604
+ color = normalize(color) * length(v_color);
2605
+
2606
+ gl_FragColor = vec4(color, 1.0);
2607
+ }
2608
+ `;
2609
+ // Create shaders
2610
+ const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
2611
+ const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
2612
+ if (!vertexShader || !fragmentShader) {
2613
+ return '';
1896
2614
  }
1897
- return false; // Default to not private if we can't determine
1898
- }
1899
- catch {
1900
- return false;
1901
- }
1902
- }
1903
- /**
1904
- * Gets browser engine name for debugging
1905
- */
1906
- function getBrowserEngine$1() {
1907
- if (isChrome())
1908
- return 'Blink';
1909
- if (isFirefox())
1910
- return 'Gecko';
1911
- if (isWebKit$1())
1912
- return 'WebKit';
1913
- if (isEdge())
1914
- return 'EdgeHTML/Blink';
1915
- return 'Unknown';
1916
- }
1917
- /**
1918
- * Checks if the current environment supports DOM manipulation
1919
- */
1920
- function supportsDOMManipulation() {
1921
- try {
1922
- return (typeof document !== 'undefined' &&
1923
- typeof document.createElement === 'function' &&
1924
- typeof document.body !== 'undefined');
2615
+ // Create program
2616
+ const program = createProgram(gl, vertexShader, fragmentShader);
2617
+ if (!program) {
2618
+ return '';
2619
+ }
2620
+ gl.useProgram(program);
2621
+ // Create complex geometry that differentiates GPU rendering
2622
+ const vertices = new Float32Array([
2623
+ // Triangle 1 (with colors)
2624
+ -0.8, -0.6, 1.0, 0.2, 0.3,
2625
+ 0.0, 0.8, 0.3, 1.0, 0.2,
2626
+ 0.8, -0.6, 0.2, 0.3, 1.0,
2627
+ // Triangle 2 (overlapping for blend complexity)
2628
+ -0.5, -0.2, 0.8, 0.9, 0.1,
2629
+ 0.3, 0.6, 0.1, 0.8, 0.9,
2630
+ 0.7, -0.3, 0.9, 0.1, 0.8,
2631
+ ]);
2632
+ const buffer = gl.createBuffer();
2633
+ gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
2634
+ gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
2635
+ // Set up attributes
2636
+ const positionLocation = gl.getAttribLocation(program, 'a_position');
2637
+ const colorLocation = gl.getAttribLocation(program, 'a_color');
2638
+ const timeLocation = gl.getUniformLocation(program, 'u_time');
2639
+ gl.enableVertexAttribArray(positionLocation);
2640
+ gl.enableVertexAttribArray(colorLocation);
2641
+ gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 20, 0);
2642
+ gl.vertexAttribPointer(colorLocation, 3, gl.FLOAT, false, 20, 8);
2643
+ // Set time uniform to create animation-like effects
2644
+ gl.uniform1f(timeLocation, 1.23456789);
2645
+ // Enable blending for more complex GPU calculations
2646
+ gl.enable(gl.BLEND);
2647
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
2648
+ // Clear with specific color to affect background
2649
+ gl.clearColor(0.1, 0.2, 0.3, 1.0);
2650
+ gl.clear(gl.COLOR_BUFFER_BIT);
2651
+ // Draw triangles
2652
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
2653
+ // Read pixels to generate hash
2654
+ const pixels = new Uint8Array(gl.canvas.width * gl.canvas.height * 4);
2655
+ gl.readPixels(0, 0, gl.canvas.width, gl.canvas.height, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
2656
+ // Generate hash from rendered pixels
2657
+ let hash = 0;
2658
+ for (let i = 0; i < pixels.length; i += 4) {
2659
+ // Combine RGBA values with different weights (with null checks)
2660
+ const r = (pixels[i] ?? 0) * 1;
2661
+ const g = (pixels[i + 1] ?? 0) * 256;
2662
+ const b = (pixels[i + 2] ?? 0) * 65536;
2663
+ const a = (pixels[i + 3] ?? 0) * 16777216;
2664
+ const pixelValue = r + g + b + a;
2665
+ hash = (hash * 33 + pixelValue) >>> 0; // Use unsigned 32-bit arithmetic
2666
+ }
2667
+ // Cleanup
2668
+ gl.deleteProgram(program);
2669
+ gl.deleteShader(vertexShader);
2670
+ gl.deleteShader(fragmentShader);
2671
+ gl.deleteBuffer(buffer);
2672
+ return hash.toString(16);
1925
2673
  }
1926
- catch {
1927
- return false;
2674
+ catch (error) {
2675
+ return '';
2676
+ }
2677
+ }
2678
+ /**
2679
+ * Get advanced WebGL capabilities for differentiation
2680
+ */
2681
+ function getAdvancedWebGLCapabilities(gl) {
2682
+ const capabilities = {};
2683
+ try {
2684
+ // GPU Memory information (if available)
2685
+ const memoryInfo = gl.getExtension('WEBGL_debug_renderer_info');
2686
+ if (memoryInfo) {
2687
+ capabilities.unmaskedVendor = gl.getParameter(memoryInfo.UNMASKED_VENDOR_WEBGL);
2688
+ capabilities.unmaskedRenderer = gl.getParameter(memoryInfo.UNMASKED_RENDERER_WEBGL);
2689
+ }
2690
+ // Texture capabilities
2691
+ capabilities.maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);
2692
+ capabilities.maxCombinedTextureImageUnits = gl.getParameter(gl.MAX_COMBINED_TEXTURE_IMAGE_UNITS);
2693
+ // Viewport and rendering capabilities
2694
+ capabilities.maxViewportDims = gl.getParameter(gl.MAX_VIEWPORT_DIMS);
2695
+ capabilities.maxRenderbufferSize = gl.getParameter(gl.MAX_RENDERBUFFER_SIZE);
2696
+ // Vertex and fragment shader limits
2697
+ capabilities.maxVertexAttribs = gl.getParameter(gl.MAX_VERTEX_ATTRIBS);
2698
+ capabilities.maxVertexUniformVectors = gl.getParameter(gl.MAX_VERTEX_UNIFORM_VECTORS);
2699
+ capabilities.maxFragmentUniformVectors = gl.getParameter(gl.MAX_FRAGMENT_UNIFORM_VECTORS);
2700
+ capabilities.maxVaryingVectors = gl.getParameter(gl.MAX_VARYING_VECTORS);
2701
+ // Antialiasing support
2702
+ capabilities.antialias = gl.getContextAttributes()?.antialias || false;
2703
+ // Floating point texture support
2704
+ const floatTextureExt = gl.getExtension('OES_texture_float');
2705
+ capabilities.floatTextures = !!floatTextureExt;
2706
+ // Half float texture support
2707
+ const halfFloatTextureExt = gl.getExtension('OES_texture_half_float');
2708
+ capabilities.halfFloatTextures = !!halfFloatTextureExt;
2709
+ // Compressed texture formats
2710
+ const compressedFormats = gl.getParameter(gl.COMPRESSED_TEXTURE_FORMATS);
2711
+ capabilities.compressedTextureFormats = Array.isArray(compressedFormats) ? compressedFormats.length : 0;
2712
+ // WebGL 2.0 specific features (if available)
2713
+ const isWebGL2 = gl instanceof WebGL2RenderingContext;
2714
+ capabilities.webgl2 = isWebGL2;
2715
+ if (isWebGL2) {
2716
+ const gl2 = gl;
2717
+ capabilities.maxColorAttachments = gl2.getParameter(gl2.MAX_COLOR_ATTACHMENTS);
2718
+ capabilities.maxDrawBuffers = gl2.getParameter(gl2.MAX_DRAW_BUFFERS);
2719
+ capabilities.maxTexture3DSize = gl2.getParameter(gl2.MAX_3D_TEXTURE_SIZE);
2720
+ }
2721
+ // Line width range
2722
+ const lineWidthRange = gl.getParameter(gl.ALIASED_LINE_WIDTH_RANGE);
2723
+ capabilities.lineWidthRange = Array.isArray(lineWidthRange) ? lineWidthRange.join(',') : '';
2724
+ // Point size range
2725
+ const pointSizeRange = gl.getParameter(gl.ALIASED_POINT_SIZE_RANGE);
2726
+ capabilities.pointSizeRange = Array.isArray(pointSizeRange) ? pointSizeRange.join(',') : '';
2727
+ }
2728
+ catch (error) {
2729
+ // Capabilities detection failed
1928
2730
  }
2731
+ return capabilities;
1929
2732
  }
1930
2733
  /**
1931
- * Checks if the current environment supports media queries
2734
+ * Generate WebGL fingerprint (enhanced with 3D rendering and advanced capabilities)
1932
2735
  */
1933
- function supportsMediaQueries() {
2736
+ async function getWebGLFingerprint() {
2737
+ const startTime = performance.now();
1934
2738
  try {
1935
- return (typeof window !== 'undefined' &&
1936
- typeof window.matchMedia === 'function');
2739
+ // Check if WebGL is available for this browser
2740
+ if (!isWebGLAvailable()) {
2741
+ throw new Error('WebGL not supported in this browser');
2742
+ }
2743
+ const gl = getCachedWebGLContext();
2744
+ if (!gl) {
2745
+ throw new Error('WebGL context not available');
2746
+ }
2747
+ // Verify context is still valid
2748
+ if (!isWebGLContextValid()) {
2749
+ throw new Error('WebGL context lost');
2750
+ }
2751
+ // Get basic WebGL information using cached parameters
2752
+ const cachedParams = getWebGLParameters();
2753
+ const vendor = cachedParams.VENDOR || 'unknown';
2754
+ const renderer = cachedParams.RENDERER || 'unknown';
2755
+ const version = cachedParams.VERSION || 'unknown';
2756
+ // ENHANCED: Get unmasked vendor/renderer with retry logic
2757
+ let unmaskedVendor = vendor;
2758
+ let unmaskedRenderer = renderer;
2759
+ for (let attempt = 0; attempt < 3; attempt++) {
2760
+ try {
2761
+ const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
2762
+ if (debugInfo) {
2763
+ const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
2764
+ const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
2765
+ if (vendor && vendor !== 'unknown')
2766
+ unmaskedVendor = vendor;
2767
+ if (renderer && renderer !== 'unknown')
2768
+ unmaskedRenderer = renderer;
2769
+ break; // Success, exit retry loop
2770
+ }
2771
+ }
2772
+ catch (error) {
2773
+ if (attempt === 2) {
2774
+ // Final attempt failed, use fallback
2775
+ break;
2776
+ }
2777
+ // Wait a bit before retry
2778
+ await new Promise(resolve => setTimeout(resolve, 10));
2779
+ }
2780
+ }
2781
+ // Collect extensions (cached)
2782
+ const extensions = getWebGLExtensions$1();
2783
+ // Collect parameters (cached)
2784
+ const parameters = collectWebGLParameters(gl);
2785
+ // Get shader precision
2786
+ const shaderPrecision = getShaderPrecision(gl);
2787
+ // ENHANCED: Render 3D scene for GPU-specific hash
2788
+ const renderHash = render3DScene(gl);
2789
+ // ENHANCED: Get advanced capabilities
2790
+ const capabilities = getAdvancedWebGLCapabilities(gl);
2791
+ // ENHANCED: Create vendor/renderer hash for stable identification
2792
+ const vendorRendererHash = (() => {
2793
+ const vendorStr = unmaskedVendor || vendor || '';
2794
+ const rendererStr = unmaskedRenderer || renderer || '';
2795
+ const combined = `${vendorStr}|${rendererStr}`.toLowerCase();
2796
+ // Simple hash function for consistent results
2797
+ let hash = 0;
2798
+ for (let i = 0; i < combined.length; i++) {
2799
+ const char = combined.charCodeAt(i);
2800
+ hash = ((hash << 5) - hash + char) & 0xffffffff;
2801
+ }
2802
+ return Math.abs(hash).toString(16);
2803
+ })();
2804
+ const endTime = performance.now();
2805
+ const result = {
2806
+ vendor: unmaskedVendor,
2807
+ renderer: unmaskedRenderer,
2808
+ version,
2809
+ extensions,
2810
+ parameters,
2811
+ shaderPrecision,
2812
+ renderHash, // NEW: GPU-specific 3D rendering hash
2813
+ capabilities, // NEW: Advanced WebGL capabilities
2814
+ vendorRendererHash, // NEW: Stable vendor/renderer identification
2815
+ contextAttributes: gl.getContextAttributes() || {} // NEW: Context configuration
2816
+ };
2817
+ return {
2818
+ value: result,
2819
+ duration: endTime - startTime
2820
+ };
1937
2821
  }
1938
- catch {
1939
- return false;
2822
+ catch (error) {
2823
+ return {
2824
+ value: {
2825
+ vendor: 'unknown',
2826
+ renderer: 'unknown',
2827
+ version: 'unknown',
2828
+ extensions: [],
2829
+ parameters: {},
2830
+ shaderPrecision: { vertex: '', fragment: '' },
2831
+ renderHash: '',
2832
+ capabilities: {},
2833
+ vendorRendererHash: '',
2834
+ contextAttributes: {}
2835
+ },
2836
+ duration: performance.now() - startTime,
2837
+ error: error instanceof Error ? error.message : 'WebGL fingerprinting failed'
2838
+ };
1940
2839
  }
1941
2840
  }
1942
2841
  /**
1943
- * Checks if WebGL is likely to be available and stable
2842
+ * Singleton instance for WebGL availability check
1944
2843
  */
1945
- function supportsWebGL() {
1946
- try {
1947
- if (!supportsDOMManipulation())
1948
- return false;
1949
- const canvas = document.createElement('canvas');
1950
- const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
1951
- if (!gl)
1952
- return false;
1953
- // Quick stability check
1954
- const webglContext = gl;
1955
- const renderer = webglContext.getParameter(webglContext.RENDERER);
1956
- return Boolean(renderer && typeof renderer === 'string');
1957
- }
1958
- catch {
1959
- return false;
1960
- }
1961
- }
2844
+ let webglAvailabilityCache = null;
1962
2845
  /**
1963
- * Checks if audio context is available and not suspended
2846
+ * Check if WebGL fingerprinting is available (with proper cleanup)
1964
2847
  */
1965
- function supportsAudioContext() {
2848
+ function isWebGLAvailable() {
2849
+ // Use cached result if available
2850
+ if (webglAvailabilityCache !== null) {
2851
+ return webglAvailabilityCache;
2852
+ }
1966
2853
  try {
1967
- const AudioContext = window.AudioContext || window.webkitAudioContext;
1968
- if (!AudioContext)
1969
- return false;
1970
- // Don't create context here, just check availability
1971
- return true;
2854
+ const canvas = document.createElement('canvas');
2855
+ canvas.width = 1;
2856
+ canvas.height = 1;
2857
+ const gl = canvas.getContext('webgl', {
2858
+ failIfMajorPerformanceCaveat: true,
2859
+ antialias: false,
2860
+ alpha: false,
2861
+ depth: false,
2862
+ stencil: false,
2863
+ preserveDrawingBuffer: false
2864
+ }) || canvas.getContext('experimental-webgl', {
2865
+ failIfMajorPerformanceCaveat: true,
2866
+ antialias: false,
2867
+ alpha: false,
2868
+ depth: false,
2869
+ stencil: false,
2870
+ preserveDrawingBuffer: false
2871
+ });
2872
+ const isAvailable = gl !== null;
2873
+ // Proper cleanup to prevent context leak
2874
+ if (gl && 'getExtension' in gl) {
2875
+ try {
2876
+ const webglContext = gl;
2877
+ const ext = webglContext.getExtension('WEBGL_lose_context');
2878
+ if (ext) {
2879
+ ext.loseContext();
2880
+ }
2881
+ }
2882
+ catch (e) {
2883
+ // Ignore cleanup errors
2884
+ }
2885
+ }
2886
+ // Clean up canvas
2887
+ canvas.width = 0;
2888
+ canvas.height = 0;
2889
+ // Cache the result
2890
+ webglAvailabilityCache = isAvailable;
2891
+ return isAvailable;
1972
2892
  }
1973
2893
  catch {
2894
+ webglAvailabilityCache = false;
1974
2895
  return false;
1975
2896
  }
1976
2897
  }
1977
- function getBrowserCapabilities() {
1978
- const supportsDOM = supportsDOMManipulation();
1979
- const supportsMedia = supportsMediaQueries();
1980
- return {
1981
- engine: getBrowserEngine$1(),
1982
- supportsDOM,
1983
- supportsMediaQueries: supportsMedia,
1984
- supportsWebGL: supportsWebGL(),
1985
- supportsAudio: supportsAudioContext(),
1986
- isSecure: isSecureContext(),
1987
- isPrivateMode: isLikelyPrivateMode(),
1988
- // DOM blockers work best on WebKit and Android
1989
- canRunDOMBlockers: (isWebKit$1() || isAndroid$1()) && supportsDOM,
1990
- // Accessibility requires media queries
1991
- canRunAccessibility: supportsMedia,
1992
- // Browser APIs need secure context for many features
1993
- canRunBrowserAPIs: supportsDOM && typeof navigator !== 'undefined',
1994
- };
1995
- }
1996
2898
 
1997
2899
  /**
1998
2900
  * Enhanced Accessibility & Media Queries Detection Module
@@ -2208,7 +3110,7 @@ function getPointerCapability() {
2208
3110
  * Enhanced with browser compatibility checks
2209
3111
  */
2210
3112
  function isAccessibilityDetectionAvailable() {
2211
- const capabilities = getBrowserCapabilities();
3113
+ const capabilities = getBrowserCapabilities$1();
2212
3114
  return capabilities.canRunAccessibility;
2213
3115
  }
2214
3116
  /**
@@ -2787,120 +3689,275 @@ function getDateTimeLocaleHash(localeData) {
2787
3689
  }
2788
3690
 
2789
3691
  /**
2790
- * Font Preferences Detection
2791
- * Based on FingerprintJS font preferences detection
3692
+ * Enhanced Font Preferences Detection with Iframe Isolation
3693
+ * Based on FingerprintJS font preferences detection with enhanced security and accuracy
2792
3694
  *
2793
- * This component measures text rendering widths with different font settings
3695
+ * This component measures text rendering with different font settings using isolated iframes
2794
3696
  * to detect user's font preferences, OS-specific fonts, and browser rendering differences.
2795
3697
  *
2796
3698
  * Key advantages:
2797
- * - Detects OS-specific font rendering
2798
- * - Captures user font size preferences
2799
- * - Reveals browser-specific text rendering
2800
- * - Very stable across sessions
2801
- * - Difficult to spoof
3699
+ * - Iframe isolation prevents external interference
3700
+ * - Detects OS-specific font rendering and anti-aliasing
3701
+ * - Captures user font size preferences and system settings
3702
+ * - Reveals browser-specific text rendering differences
3703
+ * - Very stable across sessions and difficult to spoof
3704
+ * - Enhanced with sub-pixel rendering analysis
2802
3705
  */
2803
3706
  /**
2804
3707
  * Text content for measurements - chosen for maximum variation
2805
3708
  */
2806
- const defaultText = 'mmMwWLliI0fiflO&1';
2807
- const cjkText = '中文测试';
2808
- const arabicText = 'اختبار';
2809
- const hebrewText = 'בדיקה';
2810
- const emojiText = '🎯🔥💯';
2811
- const mathText = '∑∆∇∂';
2812
- const ligatureText = 'ffi ffl fi fl';
3709
+ const defaultText = "mmMwWLliI0fiflO&1";
3710
+ const cjkText = "中文测试";
3711
+ const arabicText = "اختبار";
3712
+ const hebrewText = "בדיקה";
3713
+ const emojiText = "🎯🔥💯";
3714
+ const mathText = "∑∆∇∂";
3715
+ const ligatureText = "ffi ffl fi fl";
2813
3716
  const presets = {
2814
3717
  // Basic font families
2815
3718
  default: { text: defaultText },
2816
3719
  serif: {
2817
- style: { fontFamily: 'serif' },
2818
- text: defaultText
3720
+ style: { fontFamily: "serif" },
3721
+ text: defaultText,
2819
3722
  },
2820
3723
  sans: {
2821
- style: { fontFamily: 'sans-serif' },
2822
- text: defaultText
3724
+ style: { fontFamily: "sans-serif" },
3725
+ text: defaultText,
2823
3726
  },
2824
3727
  mono: {
2825
- style: { fontFamily: 'monospace' },
2826
- text: defaultText
3728
+ style: { fontFamily: "monospace" },
3729
+ text: defaultText,
2827
3730
  },
2828
3731
  // OS-specific fonts
2829
3732
  apple: {
2830
- style: { font: '-apple-system-body' },
2831
- text: defaultText
3733
+ style: { font: "-apple-system-body" },
3734
+ text: defaultText,
2832
3735
  },
2833
3736
  system: {
2834
- style: { fontFamily: 'system-ui' },
2835
- text: defaultText
3737
+ style: { fontFamily: "system-ui" },
3738
+ text: defaultText,
2836
3739
  },
2837
3740
  // Size variations
2838
3741
  min: {
2839
- style: { fontSize: '1px' },
2840
- text: defaultText
3742
+ style: { fontSize: "1px" },
3743
+ text: defaultText,
2841
3744
  },
2842
3745
  large: {
2843
- style: { fontSize: '72px' },
2844
- text: defaultText
3746
+ style: { fontSize: "72px" },
3747
+ text: defaultText,
2845
3748
  },
2846
3749
  // Browser UI fonts
2847
3750
  ui: {
2848
- style: { fontFamily: 'ui-serif' },
2849
- text: defaultText
3751
+ style: { fontFamily: "ui-serif" },
3752
+ text: defaultText,
2850
3753
  },
2851
3754
  emoji: {
2852
- style: { fontFamily: 'Apple Color Emoji, Segoe UI Emoji, Noto Color Emoji' },
2853
- text: emojiText
3755
+ style: {
3756
+ fontFamily: "Apple Color Emoji, Segoe UI Emoji, Noto Color Emoji",
3757
+ },
3758
+ text: emojiText,
2854
3759
  },
2855
3760
  math: {
2856
- style: { fontFamily: 'STIX Two Math, Cambria Math' },
2857
- text: mathText
3761
+ style: { fontFamily: "STIX Two Math, Cambria Math" },
3762
+ text: mathText,
2858
3763
  },
2859
3764
  // Script-specific fonts
2860
3765
  cjk: {
2861
- style: { fontFamily: 'Hiragino Sans, Microsoft YaHei, SimSun' },
2862
- text: cjkText
3766
+ style: { fontFamily: "Hiragino Sans, Microsoft YaHei, SimSun" },
3767
+ text: cjkText,
2863
3768
  },
2864
3769
  arabic: {
2865
- style: { fontFamily: 'Tahoma, Arial Unicode MS' },
2866
- text: arabicText
3770
+ style: { fontFamily: "Tahoma, Arial Unicode MS" },
3771
+ text: arabicText,
2867
3772
  },
2868
3773
  hebrew: {
2869
- style: { fontFamily: 'Tahoma, Arial Unicode MS' },
2870
- text: hebrewText
3774
+ style: { fontFamily: "Tahoma, Arial Unicode MS" },
3775
+ text: hebrewText,
2871
3776
  },
2872
3777
  // Advanced rendering features
2873
3778
  subpixel: {
2874
3779
  style: {
2875
- fontFamily: 'Arial',
2876
- fontSize: '11px',
2877
- textRendering: 'optimizeLegibility'
3780
+ fontFamily: "Arial",
3781
+ fontSize: "11px",
3782
+ textRendering: "optimizeLegibility",
2878
3783
  },
2879
- text: defaultText
3784
+ text: defaultText,
2880
3785
  },
2881
3786
  kerning: {
2882
3787
  style: {
2883
- fontFamily: 'Times',
2884
- fontSize: '24px',
2885
- fontKerning: 'normal'
3788
+ fontFamily: "Times",
3789
+ fontSize: "24px",
3790
+ fontKerning: "normal",
2886
3791
  },
2887
- text: 'AV To'
3792
+ text: "AV To",
2888
3793
  },
2889
3794
  ligatures: {
2890
3795
  style: {
2891
- fontFamily: 'Times',
2892
- fontSize: '24px',
2893
- fontVariantLigatures: 'common-ligatures'
3796
+ fontFamily: "Times",
3797
+ fontSize: "24px",
3798
+ fontVariantLigatures: "common-ligatures",
2894
3799
  },
2895
- text: ligatureText
2896
- }
3800
+ text: ligatureText,
3801
+ },
2897
3802
  };
3803
+ /**
3804
+ * Analyze rendering quality to detect anti-aliasing and font smoothing
3805
+ */
3806
+ function analyzeRenderingQuality(iframeDoc, container, fontFamily = "Arial") {
3807
+ try {
3808
+ // Create canvas for pixel analysis
3809
+ const canvas = iframeDoc.createElement("canvas");
3810
+ canvas.width = 100;
3811
+ canvas.height = 50;
3812
+ const ctx = canvas.getContext("2d", { willReadFrequently: true });
3813
+ if (!ctx)
3814
+ return { quality: 0, antiAliasing: false, smoothing: 0 };
3815
+ // Draw text at sub-pixel position to trigger anti-aliasing
3816
+ ctx.font = `24px "${fontFamily}"`;
3817
+ ctx.fillStyle = "#000000";
3818
+ ctx.fillText("Wg", 10.5, 30.7);
3819
+ // Analyze pixel data for anti-aliasing detection
3820
+ const imageData = ctx.getImageData(10, 20, 30, 20);
3821
+ const pixels = imageData.data;
3822
+ let edgePixels = 0;
3823
+ let grayscalePixels = 0;
3824
+ let totalNonWhitePixels = 0;
3825
+ for (let i = 0; i < pixels.length; i += 4) {
3826
+ const r = pixels[i] || 0;
3827
+ const g = pixels[i + 1] || 0;
3828
+ const b = pixels[i + 2] || 0;
3829
+ const alpha = pixels[i + 3] || 0;
3830
+ // Skip transparent pixels
3831
+ if (alpha < 10)
3832
+ continue;
3833
+ totalNonWhitePixels++;
3834
+ // Detect grayscale pixels (anti-aliasing indicator)
3835
+ if (r === g && g === b && r > 0 && r < 255) {
3836
+ grayscalePixels++;
3837
+ }
3838
+ // Detect edge transitions
3839
+ if (i > 4 && Math.abs(r - (pixels[i - 4] || 0)) > 50) {
3840
+ edgePixels++;
3841
+ }
3842
+ }
3843
+ const antiAliasingRatio = totalNonWhitePixels > 0 ? grayscalePixels / totalNonWhitePixels : 0;
3844
+ const smoothingLevel = totalNonWhitePixels > 0
3845
+ ? (grayscalePixels * 100) / totalNonWhitePixels
3846
+ : 0;
3847
+ return {
3848
+ quality: Math.round(smoothingLevel),
3849
+ antiAliasing: antiAliasingRatio > 0.1,
3850
+ smoothing: Math.round(smoothingLevel * 100) / 100,
3851
+ };
3852
+ }
3853
+ catch (error) {
3854
+ return { quality: 0, antiAliasing: false, smoothing: 0 };
3855
+ }
3856
+ }
3857
+ /**
3858
+ * Detect system font hinting and ClearType
3859
+ */
3860
+ function detectSystemFontHints(iframeDoc, container) {
3861
+ try {
3862
+ const canvas = iframeDoc.createElement("canvas");
3863
+ canvas.width = 100;
3864
+ canvas.height = 50;
3865
+ const ctx = canvas.getContext("2d", { willReadFrequently: true });
3866
+ if (!ctx) {
3867
+ return { clearType: false, hintingLevel: 0, subpixelRendering: false };
3868
+ }
3869
+ // Test with known problematic character combinations
3870
+ const testTexts = ["rn", "il", "WW", "mm"];
3871
+ let clearTypeIndicators = 0;
3872
+ let subpixelIndicators = 0;
3873
+ let hintingScore = 0;
3874
+ for (const text of testTexts) {
3875
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
3876
+ ctx.font = "16px Arial";
3877
+ ctx.fillStyle = "#000";
3878
+ ctx.fillText(text, 10, 25);
3879
+ const imageData = ctx.getImageData(10, 15, 40, 20);
3880
+ const pixels = imageData.data;
3881
+ // Analyze RGB sub-pixel rendering patterns
3882
+ for (let i = 0; i < pixels.length; i += 12) {
3883
+ // Check every 3rd pixel
3884
+ const r1 = pixels[i] || 0;
3885
+ const g1 = pixels[i + 1] || 0;
3886
+ const b1 = pixels[i + 2] || 0;
3887
+ const r2 = pixels[i + 4] || 0;
3888
+ const g2 = pixels[i + 5] || 0;
3889
+ const b2 = pixels[i + 6] || 0;
3890
+ // ClearType creates RGB fringing
3891
+ if (Math.abs(r1 - g1) > 20 || Math.abs(g1 - b1) > 20) {
3892
+ clearTypeIndicators++;
3893
+ }
3894
+ // Sub-pixel rendering shows color channel differences
3895
+ if (Math.abs(r1 - r2) + Math.abs(g1 - g2) + Math.abs(b1 - b2) > 50) {
3896
+ subpixelIndicators++;
3897
+ }
3898
+ // Font hinting creates consistent patterns
3899
+ if ((r1 > 0 && r1 < 255) ||
3900
+ (g1 > 0 && g1 < 255) ||
3901
+ (b1 > 0 && b1 < 255)) {
3902
+ hintingScore++;
3903
+ }
3904
+ }
3905
+ }
3906
+ return {
3907
+ clearType: clearTypeIndicators > 10,
3908
+ hintingLevel: Math.min(Math.round(hintingScore / 10), 5),
3909
+ subpixelRendering: subpixelIndicators > 5,
3910
+ };
3911
+ }
3912
+ catch (error) {
3913
+ return {
3914
+ clearType: false,
3915
+ hintingLevel: 0,
3916
+ subpixelRendering: false,
3917
+ };
3918
+ }
3919
+ }
3920
+ /**
3921
+ * Test font fallback behavior
3922
+ */
3923
+ function testFontFallback(iframeDoc, container, fontFamily) {
3924
+ try {
3925
+ const testString = "mmmmmmmmmmlli"; // Characters that vary significantly between fonts
3926
+ // Measure with specified font
3927
+ const withFont = measureText(iframeDoc, container, {
3928
+ style: { fontFamily },
3929
+ text: testString,
3930
+ });
3931
+ // Measure with known fallback
3932
+ const withFallback = measureText(iframeDoc, container, {
3933
+ style: { fontFamily: "monospace" },
3934
+ text: testString,
3935
+ });
3936
+ // Compare dimensions to detect fallback
3937
+ const widthDiff = Math.abs(withFont - withFallback);
3938
+ if (widthDiff < 1) {
3939
+ return "monospace-fallback";
3940
+ }
3941
+ const withSansSerif = measureText(iframeDoc, container, {
3942
+ style: { fontFamily: "sans-serif" },
3943
+ text: testString,
3944
+ });
3945
+ const sansWidthDiff = Math.abs(withFont - withSansSerif);
3946
+ if (sansWidthDiff < 1) {
3947
+ return "sans-serif-fallback";
3948
+ }
3949
+ return "native-font";
3950
+ }
3951
+ catch (error) {
3952
+ return "fallback-error";
3953
+ }
3954
+ }
2898
3955
  /**
2899
3956
  * Create measurement iframe for isolated font rendering
2900
3957
  */
2901
3958
  function createMeasurementIframe() {
2902
3959
  return new Promise((resolve, reject) => {
2903
- const iframe = document.createElement('iframe');
3960
+ const iframe = document.createElement("iframe");
2904
3961
  // Iframe HTML with proper viewport for Android font size detection
2905
3962
  const iframeHtml = `<!doctype html>
2906
3963
  <html>
@@ -2919,14 +3976,15 @@ function createMeasurementIframe() {
2919
3976
  <body></body>
2920
3977
  </html>`;
2921
3978
  // Hide iframe
2922
- iframe.style.cssText = 'position: absolute; left: -9999px; top: -9999px; width: 1px; height: 1px; opacity: 0;';
3979
+ iframe.style.cssText =
3980
+ "position: absolute; left: -9999px; top: -9999px; width: 1px; height: 1px; opacity: 0;";
2923
3981
  iframe.srcdoc = iframeHtml;
2924
3982
  iframe.onload = () => {
2925
3983
  try {
2926
3984
  const iframeDocument = iframe.contentDocument;
2927
3985
  const iframeWindow = iframe.contentWindow;
2928
3986
  if (!iframeDocument || !iframeWindow) {
2929
- reject(new Error('Cannot access iframe content'));
3987
+ reject(new Error("Cannot access iframe content"));
2930
3988
  return;
2931
3989
  }
2932
3990
  const body = iframeDocument.body;
@@ -2935,11 +3993,11 @@ function createMeasurementIframe() {
2935
3993
  body.style.zoom = `${1 / iframeWindow.devicePixelRatio}`;
2936
3994
  }
2937
3995
  else if (isWebKit$1()) {
2938
- body.style.zoom = 'reset';
3996
+ body.style.zoom = "reset";
2939
3997
  }
2940
3998
  // Add dummy content for Android font size detection
2941
- const dummyText = iframeDocument.createElement('div');
2942
- dummyText.textContent = Array(200).fill('word').join(' ');
3999
+ const dummyText = iframeDocument.createElement("div");
4000
+ dummyText.textContent = Array(200).fill("word").join(" ");
2943
4001
  body.appendChild(dummyText);
2944
4002
  const cleanup = () => {
2945
4003
  if (iframe.parentNode) {
@@ -2949,7 +4007,7 @@ function createMeasurementIframe() {
2949
4007
  resolve({
2950
4008
  document: iframeDocument,
2951
4009
  container: body,
2952
- cleanup
4010
+ cleanup,
2953
4011
  });
2954
4012
  }
2955
4013
  catch (error) {
@@ -2957,7 +4015,7 @@ function createMeasurementIframe() {
2957
4015
  }
2958
4016
  };
2959
4017
  iframe.onerror = () => {
2960
- reject(new Error('Failed to load iframe'));
4018
+ reject(new Error("Failed to load iframe"));
2961
4019
  };
2962
4020
  document.body.appendChild(iframe);
2963
4021
  });
@@ -2966,12 +4024,13 @@ function createMeasurementIframe() {
2966
4024
  * Measure text width with specific font settings
2967
4025
  */
2968
4026
  function measureText(document, container, preset) {
2969
- const element = document.createElement('span');
4027
+ const element = document.createElement("span");
2970
4028
  const { style = {}, text = defaultText } = preset;
2971
4029
  // Set text content
2972
4030
  element.textContent = text;
2973
4031
  // Apply base styles
2974
- element.style.cssText = 'position: absolute; left: 0; top: 0; white-space: nowrap; visibility: hidden;';
4032
+ element.style.cssText =
4033
+ "position: absolute; left: 0; top: 0; white-space: nowrap; visibility: hidden;";
2975
4034
  // Apply preset styles
2976
4035
  Object.entries(style).forEach(([key, value]) => {
2977
4036
  if (value !== undefined) {
@@ -2985,11 +4044,11 @@ function measureText(document, container, preset) {
2985
4044
  return Math.round(width * 100) / 100; // Round to 2 decimal places
2986
4045
  }
2987
4046
  /**
2988
- * Get font preferences fingerprint
4047
+ * Get enhanced font preferences fingerprint with iframe isolation
2989
4048
  */
2990
4049
  async function getFontPreferences() {
2991
4050
  try {
2992
- const { document: iframeDoc, container, cleanup } = await createMeasurementIframe();
4051
+ const { document: iframeDoc, container, cleanup, } = await createMeasurementIframe();
2993
4052
  const measurements = {};
2994
4053
  // Measure all presets
2995
4054
  for (const [key, preset] of Object.entries(presets)) {
@@ -3001,15 +4060,68 @@ async function getFontPreferences() {
3001
4060
  measurements[key] = 0;
3002
4061
  }
3003
4062
  }
4063
+ // NEW: Enhanced rendering analysis
4064
+ const renderingAnalysis = analyzeRenderingQuality(iframeDoc, container, "Arial");
4065
+ const systemHints = detectSystemFontHints(iframeDoc, container);
4066
+ // NEW: Test fallback behavior for common fonts
4067
+ const fallbackBehavior = {};
4068
+ const commonFonts = [
4069
+ "Arial",
4070
+ "Helvetica",
4071
+ "Times New Roman",
4072
+ "Georgia",
4073
+ "Courier New",
4074
+ ];
4075
+ for (const font of commonFonts) {
4076
+ try {
4077
+ fallbackBehavior[font] = testFontFallback(iframeDoc, container, font);
4078
+ }
4079
+ catch (error) {
4080
+ fallbackBehavior[font] = "fallback-error";
4081
+ }
4082
+ }
4083
+ // NEW: Create comprehensive hash for isolation verification
4084
+ const hashComponents = [
4085
+ JSON.stringify(measurements),
4086
+ JSON.stringify(renderingAnalysis),
4087
+ JSON.stringify(systemHints),
4088
+ JSON.stringify(fallbackBehavior),
4089
+ ];
4090
+ const isolatedHash = hash32(hashComponents.join("|"));
3004
4091
  cleanup();
3005
- return measurements;
4092
+ return {
4093
+ ...measurements,
4094
+ antiAliasing: {
4095
+ enabled: renderingAnalysis.antiAliasing,
4096
+ type: systemHints.clearType
4097
+ ? "cleartype"
4098
+ : renderingAnalysis.antiAliasing
4099
+ ? "standard"
4100
+ : "none",
4101
+ smoothingLevel: renderingAnalysis.smoothing,
4102
+ },
4103
+ systemFontHints: systemHints,
4104
+ fallbackBehavior,
4105
+ isolatedHash,
4106
+ };
3006
4107
  }
3007
4108
  catch (error) {
3008
4109
  // Return fallback measurements if iframe creation fails
3009
- return Object.keys(presets).reduce((acc, key) => {
4110
+ const fallbackMeasurements = Object.keys(presets).reduce((acc, key) => {
3010
4111
  acc[key] = 0;
3011
4112
  return acc;
3012
4113
  }, {});
4114
+ return {
4115
+ ...fallbackMeasurements,
4116
+ antiAliasing: { enabled: false, type: "unknown", smoothingLevel: 0 },
4117
+ systemFontHints: {
4118
+ clearType: false,
4119
+ hintingLevel: 0,
4120
+ subpixelRendering: false,
4121
+ },
4122
+ fallbackBehavior: {},
4123
+ isolatedHash: "error",
4124
+ };
3013
4125
  }
3014
4126
  }
3015
4127
  /**
@@ -3017,24 +4129,52 @@ async function getFontPreferences() {
3017
4129
  */
3018
4130
  function isFontPreferencesAvailable() {
3019
4131
  try {
3020
- return typeof document !== 'undefined' &&
3021
- typeof document.createElement === 'function' &&
3022
- typeof document.body !== 'undefined';
4132
+ return (typeof document !== "undefined" &&
4133
+ typeof document.createElement === "function" &&
4134
+ typeof document.body !== "undefined");
3023
4135
  }
3024
4136
  catch {
3025
4137
  return false;
3026
4138
  }
3027
4139
  }
3028
4140
  /**
3029
- * Analyze font preferences for confidence and uniqueness
4141
+ * Analyze enhanced font preferences for confidence and uniqueness
3030
4142
  */
3031
4143
  function analyzeFontPreferences(preferences) {
3032
- const values = Object.values(preferences);
4144
+ // Get numeric measurements (excluding enhanced objects)
4145
+ const numericValues = [
4146
+ preferences.default,
4147
+ preferences.serif,
4148
+ preferences.sans,
4149
+ preferences.mono,
4150
+ preferences.apple,
4151
+ preferences.system,
4152
+ preferences.min,
4153
+ preferences.large,
4154
+ preferences.ui,
4155
+ preferences.emoji,
4156
+ preferences.math,
4157
+ preferences.cjk,
4158
+ preferences.arabic,
4159
+ preferences.hebrew,
4160
+ preferences.subpixel,
4161
+ preferences.kerning,
4162
+ preferences.ligatures,
4163
+ ];
3033
4164
  // Check if we have real measurements (not all zeros)
3034
- const hasRealValues = values.some(val => val > 0);
3035
- const nonZeroCount = values.filter(val => val > 0).length;
3036
- const totalCount = values.length;
3037
- let confidence = hasRealValues ? (nonZeroCount / totalCount) : 0;
4165
+ const hasRealValues = numericValues.some((val) => val > 0);
4166
+ const nonZeroCount = numericValues.filter((val) => val > 0).length;
4167
+ const totalCount = numericValues.length;
4168
+ let confidence = hasRealValues ? nonZeroCount / totalCount : 0;
4169
+ // Boost confidence if enhanced features are working
4170
+ if (preferences.antiAliasing.enabled ||
4171
+ preferences.systemFontHints.clearType) {
4172
+ confidence += 0.2;
4173
+ }
4174
+ if (Object.keys(preferences.fallbackBehavior).length > 0) {
4175
+ confidence += 0.1;
4176
+ }
4177
+ confidence = Math.min(confidence, 1);
3038
4178
  // Check for system font support
3039
4179
  const hasSystemFonts = preferences.apple > 0 || preferences.system > 0;
3040
4180
  // Check for advanced rendering features
@@ -3043,25 +4183,80 @@ function analyzeFontPreferences(preferences) {
3043
4183
  preferences.kerning,
3044
4184
  preferences.ligatures,
3045
4185
  preferences.emoji,
3046
- preferences.math
3047
- ].some(val => val > 0);
4186
+ preferences.math,
4187
+ ].some((val) => val > 0) ||
4188
+ preferences.antiAliasing.enabled ||
4189
+ preferences.systemFontHints.clearType;
3048
4190
  // Calculate uniqueness based on measurement variance
3049
- const average = values.reduce((sum, val) => sum + val, 0) / values.length;
3050
- const variance = values.reduce((sum, val) => sum + Math.pow(val - average, 2), 0) / values.length;
3051
- const uniqueness = Math.min(variance / 100, 1); // Normalize to 0-1
4191
+ const average = numericValues.reduce((sum, val) => sum + val, 0) / numericValues.length;
4192
+ const variance = numericValues.reduce((sum, val) => sum + Math.pow(val - average, 2), 0) /
4193
+ numericValues.length;
4194
+ let uniqueness = Math.min(variance / 100, 1); // Normalize to 0-1
4195
+ // Boost uniqueness with enhanced features
4196
+ if (preferences.antiAliasing.smoothingLevel > 0) {
4197
+ uniqueness += 0.1;
4198
+ }
4199
+ if (preferences.systemFontHints.hintingLevel > 0) {
4200
+ uniqueness += 0.1;
4201
+ }
4202
+ uniqueness = Math.min(uniqueness, 1);
4203
+ // Determine rendering quality
4204
+ let renderingQuality = "low";
4205
+ const smoothingLevel = preferences.antiAliasing.smoothingLevel;
4206
+ if (smoothingLevel > 50 || preferences.systemFontHints.clearType) {
4207
+ renderingQuality = "high";
4208
+ }
4209
+ else if (smoothingLevel > 20 ||
4210
+ preferences.systemFontHints.hintingLevel > 2) {
4211
+ renderingQuality = "medium";
4212
+ }
4213
+ // Check isolation integrity
4214
+ const isolationIntegrity = preferences.isolatedHash !== "error" && preferences.isolatedHash.length > 0;
3052
4215
  return {
3053
4216
  confidence: Math.round(confidence * 100) / 100,
3054
4217
  uniqueness: Math.round(uniqueness * 100) / 100,
3055
4218
  hasSystemFonts,
3056
- hasAdvancedFeatures
4219
+ hasAdvancedFeatures,
4220
+ renderingQuality,
4221
+ isolationIntegrity,
3057
4222
  };
3058
4223
  }
3059
4224
  /**
3060
- * Get a compact hash of font preferences for quick comparison
4225
+ * Get a compact hash of enhanced font preferences for quick comparison
3061
4226
  */
3062
4227
  function getFontPreferencesHash(preferences) {
3063
- const values = Object.values(preferences).map(val => Math.round(val));
3064
- return values.join(',');
4228
+ // Use the isolated hash if available (most comprehensive)
4229
+ if (preferences.isolatedHash && preferences.isolatedHash !== "error") {
4230
+ return preferences.isolatedHash;
4231
+ }
4232
+ // Fallback to creating hash from numeric values
4233
+ const numericValues = [
4234
+ preferences.default,
4235
+ preferences.serif,
4236
+ preferences.sans,
4237
+ preferences.mono,
4238
+ preferences.apple,
4239
+ preferences.system,
4240
+ preferences.min,
4241
+ preferences.large,
4242
+ preferences.ui,
4243
+ preferences.emoji,
4244
+ preferences.math,
4245
+ preferences.cjk,
4246
+ preferences.arabic,
4247
+ preferences.hebrew,
4248
+ preferences.subpixel,
4249
+ preferences.kerning,
4250
+ preferences.ligatures,
4251
+ ].map((val) => Math.round(val));
4252
+ // Include rendering characteristics in fallback hash
4253
+ const renderingIndicators = [
4254
+ preferences.antiAliasing.enabled ? 1 : 0,
4255
+ preferences.systemFontHints.clearType ? 1 : 0,
4256
+ preferences.systemFontHints.hintingLevel,
4257
+ Math.round(preferences.antiAliasing.smoothingLevel),
4258
+ ];
4259
+ return [...numericValues, ...renderingIndicators].join(",");
3065
4260
  }
3066
4261
 
3067
4262
  /**
@@ -4361,14 +5556,21 @@ async function collectFingerprint(options = {}) {
4361
5556
  // Silently fail - device signals are optional
4362
5557
  }
4363
5558
  const coreVector = {
4364
- // User agent family and major version only
4365
- // PRIORITY 1: Browser family only (no version - versions change frequently)
5559
+ // PRIORITY 1: Enhanced browser detection with precise Chromium-based browser identification
4366
5560
  ua: (() => {
4367
- const ua = uaStr;
4368
- const m = ua.match(/(chrome|safari|firefox|edge|edg|brave|opera|opr|chromium)/i);
4369
- // Only use browser family, NOT version (versions update frequently causing hash changes)
4370
- const family = (m?.[1] || ua.split(" ")[0] || "unknown").toLowerCase();
4371
- return family; // Remove version for maximum stability
5561
+ try {
5562
+ const browserInfo = getBrowserInfo();
5563
+ // Use the precise browser family for maximum differentiation
5564
+ // This properly distinguishes Brave, Arc, Opera, etc. from Chrome
5565
+ return browserInfo.family; // e.g., "brave-chromium", "arc-chromium", "chrome", "firefox"
5566
+ }
5567
+ catch {
5568
+ // Fallback to legacy detection if imports fail
5569
+ const ua = uaStr;
5570
+ const m = ua.match(/(chrome|safari|firefox|edge|edg|brave|opera|opr|chromium)/i);
5571
+ const family = (m?.[1] || ua.split(" ")[0] || "unknown").toLowerCase();
5572
+ return family;
5573
+ }
4372
5574
  })(),
4373
5575
  // PRIORITY 2: Timezone (very stable, changes rarely)
4374
5576
  timezone: browser?.timezone || "unknown",
@@ -4381,40 +5583,113 @@ async function collectFingerprint(options = {}) {
4381
5583
  platform: (browser?.platform || "unknown").toLowerCase(),
4382
5584
  // PRIORITY 5: Device type (critical for differentiation, ultra-stable)
4383
5585
  deviceType,
4384
- // PRIORITY 6: Screen resolution bucket (more stable than exact pixels)
5586
+ // PRIORITY 6: Enhanced screen resolution buckets with 2024 display trends
4385
5587
  screen: screen
4386
5588
  ? (() => {
4387
- // Use screen buckets instead of exact pixels for stability
5589
+ // Use enhanced screen buckets for modern display landscape
4388
5590
  const dims = [screen.width, screen.height].sort((a, b) => b - a);
4389
5591
  const w = dims[0];
4390
5592
  const h = dims[1];
4391
- // Create stability buckets that group similar resolutions
5593
+ // Enhanced screen bucketing based on 2024 display standards
4392
5594
  const getScreenBucket = (width, height) => {
4393
- // Common resolution buckets for stability
5595
+ // Ultra-high resolution displays (2024: 8K, 5K, etc.)
5596
+ if (width >= 7680)
5597
+ return "8k"; // 8K displays
5598
+ if (width >= 5120)
5599
+ return "5k"; // 5K displays (iMac, etc.)
5600
+ if (width >= 4096)
5601
+ return "4k_cinema"; // Cinema 4K
5602
+ if (width >= 3840)
5603
+ return "4k_uhd"; // 4K UHD standard
5604
+ if (width >= 3440)
5605
+ return "ultrawide"; // Ultrawide QHD
5606
+ if (width >= 2880)
5607
+ return "retina_4k"; // Retina 4K/5K scaled
4394
5608
  if (width >= 2560)
4395
- return "4k"; // 4K and above
5609
+ return "qhd"; // QHD/WQHD
5610
+ // Standard high-resolution displays
5611
+ if (width >= 2048)
5612
+ return "qxga"; // QXGA
4396
5613
  if (width >= 1920)
4397
- return "fhd"; // Full HD
5614
+ return "fhd"; // Full HD 1080p
5615
+ if (width >= 1680)
5616
+ return "wsxga+"; // WSXGA+
4398
5617
  if (width >= 1600)
4399
- return "hd+"; // HD+
5618
+ return "uxga"; // UXGA
5619
+ if (width >= 1440)
5620
+ return "wxga++"; // WXGA++
4400
5621
  if (width >= 1366)
4401
- return "hd"; // HD
5622
+ return "hd"; // HD 720p
5623
+ if (width >= 1280)
5624
+ return "sxga"; // SXGA
5625
+ // Tablet and mobile displays
4402
5626
  if (width >= 1024)
4403
- return "xga"; // XGA
5627
+ return "xga"; // XGA (tablets)
4404
5628
  if (width >= 768)
4405
- return "tablet"; // Tablet
4406
- return "mobile"; // Mobile
5629
+ return "svga"; // SVGA (small tablets)
5630
+ if (width >= 640)
5631
+ return "vga"; // VGA (phones)
5632
+ if (width >= 480)
5633
+ return "hvga"; // HVGA (older phones)
5634
+ if (width >= 320)
5635
+ return "qvga"; // QVGA (legacy phones)
5636
+ return "minimal"; // Sub-QVGA
5637
+ };
5638
+ // Enhanced aspect ratio classification for device type hints
5639
+ const getAspectRatioClass = (width, height) => {
5640
+ if (height === 0)
5641
+ return "unknown";
5642
+ const ratio = width / height;
5643
+ // Ultra-wide displays (gaming, productivity)
5644
+ if (ratio >= 3.0)
5645
+ return "super_ultrawide"; // 32:9+
5646
+ if (ratio >= 2.3)
5647
+ return "ultrawide"; // 21:9
5648
+ if (ratio >= 2.0)
5649
+ return "wide"; // 18:9
5650
+ // Standard desktop ratios
5651
+ if (ratio >= 1.7 && ratio <= 1.8)
5652
+ return "standard"; // 16:9
5653
+ if (ratio >= 1.6 && ratio <= 1.67)
5654
+ return "classic"; // 16:10, 5:3
5655
+ if (ratio >= 1.3 && ratio <= 1.35)
5656
+ return "traditional"; // 4:3
5657
+ // Mobile/portrait ratios
5658
+ if (ratio >= 0.5 && ratio <= 0.8)
5659
+ return "mobile_portrait"; // Phones in portrait
5660
+ if (ratio >= 0.4 && ratio <= 0.5)
5661
+ return "tall_mobile"; // Modern tall phones
5662
+ return "custom"; // Non-standard ratio
4407
5663
  };
4408
5664
  // Ensure w and h are valid numbers
4409
5665
  const width = w || 0;
4410
5666
  const height = h || 0;
5667
+ // Screen density class based on common DPR patterns
5668
+ const getDensityClass = (pixelRatio) => {
5669
+ if (!pixelRatio)
5670
+ return "standard";
5671
+ if (pixelRatio >= 3.0)
5672
+ return "ultra_high"; // iPhone Plus, high-end Android
5673
+ if (pixelRatio >= 2.0)
5674
+ return "high"; // Retina, standard high-DPI
5675
+ if (pixelRatio >= 1.5)
5676
+ return "medium"; // Mid-range displays
5677
+ if (pixelRatio >= 1.25)
5678
+ return "enhanced"; // Slightly enhanced DPI
5679
+ return "standard"; // Standard 1x displays
5680
+ };
4411
5681
  return {
4412
5682
  bucket: getScreenBucket(width),
4413
- ratio: height > 0 ? Math.round((width / height) * 10) / 10 : 0, // Aspect ratio rounded to 1 decimal
4414
- // Remove exact dimensions and pixel ratio - they can vary slightly
5683
+ aspectClass: getAspectRatioClass(width, height),
5684
+ densityClass: getDensityClass(screen.pixelRatio || window.devicePixelRatio),
5685
+ // Simplified ratio for matching (rounded to prevent micro-variations)
5686
+ ratio: height > 0 ? Math.round((width / height) * 100) / 100 : 0,
5687
+ // Size category for general device classification
5688
+ sizeCategory: width >= 2560 ? "large" : width >= 1920 ? "desktop" :
5689
+ width >= 1024 ? "laptop" : width >= 768 ? "tablet" : "mobile"
4415
5690
  };
4416
5691
  })()
4417
- : { bucket: "unknown", ratio: 0 },
5692
+ : { bucket: "unknown", aspectClass: "unknown", densityClass: "unknown", ratio: 0, sizeCategory: "unknown" },
4418
5693
  // CRITICAL: Do NOT include availability flags (canvas/webgl/audio) in stableCoreVector
4419
5694
  // These flags can change between requests due to timeouts, permissions, or hardware issues
4420
5695
  // and would cause the stableCoreHash to change, breaking visitor identification
@@ -4426,50 +5701,163 @@ async function collectFingerprint(options = {}) {
4426
5701
  // - Order may vary despite sorting
4427
5702
  // Keep plugins only in full fingerprint for analysis, not in stableCoreHash
4428
5703
  // plugins: removed for stability
4429
- // PRIORITY 7: Core hardware characteristics (buckets for stability)
4430
- // Use hardware buckets instead of exact values to prevent micro-variations
5704
+ // PRIORITY 7: Enhanced hardware characteristics with precise 2024 buckets
5705
+ // Use advanced hardware buckets based on current hardware landscape for maximum stability
4431
5706
  hardware: (() => {
4432
5707
  const cores = deviceSignals$1.hardwareConcurrency;
4433
5708
  const memory = deviceSignals$1.deviceMemory;
4434
5709
  if (!cores && !memory)
4435
5710
  return undefined;
4436
- // Create stable hardware buckets
5711
+ // Enhanced CPU core buckets based on 2024 hardware trends
4437
5712
  const getCoreBucket = (cores) => {
5713
+ // High-end workstations and servers (2024: up to 128 cores available)
5714
+ if (cores >= 32)
5715
+ return "workstation"; // 32+ cores: Threadripper, Xeon
5716
+ if (cores >= 20)
5717
+ return "ultra"; // 20-31 cores: High-end desktop
4438
5718
  if (cores >= 16)
4439
- return "high"; // 16+ cores
5719
+ return "high"; // 16-19 cores: Gaming/content creation
5720
+ if (cores >= 12)
5721
+ return "premium"; // 12-15 cores: Modern mid-high desktop
4440
5722
  if (cores >= 8)
4441
- return "mid"; // 8-15 cores
5723
+ return "mid"; // 8-11 cores: Standard desktop/laptop
5724
+ if (cores >= 6)
5725
+ return "normal"; // 6-7 cores: Entry-level modern
4442
5726
  if (cores >= 4)
4443
- return "normal"; // 4-7 cores
4444
- return "low"; // 1-3 cores
5727
+ return "basic"; // 4-5 cores: Older desktop/budget laptop
5728
+ if (cores >= 2)
5729
+ return "low"; // 2-3 cores: Older mobile/budget
5730
+ return "minimal"; // 1 core: Legacy/embedded
4445
5731
  };
5732
+ // Enhanced memory buckets based on 2024 standards
4446
5733
  const getMemoryBucket = (memory) => {
5734
+ // Professional/Gaming systems (2024: up to 256GB+ available)
5735
+ if (memory >= 64)
5736
+ return "workstation"; // 64GB+: Professional/content creation
5737
+ if (memory >= 32)
5738
+ return "ultra"; // 32-63GB: High-end gaming/work
5739
+ if (memory >= 24)
5740
+ return "high"; // 24-31GB: Premium gaming
4447
5741
  if (memory >= 16)
4448
- return "high"; // 16GB+
5742
+ return "premium"; // 16-23GB: Standard gaming/work
5743
+ if (memory >= 12)
5744
+ return "mid"; // 12-15GB: Good performance
4449
5745
  if (memory >= 8)
4450
- return "mid"; // 8-15GB
5746
+ return "normal"; // 8-11GB: Acceptable performance
4451
5747
  if (memory >= 4)
4452
- return "normal"; // 4-7GB
4453
- return "low"; // <4GB
5748
+ return "basic"; // 4-7GB: Budget desktop/laptop
5749
+ if (memory >= 2)
5750
+ return "low"; // 2-3GB: Older mobile devices
5751
+ return "minimal"; // <2GB: Legacy/embedded devices
5752
+ };
5753
+ // CPU architecture detection for additional differentiation
5754
+ const getArchitectureBucket = (arch) => {
5755
+ if (arch === undefined)
5756
+ return "unknown";
5757
+ // Based on floating point NaN behavior patterns
5758
+ if (arch === 255)
5759
+ return "arm64"; // ARM processors
5760
+ if (arch === 0)
5761
+ return "x86_64"; // Intel/AMD x86-64
5762
+ return "other"; // Other architectures
5763
+ };
5764
+ // Combined hardware profile for enhanced bucketing
5765
+ const coresBucket = cores ? getCoreBucket(cores) : "unknown";
5766
+ const memoryBucket = memory ? getMemoryBucket(memory) : "unknown";
5767
+ const archBucket = getArchitectureBucket(deviceSignals$1.architecture);
5768
+ // Create hardware class based on combined characteristics
5769
+ const getHardwareClass = (cores, memory, arch) => {
5770
+ // Professional workstation patterns
5771
+ if ((cores === "workstation" || cores === "ultra") &&
5772
+ (memory === "workstation" || memory === "ultra")) {
5773
+ return "professional";
5774
+ }
5775
+ // High-end gaming/content creation
5776
+ if ((cores === "high" || cores === "premium") &&
5777
+ (memory === "high" || memory === "premium" || memory === "ultra")) {
5778
+ return "enthusiast";
5779
+ }
5780
+ // Standard desktop/laptop
5781
+ if ((cores === "mid" || cores === "normal") &&
5782
+ (memory === "mid" || memory === "normal" || memory === "premium")) {
5783
+ return "mainstream";
5784
+ }
5785
+ // Budget/older systems
5786
+ if ((cores === "basic" || cores === "low") &&
5787
+ (memory === "basic" || memory === "low" || memory === "normal")) {
5788
+ return "budget";
5789
+ }
5790
+ // Mobile/embedded devices
5791
+ if (cores === "minimal" || memory === "minimal") {
5792
+ return "mobile";
5793
+ }
5794
+ // Mixed/uncertain configuration
5795
+ return "mixed";
4454
5796
  };
4455
5797
  return {
4456
- cores: cores ? getCoreBucket(cores) : undefined,
4457
- memory: memory ? getMemoryBucket(memory) : undefined,
5798
+ cores: coresBucket,
5799
+ memory: memoryBucket,
5800
+ arch: archBucket,
5801
+ class: getHardwareClass(coresBucket, memoryBucket),
5802
+ // Touch capability for mobile/desktop differentiation
5803
+ touch: deviceSignals$1.touchSupport?.maxTouchPoints ?
5804
+ (deviceSignals$1.touchSupport.maxTouchPoints >= 10 ? "multi" :
5805
+ deviceSignals$1.touchSupport.maxTouchPoints >= 5 ? "standard" : "basic") :
5806
+ "none"
4458
5807
  };
4459
5808
  })(),
4460
- // PRIORITY 8: Architecture (ultra-stable)
4461
- arch: deviceSignals$1.architecture || undefined,
4462
- // PRIORITY 9: Color depth bucket (more stable than exact values)
4463
- colorBucket: (() => {
5809
+ // PRIORITY 8: Enhanced color and display capabilities bucketing
5810
+ display: (() => {
4464
5811
  const depth = deviceSignals$1.colorDepth;
4465
- if (!depth)
4466
- return undefined;
4467
- // Bucket color depths for stability
4468
- if (depth >= 32)
4469
- return "high"; // 32-bit+
4470
- if (depth >= 24)
4471
- return "normal"; // 24-bit
4472
- return "low"; // <24-bit
5812
+ const gamut = deviceSignals$1.colorGamut;
5813
+ // Enhanced color depth classification
5814
+ const getDepthClass = (depth) => {
5815
+ if (!depth)
5816
+ return "unknown";
5817
+ if (depth >= 48)
5818
+ return "ultra_high"; // 48-bit+ (professional displays)
5819
+ if (depth >= 32)
5820
+ return "high"; // 32-bit (high-end displays)
5821
+ if (depth >= 24)
5822
+ return "normal"; // 24-bit (standard displays)
5823
+ if (depth >= 16)
5824
+ return "basic"; // 16-bit (older displays)
5825
+ return "low"; // <16-bit (legacy displays)
5826
+ };
5827
+ // Color gamut classification for display capabilities
5828
+ const getGamutClass = (gamut) => {
5829
+ if (!gamut)
5830
+ return "unknown";
5831
+ switch (gamut) {
5832
+ case "rec2020": return "professional"; // Professional/HDR displays
5833
+ case "p3": return "enhanced"; // Modern displays (Apple, etc.)
5834
+ case "srgb": return "standard"; // Standard displays
5835
+ default: return "unknown";
5836
+ }
5837
+ };
5838
+ // Combined display quality classification
5839
+ const getDisplayClass = (depthClass, gamutClass) => {
5840
+ if (depthClass === "ultra_high" || gamutClass === "professional") {
5841
+ return "professional"; // Pro displays
5842
+ }
5843
+ if (depthClass === "high" || gamutClass === "enhanced") {
5844
+ return "premium"; // High-end displays
5845
+ }
5846
+ if (depthClass === "normal" && gamutClass === "standard") {
5847
+ return "standard"; // Standard displays
5848
+ }
5849
+ if (depthClass === "basic") {
5850
+ return "basic"; // Older displays
5851
+ }
5852
+ return "legacy"; // Legacy displays
5853
+ };
5854
+ const depthClass = getDepthClass(depth);
5855
+ const gamutClass = getGamutClass(gamut);
5856
+ return {
5857
+ depth: depthClass,
5858
+ gamut: gamutClass,
5859
+ class: getDisplayClass(depthClass, gamutClass)
5860
+ };
4473
5861
  })(),
4474
5862
  };
4475
5863
  // ADDITIONAL ENTROPY: Add a deterministic but unique component based on ultra-stable signals
@@ -9871,7 +11259,7 @@ async function getDomBlockers(options = {}) {
9871
11259
  * Based on FingerprintJS best practices
9872
11260
  */
9873
11261
  function isDomBlockersDetectionAvailable() {
9874
- const capabilities = getBrowserCapabilities();
11262
+ const capabilities = getBrowserCapabilities$1();
9875
11263
  return capabilities.canRunDOMBlockers;
9876
11264
  }
9877
11265
  async function getDomBlockersFingerprint(options = {}) {
@@ -10523,7 +11911,7 @@ function generatePluginSignature(plugins, mimeTypes) {
10523
11911
  * Enhanced with browser capability checks
10524
11912
  */
10525
11913
  function isEnhancedPluginDetectionAvailable() {
10526
- const capabilities = getBrowserCapabilities();
11914
+ const capabilities = getBrowserCapabilities$1();
10527
11915
  return capabilities.canRunBrowserAPIs && typeof navigator !== 'undefined';
10528
11916
  }
10529
11917
  /**