diff --git a/apps/sim/executor/handlers/agent/agent-handler.test.ts b/apps/sim/executor/handlers/agent/agent-handler.test.ts index 7c13d17db5f..0132ff08c31 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.test.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.test.ts @@ -91,17 +91,13 @@ vi.mock('@/providers', () => ({ vi.mock('@/executor/utils/http', () => ({ buildAuthHeaders: vi.fn().mockResolvedValue({ 'Content-Type': 'application/json' }), - buildAPIUrl: vi.fn((path: string, params?: Record) => { - const url = new URL(path, 'http://localhost:3000') - if (params) { - for (const [key, value] of Object.entries(params)) { - if (value !== undefined && value !== null) { - url.searchParams.set(key, value) - } - } - } - return url - }), + internalApiUrl: vi.fn( + (segments: TemplateStringsArray, ...values: unknown[]) => + new URL( + String.raw({ raw: segments }, ...values.map((v) => encodeURIComponent(String(v)))), + 'http://localhost:3000' + ) + ), extractAPIErrorMessage: vi.fn(async (response: Response) => { const defaultMessage = `API request failed with status ${response.status}` try { diff --git a/apps/sim/executor/handlers/agent/agent-handler.ts b/apps/sim/executor/handlers/agent/agent-handler.ts index d1e972180da..85c8f43d53a 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.ts @@ -59,7 +59,7 @@ import type { import { parseResponseFormat } from '@/executor/handlers/shared/response-format' import type { BlockHandler, ExecutionContext, StreamingExecution } from '@/executor/types' import { collectBlockData } from '@/executor/utils/block-data' -import { buildAPIUrl, buildAuthHeaders } from '@/executor/utils/http' +import { buildAuthHeaders, internalApiUrl } from '@/executor/utils/http' import { stringifyJSON } from '@/executor/utils/json' import { projectResolvedSecretDiagnosticContent } from '@/executor/utils/resolved-secret-content-projection' import { prepareResolvedSecretProjectedInputs } from '@/executor/utils/resolved-secret-input-projection' @@ -1212,12 +1212,11 @@ export class AgentBlockHandler implements BlockHandler { } const headers = await buildAuthHeaders(ctx.userId) - const url = buildAPIUrl('/api/mcp/tools/discover', { - serverId, - workspaceId: ctx.workspaceId, - workflowId: ctx.workflowId, - ...(ctx.userId ? { userId: ctx.userId } : {}), - }) + const url = internalApiUrl`/api/mcp/tools/discover` + url.searchParams.set('serverId', serverId) + url.searchParams.set('workspaceId', ctx.workspaceId) + url.searchParams.set('workflowId', ctx.workflowId) + if (ctx.userId) url.searchParams.set('userId', ctx.userId) const maxAttempts = 2 for (let attempt = 0; attempt < maxAttempts; attempt++) { diff --git a/apps/sim/executor/handlers/evaluator/evaluator-handler.ts b/apps/sim/executor/handlers/evaluator/evaluator-handler.ts index 16ba1c8e831..d5ffa641582 100644 --- a/apps/sim/executor/handlers/evaluator/evaluator-handler.ts +++ b/apps/sim/executor/handlers/evaluator/evaluator-handler.ts @@ -15,7 +15,7 @@ import type { BlockOutput } from '@/blocks/types' import { validateModelProvider } from '@/ee/access-control/utils/permission-check' import { BlockType, DEFAULTS, EVALUATOR } from '@/executor/constants' import type { BlockHandler, ExecutionContext } from '@/executor/types' -import { buildAPIUrl, buildAuthHeaders, extractAPIErrorMessage } from '@/executor/utils/http' +import { buildAuthHeaders, extractAPIErrorMessage, internalApiUrl } from '@/executor/utils/http' import { isJSONString, parseJSON, stringifyJSON } from '@/executor/utils/json' import { projectResolvedSecretDiagnosticError } from '@/executor/utils/resolved-secret-content-projection' import type { @@ -186,7 +186,8 @@ export class EvaluatorBlockHandler implements BlockHandler { } try { - const url = buildAPIUrl('/api/providers', ctx.userId ? { userId: ctx.userId } : {}) + const url = internalApiUrl`/api/providers` + if (ctx.userId) url.searchParams.set('userId', ctx.userId) const providerRequest: ProviderRequest = { model, diff --git a/apps/sim/executor/handlers/mothership/mothership-handler.test.ts b/apps/sim/executor/handlers/mothership/mothership-handler.test.ts index 6c9a752ec05..c9724324a6f 100644 --- a/apps/sim/executor/handlers/mothership/mothership-handler.test.ts +++ b/apps/sim/executor/handlers/mothership/mothership-handler.test.ts @@ -32,7 +32,7 @@ const PRIVATE_PROVENANCE = { const { mockAreModelSafeWorkspaceFileKeys, mockBuildAuthHeaders, - mockBuildAPIUrl, + mockInternalApiUrl, mockExtractAPIErrorMessage, mockGenerateId, mockIsExecutionCancelled, @@ -41,7 +41,7 @@ const { } = vi.hoisted(() => ({ mockAreModelSafeWorkspaceFileKeys: vi.fn(), mockBuildAuthHeaders: vi.fn(), - mockBuildAPIUrl: vi.fn(), + mockInternalApiUrl: vi.fn(), mockExtractAPIErrorMessage: vi.fn(), mockGenerateId: vi.fn(), mockIsExecutionCancelled: vi.fn(), @@ -57,7 +57,7 @@ vi.mock('@/lib/uploads/contexts/workspace/workspace-file-secret-provenance', () vi.mock('@/executor/utils/http', () => ({ buildAuthHeaders: mockBuildAuthHeaders, - buildAPIUrl: mockBuildAPIUrl, + internalApiUrl: mockInternalApiUrl, extractAPIErrorMessage: mockExtractAPIErrorMessage, })) @@ -155,7 +155,7 @@ describe('MothershipBlockHandler', () => { vi.stubGlobal('fetch', fetchMock) mockBuildAuthHeaders.mockResolvedValue({ Authorization: 'Bearer internal' }) - mockBuildAPIUrl.mockReturnValue(new URL('/api/mothership/execute', 'http://localhost:3000')) + mockInternalApiUrl.mockReturnValue(new URL('/api/mothership/execute', 'http://localhost:3000')) mockExtractAPIErrorMessage.mockResolvedValue('boom') mockGenerateId.mockReset() mockIsExecutionCancelled.mockReset() diff --git a/apps/sim/executor/handlers/mothership/mothership-handler.ts b/apps/sim/executor/handlers/mothership/mothership-handler.ts index 1c77fdaccb7..f7699757d8d 100644 --- a/apps/sim/executor/handlers/mothership/mothership-handler.ts +++ b/apps/sim/executor/handlers/mothership/mothership-handler.ts @@ -42,7 +42,7 @@ import type { NormalizedBlockOutput, StreamingExecution, } from '@/executor/types' -import { buildAPIUrl, buildAuthHeaders, extractAPIErrorMessage } from '@/executor/utils/http' +import { buildAuthHeaders, extractAPIErrorMessage, internalApiUrl } from '@/executor/utils/http' import type { ResolvedSecretInputPath, ResolvedSecretTraceRegistry, @@ -796,7 +796,7 @@ export class MothershipBlockHandler implements BlockHandler { requestId ) - const url = buildAPIUrl('/api/mothership/execute') + const url = internalApiUrl`/api/mothership/execute` const headers = await buildAuthHeaders(ctx.userId) headers.Accept = 'application/x-ndjson' headers[MOTHERSHIP_EXECUTE_STREAM_HEADER] = MOTHERSHIP_EXECUTE_STREAM_VALUE diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.test.ts b/apps/sim/executor/handlers/workflow/workflow-handler.test.ts index 1f50fd48702..1de0c653bbc 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.test.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.test.ts @@ -183,7 +183,13 @@ vi.mock('@/lib/auth/internal', () => ({ vi.mock('@/executor/utils/http', () => ({ buildAuthHeaders: vi.fn().mockResolvedValue({ 'Content-Type': 'application/json' }), - buildAPIUrl: vi.fn((path: string) => new URL(path, 'http://localhost:3000')), + internalApiUrl: vi.fn( + (segments: TemplateStringsArray, ...values: unknown[]) => + new URL( + String.raw({ raw: segments }, ...values.map((v) => encodeURIComponent(String(v)))), + 'http://localhost:3000' + ) + ), extractAPIErrorMessage: vi.fn(async (response: Response) => { const defaultMessage = `API request failed with status ${response.status}` try { diff --git a/apps/sim/executor/handlers/workflow/workflow-handler.ts b/apps/sim/executor/handlers/workflow/workflow-handler.ts index e795c2b99e9..3a9463721a3 100644 --- a/apps/sim/executor/handlers/workflow/workflow-handler.ts +++ b/apps/sim/executor/handlers/workflow/workflow-handler.ts @@ -42,7 +42,7 @@ import { type StreamingExecution, } from '@/executor/types' import { hasExecutionResult } from '@/executor/utils/errors' -import { buildAPIUrl, buildAuthHeaders } from '@/executor/utils/http' +import { buildAuthHeaders, internalApiUrl } from '@/executor/utils/http' import { getIterationContext } from '@/executor/utils/iteration-context' import { parseJSON } from '@/executor/utils/json' import { lazyCleanupInputMapping } from '@/executor/utils/lazy-cleanup' @@ -952,7 +952,7 @@ export class WorkflowBlockHandler implements BlockHandler { private async loadChildWorkflow(workflowId: string, userId?: string) { const headers = await buildAuthHeaders(userId) - const url = buildAPIUrl(`/api/workflows/${workflowId}`) + const url = internalApiUrl`/api/workflows/${workflowId}` const response = await fetch(url.toString(), { headers }) @@ -1015,7 +1015,7 @@ export class WorkflowBlockHandler implements BlockHandler { private async checkChildDeployment(workflowId: string, userId?: string): Promise { try { const headers = await buildAuthHeaders(userId) - const url = buildAPIUrl(`/api/workflows/${workflowId}/deployed`) + const url = internalApiUrl`/api/workflows/${workflowId}/deployed` const response = await fetch(url.toString(), { headers, @@ -1037,7 +1037,7 @@ export class WorkflowBlockHandler implements BlockHandler { private async loadChildWorkflowDeployed(workflowId: string, userId?: string) { const headers = await buildAuthHeaders(userId) - const deployedUrl = buildAPIUrl(`/api/workflows/${workflowId}/deployed`) + const deployedUrl = internalApiUrl`/api/workflows/${workflowId}/deployed` const deployedRes = await fetch(deployedUrl.toString(), { headers, @@ -1058,7 +1058,7 @@ export class WorkflowBlockHandler implements BlockHandler { throw new Error(`Deployed state missing or invalid for child workflow ${workflowId}`) } - const metaUrl = buildAPIUrl(`/api/workflows/${workflowId}`) + const metaUrl = internalApiUrl`/api/workflows/${workflowId}` const metaRes = await fetch(metaUrl.toString(), { headers, cache: 'no-store', diff --git a/apps/sim/executor/utils/http.test.ts b/apps/sim/executor/utils/http.test.ts new file mode 100644 index 00000000000..1cf57695900 --- /dev/null +++ b/apps/sim/executor/utils/http.test.ts @@ -0,0 +1,54 @@ +/** + * @vitest-environment node + */ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { mockGetInternalApiBaseUrl } = vi.hoisted(() => ({ + mockGetInternalApiBaseUrl: vi.fn(), +})) + +vi.mock('@/lib/core/utils/urls', () => ({ + getInternalApiBaseUrl: mockGetInternalApiBaseUrl, +})) + +vi.mock('@/lib/auth/internal', () => ({ + generateInternalToken: vi.fn().mockResolvedValue('token'), +})) + +import { internalApiUrl } from '@/executor/utils/http' + +describe('internalApiUrl', () => { + beforeEach(() => { + vi.clearAllMocks() + mockGetInternalApiBaseUrl.mockReturnValue('http://internal.sim.local') + }) + + it('resolves an internal route against the internal base URL', () => { + const url = internalApiUrl`/api/workflows/${'wf-1'}` + + expect(url.toString()).toBe('http://internal.sim.local/api/workflows/wf-1') + }) + + it('encodes an interpolated id so it cannot widen the path', () => { + const url = internalApiUrl`/api/table/${'../../admin/secrets'}/rows` + + expect(url.pathname).toBe('/api/table/..%2F..%2Fadmin%2Fsecrets/rows') + }) + + it('encodes a query-shaped id rather than letting it add params', () => { + const url = internalApiUrl`/api/table/${'t-1?workspaceId=other'}` + + expect(url.searchParams.get('workspaceId')).toBeNull() + expect(url.pathname).toBe('/api/table/t-1%3FworkspaceId%3Dother') + }) + + it('rejects a route that is not an internal API path', () => { + expect(() => internalApiUrl`/health`).toThrow(/must start with \/api\//) + }) + + it('rejects an interpolated absolute URL, which would escape the internal base', () => { + expect(() => internalApiUrl`${'https://attacker.example/api/x'}`).toThrow( + /must start with \/api\// + ) + }) +}) diff --git a/apps/sim/executor/utils/http.ts b/apps/sim/executor/utils/http.ts index 57ea632a41b..088e4d724ab 100644 --- a/apps/sim/executor/utils/http.ts +++ b/apps/sim/executor/utils/http.ts @@ -1,5 +1,5 @@ import { generateInternalToken } from '@/lib/auth/internal' -import { getBaseUrl, getInternalApiBaseUrl } from '@/lib/core/utils/urls' +import { getInternalApiBaseUrl } from '@/lib/core/utils/urls' import { HTTP } from '@/executor/constants' export async function buildAuthHeaders(userId?: string): Promise> { @@ -15,19 +15,33 @@ export async function buildAuthHeaders(userId?: string): Promise): URL { - const baseUrl = path.startsWith('/api/') ? getInternalApiBaseUrl() : getBaseUrl() - const url = new URL(path, baseUrl) +/** + * Builds a URL for one of Sim's own API routes, as a tagged template: + * + * ```ts + * const url = internalApiUrl`/api/workflows/${workflowId}/deployed` + * ``` + * + * Callers pair this with {@link buildAuthHeaders}, so the request carries an internal token for + * the executing user — which makes it critical that the *route* comes from this module's source + * and only resource ids come from data. The template's literal segments provide that: they are + * fixed at author time, and every interpolated value is percent-encoded, so an id can never widen + * the path into a different route. + * + * @throws when the resolved path is not a relative `/api/` path, which would otherwise send an + * internally-signed request somewhere the caller did not intend. + */ +export function internalApiUrl(segments: TemplateStringsArray, ...values: unknown[]): URL { + let path = segments[0] + for (const [index, value] of values.entries()) { + path += encodeURIComponent(String(value)) + segments[index + 1] + } - if (params) { - for (const [key, value] of Object.entries(params)) { - if (value !== undefined && value !== null) { - url.searchParams.set(key, value) - } - } + if (!path.startsWith('/api/')) { + throw new Error(`Internal API path must start with /api/: ${path}`) } - return url + return new URL(path, getInternalApiBaseUrl()) } export async function extractAPIErrorMessage(response: Response): Promise { diff --git a/apps/sim/providers/utils.ts b/apps/sim/providers/utils.ts index c6017dba864..bfa6213ef7d 100644 --- a/apps/sim/providers/utils.ts +++ b/apps/sim/providers/utils.ts @@ -84,10 +84,10 @@ async function fetchWorkflowMetadata( workflowId: string ): Promise<{ name: string; description: string | null } | null> { try { - const { buildAuthHeaders, buildAPIUrl } = await import('@/executor/utils/http') + const { buildAuthHeaders, internalApiUrl } = await import('@/executor/utils/http') const headers = await buildAuthHeaders() - const url = buildAPIUrl(`/api/workflows/${workflowId}`) + const url = internalApiUrl`/api/workflows/${workflowId}` const response = await fetch(url.toString(), { headers }) if (!response.ok) { diff --git a/apps/sim/tools/agiloft/attachment_info.ts b/apps/sim/tools/agiloft/attachment_info.ts index 4a577ca90db..048cb2fcd79 100644 --- a/apps/sim/tools/agiloft/attachment_info.ts +++ b/apps/sim/tools/agiloft/attachment_info.ts @@ -59,7 +59,7 @@ export const agiloftAttachmentInfoTool: ToolConfig< }, request: { - url: () => '/api/tools/agiloft/attachment_info', + url: '/api/tools/agiloft/attachment_info', method: 'POST', headers: () => ({ 'Content-Type': 'application/json' }), body: (params) => ({ diff --git a/apps/sim/tools/agiloft/create_record.ts b/apps/sim/tools/agiloft/create_record.ts index 216008e354a..c40569a2286 100644 --- a/apps/sim/tools/agiloft/create_record.ts +++ b/apps/sim/tools/agiloft/create_record.ts @@ -49,7 +49,7 @@ export const agiloftCreateRecordTool: ToolConfig '/api/tools/agiloft/create_record', + url: '/api/tools/agiloft/create_record', method: 'POST', headers: () => ({ 'Content-Type': 'application/json' }), body: (params) => ({ diff --git a/apps/sim/tools/agiloft/delete_record.ts b/apps/sim/tools/agiloft/delete_record.ts index f0599da85ef..e520651beac 100644 --- a/apps/sim/tools/agiloft/delete_record.ts +++ b/apps/sim/tools/agiloft/delete_record.ts @@ -48,7 +48,7 @@ export const agiloftDeleteRecordTool: ToolConfig '/api/tools/agiloft/delete_record', + url: '/api/tools/agiloft/delete_record', method: 'POST', headers: () => ({ 'Content-Type': 'application/json' }), body: (params) => ({ diff --git a/apps/sim/tools/agiloft/get_choice_line_id.ts b/apps/sim/tools/agiloft/get_choice_line_id.ts index d2568933123..c4c45e18985 100644 --- a/apps/sim/tools/agiloft/get_choice_line_id.ts +++ b/apps/sim/tools/agiloft/get_choice_line_id.ts @@ -60,7 +60,7 @@ export const agiloftGetChoiceLineIdTool: ToolConfig< }, request: { - url: () => '/api/tools/agiloft/get_choice_line_id', + url: '/api/tools/agiloft/get_choice_line_id', method: 'POST', headers: () => ({ 'Content-Type': 'application/json' }), body: (params) => ({ diff --git a/apps/sim/tools/agiloft/lock_record.ts b/apps/sim/tools/agiloft/lock_record.ts index 4497d88da69..fb35b676041 100644 --- a/apps/sim/tools/agiloft/lock_record.ts +++ b/apps/sim/tools/agiloft/lock_record.ts @@ -53,7 +53,7 @@ export const agiloftLockRecordTool: ToolConfig '/api/tools/agiloft/lock_record', + url: '/api/tools/agiloft/lock_record', method: 'POST', headers: () => ({ 'Content-Type': 'application/json' }), body: (params) => ({ diff --git a/apps/sim/tools/agiloft/read_record.ts b/apps/sim/tools/agiloft/read_record.ts index dcb495c61dc..ef3d84e66e0 100644 --- a/apps/sim/tools/agiloft/read_record.ts +++ b/apps/sim/tools/agiloft/read_record.ts @@ -53,7 +53,7 @@ export const agiloftReadRecordTool: ToolConfig '/api/tools/agiloft/read_record', + url: '/api/tools/agiloft/read_record', method: 'POST', headers: () => ({ 'Content-Type': 'application/json' }), body: (params) => ({ diff --git a/apps/sim/tools/agiloft/remove_attachment.ts b/apps/sim/tools/agiloft/remove_attachment.ts index 8eabc5e476b..65741329657 100644 --- a/apps/sim/tools/agiloft/remove_attachment.ts +++ b/apps/sim/tools/agiloft/remove_attachment.ts @@ -65,7 +65,7 @@ export const agiloftRemoveAttachmentTool: ToolConfig< }, request: { - url: () => '/api/tools/agiloft/remove_attachment', + url: '/api/tools/agiloft/remove_attachment', method: 'POST', headers: () => ({ 'Content-Type': 'application/json' }), body: (params) => ({ diff --git a/apps/sim/tools/agiloft/saved_search.ts b/apps/sim/tools/agiloft/saved_search.ts index 6199c18ef69..0349d840805 100644 --- a/apps/sim/tools/agiloft/saved_search.ts +++ b/apps/sim/tools/agiloft/saved_search.ts @@ -44,7 +44,7 @@ export const agiloftSavedSearchTool: ToolConfig< }, request: { - url: () => '/api/tools/agiloft/saved_search', + url: '/api/tools/agiloft/saved_search', method: 'POST', headers: () => ({ 'Content-Type': 'application/json' }), body: (params) => ({ diff --git a/apps/sim/tools/agiloft/search_records.ts b/apps/sim/tools/agiloft/search_records.ts index 8cbc759f3a4..c8be4799e8a 100644 --- a/apps/sim/tools/agiloft/search_records.ts +++ b/apps/sim/tools/agiloft/search_records.ts @@ -69,7 +69,7 @@ export const agiloftSearchRecordsTool: ToolConfig< }, request: { - url: () => '/api/tools/agiloft/search_records', + url: '/api/tools/agiloft/search_records', method: 'POST', headers: () => ({ 'Content-Type': 'application/json' }), body: (params) => ({ diff --git a/apps/sim/tools/agiloft/select_records.ts b/apps/sim/tools/agiloft/select_records.ts index 72af1539720..e5f7e34d741 100644 --- a/apps/sim/tools/agiloft/select_records.ts +++ b/apps/sim/tools/agiloft/select_records.ts @@ -51,7 +51,7 @@ export const agiloftSelectRecordsTool: ToolConfig< }, request: { - url: () => '/api/tools/agiloft/select_records', + url: '/api/tools/agiloft/select_records', method: 'POST', headers: () => ({ 'Content-Type': 'application/json' }), body: (params) => ({ diff --git a/apps/sim/tools/agiloft/update_record.ts b/apps/sim/tools/agiloft/update_record.ts index 4b887e50fa3..b8dfd1bab73 100644 --- a/apps/sim/tools/agiloft/update_record.ts +++ b/apps/sim/tools/agiloft/update_record.ts @@ -55,7 +55,7 @@ export const agiloftUpdateRecordTool: ToolConfig '/api/tools/agiloft/update_record', + url: '/api/tools/agiloft/update_record', method: 'POST', headers: () => ({ 'Content-Type': 'application/json' }), body: (params) => ({ diff --git a/apps/sim/tools/confluence/add_label.ts b/apps/sim/tools/confluence/add_label.ts index db931b3cf0d..ee83acc0dc2 100644 --- a/apps/sim/tools/confluence/add_label.ts +++ b/apps/sim/tools/confluence/add_label.ts @@ -75,7 +75,7 @@ export const confluenceAddLabelTool: ToolConfig< }, request: { - url: () => '/api/tools/confluence/labels', + url: '/api/tools/confluence/labels', method: 'POST', headers: (params: ConfluenceAddLabelParams) => ({ Accept: 'application/json', diff --git a/apps/sim/tools/confluence/create_blogpost.ts b/apps/sim/tools/confluence/create_blogpost.ts index b39e91b7f18..5044bbc12a6 100644 --- a/apps/sim/tools/confluence/create_blogpost.ts +++ b/apps/sim/tools/confluence/create_blogpost.ts @@ -91,7 +91,7 @@ export const confluenceCreateBlogPostTool: ToolConfig< }, request: { - url: () => '/api/tools/confluence/blogposts', + url: '/api/tools/confluence/blogposts', method: 'POST', headers: (params: ConfluenceCreateBlogPostParams) => ({ Accept: 'application/json', diff --git a/apps/sim/tools/confluence/create_comment.ts b/apps/sim/tools/confluence/create_comment.ts index aa18a5c4a0b..dfba3559897 100644 --- a/apps/sim/tools/confluence/create_comment.ts +++ b/apps/sim/tools/confluence/create_comment.ts @@ -66,7 +66,7 @@ export const confluenceCreateCommentTool: ToolConfig< }, request: { - url: () => '/api/tools/confluence/comments', + url: '/api/tools/confluence/comments', method: 'POST', headers: (params: ConfluenceCreateCommentParams) => { return { diff --git a/apps/sim/tools/confluence/create_page.ts b/apps/sim/tools/confluence/create_page.ts index 7a4fec8a846..a96bebd61a3 100644 --- a/apps/sim/tools/confluence/create_page.ts +++ b/apps/sim/tools/confluence/create_page.ts @@ -87,7 +87,7 @@ export const confluenceCreatePageTool: ToolConfig< }, request: { - url: () => '/api/tools/confluence/create-page', + url: '/api/tools/confluence/create-page', method: 'POST', headers: (params: ConfluenceCreatePageParams) => { return { diff --git a/apps/sim/tools/confluence/create_page_property.ts b/apps/sim/tools/confluence/create_page_property.ts index 36ebfb04a06..f1f0393ab86 100644 --- a/apps/sim/tools/confluence/create_page_property.ts +++ b/apps/sim/tools/confluence/create_page_property.ts @@ -79,7 +79,7 @@ export const confluenceCreatePagePropertyTool: ToolConfig< }, request: { - url: () => '/api/tools/confluence/page-properties', + url: '/api/tools/confluence/page-properties', method: 'POST', headers: (params: ConfluenceCreatePagePropertyParams) => ({ Accept: 'application/json', diff --git a/apps/sim/tools/confluence/create_space.ts b/apps/sim/tools/confluence/create_space.ts index f2d8b8a734a..cbf0dd2c3f4 100644 --- a/apps/sim/tools/confluence/create_space.ts +++ b/apps/sim/tools/confluence/create_space.ts @@ -80,7 +80,7 @@ export const confluenceCreateSpaceTool: ToolConfig< }, request: { - url: () => '/api/tools/confluence/space', + url: '/api/tools/confluence/space', method: 'POST', headers: (params: ConfluenceCreateSpaceParams) => ({ Accept: 'application/json', diff --git a/apps/sim/tools/confluence/create_space_property.ts b/apps/sim/tools/confluence/create_space_property.ts index c702f63539a..7b865f5c11c 100644 --- a/apps/sim/tools/confluence/create_space_property.ts +++ b/apps/sim/tools/confluence/create_space_property.ts @@ -76,7 +76,7 @@ export const confluenceCreateSpacePropertyTool: ToolConfig< }, request: { - url: () => '/api/tools/confluence/space-properties', + url: '/api/tools/confluence/space-properties', method: 'POST', headers: (params: ConfluenceCreateSpacePropertyParams) => ({ Accept: 'application/json', diff --git a/apps/sim/tools/confluence/delete_attachment.ts b/apps/sim/tools/confluence/delete_attachment.ts index 37d2d093d80..dd38ad325e4 100644 --- a/apps/sim/tools/confluence/delete_attachment.ts +++ b/apps/sim/tools/confluence/delete_attachment.ts @@ -59,7 +59,7 @@ export const confluenceDeleteAttachmentTool: ToolConfig< }, request: { - url: () => '/api/tools/confluence/attachment', + url: '/api/tools/confluence/attachment', method: 'DELETE', headers: (params: ConfluenceDeleteAttachmentParams) => { return { diff --git a/apps/sim/tools/confluence/delete_blogpost.ts b/apps/sim/tools/confluence/delete_blogpost.ts index c53562cf283..ed0efca33c3 100644 --- a/apps/sim/tools/confluence/delete_blogpost.ts +++ b/apps/sim/tools/confluence/delete_blogpost.ts @@ -60,7 +60,7 @@ export const confluenceDeleteBlogPostTool: ToolConfig< }, request: { - url: () => '/api/tools/confluence/blogposts', + url: '/api/tools/confluence/blogposts', method: 'DELETE', headers: (params: ConfluenceDeleteBlogPostParams) => ({ Accept: 'application/json', diff --git a/apps/sim/tools/confluence/delete_comment.ts b/apps/sim/tools/confluence/delete_comment.ts index 6564181dfe0..e9fafe04c8e 100644 --- a/apps/sim/tools/confluence/delete_comment.ts +++ b/apps/sim/tools/confluence/delete_comment.ts @@ -59,7 +59,7 @@ export const confluenceDeleteCommentTool: ToolConfig< }, request: { - url: () => '/api/tools/confluence/comment', + url: '/api/tools/confluence/comment', method: 'DELETE', headers: (params: ConfluenceDeleteCommentParams) => { return { diff --git a/apps/sim/tools/confluence/delete_label.ts b/apps/sim/tools/confluence/delete_label.ts index 2f92766fc67..0f368e8310e 100644 --- a/apps/sim/tools/confluence/delete_label.ts +++ b/apps/sim/tools/confluence/delete_label.ts @@ -68,7 +68,7 @@ export const confluenceDeleteLabelTool: ToolConfig< }, request: { - url: () => '/api/tools/confluence/labels', + url: '/api/tools/confluence/labels', method: 'DELETE', headers: (params: ConfluenceDeleteLabelParams) => ({ Accept: 'application/json', diff --git a/apps/sim/tools/confluence/delete_page.ts b/apps/sim/tools/confluence/delete_page.ts index a648a2b37c5..69c9b6fb8c2 100644 --- a/apps/sim/tools/confluence/delete_page.ts +++ b/apps/sim/tools/confluence/delete_page.ts @@ -68,7 +68,7 @@ export const confluenceDeletePageTool: ToolConfig< }, request: { - url: (params: ConfluenceDeletePageParams) => '/api/tools/confluence/page', + url: '/api/tools/confluence/page', method: 'DELETE', headers: (params: ConfluenceDeletePageParams) => { return { diff --git a/apps/sim/tools/confluence/delete_page_property.ts b/apps/sim/tools/confluence/delete_page_property.ts index d7b6c5fbb49..84d6a48412d 100644 --- a/apps/sim/tools/confluence/delete_page_property.ts +++ b/apps/sim/tools/confluence/delete_page_property.ts @@ -68,7 +68,7 @@ export const confluenceDeletePagePropertyTool: ToolConfig< }, request: { - url: () => '/api/tools/confluence/page-properties', + url: '/api/tools/confluence/page-properties', method: 'DELETE', headers: (params: ConfluenceDeletePagePropertyParams) => ({ Accept: 'application/json', diff --git a/apps/sim/tools/confluence/delete_space.ts b/apps/sim/tools/confluence/delete_space.ts index e6a2fc9d172..bf0420b8148 100644 --- a/apps/sim/tools/confluence/delete_space.ts +++ b/apps/sim/tools/confluence/delete_space.ts @@ -62,7 +62,7 @@ export const confluenceDeleteSpaceTool: ToolConfig< }, request: { - url: () => '/api/tools/confluence/space', + url: '/api/tools/confluence/space', method: 'DELETE', headers: (params: ConfluenceDeleteSpaceParams) => ({ Accept: 'application/json', diff --git a/apps/sim/tools/confluence/delete_space_property.ts b/apps/sim/tools/confluence/delete_space_property.ts index 9c69431aac4..b2b86da16bd 100644 --- a/apps/sim/tools/confluence/delete_space_property.ts +++ b/apps/sim/tools/confluence/delete_space_property.ts @@ -68,7 +68,7 @@ export const confluenceDeleteSpacePropertyTool: ToolConfig< }, request: { - url: () => '/api/tools/confluence/space-properties', + url: '/api/tools/confluence/space-properties', method: 'POST', headers: (params: ConfluenceDeleteSpacePropertyParams) => ({ Accept: 'application/json', diff --git a/apps/sim/tools/confluence/get_blogpost.ts b/apps/sim/tools/confluence/get_blogpost.ts index 94c9b02de7c..cb3af928b40 100644 --- a/apps/sim/tools/confluence/get_blogpost.ts +++ b/apps/sim/tools/confluence/get_blogpost.ts @@ -84,7 +84,7 @@ export const confluenceGetBlogPostTool: ToolConfig< }, request: { - url: () => '/api/tools/confluence/blogposts', + url: '/api/tools/confluence/blogposts', method: 'POST', headers: (params: ConfluenceGetBlogPostParams) => ({ Accept: 'application/json', diff --git a/apps/sim/tools/confluence/get_page_ancestors.ts b/apps/sim/tools/confluence/get_page_ancestors.ts index 20b7be3ca2c..4f5d41b089b 100644 --- a/apps/sim/tools/confluence/get_page_ancestors.ts +++ b/apps/sim/tools/confluence/get_page_ancestors.ts @@ -74,7 +74,7 @@ export const confluenceGetPageAncestorsTool: ToolConfig< }, request: { - url: () => '/api/tools/confluence/page-ancestors', + url: '/api/tools/confluence/page-ancestors', method: 'POST', headers: (params: ConfluenceGetPageAncestorsParams) => ({ Accept: 'application/json', diff --git a/apps/sim/tools/confluence/get_page_children.ts b/apps/sim/tools/confluence/get_page_children.ts index 7ca7ca10eda..773848ebcc2 100644 --- a/apps/sim/tools/confluence/get_page_children.ts +++ b/apps/sim/tools/confluence/get_page_children.ts @@ -83,7 +83,7 @@ export const confluenceGetPageChildrenTool: ToolConfig< }, request: { - url: () => '/api/tools/confluence/page-children', + url: '/api/tools/confluence/page-children', method: 'POST', headers: (params: ConfluenceGetPageChildrenParams) => ({ Accept: 'application/json', diff --git a/apps/sim/tools/confluence/get_page_descendants.ts b/apps/sim/tools/confluence/get_page_descendants.ts index a9e0bc5a323..471451d2299 100644 --- a/apps/sim/tools/confluence/get_page_descendants.ts +++ b/apps/sim/tools/confluence/get_page_descendants.ts @@ -84,7 +84,7 @@ export const confluenceGetPageDescendantsTool: ToolConfig< }, request: { - url: () => '/api/tools/confluence/page-descendants', + url: '/api/tools/confluence/page-descendants', method: 'POST', headers: (params: ConfluenceGetPageDescendantsParams) => ({ Accept: 'application/json', diff --git a/apps/sim/tools/confluence/get_page_version.ts b/apps/sim/tools/confluence/get_page_version.ts index dc496b38a24..29f657b04fc 100644 --- a/apps/sim/tools/confluence/get_page_version.ts +++ b/apps/sim/tools/confluence/get_page_version.ts @@ -89,7 +89,7 @@ export const confluenceGetPageVersionTool: ToolConfig< }, request: { - url: () => '/api/tools/confluence/page-versions', + url: '/api/tools/confluence/page-versions', method: 'POST', headers: (params: ConfluenceGetPageVersionParams) => ({ Accept: 'application/json', diff --git a/apps/sim/tools/confluence/get_pages_by_label.ts b/apps/sim/tools/confluence/get_pages_by_label.ts index af67210a0b0..41e599fb13e 100644 --- a/apps/sim/tools/confluence/get_pages_by_label.ts +++ b/apps/sim/tools/confluence/get_pages_by_label.ts @@ -88,6 +88,7 @@ export const confluenceGetPagesByLabelTool: ToolConfig< }, request: { + internalRoute: true, url: (params: ConfluenceGetPagesByLabelParams) => { const query = new URLSearchParams({ domain: params.domain, diff --git a/apps/sim/tools/confluence/get_space.ts b/apps/sim/tools/confluence/get_space.ts index fbadd7a6575..0bb4eaf4307 100644 --- a/apps/sim/tools/confluence/get_space.ts +++ b/apps/sim/tools/confluence/get_space.ts @@ -71,6 +71,7 @@ export const confluenceGetSpaceTool: ToolConfig< }, request: { + internalRoute: true, url: (params: ConfluenceGetSpaceParams) => { const query = new URLSearchParams({ domain: params.domain, diff --git a/apps/sim/tools/confluence/get_task.ts b/apps/sim/tools/confluence/get_task.ts index cf0b6177654..d096cca913f 100644 --- a/apps/sim/tools/confluence/get_task.ts +++ b/apps/sim/tools/confluence/get_task.ts @@ -70,7 +70,7 @@ export const confluenceGetTaskTool: ToolConfig '/api/tools/confluence/tasks', + url: '/api/tools/confluence/tasks', method: 'POST', headers: (params: ConfluenceGetTaskParams) => ({ Accept: 'application/json', diff --git a/apps/sim/tools/confluence/get_user.ts b/apps/sim/tools/confluence/get_user.ts index 23048361356..9c851c1522e 100644 --- a/apps/sim/tools/confluence/get_user.ts +++ b/apps/sim/tools/confluence/get_user.ts @@ -62,7 +62,7 @@ export const confluenceGetUserTool: ToolConfig '/api/tools/confluence/user', + url: '/api/tools/confluence/user', method: 'POST', headers: (params: ConfluenceGetUserParams) => ({ Accept: 'application/json', diff --git a/apps/sim/tools/confluence/list_attachments.ts b/apps/sim/tools/confluence/list_attachments.ts index 932aa9b6876..67f6640e0ef 100644 --- a/apps/sim/tools/confluence/list_attachments.ts +++ b/apps/sim/tools/confluence/list_attachments.ts @@ -80,6 +80,7 @@ export const confluenceListAttachmentsTool: ToolConfig< }, request: { + internalRoute: true, url: (params: ConfluenceListAttachmentsParams) => { const query = new URLSearchParams({ domain: params.domain, diff --git a/apps/sim/tools/confluence/list_blogposts.ts b/apps/sim/tools/confluence/list_blogposts.ts index a6b78e2b5e4..2118635e47b 100644 --- a/apps/sim/tools/confluence/list_blogposts.ts +++ b/apps/sim/tools/confluence/list_blogposts.ts @@ -95,6 +95,7 @@ export const confluenceListBlogPostsTool: ToolConfig< }, request: { + internalRoute: true, url: (params: ConfluenceListBlogPostsParams) => { const query = new URLSearchParams({ domain: params.domain, diff --git a/apps/sim/tools/confluence/list_blogposts_in_space.ts b/apps/sim/tools/confluence/list_blogposts_in_space.ts index d32fcd9f4da..0c925bb9fa6 100644 --- a/apps/sim/tools/confluence/list_blogposts_in_space.ts +++ b/apps/sim/tools/confluence/list_blogposts_in_space.ts @@ -108,7 +108,7 @@ export const confluenceListBlogPostsInSpaceTool: ToolConfig< }, request: { - url: () => '/api/tools/confluence/space-blogposts', + url: '/api/tools/confluence/space-blogposts', method: 'POST', headers: (params: ConfluenceListBlogPostsInSpaceParams) => ({ Accept: 'application/json', diff --git a/apps/sim/tools/confluence/list_comments.ts b/apps/sim/tools/confluence/list_comments.ts index d841dd6607c..07baeb5dafb 100644 --- a/apps/sim/tools/confluence/list_comments.ts +++ b/apps/sim/tools/confluence/list_comments.ts @@ -87,6 +87,7 @@ export const confluenceListCommentsTool: ToolConfig< }, request: { + internalRoute: true, url: (params: ConfluenceListCommentsParams) => { const query = new URLSearchParams({ domain: params.domain, diff --git a/apps/sim/tools/confluence/list_labels.ts b/apps/sim/tools/confluence/list_labels.ts index c28bac10bcf..7c8faee1a19 100644 --- a/apps/sim/tools/confluence/list_labels.ts +++ b/apps/sim/tools/confluence/list_labels.ts @@ -78,6 +78,7 @@ export const confluenceListLabelsTool: ToolConfig< }, request: { + internalRoute: true, url: (params: ConfluenceListLabelsParams) => { const query = new URLSearchParams({ domain: params.domain, diff --git a/apps/sim/tools/confluence/list_page_properties.ts b/apps/sim/tools/confluence/list_page_properties.ts index cd26739c44c..30579b4f34b 100644 --- a/apps/sim/tools/confluence/list_page_properties.ts +++ b/apps/sim/tools/confluence/list_page_properties.ts @@ -84,6 +84,7 @@ export const confluenceListPagePropertiesTool: ToolConfig< }, request: { + internalRoute: true, url: (params: ConfluenceListPagePropertiesParams) => { const query = new URLSearchParams({ domain: params.domain, diff --git a/apps/sim/tools/confluence/list_page_versions.ts b/apps/sim/tools/confluence/list_page_versions.ts index 8e97f9fdec1..1d48d32dec3 100644 --- a/apps/sim/tools/confluence/list_page_versions.ts +++ b/apps/sim/tools/confluence/list_page_versions.ts @@ -81,7 +81,7 @@ export const confluenceListPageVersionsTool: ToolConfig< }, request: { - url: () => '/api/tools/confluence/page-versions', + url: '/api/tools/confluence/page-versions', method: 'POST', headers: (params: ConfluenceListPageVersionsParams) => ({ Accept: 'application/json', diff --git a/apps/sim/tools/confluence/list_pages_in_space.ts b/apps/sim/tools/confluence/list_pages_in_space.ts index 558a8c50e39..6ed5da21cc4 100644 --- a/apps/sim/tools/confluence/list_pages_in_space.ts +++ b/apps/sim/tools/confluence/list_pages_in_space.ts @@ -111,7 +111,7 @@ export const confluenceListPagesInSpaceTool: ToolConfig< }, request: { - url: () => '/api/tools/confluence/space-pages', + url: '/api/tools/confluence/space-pages', method: 'POST', headers: (params: ConfluenceListPagesInSpaceParams) => ({ Accept: 'application/json', diff --git a/apps/sim/tools/confluence/list_space_labels.ts b/apps/sim/tools/confluence/list_space_labels.ts index d30990d06ed..4b92a76e591 100644 --- a/apps/sim/tools/confluence/list_space_labels.ts +++ b/apps/sim/tools/confluence/list_space_labels.ts @@ -79,6 +79,7 @@ export const confluenceListSpaceLabelsTool: ToolConfig< }, request: { + internalRoute: true, url: (params: ConfluenceListSpaceLabelsParams) => { const query = new URLSearchParams({ domain: params.domain, diff --git a/apps/sim/tools/confluence/list_space_permissions.ts b/apps/sim/tools/confluence/list_space_permissions.ts index 3d8fe00f2b2..ad37fd9e03d 100644 --- a/apps/sim/tools/confluence/list_space_permissions.ts +++ b/apps/sim/tools/confluence/list_space_permissions.ts @@ -83,7 +83,7 @@ export const confluenceListSpacePermissionsTool: ToolConfig< }, request: { - url: () => '/api/tools/confluence/space-permissions', + url: '/api/tools/confluence/space-permissions', method: 'POST', headers: (params: ConfluenceListSpacePermissionsParams) => ({ Accept: 'application/json', diff --git a/apps/sim/tools/confluence/list_space_properties.ts b/apps/sim/tools/confluence/list_space_properties.ts index d47c4570b08..a209731f3b3 100644 --- a/apps/sim/tools/confluence/list_space_properties.ts +++ b/apps/sim/tools/confluence/list_space_properties.ts @@ -79,7 +79,7 @@ export const confluenceListSpacePropertiesTool: ToolConfig< }, request: { - url: () => '/api/tools/confluence/space-properties', + url: '/api/tools/confluence/space-properties', method: 'POST', headers: (params: ConfluenceListSpacePropertiesParams) => ({ Accept: 'application/json', diff --git a/apps/sim/tools/confluence/list_spaces.ts b/apps/sim/tools/confluence/list_spaces.ts index 3859aad2bc4..6870aabda4c 100644 --- a/apps/sim/tools/confluence/list_spaces.ts +++ b/apps/sim/tools/confluence/list_spaces.ts @@ -73,6 +73,7 @@ export const confluenceListSpacesTool: ToolConfig< }, request: { + internalRoute: true, url: (params: ConfluenceListSpacesParams) => { const query = new URLSearchParams({ domain: params.domain, diff --git a/apps/sim/tools/confluence/list_tasks.ts b/apps/sim/tools/confluence/list_tasks.ts index 4f44678a89f..ddce4b3ef0a 100644 --- a/apps/sim/tools/confluence/list_tasks.ts +++ b/apps/sim/tools/confluence/list_tasks.ts @@ -111,7 +111,7 @@ export const confluenceListTasksTool: ToolConfig< }, request: { - url: () => '/api/tools/confluence/tasks', + url: '/api/tools/confluence/tasks', method: 'POST', headers: (params: ConfluenceListTasksParams) => ({ Accept: 'application/json', diff --git a/apps/sim/tools/confluence/retrieve.ts b/apps/sim/tools/confluence/retrieve.ts index ded0fda90cf..dc0c50468c4 100644 --- a/apps/sim/tools/confluence/retrieve.ts +++ b/apps/sim/tools/confluence/retrieve.ts @@ -50,6 +50,7 @@ export const confluenceRetrieveTool: ToolConfig< }, request: { + internalRoute: true, url: (params: ConfluenceRetrieveParams) => { return '/api/tools/confluence/page' }, diff --git a/apps/sim/tools/confluence/search.ts b/apps/sim/tools/confluence/search.ts index 9b551ade892..48f87a97bcc 100644 --- a/apps/sim/tools/confluence/search.ts +++ b/apps/sim/tools/confluence/search.ts @@ -69,7 +69,7 @@ export const confluenceSearchTool: ToolConfig '/api/tools/confluence/search', + url: '/api/tools/confluence/search', method: 'POST', headers: (params: ConfluenceSearchParams) => { return { diff --git a/apps/sim/tools/confluence/search_in_space.ts b/apps/sim/tools/confluence/search_in_space.ts index 5b10a5c6294..633251faad9 100644 --- a/apps/sim/tools/confluence/search_in_space.ts +++ b/apps/sim/tools/confluence/search_in_space.ts @@ -91,7 +91,7 @@ export const confluenceSearchInSpaceTool: ToolConfig< }, request: { - url: () => '/api/tools/confluence/search-in-space', + url: '/api/tools/confluence/search-in-space', method: 'POST', headers: (params: ConfluenceSearchInSpaceParams) => ({ Accept: 'application/json', diff --git a/apps/sim/tools/confluence/update.ts b/apps/sim/tools/confluence/update.ts index e5daa6628fd..56e2c9686fd 100644 --- a/apps/sim/tools/confluence/update.ts +++ b/apps/sim/tools/confluence/update.ts @@ -54,6 +54,7 @@ export const confluenceUpdateTool: ToolConfig { return '/api/tools/confluence/page' }, diff --git a/apps/sim/tools/confluence/update_blogpost.ts b/apps/sim/tools/confluence/update_blogpost.ts index ea873cea179..b0167aaa02b 100644 --- a/apps/sim/tools/confluence/update_blogpost.ts +++ b/apps/sim/tools/confluence/update_blogpost.ts @@ -78,7 +78,7 @@ export const confluenceUpdateBlogPostTool: ToolConfig< }, request: { - url: () => '/api/tools/confluence/blogposts', + url: '/api/tools/confluence/blogposts', method: 'PUT', headers: (params: ConfluenceUpdateBlogPostParams) => ({ Accept: 'application/json', diff --git a/apps/sim/tools/confluence/update_comment.ts b/apps/sim/tools/confluence/update_comment.ts index 897517f8b6d..139e3e49da7 100644 --- a/apps/sim/tools/confluence/update_comment.ts +++ b/apps/sim/tools/confluence/update_comment.ts @@ -66,7 +66,7 @@ export const confluenceUpdateCommentTool: ToolConfig< }, request: { - url: () => '/api/tools/confluence/comment', + url: '/api/tools/confluence/comment', method: 'PUT', headers: (params: ConfluenceUpdateCommentParams) => { return { diff --git a/apps/sim/tools/confluence/update_space.ts b/apps/sim/tools/confluence/update_space.ts index c1cc6bd6dbf..050ae037b92 100644 --- a/apps/sim/tools/confluence/update_space.ts +++ b/apps/sim/tools/confluence/update_space.ts @@ -79,7 +79,7 @@ export const confluenceUpdateSpaceTool: ToolConfig< }, request: { - url: () => '/api/tools/confluence/space', + url: '/api/tools/confluence/space', method: 'PUT', headers: (params: ConfluenceUpdateSpaceParams) => ({ Accept: 'application/json', diff --git a/apps/sim/tools/confluence/update_task.ts b/apps/sim/tools/confluence/update_task.ts index d7d87387eb9..180761a07d2 100644 --- a/apps/sim/tools/confluence/update_task.ts +++ b/apps/sim/tools/confluence/update_task.ts @@ -79,7 +79,7 @@ export const confluenceUpdateTaskTool: ToolConfig< }, request: { - url: () => '/api/tools/confluence/tasks', + url: '/api/tools/confluence/tasks', method: 'POST', headers: (params: ConfluenceUpdateTaskParams) => ({ Accept: 'application/json', diff --git a/apps/sim/tools/confluence/upload_attachment.ts b/apps/sim/tools/confluence/upload_attachment.ts index e8fabd5c0bb..b5ba96d2004 100644 --- a/apps/sim/tools/confluence/upload_attachment.ts +++ b/apps/sim/tools/confluence/upload_attachment.ts @@ -84,7 +84,7 @@ export const confluenceUploadAttachmentTool: ToolConfig< }, request: { - url: () => '/api/tools/confluence/upload-attachment', + url: '/api/tools/confluence/upload-attachment', method: 'POST', headers: (params: ConfluenceUploadAttachmentParams) => { return { diff --git a/apps/sim/tools/deployments/get_version.ts b/apps/sim/tools/deployments/get_version.ts index 088af15188c..d923ea04973 100644 --- a/apps/sim/tools/deployments/get_version.ts +++ b/apps/sim/tools/deployments/get_version.ts @@ -30,6 +30,7 @@ export const deploymentsGetVersionTool: ToolConfig< }, request: { + internalRoute: true, url: (params) => { const workspaceId = params._context?.workspaceId if (!workspaceId) { diff --git a/apps/sim/tools/deployments/list_versions.ts b/apps/sim/tools/deployments/list_versions.ts index ce349bd6723..d7c9b0c8688 100644 --- a/apps/sim/tools/deployments/list_versions.ts +++ b/apps/sim/tools/deployments/list_versions.ts @@ -24,6 +24,7 @@ export const deploymentsListVersionsTool: ToolConfig< }, request: { + internalRoute: true, url: (params) => { const workspaceId = params._context?.workspaceId if (!workspaceId) { diff --git a/apps/sim/tools/google_drive/upload.ts b/apps/sim/tools/google_drive/upload.ts index 03cb3ed44f8..7ee9b9bbb34 100644 --- a/apps/sim/tools/google_drive/upload.ts +++ b/apps/sim/tools/google_drive/upload.ts @@ -67,6 +67,7 @@ export const uploadTool: ToolConfig { // Use custom API route if file is provided, otherwise use Google Drive API directly if (params.file) { diff --git a/apps/sim/tools/grafana/update_alert_rule.ts b/apps/sim/tools/grafana/update_alert_rule.ts index 1afe913709d..92545a0e48f 100644 --- a/apps/sim/tools/grafana/update_alert_rule.ts +++ b/apps/sim/tools/grafana/update_alert_rule.ts @@ -131,7 +131,7 @@ export const updateAlertRuleTool: ToolConfig '/api/tools/grafana/update_alert_rule', + url: '/api/tools/grafana/update_alert_rule', method: 'POST', headers: () => ({ 'Content-Type': 'application/json' }), body: (params) => ({ diff --git a/apps/sim/tools/grafana/update_dashboard.ts b/apps/sim/tools/grafana/update_dashboard.ts index 70026f73d6f..13b8b5e4993 100644 --- a/apps/sim/tools/grafana/update_dashboard.ts +++ b/apps/sim/tools/grafana/update_dashboard.ts @@ -85,7 +85,7 @@ export const updateDashboardTool: ToolConfig '/api/tools/grafana/update_dashboard', + url: '/api/tools/grafana/update_dashboard', method: 'POST', headers: () => ({ 'Content-Type': 'application/json' }), body: (params) => ({ diff --git a/apps/sim/tools/grafana/update_folder.ts b/apps/sim/tools/grafana/update_folder.ts index 3c4b16c0889..ea34ed7c3b9 100644 --- a/apps/sim/tools/grafana/update_folder.ts +++ b/apps/sim/tools/grafana/update_folder.ts @@ -41,7 +41,7 @@ export const updateFolderTool: ToolConfig '/api/tools/grafana/update_folder', + url: '/api/tools/grafana/update_folder', method: 'POST', headers: () => ({ 'Content-Type': 'application/json' }), body: (params) => ({ diff --git a/apps/sim/tools/index.test.ts b/apps/sim/tools/index.test.ts index ad87e5608d7..0ce2ba5693b 100644 --- a/apps/sim/tools/index.test.ts +++ b/apps/sim/tools/index.test.ts @@ -3158,6 +3158,7 @@ describe('Automatic Internal Route Detection', () => { resourceId: { type: 'string', required: true }, }, request: { + internalRoute: true, url: (params: any) => `/api/resources/${params.resourceId}`, method: 'GET', headers: () => ({ 'Content-Type': 'application/json' }), @@ -3196,6 +3197,8 @@ describe('Automatic Internal Route Detection', () => { expect(result.success).toBe(true) expect(result.output.result).toBe('Dynamic internal route success') expect(mockTool.transformResponse).toHaveBeenCalled() + expect(global.fetch).toHaveBeenCalled() + expect(mockSecureFetchWithPinnedIP).not.toHaveBeenCalled() Object.assign(tools, originalTools) }) @@ -4593,6 +4596,32 @@ describe('MCP Tool Execution', () => { }) describe('Tool request retries', () => { + const internalRetryTool = { + id: 'test_internal_retry', + name: 'Test Internal Retry Tool', + description: 'An internal tool used to exercise retry pacing', + version: '1.0.0', + params: {}, + request: { + url: '/api/test', + method: 'GET', + headers: () => ({ 'Content-Type': 'application/json' }), + retry: { enabled: true, retryIdempotentOnly: true }, + }, + transformResponse: async (response: Response) => ({ + success: response.ok, + output: { status: response.status }, + }), + } + + beforeEach(() => { + ;(tools as Record).test_internal_retry = internalRetryTool + }) + + afterEach(() => { + ;(tools as Record).test_internal_retry = undefined + }) + function makeJsonResponse( status: number, body: unknown, @@ -4611,7 +4640,7 @@ describe('MCP Tool Execution', () => { } } - it('retries on 5xx responses for http_request', async () => { + it('retries on 5xx responses', async () => { global.fetch = Object.assign( vi .fn() @@ -4620,8 +4649,7 @@ describe('MCP Tool Execution', () => { { preconnect: vi.fn() } ) as typeof fetch - const result = await executeTool('http_request', { - url: '/api/test', + const result = await executeTool('test_internal_retry', { method: 'GET', retries: 2, retryDelayMs: 0, @@ -4639,8 +4667,7 @@ describe('MCP Tool Execution', () => { { preconnect: vi.fn() } ) as typeof fetch - const result = await executeTool('http_request', { - url: '/api/test', + const result = await executeTool('test_internal_retry', { method: 'GET', }) @@ -4648,14 +4675,13 @@ describe('MCP Tool Execution', () => { expect(result.success).toBe(false) }) - it('stops retrying after max attempts for http_request', async () => { + it('stops retrying after max attempts', async () => { global.fetch = Object.assign( vi.fn().mockResolvedValue(makeJsonResponse(502, { error: 'bad gateway' })), { preconnect: vi.fn() } ) as typeof fetch - const result = await executeTool('http_request', { - url: '/api/test', + const result = await executeTool('test_internal_retry', { method: 'GET', retries: 2, retryDelayMs: 0, @@ -4666,14 +4692,13 @@ describe('MCP Tool Execution', () => { expect(result.success).toBe(false) }) - it('does not retry on 4xx responses for http_request', async () => { + it('does not retry on 4xx responses', async () => { global.fetch = Object.assign( vi.fn().mockResolvedValue(makeJsonResponse(400, { error: 'bad request' })), { preconnect: vi.fn() } ) as typeof fetch - const result = await executeTool('http_request', { - url: '/api/test', + const result = await executeTool('test_internal_retry', { method: 'GET', retries: 5, retryDelayMs: 0, @@ -4693,8 +4718,7 @@ describe('MCP Tool Execution', () => { { preconnect: vi.fn() } ) as typeof fetch - const result = await executeTool('http_request', { - url: '/api/test', + const result = await executeTool('test_internal_retry', { method: 'POST', retries: 2, retryDelayMs: 0, @@ -4714,8 +4738,7 @@ describe('MCP Tool Execution', () => { { preconnect: vi.fn() } ) as typeof fetch - const result = await executeTool('http_request', { - url: '/api/test', + const result = await executeTool('test_internal_retry', { method: 'POST', retries: 1, retryNonIdempotent: true, @@ -4728,7 +4751,7 @@ describe('MCP Tool Execution', () => { expect((result.output as any).status).toBe(200) }) - it('retries on timeout errors for http_request', async () => { + it('retries on timeout errors', async () => { const abortError = Object.assign(new Error('Aborted'), { name: 'AbortError' }) global.fetch = Object.assign( vi @@ -4738,8 +4761,7 @@ describe('MCP Tool Execution', () => { { preconnect: vi.fn() } ) as typeof fetch - const result = await executeTool('http_request', { - url: '/api/test', + const result = await executeTool('test_internal_retry', { method: 'GET', retries: 1, retryDelayMs: 0, @@ -4761,8 +4783,7 @@ describe('MCP Tool Execution', () => { { preconnect: vi.fn() } ) as typeof fetch - const result = await executeTool('http_request', { - url: '/api/test', + const result = await executeTool('test_internal_retry', { method: 'GET', retries: 3, retryMaxDelayMs: 5000, @@ -4783,8 +4804,7 @@ describe('MCP Tool Execution', () => { { preconnect: vi.fn() } ) as typeof fetch - const result = await executeTool('http_request', { - url: '/api/test', + const result = await executeTool('test_internal_retry', { method: 'GET', retries: 3, retryMaxDelayMs: 40000, @@ -4805,8 +4825,7 @@ describe('MCP Tool Execution', () => { { preconnect: vi.fn() } ) as typeof fetch - const result = await executeTool('http_request', { - url: '/api/test', + const result = await executeTool('test_internal_retry', { method: 'GET', retries: 2, retryDelayMs: 0, @@ -4817,7 +4836,7 @@ describe('MCP Tool Execution', () => { expect(result.success).toBe(true) }) - it('retries on ETIMEDOUT errors for http_request', async () => { + it('retries on ETIMEDOUT errors', async () => { const etimedoutError = Object.assign(new Error('connect ETIMEDOUT 10.0.0.1:443'), { code: 'ETIMEDOUT', }) @@ -4829,8 +4848,7 @@ describe('MCP Tool Execution', () => { { preconnect: vi.fn() } ) as typeof fetch - const result = await executeTool('http_request', { - url: '/api/test', + const result = await executeTool('test_internal_retry', { method: 'GET', retries: 1, retryDelayMs: 0, diff --git a/apps/sim/tools/knowledge/create_document.ts b/apps/sim/tools/knowledge/create_document.ts index 002cfe3e95a..b1e61fb98eb 100644 --- a/apps/sim/tools/knowledge/create_document.ts +++ b/apps/sim/tools/knowledge/create_document.ts @@ -48,7 +48,8 @@ export const knowledgeCreateDocumentTool: ToolConfig `/api/knowledge/${params.knowledgeBaseId}/documents`, + internalRoute: true, + url: (params) => `/api/knowledge/${encodeURIComponent(params.knowledgeBaseId)}/documents`, method: 'POST', secretProvenance: { request: selectKnowledgeDocumentWriteSecretProvenance, diff --git a/apps/sim/tools/knowledge/delete_chunk.ts b/apps/sim/tools/knowledge/delete_chunk.ts index 3bc759af63f..b9b5416e743 100644 --- a/apps/sim/tools/knowledge/delete_chunk.ts +++ b/apps/sim/tools/knowledge/delete_chunk.ts @@ -29,8 +29,9 @@ export const knowledgeDeleteChunkTool: ToolConfig - `/api/knowledge/${params.knowledgeBaseId}/documents/${params.documentId}/chunks/${params.chunkId}`, + `/api/knowledge/${encodeURIComponent(params.knowledgeBaseId)}/documents/${encodeURIComponent(params.documentId)}/chunks/${encodeURIComponent(params.chunkId)}`, method: 'DELETE', headers: () => ({ 'Content-Type': 'application/json', diff --git a/apps/sim/tools/knowledge/delete_document.ts b/apps/sim/tools/knowledge/delete_document.ts index 39493e38283..54855c63874 100644 --- a/apps/sim/tools/knowledge/delete_document.ts +++ b/apps/sim/tools/knowledge/delete_document.ts @@ -23,7 +23,9 @@ export const knowledgeDeleteDocumentTool: ToolConfig `/api/knowledge/${params.knowledgeBaseId}/documents/${params.documentId}`, + internalRoute: true, + url: (params) => + `/api/knowledge/${encodeURIComponent(params.knowledgeBaseId)}/documents/${encodeURIComponent(params.documentId)}`, method: 'DELETE', headers: () => ({ 'Content-Type': 'application/json', diff --git a/apps/sim/tools/knowledge/get_connector.ts b/apps/sim/tools/knowledge/get_connector.ts index 9ae1e03e548..a7034b57059 100644 --- a/apps/sim/tools/knowledge/get_connector.ts +++ b/apps/sim/tools/knowledge/get_connector.ts @@ -24,7 +24,9 @@ export const knowledgeGetConnectorTool: ToolConfig `/api/knowledge/${params.knowledgeBaseId}/connectors/${params.connectorId}`, + internalRoute: true, + url: (params) => + `/api/knowledge/${encodeURIComponent(params.knowledgeBaseId)}/connectors/${encodeURIComponent(params.connectorId)}`, method: 'GET', headers: () => ({ 'Content-Type': 'application/json', diff --git a/apps/sim/tools/knowledge/get_document.ts b/apps/sim/tools/knowledge/get_document.ts index 2ac840ca032..c1ea75a4c5f 100644 --- a/apps/sim/tools/knowledge/get_document.ts +++ b/apps/sim/tools/knowledge/get_document.ts @@ -24,7 +24,9 @@ export const knowledgeGetDocumentTool: ToolConfig `/api/knowledge/${params.knowledgeBaseId}/documents/${params.documentId}`, + internalRoute: true, + url: (params) => + `/api/knowledge/${encodeURIComponent(params.knowledgeBaseId)}/documents/${encodeURIComponent(params.documentId)}`, method: 'GET', secretProvenance: { response: { incomplete: 'reject' } }, headers: () => ({ diff --git a/apps/sim/tools/knowledge/list_chunks.ts b/apps/sim/tools/knowledge/list_chunks.ts index 7198b63fa6b..19f3907fe15 100644 --- a/apps/sim/tools/knowledge/list_chunks.ts +++ b/apps/sim/tools/knowledge/list_chunks.ts @@ -48,6 +48,7 @@ export const knowledgeListChunksTool: ToolConfig { const queryParams = new URLSearchParams() if (params.search) queryParams.set('search', params.search) @@ -56,7 +57,7 @@ export const knowledgeListChunksTool: ToolConfig `/api/knowledge/${params.knowledgeBaseId}/connectors`, + internalRoute: true, + url: (params) => `/api/knowledge/${encodeURIComponent(params.knowledgeBaseId)}/connectors`, method: 'GET', headers: () => ({ 'Content-Type': 'application/json', diff --git a/apps/sim/tools/knowledge/list_documents.ts b/apps/sim/tools/knowledge/list_documents.ts index 6bf491a6521..645a607895c 100644 --- a/apps/sim/tools/knowledge/list_documents.ts +++ b/apps/sim/tools/knowledge/list_documents.ts @@ -41,6 +41,7 @@ export const knowledgeListDocumentsTool: ToolConfig { const queryParams = new URLSearchParams() if (params.search) queryParams.set('search', params.search) @@ -48,7 +49,7 @@ export const knowledgeListDocumentsTool: ToolConfig = }, request: { - url: (params) => `/api/knowledge/${params.knowledgeBaseId}/tag-definitions`, + internalRoute: true, + url: (params) => `/api/knowledge/${encodeURIComponent(params.knowledgeBaseId)}/tag-definitions`, method: 'GET', headers: () => ({ 'Content-Type': 'application/json', diff --git a/apps/sim/tools/knowledge/search.ts b/apps/sim/tools/knowledge/search.ts index 33f7cd3b6cd..553c87e545d 100644 --- a/apps/sim/tools/knowledge/search.ts +++ b/apps/sim/tools/knowledge/search.ts @@ -85,7 +85,7 @@ export const knowledgeSearchTool: ToolConfig = { }, request: { - url: () => '/api/knowledge/search', + url: '/api/knowledge/search', method: 'POST', modelInput: { mode: 'private-provenance', diff --git a/apps/sim/tools/knowledge/trigger_sync.ts b/apps/sim/tools/knowledge/trigger_sync.ts index 127c37a0c1a..6f5c07345fe 100644 --- a/apps/sim/tools/knowledge/trigger_sync.ts +++ b/apps/sim/tools/knowledge/trigger_sync.ts @@ -23,8 +23,9 @@ export const knowledgeTriggerSyncTool: ToolConfig - `/api/knowledge/${params.knowledgeBaseId}/connectors/${params.connectorId}/sync`, + `/api/knowledge/${encodeURIComponent(params.knowledgeBaseId)}/connectors/${encodeURIComponent(params.connectorId)}/sync`, method: 'POST', headers: () => ({ 'Content-Type': 'application/json', diff --git a/apps/sim/tools/knowledge/update_chunk.ts b/apps/sim/tools/knowledge/update_chunk.ts index 7333567bff6..ad9baab1a11 100644 --- a/apps/sim/tools/knowledge/update_chunk.ts +++ b/apps/sim/tools/knowledge/update_chunk.ts @@ -41,8 +41,9 @@ export const knowledgeUpdateChunkTool: ToolConfig - `/api/knowledge/${params.knowledgeBaseId}/documents/${params.documentId}/chunks/${params.chunkId}`, + `/api/knowledge/${encodeURIComponent(params.knowledgeBaseId)}/documents/${encodeURIComponent(params.documentId)}/chunks/${encodeURIComponent(params.chunkId)}`, method: 'PUT', secretProvenance: { request: (params) => diff --git a/apps/sim/tools/knowledge/upload_chunk.ts b/apps/sim/tools/knowledge/upload_chunk.ts index 5701bac0839..ec2f3634148 100644 --- a/apps/sim/tools/knowledge/upload_chunk.ts +++ b/apps/sim/tools/knowledge/upload_chunk.ts @@ -29,8 +29,9 @@ export const knowledgeUploadChunkTool: ToolConfig - `/api/knowledge/${params.knowledgeBaseId}/documents/${params.documentId}/chunks`, + `/api/knowledge/${encodeURIComponent(params.knowledgeBaseId)}/documents/${encodeURIComponent(params.documentId)}/chunks`, method: 'POST', secretProvenance: { request: () => [{ key: 'chunk-content', inputPaths: [['content']] }], diff --git a/apps/sim/tools/knowledge/upsert_document.ts b/apps/sim/tools/knowledge/upsert_document.ts index 2cfec98070e..e5cc6e21f13 100644 --- a/apps/sim/tools/knowledge/upsert_document.ts +++ b/apps/sim/tools/knowledge/upsert_document.ts @@ -60,7 +60,9 @@ export const knowledgeUpsertDocumentTool: ToolConfig< }, request: { - url: (params) => `/api/knowledge/${params.knowledgeBaseId}/documents/upsert`, + internalRoute: true, + url: (params) => + `/api/knowledge/${encodeURIComponent(params.knowledgeBaseId)}/documents/upsert`, method: 'POST', secretProvenance: { request: selectKnowledgeDocumentWriteSecretProvenance, diff --git a/apps/sim/tools/llm/chat.ts b/apps/sim/tools/llm/chat.ts index 9d4362be13a..32a81862bc0 100644 --- a/apps/sim/tools/llm/chat.ts +++ b/apps/sim/tools/llm/chat.ts @@ -128,7 +128,7 @@ export const llmChatTool: ToolConfig = { }, request: { - url: () => '/api/providers', + url: '/api/providers', method: 'POST', headers: () => ({ 'Content-Type': 'application/json', diff --git a/apps/sim/tools/logs/get_execution.ts b/apps/sim/tools/logs/get_execution.ts index a62eef0525b..d4ee1939c65 100644 --- a/apps/sim/tools/logs/get_execution.ts +++ b/apps/sim/tools/logs/get_execution.ts @@ -18,6 +18,7 @@ export const logsGetExecutionTool: ToolConfig `/api/logs/execution/${encodeURIComponent(params.executionId)}`, method: 'GET', headers: () => ({ diff --git a/apps/sim/tools/logs/get_log.ts b/apps/sim/tools/logs/get_log.ts index 92e41e79b83..137059cddb2 100644 --- a/apps/sim/tools/logs/get_log.ts +++ b/apps/sim/tools/logs/get_log.ts @@ -17,6 +17,7 @@ export const logsGetTool: ToolConfig = { }, request: { + internalRoute: true, url: (params) => { const workspaceId = params._context?.workspaceId if (!workspaceId) { diff --git a/apps/sim/tools/logs/get_run_details.ts b/apps/sim/tools/logs/get_run_details.ts index 6326e3afcbd..68e05d609bf 100644 --- a/apps/sim/tools/logs/get_run_details.ts +++ b/apps/sim/tools/logs/get_run_details.ts @@ -21,6 +21,7 @@ export const logsGetRunDetailsTool: ToolConfig { const workspaceId = params._context?.workspaceId if (!workspaceId) { diff --git a/apps/sim/tools/logs/query.ts b/apps/sim/tools/logs/query.ts index 8ea660ee29a..385b53b3a87 100644 --- a/apps/sim/tools/logs/query.ts +++ b/apps/sim/tools/logs/query.ts @@ -78,6 +78,7 @@ export const logsQueryTool: ToolConfig = { }, request: { + internalRoute: true, url: (params) => { const workspaceId = params._context?.workspaceId if (!workspaceId) { diff --git a/apps/sim/tools/logs/query_runs.ts b/apps/sim/tools/logs/query_runs.ts index ae73a32a73e..26cd4fa983b 100644 --- a/apps/sim/tools/logs/query_runs.ts +++ b/apps/sim/tools/logs/query_runs.ts @@ -99,6 +99,7 @@ export const logsQueryRunsTool: ToolConfig { const workspaceId = params._context?.workspaceId if (!workspaceId) { diff --git a/apps/sim/tools/memory/delete.ts b/apps/sim/tools/memory/delete.ts index b32d1fcbf19..48046e64a0c 100644 --- a/apps/sim/tools/memory/delete.ts +++ b/apps/sim/tools/memory/delete.ts @@ -25,6 +25,7 @@ export const memoryDeleteTool: ToolConfig = { }, request: { + internalRoute: true, url: (params) => { const workspaceId = params._context?.workspaceId if (!workspaceId) { diff --git a/apps/sim/tools/memory/get.ts b/apps/sim/tools/memory/get.ts index 7d523ecfcf6..511b4032e47 100644 --- a/apps/sim/tools/memory/get.ts +++ b/apps/sim/tools/memory/get.ts @@ -25,6 +25,7 @@ export const memoryGetTool: ToolConfig = { }, request: { + internalRoute: true, url: (params) => { const workspaceId = params._context?.workspaceId if (!workspaceId) { diff --git a/apps/sim/tools/memory/get_all.ts b/apps/sim/tools/memory/get_all.ts index cf2423f023d..4e91084dc81 100644 --- a/apps/sim/tools/memory/get_all.ts +++ b/apps/sim/tools/memory/get_all.ts @@ -10,6 +10,7 @@ export const memoryGetAllTool: ToolConfig = { params: {}, request: { + internalRoute: true, url: (params) => { const workspaceId = params._context?.workspaceId if (!workspaceId) { diff --git a/apps/sim/tools/microsoft_teams/delete_chat_message.ts b/apps/sim/tools/microsoft_teams/delete_chat_message.ts index 4ba46075944..5d40d599645 100644 --- a/apps/sim/tools/microsoft_teams/delete_chat_message.ts +++ b/apps/sim/tools/microsoft_teams/delete_chat_message.ts @@ -47,6 +47,7 @@ export const deleteChatMessageTool: ToolConfig< }, request: { + internalRoute: true, url: (params) => { const chatId = params.chatId?.trim() const messageId = params.messageId?.trim() diff --git a/apps/sim/tools/microsoft_teams/write_channel.ts b/apps/sim/tools/microsoft_teams/write_channel.ts index c4bffeb35e9..c5e6f9b8bfa 100644 --- a/apps/sim/tools/microsoft_teams/write_channel.ts +++ b/apps/sim/tools/microsoft_teams/write_channel.ts @@ -62,6 +62,7 @@ export const writeChannelTool: ToolConfig { const teamId = params.teamId?.trim() if (!teamId) { diff --git a/apps/sim/tools/microsoft_teams/write_chat.ts b/apps/sim/tools/microsoft_teams/write_chat.ts index e5c9b0c2c89..9847683e233 100644 --- a/apps/sim/tools/microsoft_teams/write_chat.ts +++ b/apps/sim/tools/microsoft_teams/write_chat.ts @@ -54,6 +54,7 @@ export const writeChatTool: ToolConfig { // Ensure chatId is valid const chatId = params.chatId?.trim() diff --git a/apps/sim/tools/onedrive/upload.ts b/apps/sim/tools/onedrive/upload.ts index 9478fdca285..b25fb65c8e7 100644 --- a/apps/sim/tools/onedrive/upload.ts +++ b/apps/sim/tools/onedrive/upload.ts @@ -56,6 +56,7 @@ export const uploadTool: ToolConfig }, request: { + internalRoute: true, url: (params) => { const isExcelFile = params.mimeType === 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' diff --git a/apps/sim/tools/params.ts b/apps/sim/tools/params.ts index ebccb601991..93c831d575f 100644 --- a/apps/sim/tools/params.ts +++ b/apps/sim/tools/params.ts @@ -776,10 +776,10 @@ async function fetchWorkflowInputFields( workflowId: string ): Promise> { try { - const { buildAuthHeaders, buildAPIUrl } = await import('@/executor/utils/http') + const { buildAuthHeaders, internalApiUrl } = await import('@/executor/utils/http') const headers = await buildAuthHeaders() - const url = buildAPIUrl(`/api/workflows/${workflowId}`) + const url = internalApiUrl`/api/workflows/${workflowId}` const response = await fetch(url.toString(), { headers }) if (!response.ok) { diff --git a/apps/sim/tools/request-transport.test.ts b/apps/sim/tools/request-transport.test.ts index bfa21b1fc76..23c64bacafa 100644 --- a/apps/sim/tools/request-transport.test.ts +++ b/apps/sim/tools/request-transport.test.ts @@ -59,3 +59,140 @@ describe('private-provenance tool registry invariant', () => { } ) }) + +interface ProbeParams { + url: string + tableId: string +} + +function buildProbeTool( + request: Partial['request']> +): ToolConfig { + return { + id: 'probe_tool', + name: 'Probe', + description: 'probe', + version: '1.0.0', + params: { + url: { type: 'string', visibility: 'user-or-llm' }, + tableId: { type: 'string', visibility: 'user-or-llm' }, + }, + request: { + url: '/api/probe', + method: 'GET', + headers: () => ({}), + ...request, + }, + } +} + +describe('internal transport selection', () => { + it('trusts a statically configured internal URL', () => { + const prepared = prepareToolRequest(buildProbeTool({ url: '/api/probe' }), {}) + + expect(prepared.isInternalRoute).toBe(true) + }) + + it('trusts a URL builder that declares an internal route', () => { + const prepared = prepareToolRequest( + buildProbeTool({ + internalRoute: true, + url: (params) => `/api/table/${params.tableId}/rows`, + }), + { tableId: 'table-1' } + ) + + expect(prepared.isInternalRoute).toBe(true) + }) + + it('does not trust a declared internal tool that resolves to an external URL', () => { + const prepared = prepareToolRequest( + buildProbeTool({ internalRoute: true, url: () => 'https://attacker.example/x' }), + {} + ) + + expect(prepared.isInternalRoute).toBe(false) + }) + + it('rejects private provenance on an undeclared builder that emits an internal path', () => { + const tool = buildProbeTool({ + url: (params) => params.url, + body: () => ({ probe: true }), + modelInput: { mode: 'private-provenance', inputPaths: () => [] }, + }) + + expect(() => + prepareToolRequest(tool, { url: '/api/probe' }, new ResolvedSecretTraceRegistry()) + ).toThrow(/internal routes/) + }) +}) + +describe('caller-supplied tool URLs never reach the internal transport', () => { + it.each(['http_request', 'webhook_request'])( + '%s cannot mint an internal request from a relative URL', + (toolId) => { + const prepared = prepareToolRequest(tools[toolId], { + url: '/api/auth/oauth/token', + method: 'POST', + body: { credentialId: 'cred-1' }, + }) + + expect(prepared.isInternalRoute).toBe(false) + } + ) + + it('keeps a self-hosted integration external when its host param is blank', () => { + const prepared = prepareToolRequest(tools.grafana_list_folders, { + baseUrl: '', + serviceAccountToken: 'token', + }) + + expect(prepared.url).toMatch(/^\/api\//) + expect(prepared.isInternalRoute).toBe(false) + }) + + const workspaceContext = { _context: { workspaceId: 'workspace-1', userId: 'user-1' } } + + it.each([ + ['function_execute', { code: 'return 1' }], + ['knowledge_search', { knowledgeBaseIds: ['kb-1'], query: 'q' }], + ['memory_get_all', workspaceContext], + ['table_list', workspaceContext], + ['workflow_executor', { workflowId: 'workflow-1' }], + ])('%s still uses the internal transport', (toolId, params) => { + const prepared = prepareToolRequest(tools[toolId], params) + + expect(prepared.isInternalRoute).toBe(true) + }) +}) + +const PROBE_HOST = 'https://internal-route-probe.invalid' + +const declaredInternalTools = Object.entries(tools).filter( + ([, tool]) => tool.request.internalRoute === true +) + +describe('declared internal tool registry invariant', () => { + it('covers at least one registered tool', () => { + expect(declaredInternalTools.length).toBeGreaterThan(0) + }) + + it.each(declaredInternalTools)( + '%s keeps its internal path out of reach of its params', + (registryId, tool) => { + const probeParams: Record = { + _context: { workspaceId: 'workspace-1', userId: 'user-1' }, + } + for (const name of Object.keys(tool.params)) probeParams[name] = PROBE_HOST + + let url: string + try { + url = (tool.request.url as (params: Record) => string)(probeParams) + } catch { + return + } + + expect(url, `${registryId} builds its host from a caller-supplied param`).toMatch(/^\/api\//) + } + ) +}) diff --git a/apps/sim/tools/request-transport.ts b/apps/sim/tools/request-transport.ts index 64d1f3c20ab..d8e977f2b9b 100644 --- a/apps/sim/tools/request-transport.ts +++ b/apps/sim/tools/request-transport.ts @@ -121,6 +121,12 @@ export function projectToolModelInputParams( } } +/** The pre-authenticated internal transport needs both a declared-internal tool and a resolved internal path. */ +function isInternalToolRoute(tool: ToolConfig, url: string): boolean { + if (!url.startsWith('/api/')) return false + return typeof tool.request.url === 'string' || tool.request.internalRoute === true +} + function formatToolRequest(tool: ToolConfig, params: Record): PreparedToolRequest { const url = typeof tool.request.url === 'function' ? tool.request.url(params) : tool.request.url const method = @@ -169,7 +175,7 @@ function formatToolRequest(tool: ToolConfig, params: Record): Prepa timeout: validTimeout, proxyUrl, stripAuthOnRedirect: tool.request.stripAuthOnRedirect, - isInternalRoute: url.startsWith('/api/'), + isInternalRoute: isInternalToolRoute(tool, url), } } @@ -187,10 +193,10 @@ export function prepareToolRequest( modelInput?.mode === 'private-provenance' || (modelInput?.mode === 'project' && modelInput.privateInputPaths !== undefined) - if (hasPrivateModelInputProvenance && !configuredUrl.startsWith('/api/')) { + if (hasPrivateModelInputProvenance && !isInternalToolRoute(tool, configuredUrl)) { throw new Error(PRIVATE_MODEL_INPUT_EXTERNAL_URL_ERROR_MESSAGE) } - if (secretProvenance && !configuredUrl.startsWith('/api/')) { + if (secretProvenance && !isInternalToolRoute(tool, configuredUrl)) { throw new Error(PRIVATE_SECRET_PROVENANCE_EXTERNAL_URL_ERROR_MESSAGE) } diff --git a/apps/sim/tools/schema-enrichers.test.ts b/apps/sim/tools/schema-enrichers.test.ts index f7542177272..b2918dc8081 100644 --- a/apps/sim/tools/schema-enrichers.test.ts +++ b/apps/sim/tools/schema-enrichers.test.ts @@ -3,20 +3,20 @@ */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -const { mockBuildAPIUrl, mockBuildAuthHeaders, mockExtractAPIErrorMessage } = vi.hoisted(() => ({ - mockBuildAPIUrl: vi.fn((path: string, params?: Record) => { - const url = new URL(path, 'http://localhost:3000') - for (const [key, value] of Object.entries(params ?? {})) { - url.searchParams.set(key, value) - } - return url - }), +const { mockInternalApiUrl, mockBuildAuthHeaders, mockExtractAPIErrorMessage } = vi.hoisted(() => ({ + mockInternalApiUrl: vi.fn( + (segments: TemplateStringsArray, ...values: unknown[]) => + new URL( + String.raw({ raw: segments }, ...values.map((v) => encodeURIComponent(String(v)))), + 'http://localhost:3000' + ) + ), mockBuildAuthHeaders: vi.fn(), mockExtractAPIErrorMessage: vi.fn(), })) vi.mock('@/executor/utils/http', () => ({ - buildAPIUrl: mockBuildAPIUrl, + internalApiUrl: mockInternalApiUrl, buildAuthHeaders: mockBuildAuthHeaders, extractAPIErrorMessage: mockExtractAPIErrorMessage, })) @@ -101,6 +101,27 @@ describe('enrichTableToolSchema', () => { enrichTableToolSchema('table-1', 'table_query_rows', ORIGINAL_SCHEMA, 'Query rows', {}) ).rejects.toThrow('Workspace ID is required to enrich table tool schema for table-1') }) + + it('keeps a traversal-shaped table id inside the table route', async () => { + const mockFetch = vi.fn().mockResolvedValue(new Response(null, { status: 404 })) + vi.stubGlobal('fetch', mockFetch) + mockExtractAPIErrorMessage.mockResolvedValue('Table not found') + + await expect( + enrichTableToolSchema( + '../../workflows/wf-1', + 'table_query_rows', + ORIGINAL_SCHEMA, + 'Query rows', + { workspaceId: 'workspace-1', userId: 'user-1' } + ) + ).rejects.toThrow() + + expect(mockFetch).toHaveBeenCalledWith( + 'http://localhost:3000/api/table/..%2F..%2Fworkflows%2Fwf-1?workspaceId=workspace-1', + expect.anything() + ) + }) }) describe('enrichKBTagsSchema', () => { @@ -139,4 +160,16 @@ describe('enrichKBTagsSchema', () => { expect(mockFetch).not.toHaveBeenCalled() expect(mockBuildAuthHeaders).not.toHaveBeenCalled() }) + + it('keeps a traversal-shaped knowledge base id inside the tag-definitions route', async () => { + const mockFetch = vi.fn().mockResolvedValue(new Response(null, { status: 404 })) + vi.stubGlobal('fetch', mockFetch) + + await enrichKBTagsSchema('../../workflows/wf-1', { userId: 'user-1' }) + + expect(mockFetch).toHaveBeenCalledWith( + 'http://localhost:3000/api/knowledge/..%2F..%2Fworkflows%2Fwf-1/tag-definitions', + expect.anything() + ) + }) }) diff --git a/apps/sim/tools/schema-enrichers.ts b/apps/sim/tools/schema-enrichers.ts index 27f91fd50f7..4e84c19c173 100644 --- a/apps/sim/tools/schema-enrichers.ts +++ b/apps/sim/tools/schema-enrichers.ts @@ -18,12 +18,13 @@ async function fetchTableSchema( throw new Error(`User ID is required to enrich table tool schema for ${tableId}`) } - const { buildAuthHeaders, buildAPIUrl, extractAPIErrorMessage } = await import( + const { buildAuthHeaders, internalApiUrl, extractAPIErrorMessage } = await import( '@/executor/utils/http' ) const headers = await buildAuthHeaders(context.userId) - const url = buildAPIUrl(`/api/table/${tableId}`, { workspaceId: context.workspaceId }) + const url = internalApiUrl`/api/table/${tableId}` + url.searchParams.set('workspaceId', context.workspaceId) const response = await fetch(url.toString(), { headers }) if (!response.ok) { @@ -127,10 +128,10 @@ async function fetchTagDefinitions( } try { - const { buildAuthHeaders, buildAPIUrl } = await import('@/executor/utils/http') + const { buildAuthHeaders, internalApiUrl } = await import('@/executor/utils/http') const headers = await buildAuthHeaders(context.userId) - const url = buildAPIUrl(`/api/knowledge/${knowledgeBaseId}/tag-definitions`) + const url = internalApiUrl`/api/knowledge/${knowledgeBaseId}/tag-definitions` logger.info(`Fetching tag definitions for KB ${knowledgeBaseId} from ${url.toString()}`) diff --git a/apps/sim/tools/search/tool.ts b/apps/sim/tools/search/tool.ts index 540a3383134..76cf3f171fa 100644 --- a/apps/sim/tools/search/tool.ts +++ b/apps/sim/tools/search/tool.ts @@ -22,7 +22,7 @@ export const searchTool: ToolConfig = { mode: 'project', select: (params) => ({ query: params.query }), }, - url: () => '/api/tools/search', + url: '/api/tools/search', method: 'POST', headers: () => ({ 'Content-Type': 'application/json', diff --git a/apps/sim/tools/table/batch_insert_rows.ts b/apps/sim/tools/table/batch_insert_rows.ts index 5911b2d4532..6747f2e8a2d 100644 --- a/apps/sim/tools/table/batch_insert_rows.ts +++ b/apps/sim/tools/table/batch_insert_rows.ts @@ -35,11 +35,13 @@ export const tableBatchInsertRowsTool: ToolConfig< }, request: { + internalRoute: true, secretProvenance: { request: (params) => selectTableRowSecretProvenance(params.rows, 'rows'), response: { incomplete: 'propagate' }, }, - url: (params: TableBatchInsertParams) => `/api/table/${params.tableId}/rows`, + url: (params: TableBatchInsertParams) => + `/api/table/${encodeURIComponent(params.tableId)}/rows`, method: 'POST', headers: () => ({ 'Content-Type': 'application/json', diff --git a/apps/sim/tools/table/delete_row.ts b/apps/sim/tools/table/delete_row.ts index 47d46d699aa..1acf59391f1 100644 --- a/apps/sim/tools/table/delete_row.ts +++ b/apps/sim/tools/table/delete_row.ts @@ -23,7 +23,9 @@ export const tableDeleteRowTool: ToolConfig `/api/table/${params.tableId}/rows/${params.rowId}`, + internalRoute: true, + url: (params: TableRowDeleteParams) => + `/api/table/${encodeURIComponent(params.tableId)}/rows/${encodeURIComponent(params.rowId)}`, method: 'DELETE', headers: () => ({ 'Content-Type': 'application/json', diff --git a/apps/sim/tools/table/delete_rows_by_filter.ts b/apps/sim/tools/table/delete_rows_by_filter.ts index cad82ba66aa..d99b484884f 100644 --- a/apps/sim/tools/table/delete_rows_by_filter.ts +++ b/apps/sim/tools/table/delete_rows_by_filter.ts @@ -42,7 +42,9 @@ export const tableDeleteRowsByFilterTool: ToolConfig< }, request: { - url: (params: TableDeleteByFilterParams) => `/api/table/${params.tableId}/rows`, + internalRoute: true, + url: (params: TableDeleteByFilterParams) => + `/api/table/${encodeURIComponent(params.tableId)}/rows`, method: 'DELETE', headers: () => ({ 'Content-Type': 'application/json', diff --git a/apps/sim/tools/table/get_row.ts b/apps/sim/tools/table/get_row.ts index 7b76e605fda..430bd0b0aaf 100644 --- a/apps/sim/tools/table/get_row.ts +++ b/apps/sim/tools/table/get_row.ts @@ -23,6 +23,7 @@ export const tableGetRowTool: ToolConfig = }, request: { + internalRoute: true, secretProvenance: { response: { incomplete: 'propagate' } }, url: (params: TableRowGetParams) => { const workspaceId = params._context?.workspaceId @@ -30,7 +31,7 @@ export const tableGetRowTool: ToolConfig = throw new Error('Workspace ID is required in execution context') } - return `/api/table/${params.tableId}/rows/${params.rowId}?workspaceId=${encodeURIComponent(workspaceId)}` + return `/api/table/${encodeURIComponent(params.tableId)}/rows/${encodeURIComponent(params.rowId)}?workspaceId=${encodeURIComponent(workspaceId)}` }, method: 'GET', headers: () => ({ diff --git a/apps/sim/tools/table/get_schema.ts b/apps/sim/tools/table/get_schema.ts index 7f96f0dd065..5003fa659b2 100644 --- a/apps/sim/tools/table/get_schema.ts +++ b/apps/sim/tools/table/get_schema.ts @@ -19,13 +19,14 @@ export const tableGetSchemaTool: ToolConfig { const workspaceId = params._context?.workspaceId if (!workspaceId) { throw new Error('Workspace ID is required in execution context') } - return `/api/table/${params.tableId}?workspaceId=${encodeURIComponent(workspaceId)}` + return `/api/table/${encodeURIComponent(params.tableId)}?workspaceId=${encodeURIComponent(workspaceId)}` }, method: 'GET', headers: () => ({ diff --git a/apps/sim/tools/table/insert_row.ts b/apps/sim/tools/table/insert_row.ts index b8751610019..9031e3a0353 100644 --- a/apps/sim/tools/table/insert_row.ts +++ b/apps/sim/tools/table/insert_row.ts @@ -32,11 +32,12 @@ export const tableInsertRowTool: ToolConfig selectTableRowSecretProvenance([params.data]), response: { incomplete: 'propagate' }, }, - url: (params: TableRowInsertParams) => `/api/table/${params.tableId}/rows`, + url: (params: TableRowInsertParams) => `/api/table/${encodeURIComponent(params.tableId)}/rows`, method: 'POST', headers: () => ({ 'Content-Type': 'application/json', diff --git a/apps/sim/tools/table/list.ts b/apps/sim/tools/table/list.ts index 18718c8071b..34d102d7e06 100644 --- a/apps/sim/tools/table/list.ts +++ b/apps/sim/tools/table/list.ts @@ -10,6 +10,7 @@ export const tableListTool: ToolConfig = { params: {}, request: { + internalRoute: true, url: (params: TableListParams) => { const workspaceId = params._context?.workspaceId if (!workspaceId) { diff --git a/apps/sim/tools/table/query_rows.ts b/apps/sim/tools/table/query_rows.ts index 828c9f775b5..554e20326f5 100644 --- a/apps/sim/tools/table/query_rows.ts +++ b/apps/sim/tools/table/query_rows.ts @@ -50,6 +50,7 @@ export const tableQueryRowsTool: ToolConfig { const workspaceId = params._context?.workspaceId @@ -74,7 +75,7 @@ export const tableQueryRowsTool: ToolConfig ({ diff --git a/apps/sim/tools/table/query_rows_v2.ts b/apps/sim/tools/table/query_rows_v2.ts index 3483774b0b3..bd198dc6e35 100644 --- a/apps/sim/tools/table/query_rows_v2.ts +++ b/apps/sim/tools/table/query_rows_v2.ts @@ -57,8 +57,10 @@ export const tableQueryRowsV2Tool: ToolConfig `/api/table/${params.tableId}/query`, + url: (params: TableRowQueryV2Params) => + `/api/table/${encodeURIComponent(params.tableId)}/query`, method: 'POST', headers: () => ({ 'Content-Type': 'application/json' }), body: (params: TableRowQueryV2Params) => { diff --git a/apps/sim/tools/table/update_row.ts b/apps/sim/tools/table/update_row.ts index c9792f95680..23e59516454 100644 --- a/apps/sim/tools/table/update_row.ts +++ b/apps/sim/tools/table/update_row.ts @@ -38,11 +38,13 @@ export const tableUpdateRowTool: ToolConfig selectTableRowSecretProvenance([params.data]), response: { incomplete: 'propagate' }, }, - url: (params: TableRowUpdateParams) => `/api/table/${params.tableId}/rows/${params.rowId}`, + url: (params: TableRowUpdateParams) => + `/api/table/${encodeURIComponent(params.tableId)}/rows/${encodeURIComponent(params.rowId)}`, method: 'PATCH', headers: () => ({ 'Content-Type': 'application/json', diff --git a/apps/sim/tools/table/update_rows_by_filter.ts b/apps/sim/tools/table/update_rows_by_filter.ts index d1f2b759eba..f9f459b7431 100644 --- a/apps/sim/tools/table/update_rows_by_filter.ts +++ b/apps/sim/tools/table/update_rows_by_filter.ts @@ -49,10 +49,12 @@ export const tableUpdateRowsByFilterTool: ToolConfig< }, request: { + internalRoute: true, secretProvenance: { request: (params) => selectTableRowSecretProvenance([params.data]), }, - url: (params: TableUpdateByFilterParams) => `/api/table/${params.tableId}/rows`, + url: (params: TableUpdateByFilterParams) => + `/api/table/${encodeURIComponent(params.tableId)}/rows`, method: 'PUT', headers: () => ({ 'Content-Type': 'application/json', diff --git a/apps/sim/tools/table/upsert_row.ts b/apps/sim/tools/table/upsert_row.ts index 70afc179872..70e05cebce4 100644 --- a/apps/sim/tools/table/upsert_row.ts +++ b/apps/sim/tools/table/upsert_row.ts @@ -39,11 +39,13 @@ export const tableUpsertRowTool: ToolConfig selectTableRowSecretProvenance([params.data]), response: { incomplete: 'propagate' }, }, - url: (params: TableRowInsertParams) => `/api/table/${params.tableId}/rows/upsert`, + url: (params: TableRowInsertParams) => + `/api/table/${encodeURIComponent(params.tableId)}/rows/upsert`, method: 'POST', headers: () => ({ 'Content-Type': 'application/json', diff --git a/apps/sim/tools/tiktok/upload_video_draft.ts b/apps/sim/tools/tiktok/upload_video_draft.ts index ca478dd32dc..7a5ce910730 100644 --- a/apps/sim/tools/tiktok/upload_video_draft.ts +++ b/apps/sim/tools/tiktok/upload_video_draft.ts @@ -37,7 +37,7 @@ export const tiktokUploadVideoDraftTool: ToolConfig< }, request: { - url: () => '/api/tools/tiktok/upload-video-draft', + url: '/api/tools/tiktok/upload-video-draft', method: 'POST', headers: () => ({ 'Content-Type': 'application/json', diff --git a/apps/sim/tools/types.ts b/apps/sim/tools/types.ts index c9b15ee356c..484a21beb64 100644 --- a/apps/sim/tools/types.ts +++ b/apps/sim/tools/types.ts @@ -175,6 +175,15 @@ export interface ToolConfig

{ // Request configuration request: { url: string | ((params: P) => string) + /** + * Declares that this tool targets Sim's own API, authorizing the transport to resolve the + * request against the internal base URL and sign it for the executing user. + * + * Required for every tool whose `url` is a builder returning an internal path. A static `url` + * string is self-declaring; a builder is not, because params steer its output — a blank host + * param collapses `${host}/api/v2/x` to `/api/v2/x`. + */ + internalRoute?: boolean method: HttpMethod | ((params: P) => HttpMethod) headers: (params: P) => Record body?: (params: P) => Record | string | FormData | undefined diff --git a/apps/sim/tools/wordpress/upload_media.ts b/apps/sim/tools/wordpress/upload_media.ts index 7115346aaa0..e2fd0d3d924 100644 --- a/apps/sim/tools/wordpress/upload_media.ts +++ b/apps/sim/tools/wordpress/upload_media.ts @@ -63,7 +63,7 @@ export const uploadMediaTool: ToolConfig '/api/tools/wordpress/upload', + url: '/api/tools/wordpress/upload', method: 'POST', headers: () => ({ 'Content-Type': 'application/json', diff --git a/apps/sim/tools/workflow/executor.ts b/apps/sim/tools/workflow/executor.ts index 875e47044f8..2b5830c3386 100644 --- a/apps/sim/tools/workflow/executor.ts +++ b/apps/sim/tools/workflow/executor.ts @@ -34,7 +34,9 @@ export const workflowExecutorTool: ToolConfig< }, }, request: { - url: (params: WorkflowExecutorParams) => `/api/workflows/${params.workflowId}/execute`, + internalRoute: true, + url: (params: WorkflowExecutorParams) => + `/api/workflows/${encodeURIComponent(params.workflowId)}/execute`, method: 'POST', headers: () => ({ 'Content-Type': 'application/json' }), secretProvenance: { diff --git a/packages/testing/src/mocks/executor.mock.ts b/packages/testing/src/mocks/executor.mock.ts index 8698c258b7a..cd937cb8cdc 100644 --- a/packages/testing/src/mocks/executor.mock.ts +++ b/packages/testing/src/mocks/executor.mock.ts @@ -73,7 +73,13 @@ vi.mock('@/executor/resolver', () => ({ })) vi.mock('@/executor/utils/http', () => ({ buildAuthHeaders: vi.fn().mockResolvedValue({ 'Content-Type': 'application/json' }), - buildAPIUrl: vi.fn((path: string) => new URL(path, 'http://localhost:3000')), + internalApiUrl: vi.fn( + (segments: TemplateStringsArray, ...values: unknown[]) => + new URL( + String.raw({ raw: segments }, ...values.map((v) => encodeURIComponent(String(v)))), + 'http://localhost:3000' + ) + ), extractAPIErrorMessage: vi.fn(async (response: Response) => { const defaultMessage = `API request failed with status ${response.status}` try {