Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/sim/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ BETTER_AUTH_URL=http://localhost:3000

# NextJS (Required)
NEXT_PUBLIC_APP_URL=http://localhost:3000
# INTERNAL_API_BASE_URL=http://sim-app.default.svc.cluster.local:3000 # Optional: internal URL for server-side /api self-calls; defaults to NEXT_PUBLIC_APP_URL

# Security (Required)
ENCRYPTION_KEY=your_encryption_key # Use `openssl rand -hex 32` to generate, used to encrypt environment variables
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/app/api/a2a/serve/[agentId]/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { Artifact, Message, PushNotificationConfig, Task, TaskState } from '@a2a-js/sdk'
import { v4 as uuidv4 } from 'uuid'
import { generateInternalToken } from '@/lib/auth/internal'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'

/** A2A v0.3 JSON-RPC method names */
export const A2A_METHODS = {
Expand Down Expand Up @@ -118,7 +118,7 @@ export interface ExecuteRequestResult {
export async function buildExecuteRequest(
config: ExecuteRequestConfig
): Promise<ExecuteRequestResult> {
const url = `${getBaseUrl()}/api/workflows/${config.workflowId}/execute`
const url = `${getInternalApiBaseUrl()}/api/workflows/${config.workflowId}/execute`
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
let useInternalAuth = false

Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/api/copilot/checkpoints/revert/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ describe('Copilot Checkpoints Revert API Route', () => {
setupCommonApiMocks()
mockCryptoUuid()

// Mock getBaseUrl to return localhost for tests
vi.doMock('@/lib/core/utils/urls', () => ({
getBaseUrl: vi.fn(() => 'http://localhost:3000'),
getInternalApiBaseUrl: vi.fn(() => 'http://localhost:3000'),
getBaseDomain: vi.fn(() => 'localhost:3000'),
getEmailDomain: vi.fn(() => 'localhost:3000'),
}))
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/app/api/copilot/checkpoints/revert/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
createRequestTracker,
createUnauthorizedResponse,
} from '@/lib/copilot/request-helpers'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
import { isUuidV4 } from '@/executor/constants'

Expand Down Expand Up @@ -99,7 +99,7 @@ export async function POST(request: NextRequest) {
}

const stateResponse = await fetch(
`${getBaseUrl()}/api/workflows/${checkpoint.workflowId}/state`,
`${getInternalApiBaseUrl()}/api/workflows/${checkpoint.workflowId}/state`,
{
method: 'PUT',
headers: {
Expand Down
1 change: 1 addition & 0 deletions apps/sim/app/api/mcp/serve/[serverId]/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ describe('MCP Serve Route', () => {
}))
vi.doMock('@/lib/core/utils/urls', () => ({
getBaseUrl: () => 'http://localhost:3000',
getInternalApiBaseUrl: () => 'http://localhost:3000',
}))
vi.doMock('@/lib/core/execution-limits', () => ({
getMaxExecutionTimeout: () => 10_000,
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/app/api/mcp/serve/[serverId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { type AuthResult, checkHybridAuth } from '@/lib/auth/hybrid'
import { generateInternalToken } from '@/lib/auth/internal'
import { getMaxExecutionTimeout } from '@/lib/core/execution-limits'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'

const logger = createLogger('WorkflowMcpServeAPI')
Expand Down Expand Up @@ -285,7 +285,7 @@ async function handleToolsCall(
)
}

const executeUrl = `${getBaseUrl()}/api/workflows/${tool.workflowId}/execute`
const executeUrl = `${getInternalApiBaseUrl()}/api/workflows/${tool.workflowId}/execute`
const headers: Record<string, string> = { 'Content-Type': 'application/json' }

if (publicServerOwnerId) {
Expand Down
23 changes: 13 additions & 10 deletions apps/sim/app/api/templates/[id]/use/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { type NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { getSession } from '@/lib/auth'
import { generateRequestId } from '@/lib/core/utils/request'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
import {
type RegenerateStateInput,
regenerateWorkflowStateIds,
Expand Down Expand Up @@ -115,15 +115,18 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
// Step 3: Save the workflow state using the existing state endpoint (like imports do)
// Ensure variables in state are remapped for the new workflow as well
const workflowStateWithVariables = { ...workflowState, variables: remappedVariables }
const stateResponse = await fetch(`${getBaseUrl()}/api/workflows/${newWorkflowId}/state`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
// Forward the session cookie for authentication
cookie: request.headers.get('cookie') || '',
},
body: JSON.stringify(workflowStateWithVariables),
})
const stateResponse = await fetch(
`${getInternalApiBaseUrl()}/api/workflows/${newWorkflowId}/state`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
// Forward the session cookie for authentication
cookie: request.headers.get('cookie') || '',
},
body: JSON.stringify(workflowStateWithVariables),
}
)

if (!stateResponse.ok) {
logger.error(`[${requestId}] Failed to save workflow state for template use`)
Expand Down
6 changes: 3 additions & 3 deletions apps/sim/executor/handlers/router/router-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { db } from '@sim/db'
import { account } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { generateRouterPrompt, generateRouterV2Prompt } from '@/blocks/blocks/router'
import type { BlockOutput } from '@/blocks/types'
Expand Down Expand Up @@ -79,7 +79,7 @@ export class RouterBlockHandler implements BlockHandler {
const providerId = getProviderFromModel(routerConfig.model)

try {
const url = new URL('/api/providers', getBaseUrl())
const url = new URL('/api/providers', getInternalApiBaseUrl())
if (ctx.userId) url.searchParams.set('userId', ctx.userId)

const messages = [{ role: 'user', content: routerConfig.prompt }]
Expand Down Expand Up @@ -209,7 +209,7 @@ export class RouterBlockHandler implements BlockHandler {
const providerId = getProviderFromModel(routerConfig.model)

try {
const url = new URL('/api/providers', getBaseUrl())
const url = new URL('/api/providers', getInternalApiBaseUrl())
if (ctx.userId) url.searchParams.set('userId', ctx.userId)

const messages = [{ role: 'user', content: routerConfig.context }]
Expand Down
5 changes: 3 additions & 2 deletions apps/sim/executor/utils/http.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { generateInternalToken } from '@/lib/auth/internal'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { getBaseUrl, getInternalApiBaseUrl } from '@/lib/core/utils/urls'
import { HTTP } from '@/executor/constants'

export async function buildAuthHeaders(): Promise<Record<string, string>> {
Expand All @@ -16,7 +16,8 @@ export async function buildAuthHeaders(): Promise<Record<string, string>> {
}

export function buildAPIUrl(path: string, params?: Record<string, string>): URL {
const url = new URL(path, getBaseUrl())
const baseUrl = path.startsWith('/api/') ? getInternalApiBaseUrl() : getBaseUrl()
const url = new URL(path, baseUrl)

if (params) {
for (const [key, value] of Object.entries(params)) {
Expand Down
1 change: 1 addition & 0 deletions apps/sim/lib/core/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ export const env = createEnv({
SOCKET_SERVER_URL: z.string().url().optional(), // WebSocket server URL for real-time features
SOCKET_PORT: z.number().optional(), // Port for WebSocket server
PORT: z.number().optional(), // Main application port
INTERNAL_API_BASE_URL: z.string().optional(), // Optional internal base URL for server-side self-calls; must include protocol if set (e.g., http://sim-app.namespace.svc.cluster.local:3000)
ALLOWED_ORIGINS: z.string().optional(), // CORS allowed origins

// OAuth Integration Credentials - All optional, enables third-party integrations
Expand Down
37 changes: 32 additions & 5 deletions apps/sim/lib/core/utils/urls.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,54 @@
import { getEnv } from '@/lib/core/config/env'
import { isProd } from '@/lib/core/config/feature-flags'

function hasHttpProtocol(url: string): boolean {
return /^https?:\/\//i.test(url)
}

function normalizeBaseUrl(url: string): string {
if (hasHttpProtocol(url)) {
return url
}

const protocol = isProd ? 'https://' : 'http://'
return `${protocol}${url}`
}

/**
* Returns the base URL of the application from NEXT_PUBLIC_APP_URL
* This ensures webhooks, callbacks, and other integrations always use the correct public URL
* @returns The base URL string (e.g., 'http://localhost:3000' or 'https://example.com')
* @throws Error if NEXT_PUBLIC_APP_URL is not configured
*/
export function getBaseUrl(): string {
const baseUrl = getEnv('NEXT_PUBLIC_APP_URL')
const baseUrl = getEnv('NEXT_PUBLIC_APP_URL')?.trim()

if (!baseUrl) {
throw new Error(
'NEXT_PUBLIC_APP_URL must be configured for webhooks and callbacks to work correctly'
)
}

if (baseUrl.startsWith('http://') || baseUrl.startsWith('https://')) {
return baseUrl
return normalizeBaseUrl(baseUrl)
}

/**
* Returns the base URL used by server-side internal API calls.
* Falls back to NEXT_PUBLIC_APP_URL when INTERNAL_API_BASE_URL is not set.
*/
export function getInternalApiBaseUrl(): string {
const internalBaseUrl = getEnv('INTERNAL_API_BASE_URL')?.trim()
if (!internalBaseUrl) {
return getBaseUrl()
}

if (!hasHttpProtocol(internalBaseUrl)) {
throw new Error(
'INTERNAL_API_BASE_URL must include protocol (http:// or https://), e.g. http://sim-app.default.svc.cluster.local:3000'
)
}

const protocol = isProd ? 'https://' : 'http://'
return `${protocol}${baseUrl}`
return internalBaseUrl
}

/**
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/lib/guardrails/validate_hallucination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { db } from '@sim/db'
import { account } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { executeProviderRequest } from '@/providers'
import { getProviderFromModel } from '@/providers/utils'
Expand Down Expand Up @@ -61,7 +61,7 @@ async function queryKnowledgeBase(
})

// Call the knowledge base search API directly
const searchUrl = `${getBaseUrl()}/api/knowledge/search`
const searchUrl = `${getInternalApiBaseUrl()}/api/knowledge/search`

const response = await fetch(searchUrl, {
method: 'POST',
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/lib/knowledge/documents/document-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -539,8 +539,8 @@ async function executeMistralOCRRequest(
const isInternalRoute = url.startsWith('/')

if (isInternalRoute) {
const { getBaseUrl } = await import('@/lib/core/utils/urls')
url = `${getBaseUrl()}${url}`
const { getInternalApiBaseUrl } = await import('@/lib/core/utils/urls')
url = `${getInternalApiBaseUrl()}${url}`
}

let headers =
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/lib/webhooks/gmail-polling-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { and, eq, isNull, or, sql } from 'drizzle-orm'
import { nanoid } from 'nanoid'
import { isOrganizationOnTeamOrEnterprisePlan } from '@/lib/billing'
import { pollingIdempotency } from '@/lib/core/idempotency/service'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
import { getOAuthToken, refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import type { GmailAttachment } from '@/tools/gmail/types'
import { downloadAttachments, extractAttachmentInfo } from '@/tools/gmail/utils'
Expand Down Expand Up @@ -691,7 +691,7 @@ async function processEmails(
`[${requestId}] Sending ${config.includeRawEmail ? 'simplified + raw' : 'simplified'} email payload for ${email.id}`
)

const webhookUrl = `${getBaseUrl()}/api/webhooks/trigger/${webhookData.path}`
const webhookUrl = `${getInternalApiBaseUrl()}/api/webhooks/trigger/${webhookData.path}`

const response = await fetch(webhookUrl, {
method: 'POST',
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/lib/webhooks/imap-polling-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import type { FetchMessageObject, MailboxLockObject } from 'imapflow'
import { ImapFlow } from 'imapflow'
import { nanoid } from 'nanoid'
import { pollingIdempotency } from '@/lib/core/idempotency/service'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
import { MAX_CONSECUTIVE_FAILURES } from '@/triggers/constants'

const logger = createLogger('ImapPollingService')
Expand Down Expand Up @@ -639,7 +639,7 @@ async function processEmails(
timestamp: new Date().toISOString(),
}

const webhookUrl = `${getBaseUrl()}/api/webhooks/trigger/${webhookData.path}`
const webhookUrl = `${getInternalApiBaseUrl()}/api/webhooks/trigger/${webhookData.path}`

const response = await fetch(webhookUrl, {
method: 'POST',
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/lib/webhooks/outlook-polling-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { htmlToText } from 'html-to-text'
import { nanoid } from 'nanoid'
import { isOrganizationOnTeamOrEnterprisePlan } from '@/lib/billing'
import { pollingIdempotency } from '@/lib/core/idempotency'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
import { getOAuthToken, refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
import { MAX_CONSECUTIVE_FAILURES } from '@/triggers/constants'

Expand Down Expand Up @@ -601,7 +601,7 @@ async function processOutlookEmails(
`[${requestId}] Processing email: ${email.subject} from ${email.from?.emailAddress?.address}`
)

const webhookUrl = `${getBaseUrl()}/api/webhooks/trigger/${webhookData.path}`
const webhookUrl = `${getInternalApiBaseUrl()}/api/webhooks/trigger/${webhookData.path}`

const response = await fetch(webhookUrl, {
method: 'POST',
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/lib/webhooks/rss-polling-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
secureFetchWithPinnedIP,
validateUrlWithDNS,
} from '@/lib/core/security/input-validation.server'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
import { MAX_CONSECUTIVE_FAILURES } from '@/triggers/constants'

const logger = createLogger('RssPollingService')
Expand Down Expand Up @@ -376,7 +376,7 @@ async function processRssItems(
timestamp: new Date().toISOString(),
}

const webhookUrl = `${getBaseUrl()}/api/webhooks/trigger/${webhookData.path}`
const webhookUrl = `${getInternalApiBaseUrl()}/api/webhooks/trigger/${webhookData.path}`

const response = await fetch(webhookUrl, {
method: 'POST',
Expand Down
10 changes: 5 additions & 5 deletions apps/sim/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
validateUrlWithDNS,
} from '@/lib/core/security/input-validation.server'
import { generateRequestId } from '@/lib/core/utils/request'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { getBaseUrl, getInternalApiBaseUrl } from '@/lib/core/utils/urls'
import { parseMcpToolId } from '@/lib/mcp/utils'
import { isCustomTool, isMcpTool } from '@/executor/constants'
import { resolveSkillContent } from '@/executor/handlers/agent/skills-resolver'
Expand Down Expand Up @@ -285,7 +285,7 @@ export async function executeTool(
`[${requestId}] Tool ${toolId} needs access token for credential: ${contextParams.credential}`
)
try {
const baseUrl = getBaseUrl()
const baseUrl = getInternalApiBaseUrl()

const workflowId = contextParams._context?.workflowId
const userId = contextParams._context?.userId
Expand Down Expand Up @@ -597,12 +597,12 @@ async function executeToolRequest(
const requestParams = formatRequestParams(tool, params)

try {
const baseUrl = getBaseUrl()
const endpointUrl =
typeof tool.request.url === 'function' ? tool.request.url(params) : tool.request.url
const isInternalRoute = endpointUrl.startsWith('/api/')
const baseUrl = isInternalRoute ? getInternalApiBaseUrl() : getBaseUrl()

const fullUrlObj = new URL(endpointUrl, baseUrl)
const isInternalRoute = endpointUrl.startsWith('/api/')

if (isInternalRoute) {
const workflowId = params._context?.workflowId
Expand Down Expand Up @@ -922,7 +922,7 @@ async function executeMcpTool(

const { serverId, toolName } = parseMcpToolId(toolId)

const baseUrl = getBaseUrl()
const baseUrl = getInternalApiBaseUrl()

const headers: Record<string, string> = { 'Content-Type': 'application/json' }

Expand Down
4 changes: 2 additions & 2 deletions apps/sim/tools/openai/image.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createLogger } from '@sim/logger'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { getInternalApiBaseUrl } from '@/lib/core/utils/urls'
import type { BaseImageRequestBody } from '@/tools/openai/types'
import type { ToolConfig } from '@/tools/types'

Expand Down Expand Up @@ -122,7 +122,7 @@ export const imageTool: ToolConfig = {
if (imageUrl && !base64Image) {
try {
logger.info('Fetching image from URL via proxy...')
const baseUrl = getBaseUrl()
const baseUrl = getInternalApiBaseUrl()
const proxyUrl = new URL('/api/tools/image', baseUrl)
proxyUrl.searchParams.append('url', imageUrl)

Expand Down
Loading