fix(frontend): polyfill crypto.randomUUID 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.

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) <noreply@anthropic.com>
This commit is contained in:
David Montero Crespo 2026-05-05 13:41:30 -03:00
parent cd3ee6172b
commit 694a2f4073
3 changed files with 37 additions and 6 deletions

View File

@ -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;

View File

@ -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<EditorState>((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<EditorState>((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<EditorState>((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<EditorState>((set, get) => ({
const openGroupFileIds: Record<string, string[]> = {};
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,

View File

@ -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)}`;
}