diff --git a/apps/sim/app/api/proxy/image/route.ts b/apps/sim/app/api/proxy/image/route.ts index fc7717b671..c4f0b91e66 100644 --- a/apps/sim/app/api/proxy/image/route.ts +++ b/apps/sim/app/api/proxy/image/route.ts @@ -1,4 +1,5 @@ import { type NextRequest, NextResponse } from 'next/server' +import { checkHybridAuth } from '@/lib/auth/hybrid' import { createLogger } from '@/lib/logs/console/logger' import { validateImageUrl } from '@/lib/security/input-validation' import { generateRequestId } from '@/lib/utils' @@ -14,6 +15,12 @@ export async function GET(request: NextRequest) { const imageUrl = url.searchParams.get('url') const requestId = generateRequestId() + const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + if (!authResult.success) { + logger.error(`[${requestId}] Authentication failed for image proxy:`, authResult.error) + return new NextResponse('Unauthorized', { status: 401 }) + } + if (!imageUrl) { logger.error(`[${requestId}] Missing 'url' parameter`) return new NextResponse('Missing URL parameter', { status: 400 }) diff --git a/apps/sim/app/api/proxy/route.ts b/apps/sim/app/api/proxy/route.ts index f4699e7234..8848ed6cdf 100644 --- a/apps/sim/app/api/proxy/route.ts +++ b/apps/sim/app/api/proxy/route.ts @@ -1,4 +1,6 @@ +import type { NextRequest } from 'next/server' import { NextResponse } from 'next/server' +import { checkHybridAuth } from '@/lib/auth/hybrid' import { generateInternalToken } from '@/lib/auth/internal' import { isDev } from '@/lib/environment' import { createLogger } from '@/lib/logs/console/logger' @@ -242,12 +244,18 @@ export async function GET(request: Request) { } } -export async function POST(request: Request) { +export async function POST(request: NextRequest) { const requestId = generateRequestId() const startTime = new Date() const startTimeISO = startTime.toISOString() try { + const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + if (!authResult.success) { + logger.error(`[${requestId}] Authentication failed for proxy:`, authResult.error) + return createErrorResponse('Unauthorized', 401) + } + let requestBody try { requestBody = await request.json() @@ -311,7 +319,6 @@ export async function POST(request: Request) { error: result.error || 'Unknown error', }) - // Let the main executeTool handle error transformation to avoid double transformation throw new Error(result.error || 'Tool execution failed') } @@ -319,10 +326,8 @@ export async function POST(request: Request) { const endTimeISO = endTime.toISOString() const duration = endTime.getTime() - startTime.getTime() - // Add explicit timing information directly to the response const responseWithTimingData = { ...result, - // Add timing data both at root level and in nested timing object startTime: startTimeISO, endTime: endTimeISO, duration, @@ -335,7 +340,6 @@ export async function POST(request: Request) { logger.info(`[${requestId}] Tool executed successfully: ${toolId} (${duration}ms)`) - // Return the response with CORS headers return formatResponse(responseWithTimingData) } catch (error: any) { logger.error(`[${requestId}] Proxy request failed`, { @@ -344,7 +348,6 @@ export async function POST(request: Request) { name: error instanceof Error ? error.name : undefined, }) - // Add timing information even to error responses const endTime = new Date() const endTimeISO = endTime.toISOString() const duration = endTime.getTime() - startTime.getTime() diff --git a/apps/sim/app/api/proxy/tts/route.ts b/apps/sim/app/api/proxy/tts/route.ts index 609ea53e4f..b9a322e2d5 100644 --- a/apps/sim/app/api/proxy/tts/route.ts +++ b/apps/sim/app/api/proxy/tts/route.ts @@ -1,4 +1,6 @@ +import type { NextRequest } from 'next/server' import { NextResponse } from 'next/server' +import { checkHybridAuth } from '@/lib/auth/hybrid' import { createLogger } from '@/lib/logs/console/logger' import { validateAlphanumericId } from '@/lib/security/input-validation' import { uploadFile } from '@/lib/uploads/storage-client' @@ -6,19 +8,25 @@ import { getBaseUrl } from '@/lib/urls/utils' const logger = createLogger('ProxyTTSAPI') -export async function POST(request: Request) { +export async function POST(request: NextRequest) { try { + const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + if (!authResult.success) { + logger.error('Authentication failed for TTS proxy:', authResult.error) + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + const body = await request.json() const { text, voiceId, apiKey, modelId = 'eleven_monolingual_v1' } = body if (!text || !voiceId || !apiKey) { - return new NextResponse('Missing required parameters', { status: 400 }) + return NextResponse.json({ error: 'Missing required parameters' }, { status: 400 }) } const voiceIdValidation = validateAlphanumericId(voiceId, 'voiceId', 255) if (!voiceIdValidation.isValid) { logger.error(`Invalid voice ID: ${voiceIdValidation.error}`) - return new NextResponse(voiceIdValidation.error, { status: 400 }) + return NextResponse.json({ error: voiceIdValidation.error }, { status: 400 }) } logger.info('Proxying TTS request for voice:', voiceId) @@ -41,16 +49,17 @@ export async function POST(request: Request) { if (!response.ok) { logger.error(`Failed to generate TTS: ${response.status} ${response.statusText}`) - return new NextResponse(`Failed to generate TTS: ${response.status} ${response.statusText}`, { - status: response.status, - }) + return NextResponse.json( + { error: `Failed to generate TTS: ${response.status} ${response.statusText}` }, + { status: response.status } + ) } const audioBlob = await response.blob() if (audioBlob.size === 0) { logger.error('Empty audio received from ElevenLabs') - return new NextResponse('Empty audio received', { status: 422 }) + return NextResponse.json({ error: 'Empty audio received' }, { status: 422 }) } const audioBuffer = Buffer.from(await audioBlob.arrayBuffer()) @@ -67,11 +76,11 @@ export async function POST(request: Request) { } catch (error) { logger.error('Error proxying TTS:', error) - return new NextResponse( - `Internal Server Error: ${error instanceof Error ? error.message : 'Unknown error'}`, + return NextResponse.json( { - status: 500, - } + error: `Internal Server Error: ${error instanceof Error ? error.message : 'Unknown error'}`, + }, + { status: 500 } ) } } diff --git a/apps/sim/app/api/proxy/tts/stream/route.ts b/apps/sim/app/api/proxy/tts/stream/route.ts index a2910d3738..1f090d36d6 100644 --- a/apps/sim/app/api/proxy/tts/stream/route.ts +++ b/apps/sim/app/api/proxy/tts/stream/route.ts @@ -1,4 +1,5 @@ import type { NextRequest } from 'next/server' +import { checkHybridAuth } from '@/lib/auth/hybrid' import { env } from '@/lib/env' import { createLogger } from '@/lib/logs/console/logger' import { validateAlphanumericId } from '@/lib/security/input-validation' @@ -7,6 +8,12 @@ const logger = createLogger('ProxyTTSStreamAPI') export async function POST(request: NextRequest) { try { + const authResult = await checkHybridAuth(request, { requireWorkflowId: false }) + if (!authResult.success) { + logger.error('Authentication failed for TTS stream proxy:', authResult.error) + return new Response('Unauthorized', { status: 401 }) + } + const body = await request.json() const { text, voiceId, modelId = 'eleven_turbo_v2_5' } = body diff --git a/apps/sim/tools/elevenlabs/tts.ts b/apps/sim/tools/elevenlabs/tts.ts index 5712c64493..ee132711de 100644 --- a/apps/sim/tools/elevenlabs/tts.ts +++ b/apps/sim/tools/elevenlabs/tts.ts @@ -51,6 +51,16 @@ export const elevenLabsTtsTool: ToolConfig { const data = await response.json() + if (!response.ok || data.error) { + return { + success: false, + error: data.error || 'Unknown error occurred', + output: { + audioUrl: '', + }, + } + } + return { success: true, output: { diff --git a/apps/sim/tools/index.test.ts b/apps/sim/tools/index.test.ts index 92ca41f9c8..ac851b69a1 100644 --- a/apps/sim/tools/index.test.ts +++ b/apps/sim/tools/index.test.ts @@ -31,7 +31,7 @@ const createMockExecutionContext = (overrides?: Partial): Exec }) describe('Tools Registry', () => { - it.concurrent('should include all expected built-in tools', () => { + it('should include all expected built-in tools', () => { expect(Object.keys(tools).length).toBeGreaterThan(10) // Check for existence of some core tools @@ -45,7 +45,7 @@ describe('Tools Registry', () => { expect(tools.serper_search).toBeDefined() }) - it.concurrent('getTool should return the correct tool by ID', () => { + it('getTool should return the correct tool by ID', () => { const httpTool = getTool('http_request') expect(httpTool).toBeDefined() expect(httpTool?.id).toBe('http_request') @@ -57,7 +57,7 @@ describe('Tools Registry', () => { expect(gmailTool?.name).toBe('Gmail Read') }) - it.concurrent('getTool should return undefined for non-existent tool', () => { + it('getTool should return undefined for non-existent tool', () => { const nonExistentTool = getTool('non_existent_tool') expect(nonExistentTool).toBeUndefined() }) @@ -133,7 +133,7 @@ describe('Custom Tools', () => { vi.resetAllMocks() }) - it.concurrent('should get custom tool by ID', () => { + it('should get custom tool by ID', () => { const customTool = getTool('custom_custom-tool-123') expect(customTool).toBeDefined() expect(customTool?.name).toBe('Custom Weather Tool') @@ -142,7 +142,7 @@ describe('Custom Tools', () => { expect(customTool?.params.location.required).toBe(true) }) - it.concurrent('should handle non-existent custom tool', () => { + it('should handle non-existent custom tool', () => { const nonExistentTool = getTool('custom_non-existent') expect(nonExistentTool).toBeUndefined() }) @@ -193,7 +193,7 @@ describe('executeTool Function', () => { cleanupEnvVars() }) - it.concurrent('should execute a tool successfully', async () => { + it('should execute a tool successfully', async () => { const result = await executeTool( 'http_request', { @@ -241,7 +241,7 @@ describe('executeTool Function', () => { ) }) - it.concurrent('should handle non-existent tool', async () => { + it('should handle non-existent tool', async () => { // Create the mock with a matching implementation vi.spyOn(console, 'error').mockImplementation(() => {}) @@ -254,7 +254,7 @@ describe('executeTool Function', () => { vi.restoreAllMocks() }) - it.concurrent('should handle errors from tools', async () => { + it('should handle errors from tools', async () => { // Mock a failed response global.fetch = Object.assign( vi.fn().mockImplementation(async () => { @@ -284,7 +284,7 @@ describe('executeTool Function', () => { expect(result.timing).toBeDefined() }) - it.concurrent('should add timing information to results', async () => { + it('should add timing information to results', async () => { const result = await executeTool( 'http_request', { @@ -315,58 +315,59 @@ describe('Automatic Internal Route Detection', () => { cleanupEnvVars() }) - it.concurrent( - 'should detect internal routes (URLs starting with /api/) and call them directly', - async () => { - // Mock a tool with an internal route - const mockTool = { - id: 'test_internal_tool', - name: 'Test Internal Tool', - description: 'A test tool with internal route', - version: '1.0.0', - params: {}, - request: { - url: '/api/test/endpoint', - method: 'POST', - headers: () => ({ 'Content-Type': 'application/json' }), - }, - transformResponse: vi.fn().mockResolvedValue({ - success: true, - output: { result: 'Internal route success' }, - }), - } - - // Mock the tool registry to include our test tool - const originalTools = { ...tools } - ;(tools as any).test_internal_tool = mockTool - - // Mock fetch for the internal API call - global.fetch = Object.assign( - vi.fn().mockImplementation(async (url) => { - // Should call the internal API directly, not the proxy - expect(url).toBe('http://localhost:3000/api/test/endpoint') - return { - ok: true, - status: 200, - json: () => Promise.resolve({ success: true, data: 'test' }), - clone: vi.fn().mockReturnThis(), - } - }), - { preconnect: vi.fn() } - ) as typeof fetch + it('should detect internal routes (URLs starting with /api/) and call them directly', async () => { + // Mock a tool with an internal route + const mockTool = { + id: 'test_internal_tool', + name: 'Test Internal Tool', + description: 'A test tool with internal route', + version: '1.0.0', + params: {}, + request: { + url: '/api/test/endpoint', + method: 'POST', + headers: () => ({ 'Content-Type': 'application/json' }), + }, + transformResponse: vi.fn().mockResolvedValue({ + success: true, + output: { result: 'Internal route success' }, + }), + } + + // Mock the tool registry to include our test tool + const originalTools = { ...tools } + ;(tools as any).test_internal_tool = mockTool + + // Mock fetch for the internal API call + global.fetch = Object.assign( + vi.fn().mockImplementation(async (url) => { + // Should call the internal API directly, not the proxy + expect(url).toBe('http://localhost:3000/api/test/endpoint') + const responseData = { success: true, data: 'test' } + return { + ok: true, + status: 200, + statusText: 'OK', + headers: new Headers(), + json: () => Promise.resolve(responseData), + text: () => Promise.resolve(JSON.stringify(responseData)), + clone: vi.fn().mockReturnThis(), + } + }), + { preconnect: vi.fn() } + ) as typeof fetch - const result = await executeTool('test_internal_tool', {}, false) + const result = await executeTool('test_internal_tool', {}, false) - expect(result.success).toBe(true) - expect(result.output.result).toBe('Internal route success') - expect(mockTool.transformResponse).toHaveBeenCalled() + expect(result.success).toBe(true) + expect(result.output.result).toBe('Internal route success') + expect(mockTool.transformResponse).toHaveBeenCalled() - // Restore original tools - Object.assign(tools, originalTools) - } - ) + // Restore original tools + Object.assign(tools, originalTools) + }) - it.concurrent('should detect external routes (full URLs) and use proxy', async () => { + it('should detect external routes (full URLs) and use proxy', async () => { // Mock a tool with an external route const mockTool = { id: 'test_external_tool', @@ -390,14 +391,17 @@ describe('Automatic Internal Route Detection', () => { vi.fn().mockImplementation(async (url) => { // Should call the proxy, not the external API directly expect(url).toBe('http://localhost:3000/api/proxy') + const responseData = { + success: true, + output: { result: 'External route via proxy' }, + } return { ok: true, status: 200, - json: () => - Promise.resolve({ - success: true, - output: { result: 'External route via proxy' }, - }), + statusText: 'OK', + headers: new Headers(), + json: () => Promise.resolve(responseData), + text: () => Promise.resolve(JSON.stringify(responseData)), } }), { preconnect: vi.fn() } @@ -412,7 +416,7 @@ describe('Automatic Internal Route Detection', () => { Object.assign(tools, originalTools) }) - it.concurrent('should handle dynamic URLs that resolve to internal routes', async () => { + it('should handle dynamic URLs that resolve to internal routes', async () => { // Mock a tool with a dynamic URL function that returns internal route const mockTool = { id: 'test_dynamic_internal', @@ -442,10 +446,14 @@ describe('Automatic Internal Route Detection', () => { vi.fn().mockImplementation(async (url) => { // Should call the internal API directly with the resolved dynamic URL expect(url).toBe('http://localhost:3000/api/resources/123') + const responseData = { success: true, data: 'test' } return { ok: true, status: 200, - json: () => Promise.resolve({ success: true, data: 'test' }), + statusText: 'OK', + headers: new Headers(), + json: () => Promise.resolve(responseData), + text: () => Promise.resolve(JSON.stringify(responseData)), clone: vi.fn().mockReturnThis(), } }), @@ -461,7 +469,7 @@ describe('Automatic Internal Route Detection', () => { Object.assign(tools, originalTools) }) - it.concurrent('should handle dynamic URLs that resolve to external routes', async () => { + it('should handle dynamic URLs that resolve to external routes', async () => { // Mock a tool with a dynamic URL function that returns external route const mockTool = { id: 'test_dynamic_external', @@ -487,14 +495,17 @@ describe('Automatic Internal Route Detection', () => { vi.fn().mockImplementation(async (url) => { // Should call the proxy, not the external API directly expect(url).toBe('http://localhost:3000/api/proxy') + const responseData = { + success: true, + output: { result: 'Dynamic external route via proxy' }, + } return { ok: true, status: 200, - json: () => - Promise.resolve({ - success: true, - output: { result: 'Dynamic external route via proxy' }, - }), + statusText: 'OK', + headers: new Headers(), + json: () => Promise.resolve(responseData), + text: () => Promise.resolve(JSON.stringify(responseData)), } }), { preconnect: vi.fn() } @@ -509,51 +520,48 @@ describe('Automatic Internal Route Detection', () => { Object.assign(tools, originalTools) }) - it.concurrent( - 'should respect skipProxy parameter and call internal routes directly even for external URLs', - async () => { - const mockTool = { - id: 'test_skip_proxy', - name: 'Test Skip Proxy Tool', - description: 'A test tool to verify skipProxy behavior', - version: '1.0.0', - params: {}, - request: { - url: 'https://api.example.com/endpoint', - method: 'GET', - headers: () => ({ 'Content-Type': 'application/json' }), - }, - transformResponse: vi.fn().mockResolvedValue({ - success: true, - output: { result: 'Skipped proxy, called directly' }, - }), - } - - const originalTools = { ...tools } - ;(tools as any).test_skip_proxy = mockTool - - global.fetch = Object.assign( - vi.fn().mockImplementation(async (url) => { - expect(url).toBe('https://api.example.com/endpoint') - return { - ok: true, - status: 200, - json: () => Promise.resolve({ success: true, data: 'test' }), - clone: vi.fn().mockReturnThis(), - } - }), - { preconnect: vi.fn() } - ) as typeof fetch + it('should respect skipProxy parameter and call internal routes directly even for external URLs', async () => { + const mockTool = { + id: 'test_skip_proxy', + name: 'Test Skip Proxy Tool', + description: 'A test tool to verify skipProxy behavior', + version: '1.0.0', + params: {}, + request: { + url: 'https://api.example.com/endpoint', + method: 'GET', + headers: () => ({ 'Content-Type': 'application/json' }), + }, + transformResponse: vi.fn().mockResolvedValue({ + success: true, + output: { result: 'Skipped proxy, called directly' }, + }), + } + + const originalTools = { ...tools } + ;(tools as any).test_skip_proxy = mockTool + + global.fetch = Object.assign( + vi.fn().mockImplementation(async (url) => { + expect(url).toBe('https://api.example.com/endpoint') + return { + ok: true, + status: 200, + json: () => Promise.resolve({ success: true, data: 'test' }), + clone: vi.fn().mockReturnThis(), + } + }), + { preconnect: vi.fn() } + ) as typeof fetch - const result = await executeTool('test_skip_proxy', {}, true) // skipProxy = true + const result = await executeTool('test_skip_proxy', {}, true) // skipProxy = true - expect(result.success).toBe(true) - expect(result.output.result).toBe('Skipped proxy, called directly') - expect(mockTool.transformResponse).toHaveBeenCalled() + expect(result.success).toBe(true) + expect(result.output.result).toBe('Skipped proxy, called directly') + expect(mockTool.transformResponse).toHaveBeenCalled() - Object.assign(tools, originalTools) - } - ) + Object.assign(tools, originalTools) + }) }) describe('Centralized Error Handling', () => { @@ -600,7 +608,7 @@ describe('Centralized Error Handling', () => { expect(result.error).toBe(expectedError) } - it.concurrent('should extract GraphQL error format (Linear API)', async () => { + it('should extract GraphQL error format (Linear API)', async () => { await testErrorFormat( 'GraphQL', { errors: [{ message: 'Invalid query field' }] }, @@ -608,7 +616,7 @@ describe('Centralized Error Handling', () => { ) }) - it.concurrent('should extract X/Twitter API error format', async () => { + it('should extract X/Twitter API error format', async () => { await testErrorFormat( 'X/Twitter', { errors: [{ detail: 'Rate limit exceeded' }] }, @@ -616,15 +624,15 @@ describe('Centralized Error Handling', () => { ) }) - it.concurrent('should extract Hunter API error format', async () => { + it('should extract Hunter API error format', async () => { await testErrorFormat('Hunter', { errors: [{ details: 'Invalid API key' }] }, 'Invalid API key') }) - it.concurrent('should extract direct errors array (string)', async () => { + it('should extract direct errors array (string)', async () => { await testErrorFormat('Direct string array', { errors: ['Network timeout'] }, 'Network timeout') }) - it.concurrent('should extract direct errors array (object)', async () => { + it('should extract direct errors array (object)', async () => { await testErrorFormat( 'Direct object array', { errors: [{ message: 'Validation failed' }] }, @@ -632,11 +640,11 @@ describe('Centralized Error Handling', () => { ) }) - it.concurrent('should extract OAuth error description', async () => { + it('should extract OAuth error description', async () => { await testErrorFormat('OAuth', { error_description: 'Invalid grant' }, 'Invalid grant') }) - it.concurrent('should extract SOAP fault error', async () => { + it('should extract SOAP fault error', async () => { await testErrorFormat( 'SOAP fault', { fault: { faultstring: 'Server unavailable' } }, @@ -644,7 +652,7 @@ describe('Centralized Error Handling', () => { ) }) - it.concurrent('should extract simple SOAP faultstring', async () => { + it('should extract simple SOAP faultstring', async () => { await testErrorFormat( 'Simple SOAP', { faultstring: 'Authentication failed' }, @@ -652,11 +660,11 @@ describe('Centralized Error Handling', () => { ) }) - it.concurrent('should extract Notion/Discord message format', async () => { + it('should extract Notion/Discord message format', async () => { await testErrorFormat('Notion/Discord', { message: 'Page not found' }, 'Page not found') }) - it.concurrent('should extract Airtable error object format', async () => { + it('should extract Airtable error object format', async () => { await testErrorFormat( 'Airtable', { error: { message: 'Invalid table ID' } }, @@ -664,7 +672,7 @@ describe('Centralized Error Handling', () => { ) }) - it.concurrent('should extract simple error string format', async () => { + it('should extract simple error string format', async () => { await testErrorFormat( 'Simple string', { error: 'Simple error message' }, @@ -672,7 +680,7 @@ describe('Centralized Error Handling', () => { ) }) - it.concurrent('should fall back to HTTP status when JSON parsing fails', async () => { + it('should fall back to HTTP status when JSON parsing fails', async () => { global.fetch = Object.assign( vi.fn().mockImplementation(async () => ({ ok: false, @@ -701,7 +709,7 @@ describe('Centralized Error Handling', () => { expect(result.error).toBe('Failed to parse response from function_execute: Error: Invalid JSON') }) - it.concurrent('should handle complex nested error objects', async () => { + it('should handle complex nested error objects', async () => { await testErrorFormat( 'Complex nested', { error: { code: 400, message: 'Complex validation error', details: 'Field X is invalid' } }, @@ -709,7 +717,7 @@ describe('Centralized Error Handling', () => { ) }) - it.concurrent('should handle error arrays with multiple entries (take first)', async () => { + it('should handle error arrays with multiple entries (take first)', async () => { await testErrorFormat( 'Multiple errors', { errors: [{ message: 'First error' }, { message: 'Second error' }] }, @@ -717,7 +725,7 @@ describe('Centralized Error Handling', () => { ) }) - it.concurrent('should stringify complex error objects when no message found', async () => { + it('should stringify complex error objects when no message found', async () => { const complexError = { code: 500, type: 'ServerError', context: { requestId: '123' } } await testErrorFormat( 'Complex object stringify', @@ -742,7 +750,7 @@ describe('MCP Tool Execution', () => { cleanupEnvVars() }) - it.concurrent('should execute MCP tool with valid tool ID', async () => { + it('should execute MCP tool with valid tool ID', async () => { global.fetch = Object.assign( vi.fn().mockImplementation(async (url, options) => { expect(url).toBe('http://localhost:3000/api/mcp/tools/execute') @@ -787,7 +795,7 @@ describe('MCP Tool Execution', () => { expect(result.timing).toBeDefined() }) - it.concurrent('should handle MCP tool ID parsing correctly', async () => { + it('should handle MCP tool ID parsing correctly', async () => { global.fetch = Object.assign( vi.fn().mockImplementation(async (url, options) => { const body = JSON.parse(options?.body as string) @@ -818,7 +826,7 @@ describe('MCP Tool Execution', () => { ) }) - it.concurrent('should handle MCP block arguments format', async () => { + it('should handle MCP block arguments format', async () => { global.fetch = Object.assign( vi.fn().mockImplementation(async (url, options) => { const body = JSON.parse(options?.body as string) @@ -852,7 +860,7 @@ describe('MCP Tool Execution', () => { ) }) - it.concurrent('should handle agent block MCP arguments format', async () => { + it('should handle agent block MCP arguments format', async () => { global.fetch = Object.assign( vi.fn().mockImplementation(async (url, options) => { const body = JSON.parse(options?.body as string) @@ -890,7 +898,7 @@ describe('MCP Tool Execution', () => { ) }) - it.concurrent('should handle MCP tool execution errors', async () => { + it('should handle MCP tool execution errors', async () => { global.fetch = Object.assign( vi.fn().mockImplementation(async () => ({ ok: false, @@ -920,14 +928,14 @@ describe('MCP Tool Execution', () => { expect(result.timing).toBeDefined() }) - it.concurrent('should require workspaceId for MCP tools', async () => { + it('should require workspaceId for MCP tools', async () => { const result = await executeTool('mcp-123-test_tool', { param: 'value' }) expect(result.success).toBe(false) expect(result.error).toContain('Missing workspaceId in execution context for MCP tool') }) - it.concurrent('should handle invalid MCP tool ID format', async () => { + it('should handle invalid MCP tool ID format', async () => { const mockContext6 = createMockExecutionContext() const result = await executeTool( @@ -942,7 +950,7 @@ describe('MCP Tool Execution', () => { expect(result.error).toContain('Tool not found') }) - it.concurrent('should handle MCP API network errors', async () => { + it('should handle MCP API network errors', async () => { global.fetch = Object.assign(vi.fn().mockRejectedValue(new Error('Network error')), { preconnect: vi.fn(), }) as typeof fetch diff --git a/apps/sim/tools/index.ts b/apps/sim/tools/index.ts index 1557cb2cda..7c7d0e92ae 100644 --- a/apps/sim/tools/index.ts +++ b/apps/sim/tools/index.ts @@ -408,6 +408,38 @@ function isErrorResponse( return { isError: false } } +/** + * Add internal authentication token to headers if running on server + * @param headers - Headers object to modify + * @param isInternalRoute - Whether the target URL is an internal route + * @param requestId - Request ID for logging + * @param context - Context string for logging (e.g., toolId or 'proxy') + */ +async function addInternalAuthIfNeeded( + headers: Headers | Record, + isInternalRoute: boolean, + requestId: string, + context: string +): Promise { + if (typeof window === 'undefined') { + if (isInternalRoute) { + try { + const internalToken = await generateInternalToken() + if (headers instanceof Headers) { + headers.set('Authorization', `Bearer ${internalToken}`) + } else { + headers.Authorization = `Bearer ${internalToken}` + } + logger.info(`[${requestId}] Added internal auth token for ${context}`) + } catch (error) { + logger.error(`[${requestId}] Failed to generate internal token for ${context}:`, error) + } + } else { + logger.info(`[${requestId}] Skipping internal auth token for external URL: ${context}`) + } + } +} + /** * Handle an internal/direct tool request */ @@ -448,19 +480,7 @@ async function handleInternalRequest( } const headers = new Headers(requestParams.headers) - if (typeof window === 'undefined') { - if (isInternalRoute) { - try { - const internalToken = await generateInternalToken() - headers.set('Authorization', `Bearer ${internalToken}`) - logger.info(`[${requestId}] Added internal auth token for ${toolId}`) - } catch (error) { - logger.error(`[${requestId}] Failed to generate internal token for ${toolId}:`, error) - } - } else { - logger.info(`[${requestId}] Skipping internal auth token for external URL: ${endpointUrl}`) - } - } + await addInternalAuthIfNeeded(headers, isInternalRoute, requestId, toolId) // Prepare request options const requestOptions = { @@ -652,9 +672,12 @@ async function handleProxyRequest( const proxyUrl = new URL('/api/proxy', baseUrl).toString() try { + const headers: Record = { 'Content-Type': 'application/json' } + await addInternalAuthIfNeeded(headers, true, requestId, `proxy:${toolId}`) + const response = await fetch(proxyUrl, { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers, body: JSON.stringify({ toolId, params, executionContext }), }) @@ -669,9 +692,7 @@ async function handleProxyRequest( let errorMessage = `HTTP error ${response.status}: ${response.statusText}` try { - // Try to parse as JSON for more details const errorJson = JSON.parse(errorText) - // Enhanced error extraction to match internal API patterns errorMessage = // Primary error patterns errorJson.errors?.[0]?.message || diff --git a/apps/sim/tools/openai/image.ts b/apps/sim/tools/openai/image.ts index 874a5e50d3..90cf2a1f42 100644 --- a/apps/sim/tools/openai/image.ts +++ b/apps/sim/tools/openai/image.ts @@ -127,10 +127,23 @@ export const imageTool: ToolConfig = { const proxyUrl = new URL('/api/proxy/image', baseUrl) proxyUrl.searchParams.append('url', imageUrl) + const headers: Record = { + Accept: 'image/*, */*', + } + + if (typeof window === 'undefined') { + const { generateInternalToken } = await import('@/lib/auth/internal') + try { + const token = await generateInternalToken() + headers.Authorization = `Bearer ${token}` + logger.info('Added internal auth token for image proxy request') + } catch (error) { + logger.error('Failed to generate internal token for image proxy:', error) + } + } + const imageResponse = await fetch(proxyUrl.toString(), { - headers: { - Accept: 'image/*, */*', - }, + headers, cache: 'no-store', })