@zaplier/sdk 1.0.2 → 1.0.4

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