@zaplier/sdk 1.0.2 → 1.0.3

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