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