From 694a2f4073872566e6b13e6110ba342dc2315ddd Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Tue, 5 May 2026 13:41:30 -0300 Subject: [PATCH] fix(frontend): polyfill crypto.randomUUID for non-secure contexts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit crypto.randomUUID() is only exposed on secure contexts (HTTPS, localhost, 127.0.0.1, ::1). When Velxio is self-hosted and accessed via a LAN IP over plain HTTP (e.g. http://192.168.31.139:3080/), crypto.randomUUID is undefined and any code path that calls it throws TypeError. This silently broke ESP32 simulation start for self-hosters: the frontend generates a UUID for the WS client_id when Run is clicked; the throw rejected the promise before reaching the WS connect, so the backend never got the start request — no worker spawned, logs empty, simulation "didn't start" with no visible error. Same root cause would also break the multi-file editor (createFile, createFileGroup) on the same LAN-HTTP self-host setup, just less observably. Add a single generateUUID() helper that: 1. Uses crypto.randomUUID() when available (secure context fast path). 2. Falls back to crypto.getRandomValues() — which IS available in non-secure contexts — to build a v4 UUID by hand. 3. Final fallback to Math.random() if even that is missing (defensive — Web Crypto getRandomValues has been universal for years). Replace all 6 crypto.randomUUID() call sites: - frontend/src/simulation/Esp32Bridge.ts (2 sites — getTabSessionId) - frontend/src/store/useEditorStore.ts (4 sites — file IDs) Reported by a self-hoster on OrangePi 5B accessing Velxio via LAN IP. DevTools console showed: TypeError: crypto.randomUUID is not a function at Ph (...) at wh.connect (...) at startBoard (...) Co-Authored-By: Claude Opus 4.7 (1M context) --- frontend/src/simulation/Esp32Bridge.ts | 5 +++-- frontend/src/store/useEditorStore.ts | 9 ++++---- frontend/src/utils/uuid.ts | 29 ++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 6 deletions(-) create mode 100644 frontend/src/utils/uuid.ts diff --git a/frontend/src/simulation/Esp32Bridge.ts b/frontend/src/simulation/Esp32Bridge.ts index 5be39b4f..4e2679f9 100644 --- a/frontend/src/simulation/Esp32Bridge.ts +++ b/frontend/src/simulation/Esp32Bridge.ts @@ -32,6 +32,7 @@ */ import type { BoardKind } from '../types/board'; +import { generateUUID } from '../utils/uuid'; /** * Map any ESP32-family board kind to the 3 base QEMU machine types understood @@ -51,11 +52,11 @@ const API_BASE = (): string => /** Returns a stable UUID for this browser tab (persists across reloads, resets on new tab). */ export function getTabSessionId(): string { // sessionStorage is not available in Node/test environments - if (typeof sessionStorage === 'undefined') return crypto.randomUUID(); + if (typeof sessionStorage === 'undefined') return generateUUID(); const KEY = 'velxio-tab-id'; let id = sessionStorage.getItem(KEY); if (!id) { - id = crypto.randomUUID(); + id = generateUUID(); sessionStorage.setItem(KEY, id); } return id; diff --git a/frontend/src/store/useEditorStore.ts b/frontend/src/store/useEditorStore.ts index df67fa75..bd48f258 100644 --- a/frontend/src/store/useEditorStore.ts +++ b/frontend/src/store/useEditorStore.ts @@ -1,4 +1,5 @@ import { create } from 'zustand'; +import { generateUUID } from '../utils/uuid'; export interface WorkspaceFile { id: string; @@ -151,7 +152,7 @@ export const useEditorStore = create((set, get) => ({ // ── File operations (legacy API — operate on active group) ────────────── createFile: (name: string) => { - const id = crypto.randomUUID(); + const id = generateUUID(); const newFile: WorkspaceFile = { id, name, content: '', modified: false }; set((s) => { const groupId = s.activeGroupId; @@ -279,7 +280,7 @@ export const useEditorStore = create((set, get) => ({ loadFiles: (incoming: { name: string; content: string }[]) => { const files: WorkspaceFile[] = incoming.map((f, i) => ({ - id: i === 0 ? MAIN_ID : crypto.randomUUID(), + id: i === 0 ? MAIN_ID : generateUUID(), name: f.name, content: f.content, modified: false, @@ -315,7 +316,7 @@ export const useEditorStore = create((set, get) => ({ let files: WorkspaceFile[]; if (initialFiles && initialFiles.length > 0) { files = initialFiles.map((f, i) => ({ - id: i === 0 ? `${groupId}-main` : crypto.randomUUID(), + id: i === 0 ? `${groupId}-main` : generateUUID(), name: f.name, content: f.content, modified: false, @@ -399,7 +400,7 @@ export const useEditorStore = create((set, get) => ({ const openGroupFileIds: Record = {}; for (const [gid, files] of Object.entries(groups)) { const wsFiles: WorkspaceFile[] = files.map((f, i) => ({ - id: i === 0 ? `${gid}-main` : crypto.randomUUID(), + id: i === 0 ? `${gid}-main` : generateUUID(), name: f.name, content: f.content, modified: false, diff --git a/frontend/src/utils/uuid.ts b/frontend/src/utils/uuid.ts new file mode 100644 index 00000000..5e9c0657 --- /dev/null +++ b/frontend/src/utils/uuid.ts @@ -0,0 +1,29 @@ +/** + * UUID v4 generator with a polyfill for non-secure contexts. + * + * `crypto.randomUUID()` is only exposed on secure contexts (HTTPS, localhost, + * 127.0.0.1, ::1). When Velxio is self-hosted and accessed via a LAN IP over + * plain HTTP (e.g. `http://192.168.31.139:3080/`), `crypto.randomUUID` is + * `undefined` and any code path that calls it throws `TypeError`. That bug + * silently broke ESP32 simulation start for self-hosters — frontend never sent + * the start request because the UUID call rejected before reaching the WS + * connection. + * + * `crypto.getRandomValues()` IS available in non-secure contexts, so we + * fall back to building a v4 UUID by hand. + */ +export function generateUUID(): string { + if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { + return crypto.randomUUID(); + } + const bytes = new Uint8Array(16); + if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') { + crypto.getRandomValues(bytes); + } else { + for (let i = 0; i < 16; i++) bytes[i] = Math.floor(Math.random() * 256); + } + 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)}`; +}