/** * A random opaque identifier, in every context the app is actually served from. * * `crypto.randomUUID()` is only defined in a **secure context** — HTTPS, or a * `localhost` origin. `crypto.getRandomValues()` has no such restriction and is * available everywhere `crypto` is. That difference is easy to never notice, * because every way a developer normally opens this app is a secure context: * `vite dev` and `vite preview` both serve `localhost`, and the deployed site is * HTTPS. * * It is not a hypothetical gap. `main.ts` called `crypto.randomUUID()` at module * top level for its three wire identities, so on any other origin the call threw * before the scene was built and the app stopped at "Starting up" with one * `TypeError` in the console and no other symptom. That is: * * - the brand-capture harness, which serves `dist/` over * `http://tera.lumbridgecorp.com:5210` so the app can read its own hostname and * decide which door it is — this is how the bug was found, as a screenshot run * that timed out waiting for a boot that was never coming; * - a plain `http://` static host, which `deploy/STATIC.md` explicitly invites * ("Serve `dist/` from anything. A Content-Security-Policy of `default-src * 'self'` is sufficient") — the zero-config path this project cares about; * - anyone opening the dev server by LAN IP to try it on a phone. * * So the fallback is not defensive clutter. It is the difference between "runs * anywhere" being true and being a sentence in a document. * * The output is UUID-shaped rather than merely random because these strings go * on the realtime wire next to ids minted by other clients, and one shape is * easier to read in a log than two. Version and variant nibbles are set so it is * a well-formed v4 and not something that merely looks like one. */ /** 16 random bytes, from the best source this context actually has. */ function randomBytes(): Uint8Array { const bytes = new Uint8Array(16); const source = globalThis.crypto; if (source !== undefined && typeof source.getRandomValues === "function") { source.getRandomValues(bytes); return bytes; } // No `crypto` at all. Not reachable in a browser this app supports, and // reachable under a bare test runner, so it returns something valid rather // than throwing — these ids are opaque handles, never a security boundary. for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256); return bytes; } const HEX: string[] = Array.from({ length: 256 }, (_, i) => i.toString(16).padStart(2, "0")); /** * A v4-shaped identifier: `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`. * * Prefers the platform's own `randomUUID` when it exists, so that in the common * case this is exactly what the code it replaced produced. */ export function randomId(): string { const source = globalThis.crypto; if (source !== undefined && typeof source.randomUUID === "function") { return source.randomUUID(); } const b = randomBytes(); // Version 4 in the high nibble of byte 6, RFC 4122 variant in byte 8. b[6] = (b[6]! & 0x0f) | 0x40; b[8] = (b[8]! & 0x3f) | 0x80; const h = (i: number): string => HEX[b[i]!]!; return ( h(0) + h(1) + h(2) + h(3) + "-" + h(4) + h(5) + "-" + h(6) + h(7) + "-" + h(8) + h(9) + "-" + h(10) + h(11) + h(12) + h(13) + h(14) + h(15) ); }