From 379f797b52ece0a498bb6838a972304936afd5e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Bramer=20Schmidt?= Date: Fri, 17 Jul 2026 21:06:57 +0700 Subject: [PATCH] fix: fall back to getRandomValues when crypto.randomUUID is unavailable crypto.randomUUID only exists in secure contexts (https or localhost). When Studio is served over plain HTTP on a non-localhost host (e.g. http://192.168.x.x:5555), staging a new row crashed with "TypeError: crypto.randomUUID is not a function". Route UUID generation through a shared helper that prefers the native crypto.randomUUID and otherwise builds an RFC 4122 UUIDv4 from crypto.getRandomValues, which is available in non-secure contexts. Fixes prisma/studio#1493 Co-Authored-By: Claude Fable 5 --- .changeset/plenty-pumas-repeat.md | 5 ++ ui/lib/random-uuid.test.ts | 62 +++++++++++++++++++++++ ui/lib/random-uuid.ts | 26 ++++++++++ ui/studio/views/table/ActiveTableView.tsx | 3 +- 4 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 .changeset/plenty-pumas-repeat.md create mode 100644 ui/lib/random-uuid.test.ts create mode 100644 ui/lib/random-uuid.ts diff --git a/.changeset/plenty-pumas-repeat.md b/.changeset/plenty-pumas-repeat.md new file mode 100644 index 00000000..6c25082d --- /dev/null +++ b/.changeset/plenty-pumas-repeat.md @@ -0,0 +1,5 @@ +--- +"@prisma/studio-core": patch +--- + +Fix `TypeError: crypto.randomUUID is not a function` when Studio is served over plain HTTP on a non-localhost host (non-secure context, e.g. `http://192.168.x.x:5555`). UUID generation now falls back to a UUIDv4 built from `crypto.getRandomValues` when `crypto.randomUUID` is unavailable. diff --git a/ui/lib/random-uuid.test.ts b/ui/lib/random-uuid.test.ts new file mode 100644 index 00000000..fa295ef1 --- /dev/null +++ b/ui/lib/random-uuid.test.ts @@ -0,0 +1,62 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +import { randomUUID } from "./random-uuid"; + +const UUID_V4_PATTERN = + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; + +const nativeGetRandomValues = globalThis.crypto.getRandomValues.bind( + globalThis.crypto, +); + +describe("randomUUID", () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("uses the native crypto.randomUUID when available", () => { + const nativeRandomUUID = vi.fn( + () => "11111111-2222-4333-8444-555555555555" as const, + ); + + vi.stubGlobal("crypto", { + getRandomValues: nativeGetRandomValues, + randomUUID: nativeRandomUUID, + }); + + expect(randomUUID()).toBe("11111111-2222-4333-8444-555555555555"); + expect(nativeRandomUUID).toHaveBeenCalledTimes(1); + }); + + describe("when crypto.randomUUID is unavailable (non-secure context)", () => { + it("falls back to a UUIDv4 built from crypto.getRandomValues", () => { + vi.stubGlobal("crypto", { + getRandomValues: nativeGetRandomValues, + }); + + const generated = new Set(Array.from({ length: 32 }, () => randomUUID())); + + for (const uuid of generated) { + expect(uuid).toMatch(UUID_V4_PATTERN); + } + + expect(generated.size).toBe(32); + }); + + it("sets the RFC 4122 version and variant bits", () => { + const fillWith = (value: number) => + vi.stubGlobal("crypto", { + getRandomValues: (array: Uint8Array) => { + array.fill(value); + return array; + }, + }); + + fillWith(0x00); + expect(randomUUID()).toBe("00000000-0000-4000-8000-000000000000"); + + fillWith(0xff); + expect(randomUUID()).toBe("ffffffff-ffff-4fff-bfff-ffffffffffff"); + }); + }); +}); diff --git a/ui/lib/random-uuid.ts b/ui/lib/random-uuid.ts new file mode 100644 index 00000000..70712041 --- /dev/null +++ b/ui/lib/random-uuid.ts @@ -0,0 +1,26 @@ +/** + * Generates a random UUIDv4 string. + * + * Prefers the native `crypto.randomUUID`, which is only available in secure + * contexts (https or localhost). When Studio is served over plain HTTP on a + * non-localhost host (e.g. `http://192.168.x.x:5555`), `crypto.randomUUID` is + * undefined, so this falls back to building a UUIDv4 from + * `crypto.getRandomValues`, which is available in non-secure contexts too. + */ +export function randomUUID(): string { + if (typeof globalThis.crypto.randomUUID === "function") { + return globalThis.crypto.randomUUID(); + } + + const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16)); + + // Per RFC 4122 section 4.4: set the version to 4 and the variant to 10xx. + bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40; + bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80; + + const hex = Array.from(bytes, (byte) => + byte.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/ui/studio/views/table/ActiveTableView.tsx b/ui/studio/views/table/ActiveTableView.tsx index cdf7f11b..3b133bf2 100644 --- a/ui/studio/views/table/ActiveTableView.tsx +++ b/ui/studio/views/table/ActiveTableView.tsx @@ -54,6 +54,7 @@ import { useSorting } from "../../../hooks/use-sorting"; import { useStreams } from "../../../hooks/use-streams"; import { useTableUiState } from "../../../hooks/use-table-ui-state"; import { useUiState } from "../../../hooks/use-ui-state"; +import { randomUUID } from "../../../lib/random-uuid"; import { cn } from "../../../lib/utils"; import { Cell, @@ -1856,7 +1857,7 @@ function createEditorCellKey(args: { function createEmptyStagedRowDraft(): Record { return { - [STAGED_ROW_DRAFT_ID_KEY]: crypto.randomUUID(), + [STAGED_ROW_DRAFT_ID_KEY]: randomUUID(), }; }