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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .cursor/rules/global.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
3 changes: 2 additions & 1 deletion apps/realtime/src/database/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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,
Expand Down
5 changes: 3 additions & 2 deletions apps/realtime/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -95,7 +96,7 @@ describe('Socket Server Index Integration', () => {
})

beforeEach(async () => {
PORT = 3333 + Math.floor(Math.random() * 1000)
PORT = 3333 + randomInt(1000)

httpServer = createServer()

Expand All @@ -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()
Expand Down
1 change: 1 addition & 0 deletions apps/sim/app/api/chat/[identifier]/otp/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
})

Expand Down
4 changes: 2 additions & 2 deletions apps/sim/app/api/chat/[identifier]/otp/route.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/app/api/files/parse/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/app/api/tools/file/manage/route.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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(
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/app/api/tools/google_drive/upload/route.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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)

Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -30,7 +31,9 @@ function LineChartComponent({
series?: LineChartMultiSeries[]
}) {
const containerRef = useRef<HTMLDivElement | null>(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<number | null>(null)
const width = containerWidth ?? 0
const height = 166
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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'
Expand All @@ -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'
Expand Down Expand Up @@ -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'
Expand All @@ -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'
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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,
}))
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/background/workspace-notification-delivery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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)
}

Expand Down
3 changes: 2 additions & 1 deletion apps/sim/instrumentation-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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(
Expand Down
41 changes: 22 additions & 19 deletions apps/sim/lib/api-key/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
* - Edge cases
*/

import { randomBytes } from 'crypto'
import {
createEncryptedApiKey,
createLegacyApiKey,
Expand All @@ -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)

Expand Down
4 changes: 2 additions & 2 deletions apps/sim/lib/api-key/crypto.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => ({
Expand Down Expand Up @@ -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)
Expand Down
Loading
Loading