mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
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:
co-authored by
Claude Opus 4.6
parent
235f0748ca
commit
a680cec78f
@@ -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,
|
||||
|
||||
@@ -73,11 +73,9 @@ export {
|
||||
mockAuth,
|
||||
mockCommonSchemas,
|
||||
mockConsoleLogger,
|
||||
mockCryptoUuid,
|
||||
mockDrizzleOrm,
|
||||
mockHybridAuth,
|
||||
mockKnowledgeSchemas,
|
||||
mockUuid,
|
||||
requestUtilsMock,
|
||||
setupCommonApiMocks,
|
||||
setupGlobalFetchMock,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user