@zaplier/sdk 1.0.2 → 1.0.4

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