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