diff options
| author | Adam Malczewski <[email protected]> | 2026-06-07 00:54:08 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-07 00:54:08 +0900 |
| commit | 7889918d23ffa428cf266e52d42b9683f16160fa (patch) | |
| tree | 7f403136663562dd278b6c680449ec29af612d06 | |
| parent | 8015ed30a9904a89efe083067f505eb11fa46034 (diff) | |
| download | dispatch-web-7889918d23ffa428cf266e52d42b9683f16160fa.tar.gz dispatch-web-7889918d23ffa428cf266e52d42b9683f16160fa.zip | |
fix: blank page on non-localhost HTTP (secure-context crypto.randomUUID)
crypto.randomUUID() is secure-context-only — undefined on plain-HTTP
non-localhost origins (e.g. http://arch-razer:24204), so createAppStore threw
during mount and nothing rendered. Add src/app/uuid.ts randomId(): prefer
crypto.randomUUID when present, else build a v4 from crypto.getRandomValues
(available in insecure contexts), else Math.random fallback. Use it for the
conversation id.
Verified: svelte-check 0/0, vitest 221, build ok.
| -rw-r--r-- | src/app/store.svelte.ts | 3 | ||||
| -rw-r--r-- | src/app/uuid.test.ts | 31 | ||||
| -rw-r--r-- | src/app/uuid.ts | 65 |
3 files changed, 98 insertions, 1 deletions
diff --git a/src/app/store.svelte.ts b/src/app/store.svelte.ts index 914e682..bd3f82f 100644 --- a/src/app/store.svelte.ts +++ b/src/app/store.svelte.ts @@ -17,6 +17,7 @@ import type { ConversationCache } from "../features/conversation-cache"; import { createConversationCache } from "../features/conversation-cache"; import { resolveHttpUrl } from "./resolve-http-url"; import { resolveWsUrl } from "./resolve-ws-url"; +import { randomId } from "./uuid"; export interface AppStore { readonly catalog: ProtocolState["catalog"]; @@ -83,7 +84,7 @@ export function createAppStore(opts?: CreateAppStoreOptions): AppStore { const fetchImpl = opts?.fetchImpl ?? globalThis.fetch.bind(globalThis); const indexedDBFactory = opts?.indexedDB ?? globalThis.indexedDB; - const conversationId = opts?.conversationId ?? crypto.randomUUID(); + const conversationId = opts?.conversationId ?? randomId(); const cache: ConversationCache = createConversationCache( createIdbChunkStore({ indexedDB: indexedDBFactory }), diff --git a/src/app/uuid.test.ts b/src/app/uuid.test.ts new file mode 100644 index 0000000..bd8e306 --- /dev/null +++ b/src/app/uuid.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "vitest"; +import { randomId } from "./uuid"; + +const V4_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; + +describe("randomId", () => { + it("returns a v4-shaped uuid", () => { + const id = randomId(); + expect(id).toMatch(V4_RE); + }); + + it("returns distinct values across calls", () => { + const ids = new Set<string>(); + for (let i = 0; i < 200; i++) { + ids.add(randomId()); + } + expect(ids.size).toBe(200); + }); + + it("works without crypto.randomUUID (getRandomValues branch)", () => { + const origRandomUUID = crypto.randomUUID; + try { + // Remove randomUUID so the getRandomValues branch is taken + delete (crypto as { randomUUID?: () => string }).randomUUID; + const id = randomId(); + expect(id).toMatch(V4_RE); + } finally { + crypto.randomUUID = origRandomUUID; + } + }); +}); diff --git a/src/app/uuid.ts b/src/app/uuid.ts new file mode 100644 index 0000000..ae39d4d --- /dev/null +++ b/src/app/uuid.ts @@ -0,0 +1,65 @@ +const HEX = "0123456789abcdef"; + +function hexChar(n: number): string { + return HEX.charAt(n & 0xf); +} + +function hexFromBytes(bytes: Uint8Array): string { + let out = ""; + for (let i = 0; i < bytes.length; i++) { + const b = bytes[i] as number; + out += hexChar(b >> 4); + out += hexChar(b); + } + return out; +} + +function formatV4(rand: Uint8Array): string { + const h = hexFromBytes(rand); + return ( + h.slice(0, 8) + + "-" + + h.slice(8, 12) + + "-4" + + h.slice(13, 16) + + "-" + + ((parseInt(h.slice(16, 18), 16) & 0x3f) | 0x80).toString(16).padStart(2, "0") + + h.slice(18, 20) + + "-" + + h.slice(20, 32) + ); +} + +function uuidFromGetRandomValues(): string { + const buf = new Uint8Array(16); + crypto.getRandomValues(buf); + buf[6] = ((buf[6] as number) & 0x0f) | 0x40; + buf[8] = ((buf[8] as number) & 0x3f) | 0x80; + return formatV4(buf); +} + +function uuidFromMathRandom(): string { + let s = ""; + for (let i = 0; i < 36; i++) { + if (i === 8 || i === 13 || i === 18 || i === 23) { + s += "-"; + } else if (i === 14) { + s += "4"; + } else if (i === 19) { + s += hexChar(Math.floor(Math.random() * 4) + 8); + } else { + s += hexChar(Math.floor(Math.random() * 16)); + } + } + return s; +} + +export function randomId(): string { + if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") { + return crypto.randomUUID(); + } + if (typeof crypto !== "undefined" && typeof crypto.getRandomValues === "function") { + return uuidFromGetRandomValues(); + } + return uuidFromMathRandom(); +} |
