mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
fix(triggers): env var resolution in provider configs (#4160)
* fix(triggers): env var resolution in provider configs * throw on errored resolution
This commit is contained in:
@@ -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'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
@@ -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) {
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user