From b721935fc1838add055c03e994711db7a441c5db Mon Sep 17 00:00:00 2001 From: Andrew Aquino Date: Tue, 11 Aug 2026 09:38:28 -0700 Subject: [PATCH] chore(site): add generateConnectionSessionId helper function (#27935) - adds a `generateConnectionSessionId` helper function which reuses our existing `generateRandomString` logic - adds tests for `generateConnectionSessionId` in utils/random.test.ts (utils/random.ts was previously untested) - renames our existing `generateRandomString` function to be `generateRandomBase64String` additional context in https://github.com/coder/coder/pull/27671#discussion_r3731213459 This PR doesn't change any of our UIs. The new `generateConnectionSessionId` function is a piece of frontend plumbing related to DEVEX-663. I originally implemented #27677 so that web terminal connections to workspaces would be identified by uuids--but according to the RFC for DEVEX-663, those connection session ids should be lowercase hexadecimal strings (not uuids). --- site/src/contexts/useProxyLatency.ts | 4 ++-- site/src/pages/UsersPage/UsersPage.tsx | 4 ++-- site/src/utils/random.test.ts | 23 ++++++++++++++++++++ site/src/utils/random.ts | 30 +++++++++++++++++--------- 4 files changed, 47 insertions(+), 14 deletions(-) create mode 100644 site/src/utils/random.test.ts diff --git a/site/src/contexts/useProxyLatency.ts b/site/src/contexts/useProxyLatency.ts index a3f63f2130..ae1d26edbf 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 2151dc954d..3714425467 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.test.ts b/site/src/utils/random.test.ts new file mode 100644 index 0000000000..99efb5ef74 --- /dev/null +++ b/site/src/utils/random.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; +import { generateConnectionSessionId } from "#/utils/random"; + +describe("generateConnectionSessionId", () => { + it("is 32 characters long", () => { + const id = generateConnectionSessionId(); + expect(id.length).toBe(32); + }); + + it("outputs a hexadecimal string", () => { + const id = generateConnectionSessionId(); + 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 }, () => generateConnectionSessionId()), + ); + expect(ids.size).toBe(numValues); + }); +}); diff --git a/site/src/utils/random.ts b/site/src/utils/random.ts index 6a995b2939..39cfed9041 100644 --- a/site/src/utils/random.ts +++ b/site/src/utils/random.ts @@ -1,3 +1,13 @@ +/** + * Generate a random hexadecimal string from the specified number of bytes. + */ +const generateRandomString = (bytes: number): string => { + const byteArr = crypto.getRandomValues(new Uint8Array(bytes)); + return [...byteArr] + .map((byte) => byte.toString(16).padStart(2, "0")) + .join(""); +}; + /** * Generate a cryptographically secure random string using the specified number * of bytes then encode with base64. @@ -6,14 +16,14 @@ * equal the number of randomly generated bytes. * @see */ -export const generateRandomString = (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("")); +export const generateRandomBase64String = (bytes: number): string => { + return btoa(generateRandomString(bytes)); +}; + +/** + * Generate a 16-byte (32-character) hexadecimal string for identifying + * workspace connection sessions. + */ +export const generateConnectionSessionId = (): string => { + return generateRandomString(16); };