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