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