Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion resources/js/lib/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
7 changes: 4 additions & 3 deletions resources/js/lib/commit-photo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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);
Expand All @@ -221,7 +222,7 @@ export function openCommitPhoto(commit: Commit, index: number): void {
? source
: {
sessionId: entry.sessionId,
entryId: crypto.randomUUID(),
entryId: randomUuid(),
},
},
'',
Expand Down
3 changes: 2 additions & 1 deletion resources/js/lib/remote-notifications.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -96,7 +97,7 @@ export function createRemoteAlerts(
const notifications = new Set<Notification>();
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) {
Expand Down
19 changes: 19 additions & 0 deletions resources/js/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
}
Expand Down
35 changes: 35 additions & 0 deletions tests/Unit/RandomUuid.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});