/** * Anonymous listener id interop (Phase 16 §3, wave 16.3, D5 Option A). Mints a random first-party GUID * on first visit, stores it in localStorage, and reads it back thereafter — one opaque token per * browser-install-until-cleared. No PII, no fingerprinting, no cross-site use: it is a "this browser, * until you clear it" token the server counts distinctly to estimate unique listeners. * * Degrades safely: if localStorage is unavailable (private mode, blocked, partitioned third-party * iframe) it returns null rather than throwing, and the caller simply sends no anonId. Over-counting is * the known, accepted direction of error (§3). * * Exposed on window.DeepDrftAnonId; imported once in App.razor alongside the audio engine and beacon. */ const STORAGE_KEY = 'deepdrft.anonId'; // crypto.randomUUID is the standard, secure source. A guarded fallback covers older/insecure-context // browsers where it is absent — still a random opaque token, not a fingerprint. function mint(): string { if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { return crypto.randomUUID(); } // RFC4122-ish fallback from getRandomValues; only reached on browsers lacking randomUUID. const bytes = new Uint8Array(16); crypto.getRandomValues(bytes); bytes[6] = (bytes[6] & 0x0f) | 0x40; bytes[8] = (bytes[8] & 0x3f) | 0x80; const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join(''); return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; } const DeepDrftAnonId = { /** * Read the stored anon id, minting and persisting one on first call. Returns null if localStorage * cannot be read or written (private mode / blocked / partitioned) — telemetry then omits the id. * Both the read and the write are guarded independently: a readable-but-unwritable store still mints * a fresh id each call (acceptable over-count) rather than throwing. */ get: (): string | null => { try { const existing = localStorage.getItem(STORAGE_KEY); if (existing) return existing; const minted = mint(); try { localStorage.setItem(STORAGE_KEY, minted); } catch { // Read worked, write did not — return the minted value anyway; it just won't persist. } return minted; } catch { // localStorage is entirely unavailable — send no anonId. return null; } }, }; declare global { interface Window { DeepDrftAnonId: typeof DeepDrftAnonId; } } window.DeepDrftAnonId = DeepDrftAnonId; export { DeepDrftAnonId };