From 7ca44f050cf0be679fbf69377c23af7ce9a576f2 Mon Sep 17 00:00:00 2001 From: waleed Date: Fri, 15 May 2026 16:14:46 -0700 Subject: [PATCH 1/2] chore(utils): migrate to shared random/ID utilities and add enforcement linting --- .cursor/rules/global.mdc | 12 ++ 0 | 0 AGENTS.md | 1 + CLAUDE.md | 1 + apps/realtime/src/database/operations.ts | 3 +- apps/realtime/src/index.test.ts | 5 +- .../api/chat/[identifier]/otp/route.test.ts | 1 + .../app/api/chat/[identifier]/otp/route.ts | 4 +- apps/sim/app/api/files/parse/route.ts | 3 +- apps/sim/app/api/tools/file/manage/route.ts | 3 +- .../api/tools/google_drive/upload/route.ts | 3 +- .../components/line-chart/line-chart.tsx | 5 +- .../components/secrets/secrets-manager.tsx | 13 +- .../components/file-upload/file-upload.tsx | 9 +- .../hooks/use-workflow-execution.ts | 5 +- .../workspace-notification-delivery.ts | 3 +- apps/sim/instrumentation-client.ts | 3 +- apps/sim/lib/api-key/auth.test.ts | 41 +++--- apps/sim/lib/api-key/crypto.test.ts | 4 +- apps/sim/lib/copilot/request/otel.ts | 6 +- apps/sim/lib/copilot/vfs/file-reader.test.ts | 5 +- apps/sim/lib/core/config/redis.ts | 3 +- apps/sim/lib/core/security/encryption.ts | 9 +- apps/sim/lib/core/utils.test.ts | 23 ++-- apps/sim/lib/core/utils/restore-name.ts | 4 +- .../sim/lib/data-drains/destinations/utils.ts | 3 +- apps/sim/lib/execution/event-buffer.ts | 3 +- apps/sim/lib/execution/isolated-vm.ts | 3 +- .../lib/knowledge/connectors/sync-engine.ts | 4 +- .../knowledge/documents/document-processor.ts | 6 +- apps/sim/lib/knowledge/documents/utils.ts | 3 +- apps/sim/lib/messaging/email/unsubscribe.ts | 4 +- apps/sim/lib/table/constants.ts | 11 +- .../lib/uploads/contexts/execution/utils.ts | 3 +- .../workspace/workspace-file-manager.ts | 4 +- apps/sim/lib/uploads/core/storage-service.ts | 4 +- .../lib/webhooks/providers/twilio-voice.ts | 14 +- apps/sim/lib/workflows/colors.ts | 4 +- apps/sim/lib/workspaces/colors.ts | 4 +- apps/sim/lib/workspaces/naming.ts | 3 +- apps/sim/providers/anthropic/utils.ts | 3 +- apps/sim/providers/bedrock/utils.ts | 3 +- apps/sim/stores/workflows/registry/store.ts | 3 +- apps/sim/stores/workflows/registry/utils.ts | 6 +- apps/sim/tools/gmail/utils.ts | 3 +- apps/sim/tools/google_slides/add_image.ts | 3 +- apps/sim/tools/google_slides/add_slide.ts | 3 +- apps/sim/tools/google_slides/create_shape.ts | 3 +- apps/sim/tools/google_slides/create_table.ts | 3 +- apps/sim/tools/index.ts | 3 +- apps/sim/tools/mistral/parser.ts | 3 +- biome.json | 18 ++- bun.lock | 3 + endTime | 0 package.json | 1 + .../scripts/migrate-block-api-keys-to-byok.ts | 5 +- packages/db/scripts/seed-stress-test-users.ts | 13 +- packages/security/package.json | 4 +- packages/security/src/encryption.test.ts | 4 +- packages/security/src/encryption.ts | 5 +- packages/security/src/tokens.ts | 4 +- .../testing/src/builders/execution.builder.ts | 3 +- .../testing/src/builders/workflow.builder.ts | 3 +- .../testing/src/factories/block.factory.ts | 3 +- packages/testing/src/factories/dag.factory.ts | 3 +- .../testing/src/factories/edge.factory.ts | 4 +- .../src/factories/execution.factory.ts | 3 +- packages/testing/src/factories/id.ts | 14 +- .../src/factories/tool-responses.factory.ts | 4 +- .../testing/src/factories/user.factory.ts | 11 +- packages/testing/src/mocks/socket.mock.ts | 3 +- .../src/mocks/terminal-console.mock.ts | 3 +- packages/ts-sdk/src/index.ts | 12 +- packages/utils/package.json | 4 + packages/utils/src/id.ts | 39 ++---- packages/utils/src/index.ts | 9 ++ packages/utils/src/random.test.ts | 47 +++++++ packages/utils/src/random.ts | 119 +++++++++++++++++ scripts/check-utils-enforcement.ts | 123 ++++++++++++++++++ 79 files changed, 555 insertions(+), 187 deletions(-) create mode 100644 0 create mode 100644 endTime create mode 100644 packages/utils/src/random.test.ts create mode 100644 packages/utils/src/random.ts create mode 100644 scripts/check-utils-enforcement.ts diff --git a/.cursor/rules/global.mdc b/.cursor/rules/global.mdc index 78f0cb106b3..fe2c5eacc81 100644 --- a/.cursor/rules/global.mdc +++ b/.cursor/rules/global.mdc @@ -37,6 +37,18 @@ const shortId = generateShortId() const tiny = generateShortId(8) ``` +## Randomness +Never use `Math.random()`, `crypto.randomBytes()`, or one-off random helpers. Use `@sim/utils/random`: + +- `generateRandomBytes(size)` — secure bytes +- `generateRandomHex(byteLength)` — lowercase hex for salts, object keys, and trace/span IDs +- `generateRandomString(size?, alphabet?)` — URL-safe or custom-alphabet suffixes +- `randomFloat()` — sampling and jitter in `[0, 1)` +- `randomInt(maxExclusive)` — unbiased integer ranges +- `randomItem(items)` — unbiased array selection + +Use these for IDs, suffixes, MIME boundaries, storage keys, salts, tokens, retry jitter, telemetry sampling, and random UI palette/name selection. Do not add a `Math.random()` fallback; `crypto.getRandomValues()` is the standard CSPRNG primitive and works in insecure browser contexts. Only standalone published packages that cannot depend on `@sim/utils` may use Web Crypto directly, with a comment explaining why. + ## Common Utilities Use shared helpers from `@sim/utils` instead of writing inline implementations: diff --git a/0 b/0 new file mode 100644 index 00000000000..e69de29bb2d diff --git a/AGENTS.md b/AGENTS.md index cfa04710cf1..ab86caf0399 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,6 +9,7 @@ You are a professional software engineer. All code must follow best practices: a - **Comments**: Use TSDoc for documentation. No `====` separators. No non-TSDoc comments - **Styling**: Never update global styles. Keep all styling local to components - **ID Generation**: Never use `crypto.randomUUID()`, `nanoid`, or `uuid` package. Use `generateId()` (UUID v4) or `generateShortId()` (compact) from `@sim/utils/id` +- **Randomness**: Never use `Math.random()`, `crypto.randomBytes()`, or ad-hoc random helpers. Use `@sim/utils/random`: `generateRandomBytes`, `generateRandomHex`, `generateRandomString`, `randomFloat`, `randomInt`, or `randomItem`. Use `randomInt` / `randomItem` for unbiased selection, jitter, and sampling; use hex/string helpers for suffixes, object keys, boundaries, salts, and tokens. Do not add a `Math.random()` fallback — the shared util uses `crypto.getRandomValues()`, which is the standard CSPRNG primitive that also works in non-secure browser contexts. Only standalone published packages that cannot depend on `@sim/utils` may use Web Crypto directly, with a comment explaining why. - **Package Manager**: Use `bun` and `bunx`, not `npm` and `npx` ## Architecture diff --git a/CLAUDE.md b/CLAUDE.md index 9807d221ca6..f16db39a9a3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,6 +10,7 @@ You are a professional software engineer. All code must follow best practices: a - **Comments**: Use TSDoc for documentation. No `====` separators. No non-TSDoc comments - **Styling**: Never update global styles. Keep all styling local to components - **ID Generation**: Never use `crypto.randomUUID()`, `nanoid`, or `uuid` package. Use `generateId()` (UUID v4) or `generateShortId()` (compact) from `@sim/utils/id` +- **Randomness**: Never use `Math.random()`, `crypto.randomBytes()`, or ad-hoc random helpers. Use `@sim/utils/random`: `generateRandomBytes`, `generateRandomHex`, `generateRandomString`, `randomFloat`, `randomInt`, or `randomItem`. Use `randomInt` / `randomItem` for unbiased selection, jitter, and sampling; use hex/string helpers for suffixes, object keys, boundaries, salts, and tokens. Do not add a `Math.random()` fallback — the shared util uses `crypto.getRandomValues()`, which is the standard CSPRNG primitive that also works in non-secure browser contexts. Only standalone published packages that cannot depend on `@sim/utils` may use Web Crypto directly, with a comment explaining why. - **Common Utilities**: Use shared helpers from `@sim/utils` instead of inline implementations. `sleep(ms)` from `@sim/utils/helpers` for delays, `toError(e)` from `@sim/utils/errors` to normalize caught values. - **Package Manager**: Use `bun` and `bunx`, not `npm` and `npx` diff --git a/apps/realtime/src/database/operations.ts b/apps/realtime/src/database/operations.ts index 4904daccb8f..8e301acf9bc 100644 --- a/apps/realtime/src/database/operations.ts +++ b/apps/realtime/src/database/operations.ts @@ -13,6 +13,7 @@ import { VARIABLE_OPERATIONS, WORKFLOW_OPERATIONS, } from '@sim/realtime-protocol/constants' +import { randomFloat } from '@sim/utils/random' import { getActiveWorkflowContext } from '@sim/workflow-authz' import { loadWorkflowFromNormalizedTablesRaw } from '@sim/workflow-persistence/load' import { mergeSubBlockValues } from '@sim/workflow-persistence/subblocks' @@ -204,7 +205,7 @@ export async function persistWorkflowOperation(workflowId: string, operation: an throw new Error(`Workflow ${workflowId} is archived or unavailable`) } - if (op === BLOCK_OPERATIONS.UPDATE_POSITION && Math.random() < 0.01) { + if (op === BLOCK_OPERATIONS.UPDATE_POSITION && randomFloat() < 0.01) { logger.debug('Socket DB operation sample:', { operation: op, target, diff --git a/apps/realtime/src/index.test.ts b/apps/realtime/src/index.test.ts index c00592e0d6d..0edad06324b 100644 --- a/apps/realtime/src/index.test.ts +++ b/apps/realtime/src/index.test.ts @@ -5,6 +5,7 @@ */ import { createServer, request as httpRequest } from 'http' import { createMockLogger } from '@sim/testing' +import { randomInt } from '@sim/utils/random' import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' import { createSocketIOServer } from '@/config/socket' import { MemoryRoomManager } from '@/rooms' @@ -95,7 +96,7 @@ describe('Socket Server Index Integration', () => { }) beforeEach(async () => { - PORT = 3333 + Math.floor(Math.random() * 1000) + PORT = 3333 + randomInt(1000) httpServer = createServer() @@ -120,7 +121,7 @@ describe('Socket Server Index Integration', () => { httpServer.on('error', (err: any) => { clearTimeout(timeout) if (err.code === 'EADDRINUSE') { - PORT = 3333 + Math.floor(Math.random() * 1000) + PORT = 3333 + randomInt(1000) httpServer.close(() => { httpServer.listen(PORT, '0.0.0.0', () => { resolve() diff --git a/apps/sim/app/api/chat/[identifier]/otp/route.test.ts b/apps/sim/app/api/chat/[identifier]/otp/route.test.ts index 547a164b069..956e5fc12ef 100644 --- a/apps/sim/app/api/chat/[identifier]/otp/route.test.ts +++ b/apps/sim/app/api/chat/[identifier]/otp/route.test.ts @@ -213,6 +213,7 @@ describe('Chat OTP API Route', () => { vi.stubGlobal('crypto', { ...crypto, + getRandomValues: crypto.getRandomValues.bind(crypto), randomUUID: vi.fn().mockReturnValue('test-uuid-1234'), }) diff --git a/apps/sim/app/api/chat/[identifier]/otp/route.ts b/apps/sim/app/api/chat/[identifier]/otp/route.ts index b2e129b5fa8..06e966ad15d 100644 --- a/apps/sim/app/api/chat/[identifier]/otp/route.ts +++ b/apps/sim/app/api/chat/[identifier]/otp/route.ts @@ -1,8 +1,8 @@ -import { randomInt } from 'crypto' import { db } from '@sim/db' import { chat, verification } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' +import { randomInt } from '@sim/utils/random' import { and, eq, gt, isNull } from 'drizzle-orm' import type { NextRequest } from 'next/server' import { renderOTPEmail } from '@/components/emails' @@ -36,7 +36,7 @@ const OTP_EMAIL_RATE_LIMIT: TokenBucketConfig = { } function generateOTP(): string { - return randomInt(100000, 1000000).toString() + return (randomInt(900000) + 100000).toString() } const OTP_EXPIRY = 15 * 60 // 15 minutes diff --git a/apps/sim/app/api/files/parse/route.ts b/apps/sim/app/api/files/parse/route.ts index 4bcd7d01914..cda61331fa9 100644 --- a/apps/sim/app/api/files/parse/route.ts +++ b/apps/sim/app/api/files/parse/route.ts @@ -3,6 +3,7 @@ import { createHash } from 'crypto' import fsPromises, { readFile } from 'fs/promises' import path from 'path' import { createLogger } from '@sim/logger' +import { generateRandomString, LOWERCASE_ALPHANUMERIC_ALPHABET } from '@sim/utils/random' import binaryExtensionsList from 'binary-extensions' import { type NextRequest, NextResponse } from 'next/server' import { fileParseContract } from '@/lib/api/contracts/storage-transfer' @@ -552,7 +553,7 @@ async function handleCloudFile( // If file is already from execution context, create UserFile reference without re-uploading if (context === 'execution') { userFile = { - id: `file_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`, + id: `file_${Date.now()}_${generateRandomString(7, LOWERCASE_ALPHANUMERIC_ALPHABET)}`, name: filename, url: normalizedFilePath, size: fileBuffer.length, diff --git a/apps/sim/app/api/tools/file/manage/route.ts b/apps/sim/app/api/tools/file/manage/route.ts index cfb53f06430..08963c2016d 100644 --- a/apps/sim/app/api/tools/file/manage/route.ts +++ b/apps/sim/app/api/tools/file/manage/route.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { generateRandomString, LOWERCASE_ALPHANUMERIC_ALPHABET } from '@sim/utils/random' import { type NextRequest, NextResponse } from 'next/server' import { fileManageContract } from '@/lib/api/contracts/tools/file' import { parseRequest } from '@/lib/api/server' @@ -314,7 +315,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { } const lockKey = `file-append:${workspaceId}:${existing.id}` - const lockValue = `${Date.now()}-${Math.random().toString(36).slice(2)}` + const lockValue = `${Date.now()}-${generateRandomString(10, LOWERCASE_ALPHANUMERIC_ALPHABET)}` const acquired = await acquireLock(lockKey, lockValue, 30) if (!acquired) { return NextResponse.json( diff --git a/apps/sim/app/api/tools/google_drive/upload/route.ts b/apps/sim/app/api/tools/google_drive/upload/route.ts index 797d7cf11d2..5c5710e2f81 100644 --- a/apps/sim/app/api/tools/google_drive/upload/route.ts +++ b/apps/sim/app/api/tools/google_drive/upload/route.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { generateRandomString, LOWERCASE_ALPHANUMERIC_ALPHABET } from '@sim/utils/random' import { type NextRequest, NextResponse } from 'next/server' import { googleDriveUploadContract } from '@/lib/api/contracts/tools/google' import { parseRequest } from '@/lib/api/server' @@ -170,7 +171,7 @@ export const POST = withRouteHandler(async (request: NextRequest) => { metadata.parents = [validatedData.folderId.trim()] } - const boundary = `boundary_${Date.now()}_${Math.random().toString(36).substring(7)}` + const boundary = `boundary_${Date.now()}_${generateRandomString(7, LOWERCASE_ALPHANUMERIC_ALPHABET)}` const multipartBody = buildMultipartBody(metadata, fileBuffer, uploadMimeType, boundary) diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/line-chart/line-chart.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/line-chart/line-chart.tsx index 92a184b60ef..e4f96fcbd96 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/line-chart/line-chart.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/components/line-chart/line-chart.tsx @@ -1,4 +1,5 @@ import { memo, useEffect, useMemo, useRef, useState } from 'react' +import { generateRandomString, LOWERCASE_ALPHANUMERIC_ALPHABET } from '@sim/utils/random' import { Button } from '@/components/emcn' import { cn } from '@/lib/core/utils/cn' import { formatDate, formatLatency } from '@/app/workspace/[workspaceId]/logs/utils' @@ -30,7 +31,9 @@ function LineChartComponent({ series?: LineChartMultiSeries[] }) { const containerRef = useRef(null) - const uniqueId = useRef(`chart-${Math.random().toString(36).substring(2, 9)}`).current + const uniqueId = useRef( + `chart-${generateRandomString(7, LOWERCASE_ALPHANUMERIC_ALPHABET)}` + ).current const [containerWidth, setContainerWidth] = useState(null) const width = containerWidth ?? 0 const height = 166 diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/secrets-manager.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/secrets-manager.tsx index 95529a6b53f..be2c58f256f 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/secrets-manager.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/secrets-manager.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' +import { generateRandomString } from '@sim/utils/random' import { useQueryClient } from '@tanstack/react-query' import { Check, Clipboard, Key, Search } from 'lucide-react' import { useParams, useRouter } from 'next/navigation' @@ -217,7 +218,7 @@ function WorkspaceVariableRow({ onPendingKeyChange(e.target.value) }} onBlur={() => onRenameEnd(envKey, value)} - name={`workspace_env_key_${envKey}_${Math.random()}`} + name={`workspace_env_key_${envKey}_${generateRandomString(8)}`} autoComplete='off' autoCapitalize='off' spellCheck='false' @@ -242,7 +243,7 @@ function WorkspaceVariableRow({ onBlur={() => { if (canEdit) setValueFocused(false) }} - name={`workspace_env_value_${envKey}_${Math.random()}`} + name={`workspace_env_value_${envKey}_${generateRandomString(8)}`} autoComplete='off' autoCorrect='off' autoCapitalize='off' @@ -298,7 +299,7 @@ function NewWorkspaceVariableRow({ onChange={(e) => onUpdate(index, 'key', e.target.value)} onPaste={onPaste ? (e) => onPaste(e, index) : undefined} placeholder='API_KEY' - name={`new_workspace_key_${envVar.id || index}_${Math.random()}`} + name={`new_workspace_key_${envVar.id || index}_${generateRandomString(8)}`} autoComplete='off' autoCapitalize='off' spellCheck='false' @@ -314,7 +315,7 @@ function NewWorkspaceVariableRow({ onPaste={onPaste ? (e) => onPaste(e, index) : undefined} placeholder='Enter value' type={valueFocused ? 'text' : 'password'} - name={`new_workspace_value_${envVar.id || index}_${Math.random()}`} + name={`new_workspace_value_${envVar.id || index}_${generateRandomString(8)}`} autoComplete='off' autoCapitalize='off' spellCheck='false' @@ -1088,7 +1089,7 @@ export function SecretsManager() { onChange={(e) => updateEnvVar(originalIndex, 'key', e.target.value)} onPaste={(e) => handlePaste(e, originalIndex)} placeholder='API_KEY' - name={`env_variable_name_${envVar.id || originalIndex}_${Math.random()}`} + name={`env_variable_name_${envVar.id || originalIndex}_${generateRandomString(8)}`} autoComplete='off' autoCapitalize='off' spellCheck='false' @@ -1116,7 +1117,7 @@ export function SecretsManager() { onBlur={() => setFocusedValueIndex(null)} onPaste={(e) => handlePaste(e, originalIndex)} placeholder={isConflict ? 'Workspace override active' : 'Enter value'} - name={`env_variable_value_${envVar.id || originalIndex}_${Math.random()}`} + name={`env_variable_value_${envVar.id || originalIndex}_${generateRandomString(8)}`} autoComplete='off' autoCapitalize='off' spellCheck='false' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-upload/file-upload.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-upload/file-upload.tsx index 0cc64dfbfa8..e9160774147 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-upload/file-upload.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/file-upload/file-upload.tsx @@ -2,6 +2,11 @@ import { useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' +import { + generateRandomString, + LOWERCASE_ALPHANUMERIC_ALPHABET, + randomFloat, +} from '@sim/utils/random' import { useQueryClient } from '@tanstack/react-query' import { X } from 'lucide-react' import { useParams } from 'next/navigation' @@ -293,7 +298,7 @@ export function FileUpload({ } const uploading = validFiles.map((file) => ({ - id: `upload-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`, + id: `upload-${Date.now()}-${generateRandomString(7, LOWERCASE_ALPHANUMERIC_ALPHABET)}`, name: file.name, size: file.size, })) @@ -308,7 +313,7 @@ export function FileUpload({ progressInterval = setInterval(() => { setUploadProgress((prev) => { - const newProgress = prev + Math.random() * 10 + const newProgress = prev + randomFloat() * 10 return newProgress > 90 ? 90 : newProgress }) }, 200) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts index 604d0edd9c4..459ac76d94d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution.ts @@ -3,6 +3,7 @@ import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { sleep } from '@sim/utils/helpers' import { generateId } from '@sim/utils/id' +import { generateRandomString, LOWERCASE_ALPHANUMERIC_ALPHABET } from '@sim/utils/random' import { useQueryClient } from '@tanstack/react-query' import { useParams } from 'next/navigation' import { useShallow } from 'zustand/react/shallow' @@ -518,7 +519,7 @@ export function useWorkflowExecution() { presignedEndpoint, }) uploadedFiles.push({ - id: `file_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`, + id: `file_${Date.now()}_${generateRandomString(7, LOWERCASE_ALPHANUMERIC_ALPHABET)}`, name: fileData.file.name, url: result.path, size: fileData.file.size, @@ -560,7 +561,7 @@ export function useWorkflowExecution() { const processUploadResult = (r: any) => ({ id: r.id || - `file_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`, + `file_${Date.now()}_${generateRandomString(7, LOWERCASE_ALPHANUMERIC_ALPHABET)}`, name: r.name, url: r.url, size: r.size, diff --git a/apps/sim/background/workspace-notification-delivery.ts b/apps/sim/background/workspace-notification-delivery.ts index fdaae3257d7..c86c5cd68b5 100644 --- a/apps/sim/background/workspace-notification-delivery.ts +++ b/apps/sim/background/workspace-notification-delivery.ts @@ -9,6 +9,7 @@ import { hmacSha256Hex } from '@sim/security/hmac' import { toError } from '@sim/utils/errors' import { formatDuration } from '@sim/utils/formatting' import { generateId } from '@sim/utils/id' +import { randomFloat } from '@sim/utils/random' import { getActiveWorkflowContext } from '@sim/workflow-authz' import { task } from '@trigger.dev/sdk' import { and, eq, isNull, lte, or, sql } from 'drizzle-orm' @@ -34,7 +35,7 @@ const logger = createLogger('WorkspaceNotificationDelivery') const MAX_ATTEMPTS = 5 const RETRY_DELAYS = [5 * 1000, 15 * 1000, 60 * 1000, 3 * 60 * 1000, 10 * 60 * 1000] function getRetryDelayWithJitter(baseDelay: number): number { - const jitter = Math.random() * 0.1 * baseDelay + const jitter = randomFloat() * 0.1 * baseDelay return Math.floor(baseDelay + jitter) } diff --git a/apps/sim/instrumentation-client.ts b/apps/sim/instrumentation-client.ts index fb6cf002ec5..4164f5e225f 100644 --- a/apps/sim/instrumentation-client.ts +++ b/apps/sim/instrumentation-client.ts @@ -2,6 +2,7 @@ * Sim Telemetry - Client-side Instrumentation */ +import { randomFloat } from '@sim/utils/random' import { env } from './lib/core/config/env' import { sanitizeEventData } from './lib/core/security/redaction' @@ -117,7 +118,7 @@ if (typeof window !== 'undefined') { } if (telemetryEnabled) { - const shouldTrackVitals = Math.random() < 0.1 + const shouldTrackVitals = randomFloat() < 0.1 if (shouldTrackVitals) { window.addEventListener( diff --git a/apps/sim/lib/api-key/auth.test.ts b/apps/sim/lib/api-key/auth.test.ts index ac4bb4089f0..34097211f0b 100644 --- a/apps/sim/lib/api-key/auth.test.ts +++ b/apps/sim/lib/api-key/auth.test.ts @@ -9,7 +9,6 @@ * - Edge cases */ -import { randomBytes } from 'crypto' import { createEncryptedApiKey, createLegacyApiKey, @@ -18,24 +17,28 @@ import { } from '@sim/testing' import { describe, expect, it, vi } from 'vitest' -const cryptoMock = vi.hoisted(() => ({ - isEncryptedApiKeyFormat: (key: string) => key.startsWith('sk-sim-'), - isLegacyApiKeyFormat: (key: string) => key.startsWith('sim_') && !key.startsWith('sk-sim-'), - generateApiKey: () => `sim_${randomBytes(24).toString('base64url')}`, - generateEncryptedApiKey: () => `sk-sim-${randomBytes(24).toString('base64url')}`, - encryptApiKey: async (apiKey: string) => ({ - encrypted: `mock-iv:${Buffer.from(apiKey).toString('hex')}:mock-tag`, - iv: 'mock-iv', - }), - decryptApiKey: async (encryptedValue: string) => { - if (!encryptedValue.includes(':') || encryptedValue.split(':').length !== 3) { - return { decrypted: encryptedValue } - } - const parts = encryptedValue.split(':') - const hexPart = parts[1] - return { decrypted: Buffer.from(hexPart, 'hex').toString('utf8') } - }, -})) +const cryptoMock = vi.hoisted(() => { + let keyCounter = 0 + + return { + isEncryptedApiKeyFormat: (key: string) => key.startsWith('sk-sim-'), + isLegacyApiKeyFormat: (key: string) => key.startsWith('sim_') && !key.startsWith('sk-sim-'), + generateApiKey: () => `sim_test_key_${++keyCounter}`, + generateEncryptedApiKey: () => `sk-sim-test-key-${++keyCounter}`, + encryptApiKey: async (apiKey: string) => ({ + encrypted: `mock-iv:${Buffer.from(apiKey).toString('hex')}:mock-tag`, + iv: 'mock-iv', + }), + decryptApiKey: async (encryptedValue: string) => { + if (!encryptedValue.includes(':') || encryptedValue.split(':').length !== 3) { + return { decrypted: encryptedValue } + } + const parts = encryptedValue.split(':') + const hexPart = parts[1] + return { decrypted: Buffer.from(hexPart, 'hex').toString('utf8') } + }, + } +}) vi.mock('@/lib/api-key/crypto', () => cryptoMock) diff --git a/apps/sim/lib/api-key/crypto.test.ts b/apps/sim/lib/api-key/crypto.test.ts index f6243b876e4..ff6562f6035 100644 --- a/apps/sim/lib/api-key/crypto.test.ts +++ b/apps/sim/lib/api-key/crypto.test.ts @@ -8,7 +8,7 @@ * * @vitest-environment node */ -import { randomBytes } from 'crypto' +import { generateRandomHex } from '@sim/utils/random' import { beforeEach, describe, expect, it, vi } from 'vitest' const { mockEnv } = vi.hoisted(() => ({ @@ -56,7 +56,7 @@ describe('backfill idempotency — encrypted round-trip', () => { }) it('re-running the backfill on the same row yields the same keyHash', async () => { - const plainKey = `sk-sim-${randomBytes(12).toString('hex')}` + const plainKey = `sk-sim-${generateRandomHex(12)}` const { encrypted } = await encryptApiKey(plainKey) const { decrypted: first } = await decryptApiKey(encrypted) diff --git a/apps/sim/lib/copilot/request/otel.ts b/apps/sim/lib/copilot/request/otel.ts index 5ceb78c8a73..f7fcdd09d63 100644 --- a/apps/sim/lib/copilot/request/otel.ts +++ b/apps/sim/lib/copilot/request/otel.ts @@ -1,4 +1,3 @@ -import { randomBytes } from 'crypto' import { type Context, context, @@ -11,6 +10,7 @@ import { trace, } from '@opentelemetry/api' import { toError } from '@sim/utils/errors' +import { generateRandomHex } from '@sim/utils/random' import { RequestTraceV1Outcome } from '@/lib/copilot/generated/request-trace-v1' import { CopilotBranchKind, @@ -333,8 +333,8 @@ function isValidSpanContext(spanContext: SpanContext): boolean { function createFallbackSpanContext(): SpanContext { return { - traceId: randomBytes(16).toString('hex'), - spanId: randomBytes(8).toString('hex'), + traceId: generateRandomHex(16), + spanId: generateRandomHex(8), traceFlags: TraceFlags.SAMPLED, } } diff --git a/apps/sim/lib/copilot/vfs/file-reader.test.ts b/apps/sim/lib/copilot/vfs/file-reader.test.ts index f4326b32035..a64255a6d08 100644 --- a/apps/sim/lib/copilot/vfs/file-reader.test.ts +++ b/apps/sim/lib/copilot/vfs/file-reader.test.ts @@ -2,8 +2,8 @@ * @vitest-environment node */ -import { randomFillSync } from 'node:crypto' import { loggerMock } from '@sim/testing' +import { generateRandomBytes } from '@sim/utils/random' import { describe, expect, it, vi } from 'vitest' const { fetchWorkspaceFileBuffer } = vi.hoisted(() => ({ @@ -21,8 +21,7 @@ const MAX_IMAGE_READ_BYTES = 5 * 1024 * 1024 async function makeNoisePng(width: number, height: number): Promise { const sharp = (await import('sharp')).default - const raw = Buffer.alloc(width * height * 3) - randomFillSync(raw) + const raw = Buffer.from(generateRandomBytes(width * height * 3)) return sharp(raw, { raw: { width, height, channels: 3 } }) .png() .toBuffer() diff --git a/apps/sim/lib/core/config/redis.ts b/apps/sim/lib/core/config/redis.ts index c6e904ec7b3..ae303b4d0a7 100644 --- a/apps/sim/lib/core/config/redis.ts +++ b/apps/sim/lib/core/config/redis.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' +import { randomFloat } from '@sim/utils/random' import Redis from 'ioredis' import { env } from '@/lib/core/config/env' @@ -102,7 +103,7 @@ export function getRedisClient(): Redis | null { return 30000 } const base = Math.min(1000 * 2 ** (times - 1), 10000) - const jitter = Math.random() * base * 0.3 + const jitter = randomFloat() * base * 0.3 const delay = Math.round(base + jitter) logger.warn('Redis reconnecting', { attempt: times, nextRetryMs: delay }) return delay diff --git a/apps/sim/lib/core/security/encryption.ts b/apps/sim/lib/core/security/encryption.ts index 3d2e7390ec1..084dcf16cad 100644 --- a/apps/sim/lib/core/security/encryption.ts +++ b/apps/sim/lib/core/security/encryption.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import { decrypt, encrypt } from '@sim/security/encryption' import { toError } from '@sim/utils/errors' +import { generateRandomString } from '@sim/utils/random' import { env } from '@/lib/core/config/env' const logger = createLogger('Encryption') @@ -42,11 +43,5 @@ export async function decryptSecret(encryptedValue: string): Promise<{ decrypted */ export function generatePassword(length = 24): string { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_-+=' - let result = '' - - for (let i = 0; i < length; i++) { - result += chars.charAt(Math.floor(Math.random() * chars.length)) - } - - return result + return generateRandomString(length, chars) } diff --git a/apps/sim/lib/core/utils.test.ts b/apps/sim/lib/core/utils.test.ts index 5aa5a91804f..d534857465d 100644 --- a/apps/sim/lib/core/utils.test.ts +++ b/apps/sim/lib/core/utils.test.ts @@ -13,21 +13,16 @@ import { cn } from '@/lib/core/utils/cn' import { convertScheduleOptionsToCron } from '@/lib/core/utils/scheduling' import { getInvalidCharacters, isValidName, validateName } from '@/lib/core/utils/validation' -vi.mock('crypto', () => ({ - createCipheriv: vi.fn().mockReturnValue({ - update: vi.fn().mockReturnValue('encrypted-data'), - final: vi.fn().mockReturnValue('final-data'), - getAuthTag: vi.fn().mockReturnValue({ - toString: vi.fn().mockReturnValue('auth-tag'), - }), +vi.mock('@sim/security/encryption', () => ({ + encrypt: vi.fn().mockResolvedValue({ + encrypted: 'random-iv:encrypted-datafinal-data:auth-tag', + iv: 'random-iv', }), - createDecipheriv: vi.fn().mockReturnValue({ - update: vi.fn().mockReturnValue('decrypted-data'), - final: vi.fn().mockReturnValue('final-data'), - setAuthTag: vi.fn(), - }), - randomBytes: vi.fn().mockReturnValue({ - toString: vi.fn().mockReturnValue('random-iv'), + decrypt: vi.fn().mockImplementation(async (encryptedValue: string) => { + if (!encryptedValue.includes(':')) { + throw new Error('Invalid encrypted value format') + } + return { decrypted: 'decrypted-datafinal-data' } }), })) diff --git a/apps/sim/lib/core/utils/restore-name.ts b/apps/sim/lib/core/utils/restore-name.ts index 352cfde22cb..8a9ba48c443 100644 --- a/apps/sim/lib/core/utils/restore-name.ts +++ b/apps/sim/lib/core/utils/restore-name.ts @@ -1,4 +1,4 @@ -import { randomBytes } from 'crypto' +import { generateRandomHex } from '@sim/utils/random' const HASH_ATTEMPTS = 8 @@ -23,7 +23,7 @@ export async function generateRestoreName( } for (let i = 0; i < HASH_ATTEMPTS; i++) { - const hash = randomBytes(3).toString('hex') + const hash = generateRandomHex(3) const candidate = addSuffix(originalName, `_restored_${hash}`, options?.hasExtension) if (!(await nameExists(candidate))) { return candidate diff --git a/apps/sim/lib/data-drains/destinations/utils.ts b/apps/sim/lib/data-drains/destinations/utils.ts index d9c59f8887c..d569bd1c445 100644 --- a/apps/sim/lib/data-drains/destinations/utils.ts +++ b/apps/sim/lib/data-drains/destinations/utils.ts @@ -1,4 +1,5 @@ import { toError } from '@sim/utils/errors' +import { randomFloat } from '@sim/utils/random' import { z } from 'zod' /** @@ -57,7 +58,7 @@ export function backoffWithJitter( return Math.min(Math.max(retryAfterMs, baseMs), maxMs) } const exponential = Math.min(baseMs * 2 ** (attempt - 1), maxMs) - return exponential * (0.8 + Math.random() * 0.4) + return exponential * (0.8 + randomFloat() * 0.4) } /** diff --git a/apps/sim/lib/execution/event-buffer.ts b/apps/sim/lib/execution/event-buffer.ts index 8c0d08090cb..0bd098a1119 100644 --- a/apps/sim/lib/execution/event-buffer.ts +++ b/apps/sim/lib/execution/event-buffer.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' +import { randomInt } from '@sim/utils/random' import { env } from '@/lib/core/config/env' import { getRedisClient } from '@/lib/core/config/redis' import { LARGE_VALUE_THRESHOLD_BYTES } from '@/lib/execution/payloads/large-value-ref' @@ -754,7 +755,7 @@ export function createExecutionEventWriter( FLUSH_INTERVAL_MS * 2 ** Math.min(consecutiveFlushFailures, 6), FLUSH_MAX_RETRY_INTERVAL_MS ) - return backoff + Math.floor(Math.random() * FLUSH_INTERVAL_MS) + return backoff + randomInt(FLUSH_INTERVAL_MS) } const scheduleFlush = (delayMs = FLUSH_INTERVAL_MS) => { diff --git a/apps/sim/lib/execution/isolated-vm.ts b/apps/sim/lib/execution/isolated-vm.ts index 05b23940a9f..d53be0553c0 100644 --- a/apps/sim/lib/execution/isolated-vm.ts +++ b/apps/sim/lib/execution/isolated-vm.ts @@ -4,6 +4,7 @@ import path from 'node:path' import { fileURLToPath } from 'node:url' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' +import { generateRandomString, LOWERCASE_ALPHANUMERIC_ALPHABET } from '@sim/utils/random' import { env } from '@/lib/core/config/env' import { getRedisClient } from '@/lib/core/config/redis' import { @@ -1336,7 +1337,7 @@ export async function executeInIsolatedVM( } } - const distributedLeaseId = `${req.requestId}:${Date.now()}:${Math.random().toString(36).slice(2, 10)}` + const distributedLeaseId = `${req.requestId}:${Date.now()}:${generateRandomString(8, LOWERCASE_ALPHANUMERIC_ALPHABET)}` const leaseAcquireResult = await tryAcquireDistributedLease( ownerKey, distributedLeaseId, diff --git a/apps/sim/lib/knowledge/connectors/sync-engine.ts b/apps/sim/lib/knowledge/connectors/sync-engine.ts index 22c79598c59..7cf44f6d34a 100644 --- a/apps/sim/lib/knowledge/connectors/sync-engine.ts +++ b/apps/sim/lib/knowledge/connectors/sync-engine.ts @@ -9,6 +9,7 @@ import { import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { generateId } from '@sim/utils/id' +import { randomInt } from '@sim/utils/random' import { and, eq, gt, inArray, isNull, lt, ne, or, sql } from 'drizzle-orm' import { decryptApiKey } from '@/lib/api-key/crypto' import { getInternalApiBaseUrl } from '@/lib/core/utils/urls' @@ -93,7 +94,8 @@ async function isKnowledgeBaseActiveInTx( function calculateNextSyncTime(syncIntervalMinutes: number): Date | null { if (syncIntervalMinutes <= 0) return null const now = Date.now() - const jitterMs = Math.floor(Math.random() * Math.min(syncIntervalMinutes * 6_000, 300_000)) + const jitterWindowMs = Math.min(syncIntervalMinutes * 6_000, 300_000) + const jitterMs = jitterWindowMs > 0 ? randomInt(jitterWindowMs) : 0 return new Date(now + syncIntervalMinutes * 60_000 + jitterMs) } diff --git a/apps/sim/lib/knowledge/documents/document-processor.ts b/apps/sim/lib/knowledge/documents/document-processor.ts index a5b5223fba2..b9f5d25367c 100644 --- a/apps/sim/lib/knowledge/documents/document-processor.ts +++ b/apps/sim/lib/knowledge/documents/document-processor.ts @@ -1,6 +1,6 @@ -import { randomBytes } from 'crypto' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' +import { generateRandomHex } from '@sim/utils/random' import { PDFDocument } from 'pdf-lib' import { getBYOKKey } from '@/lib/api-key/byok' import { @@ -354,7 +354,7 @@ async function handleFileForOCR( } const timestamp = Date.now() - const uniqueId = randomBytes(8).toString('hex') + const uniqueId = generateRandomHex(8) const safeFileName = filename.replace(/[^a-zA-Z0-9.-]/g, '_') const customKey = `kb/${timestamp}-${uniqueId}-${safeFileName}` @@ -651,7 +651,7 @@ async function processChunk( try { const timestamp = Date.now() - const uniqueId = randomBytes(8).toString('hex') + const uniqueId = generateRandomHex(8) const safeFileName = filename.replace(/[^a-zA-Z0-9.-]/g, '_') const chunkKey = `kb/${timestamp}-${uniqueId}-chunk${chunkIndex + 1}-${safeFileName}` diff --git a/apps/sim/lib/knowledge/documents/utils.ts b/apps/sim/lib/knowledge/documents/utils.ts index 597c64602fa..0a2221cdc33 100644 --- a/apps/sim/lib/knowledge/documents/utils.ts +++ b/apps/sim/lib/knowledge/documents/utils.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { sleep } from '@sim/utils/helpers' +import { randomFloat } from '@sim/utils/random' const logger = createLogger('RetryUtils') @@ -142,7 +143,7 @@ export async function retryWithExponentialBackoff( ) } - const jitter = Math.random() * 0.1 * delay + const jitter = randomFloat() * 0.1 * delay const actualDelay = cappedRetryAfter ?? Math.min(delay + jitter, maxDelayMs) logger.info( diff --git a/apps/sim/lib/messaging/email/unsubscribe.ts b/apps/sim/lib/messaging/email/unsubscribe.ts index 3c02fdfe3a3..dc61ad231d9 100644 --- a/apps/sim/lib/messaging/email/unsubscribe.ts +++ b/apps/sim/lib/messaging/email/unsubscribe.ts @@ -1,8 +1,8 @@ -import { randomBytes } from 'crypto' import { db } from '@sim/db' import { settings, user } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { sha256Hex } from '@sim/security/hash' +import { generateRandomHex } from '@sim/utils/random' import { eq } from 'drizzle-orm' import { env } from '@/lib/core/config/env' import type { EmailType } from '@/lib/messaging/email/mailer' @@ -20,7 +20,7 @@ export interface EmailPreferences { * Generate a secure unsubscribe token for an email address */ export function generateUnsubscribeToken(email: string, emailType = 'marketing'): string { - const salt = randomBytes(16).toString('hex') + const salt = generateRandomHex(16) const hash = sha256Hex(`${email}:${salt}:${emailType}:${env.BETTER_AUTH_SECRET}`) return `${salt}:${hash}:${emailType}` diff --git a/apps/sim/lib/table/constants.ts b/apps/sim/lib/table/constants.ts index 67f01eb4190..248fe04b768 100644 --- a/apps/sim/lib/table/constants.ts +++ b/apps/sim/lib/table/constants.ts @@ -2,6 +2,7 @@ * Limits and constants for user-defined tables. */ +import { randomInt, randomItem } from '@sim/utils/random' import { env, envNumber } from '@/lib/core/config/env' export const TABLE_LIMITS = { @@ -311,14 +312,14 @@ export function generateUniqueTableName(existingNames: string[]): string { const maxAttempts = 50 for (let i = 0; i < maxAttempts; i++) { - const adj = TABLE_NAME_ADJECTIVES[Math.floor(Math.random() * TABLE_NAME_ADJECTIVES.length)] - const noun = TABLE_NAME_NOUNS[Math.floor(Math.random() * TABLE_NAME_NOUNS.length)] + const adj = randomItem(TABLE_NAME_ADJECTIVES) + const noun = randomItem(TABLE_NAME_NOUNS) const name = `${adj.toLowerCase()}_${noun.toLowerCase()}` if (!taken.has(name)) return name } - const adj = TABLE_NAME_ADJECTIVES[Math.floor(Math.random() * TABLE_NAME_ADJECTIVES.length)] - const noun = TABLE_NAME_NOUNS[Math.floor(Math.random() * TABLE_NAME_NOUNS.length)] - const suffix = Math.floor(Math.random() * 900) + 100 + const adj = randomItem(TABLE_NAME_ADJECTIVES) + const noun = randomItem(TABLE_NAME_NOUNS) + const suffix = randomInt(900) + 100 return `${adj.toLowerCase()}_${noun.toLowerCase()}_${suffix}` } diff --git a/apps/sim/lib/uploads/contexts/execution/utils.ts b/apps/sim/lib/uploads/contexts/execution/utils.ts index 94c35b2ade5..8d77dca5f1f 100644 --- a/apps/sim/lib/uploads/contexts/execution/utils.ts +++ b/apps/sim/lib/uploads/contexts/execution/utils.ts @@ -1,3 +1,4 @@ +import { generateRandomString, LOWERCASE_ALPHANUMERIC_ALPHABET } from '@sim/utils/random' import { isUuid, sanitizeFileName } from '@/executor/constants' import type { UserFile } from '@/executor/types' @@ -24,7 +25,7 @@ export function generateExecutionFileKey(context: ExecutionContext, fileName: st * Generate unique file ID for execution files */ export function generateFileId(): string { - return `file_${Date.now()}_${Math.random().toString(36).substring(2, 9)}` + return `file_${Date.now()}_${generateRandomString(7, LOWERCASE_ALPHANUMERIC_ALPHABET)}` } /** diff --git a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts index 3738eea03e7..7344b8f347a 100644 --- a/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts +++ b/apps/sim/lib/uploads/contexts/workspace/workspace-file-manager.ts @@ -3,12 +3,12 @@ * Files uploaded at workspace level persist indefinitely and are accessible across all workflows */ -import { randomBytes } from 'crypto' import { db } from '@sim/db' import { workspaceFiles } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { getPostgresConstraintName, getPostgresErrorCode } from '@sim/utils/errors' import { generateShortId } from '@sim/utils/id' +import { generateRandomHex } from '@sim/utils/random' import { and, eq, isNull, sql } from 'drizzle-orm' import { checkStorageQuota, @@ -118,7 +118,7 @@ export function parseWorkspaceFileKey(key: string): string | null { */ export function generateWorkspaceFileKey(workspaceId: string, fileName: string): string { const timestamp = Date.now() - const random = randomBytes(8).toString('hex') + const random = generateRandomHex(8) const safeFileName = sanitizeFileName(fileName) return `workspace/${workspaceId}/${timestamp}-${random}-${safeFileName}` } diff --git a/apps/sim/lib/uploads/core/storage-service.ts b/apps/sim/lib/uploads/core/storage-service.ts index 75632f2cba7..f766d65467f 100644 --- a/apps/sim/lib/uploads/core/storage-service.ts +++ b/apps/sim/lib/uploads/core/storage-service.ts @@ -1,5 +1,5 @@ -import { randomBytes } from 'crypto' import { createLogger } from '@sim/logger' +import { generateRandomHex } from '@sim/utils/random' import { getStorageConfig, USE_BLOB_STORAGE, USE_S3_STORAGE } from '@/lib/uploads/config' import type { BlobConfig } from '@/lib/uploads/providers/blob/types' import type { S3Config } from '@/lib/uploads/providers/s3/types' @@ -295,7 +295,7 @@ export async function generatePresignedUploadUrl( key = customKey } else { const timestamp = Date.now() - const uniqueId = randomBytes(8).toString('hex') + const uniqueId = generateRandomHex(8) const safeFileName = fileName.replace(/[^a-zA-Z0-9.-]/g, '_') key = `${context}/${timestamp}-${uniqueId}-${safeFileName}` } diff --git a/apps/sim/lib/webhooks/providers/twilio-voice.ts b/apps/sim/lib/webhooks/providers/twilio-voice.ts index 543264cd730..b6287c56761 100644 --- a/apps/sim/lib/webhooks/providers/twilio-voice.ts +++ b/apps/sim/lib/webhooks/providers/twilio-voice.ts @@ -1,4 +1,4 @@ -import crypto from 'crypto' +import { createHmac } from 'node:crypto' import { createLogger } from '@sim/logger' import { safeCompare } from '@sim/security/compare' import { NextResponse } from 'next/server' @@ -37,17 +37,7 @@ async function validateTwilioSignature( sortedKeys, dataLength: data.length, }) - const encoder = new TextEncoder() - const key = await crypto.subtle.importKey( - 'raw', - encoder.encode(authToken), - { name: 'HMAC', hash: 'SHA-1' }, - false, - ['sign'] - ) - const signatureBytes = await crypto.subtle.sign('HMAC', key, encoder.encode(data)) - const signatureArray = Array.from(new Uint8Array(signatureBytes)) - const signatureBase64 = btoa(String.fromCharCode(...signatureArray)) + const signatureBase64 = createHmac('sha1', authToken).update(data, 'utf8').digest('base64') logger.debug('Twilio signature comparison', { computedSignature: `${signatureBase64.substring(0, 10)}...`, providedSignature: `${signature.substring(0, 10)}...`, diff --git a/apps/sim/lib/workflows/colors.ts b/apps/sim/lib/workflows/colors.ts index d7acbb74abf..0b16c72b8b4 100644 --- a/apps/sim/lib/workflows/colors.ts +++ b/apps/sim/lib/workflows/colors.ts @@ -11,6 +11,8 @@ * - Pink: (#ec4899) */ +import { randomItem } from '@sim/utils/random' + /** * Full list of available workflow colors with names. * Used for color picker and random color assignment. @@ -69,5 +71,5 @@ export const WORKFLOW_COLORS = [ * @returns A hex color string from the available workflow colors */ export function getNextWorkflowColor(): string { - return WORKFLOW_COLORS[Math.floor(Math.random() * WORKFLOW_COLORS.length)].color + return randomItem(WORKFLOW_COLORS).color } diff --git a/apps/sim/lib/workspaces/colors.ts b/apps/sim/lib/workspaces/colors.ts index b652ebd36d9..148ac7c0229 100644 --- a/apps/sim/lib/workspaces/colors.ts +++ b/apps/sim/lib/workspaces/colors.ts @@ -1,3 +1,5 @@ +import { randomItem } from '@sim/utils/random' + /** Color palette for workspace accents, aligned with the workflow color family. */ export const WORKSPACE_COLORS = [ '#2ABBF8', // Blue @@ -11,7 +13,7 @@ export const WORKSPACE_COLORS = [ /** Picks a random workspace color from the hero palette. */ export function getRandomWorkspaceColor(): string { - return WORKSPACE_COLORS[Math.floor(Math.random() * WORKSPACE_COLORS.length)] + return randomItem(WORKSPACE_COLORS) } const APP_COLORS = [ diff --git a/apps/sim/lib/workspaces/naming.ts b/apps/sim/lib/workspaces/naming.ts index 3c050926402..5bb9f80a789 100644 --- a/apps/sim/lib/workspaces/naming.ts +++ b/apps/sim/lib/workspaces/naming.ts @@ -2,6 +2,7 @@ * Utility functions for generating names for workspaces and folders */ +import { randomItem } from '@sim/utils/random' import { requestJson } from '@/lib/api/client/request' import { type FolderApi, listFoldersContract } from '@/lib/api/contracts/folders' @@ -89,7 +90,7 @@ export function generateIncrementalName( * Generates a random cosmos-themed workspace name */ export function generateWorkspaceName(): string { - return WORKSPACE_NOUNS[Math.floor(Math.random() * WORKSPACE_NOUNS.length)] + return randomItem(WORKSPACE_NOUNS) } async function fetchWorkspaceFolders(workspaceId: string): Promise { diff --git a/apps/sim/providers/anthropic/utils.ts b/apps/sim/providers/anthropic/utils.ts index 6ab45379f8d..247c551ab93 100644 --- a/apps/sim/providers/anthropic/utils.ts +++ b/apps/sim/providers/anthropic/utils.ts @@ -5,6 +5,7 @@ import type { Usage, } from '@anthropic-ai/sdk/resources' import { createLogger } from '@sim/logger' +import { generateRandomString, LOWERCASE_ALPHANUMERIC_ALPHABET } from '@sim/utils/random' import { trackForcedToolUsage } from '@/providers/utils' const logger = createLogger('AnthropicUtils') @@ -53,7 +54,7 @@ export function createReadableStreamFromAnthropicStream( } export function generateToolUseId(toolName: string): string { - return `${toolName}-${Date.now()}-${Math.random().toString(36).substring(2, 7)}` + return `${toolName}-${Date.now()}-${generateRandomString(5, LOWERCASE_ALPHANUMERIC_ALPHABET)}` } export function checkForForcedToolUsage( diff --git a/apps/sim/providers/bedrock/utils.ts b/apps/sim/providers/bedrock/utils.ts index 9400d2378f1..feee65ad364 100644 --- a/apps/sim/providers/bedrock/utils.ts +++ b/apps/sim/providers/bedrock/utils.ts @@ -1,5 +1,6 @@ import type { ConverseStreamOutput } from '@aws-sdk/client-bedrock-runtime' import { createLogger } from '@sim/logger' +import { generateRandomString, LOWERCASE_ALPHANUMERIC_ALPHABET } from '@sim/utils/random' import { trackForcedToolUsage } from '@/providers/utils' const logger = createLogger('BedrockUtils') @@ -73,7 +74,7 @@ export function checkForForcedToolUsage( */ export function generateToolUseId(toolName: string): string { const timestamp = Date.now().toString(36) // Base36 timestamp (9 chars) - const random = Math.random().toString(36).substring(2, 7) // 5 random chars + const random = generateRandomString(5, LOWERCASE_ALPHANUMERIC_ALPHABET) const suffix = `-${timestamp}-${random}` // ~15 chars const maxNameLength = 64 - suffix.length const truncatedName = toolName.substring(0, maxNameLength).replace(/[^a-zA-Z0-9_-]/g, '_') diff --git a/apps/sim/stores/workflows/registry/store.ts b/apps/sim/stores/workflows/registry/store.ts index b9c099c4889..e33bc15e3da 100644 --- a/apps/sim/stores/workflows/registry/store.ts +++ b/apps/sim/stores/workflows/registry/store.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { generateRandomHex } from '@sim/utils/random' import { create } from 'zustand' import { devtools } from 'zustand/middleware' import { requestJson } from '@/lib/api/client/request' @@ -25,7 +26,7 @@ const initialHydration: HydrationState = { error: null, } -const createRequestId = () => `${Date.now()}-${Math.random().toString(16).slice(2)}` +const createRequestId = () => `${Date.now()}-${generateRandomHex(8)}` function resetWorkflowStores() { useWorkflowStore.setState({ diff --git a/apps/sim/stores/workflows/registry/utils.ts b/apps/sim/stores/workflows/registry/utils.ts index 8be102e4a60..e893b17afc4 100644 --- a/apps/sim/stores/workflows/registry/utils.ts +++ b/apps/sim/stores/workflows/registry/utils.ts @@ -1,3 +1,5 @@ +import { randomItem } from '@sim/utils/random' + // Cosmos-themed adjectives and nouns for creative workflow names (max 9 chars each) const ADJECTIVES = [ // Light & Luminosity @@ -412,7 +414,7 @@ const NOUNS = [ * @returns A creative workflow name like "blazing-phoenix" or "crystal-dragon" */ export function generateCreativeWorkflowName(): string { - const adjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)] - const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)] + const adjective = randomItem(ADJECTIVES) + const noun = randomItem(NOUNS) return `${adjective.toLowerCase()}-${noun.toLowerCase()}` } diff --git a/apps/sim/tools/gmail/utils.ts b/apps/sim/tools/gmail/utils.ts index 5de6f0ae2e4..06e3d72631d 100644 --- a/apps/sim/tools/gmail/utils.ts +++ b/apps/sim/tools/gmail/utils.ts @@ -1,3 +1,4 @@ +import { generateRandomString, LOWERCASE_ALPHANUMERIC_ALPHABET } from '@sim/utils/random' import { convert } from 'html-to-text' import type { GmailAttachment, @@ -292,7 +293,7 @@ export function createMessagesSummary(messages: any[]): string { * Generate a unique MIME boundary string */ function generateBoundary(): string { - return `----=_Part_${Date.now()}_${Math.random().toString(36).substring(2, 15)}` + return `----=_Part_${Date.now()}_${generateRandomString(13, LOWERCASE_ALPHANUMERIC_ALPHABET)}` } /** diff --git a/apps/sim/tools/google_slides/add_image.ts b/apps/sim/tools/google_slides/add_image.ts index 3722dbe0dfd..a16a1407e6c 100644 --- a/apps/sim/tools/google_slides/add_image.ts +++ b/apps/sim/tools/google_slides/add_image.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { generateRandomString, LOWERCASE_ALPHANUMERIC_ALPHABET } from '@sim/utils/random' import type { ToolConfig } from '@/tools/types' const logger = createLogger('GoogleSlidesAddImageTool') @@ -122,7 +123,7 @@ export const addImageTool: ToolConfig = { } // Generate a unique object ID for the new image - const imageObjectId = `image_${Date.now()}_${Math.random().toString(36).substring(2, 9)}` + const imageObjectId = `image_${Date.now()}_${generateRandomString(7, LOWERCASE_ALPHANUMERIC_ALPHABET)}` // Convert points to EMU (default sizes if not specified) const widthEmu = (params.width || 300) * PT_TO_EMU diff --git a/apps/sim/tools/google_slides/add_slide.ts b/apps/sim/tools/google_slides/add_slide.ts index d5bef81c22a..fcafdb74581 100644 --- a/apps/sim/tools/google_slides/add_slide.ts +++ b/apps/sim/tools/google_slides/add_slide.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { generateRandomString, LOWERCASE_ALPHANUMERIC_ALPHABET } from '@sim/utils/random' import type { ToolConfig } from '@/tools/types' const logger = createLogger('GoogleSlidesAddSlideTool') @@ -106,7 +107,7 @@ export const addSlideTool: ToolConfig = { }, body: (params) => { // Generate a unique object ID for the new slide - const slideObjectId = `slide_${Date.now()}_${Math.random().toString(36).substring(2, 9)}` + const slideObjectId = `slide_${Date.now()}_${generateRandomString(7, LOWERCASE_ALPHANUMERIC_ALPHABET)}` // Validate and normalize the layout let layout = (params.layout || 'BLANK').toUpperCase() diff --git a/apps/sim/tools/google_slides/create_shape.ts b/apps/sim/tools/google_slides/create_shape.ts index 6c063c38642..70a352dca2e 100644 --- a/apps/sim/tools/google_slides/create_shape.ts +++ b/apps/sim/tools/google_slides/create_shape.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { generateRandomString, LOWERCASE_ALPHANUMERIC_ALPHABET } from '@sim/utils/random' import type { ToolConfig } from '@/tools/types' const logger = createLogger('GoogleSlidesCreateShapeTool') @@ -271,7 +272,7 @@ export const createShapeTool: ToolConfig } // Generate a unique object ID for the new shape - const shapeObjectId = `shape_${Date.now()}_${Math.random().toString(36).substring(2, 9)}` + const shapeObjectId = `shape_${Date.now()}_${generateRandomString(7, LOWERCASE_ALPHANUMERIC_ALPHABET)}` // Convert points to EMU const widthEmu = (params.width || 200) * PT_TO_EMU diff --git a/apps/sim/tools/google_slides/create_table.ts b/apps/sim/tools/google_slides/create_table.ts index f6f144af8a9..30f9f9cb56e 100644 --- a/apps/sim/tools/google_slides/create_table.ts +++ b/apps/sim/tools/google_slides/create_table.ts @@ -1,4 +1,5 @@ import { createLogger } from '@sim/logger' +import { generateRandomString, LOWERCASE_ALPHANUMERIC_ALPHABET } from '@sim/utils/random' import type { ToolConfig } from '@/tools/types' const logger = createLogger('GoogleSlidesCreateTableTool') @@ -135,7 +136,7 @@ export const createTableTool: ToolConfig } // Generate a unique object ID for the new table - const tableObjectId = `table_${Date.now()}_${Math.random().toString(36).substring(2, 9)}` + const tableObjectId = `table_${Date.now()}_${generateRandomString(7, LOWERCASE_ALPHANUMERIC_ALPHABET)}` // Convert points to EMU const widthEmu = (params.width || 400) * PT_TO_EMU diff --git a/apps/sim/tools/index.ts b/apps/sim/tools/index.ts index f19ba67626c..01d8ed1f1bd 100644 --- a/apps/sim/tools/index.ts +++ b/apps/sim/tools/index.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' import { sleep } from '@sim/utils/helpers' +import { randomFloat } from '@sim/utils/random' import { getBYOKKey } from '@/lib/api-key/byok' import { generateInternalToken } from '@/lib/auth/internal' import { isHosted } from '@/lib/core/config/feature-flags' @@ -1273,7 +1274,7 @@ function isRetryableFailure(error: unknown, status?: number): boolean { function calculateBackoff(attempt: number, initialDelayMs: number, maxDelayMs: number): number { const base = Math.min(initialDelayMs * 2 ** attempt, maxDelayMs) - return Math.round(base / 2 + Math.random() * (base / 2)) + return Math.round(base / 2 + randomFloat() * (base / 2)) } function parseRetryAfterHeader(header: string | null): number { diff --git a/apps/sim/tools/mistral/parser.ts b/apps/sim/tools/mistral/parser.ts index 884b4c0e874..2723e743a26 100644 --- a/apps/sim/tools/mistral/parser.ts +++ b/apps/sim/tools/mistral/parser.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' +import { generateRandomString, LOWERCASE_ALPHANUMERIC_ALPHABET } from '@sim/utils/random' import { isInternalFileUrl } from '@/lib/uploads/utils/file-utils' import type { MistralParserInput, @@ -296,7 +297,7 @@ export const mistralParserTool: ToolConfig { - const iv = randomBytes(16) + const iv = Buffer.from(generateRandomBytes(16)) const key = getEncryptionKeyBuffer() const cipher = createCipheriv('aes-256-gcm', key, iv, { authTagLength: 16 }) let encrypted = cipher.update(secret, 'utf8', 'hex') diff --git a/packages/db/scripts/seed-stress-test-users.ts b/packages/db/scripts/seed-stress-test-users.ts index dcab3170e20..6eb14ac6f29 100644 --- a/packages/db/scripts/seed-stress-test-users.ts +++ b/packages/db/scripts/seed-stress-test-users.ts @@ -7,6 +7,11 @@ */ import { generateId } from '@sim/utils/id' +import { + randomInt as getRandomInt, + randomItem as getRandomItem, + randomFloat, +} from '@sim/utils/random' import { eq, type InferInsertModel } from 'drizzle-orm' import { db, userTableDefinitions, userTableRows } from '../index' @@ -93,15 +98,15 @@ const lastNames = [ ] function randomItem(arr: T[]): T { - return arr[Math.floor(Math.random() * arr.length)] + return getRandomItem(arr) } function randomInt(min: number, max: number): number { - return Math.floor(Math.random() * (max - min + 1)) + min + return getRandomInt(max - min + 1) + min } function randomDate(start: Date, end: Date): string { - const date = new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())) + const date = new Date(start.getTime() + randomFloat() * (end.getTime() - start.getTime())) return date.toISOString().split('T')[0] } @@ -116,7 +121,7 @@ function generateUserRow(index: number): UserRow { age: randomInt(22, 65), department: randomItem(departments), salary: randomInt(40000, 200000), - active: Math.random() > 0.1, // 90% active + active: randomFloat() > 0.1, // 90% active hire_date: randomDate(new Date('2015-01-01'), new Date('2024-12-31')), country: randomItem(countries), } diff --git a/packages/security/package.json b/packages/security/package.json index dc823111799..8e5d178fd06 100644 --- a/packages/security/package.json +++ b/packages/security/package.json @@ -40,7 +40,9 @@ "test": "vitest run", "test:watch": "vitest" }, - "dependencies": {}, + "dependencies": { + "@sim/utils": "workspace:*" + }, "devDependencies": { "@sim/tsconfig": "workspace:*", "@types/node": "24.2.1", diff --git a/packages/security/src/encryption.test.ts b/packages/security/src/encryption.test.ts index 13369500fd1..ea15c573b45 100644 --- a/packages/security/src/encryption.test.ts +++ b/packages/security/src/encryption.test.ts @@ -1,4 +1,4 @@ -import { randomBytes } from 'node:crypto' +import { generateRandomBytes } from '@sim/utils/random' import { describe, expect, it } from 'vitest' import { decrypt, encrypt } from './encryption' @@ -68,7 +68,7 @@ describe('decrypt', () => { it('throws when decrypted with a different key', async () => { const { encrypted } = await encrypt('original', KEY) - const otherKey = randomBytes(32) + const otherKey = Buffer.from(generateRandomBytes(32)) await expect(decrypt(encrypted, otherKey)).rejects.toThrow() }) }) diff --git a/packages/security/src/encryption.ts b/packages/security/src/encryption.ts index c1bbf078e38..a727b20e28d 100644 --- a/packages/security/src/encryption.ts +++ b/packages/security/src/encryption.ts @@ -1,4 +1,5 @@ -import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto' +import { createCipheriv, createDecipheriv } from 'node:crypto' +import { generateRandomBytes } from '@sim/utils/random' /** * AES-256-GCM encryption primitive. Produces a self-contained string in the @@ -14,7 +15,7 @@ export async function encrypt( ): Promise<{ encrypted: string; iv: string }> { assertKey(key) - const iv = randomBytes(16) + const iv = Buffer.from(generateRandomBytes(16)) const cipher = createCipheriv('aes-256-gcm', key, iv, { authTagLength: 16 }) let encrypted = cipher.update(plaintext, 'utf8', 'hex') encrypted += cipher.final('hex') diff --git a/packages/security/src/tokens.ts b/packages/security/src/tokens.ts index 311aaf46796..bccf2566bc3 100644 --- a/packages/security/src/tokens.ts +++ b/packages/security/src/tokens.ts @@ -1,4 +1,4 @@ -import { randomBytes } from 'node:crypto' +import { generateRandomBytes } from '@sim/utils/random' /** * Generate a cryptographically secure random token encoded as base64url. The @@ -8,5 +8,5 @@ import { randomBytes } from 'node:crypto' * @param byteLength - Number of random bytes to draw before encoding. Defaults to 24 (~32 chars). */ export function generateSecureToken(byteLength = 24): string { - return randomBytes(byteLength).toString('base64url') + return Buffer.from(generateRandomBytes(byteLength)).toString('base64url') } diff --git a/packages/testing/src/builders/execution.builder.ts b/packages/testing/src/builders/execution.builder.ts index 138630fc623..8f87a7b8e44 100644 --- a/packages/testing/src/builders/execution.builder.ts +++ b/packages/testing/src/builders/execution.builder.ts @@ -1,3 +1,4 @@ +import { shortId } from '../factories/id' import type { ExecutionContext } from '../types' /** @@ -18,7 +19,7 @@ import type { ExecutionContext } from '../types' */ export class ExecutionContextBuilder { private workflowId = 'test-workflow' - private executionId = `exec-${Math.random().toString(36).substring(2, 10)}` + private executionId = `exec-${shortId(8)}` private blockStates = new Map() private executedBlocks = new Set() private blockLogs: any[] = [] diff --git a/packages/testing/src/builders/workflow.builder.ts b/packages/testing/src/builders/workflow.builder.ts index ce074d8f064..dd94463efbe 100644 --- a/packages/testing/src/builders/workflow.builder.ts +++ b/packages/testing/src/builders/workflow.builder.ts @@ -4,6 +4,7 @@ import { createFunctionBlock, createStarterBlock, } from '../factories/block.factory' +import { shortId } from '../factories/id' import type { Position } from '../types' /* eslint-disable @typescript-eslint/no-explicit-any */ @@ -229,7 +230,7 @@ export class WorkflowBuilder { value: any ): this { this.variables?.push({ - id: `var-${Math.random().toString(36).substring(2, 8)}`, + id: `var-${shortId(6)}`, name, type, value, diff --git a/packages/testing/src/factories/block.factory.ts b/packages/testing/src/factories/block.factory.ts index 1020f651f8e..346ac8de371 100644 --- a/packages/testing/src/factories/block.factory.ts +++ b/packages/testing/src/factories/block.factory.ts @@ -1,4 +1,5 @@ import type { BlockData, BlockOutput, Position } from '../types' +import { shortId } from './id' /* eslint-disable @typescript-eslint/no-explicit-any */ @@ -28,7 +29,7 @@ export interface BlockFactoryOptions { * Generates a unique block ID. */ function generateBlockId(prefix = 'block'): string { - return `${prefix}-${Math.random().toString(36).substring(2, 10)}` + return `${prefix}-${shortId(8)}` } /** diff --git a/packages/testing/src/factories/dag.factory.ts b/packages/testing/src/factories/dag.factory.ts index 2b3bdd74cf9..544bf20f907 100644 --- a/packages/testing/src/factories/dag.factory.ts +++ b/packages/testing/src/factories/dag.factory.ts @@ -3,6 +3,7 @@ * These are used in executor tests for DAG construction and edge management */ +import { shortId } from './id' import { createSerializedBlock, type SerializedBlock } from './serialized-block.factory' /** @@ -62,7 +63,7 @@ export interface DAGNodeFactoryOptions { * ``` */ export function createDAGNode(options: DAGNodeFactoryOptions = {}): DAGNode { - const id = options.id ?? `node-${Math.random().toString(36).substring(2, 8)}` + const id = options.id ?? `node-${shortId(6)}` const block = options.block ?? createSerializedBlock({ diff --git a/packages/testing/src/factories/edge.factory.ts b/packages/testing/src/factories/edge.factory.ts index 65e6e69f082..d5952799b1e 100644 --- a/packages/testing/src/factories/edge.factory.ts +++ b/packages/testing/src/factories/edge.factory.ts @@ -1,5 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { shortId } from './id' + /** * Options for creating a mock edge. */ @@ -17,7 +19,7 @@ export interface EdgeFactoryOptions { * Generates an edge ID from source and target. */ function generateEdgeId(source: string, target: string): string { - return `${source}-${target}-${Math.random().toString(36).substring(2, 6)}` + return `${source}-${target}-${shortId(4)}` } /** diff --git a/packages/testing/src/factories/execution.factory.ts b/packages/testing/src/factories/execution.factory.ts index 38df3cb716f..1bc42988eb6 100644 --- a/packages/testing/src/factories/execution.factory.ts +++ b/packages/testing/src/factories/execution.factory.ts @@ -1,4 +1,5 @@ import type { ExecutionContext } from '../types' +import { shortId } from './id' /** * Options for creating a mock execution context. @@ -38,7 +39,7 @@ export function createExecutionContext( ): ExecutionContext { return { workflowId: options.workflowId ?? 'test-workflow', - executionId: options.executionId ?? `exec-${Math.random().toString(36).substring(2, 10)}`, + executionId: options.executionId ?? `exec-${shortId(8)}`, blockStates: options.blockStates ?? new Map(), executedBlocks: options.executedBlocks ?? new Set(), blockLogs: options.blockLogs ?? [], diff --git a/packages/testing/src/factories/id.ts b/packages/testing/src/factories/id.ts index 858963f6058..06f0ee55e02 100644 --- a/packages/testing/src/factories/id.ts +++ b/packages/testing/src/factories/id.ts @@ -1,18 +1,10 @@ -const URL_SAFE_ALPHABET = 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict' +import { generateRandomString } from '@sim/utils/random' /** * 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. + * Uses the shared app-level random utility for consistency. */ 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 + return generateRandomString(size) } diff --git a/packages/testing/src/factories/tool-responses.factory.ts b/packages/testing/src/factories/tool-responses.factory.ts index 7440c9c6d61..a41e6575f88 100644 --- a/packages/testing/src/factories/tool-responses.factory.ts +++ b/packages/testing/src/factories/tool-responses.factory.ts @@ -4,6 +4,8 @@ * This file contains mock data samples to be used in tool unit tests. */ +import { randomFloat } from '@sim/utils/random' + /** * HTTP Request mock responses for different scenarios. */ @@ -147,7 +149,7 @@ export const mockPineconeResponses = { embedding: { embedding: Array(1536) .fill(0) - .map(() => Math.random() * 2 - 1), + .map(() => randomFloat() * 2 - 1), metadata: { text: 'Sample text for embedding', id: 'embed-123' }, }, searchResults: { diff --git a/packages/testing/src/factories/user.factory.ts b/packages/testing/src/factories/user.factory.ts index c98babab91a..0d2a5170af1 100644 --- a/packages/testing/src/factories/user.factory.ts +++ b/packages/testing/src/factories/user.factory.ts @@ -1,4 +1,5 @@ import type { User, Workflow, WorkflowState, Workspace } from '../types' +import { shortId } from './id' import { createWorkflowState } from './workflow.factory' /** @@ -20,7 +21,7 @@ export interface UserFactoryOptions { * ``` */ export function createUser(options: UserFactoryOptions = {}): User { - const id = options.id ?? `user-${Math.random().toString(36).substring(2, 10)}` + const id = options.id ?? `user-${shortId(8)}` return { id, email: options.email ?? `${id}@test.example.com`, @@ -51,9 +52,9 @@ export interface WorkspaceFactoryOptions { export function createWorkspace(options: WorkspaceFactoryOptions = {}): Workspace { const now = new Date() return { - id: options.id ?? `ws-${Math.random().toString(36).substring(2, 10)}`, + id: options.id ?? `ws-${shortId(8)}`, name: options.name ?? 'Test Workspace', - ownerId: options.ownerId ?? `user-${Math.random().toString(36).substring(2, 10)}`, + ownerId: options.ownerId ?? `user-${shortId(8)}`, createdAt: options.createdAt ?? now, updatedAt: options.updatedAt ?? now, } @@ -83,9 +84,9 @@ export interface WorkflowObjectFactoryOptions { export function createWorkflow(options: WorkflowObjectFactoryOptions = {}): Workflow { const now = new Date() return { - id: options.id ?? `wf-${Math.random().toString(36).substring(2, 10)}`, + id: options.id ?? `wf-${shortId(8)}`, name: options.name ?? 'Test Workflow', - workspaceId: options.workspaceId ?? `ws-${Math.random().toString(36).substring(2, 10)}`, + workspaceId: options.workspaceId ?? `ws-${shortId(8)}`, state: options.state ?? createWorkflowState(), createdAt: options.createdAt ?? now, updatedAt: options.updatedAt ?? now, diff --git a/packages/testing/src/mocks/socket.mock.ts b/packages/testing/src/mocks/socket.mock.ts index 249b5ac8fe5..4a142669be2 100644 --- a/packages/testing/src/mocks/socket.mock.ts +++ b/packages/testing/src/mocks/socket.mock.ts @@ -1,3 +1,4 @@ +import { generateRandomString } from '@sim/utils/random' import { type Mock, vi } from 'vitest' /** @@ -34,7 +35,7 @@ export function createMockSocket(): IMockSocket { const eventHandlers: Record any)[]> = {} const socket = { - id: `socket-${Math.random().toString(36).substring(2, 10)}`, + id: `socket-${generateRandomString(8)}`, connected: true, disconnected: false, diff --git a/packages/testing/src/mocks/terminal-console.mock.ts b/packages/testing/src/mocks/terminal-console.mock.ts index b0684c02125..29cffe19403 100644 --- a/packages/testing/src/mocks/terminal-console.mock.ts +++ b/packages/testing/src/mocks/terminal-console.mock.ts @@ -1,3 +1,4 @@ +import { generateRandomString } from '@sim/utils/random' import { vi } from 'vitest' interface ConsoleEntryLike { @@ -16,7 +17,7 @@ const entriesByWorkflow: Record = {} const mockGetWorkflowEntries = vi.fn((workflowId: string) => entriesByWorkflow[workflowId] ?? []) const mockAddConsole = vi.fn((entry: ConsoleEntryLike) => { - const stored = { ...entry, id: entry.id ?? `mock-${Math.random().toString(36).slice(2)}` } + const stored = { ...entry, id: entry.id ?? `mock-${generateRandomString(10)}` } if (!entriesByWorkflow[entry.workflowId]) entriesByWorkflow[entry.workflowId] = [] entriesByWorkflow[entry.workflowId].push(stored) return stored diff --git a/packages/ts-sdk/src/index.ts b/packages/ts-sdk/src/index.ts index ffed7ca1e7b..1d3f22ccc87 100644 --- a/packages/ts-sdk/src/index.ts +++ b/packages/ts-sdk/src/index.ts @@ -1,5 +1,15 @@ +import { webcrypto } from 'node:crypto' import fetch from 'node-fetch' +const UINT32_MAX_PLUS_ONE = 0x1_0000_0000 + +// Published SDK stays self-contained; use Web Crypto directly instead of private workspace utils. +function randomFloat(): number { + const value = new Uint32Array(1) + webcrypto.getRandomValues(value) + return value[0] / UINT32_MAX_PLUS_ONE +} + export interface SimStudioConfig { apiKey: string baseUrl?: string @@ -434,7 +444,7 @@ export class SimStudioClient { ? this.rateLimitInfo.retryAfter : Math.min(delay, maxDelay) - const jitter = waitTime * (0.75 + Math.random() * 0.5) + const jitter = waitTime * (0.75 + randomFloat() * 0.5) await new Promise((resolve) => setTimeout(resolve, jitter)) diff --git a/packages/utils/package.json b/packages/utils/package.json index 013c600c567..a54714df2cc 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -14,6 +14,10 @@ "types": "./src/id.ts", "default": "./src/id.ts" }, + "./random": { + "types": "./src/random.ts", + "default": "./src/random.ts" + }, "./errors": { "types": "./src/errors.ts", "default": "./src/errors.ts" diff --git a/packages/utils/src/id.ts b/packages/utils/src/id.ts index 333a029916b..4e8549421f8 100644 --- a/packages/utils/src/id.ts +++ b/packages/utils/src/id.ts @@ -1,3 +1,9 @@ +import { generateRandomBytes, generateRandomString } from './random.js' + +type RandomUuidSource = { + randomUUID?: () => string +} + /** * Generates a UUID v4 string safe for all contexts. * @@ -7,12 +13,12 @@ * to `crypto.getRandomValues()`, which does not require a secure context. */ export function generateId(): string { - if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') { - return crypto.randomUUID() + const cryptoProvider = (globalThis as typeof globalThis & { crypto?: RandomUuidSource }).crypto + if (typeof cryptoProvider?.randomUUID === 'function') { + return cryptoProvider.randomUUID() } - const bytes = new Uint8Array(16) - crypto.getRandomValues(bytes) + const bytes = generateRandomBytes(16) bytes[6] = (bytes[6] & 0x0f) | 0x40 bytes[8] = (bytes[8] & 0x3f) | 0x80 @@ -30,8 +36,6 @@ export function isValidUuid(value: string): boolean { return UUID_RE.test(value) } -const URL_SAFE_ALPHABET = 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict' - /** * Generates a short, URL-safe random ID. * @@ -43,25 +47,6 @@ const URL_SAFE_ALPHABET = 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghj * Length must be in [2, 256]. * @returns A random string drawn from the alphabet */ -export function generateShortId(size = 21, alphabet: string = URL_SAFE_ALPHABET): string { - const alphabetLength = alphabet.length - if (alphabetLength < 2 || alphabetLength > 256) { - throw new Error('generateShortId alphabet length must be between 2 and 256') - } - - const mask = (2 << (31 - Math.clz32((alphabetLength - 1) | 1))) - 1 - const step = Math.ceil((1.6 * mask * size) / alphabetLength) - - let id = '' - while (id.length < size) { - const bytes = new Uint8Array(step) - crypto.getRandomValues(bytes) - for (let i = 0; i < step && id.length < size; i++) { - const index = bytes[i] & mask - if (index < alphabetLength) { - id += alphabet[index] - } - } - } - return id +export function generateShortId(size = 21, alphabet?: string): string { + return generateRandomString(size, alphabet) } diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index 83147e181a0..e3c4ac47d28 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -12,3 +12,12 @@ export { } from './formatting.js' export { noop, sleep } from './helpers.js' export { generateId, generateShortId, isValidUuid } from './id.js' +export { + generateRandomBytes, + generateRandomHex, + generateRandomString, + LOWERCASE_ALPHANUMERIC_ALPHABET, + randomFloat, + randomInt, + randomItem, +} from './random.js' diff --git a/packages/utils/src/random.test.ts b/packages/utils/src/random.test.ts new file mode 100644 index 00000000000..abec7e640ae --- /dev/null +++ b/packages/utils/src/random.test.ts @@ -0,0 +1,47 @@ +/** + * @vitest-environment node + */ +import { describe, expect, it } from 'vitest' +import { + generateRandomBytes, + generateRandomHex, + generateRandomString, + randomFloat, + randomInt, + randomItem, +} from './random.js' + +describe('random utilities', () => { + it('returns random bytes with the requested length', () => { + expect(generateRandomBytes(16)).toHaveLength(16) + }) + + it('chunks random byte requests over the Web Crypto per-call limit', () => { + expect(generateRandomBytes(70_000)).toHaveLength(70_000) + }) + + it('returns lowercase hex with two chars per byte', () => { + expect(generateRandomHex(8)).toMatch(/^[0-9a-f]{16}$/) + }) + + it('returns random strings with custom alphabets', () => { + expect(generateRandomString(12, 'abc')).toMatch(/^[abc]{12}$/) + }) + + it('returns floats in [0, 1)', () => { + const value = randomFloat() + expect(value).toBeGreaterThanOrEqual(0) + expect(value).toBeLessThan(1) + }) + + it('returns integers in [0, maxExclusive)', () => { + const value = randomInt(10) + expect(value).toBeGreaterThanOrEqual(0) + expect(value).toBeLessThan(10) + expect(Number.isInteger(value)).toBe(true) + }) + + it('selects an item from an array', () => { + expect(['a', 'b', 'c']).toContain(randomItem(['a', 'b', 'c'])) + }) +}) diff --git a/packages/utils/src/random.ts b/packages/utils/src/random.ts new file mode 100644 index 00000000000..be8214eb528 --- /dev/null +++ b/packages/utils/src/random.ts @@ -0,0 +1,119 @@ +export const LOWERCASE_ALPHANUMERIC_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz' + +const DEFAULT_ALPHABET = 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict' +const MAX_RANDOM_VALUES_BYTES = 65_536 +const UINT32_MAX_PLUS_ONE = 0x1_0000_0000 + +type RandomValuesArray = Uint8Array | Uint32Array +type RandomSource = { + getRandomValues(array: T): T +} +type MaybeRandomSource = { + getRandomValues?: RandomSource['getRandomValues'] +} + +function getCrypto(): RandomSource { + const cryptoProvider = (globalThis as typeof globalThis & { crypto?: MaybeRandomSource }).crypto + if (typeof cryptoProvider?.getRandomValues === 'function') { + return cryptoProvider as RandomSource + } + + throw new Error('crypto.getRandomValues is unavailable in this runtime') +} + +/** + * Returns cryptographically secure random bytes using Web Crypto. + * + * `crypto.getRandomValues()` works in browsers even outside secure contexts, + * unlike `crypto.randomUUID()`, and is also available in supported Node runtimes. + */ +export function generateRandomBytes(size: number): Uint8Array { + if (!Number.isSafeInteger(size) || size < 0) { + throw new Error('generateRandomBytes size must be a non-negative safe integer') + } + + const cryptoProvider = getCrypto() + const bytes = new Uint8Array(size) + for (let offset = 0; offset < size; offset += MAX_RANDOM_VALUES_BYTES) { + cryptoProvider.getRandomValues(bytes.subarray(offset, offset + MAX_RANDOM_VALUES_BYTES)) + } + return bytes +} + +/** + * Returns cryptographically secure random bytes encoded as lowercase hex. + */ +export function generateRandomHex(byteLength: number): string { + return Array.from(generateRandomBytes(byteLength), (byte) => + byte.toString(16).padStart(2, '0') + ).join('') +} + +/** + * Generates a URL-safe random string from the provided alphabet. + */ +export function generateRandomString(size = 21, alphabet = DEFAULT_ALPHABET): string { + const alphabetLength = alphabet.length + if (!Number.isSafeInteger(size) || size < 0) { + throw new Error('generateRandomString size must be a non-negative safe integer') + } + if (alphabetLength < 2 || alphabetLength > 256) { + throw new Error('generateRandomString alphabet length must be between 2 and 256') + } + + const mask = (2 << (31 - Math.clz32((alphabetLength - 1) | 1))) - 1 + const step = Math.ceil((1.6 * mask * size) / alphabetLength) + + let id = '' + while (id.length < size) { + const bytes = generateRandomBytes(step) + for (let i = 0; i < step && id.length < size; i++) { + const index = bytes[i] & mask + if (index < alphabetLength) { + id += alphabet[index] + } + } + } + + return id +} + +/** + * Returns a cryptographically secure floating point value in [0, 1). + */ +export function randomFloat(): number { + const value = new Uint32Array(1) + getCrypto().getRandomValues(value) + return value[0] / UINT32_MAX_PLUS_ONE +} + +/** + * Returns a cryptographically secure integer in [0, maxExclusive). + */ +export function randomInt(maxExclusive: number): number { + if (!Number.isSafeInteger(maxExclusive) || maxExclusive <= 0) { + throw new Error('randomInt maxExclusive must be a positive safe integer') + } + if (maxExclusive > UINT32_MAX_PLUS_ONE) { + throw new Error('randomInt maxExclusive must be at most 2^32') + } + + const maxUnbiased = Math.floor(UINT32_MAX_PLUS_ONE / maxExclusive) * maxExclusive + const value = new Uint32Array(1) + + do { + getCrypto().getRandomValues(value) + } while (value[0] >= maxUnbiased) + + return value[0] % maxExclusive +} + +/** + * Selects a random item from a non-empty array. + */ +export function randomItem(items: readonly T[]): T { + if (items.length === 0) { + throw new Error('randomItem requires a non-empty array') + } + return items[randomInt(items.length)] +} diff --git a/scripts/check-utils-enforcement.ts b/scripts/check-utils-enforcement.ts new file mode 100644 index 00000000000..df8a2f81a7b --- /dev/null +++ b/scripts/check-utils-enforcement.ts @@ -0,0 +1,123 @@ +#!/usr/bin/env bun +/** + * Enforces use of shared @sim/utils/random and @sim/utils/id helpers. + * + * Biome's noRestrictedImports covers import-based bans (nanoid, uuid, crypto named imports). + * This script catches global property access patterns that static import analysis misses: + * - Math.random → randomInt / randomFloat / randomItem from @sim/utils/random + * - crypto.randomUUID → generateId / generateShortId from @sim/utils/id + * - crypto.randomBytes → generateRandomBytes / generateRandomHex from @sim/utils/random + */ +import { readdir, readFile } from 'node:fs/promises' +import path from 'node:path' + +const ROOT = path.resolve(import.meta.dir, '..') + +const SCAN_DIRS = [path.join(ROOT, 'apps'), path.join(ROOT, 'packages')] + +const SKIP_DIRS = new Set(['node_modules', 'dist', '.next', '.turbo', 'coverage', 'bundles']) + +/** Files that implement the utilities themselves — allowed to use the underlying primitives. */ +const ALLOWLISTED_FILES = new Set([ + 'packages/utils/src/random.ts', + 'packages/utils/src/id.ts', + 'packages/utils/src/random.test.ts', + 'packages/utils/src/id.test.ts', +]) + +const BANNED_PATTERNS: Array<{ + pattern: RegExp + description: string + suggestion: string +}> = [ + { + pattern: /\bMath\.random\s*\(/g, + description: 'Math.random()', + suggestion: 'randomInt / randomFloat / randomItem from @sim/utils/random', + }, + { + pattern: /\bcrypto\.randomUUID\s*\(/g, + description: 'crypto.randomUUID()', + suggestion: 'generateId() or generateShortId() from @sim/utils/id', + }, + { + pattern: /\bcrypto\.randomBytes\s*\(/g, + description: 'crypto.randomBytes()', + suggestion: 'generateRandomBytes() or generateRandomHex() from @sim/utils/random', + }, +] + +async function walk(dir: string, results: string[] = []): Promise { + let entries + try { + entries = await readdir(dir, { withFileTypes: true }) + } catch { + return results + } + for (const entry of entries) { + if (SKIP_DIRS.has(entry.name)) continue + const full = path.join(dir, entry.name) + if (entry.isDirectory()) { + await walk(full, results) + } else if (/\.(ts|tsx|mts|cts|js|jsx|mjs|cjs)$/.test(entry.name)) { + results.push(full) + } + } + return results +} + +interface Violation { + file: string + line: number + description: string + suggestion: string + snippet: string +} + +async function main() { + const allFiles: string[] = [] + for (const dir of SCAN_DIRS) { + await walk(dir, allFiles) + } + + const violations: Violation[] = [] + + for (const file of allFiles) { + const rel = path.relative(ROOT, file) + if (ALLOWLISTED_FILES.has(rel)) continue + + const content = await readFile(file, 'utf8') + const lines = content.split('\n') + + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + for (const { pattern, description, suggestion } of BANNED_PATTERNS) { + pattern.lastIndex = 0 + if (pattern.test(line)) { + violations.push({ + file: rel, + line: i + 1, + description, + suggestion, + snippet: line.trim(), + }) + } + } + } + } + + if (violations.length === 0) { + console.log('✓ No banned randomness/ID patterns found.') + process.exit(0) + } + + console.error(`\nFound ${violations.length} banned pattern(s):\n`) + for (const v of violations) { + console.error(` ${v.file}:${v.line}`) + console.error(` ✗ ${v.description} → use ${v.suggestion}`) + console.error(` ${v.snippet}\n`) + } + process.exit(1) +} + +main() From 4ad2f310a2fbd7edf326b9c84e515a837d3ad742 Mon Sep 17 00:00:00 2001 From: waleed Date: Fri, 15 May 2026 16:14:58 -0700 Subject: [PATCH 2/2] chore: remove stray files --- 0 | 0 endTime | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 0 delete mode 100644 endTime diff --git a/0 b/0 deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/endTime b/endTime deleted file mode 100644 index e69de29bb2d..00000000000