Skip to content
Merged
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: 5 additions & 0 deletions .changeset/plenty-pumas-repeat.md
Original file line number Diff line number Diff line change
@@ -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.
62 changes: 62 additions & 0 deletions ui/lib/random-uuid.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
});
26 changes: 26 additions & 0 deletions ui/lib/random-uuid.ts
Original file line number Diff line number Diff line change
@@ -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)}`;
}
3 changes: 2 additions & 1 deletion ui/studio/views/table/ActiveTableView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1856,7 +1857,7 @@ function createEditorCellKey(args: {

function createEmptyStagedRowDraft(): Record<string, unknown> {
return {
[STAGED_ROW_DRAFT_ID_KEY]: crypto.randomUUID(),
[STAGED_ROW_DRAFT_ID_KEY]: randomUUID(),
};
}

Expand Down