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
68 changes: 68 additions & 0 deletions apps/sim/background/webhook-execution.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* @vitest-environment node
*/

import { beforeEach, describe, expect, it, vi } from 'vitest'

const { mockResolveWebhookRecordProviderConfig } = vi.hoisted(() => ({
mockResolveWebhookRecordProviderConfig: vi.fn(),
}))

vi.mock('@/lib/webhooks/env-resolver', () => ({
resolveWebhookRecordProviderConfig: mockResolveWebhookRecordProviderConfig,
}))

import { resolveWebhookExecutionProviderConfig } from './webhook-execution'

describe('resolveWebhookExecutionProviderConfig', () => {
beforeEach(() => {
vi.clearAllMocks()
})

it('returns the resolved webhook record when provider config resolution succeeds', async () => {
const webhookRecord = {
id: 'webhook-1',
providerConfig: {
botToken: '{{SLACK_BOT_TOKEN}}',
},
}
const resolvedWebhookRecord = {
...webhookRecord,
providerConfig: {
botToken: 'xoxb-resolved',
},
}

mockResolveWebhookRecordProviderConfig.mockResolvedValue(resolvedWebhookRecord)

await expect(
resolveWebhookExecutionProviderConfig(webhookRecord, 'slack', 'user-1', 'workspace-1')
).resolves.toEqual(resolvedWebhookRecord)

expect(mockResolveWebhookRecordProviderConfig).toHaveBeenCalledWith(
webhookRecord,
'user-1',
'workspace-1'
)
})

it('throws a contextual error when provider config resolution fails', async () => {
mockResolveWebhookRecordProviderConfig.mockRejectedValue(new Error('env lookup failed'))

await expect(
resolveWebhookExecutionProviderConfig(
{
id: 'webhook-1',
providerConfig: {
botToken: '{{SLACK_BOT_TOKEN}}',
},
},
'slack',
'user-1',
'workspace-1'
)
).rejects.toThrow(
'Failed to resolve webhook provider config for slack webhook webhook-1: env lookup failed'
)
})
})
28 changes: 27 additions & 1 deletion apps/sim/background/webhook-execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { preprocessExecution } from '@/lib/execution/preprocessing'
import { LoggingSession } from '@/lib/logs/execution/logging-session'
import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans'
import { WebhookAttachmentProcessor } from '@/lib/webhooks/attachment-processor'
import { resolveWebhookRecordProviderConfig } from '@/lib/webhooks/env-resolver'
import { getProviderHandler } from '@/lib/webhooks/providers'
import {
executeWorkflowCore,
Expand Down Expand Up @@ -168,6 +169,24 @@ export async function executeWebhookJob(payload: WebhookExecutionPayload) {
)
}

export async function resolveWebhookExecutionProviderConfig<
T extends { id: string; providerConfig?: unknown },
>(
webhookRecord: T,
provider: string,
userId: string,
workspaceId?: string
): Promise<T & { providerConfig: Record<string, unknown> }> {
try {
return await resolveWebhookRecordProviderConfig(webhookRecord, userId, workspaceId)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
throw new Error(
`Failed to resolve webhook provider config for ${provider} webhook ${webhookRecord.id}: ${errorMessage}`
)
}
}

async function resolveCredentialAccountUserId(credentialId: string): Promise<string | undefined> {
const resolved = await resolveOAuthAccountId(credentialId)
if (!resolved) {
Expand Down Expand Up @@ -300,9 +319,16 @@ async function executeWebhookJobInternal(
throw new Error(`Webhook record not found: ${payload.webhookId}`)
}

const resolvedWebhookRecord = await resolveWebhookExecutionProviderConfig(
webhookRecord,
payload.provider,
workflowRecord.userId,
workspaceId
)

if (handler.formatInput) {
const result = await handler.formatInput({
webhook: webhookRecord,
webhook: resolvedWebhookRecord,
workflow: { id: payload.workflowId, userId: payload.userId },
body: payload.body,
headers: payload.headers,
Expand Down
74 changes: 74 additions & 0 deletions apps/sim/lib/webhooks/env-resolver.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/**
* @vitest-environment node
*/

import { beforeEach, describe, expect, it, vi } from 'vitest'

const { mockGetEffectiveDecryptedEnv } = vi.hoisted(() => ({
mockGetEffectiveDecryptedEnv: vi.fn(),
}))

vi.mock('@/lib/environment/utils', () => ({
getEffectiveDecryptedEnv: mockGetEffectiveDecryptedEnv,
}))

import {
resolveWebhookProviderConfig,
resolveWebhookRecordProviderConfig,
} from '@/lib/webhooks/env-resolver'

describe('webhook env resolver', () => {
beforeEach(() => {
vi.clearAllMocks()
mockGetEffectiveDecryptedEnv.mockResolvedValue({
SLACK_BOT_TOKEN: 'xoxb-resolved',
SLACK_HOST: 'files.slack.com',
})
})

it('resolves environment variables inside webhook provider config', async () => {
const result = await resolveWebhookProviderConfig(
{
botToken: '{{SLACK_BOT_TOKEN}}',
includeFiles: true,
nested: {
url: 'https://{{SLACK_HOST}}/api/files.info',
},
},
'user-1',
'workspace-1'
)

expect(result).toEqual({
botToken: 'xoxb-resolved',
includeFiles: true,
nested: {
url: 'https://files.slack.com/api/files.info',
},
})
expect(mockGetEffectiveDecryptedEnv).toHaveBeenCalledWith('user-1', 'workspace-1')
})

it('returns a cloned webhook record with resolved provider config', async () => {
const webhookRecord = {
id: 'webhook-1',
provider: 'slack',
providerConfig: {
botToken: '{{SLACK_BOT_TOKEN}}',
includeFiles: true,
},
}

const result = await resolveWebhookRecordProviderConfig(webhookRecord, 'user-1', 'workspace-1')

expect(result).toEqual({
...webhookRecord,
providerConfig: {
botToken: 'xoxb-resolved',
includeFiles: true,
},
})
expect(result).not.toBe(webhookRecord)
expect(result.providerConfig).not.toBe(webhookRecord.providerConfig)
})
})
40 changes: 40 additions & 0 deletions apps/sim/lib/webhooks/env-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,43 @@ export async function resolveEnvVarsInObject<T extends Record<string, unknown>>(
const envVars = await getEffectiveDecryptedEnv(userId, workspaceId)
return resolveEnvVarReferences(config, envVars, { deep: true }) as T
}

/**
* Normalizes webhook provider config into a plain object for runtime resolution.
*/
export function normalizeWebhookProviderConfig(providerConfig: unknown): Record<string, unknown> {
if (providerConfig && typeof providerConfig === 'object' && !Array.isArray(providerConfig)) {
return providerConfig as Record<string, unknown>
}

return {}
}

/**
* Resolves environment variable references inside a webhook provider config object.
*/
export async function resolveWebhookProviderConfig(
providerConfig: unknown,
userId: string,
workspaceId?: string
): Promise<Record<string, unknown>> {
return resolveEnvVarsInObject(normalizeWebhookProviderConfig(providerConfig), userId, workspaceId)
}

/**
* Clones a webhook-like record with its provider config resolved for runtime use.
*/
export async function resolveWebhookRecordProviderConfig<T extends { providerConfig?: unknown }>(
webhookRecord: T,
userId: string,
workspaceId?: string
): Promise<T & { providerConfig: Record<string, unknown> }> {
return {
...webhookRecord,
providerConfig: await resolveWebhookProviderConfig(
webhookRecord.providerConfig,
userId,
workspaceId
),
}
}
Loading