From cf7f87688058905cb9a8354dd762754e7bb97ccb Mon Sep 17 00:00:00 2001 From: Jeremy Ruppel Date: Thu, 30 Jul 2026 10:46:40 -0400 Subject: [PATCH] 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._ --- .../TemplateBuilder/TemplateBuilderPage.tsx | 3 +- site/src/utils/uuid.test.ts | 65 +++++++++++++++++++ site/src/utils/uuid.ts | 32 +++++++++ 3 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 site/src/utils/uuid.test.ts diff --git a/site/src/pages/TemplateBuilder/TemplateBuilderPage.tsx b/site/src/pages/TemplateBuilder/TemplateBuilderPage.tsx index 340c5bb35d..befcaba55c 100644 --- a/site/src/pages/TemplateBuilder/TemplateBuilderPage.tsx +++ b/site/src/pages/TemplateBuilder/TemplateBuilderPage.tsx @@ -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 = diff --git a/site/src/utils/uuid.test.ts b/site/src/utils/uuid.test.ts new file mode 100644 index 0000000000..9e381b918e --- /dev/null +++ b/site/src/utils/uuid.test.ts @@ -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: (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: (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); + }); +}); diff --git a/site/src/utils/uuid.ts b/site/src/utils/uuid.ts index 999bbbb4da..3ae65ff62f 100644 --- a/site/src/utils/uuid.ts +++ b/site/src/utils/uuid.ts @@ -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("-"); +};