UNPKG

@trillet-ai/web-sdk

Version:

Trillet Web SDK for real-time audio communication with AI agents

132 lines (131 loc) 5.15 kB
/** * Browser Fingerprinting Utility * Generates a deterministic fingerprint based on stable browser/hardware characteristics. * Used for demo link usage limit tracking. * * Strategy: * 1. localStorage persistence — generate once, reuse across tabs/sessions forever * 2. Stable hardware-only seed — if storage is cleared, the same hardware signals * produce the same hash, so the fingerprint stays consistent * 3. Server-side IP fallback — handled by the backend when no fingerprint is sent */ const STORAGE_KEY = 'trillet_dfp'; export class BrowserFingerprint { /** * Generate (or retrieve) a deterministic browser fingerprint. * Cached in memory → localStorage → derived from stable hardware signals. */ static async generate() { // 1. In-memory cache (fastest) if (this.cachedFingerprint) { return this.cachedFingerprint; } // 2. Persisted in localStorage (survives tab close / browser restart) const stored = this.getStored(); if (stored) { this.cachedFingerprint = stored; return stored; } // 3. Derive from stable hardware signals and persist const signals = this.getStableSignals(); const fingerprint = await this.hashComponents(signals); this.persist(fingerprint); this.cachedFingerprint = fingerprint; return fingerprint; } /** * Collect only hardware / environment signals that do NOT change between * tabs, sessions, or browser restarts on the same device. * * Deliberately excluded (volatile / anti-fingerprint randomised): * - canvas.toDataURL() — randomised per origin in many browsers * - font detection — noisy across rendering states * - screen.availWidth/Height — changes with taskbar / dock resize * - navigator.doNotTrack — user can toggle * - timezoneOffset — changes with DST * - navigator.userAgent — changes on browser update */ static getStableSignals() { return { // Hardware — fixed for a given device screen: { width: window.screen.width, height: window.screen.height, colorDepth: window.screen.colorDepth, pixelRatio: window.devicePixelRatio, }, hardwareConcurrency: navigator.hardwareConcurrency, deviceMemory: navigator.deviceMemory, platform: navigator.platform, maxTouchPoints: navigator.maxTouchPoints, // WebGL vendor/renderer — GPU-based, stable webgl: this.getWebGLFingerprint(), // Locale — stable unless user changes OS language or moves timezone timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, language: navigator.language, }; } /** * WebGL vendor + renderer string (GPU identifier, very stable) */ static getWebGLFingerprint() { try { const canvas = document.createElement('canvas'); const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl'); if (!gl) return 'no-webgl'; const glContext = gl; const debugInfo = glContext.getExtension('WEBGL_debug_renderer_info'); const vendor = debugInfo ? glContext.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL) : glContext.getParameter(glContext.VENDOR); const renderer = debugInfo ? glContext.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL) : glContext.getParameter(glContext.RENDERER); return `${vendor}~${renderer}`; } catch (_a) { return 'webgl-error'; } } /** * SHA-256 hash of the signals object → hex string */ static async hashComponents(components) { const data = new TextEncoder().encode(JSON.stringify(components)); const hashBuffer = await crypto.subtle.digest('SHA-256', data); return Array.from(new Uint8Array(hashBuffer)) .map((b) => b.toString(16).padStart(2, '0')) .join(''); } // ── localStorage helpers ─────────────────────────────────────────── static getStored() { try { return localStorage.getItem(STORAGE_KEY); } catch (_a) { return null; } } static persist(fingerprint) { try { localStorage.setItem(STORAGE_KEY, fingerprint); } catch (_a) { // Storage full or disabled — fingerprint still works in-memory } } /** * Clear both in-memory and persisted fingerprint (useful for testing) */ static clearCache() { this.cachedFingerprint = null; try { localStorage.removeItem(STORAGE_KEY); } catch (_a) { // Ignore } } } BrowserFingerprint.cachedFingerprint = null;