From 6dbb846d3dc75b5afc66cac2eef7bb44b837d05f Mon Sep 17 00:00:00 2001 From: pufit Date: Wed, 24 Jun 2026 12:30:53 -0400 Subject: [PATCH] Fix crypto.randomUUID crash in non-secure (plain-HTTP) contexts crypto.randomUUID() is only available in secure contexts (HTTPS or localhost), so accessing the UI over plain HTTP via a LAN host throws "TypeError: crypto.randomUUID is not a function". Add a randomUUID() helper that falls back to a v4 UUID built from crypto.getRandomValues() (available in non-secure contexts), then Math.random() as a last resort. Use it in chatStore and ChatInput. --- web/src/components/Chat/ChatInput.tsx | 3 +- web/src/stores/chatStore.ts | 3 +- web/src/utils/uuid.ts | 44 +++++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 web/src/utils/uuid.ts diff --git a/web/src/components/Chat/ChatInput.tsx b/web/src/components/Chat/ChatInput.tsx index 73d3b71..56c6455 100644 --- a/web/src/components/Chat/ChatInput.tsx +++ b/web/src/components/Chat/ChatInput.tsx @@ -3,6 +3,7 @@ import { Send, Square, X, Plus, Trash2, Sparkles, HelpCircle, StickyNote, Paperc import { useChatStore } from '../../stores/chatStore'; import type { QuoteAction, QuoteEntry } from '../../stores/chatStore'; import { api } from '../../api/client'; +import { randomUUID } from '../../utils/uuid'; import { PromptRewriteCard } from './PromptRewriteCard'; const ACTION_CONFIG: Record = { @@ -158,7 +159,7 @@ export function ChatInput({ onSend, onStop, isStreaming, disabled }: { const addFiles = useCallback(async (files: File[]) => { const newAttachments: AttachmentFile[] = files.map(file => ({ - id: crypto.randomUUID(), + id: randomUUID(), file, preview: file.type.startsWith('image/') ? URL.createObjectURL(file) : undefined, uploading: true, diff --git a/web/src/stores/chatStore.ts b/web/src/stores/chatStore.ts index b398753..27e26bb 100644 --- a/web/src/stores/chatStore.ts +++ b/web/src/stores/chatStore.ts @@ -4,6 +4,7 @@ import { ws } from '../api/websocket'; import type { WSMessage } from '../api/websocket'; import type { ChatMessage, MessageBlock, Session, AgentStatus, PanelTab, ModifiedFileSummary } from '../types/chat'; import { hydrateMessage } from '../utils/hydrateMessage'; +import { randomUUID } from '../utils/uuid'; // Helpers import { cancelAutoClose, clearAllAutoCloseTimers, MAX_COMPLETED_TABS } from './helpers/blockHelpers'; import { extractTodosFromMessages, extractCCTasksFromMessages } from './helpers/bufferReplay'; @@ -447,7 +448,7 @@ export const useChatStore = create((set, get) => ({ if (get().activeSession !== existing.id) await get().switchSession(existing.id); return; } - const id = crypto.randomUUID(); + const id = randomUUID(); const now = new Date().toISOString(); const virtual: Session = { id, title: '', source: 'web', status: 'created', diff --git a/web/src/utils/uuid.ts b/web/src/utils/uuid.ts new file mode 100644 index 0000000..3ed8720 --- /dev/null +++ b/web/src/utils/uuid.ts @@ -0,0 +1,44 @@ +/** + * UUID generation that works in non-secure contexts. + * + * `crypto.randomUUID()` is only exposed in *secure contexts* — HTTPS or + * `http://localhost`. When the app is accessed over plain HTTP via a LAN + * hostname or IP (e.g. `http://192.168.x.x:8900` or a `.local` mDNS name), + * `crypto.randomUUID` is `undefined` and calling it throws + * `TypeError: crypto.randomUUID is not a function`. + * + * `crypto.getRandomValues()`, however, IS available in non-secure contexts, + * so we derive an RFC 4122 version-4 UUID from it as a fallback. As a last + * resort (no Web Crypto at all) we fall back to `Math.random()` — these ids + * are only used as client-side keys, never for anything security-sensitive. + */ +export function randomUUID(): string { + // Fast path: native, available in secure contexts. + if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { + return crypto.randomUUID(); + } + + // Fallback: build a v4 UUID from crypto.getRandomValues (non-secure-context safe). + if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') { + const bytes = crypto.getRandomValues(new Uint8Array(16)); + bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4 + bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 10 + const hex: string[] = []; + for (let i = 0; i < 256; i++) hex.push((i + 0x100).toString(16).slice(1)); + return ( + hex[bytes[0]] + hex[bytes[1]] + hex[bytes[2]] + hex[bytes[3]] + '-' + + hex[bytes[4]] + hex[bytes[5]] + '-' + + hex[bytes[6]] + hex[bytes[7]] + '-' + + hex[bytes[8]] + hex[bytes[9]] + '-' + + hex[bytes[10]] + hex[bytes[11]] + hex[bytes[12]] + hex[bytes[13]] + hex[bytes[14]] + hex[bytes[15]] + ); + } + + // Last resort: no Web Crypto at all. Not cryptographically strong, but these + // ids are only client-side keys, never security-sensitive. + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { + const r = (Math.random() * 16) | 0; + const v = c === 'x' ? r : (r & 0x3) | 0x8; + return v.toString(16); + }); +}