Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
97 changes: 91 additions & 6 deletions apps/sim/app/global-error.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<html lang='en'>
<body>
{/* `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. */}
<NextError statusCode={0} />
<div style={{
padding: '2rem',
textAlign: 'center',
fontFamily: 'system-ui, sans-serif',
minHeight: '100vh',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
backgroundColor: '#1a1a1a',
color: '#white'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

syntax: Color value should be 'white' not '#white' - the hash prefix is incorrect for named colors

Suggested change
color: '#white'
color: 'white'

}}>
<h1 style={{ color: '#ef4444', marginBottom: '1rem' }}>
Application Error
</h1>
<p style={{ marginBottom: '1rem', color: '#a3a3a3' }}>
A client-side exception has occurred. This error has been logged for investigation.
</p>
{error.message?.includes('randomUUID') || error.message?.includes('crypto') ? (
<div style={{
backgroundColor: '#374151',
padding: '1rem',
borderRadius: '0.5rem',
marginBottom: '1rem',
color: '#fbbf24'
}}>
<p><strong>Tip:</strong> This error may be resolved by:</p>
<ul style={{ textAlign: 'left', margin: '0.5rem 0' }}>
<li>Accessing the application via HTTPS</li>
<li>Using localhost instead of other local IP addresses</li>
<li>Checking your browser security settings</li>
</ul>
</div>
) : null}
{reset && (
<button
onClick={reset}
style={{
padding: '0.5rem 1rem',
backgroundColor: '#3b82f6',
color: 'white',
border: 'none',
borderRadius: '0.25rem',
cursor: 'pointer',
fontSize: '1rem'
}}
>
Try Again
</button>
)}
<details style={{ marginTop: '2rem', textAlign: 'left', maxWidth: '600px', margin: '2rem auto 0' }}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

style: The margin override ('2rem auto 0') will overwrite the marginTop property set on the same line, consider using separate properties

<summary style={{ cursor: 'pointer', marginBottom: '1rem' }}>Error Details</summary>
<pre style={{
backgroundColor: '#374151',
padding: '1rem',
borderRadius: '0.25rem',
overflow: 'auto',
fontSize: '0.8rem',
color: '#d1d5db'
}}>
{error.name}: {error.message}
{error.stack && `\n\nStack Trace:\n${error.stack}`}
</pre>
</details>
</div>
</body>
</html>
)
Expand Down
27 changes: 14 additions & 13 deletions apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import ReactFlow, {
} from 'reactflow'
import 'reactflow/dist/style.css'
import { createLogger } from '@/lib/logs/console/logger'
import { generateUUID } from '@/lib/uuid'
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'
Expand Down Expand Up @@ -483,7 +484,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)
Expand All @@ -506,7 +507,7 @@ const WorkflowContent = React.memo(() => {
const sourceHandle = determineSourceHandle(closestBlock)

autoConnectEdge = {
id: crypto.randomUUID(),
id: generateUUID(),
source: closestBlock.id,
target: id,
sourceHandle,
Expand Down Expand Up @@ -548,7 +549,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
Expand All @@ -562,7 +563,7 @@ const WorkflowContent = React.memo(() => {
const sourceHandle = determineSourceHandle(closestBlock)

autoConnectEdge = {
id: crypto.randomUUID(),
id: generateUUID(),
source: closestBlock.id,
target: id,
sourceHandle,
Expand Down Expand Up @@ -624,7 +625,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)
Expand Down Expand Up @@ -660,7 +661,7 @@ const WorkflowContent = React.memo(() => {
const sourceHandle = determineSourceHandle(closestBlock)

autoConnectEdge = {
id: crypto.randomUUID(),
id: generateUUID(),
source: closestBlock.id,
target: id,
sourceHandle,
Expand Down Expand Up @@ -697,7 +698,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}`
Expand Down Expand Up @@ -748,7 +749,7 @@ const WorkflowContent = React.memo(() => {
type: closestBlock.type,
})
addEdge({
id: crypto.randomUUID(),
id: generateUUID(),
source: closestBlock.id,
target: id,
sourceHandle,
Expand All @@ -765,7 +766,7 @@ const WorkflowContent = React.memo(() => {
: 'parallel-start-source'

addEdge({
id: crypto.randomUUID(),
id: generateUUID(),
source: containerInfo.loopId,
target: id,
sourceHandle: startSourceHandle,
Expand All @@ -784,7 +785,7 @@ const WorkflowContent = React.memo(() => {
const sourceHandle = determineSourceHandle(closestBlock)

autoConnectEdge = {
id: crypto.randomUUID(),
id: generateUUID(),
source: closestBlock.id,
target: id,
sourceHandle,
Expand Down Expand Up @@ -1125,7 +1126,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 (
Expand Down Expand Up @@ -1438,7 +1439,7 @@ const WorkflowContent = React.memo(() => {
type: closestBlock.type,
})
addEdge({
id: crypto.randomUUID(),
id: generateUUID(),
source: closestBlock.id,
target: node.id,
sourceHandle,
Expand All @@ -1455,7 +1456,7 @@ const WorkflowContent = React.memo(() => {
: 'parallel-start-source'

addEdge({
id: crypto.randomUUID(),
id: generateUUID(),
source: potentialParentId,
target: node.id,
sourceHandle: startSourceHandle,
Expand Down
94 changes: 94 additions & 0 deletions apps/sim/lib/uuid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* 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()
}
}

/**
* 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()
}
}

// Default export for easy migration
export default generateContextAwareUUID
5 changes: 3 additions & 2 deletions apps/sim/stores/copilot/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -425,7 +426,7 @@ function createUserMessage(
contexts?: ChatContext[]
): CopilotMessage {
return {
id: crypto.randomUUID(),
id: generateUUID(),
role: 'user',
content,
timestamp: new Date().toISOString(),
Expand All @@ -442,7 +443,7 @@ function createUserMessage(

function createStreamingMessage(): CopilotMessage {
return {
id: crypto.randomUUID(),
id: generateUUID(),
role: 'assistant',
content: '',
timestamp: new Date().toISOString(),
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/stores/custom-tools/store.ts
Original file line number Diff line number Diff line change
@@ -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')
Expand Down Expand Up @@ -130,7 +131,7 @@ export const useCustomToolsStore = create<CustomToolsStore>()(
},

addTool: (tool) => {
const id = crypto.randomUUID()
const id = generateUUID()
const newTool = {
...tool,
id,
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/stores/panel/chat/store.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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
Expand All @@ -19,7 +20,7 @@ export const useChatStore = create<ChatStore>()(
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(),
}

Expand Down
Loading