From e43c291e6da8d3ca0ab564a4bef464546acfc27b Mon Sep 17 00:00:00 2001 From: Palagichev <26364895+Alex20923@users.noreply.github.com> Date: Wed, 22 Jul 2026 14:01:47 +0300 Subject: [PATCH] fix: fall back to getRandomValues when crypto.randomUUID is unavailable crypto.randomUUID() is only exposed in secure contexts (HTTPS or http://localhost). When SMDBox is served over plain HTTP on an IP address (e.g. straight from a JSON-RPC backend during development), crypto.randomUUID is undefined and invoking a method throws "TypeError: crypto.randomUUID is not a function" on submit, crashing the app. Add a randomUUID() helper that uses crypto.randomUUID when available and falls back to crypto.getRandomValues (which is exposed in insecure contexts too) otherwise. Route all six call sites through it and add unit tests covering the insecure-context fallback. --- src/components/MethodInvoker.tsx | 5 +++-- src/lib/__tests__/uuid.test.ts | 27 +++++++++++++++++++++++++++ src/lib/rpc.ts | 4 +++- src/lib/uuid.ts | 25 +++++++++++++++++++++++++ src/store/store.ts | 7 ++++--- 5 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 src/lib/__tests__/uuid.test.ts create mode 100644 src/lib/uuid.ts diff --git a/src/components/MethodInvoker.tsx b/src/components/MethodInvoker.tsx index 2655bcb..9ecc379 100644 --- a/src/components/MethodInvoker.tsx +++ b/src/components/MethodInvoker.tsx @@ -8,6 +8,7 @@ import { fromCurl, rpcParams, toCurl } from '../lib/curl'; import { formatBytes, formatDuration, jsonByteSize } from '../lib/format'; import { createRequest, type JsonRpcParams, type JsonRpcRequest } from '../lib/rpc'; import type { JsonSchema } from '../lib/smdToJsonSchema'; +import { randomUUID } from '../lib/uuid'; import { useStore } from '../store/store'; import { FormFromSchema } from './FormFromSchema'; import { JsonViewer } from './JsonViewer'; @@ -65,7 +66,7 @@ export function MethodInvoker({ schema, method, endpoint, headers }: MethodInvok setMeta({ durationMs: performance.now() - startedAtRef.current, size: jsonByteSize(resp) }); setLastResult({ value: resp.error ?? resp.result, errored: Boolean(resp.error) }); addHistory({ - id: crypto.randomUUID(), + id: randomUUID(), method, params, response: resp.result, @@ -77,7 +78,7 @@ export function MethodInvoker({ schema, method, endpoint, headers }: MethodInvok onError: (err) => { if (isAbort(err)) return; addHistory({ - id: crypto.randomUUID(), + id: randomUUID(), method, params, error: { message: String(err) }, diff --git a/src/lib/__tests__/uuid.test.ts b/src/lib/__tests__/uuid.test.ts new file mode 100644 index 0000000..d6ab246 --- /dev/null +++ b/src/lib/__tests__/uuid.test.ts @@ -0,0 +1,27 @@ +import { describe, it, expect, afterEach } from 'vitest'; + +import { randomUUID } from '../uuid'; + +const V4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; + +describe('randomUUID', () => { + const original = crypto.randomUUID; + + afterEach(() => { + Object.defineProperty(crypto, 'randomUUID', { value: original, configurable: true, writable: true }); + }); + + it('returns a valid RFC 4122 v4 UUID', () => { + expect(randomUUID()).toMatch(V4); + }); + + it('returns unique values', () => { + expect(randomUUID()).not.toBe(randomUUID()); + }); + + it('falls back to getRandomValues when crypto.randomUUID is unavailable (insecure context)', () => { + // Simulate a non-secure context (plain HTTP) where randomUUID is missing. + Object.defineProperty(crypto, 'randomUUID', { value: undefined, configurable: true, writable: true }); + expect(randomUUID()).toMatch(V4); + }); +}); diff --git a/src/lib/rpc.ts b/src/lib/rpc.ts index 0bb3ea3..7eef5ba 100644 --- a/src/lib/rpc.ts +++ b/src/lib/rpc.ts @@ -4,6 +4,8 @@ * adding a request timeout and cancellation (see UX_AUDIT.md 3.8). */ +import { randomUUID } from './uuid'; + export type JsonRpcParams = Record | unknown[]; export interface JsonRpcRequest { @@ -28,7 +30,7 @@ export interface JsonRpcResponse { /** Builds a JSON-RPC 2.0 request envelope with a fresh id. */ export function createRequest(method: string, params: JsonRpcParams = {}): JsonRpcRequest { - return { id: crypto.randomUUID(), jsonrpc: '2.0', method, params }; + return { id: randomUUID(), jsonrpc: '2.0', method, params }; } /** Request headers with the JSON content type applied. */ diff --git a/src/lib/uuid.ts b/src/lib/uuid.ts new file mode 100644 index 0000000..6f8283f --- /dev/null +++ b/src/lib/uuid.ts @@ -0,0 +1,25 @@ +/** + * Returns an RFC 4122 version 4 UUID. + * + * `crypto.randomUUID()` is only exposed in secure contexts (HTTPS or + * `http://localhost`). SMDBox is frequently served over plain HTTP on an IP + * address (e.g. straight from a JSON-RPC backend during development), where + * `crypto.randomUUID` is `undefined` and calling it throws + * `TypeError: crypto.randomUUID is not a function`, crashing the app. + * + * `crypto.getRandomValues()` is available in insecure contexts too, so we fall + * back to it when `randomUUID` is missing. + */ +export function randomUUID(): string { + if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { + return crypto.randomUUID(); + } + + const bytes = crypto.getRandomValues(new Uint8Array(16)); + const at = (i: number): number => bytes[i] ?? 0; + bytes[6] = (at(6) & 0x0f) | 0x40; // version 4 + bytes[8] = (at(8) & 0x3f) | 0x80; // variant 10xx + + 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)}`; +} diff --git a/src/store/store.ts b/src/store/store.ts index 30918cf..512ba58 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -2,6 +2,7 @@ import { create } from 'zustand'; import { writeState } from '../lib/persist'; import type { JsonRpcParams } from '../lib/rpc'; +import { randomUUID } from '../lib/uuid'; // Soft cap to keep IndexedDB bounded; the old hard limit of 10 is gone. const HISTORY_LIMIT = 500; @@ -224,7 +225,7 @@ export const useStore = create((set) => ({ saveRequest: (name, method, params) => set((s) => ({ - saved: [...s.saved, { id: crypto.randomUUID(), name, method, params, ts: Date.now() }], + saved: [...s.saved, { id: randomUUID(), name, method, params, ts: Date.now() }], })), deleteSaved: (id) => set((s) => ({ saved: s.saved.filter((r) => r.id !== id) })), @@ -249,7 +250,7 @@ export const useStore = create((set) => ({ set((s) => ({ savedResponses: [ ...s.savedResponses, - { id: crypto.randomUUID(), name, method, response, ts: Date.now() }, + { id: randomUUID(), name, method, response, ts: Date.now() }, ], })), @@ -308,7 +309,7 @@ export const useStore = create((set) => ({ activeEnvironmentId: existing.id, }; } - const id = crypto.randomUUID(); + const id = randomUUID(); return { environments: [...s.environments, { id, name: trimmed, ...snapshot }], activeEnvironmentId: id,