mirror of
https://github.com/coder/coder.git
synced 2026-09-22 13:10:21 +08:00
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:
@@ -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 =
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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("-");
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user