diff --git a/apps/sim/app/api/auth/oauth/connections/route.ts b/apps/sim/app/api/auth/oauth/connections/route.ts
index 6bcb0c6b20..f436e1a146 100644
--- a/apps/sim/app/api/auth/oauth/connections/route.ts
+++ b/apps/sim/app/api/auth/oauth/connections/route.ts
@@ -3,6 +3,7 @@ import { jwtDecode } from 'jwt-decode'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
+import { generateServerUUID } from '@/lib/uuid'
import { db } from '@/db'
import { account, user } from '@/db/schema'
@@ -18,7 +19,7 @@ interface GoogleIdToken {
* Get all OAuth connections for the current user
*/
export async function GET(request: NextRequest) {
- const requestId = crypto.randomUUID().slice(0, 8)
+ const requestId = generateServerUUID().slice(0, 8)
try {
// Get the session
diff --git a/apps/sim/app/api/auth/oauth/credentials/route.ts b/apps/sim/app/api/auth/oauth/credentials/route.ts
index 9c942d8513..530f4aacda 100644
--- a/apps/sim/app/api/auth/oauth/credentials/route.ts
+++ b/apps/sim/app/api/auth/oauth/credentials/route.ts
@@ -3,6 +3,7 @@ import { jwtDecode } from 'jwt-decode'
import { type NextRequest, NextResponse } from 'next/server'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { createLogger } from '@/lib/logs/console/logger'
+import { generateSecureUUID } from '@/lib/uuid'
import type { OAuthService } from '@/lib/oauth/oauth'
import { parseProvider } from '@/lib/oauth/oauth'
import { getUserEntityPermissions } from '@/lib/permissions/utils'
@@ -23,7 +24,7 @@ interface GoogleIdToken {
* Get credentials for a specific provider
*/
export async function GET(request: NextRequest) {
- const requestId = crypto.randomUUID().slice(0, 8)
+ const requestId = generateSecureUUID().slice(0, 8)
try {
// Get query params
diff --git a/apps/sim/app/api/auth/oauth/disconnect/route.ts b/apps/sim/app/api/auth/oauth/disconnect/route.ts
index c2fc3cf01c..af0f9715b9 100644
--- a/apps/sim/app/api/auth/oauth/disconnect/route.ts
+++ b/apps/sim/app/api/auth/oauth/disconnect/route.ts
@@ -2,6 +2,7 @@ import { and, eq, like, or } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
+import { generateServerUUID } from '@/lib/uuid'
import { db } from '@/db'
import { account } from '@/db/schema'
@@ -13,7 +14,7 @@ const logger = createLogger('OAuthDisconnectAPI')
* Disconnect an OAuth provider for the current user
*/
export async function POST(request: NextRequest) {
- const requestId = crypto.randomUUID().slice(0, 8)
+ const requestId = generateServerUUID().slice(0, 8)
try {
// Get the session
diff --git a/apps/sim/app/api/auth/oauth/token/route.ts b/apps/sim/app/api/auth/oauth/token/route.ts
index f6416ef010..38f5ae123d 100644
--- a/apps/sim/app/api/auth/oauth/token/route.ts
+++ b/apps/sim/app/api/auth/oauth/token/route.ts
@@ -2,6 +2,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
import { checkHybridAuth } from '@/lib/auth/hybrid'
import { createLogger } from '@/lib/logs/console/logger'
+import { generateSecureUUID } from '@/lib/uuid'
import { getCredential, refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
export const dynamic = 'force-dynamic'
@@ -14,7 +15,7 @@ const logger = createLogger('OAuthTokenAPI')
* and workflow-based authentication (for server-side requests)
*/
export async function POST(request: NextRequest) {
- const requestId = crypto.randomUUID().slice(0, 8)
+ const requestId = generateSecureUUID().slice(0, 8)
logger.info(`[${requestId}] OAuth token API POST request received`)
@@ -59,7 +60,7 @@ export async function POST(request: NextRequest) {
* Get the access token for a specific credential
*/
export async function GET(request: NextRequest) {
- const requestId = crypto.randomUUID().slice(0, 8) // Short request ID for correlation
+ const requestId = generateSecureUUID().slice(0, 8) // Short request ID for correlation
try {
// Get the credential ID from the query params
diff --git a/apps/sim/app/api/tools/postgresql/query/route.ts b/apps/sim/app/api/tools/postgresql/query/route.ts
index 135b044b65..ec6cfd5b7a 100644
--- a/apps/sim/app/api/tools/postgresql/query/route.ts
+++ b/apps/sim/app/api/tools/postgresql/query/route.ts
@@ -2,7 +2,7 @@ import { randomUUID } from 'crypto'
import { type NextRequest, NextResponse } from 'next/server'
import { z } from 'zod'
import { createLogger } from '@/lib/logs/console/logger'
-import { createPostgresConnection, executeQuery } from '@/app/api/tools/postgresql/utils'
+import { createPostgresConnection, executeQuery, validateQuery } from '@/app/api/tools/postgresql/utils'
const logger = createLogger('PostgreSQLQueryAPI')
@@ -27,6 +27,16 @@ export async function POST(request: NextRequest) {
`[${requestId}] Executing PostgreSQL query on ${params.host}:${params.port}/${params.database}`
)
+ // Validate query for security
+ const queryValidation = validateQuery(params.query)
+ if (!queryValidation.isValid) {
+ logger.warn(`[${requestId}] Query validation failed: ${queryValidation.error}`)
+ return NextResponse.json(
+ { error: `Query validation failed: ${queryValidation.error}` },
+ { status: 400 }
+ )
+ }
+
const sql = createPostgresConnection({
host: params.host,
port: params.port,
diff --git a/apps/sim/app/api/users/me/api-keys/[id]/route.ts b/apps/sim/app/api/users/me/api-keys/[id]/route.ts
index 800fcf3853..23e4787bf5 100644
--- a/apps/sim/app/api/users/me/api-keys/[id]/route.ts
+++ b/apps/sim/app/api/users/me/api-keys/[id]/route.ts
@@ -2,6 +2,7 @@ import { and, eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { getSession } from '@/lib/auth'
import { createLogger } from '@/lib/logs/console/logger'
+import { generateSecureUUID } from '@/lib/uuid'
import { db } from '@/db'
import { apiKey } from '@/db/schema'
@@ -12,7 +13,7 @@ export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
- const requestId = crypto.randomUUID().slice(0, 8)
+ const requestId = generateSecureUUID().slice(0, 8)
const { id } = await params
try {
diff --git a/apps/sim/app/global-error.tsx b/apps/sim/app/global-error.tsx
index 17deeb509d..334341f931 100644
--- a/apps/sim/app/global-error.tsx
+++ b/apps/sim/app/global-error.tsx
@@ -3,20 +3,105 @@
import { useEffect } from 'react'
import * as Sentry from '@sentry/nextjs'
import NextError from 'next/error'
+import { createLogger } from '@/lib/logs/console/logger'
-export default function GlobalError({ error }: { error: Error & { digest?: string } }) {
+const logger = createLogger('GlobalError')
+
+export default function GlobalError({
+ error,
+ reset
+}: {
+ error: Error & { digest?: string }
+ reset?: () => void
+}) {
useEffect(() => {
+ // Enhanced error logging for debugging
+ logger.error('Global error occurred:', {
+ message: error.message,
+ name: error.name,
+ stack: error.stack,
+ digest: error.digest,
+ // Additional context for crypto-related errors
+ userAgent: typeof navigator !== 'undefined' ? navigator.userAgent : 'N/A',
+ isSecureContext: typeof window !== 'undefined' ? window.isSecureContext : 'N/A',
+ location: typeof window !== 'undefined' ? window.location.href : 'N/A',
+ })
+
+ // Check if this is a crypto-related error and provide specific guidance
+ if (error.message?.includes('randomUUID') || error.message?.includes('crypto')) {
+ logger.warn('Crypto API error detected. This may be due to insecure context (HTTP instead of HTTPS)')
+ }
+
Sentry.captureException(error)
}, [error])
return (
- {/* `NextError` is the default Next.js error page component. Its type
- definition requires a `statusCode` prop. However, since the App Router
- does not expose status codes for errors, we simply pass 0 to render a
- generic error message. */}
-
+
+
+ Application Error
+
+
+ A client-side exception has occurred. This error has been logged for investigation.
+
+ {error.message?.includes('randomUUID') || error.message?.includes('crypto') ? (
+
+
Tip: This error may be resolved by:
+
+ Accessing the application via HTTPS
+ Using localhost instead of other local IP addresses
+ Checking your browser security settings
+
+
+ ) : null}
+ {reset && (
+
+ Try Again
+
+ )}
+
+ Error Details
+
+ {error.name}: {error.message}
+ {error.stack && `\n\nStack Trace:\n${error.stack}`}
+
+
+
)
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx
index f51523d0d1..1e002661b5 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx
@@ -13,6 +13,13 @@ import ReactFlow, {
} from 'reactflow'
import 'reactflow/dist/style.css'
import { createLogger } from '@/lib/logs/console/logger'
+import { generateUUID } from '@/lib/uuid'
+import {
+ hasValidBlockDragData,
+ extractBlockDragData,
+ logDragEvent,
+ type BlockDragData
+} from '@/lib/drag-drop-utils'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
import { ControlBar } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/control-bar/control-bar'
import { DiffControls } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/diff-controls'
@@ -483,7 +490,7 @@ const WorkflowContent = React.memo(() => {
// Special handling for container nodes (loop or parallel)
if (type === 'loop' || type === 'parallel') {
// Create a unique ID and name for the container
- const id = crypto.randomUUID()
+ const id = generateUUID()
// Auto-number the blocks based on existing blocks of the same type
const existingBlocksOfType = Object.values(blocks).filter((b) => b.type === type)
@@ -506,7 +513,7 @@ const WorkflowContent = React.memo(() => {
const sourceHandle = determineSourceHandle(closestBlock)
autoConnectEdge = {
- id: crypto.randomUUID(),
+ id: generateUUID(),
source: closestBlock.id,
target: id,
sourceHandle,
@@ -548,7 +555,7 @@ const WorkflowContent = React.memo(() => {
})
// Create a new block with a unique ID
- const id = crypto.randomUUID()
+ const id = generateUUID()
const name = `${blockConfig.name} ${Object.values(blocks).filter((b) => b.type === type).length + 1}`
// Auto-connect logic
@@ -562,7 +569,7 @@ const WorkflowContent = React.memo(() => {
const sourceHandle = determineSourceHandle(closestBlock)
autoConnectEdge = {
- id: crypto.randomUUID(),
+ id: generateUUID(),
source: closestBlock.id,
target: id,
sourceHandle,
@@ -599,9 +606,26 @@ const WorkflowContent = React.memo(() => {
const onDrop = useCallback(
(event: React.DragEvent) => {
event.preventDefault()
+ logDragEvent('onDrop', event)
try {
- const data = JSON.parse(event.dataTransfer.getData('application/json'))
+ // Use robust data extraction with fallback MIME types
+ const extractionResult = extractBlockDragData(event)
+
+ if (!extractionResult.success) {
+ logger.warn('Failed to extract block drag data:', {
+ error: extractionResult.error,
+ availableTypes: Array.from(event.dataTransfer?.types || [])
+ })
+ return
+ }
+
+ const data = extractionResult.data!
+ logger.debug('Block drop successful:', {
+ blockType: data.type,
+ mimeTypeUsed: extractionResult.mimeTypeUsed
+ })
+
if (data.type === 'connectionBlock') return
const reactFlowBounds = event.currentTarget.getBoundingClientRect()
@@ -624,7 +648,7 @@ const WorkflowContent = React.memo(() => {
// Special handling for container nodes (loop or parallel)
if (data.type === 'loop' || data.type === 'parallel') {
// Create a unique ID and name for the container
- const id = crypto.randomUUID()
+ const id = generateUUID()
// Auto-number the blocks based on existing blocks of the same type
const existingBlocksOfType = Object.values(blocks).filter((b) => b.type === data.type)
@@ -660,7 +684,7 @@ const WorkflowContent = React.memo(() => {
const sourceHandle = determineSourceHandle(closestBlock)
autoConnectEdge = {
- id: crypto.randomUUID(),
+ id: generateUUID(),
source: closestBlock.id,
target: id,
sourceHandle,
@@ -697,7 +721,7 @@ const WorkflowContent = React.memo(() => {
}
// Generate id and name here so they're available in all code paths
- const id = crypto.randomUUID()
+ const id = generateUUID()
const name =
data.type === 'loop'
? `Loop ${Object.values(blocks).filter((b) => b.type === 'loop').length + 1}`
@@ -748,7 +772,7 @@ const WorkflowContent = React.memo(() => {
type: closestBlock.type,
})
addEdge({
- id: crypto.randomUUID(),
+ id: generateUUID(),
source: closestBlock.id,
target: id,
sourceHandle,
@@ -765,7 +789,7 @@ const WorkflowContent = React.memo(() => {
: 'parallel-start-source'
addEdge({
- id: crypto.randomUUID(),
+ id: generateUUID(),
source: containerInfo.loopId,
target: id,
sourceHandle: startSourceHandle,
@@ -784,7 +808,7 @@ const WorkflowContent = React.memo(() => {
const sourceHandle = determineSourceHandle(closestBlock)
autoConnectEdge = {
- id: crypto.randomUUID(),
+ id: generateUUID(),
source: closestBlock.id,
target: id,
sourceHandle,
@@ -818,8 +842,11 @@ const WorkflowContent = React.memo(() => {
(event: React.DragEvent) => {
event.preventDefault()
- // Only handle toolbar items
- if (!event.dataTransfer?.types.includes('application/json')) return
+ // Only handle valid block drag data with fallback MIME type support
+ if (!hasValidBlockDragData(event)) {
+ logDragEvent('onDragOver-rejected', event)
+ return
+ }
try {
const reactFlowBounds = event.currentTarget.getBoundingClientRect()
@@ -1125,7 +1152,7 @@ const WorkflowContent = React.memo(() => {
const targetParentId = targetNode.parentId
// Generate a unique edge ID
- const edgeId = crypto.randomUUID()
+ const edgeId = generateUUID()
// Special case for container start source: Always allow connections to nodes within the same container
if (
@@ -1438,7 +1465,7 @@ const WorkflowContent = React.memo(() => {
type: closestBlock.type,
})
addEdge({
- id: crypto.randomUUID(),
+ id: generateUUID(),
source: closestBlock.id,
target: node.id,
sourceHandle,
@@ -1455,7 +1482,7 @@ const WorkflowContent = React.memo(() => {
: 'parallel-start-source'
addEdge({
- id: crypto.randomUUID(),
+ id: generateUUID(),
source: potentialParentId,
target: node.id,
sourceHandle: startSourceHandle,
diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/toolbar/components/toolbar-block/toolbar-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/toolbar/components/toolbar-block/toolbar-block.tsx
index 98a9f2fdf0..d7898dcedf 100644
--- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/toolbar/components/toolbar-block/toolbar-block.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/toolbar/components/toolbar-block/toolbar-block.tsx
@@ -2,6 +2,7 @@ import { useCallback } from 'react'
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { cn } from '@/lib/utils'
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
+import { setBlockDragData, logDragEvent } from '@/lib/drag-drop-utils'
import type { BlockConfig } from '@/blocks/types'
export type ToolbarBlockProps = {
@@ -17,8 +18,10 @@ export function ToolbarBlock({ config, disabled = false }: ToolbarBlockProps) {
e.preventDefault()
return
}
- e.dataTransfer.setData('application/json', JSON.stringify({ type: config.type }))
- e.dataTransfer.effectAllowed = 'move'
+
+ // Use robust drag data setting with multiple MIME type fallbacks
+ setBlockDragData(e.dataTransfer, { type: config.type })
+ logDragEvent('handleDragStart', e)
}
// Handle click to add block
diff --git a/apps/sim/lib/drag-drop-utils.ts b/apps/sim/lib/drag-drop-utils.ts
new file mode 100644
index 0000000000..361a6ae514
--- /dev/null
+++ b/apps/sim/lib/drag-drop-utils.ts
@@ -0,0 +1,214 @@
+/**
+ * Drag and drop utility functions for handling MIME type variations
+ * Addresses issues with Cloudflare Tunnel normalizing MIME types
+ */
+
+import { createLogger } from '@/lib/logs/console/logger'
+
+const logger = createLogger('DragDropUtils')
+
+/**
+ * MIME types to try when extracting drag data, in order of preference
+ */
+const DRAG_DATA_MIME_TYPES = [
+ 'application/sim-block', // Custom MIME type for blocks
+ 'application/json', // Standard JSON MIME type
+ 'text/plain', // Fallback for text data
+ 'text/json', // Alternative JSON MIME type
+] as const
+
+/**
+ * Supported block types for validation
+ */
+export const SUPPORTED_BLOCK_TYPES = [
+ 'agent',
+ 'knowledge',
+ 'loop',
+ 'parallel',
+ 'connectionBlock',
+ // Add other supported block types as needed
+] as const
+
+export type SupportedBlockType = typeof SUPPORTED_BLOCK_TYPES[number]
+
+/**
+ * Structure of drag data for blocks
+ */
+export interface BlockDragData {
+ type: SupportedBlockType
+ // Allow additional properties but with more specific typing
+ metadata?: Record
+ config?: Record
+}
+
+/**
+ * Result of drag data extraction
+ */
+export interface DragDataResult {
+ success: boolean
+ data?: BlockDragData
+ error?: string
+ mimeTypeUsed?: string
+}
+
+/**
+ * Check if drag event contains valid block data using multiple MIME type fallbacks
+ * This addresses issues where Cloudflare Tunnel normalizes MIME types
+ */
+export function hasValidBlockDragData(event: React.DragEvent): boolean {
+ const types = Array.from(event.dataTransfer?.types || [])
+
+ logger.debug('Drag event MIME types detected:', { types })
+
+ // Check if any of our supported MIME types are present
+ const hasValidMimeType = DRAG_DATA_MIME_TYPES.some(mimeType => types.includes(mimeType))
+
+ if (!hasValidMimeType) {
+ logger.debug('No valid MIME types found for block drag data')
+ return false
+ }
+
+ // Try to extract and validate data
+ const result = extractBlockDragData(event)
+ return result.success
+}
+
+/**
+ * Extract block drag data using multiple MIME type fallbacks
+ * Handles cases where Cloudflare Tunnel or other proxies normalize MIME types
+ */
+export function extractBlockDragData(event: React.DragEvent): DragDataResult {
+ if (!event.dataTransfer) {
+ return {
+ success: false,
+ error: 'No dataTransfer available'
+ }
+ }
+
+ const availableTypes = Array.from(event.dataTransfer.types)
+ logger.debug('Attempting to extract block data from types:', { availableTypes })
+
+ // Try each MIME type in order of preference
+ for (const mimeType of DRAG_DATA_MIME_TYPES) {
+ if (availableTypes.includes(mimeType)) {
+ try {
+ const rawData = event.dataTransfer.getData(mimeType)
+ logger.debug(`Extracted raw data using ${mimeType}:`, { rawData })
+
+ if (!rawData) {
+ logger.debug(`No data available for MIME type: ${mimeType}`)
+ continue
+ }
+
+ // Try to parse as JSON with better type safety
+ let parsedData: unknown
+ try {
+ parsedData = JSON.parse(rawData)
+ } catch (parseError) {
+ // If JSON parsing fails, try to use as-is if it's a simple string
+ if (typeof rawData === 'string' && rawData.trim()) {
+ // Attempt to create a simple block data structure
+ parsedData = { type: rawData.trim() } as BlockDragData
+ } else {
+ logger.debug(`Failed to parse data for ${mimeType}:`, { parseError })
+ continue
+ }
+ }
+
+ // Validate the parsed data structure
+ if (isValidBlockDragData(parsedData)) {
+ logger.debug(`Successfully extracted block data using ${mimeType}:`, { parsedData })
+ return {
+ success: true,
+ data: parsedData,
+ mimeTypeUsed: mimeType
+ }
+ } else {
+ logger.debug(`Invalid block data structure for ${mimeType}:`, { parsedData })
+ }
+
+ } catch (error) {
+ logger.debug(`Error extracting data for ${mimeType}:`, { error })
+ continue
+ }
+ }
+ }
+
+ return {
+ success: false,
+ error: 'No valid block data found in any supported MIME type',
+ mimeTypeUsed: undefined
+ }
+}
+
+/**
+ * Validate that the extracted data has the correct structure for a block
+ */
+function isValidBlockDragData(data: any): data is BlockDragData {
+ if (!data || typeof data !== 'object') {
+ return false
+ }
+
+ if (!data.type || typeof data.type !== 'string') {
+ return false
+ }
+
+ // Check if it's a supported block type (or allow any string for flexibility)
+ // This can be made stricter if needed
+ return data.type.trim().length > 0
+}
+
+/**
+ * Set drag data with multiple MIME type fallbacks for better compatibility
+ */
+export function setBlockDragData(dataTransfer: DataTransfer, blockData: BlockDragData): void {
+ const jsonData = JSON.stringify(blockData)
+
+ // Set data for multiple MIME types to ensure compatibility
+ try {
+ dataTransfer.setData('application/sim-block', jsonData)
+ dataTransfer.setData('application/json', jsonData)
+ dataTransfer.setData('text/json', jsonData)
+ dataTransfer.setData('text/plain', jsonData)
+
+ logger.debug('Block drag data set with multiple MIME types:', { blockData })
+ } catch (error) {
+ logger.error('Failed to set block drag data:', { error })
+ }
+
+ dataTransfer.effectAllowed = 'move'
+}
+
+/**
+ * Enhanced logging for debugging drag and drop issues
+ * Only logs in development or when debug flag is enabled
+ */
+export function logDragEvent(eventType: string, event: React.DragEvent): void {
+ // Skip logging in production unless debug flag is set
+ if (process.env.NODE_ENV === 'production' && !process.env.DRAG_DEBUG) {
+ return
+ }
+
+ if (!event.dataTransfer) return
+
+ const types = Array.from(event.dataTransfer.types)
+ const debugInfo = {
+ eventType,
+ availableTypes: types,
+ effectAllowed: event.dataTransfer.effectAllowed,
+ dropEffect: event.dataTransfer.dropEffect,
+ }
+
+ // Try to extract data for debugging (non-destructive, limited)
+ const dataPreview: Record = {}
+ for (const type of types.slice(0, 2)) { // Reduced to 2 types for performance
+ try {
+ const data = event.dataTransfer.getData(type)
+ dataPreview[type] = data ? data.substring(0, 50) + (data.length > 50 ? '...' : '') : '(empty)'
+ } catch (error) {
+ dataPreview[type] = `(error)`
+ }
+ }
+
+ logger.debug('Drag event debug info:', { ...debugInfo, dataPreview })
+}
\ No newline at end of file
diff --git a/apps/sim/lib/uuid.ts b/apps/sim/lib/uuid.ts
new file mode 100644
index 0000000000..5533b176cc
--- /dev/null
+++ b/apps/sim/lib/uuid.ts
@@ -0,0 +1,156 @@
+/**
+ * UUID utility that works in both secure and non-secure contexts
+ * Addresses the crypto.randomUUID() issue in insecure HTTP contexts
+ *
+ * SECURITY NOTE: The fallback Math.random() UUID is cryptographically weak
+ * and should not be used for security-sensitive operations (tokens, secrets, etc.).
+ * It is suitable for client-side UI state management, temporary IDs, and similar uses.
+ */
+
+/**
+ * Fallback UUID v4 generator using Math.random()
+ * This provides a cryptographically weak but acceptable UUID
+ * when crypto.randomUUID() is not available (insecure contexts)
+ */
+function fallbackUUIDv4(): string {
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
+ const r = (Math.random() * 16) | 0
+ const v = c === 'x' ? r : (r & 0x3) | 0x8
+ return v.toString(16)
+ })
+}
+
+/**
+ * Check if we're running in a secure context where crypto.randomUUID() is available
+ * Defaults to true for older browsers that don't support window.isSecureContext
+ */
+function isSecureContext(): boolean {
+ return (
+ typeof window !== 'undefined' &&
+ (window.isSecureContext === undefined || window.isSecureContext) && // Default to true for older browsers
+ typeof crypto !== 'undefined' &&
+ typeof crypto.randomUUID === 'function'
+ )
+}
+
+/**
+ * Generate a UUID that works in both secure and insecure contexts
+ * - Uses crypto.randomUUID() in secure contexts (HTTPS, localhost)
+ * - Falls back to Math.random() based UUID in insecure contexts
+ *
+ * @returns A UUID v4 string
+ */
+export function generateUUID(): string {
+ try {
+ // Try to use crypto.randomUUID() first (secure context)
+ if (isSecureContext()) {
+ return crypto.randomUUID()
+ }
+ } catch (error) {
+ // crypto.randomUUID() not available or threw an error
+ console.warn('crypto.randomUUID() not available, falling back to Math.random() UUID generation')
+ }
+
+ // Fallback for insecure contexts or when crypto.randomUUID() is not available
+ return fallbackUUIDv4()
+}
+
+/**
+ * Server-side UUID generation using Node.js crypto module
+ * This is always secure and should be used for server-side code
+ */
+export function generateServerUUID(): string {
+ // This will use Node.js crypto.randomUUID() on the server
+ if (typeof globalThis !== 'undefined' && globalThis.crypto?.randomUUID) {
+ return globalThis.crypto.randomUUID()
+ }
+
+ // Fallback to Node.js crypto module if available
+ try {
+ const { randomUUID } = require('crypto')
+ return randomUUID()
+ } catch (error) {
+ console.warn('Node.js crypto module not available, using fallback UUID generation')
+ return fallbackUUIDv4()
+ }
+}
+
+/**
+ * Generate a cryptographically secure UUID for security-sensitive operations
+ * This function enforces secure UUID generation and throws an error if
+ * secure generation is not available.
+ *
+ * Use this for: tokens, secrets, API keys, session IDs, etc.
+ *
+ * @throws {Error} If secure UUID generation is not available
+ */
+export function generateSecureUUID(): string {
+ if (typeof window === 'undefined') {
+ // Server-side - always secure
+ return generateServerUUID()
+ }
+
+ // Client-side - must be secure context
+ if (!isSecureContext()) {
+ throw new Error('Secure UUID generation not available in insecure context. Use generateUUID() for non-security-sensitive operations.')
+ }
+
+ try {
+ return crypto.randomUUID()
+ } catch (error) {
+ throw new Error('Failed to generate secure UUID: crypto.randomUUID() not available')
+ }
+}
+
+/**
+ * Context-aware UUID generation
+ * - Uses server-side crypto on the server
+ * - Uses client-side crypto or fallback on the client
+ */
+export function generateContextAwareUUID(): string {
+ if (typeof window === 'undefined') {
+ // Server-side
+ return generateServerUUID()
+ } else {
+ // Client-side
+ return generateUUID()
+ }
+}
+
+/**
+ * Validate that we're not using a fallback UUID for security-sensitive operations
+ * This helps prevent accidental use of weak UUIDs in production
+ */
+export function validateSecureUUIDCapability(): { isSecure: boolean; reason?: string } {
+ if (typeof window === 'undefined') {
+ // Server-side - check Node.js crypto
+ try {
+ if (typeof globalThis !== 'undefined' && globalThis.crypto?.randomUUID) {
+ return { isSecure: true }
+ }
+
+ const { randomUUID } = require('crypto')
+ if (typeof randomUUID === 'function') {
+ return { isSecure: true }
+ }
+
+ return { isSecure: false, reason: 'Node.js crypto module or randomUUID function not available' }
+ } catch (error) {
+ return { isSecure: false, reason: 'Node.js crypto module not available' }
+ }
+ }
+
+ // Client-side
+ if (!isSecureContext()) {
+ return { isSecure: false, reason: 'Not running in a secure context (HTTPS required)' }
+ }
+
+ if (typeof crypto === 'undefined' || typeof crypto.randomUUID !== 'function') {
+ return { isSecure: false, reason: 'crypto.randomUUID() not available in this browser' }
+ }
+
+ return { isSecure: true }
+}
+
+// Default export for easy migration
+export default generateContextAwareUUID
\ No newline at end of file
diff --git a/apps/sim/stores/copilot/store.ts b/apps/sim/stores/copilot/store.ts
index 3fc7553bf6..e4019a7261 100644
--- a/apps/sim/stores/copilot/store.ts
+++ b/apps/sim/stores/copilot/store.ts
@@ -3,6 +3,7 @@
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { type CopilotChat, sendStreamingMessage } from '@/lib/copilot/api'
+import { generateUUID } from '@/lib/uuid'
import type {
BaseClientToolMetadata,
ClientToolDisplay,
@@ -425,7 +426,7 @@ function createUserMessage(
contexts?: ChatContext[]
): CopilotMessage {
return {
- id: crypto.randomUUID(),
+ id: generateUUID(),
role: 'user',
content,
timestamp: new Date().toISOString(),
@@ -442,7 +443,7 @@ function createUserMessage(
function createStreamingMessage(): CopilotMessage {
return {
- id: crypto.randomUUID(),
+ id: generateUUID(),
role: 'assistant',
content: '',
timestamp: new Date().toISOString(),
diff --git a/apps/sim/stores/custom-tools/store.ts b/apps/sim/stores/custom-tools/store.ts
index 318c57e48e..aefb91c298 100644
--- a/apps/sim/stores/custom-tools/store.ts
+++ b/apps/sim/stores/custom-tools/store.ts
@@ -1,6 +1,7 @@
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
import { createLogger } from '@/lib/logs/console/logger'
+import { generateUUID } from '@/lib/uuid'
import type { CustomToolsStore } from '@/stores/custom-tools/types'
const logger = createLogger('CustomToolsStore')
@@ -130,7 +131,7 @@ export const useCustomToolsStore = create()(
},
addTool: (tool) => {
- const id = crypto.randomUUID()
+ const id = generateUUID()
const newTool = {
...tool,
id,
diff --git a/apps/sim/stores/panel/chat/store.ts b/apps/sim/stores/panel/chat/store.ts
index 519b256d5e..9f2f440be3 100644
--- a/apps/sim/stores/panel/chat/store.ts
+++ b/apps/sim/stores/panel/chat/store.ts
@@ -1,6 +1,6 @@
-import { v4 as uuidv4 } from 'uuid'
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
+import { generateUUID } from '@/lib/uuid'
import type { ChatMessage, ChatStore } from '@/stores/panel/chat/types'
// MAX across all workflows
@@ -19,7 +19,7 @@ export const useChatStore = create()(
const newMessage: ChatMessage = {
...message,
// Preserve provided id and timestamp if they exist; otherwise generate new ones
- id: (message as any).id ?? crypto.randomUUID(),
+ id: (message as any).id ?? generateUUID(),
timestamp: (message as any).timestamp ?? new Date().toISOString(),
}
@@ -41,7 +41,7 @@ export const useChatStore = create()(
// Generate a new conversationId when clearing chat for a specific workflow
if (workflowId) {
const newConversationIds = { ...state.conversationIds }
- newConversationIds[workflowId] = uuidv4()
+ newConversationIds[workflowId] = generateUUID()
return {
...newState,
conversationIds: newConversationIds,
@@ -167,7 +167,7 @@ export const useChatStore = create()(
},
generateNewConversationId: (workflowId) => {
- const newId = uuidv4()
+ const newId = generateUUID()
set((state) => {
const newConversationIds = { ...state.conversationIds }
newConversationIds[workflowId] = newId
diff --git a/apps/sim/stores/panel/console/store.ts b/apps/sim/stores/panel/console/store.ts
index a94493a9c6..bfe25256a2 100644
--- a/apps/sim/stores/panel/console/store.ts
+++ b/apps/sim/stores/panel/console/store.ts
@@ -1,6 +1,7 @@
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
import { redactApiKeys } from '@/lib/utils'
+import { generateUUID } from '@/lib/uuid'
import type { NormalizedBlockOutput } from '@/executor/types'
import type { ConsoleEntry, ConsoleStore } from '@/stores/panel/console/types'
@@ -162,7 +163,7 @@ export const useConsoleStore = create()(
// Create the new entry with ID and timestamp
const newEntry = {
...redactedEntry,
- id: crypto.randomUUID(),
+ id: generateUUID(),
timestamp: new Date().toISOString(),
}
diff --git a/apps/sim/stores/panel/variables/store.ts b/apps/sim/stores/panel/variables/store.ts
index 445bbe5b15..31fa44a513 100644
--- a/apps/sim/stores/panel/variables/store.ts
+++ b/apps/sim/stores/panel/variables/store.ts
@@ -1,6 +1,7 @@
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { createLogger } from '@/lib/logs/console/logger'
+import { generateUUID } from '@/lib/uuid'
import type { Variable, VariablesStore } from '@/stores/panel/variables/types'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
@@ -126,7 +127,7 @@ export const useVariablesStore = create()(
},
addVariable: (variable, providedId?: string) => {
- const id = providedId || crypto.randomUUID()
+ const id = providedId || generateUUID()
// Get variables for this workflow
const workflowVariables = get().getVariablesByWorkflowId(variable.workflowId)
@@ -327,7 +328,7 @@ export const useVariablesStore = create()(
if (!state.variables[id]) return ''
const variable = state.variables[id]
- const newId = providedId || crypto.randomUUID()
+ const newId = providedId || generateUUID()
// Ensure the duplicated name is unique
const workflowVariables = get().getVariablesByWorkflowId(variable.workflowId)
diff --git a/apps/sim/stores/workflows/registry/store.ts b/apps/sim/stores/workflows/registry/store.ts
index 56cf71f553..c0b09dbcec 100644
--- a/apps/sim/stores/workflows/registry/store.ts
+++ b/apps/sim/stores/workflows/registry/store.ts
@@ -2,6 +2,7 @@ import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { createLogger } from '@/lib/logs/console/logger'
import { generateCreativeWorkflowName } from '@/lib/naming'
+import { generateUUID } from '@/lib/uuid'
import { API_ENDPOINTS } from '@/stores/constants'
import { useVariablesStore } from '@/stores/panel/variables/store'
import type {
@@ -642,7 +643,7 @@ export const useWorkflowRegistry = create()(
logger.info(`Created workflow from marketplace: ${options.marketplaceId}`)
} else {
// Create starter block for new workflow
- const starterId = crypto.randomUUID()
+ const starterId = generateUUID()
const starterBlock = {
id: starterId,
type: 'starter' as const,
@@ -831,7 +832,7 @@ export const useWorkflowRegistry = create()(
state: any,
metadata: Partial
) => {
- const id = crypto.randomUUID()
+ const id = generateUUID()
// Generate workflow metadata with marketplace properties
const newWorkflow: WorkflowMetadata = {
@@ -1021,7 +1022,7 @@ export const useWorkflowRegistry = create()(
} else {
// Source is not active workflow, create with starter block for now
// In a future enhancement, we could fetch from DB
- const starterId = crypto.randomUUID()
+ const starterId = generateUUID()
const starterBlock = {
id: starterId,
type: 'starter' as const,
diff --git a/apps/sim/stores/workflows/subblock/store.ts b/apps/sim/stores/workflows/subblock/store.ts
index f4e49e85ee..b2b16b640d 100644
--- a/apps/sim/stores/workflows/subblock/store.ts
+++ b/apps/sim/stores/workflows/subblock/store.ts
@@ -1,6 +1,7 @@
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import type { SubBlockConfig } from '@/blocks/types'
+import { generateUUID } from '@/lib/uuid'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import type { SubBlockStore } from '@/stores/workflows/subblock/types'
@@ -39,14 +40,14 @@ export const useSubBlockStore = create()(
if (!row || typeof row !== 'object') {
console.warn('Fixing malformed table row:', row)
return {
- id: crypto.randomUUID(),
+ id: generateUUID(),
cells: { Key: '', Value: '' },
}
}
// Ensure row has an id
if (!row.id) {
- row.id = crypto.randomUUID()
+ row.id = generateUUID()
}
// Ensure row has cells object
diff --git a/apps/sim/stores/workflows/workflow/store.ts b/apps/sim/stores/workflows/workflow/store.ts
index b659545b99..3a4c60ef5d 100644
--- a/apps/sim/stores/workflows/workflow/store.ts
+++ b/apps/sim/stores/workflows/workflow/store.ts
@@ -2,6 +2,7 @@ import type { Edge } from 'reactflow'
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { createLogger } from '@/lib/logs/console/logger'
+import { generateUUID } from '@/lib/uuid'
import { getBlock } from '@/blocks'
import { resolveOutputType } from '@/blocks/utils'
import {
@@ -393,7 +394,7 @@ export const useWorkflowStore = create()(
}
const newEdge = {
- id: edge.id || crypto.randomUUID(),
+ id: edge.id || generateUUID(),
source: edge.source,
target: edge.target,
sourceHandle: edge.sourceHandle,
@@ -516,7 +517,7 @@ export const useWorkflowStore = create()(
const block = get().blocks[id]
if (!block) return
- const newId = crypto.randomUUID()
+ const newId = generateUUID()
const offsetPosition = {
x: block.position.x + 250,
y: block.position.y + 20,
diff --git a/docker-compose.local.yml b/docker-compose.local.yml
index 95a88c405a..c18d7b6523 100644
--- a/docker-compose.local.yml
+++ b/docker-compose.local.yml
@@ -8,7 +8,9 @@ services:
deploy:
resources:
limits:
- memory: 8G
+ memory: 4G
+ reservations:
+ memory: 2G
environment:
- DATABASE_URL=postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@db:5432/${POSTGRES_DB:-simstudio}
- BETTER_AUTH_URL=${NEXT_PUBLIC_APP_URL:-http://localhost:3000}
@@ -54,7 +56,9 @@ services:
deploy:
resources:
limits:
- memory: 8G
+ memory: 2G
+ reservations:
+ memory: 1G
healthcheck:
test: ['CMD', 'wget', '--spider', '--quiet', 'http://127.0.0.1:3002/health']
interval: 90s