From 52c3535e9ab9368d463f328898178c1869f606b4 Mon Sep 17 00:00:00 2001 From: waleedlatif Date: Thu, 31 Jul 2025 12:32:16 -0700 Subject: [PATCH 1/3] fix(deployed-chat): allow non-streaming responses in deployed chat, allow partial failure responses in deployed chat --- apps/sim/app/api/chat/utils.test.ts | 73 +++++++++ apps/sim/app/api/chat/utils.ts | 142 +++++++++++++++--- .../[subdomain]/hooks/use-chat-streaming.ts | 42 +++++- .../hooks/use-workflow-execution.ts | 42 ++++-- apps/sim/executor/index.test.ts | 87 +++++++++++ apps/sim/executor/index.ts | 33 +++- 6 files changed, 387 insertions(+), 32 deletions(-) diff --git a/apps/sim/app/api/chat/utils.test.ts b/apps/sim/app/api/chat/utils.test.ts index ec3321ca2e1..335a3ec81c8 100644 --- a/apps/sim/app/api/chat/utils.test.ts +++ b/apps/sim/app/api/chat/utils.test.ts @@ -350,4 +350,77 @@ describe('Chat API Utils', () => { expect(result3.error).toBe('Email not authorized') }) }) + + describe('Execution Result Processing', () => { + it('should process logs regardless of overall success status', () => { + // Test that logs are processed even when overall execution fails + // This is key for partial success scenarios + const executionResult = { + success: false, // Overall execution failed + output: {}, + logs: [ + { + blockId: 'agent1', + startedAt: '2023-01-01T00:00:00Z', + endedAt: '2023-01-01T00:00:01Z', + durationMs: 1000, + success: true, + output: { content: 'Agent 1 succeeded' }, + error: undefined, + }, + { + blockId: 'agent2', + startedAt: '2023-01-01T00:00:00Z', + endedAt: '2023-01-01T00:00:01Z', + durationMs: 500, + success: false, + output: null, + error: 'Agent 2 failed', + }, + ], + metadata: { duration: 1000 }, + } + + // Test the key logic: logs should be processed regardless of overall success + expect(executionResult.success).toBe(false) + expect(executionResult.logs).toBeDefined() + expect(executionResult.logs).toHaveLength(2) + + // First log should be successful + expect(executionResult.logs[0].success).toBe(true) + expect(executionResult.logs[0].output?.content).toBe('Agent 1 succeeded') + + // Second log should be failed + expect(executionResult.logs[1].success).toBe(false) + expect(executionResult.logs[1].error).toBe('Agent 2 failed') + }) + + it('should handle ExecutionResult vs StreamingExecution types correctly', () => { + const executionResult = { + success: true, + output: { content: 'test' }, + logs: [], + metadata: { duration: 100 }, + } + + // Test direct ExecutionResult + const directResult = executionResult + const extractedDirect = directResult + expect(extractedDirect).toBe(executionResult) + + // Test StreamingExecution with embedded ExecutionResult + const streamingResult = { + stream: new ReadableStream(), + execution: executionResult, + } + + // Simulate the type extraction logic from executeWorkflowForChat + const extractedFromStreaming = + streamingResult && typeof streamingResult === 'object' && 'execution' in streamingResult + ? streamingResult.execution + : streamingResult + + expect(extractedFromStreaming).toBe(executionResult) + }) + }) }) diff --git a/apps/sim/app/api/chat/utils.ts b/apps/sim/app/api/chat/utils.ts index 596c1d16073..5143ea79c2a 100644 --- a/apps/sim/app/api/chat/utils.ts +++ b/apps/sim/app/api/chat/utils.ts @@ -14,7 +14,7 @@ import { getBlock } from '@/blocks' import { db } from '@/db' import { chat, environment as envTable, userStats, workflow } from '@/db/schema' import { Executor } from '@/executor' -import type { BlockLog } from '@/executor/types' +import type { BlockLog, ExecutionResult } from '@/executor/types' import { Serializer } from '@/serializer' import { mergeSubblockState } from '@/stores/workflows/server-utils' import type { WorkflowState } from '@/stores/workflows/workflow/types' @@ -549,6 +549,7 @@ export async function executeWorkflowForChat( async start(controller) { const encoder = new TextEncoder() const streamedContent = new Map() + const streamedBlocks = new Set() // Track which blocks have started streaming const onStream = async (streamingExecution: any): Promise => { if (!streamingExecution.stream) return @@ -557,6 +558,15 @@ export async function executeWorkflowForChat( const reader = streamingExecution.stream.getReader() if (blockId) { streamedContent.set(blockId, '') + + // Add separator if this is not the first block to stream + if (streamedBlocks.size > 0) { + // Send separator before the new block starts + controller.enqueue( + encoder.encode(`data: ${JSON.stringify({ blockId, chunk: '\n\n' })}\n\n`) + ) + } + streamedBlocks.add(blockId) } try { while (true) { @@ -615,25 +625,117 @@ export async function executeWorkflowForChat( throw error } - if (result && 'success' in result) { - // Update streamed content and apply tokenization - if (result.logs) { - result.logs.forEach((log: BlockLog) => { - if (streamedContent.has(log.blockId)) { - const content = streamedContent.get(log.blockId) - if (log.output) { - log.output.content = content + // Handle both ExecutionResult and StreamingExecution types + const executionResult = + result && typeof result === 'object' && 'execution' in result + ? (result.execution as ExecutionResult) + : (result as ExecutionResult) + + if (executionResult?.logs) { + // Update streamed content and apply tokenization - process regardless of overall success + // This ensures partial successes (some agents succeed, some fail) still return results + + // Add newlines between different agent outputs for better readability + const processedOutputs = new Set() + executionResult.logs.forEach((log: BlockLog) => { + if (streamedContent.has(log.blockId)) { + const content = streamedContent.get(log.blockId) + if (log.output && content) { + // Add newline separation between different outputs (but not before the first one) + const separator = processedOutputs.size > 0 ? '\n\n' : '' + log.output.content = separator + content + processedOutputs.add(log.blockId) + } + } + }) + + // Also process non-streamed outputs from selected blocks (like function blocks) + // This uses the same logic as the chat panel to ensure identical behavior + const nonStreamingLogs = executionResult.logs.filter( + (log: BlockLog) => !streamedContent.has(log.blockId) + ) + + // Extract the exact same functions used by the chat panel + const extractBlockIdFromOutputId = (outputId: string): string => { + return outputId.includes('_') ? outputId.split('_')[0] : outputId.split('.')[0] + } + + const extractPathFromOutputId = (outputId: string, blockId: string): string => { + return outputId.substring(blockId.length + 1) + } + + const parseOutputContentSafely = (output: any): any => { + if (!output?.content) { + return output + } + + if (typeof output.content === 'string') { + try { + return JSON.parse(output.content) + } catch (e) { + // Fallback to original structure if parsing fails + return output + } + } + + return output + } + + // Filter outputs that have matching logs (exactly like chat panel) + const outputsToRender = selectedOutputIds.filter((outputId) => { + const blockIdForOutput = extractBlockIdFromOutputId(outputId) + return nonStreamingLogs.some((log) => log.blockId === blockIdForOutput) + }) + + // Process each selected output (exactly like chat panel) + for (const outputId of outputsToRender) { + const blockIdForOutput = extractBlockIdFromOutputId(outputId) + const path = extractPathFromOutputId(outputId, blockIdForOutput) + const log = nonStreamingLogs.find((l) => l.blockId === blockIdForOutput) + + if (log) { + let outputValue: any = log.output + + if (path) { + // Parse JSON content safely (exactly like chat panel) + outputValue = parseOutputContentSafely(outputValue) + + const pathParts = path.split('.') + for (const part of pathParts) { + if (outputValue && typeof outputValue === 'object' && part in outputValue) { + outputValue = outputValue[part] + } else { + outputValue = undefined + break + } } } - }) - // Process all logs for streaming tokenization - const processedCount = processStreamingBlockLogs(result.logs, streamedContent) - logger.info(`[CHAT-API] Processed ${processedCount} blocks for streaming tokenization`) + if (outputValue !== undefined) { + // Add newline separation between different outputs + const separator = processedOutputs.size > 0 ? '\n\n' : '' + + // Format the output exactly like the chat panel + const formattedOutput = + typeof outputValue === 'string' ? outputValue : JSON.stringify(outputValue, null, 2) + + // Update the log content + if (!log.output.content) { + log.output.content = separator + formattedOutput + } else { + log.output.content = separator + formattedOutput + } + processedOutputs.add(log.blockId) + } + } } - const { traceSpans, totalDuration } = buildTraceSpans(result) - const enrichedResult = { ...result, traceSpans, totalDuration } + // Process all logs for streaming tokenization + const processedCount = processStreamingBlockLogs(executionResult.logs, streamedContent) + logger.info(`Processed ${processedCount} blocks for streaming tokenization`) + + const { traceSpans, totalDuration } = buildTraceSpans(executionResult) + const enrichedResult = { ...executionResult, traceSpans, totalDuration } if (conversationId) { if (!enrichedResult.metadata) { enrichedResult.metadata = { @@ -646,7 +748,7 @@ export async function executeWorkflowForChat( const executionId = uuidv4() logger.debug(`Generated execution ID for deployed chat: ${executionId}`) - if (result.success) { + if (executionResult.success) { try { await db .update(userStats) @@ -669,12 +771,12 @@ export async function executeWorkflowForChat( } // Complete logging session (for both success and failure) - if (result && 'success' in result) { - const { traceSpans } = buildTraceSpans(result) + if (executionResult?.logs) { + const { traceSpans } = buildTraceSpans(executionResult) await loggingSession.safeComplete({ endedAt: new Date().toISOString(), - totalDurationMs: result.metadata?.duration || 0, - finalOutput: result.output, + totalDurationMs: executionResult.metadata?.duration || 0, + finalOutput: executionResult.output, traceSpans, }) } diff --git a/apps/sim/app/chat/[subdomain]/hooks/use-chat-streaming.ts b/apps/sim/app/chat/[subdomain]/hooks/use-chat-streaming.ts index 67718c489c7..9bad3adf1e8 100644 --- a/apps/sim/app/chat/[subdomain]/hooks/use-chat-streaming.ts +++ b/apps/sim/app/chat/[subdomain]/hooks/use-chat-streaming.ts @@ -3,6 +3,8 @@ import { useRef, useState } from 'react' import { createLogger } from '@/lib/logs/console/logger' import type { ChatMessage } from '@/app/chat/[subdomain]/components/message/message' +// No longer need complex output extraction - backend handles this +import type { ExecutionResult } from '@/executor/types' const logger = createLogger('UseChatStreaming') @@ -96,6 +98,8 @@ export function useChatStreaming() { let accumulatedText = '' let lastAudioPosition = 0 + // Track which blocks have streamed content (like chat panel) + const messageIdMap = new Map() const messageId = crypto.randomUUID() setMessages((prev) => [ ...prev, @@ -148,13 +152,49 @@ export function useChatStreaming() { const { blockId, chunk: contentChunk, event: eventType } = json if (eventType === 'final' && json.data) { + // The backend has already processed and combined all outputs + // We just need to extract the combined content and use it + const result = json.data as ExecutionResult + + // Collect all content from logs that have output.content (backend processed) + let combinedContent = '' + if (result.logs) { + const contentParts: string[] = [] + + // Get content from all logs that have processed content + result.logs.forEach((log) => { + if (log.output?.content && typeof log.output.content === 'string') { + // The backend already includes proper separators, so just collect the content + contentParts.push(log.output.content) + } + }) + + // Join without additional separators since backend already handles this + combinedContent = contentParts.join('') + } + + // Update the existing streaming message with the final combined content setMessages((prev) => - prev.map((msg) => (msg.id === messageId ? { ...msg, isStreaming: false } : msg)) + prev.map((msg) => + msg.id === messageId + ? { + ...msg, + content: combinedContent || accumulatedText, // Use combined content or fallback to streamed + isStreaming: false, + } + : msg + ) ) + return } if (blockId && contentChunk) { + // Track that this block has streamed content (like chat panel) + if (!messageIdMap.has(blockId)) { + messageIdMap.set(blockId, messageId) + } + accumulatedText += contentChunk setMessages((prev) => prev.map((msg) => 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 36e96d23454..0c8fe0eb00e 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 @@ -323,15 +323,32 @@ export function useWorkflowExecution() { await Promise.all(streamReadingPromises) - if (result && 'success' in result) { - if (!result.metadata) { - result.metadata = { duration: 0, startTime: new Date().toISOString() } + // Handle both ExecutionResult and StreamingExecution types - process regardless of success status + if (result) { + // Handle both ExecutionResult and StreamingExecution types + const executionResult = + result && typeof result === 'object' && 'execution' in result + ? (result.execution as ExecutionResult) + : (result as ExecutionResult) + + if (!executionResult.metadata) { + executionResult.metadata = { duration: 0, startTime: new Date().toISOString() } } - ;(result.metadata as any).source = 'chat' - // Update streamed content and apply tokenization - if (result.logs) { - result.logs.forEach((log: BlockLog) => { + ;(executionResult.metadata as any).source = 'chat' + + // Update streamed content and apply tokenization - process logs regardless of success status + if (executionResult.logs) { + // Add newlines between different agent outputs for better readability + const processedOutputs = new Set() + executionResult.logs.forEach((log: BlockLog) => { if (streamedContent.has(log.blockId)) { + const content = streamedContent.get(log.blockId) + if (log.output && content) { + const separator = processedOutputs.size > 0 ? '\n\n' : '' + log.output.content = separator + content + processedOutputs.add(log.blockId) + } + // For console display, show the actual structured block output instead of formatted streaming content // This ensures console logs match the block state structure // Use replaceOutput to completely replace the output instead of merging @@ -348,14 +365,19 @@ export function useWorkflowExecution() { }) // Process all logs for streaming tokenization - const processedCount = processStreamingBlockLogs(result.logs, streamedContent) + const processedCount = processStreamingBlockLogs( + executionResult.logs, + streamedContent + ) logger.info(`Processed ${processedCount} blocks for streaming tokenization`) } controller.enqueue( - encoder.encode(`data: ${JSON.stringify({ event: 'final', data: result })}\n\n`) + encoder.encode( + `data: ${JSON.stringify({ event: 'final', data: executionResult })}\n\n` + ) ) - persistLogs(executionId, result).catch((err) => + persistLogs(executionId, executionResult).catch((err) => logger.error('Error persisting logs:', err) ) } diff --git a/apps/sim/executor/index.test.ts b/apps/sim/executor/index.test.ts index d273ebf8a26..7826c4b4398 100644 --- a/apps/sim/executor/index.test.ts +++ b/apps/sim/executor/index.test.ts @@ -967,4 +967,91 @@ describe('Executor', () => { } }) }) + + describe('Parallel Execution with Mixed Results', () => { + it.concurrent( + 'should handle parallel execution where some blocks succeed and others fail', + async () => { + // Create a workflow with two parallel agents + const workflow = { + blocks: [ + { + id: 'starter', + metadata: { id: BlockType.STARTER }, + subBlocks: {}, + enabled: true, + }, + { + id: 'agent1', + metadata: { id: BlockType.AGENT, name: 'Agent 1' }, + subBlocks: { + model: { value: 'gpt-4o' }, + input: { value: 'Hello' }, + }, + enabled: true, + }, + { + id: 'agent2', + metadata: { id: BlockType.AGENT, name: 'Agent 2' }, + subBlocks: { + model: { value: 'gpt-4o' }, + input: { value: 'Hello' }, + }, + enabled: true, + }, + ], + connections: [ + { source: 'starter', sourceHandle: 'out', target: 'agent1', targetHandle: 'in' }, + { source: 'starter', sourceHandle: 'out', target: 'agent2', targetHandle: 'in' }, + ], + loops: [], + parallels: [], + } + + const executor = new Executor(workflow) + + // Mock agent1 to succeed and agent2 to fail + const mockExecuteBlock = vi + .fn() + .mockImplementationOnce(() => ({ content: 'Success from agent1' })) // agent1 succeeds + .mockImplementationOnce(() => { + throw new Error('Agent 2 failed') + }) // agent2 fails + + // Replace the executeBlock method + + ;(executor as any).executeBlock = mockExecuteBlock + + // Mock other necessary methods + + ;(executor as any).createExecutionContext = vi.fn(() => ({ + blockStates: new Map(), + executedBlocks: new Set(['starter']), + blockLogs: [], + metadata: { startTime: new Date().toISOString() }, + pendingBlocks: [], + parallelBlockMapping: new Map(), + onStream: undefined, + })) + + ;(executor as any).getNextExecutionLayer = vi + .fn() + .mockReturnValueOnce(['agent1', 'agent2']) // First call returns both agents + .mockReturnValueOnce([]) // Second call returns empty (execution complete) + + ;(executor as any).pathTracker = { + updateExecutionPaths: vi.fn(), + } + + const result = await executor.execute('test-workflow') + + // Should succeed with partial results - not throw an error + expect(result).toBeDefined() + expect(mockExecuteBlock).toHaveBeenCalledTimes(2) + + // The execution should complete despite one block failing + // This tests our Promise.allSettled() behavior + } + ) + }) }) diff --git a/apps/sim/executor/index.ts b/apps/sim/executor/index.ts index d19243b2bb8..b170d884bd2 100644 --- a/apps/sim/executor/index.ts +++ b/apps/sim/executor/index.ts @@ -1236,10 +1236,41 @@ export class Executor { setActiveBlocks(activeBlockIds) - const results = await Promise.all( + const settledResults = await Promise.allSettled( blockIds.map((blockId) => this.executeBlock(blockId, context)) ) + // Extract successful results and collect any errors + const results: (NormalizedBlockOutput | StreamingExecution)[] = [] + const errors: Error[] = [] + + settledResults.forEach((result, index) => { + if (result.status === 'fulfilled') { + results.push(result.value) + } else { + errors.push(result.reason) + // For failed blocks, we still need to add a placeholder result + // so the results array matches the blockIds array length + results.push({ + error: result.reason?.message || 'Block execution failed', + status: 500, + }) + } + }) + + // If there were any errors, log them but don't throw immediately + // This allows successful blocks to complete their streaming + if (errors.length > 0) { + logger.warn( + `Layer execution completed with ${errors.length} failed blocks out of ${blockIds.length} total` + ) + + // Only throw if ALL blocks failed + if (errors.length === blockIds.length) { + throw errors[0] // Throw the first error if all blocks failed + } + } + blockIds.forEach((blockId) => { context.executedBlocks.add(blockId) }) From 103c852b177a54c20c391a8f0b06b20849730f94 Mon Sep 17 00:00:00 2001 From: waleedlatif Date: Thu, 31 Jul 2025 12:47:41 -0700 Subject: [PATCH 2/3] fix(csp): runtime variable resolution for CSP --- apps/sim/lib/security/csp.ts | 78 ++++++++++++++++++++++++++++++------ apps/sim/middleware.ts | 11 +++++ apps/sim/next.config.ts | 5 ++- 3 files changed, 80 insertions(+), 14 deletions(-) diff --git a/apps/sim/lib/security/csp.ts b/apps/sim/lib/security/csp.ts index acd323d5d41..95860b84ca1 100644 --- a/apps/sim/lib/security/csp.ts +++ b/apps/sim/lib/security/csp.ts @@ -1,4 +1,5 @@ -import { env } from '../env' +import crypto from 'crypto' +import { env, getEnv } from '../env' /** * Content Security Policy (CSP) configuration builder @@ -20,7 +21,8 @@ export interface CSPDirectives { 'object-src'?: string[] } -export const cspDirectives: CSPDirectives = { +// Build-time CSP directives (for next.config.ts) +export const buildTimeCSPDirectives: CSPDirectives = { 'default-src': ["'self'"], 'script-src': [ @@ -115,10 +117,60 @@ export function buildCSPString(directives: CSPDirectives): string { } /** - * Get the main CSP policy string + * Generate runtime CSP header with dynamic environment variables (safer approach) + * This maintains compatibility with existing inline scripts while fixing Docker env var issues + */ +export function generateRuntimeCSP(): string { + const socketUrl = getEnv('NEXT_PUBLIC_SOCKET_URL') || 'http://localhost:3002' + const socketWsUrl = + socketUrl.replace('http://', 'ws://').replace('https://', 'wss://') || 'ws://localhost:3002' + const appUrl = getEnv('NEXT_PUBLIC_APP_URL') || '' + const ollamaUrl = getEnv('OLLAMA_URL') || 'http://localhost:11434' + + return ` + default-src 'self'; + script-src 'self' 'unsafe-inline' 'unsafe-eval' https://*.google.com https://apis.google.com https://*.vercel-scripts.com https://*.vercel-insights.com https://vercel.live https://*.vercel.live https://vercel.com https://*.vercel.app https://vitals.vercel-insights.com https://b2bjsstore.s3.us-west-2.amazonaws.com; + style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; + img-src 'self' data: blob: https://*.googleusercontent.com https://*.google.com https://*.atlassian.com https://cdn.discordapp.com https://*.githubusercontent.com https://*.public.blob.vercel-storage.com; + media-src 'self' blob:; + font-src 'self' https://fonts.gstatic.com; + connect-src 'self' ${appUrl} ${ollamaUrl} ${socketUrl} ${socketWsUrl} https://*.up.railway.app wss://*.up.railway.app https://api.browser-use.com https://api.exa.ai https://api.firecrawl.dev https://*.googleapis.com https://*.amazonaws.com https://*.s3.amazonaws.com https://*.blob.core.windows.net https://*.vercel-insights.com https://vitals.vercel-insights.com https://*.atlassian.com https://*.supabase.co https://vercel.live https://*.vercel.live https://vercel.com https://*.vercel.app wss://*.vercel.app https://pro.ip-api.com; + frame-src https://drive.google.com https://docs.google.com https://*.google.com; + frame-ancestors 'self'; + form-action 'self'; + base-uri 'self'; + object-src 'none'; + ` + .replace(/\s{2,}/g, ' ') + .trim() +} + +/** + * Get the main CSP policy string (build-time) */ export function getMainCSPPolicy(): string { - return buildCSPString(cspDirectives) + return buildCSPString(buildTimeCSPDirectives) +} + +/** + * Generate a cryptographically secure nonce for CSP + */ +export function generateNonce(): string { + return Buffer.from(crypto.randomUUID()).toString('base64') +} + +/** + * Get the current request's nonce from headers (for use in app components) + */ +export function getNonce(): string | null { + try { + // This requires dynamic imports to avoid edge runtime issues + const { headers } = require('next/headers') + const headersList = headers() + return headersList.get('x-nonce') + } catch { + return null + } } /** @@ -129,22 +181,24 @@ export function getWorkflowExecutionCSPPolicy(): string { } /** - * Add a source to a specific directive + * Add a source to a specific directive (modifies build-time directives) */ export function addCSPSource(directive: keyof CSPDirectives, source: string): void { - if (!cspDirectives[directive]) { - cspDirectives[directive] = [] + if (!buildTimeCSPDirectives[directive]) { + buildTimeCSPDirectives[directive] = [] } - if (!cspDirectives[directive]!.includes(source)) { - cspDirectives[directive]!.push(source) + if (!buildTimeCSPDirectives[directive]!.includes(source)) { + buildTimeCSPDirectives[directive]!.push(source) } } /** - * Remove a source from a specific directive + * Remove a source from a specific directive (modifies build-time directives) */ export function removeCSPSource(directive: keyof CSPDirectives, source: string): void { - if (cspDirectives[directive]) { - cspDirectives[directive] = cspDirectives[directive]!.filter((s: string) => s !== source) + if (buildTimeCSPDirectives[directive]) { + buildTimeCSPDirectives[directive] = buildTimeCSPDirectives[directive]!.filter( + (s: string) => s !== source + ) } } diff --git a/apps/sim/middleware.ts b/apps/sim/middleware.ts index 72228008b72..7f6b3540931 100644 --- a/apps/sim/middleware.ts +++ b/apps/sim/middleware.ts @@ -2,6 +2,7 @@ import { getSessionCookie } from 'better-auth/cookies' import { type NextRequest, NextResponse } from 'next/server' import { isDev } from './lib/environment' import { createLogger } from './lib/logs/console/logger' +import { generateRuntimeCSP } from './lib/security/csp' import { getBaseDomain } from './lib/urls/utils' const logger = createLogger('Middleware') @@ -159,6 +160,16 @@ export async function middleware(request: NextRequest) { const response = NextResponse.next() response.headers.set('Vary', 'User-Agent') + + // Generate runtime CSP for main application routes that need dynamic environment variables + if ( + url.pathname.startsWith('/workspace') || + url.pathname.startsWith('/chat') || + url.pathname === '/' + ) { + response.headers.set('Content-Security-Policy', generateRuntimeCSP()) + } + return response } diff --git a/apps/sim/next.config.ts b/apps/sim/next.config.ts index b028826ed02..8f2ef990ba3 100644 --- a/apps/sim/next.config.ts +++ b/apps/sim/next.config.ts @@ -133,9 +133,10 @@ const nextConfig: NextConfig = { }, ], }, - // Apply security headers to all routes + // Apply security headers to routes not handled by middleware runtime CSP + // Middleware handles: /, /workspace/*, /chat/* { - source: '/:path*', + source: '/((?!workspace|chat$).*)', headers: [ { key: 'X-Content-Type-Options', From 71ab36dfc565f8ea425b5d304fdb5207fcfce014 Mon Sep 17 00:00:00 2001 From: waleedlatif Date: Thu, 31 Jul 2025 13:17:17 -0700 Subject: [PATCH 3/3] cleanup --- apps/sim/contexts/socket-context.tsx | 2 +- apps/sim/lib/security/csp.ts | 22 ---------------------- 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/apps/sim/contexts/socket-context.tsx b/apps/sim/contexts/socket-context.tsx index c0e9115cb10..e196351cdee 100644 --- a/apps/sim/contexts/socket-context.tsx +++ b/apps/sim/contexts/socket-context.tsx @@ -445,7 +445,7 @@ export function SocketProvider({ children, user }: SocketProviderProps) { }) socketInstance.on('workflow-state', (workflowData) => { - logger.info('Received workflow state from server:', workflowData) + logger.info('Received workflow state from server') // Update local stores with the fresh workflow state (same logic as YAML editor) if (workflowData?.state && workflowData.id === urlWorkflowId) { diff --git a/apps/sim/lib/security/csp.ts b/apps/sim/lib/security/csp.ts index 95860b84ca1..49707e127c0 100644 --- a/apps/sim/lib/security/csp.ts +++ b/apps/sim/lib/security/csp.ts @@ -1,4 +1,3 @@ -import crypto from 'crypto' import { env, getEnv } from '../env' /** @@ -152,27 +151,6 @@ export function getMainCSPPolicy(): string { return buildCSPString(buildTimeCSPDirectives) } -/** - * Generate a cryptographically secure nonce for CSP - */ -export function generateNonce(): string { - return Buffer.from(crypto.randomUUID()).toString('base64') -} - -/** - * Get the current request's nonce from headers (for use in app components) - */ -export function getNonce(): string | null { - try { - // This requires dynamic imports to avoid edge runtime issues - const { headers } = require('next/headers') - const headersList = headers() - return headersList.get('x-nonce') - } catch { - return null - } -} - /** * Permissive CSP for workflow execution endpoints */