Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/components/MethodInvoker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand All @@ -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) },
Expand Down
27 changes: 27 additions & 0 deletions src/lib/__tests__/uuid.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
4 changes: 3 additions & 1 deletion src/lib/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* adding a request timeout and cancellation (see UX_AUDIT.md 3.8).
*/

import { randomUUID } from './uuid';

export type JsonRpcParams = Record<string, unknown> | unknown[];

export interface JsonRpcRequest {
Expand All @@ -28,7 +30,7 @@ export interface JsonRpcResponse<T = unknown> {

/** 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. */
Expand Down
25 changes: 25 additions & 0 deletions src/lib/uuid.ts
Original file line number Diff line number Diff line change
@@ -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)}`;
}
7 changes: 4 additions & 3 deletions src/store/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -224,7 +225,7 @@ export const useStore = create<AppState>((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) })),
Expand All @@ -249,7 +250,7 @@ export const useStore = create<AppState>((set) => ({
set((s) => ({
savedResponses: [
...s.savedResponses,
{ id: crypto.randomUUID(), name, method, response, ts: Date.now() },
{ id: randomUUID(), name, method, response, ts: Date.now() },
],
})),

Expand Down Expand Up @@ -308,7 +309,7 @@ export const useStore = create<AppState>((set) => ({
activeEnvironmentId: existing.id,
};
}
const id = crypto.randomUUID();
const id = randomUUID();
return {
environments: [...s.environments, { id, name: trimmed, ...snapshot }],
activeEnvironmentId: id,
Expand Down