/** * A UUID v4, in a browser that may not be in a secure context. * * ── Why this exists ── * * `crypto.randomUUID()` is SECURE-CONTEXT ONLY. Over plain http on anything that * is not localhost it is not defined at all, and calling it throws * `TypeError: crypto.randomUUID is not a function`. * * Officer is reached over the tailnet — `http://officer-dev:9000` — which is * neither localhost nor https, so every one of these threw. Worst inside a * `useState` initialiser, where the throw happens during render and takes the * whole tree down: chat crashed at the end of every turn, on the assistant * message that did not have an id yet. * * `crypto.getRandomValues()` carries NO such restriction — it is on `Crypto`, * not on `SubtleCrypto`, and works in an insecure context. So the randomness * below is exactly what `randomUUID` would have given; only the convenience * wrapper was missing. * * ── This is not a weakening ── * * Same CSPRNG, same 122 bits of entropy, same version and variant bits. When * `randomUUID` exists it is used unchanged; otherwise the value is assembled by * hand from the source `randomUUID` itself draws on. * * The final fallback is `Math.random`, and it is there for completeness rather * than for use: a browser with no `crypto` object at all cannot run this app. * Never reached in practice, and marked so nobody mistakes it for a supported * path or copies it somewhere it would matter. */ export function randomId(): string { const c = globalThis.crypto; if (typeof c?.randomUUID === 'function') return c.randomUUID(); if (typeof c?.getRandomValues === 'function') { const bytes = c.getRandomValues(new Uint8Array(16)); // Version 4, and the RFC 4122 variant. Exactly what randomUUID sets. 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)}`; } // Unreachable in any browser that can run this app. Not a supported path. return `${Date.now().toString(16)}-${Math.random().toString(16).slice(2, 10)}`; }