From 7aff077e709baf6aa65d2aab296eb8742907a29a Mon Sep 17 00:00:00 2001 From: Andrew Aquino Date: Thu, 6 Aug 2026 19:38:25 +0000 Subject: [PATCH 1/7] feat: generate random hex string (non-uuid) for template builder session ID --- .../pages/TemplateBuilder/TemplateBuilderPage.tsx | 4 ++-- site/src/utils/random.ts | 13 +++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/site/src/pages/TemplateBuilder/TemplateBuilderPage.tsx b/site/src/pages/TemplateBuilder/TemplateBuilderPage.tsx index e1a443b6580..041cbc248b8 100644 --- a/site/src/pages/TemplateBuilder/TemplateBuilderPage.tsx +++ b/site/src/pages/TemplateBuilder/TemplateBuilderPage.tsx @@ -1,7 +1,6 @@ import { type FC, useCallback, useEffect, useMemo, useState } from "react"; import { useMutation, useQuery } from "react-query"; import { Navigate, useNavigate, useSearchParams } from "react-router"; -import { v4 as uuidv4 } from "uuid"; import { deploymentConfig } from "#/api/queries/deployment"; import { createTemplateFromBuilder, @@ -12,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 { generateSessionId } from "#/utils/random"; import { TemplateBuilderPageView } from "./TemplateBuilderPageView"; import type { SelectedBaseMeta, @@ -30,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(() => uuidv4(), []); + const sessionId = useMemo(() => generateSessionId(), []); const builderDisabled = data?.config?.template_builder?.disabled ?? false; const wizardReady = diff --git a/site/src/utils/random.ts b/site/src/utils/random.ts index 6a995b29393..52b3ebccdda 100644 --- a/site/src/utils/random.ts +++ b/site/src/utils/random.ts @@ -17,3 +17,16 @@ export const generateRandomString = (bytes: number): string => { } return btoa(strArr.join("")); }; + +/** + * Generate a 16-byte (32-character) hexadecimal string for identifying + * workspace connection sessions. + */ +export const generateSessionId = (): string => { + const byteArr = crypto.getRandomValues(new Uint8Array(16)); + const strArr: string[] = []; + for (const byte of byteArr) { + strArr.push(byte.toString(16).padStart(2, "0")); + } + return strArr.join(""); +}; From 9f3097e761c2a2fec0858639e99cf8808bf5ce73 Mon Sep 17 00:00:00 2001 From: Andrew Aquino Date: Thu, 6 Aug 2026 19:44:19 +0000 Subject: [PATCH 2/7] refactor: rename generateRandomString to generateRandomBase64String --- site/src/contexts/useProxyLatency.ts | 4 ++-- site/src/pages/UsersPage/UsersPage.tsx | 4 ++-- site/src/utils/random.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/site/src/contexts/useProxyLatency.ts b/site/src/contexts/useProxyLatency.ts index a3f63f21306..ae1d26edbf3 100644 --- a/site/src/contexts/useProxyLatency.ts +++ b/site/src/contexts/useProxyLatency.ts @@ -1,7 +1,7 @@ import { useEffect, useReducer, useState } from "react"; import { API } from "#/api/api"; import type { Region } from "#/api/typesGenerated"; -import { generateRandomString } from "#/utils/random"; +import { generateRandomBase64String } from "#/utils/random"; const proxyIntervalSeconds = 30; // seconds @@ -133,7 +133,7 @@ export const useProxyLatency = ( // Add a random query param to the url to make sure we don't get a cached response. // This is important in case there is some caching layer between us and the proxy. const url = new URL( - `/latency-check?cache_bust=${generateRandomString(6)}`, + `/latency-check?cache_bust=${generateRandomBase64String(6)}`, proxy.path_app_url, ); acc[url.toString()] = proxy; diff --git a/site/src/pages/UsersPage/UsersPage.tsx b/site/src/pages/UsersPage/UsersPage.tsx index 2151dc954db..37144254673 100644 --- a/site/src/pages/UsersPage/UsersPage.tsx +++ b/site/src/pages/UsersPage/UsersPage.tsx @@ -25,7 +25,7 @@ import { shouldShowAISeatColumn } from "#/modules/dashboard/entitlements"; import { useDashboard } from "#/modules/dashboard/useDashboard"; import { RoleSelectorDialog } from "#/modules/roles/RoleSelectorDialog"; import { pageTitle } from "#/utils/page"; -import { generateRandomString } from "#/utils/random"; +import { generateRandomBase64String } from "#/utils/random"; import { ResetPasswordDialog } from "./ResetPasswordDialog"; import { UsersPageView } from "./UsersPageView"; @@ -122,7 +122,7 @@ const UsersPage: React.FC = () => { newPassword: process.env.STORYBOOK === "true" ? "hello-storybook" - : generateRandomString(12), + : generateRandomBase64String(12), }); }} onSuspendUser={setUserToSuspend} diff --git a/site/src/utils/random.ts b/site/src/utils/random.ts index 52b3ebccdda..da5507d0618 100644 --- a/site/src/utils/random.ts +++ b/site/src/utils/random.ts @@ -6,7 +6,7 @@ * equal the number of randomly generated bytes. * @see */ -export const generateRandomString = (bytes: number): string => { +export const generateRandomBase64String = (bytes: number): string => { const byteArr = window.crypto.getRandomValues(new Uint8Array(bytes)); // The types for `map` don't seem to support mapping from one array type to // another and `String.fromCharCode.apply` wants `number[]` so loop like this From 0a91a080ae70ea0d186fe2bf253678ffb2e745ff Mon Sep 17 00:00:00 2001 From: Andrew Aquino Date: Thu, 6 Aug 2026 19:45:12 +0000 Subject: [PATCH 3/7] refactor: extract generateRandomString function to be reused by random.ts functions --- site/src/utils/random.ts | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/site/src/utils/random.ts b/site/src/utils/random.ts index da5507d0618..b47ca222943 100644 --- a/site/src/utils/random.ts +++ b/site/src/utils/random.ts @@ -1,3 +1,18 @@ +/** + * Generate a random hexadecimal string from the specified number of bytes. + */ +const generateRandomString = (bytes: number): string => { + const byteArr = crypto.getRandomValues(new Uint8Array(bytes)); + // The types for `map` don't seem to support mapping from one array type to + // another and `String.fromCharCode.apply` wants `number[]` so loop like this + // instead. + const strArr: string[] = []; + for (const byte of byteArr) { + strArr.push(byte.toString(16).padStart(2, "0")); + } + return strArr.join(""); +}; + /** * Generate a cryptographically secure random string using the specified number * of bytes then encode with base64. @@ -7,15 +22,7 @@ * @see */ export const generateRandomBase64String = (bytes: number): string => { - const byteArr = window.crypto.getRandomValues(new Uint8Array(bytes)); - // The types for `map` don't seem to support mapping from one array type to - // another and `String.fromCharCode.apply` wants `number[]` so loop like this - // instead. - const strArr: string[] = []; - for (const byte of byteArr) { - strArr.push(String.fromCharCode(byte)); - } - return btoa(strArr.join("")); + return btoa(generateRandomString(bytes)); }; /** @@ -23,10 +30,5 @@ export const generateRandomBase64String = (bytes: number): string => { * workspace connection sessions. */ export const generateSessionId = (): string => { - const byteArr = crypto.getRandomValues(new Uint8Array(16)); - const strArr: string[] = []; - for (const byte of byteArr) { - strArr.push(byte.toString(16).padStart(2, "0")); - } - return strArr.join(""); + return generateRandomString(16); }; From 3a68b6a6cbb9eed453273949a534ba8a9648c6aa Mon Sep 17 00:00:00 2001 From: Andrew Aquino Date: Thu, 6 Aug 2026 19:47:08 +0000 Subject: [PATCH 4/7] refactor: convert for...of to map, removing comment --- site/src/utils/random.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/site/src/utils/random.ts b/site/src/utils/random.ts index b47ca222943..52907b3c92a 100644 --- a/site/src/utils/random.ts +++ b/site/src/utils/random.ts @@ -3,14 +3,9 @@ */ const generateRandomString = (bytes: number): string => { const byteArr = crypto.getRandomValues(new Uint8Array(bytes)); - // The types for `map` don't seem to support mapping from one array type to - // another and `String.fromCharCode.apply` wants `number[]` so loop like this - // instead. - const strArr: string[] = []; - for (const byte of byteArr) { - strArr.push(byte.toString(16).padStart(2, "0")); - } - return strArr.join(""); + return [...byteArr] + .map((byte) => byte.toString(16).padStart(2, "0")) + .join(""); }; /** From 261e97f2ef67b159ed2c44d338fa06840dad16c3 Mon Sep 17 00:00:00 2001 From: Andrew Aquino Date: Thu, 6 Aug 2026 21:56:29 +0000 Subject: [PATCH 5/7] test: add random.test.ts --- site/src/utils/random.test.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 site/src/utils/random.test.ts diff --git a/site/src/utils/random.test.ts b/site/src/utils/random.test.ts new file mode 100644 index 00000000000..f7632fa2c7e --- /dev/null +++ b/site/src/utils/random.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; +import { generateSessionId } from "#/utils/random"; + +describe("generateSessionId", () => { + it("is 32 characters long", () => { + const id = generateSessionId(); + expect(id.length).toBe(32); + }); + + it("outputs a hexadecimal string", () => { + const id = generateSessionId(); + const hexRegex = /^[\da-f]+$/; + expect(hexRegex.test(id)).toBe(true); + }); + + it("generates unique values across calls", () => { + const numValues = 1000; + const ids = new Set( + Array.from({ length: numValues }, () => generateSessionId()), + ); + expect(ids.size).toBe(numValues); + }); +}); From 5347e9f8c267d7f70e341d4cef0a47edb37b1b43 Mon Sep 17 00:00:00 2001 From: Andrew Aquino Date: Mon, 10 Aug 2026 23:45:39 +0000 Subject: [PATCH 6/7] fix: revert back to uuid for template builder session id --- site/src/pages/TemplateBuilder/TemplateBuilderPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/pages/TemplateBuilder/TemplateBuilderPage.tsx b/site/src/pages/TemplateBuilder/TemplateBuilderPage.tsx index 041cbc248b8..e1a443b6580 100644 --- a/site/src/pages/TemplateBuilder/TemplateBuilderPage.tsx +++ b/site/src/pages/TemplateBuilder/TemplateBuilderPage.tsx @@ -1,6 +1,7 @@ import { type FC, useCallback, useEffect, useMemo, useState } from "react"; import { useMutation, useQuery } from "react-query"; import { Navigate, useNavigate, useSearchParams } from "react-router"; +import { v4 as uuidv4 } from "uuid"; import { deploymentConfig } from "#/api/queries/deployment"; import { createTemplateFromBuilder, @@ -11,7 +12,6 @@ import { Loader } from "#/components/Loader/Loader"; import { useAuthenticated } from "#/hooks/useAuthenticated"; import { linkToTemplate, useLinks } from "#/modules/navigation"; import { pageTitle } from "#/utils/page"; -import { generateSessionId } from "#/utils/random"; import { TemplateBuilderPageView } from "./TemplateBuilderPageView"; import type { SelectedBaseMeta, @@ -30,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(() => generateSessionId(), []); + const sessionId = useMemo(() => uuidv4(), []); const builderDisabled = data?.config?.template_builder?.disabled ?? false; const wizardReady = From 1d0cd2d10403d4b369353ac08ea311a86b5fc697 Mon Sep 17 00:00:00 2001 From: Andrew Aquino Date: Mon, 10 Aug 2026 23:48:21 +0000 Subject: [PATCH 7/7] refactor: rename generateSessionId to generateConnectionSessionId --- site/src/utils/random.test.ts | 10 +++++----- site/src/utils/random.ts | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/site/src/utils/random.test.ts b/site/src/utils/random.test.ts index f7632fa2c7e..99efb5ef746 100644 --- a/site/src/utils/random.test.ts +++ b/site/src/utils/random.test.ts @@ -1,14 +1,14 @@ import { describe, expect, it } from "vitest"; -import { generateSessionId } from "#/utils/random"; +import { generateConnectionSessionId } from "#/utils/random"; -describe("generateSessionId", () => { +describe("generateConnectionSessionId", () => { it("is 32 characters long", () => { - const id = generateSessionId(); + const id = generateConnectionSessionId(); expect(id.length).toBe(32); }); it("outputs a hexadecimal string", () => { - const id = generateSessionId(); + const id = generateConnectionSessionId(); const hexRegex = /^[\da-f]+$/; expect(hexRegex.test(id)).toBe(true); }); @@ -16,7 +16,7 @@ describe("generateSessionId", () => { it("generates unique values across calls", () => { const numValues = 1000; const ids = new Set( - Array.from({ length: numValues }, () => generateSessionId()), + Array.from({ length: numValues }, () => generateConnectionSessionId()), ); expect(ids.size).toBe(numValues); }); diff --git a/site/src/utils/random.ts b/site/src/utils/random.ts index 52907b3c92a..39cfed90416 100644 --- a/site/src/utils/random.ts +++ b/site/src/utils/random.ts @@ -24,6 +24,6 @@ export const generateRandomBase64String = (bytes: number): string => { * Generate a 16-byte (32-character) hexadecimal string for identifying * workspace connection sessions. */ -export const generateSessionId = (): string => { +export const generateConnectionSessionId = (): string => { return generateRandomString(16); };