fix(core): consolidate ID generation to prevent HTTP self-hosted crashes (#3977)

* fix(core): consolidate ID generation to prevent HTTP self-hosted crashes

crypto.randomUUID() requires a secure context (HTTPS) in browsers,
causing white-screen crashes on self-hosted HTTP deployments. This
replaces all direct usage of crypto.randomUUID(), nanoid, and the uuid
package with a central utility that falls back to crypto.getRandomValues()
which works in all contexts.

- Add generateId(), generateShortId(), isValidUuid() in @/lib/core/utils/uuid
- Replace crypto.randomUUID() imports across ~220 server + client files
- Replace nanoid imports with generateShortId()
- Replace uuid package validate with isValidUuid()
- Remove nanoid dependency from apps/sim and packages/testing
- Remove browser polyfill script from layout.tsx
- Update test mocks to target @/lib/core/utils/uuid
- Update CLAUDE.md, AGENTS.md, cursor rules, claude rules

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* update bunlock

* fix(core): remove UUID_REGEX shim, use isValidUuid directly

* fix(core): remove deprecated uuid mock helpers that use vi.doMock

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Waleed
2026-04-05 11:28:54 -07:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 235f0748ca
commit a680cec78f
321 changed files with 1133 additions and 913 deletions
+18
View File
@@ -0,0 +1,18 @@
const URL_SAFE_ALPHABET = 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict'
/**
* Generates a short, URL-safe random ID for test fixtures.
*
* Uses `crypto.getRandomValues()` instead of `crypto.randomUUID()` for
* consistency with the app-level `generateShortId` utility.
*/
export function shortId(size = 8): string {
const bytes = new Uint8Array(size)
crypto.getRandomValues(bytes)
let id = ''
for (let i = 0; i < size; i++) {
id += URL_SAFE_ALPHABET[bytes[i] & 63]
}
return id
}
@@ -1,4 +1,4 @@
import { nanoid } from 'nanoid'
import { shortId } from './id'
/**
* Permission types in order of access level (highest to lowest).
@@ -39,10 +39,10 @@ export interface PermissionFactoryOptions {
*/
export function createPermission(options: PermissionFactoryOptions = {}): Permission {
return {
id: options.id ?? nanoid(8),
userId: options.userId ?? `user-${nanoid(6)}`,
id: options.id ?? shortId(8),
userId: options.userId ?? `user-${shortId(6)}`,
entityType: options.entityType ?? 'workspace',
entityId: options.entityId ?? `ws-${nanoid(6)}`,
entityId: options.entityId ?? `ws-${shortId(6)}`,
permissionType: options.permissionType ?? 'read',
createdAt: options.createdAt ?? new Date(),
}
@@ -127,8 +127,8 @@ export interface WorkspaceRecordFactoryOptions {
export function createWorkspaceRecord(
options: WorkspaceRecordFactoryOptions = {}
): WorkspaceRecord {
const id = options.id ?? `ws-${nanoid(6)}`
const ownerId = options.ownerId ?? `user-${nanoid(6)}`
const id = options.id ?? `ws-${shortId(6)}`
const ownerId = options.ownerId ?? `user-${shortId(6)}`
return {
id,
name: options.name ?? `Workspace ${id}`,
@@ -170,11 +170,11 @@ export interface WorkflowRecordFactoryOptions {
* Creates a mock workflow database record.
*/
export function createWorkflowRecord(options: WorkflowRecordFactoryOptions = {}): WorkflowRecord {
const id = options.id ?? `wf-${nanoid(6)}`
const id = options.id ?? `wf-${shortId(6)}`
return {
id,
name: options.name ?? `Workflow ${id}`,
userId: options.userId ?? `user-${nanoid(6)}`,
userId: options.userId ?? `user-${shortId(6)}`,
workspaceId: options.workspaceId ?? null,
state: options.state ?? '{}',
isDeployed: options.isDeployed ?? false,
@@ -209,7 +209,7 @@ export interface SessionFactoryOptions {
* Creates a mock session object.
*/
export function createSession(options: SessionFactoryOptions = {}): MockSession {
const userId = options.userId ?? `user-${nanoid(6)}`
const userId = options.userId ?? `user-${shortId(6)}`
return {
user: {
id: userId,
@@ -327,7 +327,7 @@ export interface ApiKeyTestData {
* Creates test API key data.
*/
export function createLegacyApiKey(): { key: string; prefix: string } {
const random = nanoid(24)
const random = shortId(24)
return {
key: `sim_${random}`,
prefix: 'sim_',
@@ -338,7 +338,7 @@ export function createLegacyApiKey(): { key: string; prefix: string } {
* Creates test encrypted format API key data.
*/
export function createEncryptedApiKey(): { key: string; prefix: string } {
const random = nanoid(24)
const random = shortId(24)
return {
key: `sk-sim-${random}`,
prefix: 'sk-sim-',
@@ -1,4 +1,4 @@
import { nanoid } from 'nanoid'
import { shortId } from './id'
/* eslint-disable @typescript-eslint/no-explicit-any */
@@ -137,7 +137,12 @@ interface OperationEntryOptions {
* Creates a mock batch-add-blocks operation entry.
*/
export function createAddBlockEntry(blockId: string, options: OperationEntryOptions = {}): any {
const { id = nanoid(8), workflowId = 'wf-1', userId = 'user-1', createdAt = Date.now() } = options
const {
id = shortId(8),
workflowId = 'wf-1',
userId = 'user-1',
createdAt = Date.now(),
} = options
const timestamp = Date.now()
const mockBlockSnapshot = {
@@ -151,7 +156,7 @@ export function createAddBlockEntry(blockId: string, options: OperationEntryOpti
id,
createdAt,
operation: {
id: nanoid(8),
id: shortId(8),
type: 'batch-add-blocks',
timestamp,
workflowId,
@@ -163,7 +168,7 @@ export function createAddBlockEntry(blockId: string, options: OperationEntryOpti
},
},
inverse: {
id: nanoid(8),
id: shortId(8),
type: 'batch-remove-blocks',
timestamp,
workflowId,
@@ -185,7 +190,12 @@ export function createRemoveBlockEntry(
blockSnapshot: any = null,
options: OperationEntryOptions = {}
): any {
const { id = nanoid(8), workflowId = 'wf-1', userId = 'user-1', createdAt = Date.now() } = options
const {
id = shortId(8),
workflowId = 'wf-1',
userId = 'user-1',
createdAt = Date.now(),
} = options
const timestamp = Date.now()
const snapshotToUse = blockSnapshot || {
@@ -199,7 +209,7 @@ export function createRemoveBlockEntry(
id,
createdAt,
operation: {
id: nanoid(8),
id: shortId(8),
type: 'batch-remove-blocks',
timestamp,
workflowId,
@@ -211,7 +221,7 @@ export function createRemoveBlockEntry(
},
},
inverse: {
id: nanoid(8),
id: shortId(8),
type: 'batch-add-blocks',
timestamp,
workflowId,
@@ -233,7 +243,12 @@ export function createAddEdgeEntry(
edgeSnapshot: any = null,
options: OperationEntryOptions = {}
): any {
const { id = nanoid(8), workflowId = 'wf-1', userId = 'user-1', createdAt = Date.now() } = options
const {
id = shortId(8),
workflowId = 'wf-1',
userId = 'user-1',
createdAt = Date.now(),
} = options
const timestamp = Date.now()
const snapshot = edgeSnapshot || { id: edgeId, source: 'block-1', target: 'block-2' }
@@ -242,7 +257,7 @@ export function createAddEdgeEntry(
id,
createdAt,
operation: {
id: nanoid(8),
id: shortId(8),
type: 'batch-add-edges',
timestamp,
workflowId,
@@ -250,7 +265,7 @@ export function createAddEdgeEntry(
data: { edgeSnapshots: [snapshot] },
},
inverse: {
id: nanoid(8),
id: shortId(8),
type: 'batch-remove-edges',
timestamp,
workflowId,
@@ -267,14 +282,19 @@ export function createBatchRemoveEdgesEntry(
edgeSnapshots: any[],
options: OperationEntryOptions = {}
): any {
const { id = nanoid(8), workflowId = 'wf-1', userId = 'user-1', createdAt = Date.now() } = options
const {
id = shortId(8),
workflowId = 'wf-1',
userId = 'user-1',
createdAt = Date.now(),
} = options
const timestamp = Date.now()
return {
id,
createdAt,
operation: {
id: nanoid(8),
id: shortId(8),
type: 'batch-remove-edges',
timestamp,
workflowId,
@@ -282,7 +302,7 @@ export function createBatchRemoveEdgesEntry(
data: { edgeSnapshots },
},
inverse: {
id: nanoid(8),
id: shortId(8),
type: 'batch-add-edges',
timestamp,
workflowId,
@@ -302,7 +322,7 @@ interface MoveBlockOptions extends OperationEntryOptions {
*/
export function createMoveBlockEntry(blockId: string, options: MoveBlockOptions = {}): any {
const {
id = nanoid(8),
id = shortId(8),
workflowId = 'wf-1',
userId = 'user-1',
createdAt = Date.now(),
@@ -315,7 +335,7 @@ export function createMoveBlockEntry(blockId: string, options: MoveBlockOptions
id,
createdAt,
operation: {
id: nanoid(8),
id: shortId(8),
type: 'batch-move-blocks',
timestamp,
workflowId,
@@ -323,7 +343,7 @@ export function createMoveBlockEntry(blockId: string, options: MoveBlockOptions
data: { moves: [{ blockId, before, after }] },
},
inverse: {
id: nanoid(8),
id: shortId(8),
type: 'batch-move-blocks',
timestamp,
workflowId,
@@ -346,7 +366,7 @@ export function createUpdateParentEntry(
} = {}
): any {
const {
id = nanoid(8),
id = shortId(8),
workflowId = 'wf-1',
userId = 'user-1',
createdAt = Date.now(),
@@ -361,7 +381,7 @@ export function createUpdateParentEntry(
id,
createdAt,
operation: {
id: nanoid(8),
id: shortId(8),
type: 'update-parent',
timestamp,
workflowId,
@@ -369,7 +389,7 @@ export function createUpdateParentEntry(
data: { blockId, oldParentId, newParentId, oldPosition, newPosition },
},
inverse: {
id: nanoid(8),
id: shortId(8),
type: 'update-parent',
timestamp,
workflowId,
@@ -401,7 +421,7 @@ interface BatchUpdateParentOptions extends OperationEntryOptions {
*/
export function createBatchUpdateParentEntry(options: BatchUpdateParentOptions = {}): any {
const {
id = nanoid(8),
id = shortId(8),
workflowId = 'wf-1',
userId = 'user-1',
createdAt = Date.now(),
@@ -430,7 +450,7 @@ export function createBatchUpdateParentEntry(options: BatchUpdateParentOptions =
id,
createdAt,
operation: {
id: nanoid(8),
id: shortId(8),
type: 'batch-update-parent',
timestamp,
workflowId,
@@ -438,7 +458,7 @@ export function createBatchUpdateParentEntry(options: BatchUpdateParentOptions =
data: { updates: processedUpdates },
},
inverse: {
id: nanoid(8),
id: shortId(8),
type: 'batch-update-parent',
timestamp,
workflowId,
-2
View File
@@ -73,11 +73,9 @@ export {
mockAuth,
mockCommonSchemas,
mockConsoleLogger,
mockCryptoUuid,
mockDrizzleOrm,
mockHybridAuth,
mockKnowledgeSchemas,
mockUuid,
requestUtilsMock,
setupCommonApiMocks,
setupGlobalFetchMock,
-2
View File
@@ -82,5 +82,3 @@ export {
export { clearStorageMocks, createMockStorage, setupGlobalStorageMocks } from './storage.mock'
// Telemetry mocks
export { telemetryMock } from './telemetry.mock'
// UUID mocks
export { mockCryptoUuid, mockUuid } from './uuid.mock'
+4 -38
View File
@@ -1,40 +1,6 @@
/**
* Mock UUID utilities for testing
* UUID mock utilities — intentionally empty.
*
* All test files should mock `@/lib/core/utils/uuid` directly using
* `vi.hoisted()` + `vi.mock()` per project testing rules.
*/
import { vi } from 'vitest'
/**
* Mock UUID v4 generation for consistent test results.
* Uses vi.doMock to mock the uuid module.
*
* @param mockValue - The UUID value to return (defaults to 'test-uuid')
*
* @example
* ```ts
* mockUuid('my-test-uuid')
* // Now uuid.v4() will return 'my-test-uuid'
* ```
*/
export function mockUuid(mockValue = 'test-uuid') {
vi.doMock('uuid', () => ({
v4: vi.fn().mockReturnValue(mockValue),
}))
}
/**
* Mock crypto.randomUUID for tests.
* Uses vi.stubGlobal to replace the global crypto object.
*
* @param mockValue - The UUID value to return (defaults to 'mock-uuid-1234-5678')
*
* @example
* ```ts
* mockCryptoUuid('custom-uuid')
* // Now crypto.randomUUID() will return 'custom-uuid'
* ```
*/
export function mockCryptoUuid(mockValue = 'mock-uuid-1234-5678') {
vi.stubGlobal('crypto', {
randomUUID: vi.fn().mockReturnValue(mockValue),
})
}