feat(site): add generateUUID helper (#27661)

Adds a `generateUUID()` helper to `site/src/utils/uuid.ts`.

It uses `crypto.randomUUID()` when available, and otherwise falls back
to `crypto.getRandomValues()`, setting the version (4) and variant (RFC
4122) bits before formatting the 16 random bytes into the standard
`8-4-4-4-12` UUID string.

Seriously open to any implementation here, let me know if you have a
favorite!

---
_This PR was created by Coder Agents on behalf of @jeremyruppel._
This commit is contained in:
Jeremy Ruppel
2026-07-30 10:46:40 -04:00
committed by GitHub
parent b3852c707b
commit cf7f876880
3 changed files with 99 additions and 1 deletions
@@ -11,6 +11,7 @@ import { Loader } from "#/components/Loader/Loader";
import { useAuthenticated } from "#/hooks/useAuthenticated";
import { linkToTemplate, useLinks } from "#/modules/navigation";
import { pageTitle } from "#/utils/page";
import { generateUUID } from "#/utils/uuid";
import { TemplateBuilderPageView } from "./TemplateBuilderPageView";
import type {
SelectedBaseMeta,
@@ -29,7 +30,7 @@ const TemplateBuilderPage: FC = () => {
// Stable session ID for the lifetime of this page mount, shared
// across wizard_entry and compose_completion telemetry events.
const sessionId = useMemo(() => crypto.randomUUID(), []);
const sessionId = useMemo(() => generateUUID(), []);
const builderDisabled = data?.config?.template_builder?.disabled ?? false;
const wizardReady =
+65
View File
@@ -0,0 +1,65 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { generateUUID, isUUID } from "./uuid";
describe("generateUUID", () => {
afterEach(() => {
vi.restoreAllMocks();
});
it("delegates to crypto.randomUUID when available", () => {
const uuid = "11111111-1111-4111-8111-111111111111";
const randomUUID = vi.spyOn(crypto, "randomUUID").mockReturnValue(uuid);
expect(generateUUID()).toBe(uuid);
expect(randomUUID).toHaveBeenCalledTimes(1);
});
it("returns a valid version 4 UUID via the native path", () => {
expect(isUUID(generateUUID())).toBe(true);
});
describe("fallback (crypto.randomUUID unavailable)", () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it("sets the version and variant bits regardless of random input", () => {
// All-zero bytes leave only the version and variant bits that
// generateUUID sets itself.
vi.stubGlobal("crypto", {
getRandomValues: <T extends ArrayBufferView | null>(array: T): T => {
if (array instanceof Uint8Array) {
array.fill(0);
}
return array;
},
});
const uuid = generateUUID();
expect(isUUID(uuid)).toBe(true);
expect(uuid).toBe("00000000-0000-4000-8000-000000000000");
});
it("preserves the remaining random bits", () => {
// All-one bytes verify only the version and variant nibbles are
// masked (4 and b), leaving every other bit untouched.
vi.stubGlobal("crypto", {
getRandomValues: <T extends ArrayBufferView | null>(array: T): T => {
if (array instanceof Uint8Array) {
array.fill(0xff);
}
return array;
},
});
const uuid = generateUUID();
expect(isUUID(uuid)).toBe(true);
expect(uuid).toBe("ffffffff-ffff-4fff-bfff-ffffffffffff");
});
});
it("generates unique values across calls", () => {
const uuids = new Set(Array.from({ length: 1000 }, () => generateUUID()));
expect(uuids.size).toBe(1000);
});
});
+32
View File
@@ -3,3 +3,35 @@ export const isUUID = (text: string) => {
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
return UUID.test(text);
};
/**
* Generate a random RFC 4122 version 4 UUID.
*
* Uses `crypto.randomUUID()` when the runtime provides it. Falls back to
* generating random bytes with `crypto.getRandomValues()` and formatting
* them as a v4 UUID for environments where `randomUUID` is unavailable
* (for example, non-secure contexts).
*/
export const generateUUID = (): string => {
if (typeof crypto.randomUUID === "function") {
return crypto.randomUUID();
}
const bytes = crypto.getRandomValues(new Uint8Array(16));
// Set the version (4) and variant (RFC 4122) bits.
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
const hex: string[] = [];
for (const byte of bytes) {
hex.push(byte.toString(16).padStart(2, "0"));
}
return [
hex.slice(0, 4).join(""),
hex.slice(4, 6).join(""),
hex.slice(6, 8).join(""),
hex.slice(8, 10).join(""),
hex.slice(10, 16).join(""),
].join("-");
};