From 917195784348713e900aca02441b92eb03d4ee61 Mon Sep 17 00:00:00 2001 From: devzain15 <195662226+devzain15@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:02:20 +0500 Subject: [PATCH] fix: fall back to a hand-built v4 uuid when crypto.randomUUID is missing Browsers only expose crypto.randomUUID in secure contexts, so sending a message or opening a commit photo on a plain http dev site threw "crypto.randomUUID is not a function". Build the uuid from crypto.getRandomValues when the native function is absent. Closes #620 --- resources/js/lib/chat.ts | 3 +- resources/js/lib/commit-photo.ts | 7 +++-- resources/js/lib/remote-notifications.ts | 3 +- resources/js/lib/utils.ts | 19 +++++++++++++ tests/Unit/RandomUuid.test.ts | 35 ++++++++++++++++++++++++ 5 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 tests/Unit/RandomUuid.test.ts diff --git a/resources/js/lib/chat.ts b/resources/js/lib/chat.ts index c42812940..dec3ac928 100644 --- a/resources/js/lib/chat.ts +++ b/resources/js/lib/chat.ts @@ -29,6 +29,7 @@ import { isRemoteMuted, remoteAlertsEnabled, } from '@/lib/remote-notifications'; +import { randomUuid } from '@/lib/utils'; import type { VoiceDraft } from '@/lib/voice-notes'; import type { ConversationSummary, @@ -1359,7 +1360,7 @@ export function sendMessage(target: ChatTarget, draft: MessageDraft): void { rememberTarget(target); const pending: PendingMessage = { - client_id: crypto.randomUUID(), + client_id: randomUuid(), body: draft.body, reply_to: draft.replyTo, images: draft.images, diff --git a/resources/js/lib/commit-photo.ts b/resources/js/lib/commit-photo.ts index 521dd9e75..e6996700d 100644 --- a/resources/js/lib/commit-photo.ts +++ b/resources/js/lib/commit-photo.ts @@ -2,6 +2,7 @@ import type { PendingVisit } from '@inertiajs/core'; import { router } from '@inertiajs/react'; import CommitController from '@/actions/App/Http/Controllers/CommitController'; import CommitPhotoController from '@/actions/App/Http/Controllers/CommitPhotoController'; +import { randomUuid } from '@/lib/utils'; import type { Commit } from '@/types'; export type CommitPhoto = { @@ -202,8 +203,8 @@ export const commitPhotoUrl = (commit: Commit, index: number): string => export function openCommitPhoto(commit: Commit, index: number): void { const entry: PhotoHistory = { - sessionId: photoSessionId ?? crypto.randomUUID(), - entryId: crypto.randomUUID(), + sessionId: photoSessionId ?? randomUuid(), + entryId: randomUuid(), }; photoSessionId = entry.sessionId; documentPhotoSessions.add(entry.sessionId); @@ -221,7 +222,7 @@ export function openCommitPhoto(commit: Commit, index: number): void { ? source : { sessionId: entry.sessionId, - entryId: crypto.randomUUID(), + entryId: randomUuid(), }, }, '', diff --git a/resources/js/lib/remote-notifications.ts b/resources/js/lib/remote-notifications.ts index b4b08c0dc..f9eb7836d 100644 --- a/resources/js/lib/remote-notifications.ts +++ b/resources/js/lib/remote-notifications.ts @@ -1,4 +1,5 @@ import { readStorage, writeStorage } from '@/lib/storage'; +import { randomUuid } from '@/lib/utils'; import type { ConversationSummary } from '@/types'; export type RemoteAlertChannel = 'notifications' | 'sound'; @@ -96,7 +97,7 @@ export function createRemoteAlerts( const notifications = new Set(); const cooldownKey = `remote-alert-cooldown:${userId}`; const readingKey = `remote-alert-reading:${userId}`; - const tabId = crypto.randomUUID(); + const tabId = randomUuid(); const releaseReading = () => { if (readStorage(readingKey).split('\n')[0] === tabId) { diff --git a/resources/js/lib/utils.ts b/resources/js/lib/utils.ts index 1ba9e0248..2bb468414 100644 --- a/resources/js/lib/utils.ts +++ b/resources/js/lib/utils.ts @@ -20,6 +20,25 @@ export function limitCharacters(value: string, max: number): string { return Array.from(value).slice(0, max).join(''); } +/** + * Browsers only offer `crypto.randomUUID` in secure contexts, so a plain http dev site builds the v4 UUID itself. + */ +export function randomUuid(): string { + if (typeof crypto.randomUUID === 'function') { + return crypto.randomUUID(); + } + + const bytes = crypto.getRandomValues(new Uint8Array(16)); + bytes[6] = (bytes[6] & 0x0f) | 0x40; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + + const hex = Array.from(bytes, (byte) => + byte.toString(16).padStart(2, '0'), + ).join(''); + + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; +} + export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } diff --git a/tests/Unit/RandomUuid.test.ts b/tests/Unit/RandomUuid.test.ts new file mode 100644 index 000000000..22e805c2c --- /dev/null +++ b/tests/Unit/RandomUuid.test.ts @@ -0,0 +1,35 @@ +import assert from 'node:assert/strict'; +import { afterEach, test } from 'node:test'; +import { randomUuid } from '../../resources/js/lib/utils.ts'; + +const v4 = + /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; + +afterEach(() => { + delete (crypto as { randomUUID?: unknown }).randomUUID; +}); + +void test('the browser generates the uuid when it can', () => { + Object.defineProperty(crypto, 'randomUUID', { + value: () => '11111111-2222-4333-8444-555555555555', + configurable: true, + writable: true, + }); + + assert.equal(randomUuid(), '11111111-2222-4333-8444-555555555555'); +}); + +void test('an insecure context still gets a v4 uuid', () => { + Object.defineProperty(crypto, 'randomUUID', { + value: undefined, + configurable: true, + writable: true, + }); + + const first = randomUuid(); + const second = randomUuid(); + + assert.match(first, v4); + assert.match(second, v4); + assert.notEqual(first, second); +});