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