Skip to content

Commit 1232548

Browse files
committed
fix(envvars): resolution standardized
1 parent 9b72b52 commit 1232548

File tree

27 files changed

+276
-689
lines changed

27 files changed

+276
-689
lines changed

apps/sim/app/api/copilot/execute-tool/route.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -104,17 +104,11 @@ export async function POST(req: NextRequest) {
104104
})
105105

106106
// Build execution params starting with LLM-provided arguments
107-
// Resolve all {{ENV_VAR}} references in the arguments
107+
// Resolve all {{ENV_VAR}} references in the arguments (deep for nested objects)
108108
const executionParams: Record<string, any> = resolveEnvVarReferences(
109109
toolArgs,
110110
decryptedEnvVars,
111-
{
112-
resolveExactMatch: true,
113-
allowEmbedded: true,
114-
trimKeys: true,
115-
onMissing: 'keep',
116-
deep: true,
117-
}
111+
{ deep: true }
118112
) as Record<string, any>
119113

120114
logger.info(`[${tracker.requestId}] Resolved env var references in arguments`, {

apps/sim/app/api/mcp/servers/test-connection/route.ts

Lines changed: 18 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import { createLogger } from '@sim/logger'
22
import type { NextRequest } from 'next/server'
3-
import { getEffectiveDecryptedEnv } from '@/lib/environment/utils'
43
import { McpClient } from '@/lib/mcp/client'
54
import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
6-
import type { McpServerConfig, McpTransport } from '@/lib/mcp/types'
5+
import { resolveMcpConfigEnvVars } from '@/lib/mcp/resolve-config'
6+
import type { McpTransport } from '@/lib/mcp/types'
77
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
8-
import { resolveEnvVarReferences } from '@/executor/utils/reference-validation'
98

109
const logger = createLogger('McpServerTestAPI')
1110

@@ -19,30 +18,6 @@ function isUrlBasedTransport(transport: McpTransport): boolean {
1918
return transport === 'streamable-http'
2019
}
2120

22-
/**
23-
* Resolve environment variables in strings
24-
*/
25-
function resolveEnvVars(value: string, envVars: Record<string, string>): string {
26-
const missingVars: string[] = []
27-
const resolvedValue = resolveEnvVarReferences(value, envVars, {
28-
allowEmbedded: true,
29-
resolveExactMatch: true,
30-
trimKeys: true,
31-
onMissing: 'keep',
32-
deep: false,
33-
missingKeys: missingVars,
34-
}) as string
35-
36-
if (missingVars.length > 0) {
37-
const uniqueMissing = Array.from(new Set(missingVars))
38-
uniqueMissing.forEach((envKey) => {
39-
logger.warn(`Environment variable "${envKey}" not found in MCP server test`)
40-
})
41-
}
42-
43-
return resolvedValue
44-
}
45-
4621
interface TestConnectionRequest {
4722
name: string
4823
transport: McpTransport
@@ -96,39 +71,30 @@ export const POST = withMcpAuth('write')(
9671
)
9772
}
9873

99-
let resolvedUrl = body.url
100-
let resolvedHeaders = body.headers || {}
101-
102-
try {
103-
const envVars = await getEffectiveDecryptedEnv(userId, workspaceId)
104-
105-
if (resolvedUrl) {
106-
resolvedUrl = resolveEnvVars(resolvedUrl, envVars)
107-
}
108-
109-
const resolvedHeadersObj: Record<string, string> = {}
110-
for (const [key, value] of Object.entries(resolvedHeaders)) {
111-
resolvedHeadersObj[key] = resolveEnvVars(value, envVars)
112-
}
113-
resolvedHeaders = resolvedHeadersObj
114-
} catch (envError) {
115-
logger.warn(
116-
`[${requestId}] Failed to resolve environment variables, using raw values:`,
117-
envError
118-
)
119-
}
120-
121-
const testConfig: McpServerConfig = {
74+
// Build initial config for resolution
75+
const initialConfig = {
12276
id: `test-${requestId}`,
12377
name: body.name,
12478
transport: body.transport,
125-
url: resolvedUrl,
126-
headers: resolvedHeaders,
79+
url: body.url,
80+
headers: body.headers || {},
12781
timeout: body.timeout || 10000,
12882
retries: 1, // Only one retry for tests
12983
enabled: true,
13084
}
13185

86+
// Resolve env vars using shared utility (non-strict mode for testing)
87+
const { config: testConfig, missingVars } = await resolveMcpConfigEnvVars(
88+
initialConfig,
89+
userId,
90+
workspaceId,
91+
{ strict: false }
92+
)
93+
94+
if (missingVars.length > 0) {
95+
logger.warn(`[${requestId}] Some environment variables not found:`, { missingVars })
96+
}
97+
13298
const testSecurityPolicy = {
13399
requireConsent: false,
134100
auditLevel: 'none' as const,

apps/sim/app/api/webhooks/[id]/route.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { getSession } from '@/lib/auth'
77
import { validateInteger } from '@/lib/core/security/input-validation'
88
import { PlatformEvents } from '@/lib/core/telemetry'
99
import { generateRequestId } from '@/lib/core/utils/request'
10+
import { resolveEnvVarsInObject } from '@/lib/webhooks/env-resolver'
1011
import {
1112
cleanupExternalWebhook,
1213
createExternalWebhookSubscription,
@@ -112,9 +113,12 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
112113
}
113114
}
114115

116+
// Keep original config with {{ENV_VAR}} patterns for saving to DB
117+
const originalProviderConfig = providerConfig
118+
119+
// Resolve for processing (subscription checks) but don't save resolved values
115120
let resolvedProviderConfig = providerConfig
116121
if (providerConfig) {
117-
const { resolveEnvVarsInObject } = await import('@/lib/webhooks/env-resolver')
118122
const webhookDataForResolve = await db
119123
.select({
120124
workspaceId: workflow.workspaceId,
@@ -230,18 +234,22 @@ export async function PATCH(request: NextRequest, { params }: { params: Promise<
230234
hasFailedCountUpdate: failedCount !== undefined,
231235
})
232236

233-
// Merge providerConfig to preserve credential-related fields
237+
// Merge providerConfig to preserve credential-related fields and system-managed fields
238+
// Use original config (preserves {{ENV_VAR}} patterns) + system fields from existing/updated
234239
let finalProviderConfig = webhooks[0].webhook.providerConfig
235-
if (providerConfig !== undefined) {
240+
if (providerConfig !== undefined && originalProviderConfig) {
236241
const existingConfig = existingProviderConfig
237242
finalProviderConfig = {
238-
...nextProviderConfig,
243+
// Start with original config (preserves {{ENV_VAR}} patterns)
244+
...originalProviderConfig,
245+
// Preserve credential-related and system-managed fields from existing
239246
credentialId: existingConfig.credentialId,
240247
credentialSetId: existingConfig.credentialSetId,
241248
userId: existingConfig.userId,
242249
historyId: existingConfig.historyId,
243250
lastCheckedTimestamp: existingConfig.lastCheckedTimestamp,
244251
setupCompleted: existingConfig.setupCompleted,
252+
// Use updated externalId from subscription recreation, or existing
245253
externalId: nextProviderConfig.externalId ?? existingConfig.externalId,
246254
}
247255
}

apps/sim/app/api/webhooks/route.ts

Lines changed: 28 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { type NextRequest, NextResponse } from 'next/server'
77
import { getSession } from '@/lib/auth'
88
import { PlatformEvents } from '@/lib/core/telemetry'
99
import { generateRequestId } from '@/lib/core/utils/request'
10+
import { resolveEnvVarsInObject } from '@/lib/webhooks/env-resolver'
1011
import { createExternalWebhookSubscription } from '@/lib/webhooks/provider-subscriptions'
1112
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
1213

@@ -300,12 +301,14 @@ export async function POST(request: NextRequest) {
300301

301302
let savedWebhook: any = null // Variable to hold the result of save/update
302303

303-
// Use the original provider config - Gmail/Outlook configuration functions will inject userId automatically
304-
const finalProviderConfig = providerConfig || {}
304+
// Keep original config with {{ENV_VAR}} patterns - these should be saved to DB
305+
// and resolved at runtime, so users can update env vars without recreating webhooks
306+
const originalProviderConfig = providerConfig || {}
305307

306-
const { resolveEnvVarsInObject } = await import('@/lib/webhooks/env-resolver')
308+
// Resolve env vars for processing (credential detection, external subscriptions)
309+
// but don't save resolved values - save original patterns instead
307310
let resolvedProviderConfig = await resolveEnvVarsInObject(
308-
finalProviderConfig,
311+
originalProviderConfig,
309312
userId,
310313
workflowRecord.workspaceId || undefined
311314
)
@@ -469,6 +472,9 @@ export async function POST(request: NextRequest) {
469472
providerConfig: providerConfigOverride,
470473
})
471474

475+
// Config to save to DB - starts with original (preserves {{ENV_VAR}} patterns)
476+
const configToSave = { ...originalProviderConfig }
477+
472478
try {
473479
const result = await createExternalWebhookSubscription(
474480
request,
@@ -477,7 +483,16 @@ export async function POST(request: NextRequest) {
477483
userId,
478484
requestId
479485
)
480-
resolvedProviderConfig = result.updatedProviderConfig as Record<string, unknown>
486+
// Merge any new fields (like externalId) from external subscription into original config
487+
// This preserves {{ENV_VAR}} patterns while adding system-managed fields
488+
const updatedConfig = result.updatedProviderConfig as Record<string, unknown>
489+
for (const [key, value] of Object.entries(updatedConfig)) {
490+
if (!(key in originalProviderConfig)) {
491+
// New field added by external subscription (e.g., externalId, subscriptionId)
492+
configToSave[key] = value
493+
}
494+
}
495+
resolvedProviderConfig = updatedConfig // Keep for logging/credential checks
481496
externalSubscriptionCreated = result.externalSubscriptionCreated
482497
} catch (err) {
483498
logger.error(`[${requestId}] Error creating external webhook subscription`, err)
@@ -491,24 +506,23 @@ export async function POST(request: NextRequest) {
491506
}
492507

493508
// Now save to database (only if subscription succeeded or provider doesn't need external subscription)
509+
// Save configToSave which preserves {{ENV_VAR}} patterns + system-managed fields
494510
try {
495511
if (targetWebhookId) {
496512
logger.info(`[${requestId}] Updating existing webhook for path: ${finalPath}`, {
497513
webhookId: targetWebhookId,
498514
provider,
499-
hasCredentialId: !!(resolvedProviderConfig as any)?.credentialId,
500-
credentialId: (resolvedProviderConfig as any)?.credentialId,
515+
hasCredentialId: !!(configToSave as any)?.credentialId,
516+
credentialId: (configToSave as any)?.credentialId,
501517
})
502518
const updatedResult = await db
503519
.update(webhook)
504520
.set({
505521
blockId,
506522
provider,
507-
providerConfig: resolvedProviderConfig,
523+
providerConfig: configToSave,
508524
credentialSetId:
509-
((resolvedProviderConfig as Record<string, unknown>)?.credentialSetId as
510-
| string
511-
| null) || null,
525+
((configToSave as Record<string, unknown>)?.credentialSetId as string | null) || null,
512526
isActive: true,
513527
updatedAt: new Date(),
514528
})
@@ -531,11 +545,9 @@ export async function POST(request: NextRequest) {
531545
blockId,
532546
path: finalPath,
533547
provider,
534-
providerConfig: resolvedProviderConfig,
548+
providerConfig: configToSave,
535549
credentialSetId:
536-
((resolvedProviderConfig as Record<string, unknown>)?.credentialSetId as
537-
| string
538-
| null) || null,
550+
((configToSave as Record<string, unknown>)?.credentialSetId as string | null) || null,
539551
isActive: true,
540552
createdAt: new Date(),
541553
updatedAt: new Date(),
@@ -549,7 +561,7 @@ export async function POST(request: NextRequest) {
549561
try {
550562
const { cleanupExternalWebhook } = await import('@/lib/webhooks/provider-subscriptions')
551563
await cleanupExternalWebhook(
552-
createTempWebhookData(resolvedProviderConfig),
564+
createTempWebhookData(configToSave),
553565
workflowRecord,
554566
requestId
555567
)

apps/sim/app/api/workflows/[id]/execute/route.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,6 @@ type AsyncExecutionParams = {
116116
userId: string
117117
input: any
118118
triggerType: CoreTriggerType
119-
preflighted?: boolean
120119
}
121120

122121
/**
@@ -139,7 +138,6 @@ async function handleAsyncExecution(params: AsyncExecutionParams): Promise<NextR
139138
userId,
140139
input,
141140
triggerType,
142-
preflighted: params.preflighted,
143141
}
144142

145143
try {
@@ -276,7 +274,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
276274
requestId
277275
)
278276

279-
const shouldPreflightEnvVars = isAsyncMode && isTriggerDevEnabled
280277
const preprocessResult = await preprocessExecution({
281278
workflowId,
282279
userId,
@@ -285,9 +282,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
285282
requestId,
286283
checkDeployment: !shouldUseDraftState,
287284
loggingSession,
288-
preflightEnvVars: shouldPreflightEnvVars,
289285
useDraftState: shouldUseDraftState,
290-
envUserId: isClientSession ? userId : undefined,
291286
})
292287

293288
if (!preprocessResult.success) {
@@ -319,7 +314,6 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
319314
userId: actorUserId,
320315
input,
321316
triggerType: loggingTriggerType,
322-
preflighted: shouldPreflightEnvVars,
323317
})
324318
}
325319

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { useStoreWithEqualityFn } from 'zustand/traditional'
77
import { Badge, Tooltip } from '@/components/emcn'
88
import { cn } from '@/lib/core/utils/cn'
99
import { getBaseUrl } from '@/lib/core/utils/urls'
10-
import { createMcpToolId } from '@/lib/mcp/utils'
10+
import { createMcpToolId } from '@/lib/mcp/shared'
1111
import { getProviderIdFromServiceId } from '@/lib/oauth'
1212
import { BLOCK_DIMENSIONS, HANDLE_POSITIONS } from '@/lib/workflows/blocks/block-dimensions'
1313
import {

0 commit comments

Comments
 (0)