Skip to content

Commit 7529a75

Browse files
fix(triggers): env var resolution in provider configs (#4160)
* fix(triggers): env var resolution in provider configs * throw on errored resolution
1 parent 6b2e83b commit 7529a75

File tree

4 files changed

+209
-1
lines changed

4 files changed

+209
-1
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
5+
import { beforeEach, describe, expect, it, vi } from 'vitest'
6+
7+
const { mockResolveWebhookRecordProviderConfig } = vi.hoisted(() => ({
8+
mockResolveWebhookRecordProviderConfig: vi.fn(),
9+
}))
10+
11+
vi.mock('@/lib/webhooks/env-resolver', () => ({
12+
resolveWebhookRecordProviderConfig: mockResolveWebhookRecordProviderConfig,
13+
}))
14+
15+
import { resolveWebhookExecutionProviderConfig } from './webhook-execution'
16+
17+
describe('resolveWebhookExecutionProviderConfig', () => {
18+
beforeEach(() => {
19+
vi.clearAllMocks()
20+
})
21+
22+
it('returns the resolved webhook record when provider config resolution succeeds', async () => {
23+
const webhookRecord = {
24+
id: 'webhook-1',
25+
providerConfig: {
26+
botToken: '{{SLACK_BOT_TOKEN}}',
27+
},
28+
}
29+
const resolvedWebhookRecord = {
30+
...webhookRecord,
31+
providerConfig: {
32+
botToken: 'xoxb-resolved',
33+
},
34+
}
35+
36+
mockResolveWebhookRecordProviderConfig.mockResolvedValue(resolvedWebhookRecord)
37+
38+
await expect(
39+
resolveWebhookExecutionProviderConfig(webhookRecord, 'slack', 'user-1', 'workspace-1')
40+
).resolves.toEqual(resolvedWebhookRecord)
41+
42+
expect(mockResolveWebhookRecordProviderConfig).toHaveBeenCalledWith(
43+
webhookRecord,
44+
'user-1',
45+
'workspace-1'
46+
)
47+
})
48+
49+
it('throws a contextual error when provider config resolution fails', async () => {
50+
mockResolveWebhookRecordProviderConfig.mockRejectedValue(new Error('env lookup failed'))
51+
52+
await expect(
53+
resolveWebhookExecutionProviderConfig(
54+
{
55+
id: 'webhook-1',
56+
providerConfig: {
57+
botToken: '{{SLACK_BOT_TOKEN}}',
58+
},
59+
},
60+
'slack',
61+
'user-1',
62+
'workspace-1'
63+
)
64+
).rejects.toThrow(
65+
'Failed to resolve webhook provider config for slack webhook webhook-1: env lookup failed'
66+
)
67+
})
68+
})

apps/sim/background/webhook-execution.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { preprocessExecution } from '@/lib/execution/preprocessing'
1111
import { LoggingSession } from '@/lib/logs/execution/logging-session'
1212
import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans'
1313
import { WebhookAttachmentProcessor } from '@/lib/webhooks/attachment-processor'
14+
import { resolveWebhookRecordProviderConfig } from '@/lib/webhooks/env-resolver'
1415
import { getProviderHandler } from '@/lib/webhooks/providers'
1516
import {
1617
executeWorkflowCore,
@@ -168,6 +169,24 @@ export async function executeWebhookJob(payload: WebhookExecutionPayload) {
168169
)
169170
}
170171

172+
export async function resolveWebhookExecutionProviderConfig<
173+
T extends { id: string; providerConfig?: unknown },
174+
>(
175+
webhookRecord: T,
176+
provider: string,
177+
userId: string,
178+
workspaceId?: string
179+
): Promise<T & { providerConfig: Record<string, unknown> }> {
180+
try {
181+
return await resolveWebhookRecordProviderConfig(webhookRecord, userId, workspaceId)
182+
} catch (error) {
183+
const errorMessage = error instanceof Error ? error.message : String(error)
184+
throw new Error(
185+
`Failed to resolve webhook provider config for ${provider} webhook ${webhookRecord.id}: ${errorMessage}`
186+
)
187+
}
188+
}
189+
171190
async function resolveCredentialAccountUserId(credentialId: string): Promise<string | undefined> {
172191
const resolved = await resolveOAuthAccountId(credentialId)
173192
if (!resolved) {
@@ -300,9 +319,16 @@ async function executeWebhookJobInternal(
300319
throw new Error(`Webhook record not found: ${payload.webhookId}`)
301320
}
302321

322+
const resolvedWebhookRecord = await resolveWebhookExecutionProviderConfig(
323+
webhookRecord,
324+
payload.provider,
325+
workflowRecord.userId,
326+
workspaceId
327+
)
328+
303329
if (handler.formatInput) {
304330
const result = await handler.formatInput({
305-
webhook: webhookRecord,
331+
webhook: resolvedWebhookRecord,
306332
workflow: { id: payload.workflowId, userId: payload.userId },
307333
body: payload.body,
308334
headers: payload.headers,
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
5+
import { beforeEach, describe, expect, it, vi } from 'vitest'
6+
7+
const { mockGetEffectiveDecryptedEnv } = vi.hoisted(() => ({
8+
mockGetEffectiveDecryptedEnv: vi.fn(),
9+
}))
10+
11+
vi.mock('@/lib/environment/utils', () => ({
12+
getEffectiveDecryptedEnv: mockGetEffectiveDecryptedEnv,
13+
}))
14+
15+
import {
16+
resolveWebhookProviderConfig,
17+
resolveWebhookRecordProviderConfig,
18+
} from '@/lib/webhooks/env-resolver'
19+
20+
describe('webhook env resolver', () => {
21+
beforeEach(() => {
22+
vi.clearAllMocks()
23+
mockGetEffectiveDecryptedEnv.mockResolvedValue({
24+
SLACK_BOT_TOKEN: 'xoxb-resolved',
25+
SLACK_HOST: 'files.slack.com',
26+
})
27+
})
28+
29+
it('resolves environment variables inside webhook provider config', async () => {
30+
const result = await resolveWebhookProviderConfig(
31+
{
32+
botToken: '{{SLACK_BOT_TOKEN}}',
33+
includeFiles: true,
34+
nested: {
35+
url: 'https://{{SLACK_HOST}}/api/files.info',
36+
},
37+
},
38+
'user-1',
39+
'workspace-1'
40+
)
41+
42+
expect(result).toEqual({
43+
botToken: 'xoxb-resolved',
44+
includeFiles: true,
45+
nested: {
46+
url: 'https://files.slack.com/api/files.info',
47+
},
48+
})
49+
expect(mockGetEffectiveDecryptedEnv).toHaveBeenCalledWith('user-1', 'workspace-1')
50+
})
51+
52+
it('returns a cloned webhook record with resolved provider config', async () => {
53+
const webhookRecord = {
54+
id: 'webhook-1',
55+
provider: 'slack',
56+
providerConfig: {
57+
botToken: '{{SLACK_BOT_TOKEN}}',
58+
includeFiles: true,
59+
},
60+
}
61+
62+
const result = await resolveWebhookRecordProviderConfig(webhookRecord, 'user-1', 'workspace-1')
63+
64+
expect(result).toEqual({
65+
...webhookRecord,
66+
providerConfig: {
67+
botToken: 'xoxb-resolved',
68+
includeFiles: true,
69+
},
70+
})
71+
expect(result).not.toBe(webhookRecord)
72+
expect(result.providerConfig).not.toBe(webhookRecord.providerConfig)
73+
})
74+
})

apps/sim/lib/webhooks/env-resolver.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,43 @@ export async function resolveEnvVarsInObject<T extends Record<string, unknown>>(
2020
const envVars = await getEffectiveDecryptedEnv(userId, workspaceId)
2121
return resolveEnvVarReferences(config, envVars, { deep: true }) as T
2222
}
23+
24+
/**
25+
* Normalizes webhook provider config into a plain object for runtime resolution.
26+
*/
27+
export function normalizeWebhookProviderConfig(providerConfig: unknown): Record<string, unknown> {
28+
if (providerConfig && typeof providerConfig === 'object' && !Array.isArray(providerConfig)) {
29+
return providerConfig as Record<string, unknown>
30+
}
31+
32+
return {}
33+
}
34+
35+
/**
36+
* Resolves environment variable references inside a webhook provider config object.
37+
*/
38+
export async function resolveWebhookProviderConfig(
39+
providerConfig: unknown,
40+
userId: string,
41+
workspaceId?: string
42+
): Promise<Record<string, unknown>> {
43+
return resolveEnvVarsInObject(normalizeWebhookProviderConfig(providerConfig), userId, workspaceId)
44+
}
45+
46+
/**
47+
* Clones a webhook-like record with its provider config resolved for runtime use.
48+
*/
49+
export async function resolveWebhookRecordProviderConfig<T extends { providerConfig?: unknown }>(
50+
webhookRecord: T,
51+
userId: string,
52+
workspaceId?: string
53+
): Promise<T & { providerConfig: Record<string, unknown> }> {
54+
return {
55+
...webhookRecord,
56+
providerConfig: await resolveWebhookProviderConfig(
57+
webhookRecord.providerConfig,
58+
userId,
59+
workspaceId
60+
),
61+
}
62+
}

0 commit comments

Comments
 (0)