UNPKG

@scrubbe-auth/device-fingerprint

Version:
1,063 lines (1,049 loc) 39.2 kB
class CryptoUtils { static async sha256(text) { if (typeof window !== 'undefined' && window.crypto && window.crypto.subtle) { const encoder = new TextEncoder(); const data = encoder.encode(text); const hashBuffer = await window.crypto.subtle.digest('SHA-256', data); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); } // Fallback for Node.js or older browsers return this.simpleHash(text); } static sha1(text) { // Simple SHA-1 implementation for fallback return this.simpleHash(text, 'sha1'); } static md5(text) { // Simple MD5 implementation for fallback return this.simpleHash(text, 'md5'); } static murmur3(text, seed = 0) { let h1 = seed; for (let i = 0; i < text.length; i++) { let k1 = text.charCodeAt(i); k1 = Math.imul(k1, 0xcc9e2d51); k1 = (k1 << 15) | (k1 >>> 17); k1 = Math.imul(k1, 0x1b873593); h1 ^= k1; h1 = (h1 << 13) | (h1 >>> 19); h1 = Math.imul(h1, 5) + 0xe6546b64; } h1 ^= text.length; h1 ^= h1 >>> 16; h1 = Math.imul(h1, 0x85ebca6b); h1 ^= h1 >>> 13; h1 = Math.imul(h1, 0xc2b2ae35); h1 ^= h1 >>> 16; return (h1 >>> 0).toString(16); } static simpleHash(text, algorithm = 'sha256') { let hash = 0; if (text.length === 0) return hash.toString(); for (let i = 0; i < text.length; i++) { const char = text.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32bit integer } const abs = Math.abs(hash); return abs.toString(16).padStart(8, '0'); } } class CanvasCollector { async collect(config = {}) { try { if (typeof window === 'undefined' || typeof document === 'undefined') { return { hash: 'unavailable', supported: false, error: 'Canvas API not available' }; } this.canvas = document.createElement('canvas'); this.canvas.width = 200; this.canvas.height = 50; this.ctx = this.canvas.getContext('2d'); if (!this.ctx) { return { hash: 'unsupported', supported: false, error: 'Canvas 2D context not supported' }; } // Draw complex pattern for more unique fingerprint this.drawFingerprint(config.text || 'Scrubbe Analytics 2025! 🚀'); const dataURL = this.canvas.toDataURL(); const hash = await CryptoUtils.sha256(dataURL); return { hash, dataURL: dataURL.substring(0, 100) + '...', // Truncate for privacy supported: true }; } catch (error) { return { hash: 'error', supported: false, error: error instanceof Error ? error.message : 'Unknown canvas error' }; } finally { this.cleanup(); } } drawFingerprint(text) { if (!this.ctx || !this.canvas) return; // Set complex styling this.ctx.textBaseline = 'top'; this.ctx.font = '14px "Arial, sans-serif"'; this.ctx.textBaseline = 'alphabetic'; this.ctx.fillStyle = '#f60'; this.ctx.fillRect(125, 1, 62, 20); // Add gradient const gradient = this.ctx.createLinearGradient(0, 0, this.canvas.width, 0); gradient.addColorStop(0, '#00ff00'); gradient.addColorStop(0.5, '#ff0000'); gradient.addColorStop(1, '#0000ff'); this.ctx.fillStyle = gradient; // Draw text with shadow this.ctx.shadowColor = '#333'; this.ctx.shadowOffsetX = 2; this.ctx.shadowOffsetY = 2; this.ctx.shadowBlur = 2; this.ctx.fillText(text, 2, 15); // Add geometric shapes this.ctx.fillStyle = 'rgba(102, 204, 0, 0.7)'; this.ctx.fillText(text, 4, 17); // Draw circle this.ctx.beginPath(); this.ctx.arc(50, 25, 20, 0, Math.PI * 2); this.ctx.closePath(); this.ctx.fill(); // Add more complexity this.ctx.globalCompositeOperation = 'multiply'; this.ctx.fillStyle = 'rgb(255,0,255)'; this.ctx.beginPath(); this.ctx.arc(75, 25, 20, 0, Math.PI * 2); this.ctx.closePath(); this.ctx.fill(); } cleanup() { if (this.canvas) { this.canvas.remove(); this.canvas = undefined; this.ctx = undefined; } } } class WebGLCollector { async collect() { try { if (typeof window === 'undefined' || typeof document === 'undefined') { return { renderer: 'unavailable', vendor: 'unavailable', version: 'unavailable', shadingLanguageVersion: 'unavailable', extensions: [], parameters: {}, hash: 'unavailable', supported: false, error: 'WebGL not available' }; } const canvas = document.createElement('canvas'); // Try to get WebGL context with proper type casting const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); if (!gl) { return { renderer: 'unsupported', vendor: 'unsupported', version: 'unsupported', shadingLanguageVersion: 'unsupported', extensions: [], parameters: {}, hash: 'unsupported', supported: false, error: 'WebGL context creation failed' }; } // Get debug info extension with proper type checking const debugInfo = gl.getExtension('WEBGL_debug_renderer_info'); // Get renderer and vendor info const renderer = debugInfo ? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER); const vendor = debugInfo ? gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) : gl.getParameter(gl.VENDOR); const fingerprint = { renderer: this.safeStringify(renderer) || 'unknown', vendor: this.safeStringify(vendor) || 'unknown', version: this.safeStringify(gl.getParameter(gl.VERSION)) || 'unknown', shadingLanguageVersion: this.safeStringify(gl.getParameter(gl.SHADING_LANGUAGE_VERSION)) || 'unknown', extensions: this.getExtensions(gl), parameters: this.getParameters(gl), hash: '', supported: true }; // Create hash from all collected data const hashInput = JSON.stringify({ renderer: fingerprint.renderer, vendor: fingerprint.vendor, version: fingerprint.version, shadingLanguageVersion: fingerprint.shadingLanguageVersion, extensions: fingerprint.extensions, parameters: fingerprint.parameters }); fingerprint.hash = await CryptoUtils.sha256(hashInput); // Clean up canvas.remove(); return fingerprint; } catch (error) { return { renderer: 'error', vendor: 'error', version: 'error', shadingLanguageVersion: 'error', extensions: [], parameters: {}, hash: 'error', supported: false, error: error instanceof Error ? error.message : 'Unknown WebGL error' }; } } getExtensions(gl) { const extensions = []; try { const supportedExtensions = gl.getSupportedExtensions(); if (supportedExtensions) { extensions.push(...supportedExtensions); } } catch (error) { // Ignore extension enumeration errors } return extensions.sort(); } getParameters(gl) { const parameters = {}; // Define parameter constants with their values const parameterMap = { 'MAX_VERTEX_ATTRIBS': gl.MAX_VERTEX_ATTRIBS, 'MAX_VERTEX_UNIFORM_VECTORS': gl.MAX_VERTEX_UNIFORM_VECTORS, 'MAX_VERTEX_TEXTURE_IMAGE_UNITS': gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS, 'MAX_VARYING_VECTORS': gl.MAX_VARYING_VECTORS, 'ALIASED_LINE_WIDTH_RANGE': gl.ALIASED_LINE_WIDTH_RANGE, 'ALIASED_POINT_SIZE_RANGE': gl.ALIASED_POINT_SIZE_RANGE, 'MAX_FRAGMENT_UNIFORM_VECTORS': gl.MAX_FRAGMENT_UNIFORM_VECTORS, 'MAX_TEXTURE_IMAGE_UNITS': gl.MAX_TEXTURE_IMAGE_UNITS, 'MAX_TEXTURE_SIZE': gl.MAX_TEXTURE_SIZE, 'MAX_CUBE_MAP_TEXTURE_SIZE': gl.MAX_CUBE_MAP_TEXTURE_SIZE, 'MAX_VIEWPORT_DIMS': gl.MAX_VIEWPORT_DIMS, 'RED_BITS': gl.RED_BITS, 'GREEN_BITS': gl.GREEN_BITS, 'BLUE_BITS': gl.BLUE_BITS, 'ALPHA_BITS': gl.ALPHA_BITS, 'DEPTH_BITS': gl.DEPTH_BITS, 'STENCIL_BITS': gl.STENCIL_BITS }; Object.entries(parameterMap).forEach(([name, constant]) => { try { const param = gl.getParameter(constant); if (param !== null) { parameters[name] = Array.isArray(param) ? Array.from(param) : param; } } catch (error) { // Ignore individual parameter errors } }); return parameters; } safeStringify(value) { if (value === null || value === undefined) { return 'null'; } if (typeof value === 'string') { return value; } try { return String(value); } catch { return 'unknown'; } } } class AudioCollector { async collect(config = {}) { const timeout = config.timeout || 1000; return new Promise((resolve) => { const timer = setTimeout(() => { resolve({ hash: 'timeout', oscillatorHash: 'timeout', dynamicsHash: 'timeout', supported: false, error: 'Audio fingerprinting timeout' }); }, timeout); try { if (typeof window === 'undefined' || !window.AudioContext && !window.webkitAudioContext) { clearTimeout(timer); resolve({ hash: 'unavailable', oscillatorHash: 'unavailable', dynamicsHash: 'unavailable', supported: false, error: 'AudioContext not available' }); return; } const AudioContext = window.AudioContext || window.webkitAudioContext; const context = new AudioContext(); // Create oscillator fingerprint const oscillator = context.createOscillator(); const analyser = context.createAnalyser(); const gain = context.createGain(); const scriptProcessor = context.createScriptProcessor(4096, 1, 1); gain.gain.value = 0; // Mute output oscillator.type = 'triangle'; oscillator.frequency.value = 10000; oscillator.connect(analyser); analyser.connect(scriptProcessor); scriptProcessor.connect(gain); gain.connect(context.destination); oscillator.start(0); const oscillatorData = []; scriptProcessor.onaudioprocess = (event) => { const buffer = event.inputBuffer.getChannelData(0); for (let i = 0; i < buffer.length; i++) { oscillatorData.push(buffer[i]); } if (oscillatorData.length >= 4096) { oscillator.stop(); oscillator.disconnect(); scriptProcessor.disconnect(); context.close(); clearTimeout(timer); this.processAudioData(oscillatorData).then(result => { resolve({ hash: result.combined, oscillatorHash: result.oscillator, dynamicsHash: result.dynamics, supported: true }); }); } }; } catch (error) { clearTimeout(timer); resolve({ hash: 'error', oscillatorHash: 'error', dynamicsHash: 'error', supported: false, error: error instanceof Error ? error.message : 'Unknown audio error' }); } }); } async processAudioData(data) { // Calculate various audio characteristics const sum = data.reduce((a, b) => a + b, 0); const avg = sum / data.length; const variance = data.reduce((acc, val) => acc + Math.pow(val - avg, 2), 0) / data.length; const stdDev = Math.sqrt(variance); // Create frequency analysis const frequencies = this.getFrequencySignature(data); const oscillatorHash = await CryptoUtils.sha256(JSON.stringify({ sum: sum.toFixed(10), avg: avg.toFixed(10), length: data.length })); const dynamicsHash = await CryptoUtils.sha256(JSON.stringify({ variance: variance.toFixed(10), stdDev: stdDev.toFixed(10), frequencies })); const combined = await CryptoUtils.sha256(oscillatorHash + dynamicsHash); return { combined, oscillator: oscillatorHash, dynamics: dynamicsHash }; } getFrequencySignature(data) { // Simple frequency analysis - get peaks at different intervals const signature = []; const bucketSize = Math.floor(data.length / 32); for (let i = 0; i < 32; i++) { const start = i * bucketSize; const end = Math.min(start + bucketSize, data.length); const bucket = data.slice(start, end); const max = Math.max(...bucket); signature.push(parseFloat(max.toFixed(6))); } return signature; } } class ScreenCollector { collect() { if (typeof window === 'undefined' || typeof screen === 'undefined') { return { width: 0, height: 0, availWidth: 0, availHeight: 0, colorDepth: 0, pixelDepth: 0, pixelRatio: 1, touch: false }; } return { width: screen.width, height: screen.height, availWidth: screen.availWidth, availHeight: screen.availHeight, colorDepth: screen.colorDepth, pixelDepth: screen.pixelDepth || screen.colorDepth, pixelRatio: window.devicePixelRatio || 1, orientation: screen.orientation?.type, touch: 'ontouchstart' in window || navigator.maxTouchPoints > 0 }; } } class BrowserCollector { collect() { if (typeof navigator === 'undefined') { return { userAgent: 'unavailable', name: 'unknown', version: 'unknown', engine: 'unknown', engineVersion: 'unknown', os: 'unknown', osVersion: 'unknown', mobile: false, cookieEnabled: false, doNotTrack: null, onLine: false }; } const userAgent = navigator.userAgent; const parsed = this.parseUserAgent(userAgent); return { userAgent, name: parsed.browser, version: parsed.browserVersion, engine: parsed.engine, engineVersion: parsed.engineVersion, os: parsed.os, osVersion: parsed.osVersion, mobile: parsed.mobile, cookieEnabled: navigator.cookieEnabled, doNotTrack: navigator.doNotTrack || null, buildID: navigator.buildID, productSub: navigator.productSub, onLine: navigator.onLine }; } parseUserAgent(ua) { // Simplified user agent parsing const result = { browser: 'Unknown', browserVersion: '0', engine: 'Unknown', engineVersion: '0', os: 'Unknown', osVersion: '0', mobile: false }; // Detect browser if (ua.includes('Chrome/')) { result.browser = 'Chrome'; result.browserVersion = this.extractVersion(ua, 'Chrome/'); result.engine = 'Blink'; } else if (ua.includes('Firefox/')) { result.browser = 'Firefox'; result.browserVersion = this.extractVersion(ua, 'Firefox/'); result.engine = 'Gecko'; } else if (ua.includes('Safari/') && !ua.includes('Chrome/')) { result.browser = 'Safari'; result.browserVersion = this.extractVersion(ua, 'Version/'); result.engine = 'WebKit'; } else if (ua.includes('Edge/')) { result.browser = 'Edge'; result.browserVersion = this.extractVersion(ua, 'Edge/'); result.engine = 'EdgeHTML'; } // Detect OS if (ua.includes('Windows NT')) { result.os = 'Windows'; result.osVersion = this.extractVersion(ua, 'Windows NT '); } else if (ua.includes('Mac OS X')) { result.os = 'macOS'; result.osVersion = this.extractVersion(ua, 'Mac OS X ').replace(/_/g, '.'); } else if (ua.includes('Linux')) { result.os = 'Linux'; } else if (ua.includes('Android')) { result.os = 'Android'; result.osVersion = this.extractVersion(ua, 'Android '); result.mobile = true; } else if (ua.includes('iPhone') || ua.includes('iPad')) { result.os = 'iOS'; result.osVersion = this.extractVersion(ua, 'OS ').replace(/_/g, '.'); result.mobile = ua.includes('iPhone'); } return result; } extractVersion(ua, marker) { const index = ua.indexOf(marker); if (index === -1) return '0'; const start = index + marker.length; const version = ua.substring(start).split(/[^\d.]/)[0]; return version || '0'; } } class HardwareCollector { collect() { if (typeof navigator === 'undefined') { return { cpuCores: 0, platform: 'unknown', maxTouchPoints: 0, hardwareConcurrency: 0 }; } return { cpuCores: navigator.hardwareConcurrency || 0, memory: navigator.deviceMemory, platform: navigator.platform, architecture: navigator.userAgentData?.platform, maxTouchPoints: navigator.maxTouchPoints || 0, hardwareConcurrency: navigator.hardwareConcurrency || 0, deviceMemory: navigator.deviceMemory }; } } class FontCollector { async collect(customFonts = []) { const fontsToTest = [...FontCollector.DEFAULT_FONTS, ...customFonts]; const availableFonts = []; if (typeof document === 'undefined') { return availableFonts; } const testContainer = this.createTestContainer(); try { for (const font of fontsToTest) { if (await this.isFontAvailable(font, testContainer)) { availableFonts.push(font); } } } finally { this.cleanupTestContainer(testContainer); } return availableFonts.sort(); } createTestContainer() { const container = document.createElement('div'); container.style.position = 'absolute'; container.style.left = '-9999px'; container.style.top = '-9999px'; container.style.visibility = 'hidden'; document.body.appendChild(container); return container; } async isFontAvailable(fontName, container) { const testText = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; const fontSize = '72px'; const baseFonts = ['monospace', 'sans-serif', 'serif']; const measurements = []; // Measure with base fonts for (const baseFont of baseFonts) { const testElement = document.createElement('span'); testElement.style.fontSize = fontSize; testElement.style.fontFamily = baseFont; testElement.textContent = testText; container.appendChild(testElement); measurements.push(testElement.offsetWidth); container.removeChild(testElement); } // Test with target font for (let i = 0; i < baseFonts.length; i++) { const testElement = document.createElement('span'); testElement.style.fontSize = fontSize; testElement.style.fontFamily = `"${fontName}", ${baseFonts[i]}`; testElement.textContent = testText; container.appendChild(testElement); const width = testElement.offsetWidth; container.removeChild(testElement); if (width !== measurements[i]) { return true; } } return false; } cleanupTestContainer(container) { if (container.parentNode) { container.parentNode.removeChild(container); } } } FontCollector.DEFAULT_FONTS = [ // Windows fonts 'Arial', 'Arial Black', 'Calibri', 'Cambria', 'Comic Sans MS', 'Consolas', 'Courier New', 'Georgia', 'Impact', 'Lucida Console', 'Lucida Sans Unicode', 'Microsoft Sans Serif', 'Palatino Linotype', 'Segoe UI', 'Tahoma', 'Times New Roman', 'Trebuchet MS', 'Verdana', // macOS fonts 'American Typewriter', 'Andale Mono', 'Apple Chancery', 'Apple Color Emoji', 'Arial Unicode MS', 'Avenir', 'Avenir Next', 'Big Caslon', 'Brush Script MT', 'Cochin', 'Copperplate', 'Didot', 'Futura', 'Geneva', 'Gill Sans', 'Helvetica', 'Helvetica Neue', 'Hoefler Text', 'Lucida Grande', 'Marker Felt', 'Menlo', 'Monaco', 'Optima', 'Palatino', 'Papyrus', 'Phosphate', 'Rockwell', 'Savoye LET', 'SignPainter', 'Skia', 'Snell Roundhand', 'System Font', 'Times', // Linux fonts 'DejaVu Sans', 'DejaVu Sans Mono', 'DejaVu Serif', 'Droid Sans', 'Droid Serif', 'FreeMono', 'FreeSans', 'FreeSerif', 'Liberation Mono', 'Liberation Sans', 'Liberation Serif', 'Linux Libertine', 'Noto Sans', 'Open Sans', 'Source Code Pro', 'Ubuntu', 'Ubuntu Mono' ]; class PluginCollector { collect() { const plugins = []; if (typeof navigator === 'undefined' || !navigator.plugins) { return plugins; } try { for (let i = 0; i < navigator.plugins.length; i++) { const plugin = navigator.plugins[i]; plugins.push({ name: plugin.name, description: plugin.description, filename: plugin.filename, version: plugin.version || 'unknown' }); } } catch (error) { // Some browsers might restrict plugin enumeration } return plugins.sort((a, b) => a.name.localeCompare(b.name)); } } class TimezoneCollector { collect() { try { return { timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, offset: new Date().getTimezoneOffset(), dst: this.isDST(), locale: Intl.DateTimeFormat().resolvedOptions().locale }; } catch (error) { return { timezone: 'UTC', offset: 0, dst: false, locale: 'en-US' }; } } isDST() { const now = new Date(); const january = new Date(now.getFullYear(), 0, 1); const july = new Date(now.getFullYear(), 6, 1); return Math.max(january.getTimezoneOffset(), july.getTimezoneOffset()) !== now.getTimezoneOffset(); } } class LanguageCollector { collect() { try { if (typeof navigator === 'undefined') { return { language: 'en-US', languages: ['en-US'], acceptLanguage: undefined }; } return { language: navigator.language, languages: Array.from(navigator.languages || []), acceptLanguage: navigator.userLanguage }; } catch (error) { return { language: 'en-US', languages: ['en-US'], acceptLanguage: undefined }; } } } class StorageCollector { collect() { try { return { localStorage: this.testStorage('localStorage'), sessionStorage: this.testStorage('sessionStorage'), indexedDB: 'indexedDB' in window, webSQL: 'openDatabase' in window, quota: this.getQuotaInfo() }; } catch (error) { return { localStorage: false, sessionStorage: false, indexedDB: false, webSQL: false, quota: undefined }; } } testStorage(type) { try { if (typeof window === 'undefined') return false; const storage = window[type]; const test = '__storage_test__'; storage.setItem(test, test); storage.removeItem(test); return true; } catch { return false; } } getQuotaInfo() { try { if (typeof navigator !== 'undefined' && navigator.webkitTemporaryStorage) { return navigator.webkitTemporaryStorage.quotaUsage; } return undefined; } catch { return undefined; } } } const VERSION = '1.0.0'; class DeviceFingerprints { constructor(config = {}) { this.errors = []; this.config = { // Core methods enableCanvas: config.enableCanvas ?? true, enableWebGL: config.enableWebGL ?? true, enableAudio: config.enableAudio ?? false, enableScreen: config.enableScreen ?? true, enableBrowser: config.enableBrowser ?? true, enableFonts: config.enableFonts ?? false, enablePlugins: config.enablePlugins ?? false, enableTimezone: config.enableTimezone ?? true, enableLanguage: config.enableLanguage ?? true, enableHardware: config.enableHardware ?? true, enableStorage: config.enableStorage ?? true, // Advanced options canvasText: config.canvasText || 'Scrubbe Analytics 2025! 🚀', audioTimeout: config.audioTimeout || 1000, fontList: config.fontList || [], excludeUserAgent: config.excludeUserAgent ?? false, excludeLanguage: config.excludeLanguage ?? false, excludeColorDepth: config.excludeColorDepth ?? false, excludePixelRatio: config.excludePixelRatio ?? false, excludeTimezone: config.excludeTimezone ?? false, // Privacy & Security respectDNT: config.respectDNT ?? true, anonymize: config.anonymize ?? false, hashAlgorithm: config.hashAlgorithm || 'sha256', // Performance timeout: config.timeout || 5000, debug: config.debug ?? false, cache: config.cache ?? true, cacheTimeout: config.cacheTimeout || 300000 }; if (this.config.debug) { console.log('DeviceFingerprint initialized with config:', this.config); } } async generate() { // Check DNT if (this.config.respectDNT && this.isDNTEnabled()) { return this.createMinimalFingerprint('dnt-enabled'); } // Check cache if (this.config.cache && this.isCacheValid()) { if (this.config.debug) { console.log('Returning cached fingerprint'); } return this.cache; } try { const startTime = Date.now(); const fingerprint = await this.collectFingerprint(); const endTime = Date.now(); if (this.config.debug) { console.log(`Fingerprint generated in ${endTime - startTime}ms`); console.log('Errors encountered:', this.errors); } // Cache result if (this.config.cache) { this.cache = fingerprint; this.cacheTimestamp = Date.now(); } return fingerprint; } catch (error) { this.addError('generation', error instanceof Error ? error.message : 'Unknown error'); return this.createMinimalFingerprint('generation-error'); } } async collectFingerprint() { const promises = []; // Collect asynchronous fingerprint components if (this.config.enableCanvas) { promises.push(this.collectCanvas()); } if (this.config.enableWebGL) { promises.push(this.collectWebGL()); } if (this.config.enableAudio) { promises.push(this.collectAudio()); } if (this.config.enableFonts) { promises.push(this.collectFonts()); } const [canvas, webgl, audio, fonts] = await Promise.allSettled(promises); // Collect synchronous data const screen = this.config.enableScreen ? new ScreenCollector().collect() : undefined; const browser = this.config.enableBrowser ? new BrowserCollector().collect() : undefined; const hardware = this.config.enableHardware ? new HardwareCollector().collect() : undefined; const plugins = this.config.enablePlugins ? new PluginCollector().collect() : undefined; const timezone = this.config.enableTimezone ? new TimezoneCollector().collect() : undefined; const language = this.config.enableLanguage ? new LanguageCollector().collect() : undefined; const storage = this.config.enableStorage ? new StorageCollector().collect() : undefined; // Extract settled results const canvasResult = canvas.status === 'fulfilled' ? canvas.value : undefined; const webglResult = webgl.status === 'fulfilled' ? webgl.value : undefined; const audioResult = audio.status === 'fulfilled' ? audio.value : undefined; const fontsResult = fonts.status === 'fulfilled' ? fonts.value : undefined; // Generate unique device ID const components = this.getHashComponents({ canvas: canvasResult, webgl: webglResult, audio: audioResult, screen, browser, hardware, plugins, timezone, language, storage, fonts: fontsResult }); const deviceId = await this.generateDeviceId(components); const confidence = this.calculateConfidence({ canvas: canvasResult, webgl: webglResult, audio: audioResult, screen, browser, fonts: fontsResult }); const fingerprint = { deviceId, confidence, timestamp: Date.now(), canvas: canvasResult, webgl: webglResult, audio: audioResult, screen, browser, hardware, plugins, timezone, language, storage, fonts: fontsResult, version: VERSION, generatedAt: Date.now() }; if (!this.config.anonymize && browser) { fingerprint.userAgent = browser.userAgent; } return fingerprint; } async collectCanvas() { try { return await new CanvasCollector().collect({ text: this.config.canvasText, timeout: this.config.timeout }); } catch (error) { this.addError('canvas', error instanceof Error ? error.message : 'Canvas collection failed'); return undefined; } } async collectWebGL() { try { return await new WebGLCollector().collect(); } catch (error) { this.addError('webgl', error instanceof Error ? error.message : 'WebGL collection failed'); return undefined; } } async collectAudio() { try { return await new AudioCollector().collect({ timeout: this.config.audioTimeout }); } catch (error) { this.addError('audio', error instanceof Error ? error.message : 'Audio collection failed'); return undefined; } } async collectFonts() { try { return await new FontCollector().collect(this.config.fontList); } catch (error) { this.addError('fonts', error instanceof Error ? error.message : 'Font collection failed'); return undefined; } } getHashComponents(data) { const components = []; if (data.canvas?.hash) components.push(data.canvas.hash); if (data.webgl?.hash) components.push(data.webgl.hash); if (data.audio?.hash) components.push(data.audio.hash); if (data.screen && !this.config.excludeColorDepth && !this.config.excludePixelRatio) { components.push(JSON.stringify(data.screen)); } if (data.browser && !this.config.excludeUserAgent) { components.push(data.browser.userAgent); } if (data.hardware) components.push(JSON.stringify(data.hardware)); if (data.timezone && !this.config.excludeTimezone) { components.push(JSON.stringify(data.timezone)); } if (data.language && !this.config.excludeLanguage) { components.push(JSON.stringify(data.language)); } if (data.storage) components.push(JSON.stringify(data.storage)); if (data.fonts) components.push(JSON.stringify(data.fonts)); if (data.plugins) components.push(JSON.stringify(data.plugins)); return components; } async generateDeviceId(components) { const combined = components.join('|'); switch (this.config.hashAlgorithm) { case 'sha256': return await CryptoUtils.sha256(combined); case 'sha1': return await CryptoUtils.sha1(combined); case 'md5': return CryptoUtils.md5(combined); case 'murmur3': return CryptoUtils.murmur3(combined); default: return await CryptoUtils.sha256(combined); } } calculateConfidence(data) { let score = 0; let maxScore = 0; // Canvas fingerprint (high uniqueness) maxScore += 30; if (data.canvas?.supported && data.canvas.hash !== 'error') score += 30; // WebGL fingerprint (high uniqueness) maxScore += 25; if (data.webgl?.supported && data.webgl.hash !== 'error') score += 25; // Audio fingerprint (very high uniqueness) maxScore += 20; if (data.audio?.supported && data.audio.hash !== 'error') score += 20; // Screen info (medium uniqueness) maxScore += 10; if (data.screen) score += 10; // Browser info (low uniqueness but important) maxScore += 10; if (data.browser) score += 10; // Fonts (medium uniqueness) maxScore += 5; if (data.fonts && data.fonts.length > 0) score += 5; return Math.round((score / maxScore) * 100); } isDNTEnabled() { if (typeof navigator === 'undefined') return false; return navigator.doNotTrack === '1' || navigator.doNotTrack === 'yes' || navigator.msDoNotTrack === '1'; } isCacheValid() { if (!this.cache || !this.cacheTimestamp) return false; return Date.now() - this.cacheTimestamp < this.config.cacheTimeout; } createMinimalFingerprint(reason) { return { deviceId: reason, confidence: 0, timestamp: Date.now(), version: VERSION, generatedAt: Date.now() }; } addError(component, error) { this.errors.push({ component, error, timestamp: Date.now() }); } // Public utility methods clearCache() { this.cache = undefined; this.cacheTimestamp = undefined; } getErrors() { return [...this.errors]; } getConfig() { return { ...this.config }; } } export { AudioCollector, BrowserCollector, CanvasCollector, CryptoUtils, DeviceFingerprints as DeviceFingerprint, FontCollector, HardwareCollector, LanguageCollector, PluginCollector, ScreenCollector, StorageCollector, TimezoneCollector, WebGLCollector, DeviceFingerprints as default }; //# sourceMappingURL=index.esm.js.map