fix(elevenlabs): added internal auth helper for proxy routes (#1732)

* fix(elevenlabs): added internal auth helper for proxy routes

* remove concurrent tests

* build fix
This commit is contained in:
Waleed
2025-10-25 17:54:27 -07:00
committed by GitHub
parent ad7b791242
commit c552bb9c5f
8 changed files with 253 additions and 175 deletions

View File

@@ -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 })

View File

@@ -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()

View File

@@ -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 }
)
}
}

View File

@@ -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

View File

@@ -51,6 +51,16 @@ export const elevenLabsTtsTool: ToolConfig<ElevenLabsTtsParams, ElevenLabsTtsRes
transformResponse: async (response: Response) => {
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: {

View File

@@ -31,7 +31,7 @@ const createMockExecutionContext = (overrides?: Partial<ExecutionContext>): 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
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()
// Restore original tools
Object.assign(tools, originalTools)
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' },
}),
}
)
it.concurrent('should detect external routes (full URLs) and use proxy', async () => {
// 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)
expect(result.success).toBe(true)
expect(result.output.result).toBe('Internal route success')
expect(mockTool.transformResponse).toHaveBeenCalled()
// Restore original tools
Object.assign(tools, originalTools)
})
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
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()
Object.assign(tools, originalTools)
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
expect(result.success).toBe(true)
expect(result.output.result).toBe('Skipped proxy, called directly')
expect(mockTool.transformResponse).toHaveBeenCalled()
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

View File

@@ -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<string, string>,
isInternalRoute: boolean,
requestId: string,
context: string
): Promise<void> {
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<string, string> = { '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 ||

View File

@@ -127,10 +127,23 @@ export const imageTool: ToolConfig = {
const proxyUrl = new URL('/api/proxy/image', baseUrl)
proxyUrl.searchParams.append('url', imageUrl)
const headers: Record<string, string> = {
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',
})