From 820ad85adb146bdd7642989b1fe54b66044b6238 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 6 Apr 2026 10:57:00 -0700 Subject: [PATCH 1/5] feat(triggers): add Vercel webhook triggers with automatic registration --- apps/sim/blocks/blocks/vercel.ts | 24 ++ apps/sim/lib/webhooks/providers/registry.ts | 2 + apps/sim/lib/webhooks/providers/vercel.ts | 184 ++++++++++++++ apps/sim/triggers/registry.ts | 18 ++ .../triggers/vercel/deployment_canceled.ts | 37 +++ .../sim/triggers/vercel/deployment_created.ts | 40 ++++ apps/sim/triggers/vercel/deployment_error.ts | 37 +++ apps/sim/triggers/vercel/deployment_ready.ts | 37 +++ apps/sim/triggers/vercel/domain_created.ts | 37 +++ apps/sim/triggers/vercel/index.ts | 13 + apps/sim/triggers/vercel/project_created.ts | 37 +++ apps/sim/triggers/vercel/project_removed.ts | 37 +++ apps/sim/triggers/vercel/utils.ts | 225 ++++++++++++++++++ apps/sim/triggers/vercel/webhook.ts | 38 +++ 14 files changed, 766 insertions(+) create mode 100644 apps/sim/lib/webhooks/providers/vercel.ts create mode 100644 apps/sim/triggers/vercel/deployment_canceled.ts create mode 100644 apps/sim/triggers/vercel/deployment_created.ts create mode 100644 apps/sim/triggers/vercel/deployment_error.ts create mode 100644 apps/sim/triggers/vercel/deployment_ready.ts create mode 100644 apps/sim/triggers/vercel/domain_created.ts create mode 100644 apps/sim/triggers/vercel/index.ts create mode 100644 apps/sim/triggers/vercel/project_created.ts create mode 100644 apps/sim/triggers/vercel/project_removed.ts create mode 100644 apps/sim/triggers/vercel/utils.ts create mode 100644 apps/sim/triggers/vercel/webhook.ts diff --git a/apps/sim/blocks/blocks/vercel.ts b/apps/sim/blocks/blocks/vercel.ts index 0a89cc24c34..0993e6ed723 100644 --- a/apps/sim/blocks/blocks/vercel.ts +++ b/apps/sim/blocks/blocks/vercel.ts @@ -1,6 +1,7 @@ import { VercelIcon } from '@/components/icons' import type { BlockConfig } from '@/blocks/types' import { AuthMode, IntegrationType } from '@/blocks/types' +import { getTrigger } from '@/triggers' export const VercelBlock: BlockConfig = { type: 'vercel', @@ -15,6 +16,19 @@ export const VercelBlock: BlockConfig = { bgColor: '#171717', icon: VercelIcon, authMode: AuthMode.ApiKey, + triggers: { + enabled: true, + available: [ + 'vercel_deployment_created', + 'vercel_deployment_ready', + 'vercel_deployment_error', + 'vercel_deployment_canceled', + 'vercel_project_created', + 'vercel_project_removed', + 'vercel_domain_created', + 'vercel_webhook', + ], + }, subBlocks: [ { id: 'operation', @@ -649,6 +663,16 @@ export const VercelBlock: BlockConfig = { }, mode: 'advanced', }, + + // === Trigger subBlocks === + ...getTrigger('vercel_deployment_created').subBlocks, + ...getTrigger('vercel_deployment_ready').subBlocks, + ...getTrigger('vercel_deployment_error').subBlocks, + ...getTrigger('vercel_deployment_canceled').subBlocks, + ...getTrigger('vercel_project_created').subBlocks, + ...getTrigger('vercel_project_removed').subBlocks, + ...getTrigger('vercel_domain_created').subBlocks, + ...getTrigger('vercel_webhook').subBlocks, ], tools: { access: [ diff --git a/apps/sim/lib/webhooks/providers/registry.ts b/apps/sim/lib/webhooks/providers/registry.ts index 00ae58a21b1..72074552db9 100644 --- a/apps/sim/lib/webhooks/providers/registry.ts +++ b/apps/sim/lib/webhooks/providers/registry.ts @@ -30,6 +30,7 @@ import { twilioVoiceHandler } from '@/lib/webhooks/providers/twilio-voice' import { typeformHandler } from '@/lib/webhooks/providers/typeform' import type { WebhookProviderHandler } from '@/lib/webhooks/providers/types' import { verifyTokenAuth } from '@/lib/webhooks/providers/utils' +import { vercelHandler } from '@/lib/webhooks/providers/vercel' import { webflowHandler } from '@/lib/webhooks/providers/webflow' import { whatsappHandler } from '@/lib/webhooks/providers/whatsapp' @@ -64,6 +65,7 @@ const PROVIDER_HANDLERS: Record = { twilio: twilioHandler, twilio_voice: twilioVoiceHandler, typeform: typeformHandler, + vercel: vercelHandler, webflow: webflowHandler, whatsapp: whatsappHandler, } diff --git a/apps/sim/lib/webhooks/providers/vercel.ts b/apps/sim/lib/webhooks/providers/vercel.ts new file mode 100644 index 00000000000..db6f298ba05 --- /dev/null +++ b/apps/sim/lib/webhooks/providers/vercel.ts @@ -0,0 +1,184 @@ +import { createLogger } from '@sim/logger' +import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/providers/subscription-utils' +import type { + DeleteSubscriptionContext, + FormatInputContext, + FormatInputResult, + SubscriptionContext, + SubscriptionResult, + WebhookProviderHandler, +} from '@/lib/webhooks/providers/types' + +const logger = createLogger('WebhookProvider:Vercel') + +export const vercelHandler: WebhookProviderHandler = { + async createSubscription(ctx: SubscriptionContext): Promise { + const { webhook, requestId } = ctx + try { + const providerConfig = getProviderConfig(webhook) + const apiKey = providerConfig.apiKey as string | undefined + const triggerId = providerConfig.triggerId as string | undefined + const teamId = providerConfig.teamId as string | undefined + const filterProjectIds = providerConfig.filterProjectIds as string | undefined + + if (!apiKey) { + throw new Error( + 'Vercel Access Token is required. Please provide your access token in the trigger configuration.' + ) + } + + const eventTypeMap: Record = { + vercel_deployment_created: ['deployment.created'], + vercel_deployment_ready: ['deployment.ready'], + vercel_deployment_error: ['deployment.error'], + vercel_deployment_canceled: ['deployment.canceled'], + vercel_project_created: ['project.created'], + vercel_project_removed: ['project.removed'], + vercel_domain_created: ['domain.created'], + vercel_webhook: undefined, + } + + const events = eventTypeMap[triggerId ?? ''] + const notificationUrl = getNotificationUrl(webhook) + + logger.info(`[${requestId}] Creating Vercel webhook`, { + triggerId, + events, + hasTeamId: !!teamId, + hasProjectIds: !!filterProjectIds, + webhookId: webhook.id, + }) + + const requestBody: Record = { + url: notificationUrl, + events: events || [ + 'deployment.created', + 'deployment.ready', + 'deployment.error', + 'deployment.canceled', + 'project.created', + 'project.removed', + 'domain.created', + ], + } + + if (filterProjectIds) { + const projectIds = String(filterProjectIds) + .split(',') + .map((id: string) => id.trim()) + .filter(Boolean) + if (projectIds.length > 0) { + requestBody.projectIds = projectIds + } + } + + const apiUrl = teamId + ? `https://api.vercel.com/v1/webhooks?teamId=${encodeURIComponent(teamId)}` + : 'https://api.vercel.com/v1/webhooks' + + const vercelResponse = await fetch(apiUrl, { + method: 'POST', + headers: { + Authorization: `Bearer ${apiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody), + }) + + const responseBody = (await vercelResponse.json()) as Record + + if (!vercelResponse.ok) { + const errorObj = responseBody.error as Record | undefined + const errorMessage = + (errorObj?.message as string) || + (responseBody.message as string) || + 'Unknown Vercel API error' + + let userFriendlyMessage = 'Failed to create webhook subscription in Vercel' + if (vercelResponse.status === 401 || vercelResponse.status === 403) { + userFriendlyMessage = + 'Invalid or insufficient Vercel Access Token. Please verify your token has the correct permissions.' + } else if (errorMessage && errorMessage !== 'Unknown Vercel API error') { + userFriendlyMessage = `Vercel error: ${errorMessage}` + } + + throw new Error(userFriendlyMessage) + } + + logger.info( + `[${requestId}] Successfully created webhook in Vercel for webhook ${webhook.id}.`, + { vercelWebhookId: responseBody.id } + ) + + return { providerConfigUpdates: { externalId: responseBody.id } } + } catch (error: unknown) { + const err = error as Error + logger.error( + `[${requestId}] Exception during Vercel webhook creation for webhook ${webhook.id}.`, + { message: err.message, stack: err.stack } + ) + throw error + } + }, + + async deleteSubscription(ctx: DeleteSubscriptionContext): Promise { + const { webhook, requestId } = ctx + try { + const config = getProviderConfig(webhook) + const apiKey = config.apiKey as string | undefined + const externalId = config.externalId as string | undefined + const teamId = config.teamId as string | undefined + + if (!apiKey || !externalId) { + logger.warn( + `[${requestId}] Missing apiKey or externalId for Vercel webhook deletion ${webhook.id}, skipping cleanup` + ) + return + } + + const apiUrl = teamId + ? `https://api.vercel.com/v1/webhooks/${encodeURIComponent(externalId)}?teamId=${encodeURIComponent(teamId)}` + : `https://api.vercel.com/v1/webhooks/${encodeURIComponent(externalId)}` + + const response = await fetch(apiUrl, { + method: 'DELETE', + headers: { + Authorization: `Bearer ${apiKey}`, + }, + }) + + if (!response.ok && response.status !== 404) { + logger.warn( + `[${requestId}] Failed to delete Vercel webhook (non-fatal): ${response.status}` + ) + } else { + await response.body?.cancel() + logger.info(`[${requestId}] Successfully deleted Vercel webhook ${externalId}`) + } + } catch (error) { + logger.warn(`[${requestId}] Error deleting Vercel webhook (non-fatal)`, error) + } + }, + + async formatInput(ctx: FormatInputContext): Promise { + const body = ctx.body as Record + const payload = (body.payload || {}) as Record + + return { + input: { + type: body.type || '', + id: body.id || '', + createdAt: body.createdAt || 0, + region: body.region || null, + payload, + deployment: payload.deployment || null, + project: payload.project || null, + team: payload.team || null, + user: payload.user || null, + target: payload.target || null, + plan: payload.plan || null, + domain: payload.domain || null, + }, + } + }, +} diff --git a/apps/sim/triggers/registry.ts b/apps/sim/triggers/registry.ts index 4390bfeefff..03bbe908f9d 100644 --- a/apps/sim/triggers/registry.ts +++ b/apps/sim/triggers/registry.ts @@ -169,6 +169,16 @@ import { telegramWebhookTrigger } from '@/triggers/telegram' import { twilioVoiceWebhookTrigger } from '@/triggers/twilio_voice' import { typeformWebhookTrigger } from '@/triggers/typeform' import type { TriggerRegistry } from '@/triggers/types' +import { + vercelDeploymentCanceledTrigger, + vercelDeploymentCreatedTrigger, + vercelDeploymentErrorTrigger, + vercelDeploymentReadyTrigger, + vercelDomainCreatedTrigger, + vercelProjectCreatedTrigger, + vercelProjectRemovedTrigger, + vercelWebhookTrigger, +} from '@/triggers/vercel' import { webflowCollectionItemChangedTrigger, webflowCollectionItemCreatedTrigger, @@ -305,6 +315,14 @@ export const TRIGGER_REGISTRY: TriggerRegistry = { whatsapp_webhook: whatsappWebhookTrigger, google_forms_webhook: googleFormsWebhookTrigger, twilio_voice_webhook: twilioVoiceWebhookTrigger, + vercel_deployment_created: vercelDeploymentCreatedTrigger, + vercel_deployment_ready: vercelDeploymentReadyTrigger, + vercel_deployment_error: vercelDeploymentErrorTrigger, + vercel_deployment_canceled: vercelDeploymentCanceledTrigger, + vercel_project_created: vercelProjectCreatedTrigger, + vercel_project_removed: vercelProjectRemovedTrigger, + vercel_domain_created: vercelDomainCreatedTrigger, + vercel_webhook: vercelWebhookTrigger, webflow_collection_item_created: webflowCollectionItemCreatedTrigger, webflow_collection_item_changed: webflowCollectionItemChangedTrigger, webflow_collection_item_deleted: webflowCollectionItemDeletedTrigger, diff --git a/apps/sim/triggers/vercel/deployment_canceled.ts b/apps/sim/triggers/vercel/deployment_canceled.ts new file mode 100644 index 00000000000..3cf110912ef --- /dev/null +++ b/apps/sim/triggers/vercel/deployment_canceled.ts @@ -0,0 +1,37 @@ +import { VercelIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import type { TriggerConfig } from '@/triggers/types' +import { + buildDeploymentOutputs, + buildVercelExtraFields, + vercelSetupInstructions, + vercelTriggerOptions, +} from '@/triggers/vercel/utils' + +/** + * Vercel Deployment Canceled Trigger + */ +export const vercelDeploymentCanceledTrigger: TriggerConfig = { + id: 'vercel_deployment_canceled', + name: 'Vercel Deployment Canceled', + provider: 'vercel', + description: 'Trigger workflow when a deployment is canceled', + version: '1.0.0', + icon: VercelIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'vercel_deployment_canceled', + triggerOptions: vercelTriggerOptions, + setupInstructions: vercelSetupInstructions('Deployment Canceled'), + extraFields: buildVercelExtraFields('vercel_deployment_canceled'), + }), + + outputs: buildDeploymentOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/vercel/deployment_created.ts b/apps/sim/triggers/vercel/deployment_created.ts new file mode 100644 index 00000000000..67b7817405d --- /dev/null +++ b/apps/sim/triggers/vercel/deployment_created.ts @@ -0,0 +1,40 @@ +import { VercelIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import type { TriggerConfig } from '@/triggers/types' +import { + buildDeploymentOutputs, + buildVercelExtraFields, + vercelSetupInstructions, + vercelTriggerOptions, +} from '@/triggers/vercel/utils' + +/** + * Vercel Deployment Created Trigger + * + * This is the PRIMARY trigger - it includes the dropdown for selecting trigger type. + */ +export const vercelDeploymentCreatedTrigger: TriggerConfig = { + id: 'vercel_deployment_created', + name: 'Vercel Deployment Created', + provider: 'vercel', + description: 'Trigger workflow when a new deployment is created', + version: '1.0.0', + icon: VercelIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'vercel_deployment_created', + triggerOptions: vercelTriggerOptions, + includeDropdown: true, + setupInstructions: vercelSetupInstructions('Deployment Created'), + extraFields: buildVercelExtraFields('vercel_deployment_created'), + }), + + outputs: buildDeploymentOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/vercel/deployment_error.ts b/apps/sim/triggers/vercel/deployment_error.ts new file mode 100644 index 00000000000..8b14d390b0c --- /dev/null +++ b/apps/sim/triggers/vercel/deployment_error.ts @@ -0,0 +1,37 @@ +import { VercelIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import type { TriggerConfig } from '@/triggers/types' +import { + buildDeploymentOutputs, + buildVercelExtraFields, + vercelSetupInstructions, + vercelTriggerOptions, +} from '@/triggers/vercel/utils' + +/** + * Vercel Deployment Error Trigger + */ +export const vercelDeploymentErrorTrigger: TriggerConfig = { + id: 'vercel_deployment_error', + name: 'Vercel Deployment Error', + provider: 'vercel', + description: 'Trigger workflow when a deployment fails', + version: '1.0.0', + icon: VercelIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'vercel_deployment_error', + triggerOptions: vercelTriggerOptions, + setupInstructions: vercelSetupInstructions('Deployment Error'), + extraFields: buildVercelExtraFields('vercel_deployment_error'), + }), + + outputs: buildDeploymentOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/vercel/deployment_ready.ts b/apps/sim/triggers/vercel/deployment_ready.ts new file mode 100644 index 00000000000..979fe3bdebc --- /dev/null +++ b/apps/sim/triggers/vercel/deployment_ready.ts @@ -0,0 +1,37 @@ +import { VercelIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import type { TriggerConfig } from '@/triggers/types' +import { + buildDeploymentOutputs, + buildVercelExtraFields, + vercelSetupInstructions, + vercelTriggerOptions, +} from '@/triggers/vercel/utils' + +/** + * Vercel Deployment Ready Trigger + */ +export const vercelDeploymentReadyTrigger: TriggerConfig = { + id: 'vercel_deployment_ready', + name: 'Vercel Deployment Ready', + provider: 'vercel', + description: 'Trigger workflow when a deployment is ready to serve traffic', + version: '1.0.0', + icon: VercelIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'vercel_deployment_ready', + triggerOptions: vercelTriggerOptions, + setupInstructions: vercelSetupInstructions('Deployment Ready'), + extraFields: buildVercelExtraFields('vercel_deployment_ready'), + }), + + outputs: buildDeploymentOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/vercel/domain_created.ts b/apps/sim/triggers/vercel/domain_created.ts new file mode 100644 index 00000000000..d3dd35c1282 --- /dev/null +++ b/apps/sim/triggers/vercel/domain_created.ts @@ -0,0 +1,37 @@ +import { VercelIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import type { TriggerConfig } from '@/triggers/types' +import { + buildDomainOutputs, + buildVercelExtraFields, + vercelSetupInstructions, + vercelTriggerOptions, +} from '@/triggers/vercel/utils' + +/** + * Vercel Domain Created Trigger + */ +export const vercelDomainCreatedTrigger: TriggerConfig = { + id: 'vercel_domain_created', + name: 'Vercel Domain Created', + provider: 'vercel', + description: 'Trigger workflow when a domain is created', + version: '1.0.0', + icon: VercelIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'vercel_domain_created', + triggerOptions: vercelTriggerOptions, + setupInstructions: vercelSetupInstructions('Domain Created'), + extraFields: buildVercelExtraFields('vercel_domain_created'), + }), + + outputs: buildDomainOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/vercel/index.ts b/apps/sim/triggers/vercel/index.ts new file mode 100644 index 00000000000..da5ad6c7759 --- /dev/null +++ b/apps/sim/triggers/vercel/index.ts @@ -0,0 +1,13 @@ +/** + * Vercel Triggers + * Export all Vercel webhook triggers + */ + +export { vercelDeploymentCanceledTrigger } from './deployment_canceled' +export { vercelDeploymentCreatedTrigger } from './deployment_created' +export { vercelDeploymentErrorTrigger } from './deployment_error' +export { vercelDeploymentReadyTrigger } from './deployment_ready' +export { vercelDomainCreatedTrigger } from './domain_created' +export { vercelProjectCreatedTrigger } from './project_created' +export { vercelProjectRemovedTrigger } from './project_removed' +export { vercelWebhookTrigger } from './webhook' diff --git a/apps/sim/triggers/vercel/project_created.ts b/apps/sim/triggers/vercel/project_created.ts new file mode 100644 index 00000000000..2baadc631cf --- /dev/null +++ b/apps/sim/triggers/vercel/project_created.ts @@ -0,0 +1,37 @@ +import { VercelIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import type { TriggerConfig } from '@/triggers/types' +import { + buildProjectOutputs, + buildVercelExtraFields, + vercelSetupInstructions, + vercelTriggerOptions, +} from '@/triggers/vercel/utils' + +/** + * Vercel Project Created Trigger + */ +export const vercelProjectCreatedTrigger: TriggerConfig = { + id: 'vercel_project_created', + name: 'Vercel Project Created', + provider: 'vercel', + description: 'Trigger workflow when a new project is created', + version: '1.0.0', + icon: VercelIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'vercel_project_created', + triggerOptions: vercelTriggerOptions, + setupInstructions: vercelSetupInstructions('Project Created'), + extraFields: buildVercelExtraFields('vercel_project_created'), + }), + + outputs: buildProjectOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/vercel/project_removed.ts b/apps/sim/triggers/vercel/project_removed.ts new file mode 100644 index 00000000000..451c63063c7 --- /dev/null +++ b/apps/sim/triggers/vercel/project_removed.ts @@ -0,0 +1,37 @@ +import { VercelIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import type { TriggerConfig } from '@/triggers/types' +import { + buildProjectOutputs, + buildVercelExtraFields, + vercelSetupInstructions, + vercelTriggerOptions, +} from '@/triggers/vercel/utils' + +/** + * Vercel Project Removed Trigger + */ +export const vercelProjectRemovedTrigger: TriggerConfig = { + id: 'vercel_project_removed', + name: 'Vercel Project Removed', + provider: 'vercel', + description: 'Trigger workflow when a project is removed', + version: '1.0.0', + icon: VercelIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'vercel_project_removed', + triggerOptions: vercelTriggerOptions, + setupInstructions: vercelSetupInstructions('Project Removed'), + extraFields: buildVercelExtraFields('vercel_project_removed'), + }), + + outputs: buildProjectOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/vercel/utils.ts b/apps/sim/triggers/vercel/utils.ts new file mode 100644 index 00000000000..44845763933 --- /dev/null +++ b/apps/sim/triggers/vercel/utils.ts @@ -0,0 +1,225 @@ +import type { SubBlockConfig } from '@/blocks/types' +import type { TriggerOutput } from '@/triggers/types' + +/** + * Dropdown options for the Vercel trigger type selector + */ +export const vercelTriggerOptions = [ + { label: 'Deployment Created', id: 'vercel_deployment_created' }, + { label: 'Deployment Ready', id: 'vercel_deployment_ready' }, + { label: 'Deployment Error', id: 'vercel_deployment_error' }, + { label: 'Deployment Canceled', id: 'vercel_deployment_canceled' }, + { label: 'Project Created', id: 'vercel_project_created' }, + { label: 'Project Removed', id: 'vercel_project_removed' }, + { label: 'Domain Created', id: 'vercel_domain_created' }, + { label: 'Generic Webhook (All Events)', id: 'vercel_webhook' }, +] + +/** + * Generates setup instructions for Vercel webhooks. + * Webhooks are automatically created via the Vercel API. + */ +export function vercelSetupInstructions(eventType: string): string { + const instructions = [ + 'Enter your Vercel Access Token above.', + 'You can create a token at Vercel Dashboard > Settings > Tokens.', + `Click "Save Configuration" to automatically create the webhook in Vercel for ${eventType} events.`, + 'The webhook will be automatically deleted when you remove this trigger.', + ] + + return instructions + .map( + (instruction, index) => + `
${index + 1}. ${instruction}
` + ) + .join('') +} + +/** + * Vercel-specific extra fields for triggers. + * Includes API token (required) and optional project/team filters. + */ +export function buildVercelExtraFields(triggerId: string): SubBlockConfig[] { + return [ + { + id: 'apiKey', + title: 'Access Token', + type: 'short-input' as const, + placeholder: 'Enter your Vercel access token', + description: 'Required to create the webhook in Vercel.', + password: true, + required: true, + mode: 'trigger' as const, + condition: { field: 'selectedTriggerId', value: triggerId }, + }, + { + id: 'teamId', + title: 'Team ID (Optional)', + type: 'short-input' as const, + placeholder: 'team_xxxxx (leave empty for personal account)', + description: 'Scope webhook to a specific team', + mode: 'trigger' as const, + condition: { field: 'selectedTriggerId', value: triggerId }, + }, + { + id: 'filterProjectIds', + title: 'Project IDs (Optional)', + type: 'short-input' as const, + placeholder: 'prj_xxx,prj_yyy (comma-separated)', + description: 'Limit webhook to specific projects', + mode: 'trigger' as const, + condition: { field: 'selectedTriggerId', value: triggerId }, + }, + ] +} + +/** + * Core outputs present in all Vercel webhook payloads + */ +const coreOutputs = { + type: { + type: 'string', + description: 'Event type (e.g., deployment.created)', + }, + id: { + type: 'string', + description: 'Unique webhook delivery ID', + }, + createdAt: { + type: 'number', + description: 'Event timestamp in milliseconds', + }, + region: { + type: 'string', + description: 'Region where the event occurred', + }, +} as const + +/** + * Deployment-specific output fields + */ +const deploymentOutputs = { + deployment: { + id: { type: 'string', description: 'Deployment ID' }, + url: { type: 'string', description: 'Deployment URL' }, + name: { type: 'string', description: 'Deployment name' }, + }, + project: { + id: { type: 'string', description: 'Project ID' }, + name: { type: 'string', description: 'Project name' }, + }, + team: { + id: { type: 'string', description: 'Team ID' }, + }, + user: { + id: { type: 'string', description: 'User ID' }, + }, + target: { + type: 'string', + description: 'Deployment target (production, preview)', + }, + plan: { + type: 'string', + description: 'Account plan type', + }, +} as const + +/** + * Project-specific output fields + */ +const projectOutputs = { + project: { + id: { type: 'string', description: 'Project ID' }, + name: { type: 'string', description: 'Project name' }, + }, + team: { + id: { type: 'string', description: 'Team ID' }, + }, + user: { + id: { type: 'string', description: 'User ID' }, + }, +} as const + +/** + * Domain-specific output fields + */ +const domainOutputs = { + domain: { + name: { type: 'string', description: 'Domain name' }, + }, + project: { + id: { type: 'string', description: 'Project ID' }, + }, + team: { + id: { type: 'string', description: 'Team ID' }, + }, + user: { + id: { type: 'string', description: 'User ID' }, + }, +} as const + +/** + * Build outputs for deployment events + */ +export function buildDeploymentOutputs(): Record { + return { + ...coreOutputs, + ...deploymentOutputs, + } as Record +} + +/** + * Build outputs for project events + */ +export function buildProjectOutputs(): Record { + return { + ...coreOutputs, + ...projectOutputs, + } as Record +} + +/** + * Build outputs for domain events + */ +export function buildDomainOutputs(): Record { + return { + ...coreOutputs, + ...domainOutputs, + } as Record +} + +/** + * Build outputs for the generic webhook (all events) + */ +export function buildVercelOutputs(): Record { + return { + ...coreOutputs, + payload: { type: 'json', description: 'Full event payload' }, + deployment: { + id: { type: 'string', description: 'Deployment ID' }, + url: { type: 'string', description: 'Deployment URL' }, + name: { type: 'string', description: 'Deployment name' }, + }, + project: { + id: { type: 'string', description: 'Project ID' }, + name: { type: 'string', description: 'Project name' }, + }, + team: { + id: { type: 'string', description: 'Team ID' }, + }, + user: { + id: { type: 'string', description: 'User ID' }, + }, + target: { + type: 'string', + description: 'Deployment target (production, preview)', + }, + plan: { + type: 'string', + description: 'Account plan type', + }, + domain: { + name: { type: 'string', description: 'Domain name' }, + }, + } as Record +} diff --git a/apps/sim/triggers/vercel/webhook.ts b/apps/sim/triggers/vercel/webhook.ts new file mode 100644 index 00000000000..dbe7868ff59 --- /dev/null +++ b/apps/sim/triggers/vercel/webhook.ts @@ -0,0 +1,38 @@ +import { VercelIcon } from '@/components/icons' +import { buildTriggerSubBlocks } from '@/triggers' +import type { TriggerConfig } from '@/triggers/types' +import { + buildVercelExtraFields, + buildVercelOutputs, + vercelSetupInstructions, + vercelTriggerOptions, +} from '@/triggers/vercel/utils' + +/** + * Generic Vercel Webhook Trigger + * Captures all Vercel webhook events + */ +export const vercelWebhookTrigger: TriggerConfig = { + id: 'vercel_webhook', + name: 'Vercel Webhook (All Events)', + provider: 'vercel', + description: 'Trigger workflow on any Vercel webhook event', + version: '1.0.0', + icon: VercelIcon, + + subBlocks: buildTriggerSubBlocks({ + triggerId: 'vercel_webhook', + triggerOptions: vercelTriggerOptions, + setupInstructions: vercelSetupInstructions('All Events'), + extraFields: buildVercelExtraFields('vercel_webhook'), + }), + + outputs: buildVercelOutputs(), + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} From 17657c04f1d714e72de75ca5765b64b74d07a854 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 6 Apr 2026 11:10:08 -0700 Subject: [PATCH 2/5] fix(triggers): add Vercel webhook signature verification and expand generic events --- apps/sim/lib/webhooks/providers/vercel.ts | 29 ++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/apps/sim/lib/webhooks/providers/vercel.ts b/apps/sim/lib/webhooks/providers/vercel.ts index db6f298ba05..ca44437b311 100644 --- a/apps/sim/lib/webhooks/providers/vercel.ts +++ b/apps/sim/lib/webhooks/providers/vercel.ts @@ -1,4 +1,6 @@ +import crypto from 'crypto' import { createLogger } from '@sim/logger' +import { safeCompare } from '@/lib/core/security/encryption' import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/providers/subscription-utils' import type { DeleteSubscriptionContext, @@ -8,10 +10,21 @@ import type { SubscriptionResult, WebhookProviderHandler, } from '@/lib/webhooks/providers/types' +import { createHmacVerifier } from '@/lib/webhooks/providers/utils' const logger = createLogger('WebhookProvider:Vercel') export const vercelHandler: WebhookProviderHandler = { + verifyAuth: createHmacVerifier({ + configKey: 'webhookSecret', + headerName: 'x-vercel-signature', + validateFn: (secret, signature, body) => { + const hash = crypto.createHmac('sha1', secret).update(body, 'utf8').digest('hex') + return safeCompare(hash, signature) + }, + providerLabel: 'Vercel', + }), + async createSubscription(ctx: SubscriptionContext): Promise { const { webhook, requestId } = ctx try { @@ -49,16 +62,25 @@ export const vercelHandler: WebhookProviderHandler = { webhookId: webhook.id, }) + /** + * Vercel requires an explicit events list — there is no "subscribe to all" option. + * For the generic webhook trigger, we subscribe to the most commonly useful events. + * Full list: https://vercel.com/docs/webhooks/webhooks-api#event-types + */ const requestBody: Record = { url: notificationUrl, events: events || [ 'deployment.created', 'deployment.ready', + 'deployment.succeeded', 'deployment.error', 'deployment.canceled', + 'deployment.promoted', 'project.created', 'project.removed', 'domain.created', + 'edge-config.created', + 'edge-config.deleted', ], } @@ -110,7 +132,12 @@ export const vercelHandler: WebhookProviderHandler = { { vercelWebhookId: responseBody.id } ) - return { providerConfigUpdates: { externalId: responseBody.id } } + return { + providerConfigUpdates: { + externalId: responseBody.id, + webhookSecret: (responseBody.secret as string) || '', + }, + } } catch (error: unknown) { const err = error as Error logger.error( From 4edc6b41428f025b991e66b11fb4d540e34400fc Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 6 Apr 2026 11:26:04 -0700 Subject: [PATCH 3/5] fix(triggers): validate Vercel webhook ID before storing to prevent orphaned webhooks --- apps/sim/lib/webhooks/providers/vercel.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/sim/lib/webhooks/providers/vercel.ts b/apps/sim/lib/webhooks/providers/vercel.ts index ca44437b311..a4730765a1f 100644 --- a/apps/sim/lib/webhooks/providers/vercel.ts +++ b/apps/sim/lib/webhooks/providers/vercel.ts @@ -127,14 +127,19 @@ export const vercelHandler: WebhookProviderHandler = { throw new Error(userFriendlyMessage) } + const externalId = responseBody.id as string | undefined + if (!externalId) { + throw new Error('Vercel webhook creation succeeded but no webhook ID was returned') + } + logger.info( `[${requestId}] Successfully created webhook in Vercel for webhook ${webhook.id}.`, - { vercelWebhookId: responseBody.id } + { vercelWebhookId: externalId } ) return { providerConfigUpdates: { - externalId: responseBody.id, + externalId, webhookSecret: (responseBody.secret as string) || '', }, } From c637317043165661d4ebe5f6b2d58968d7bd8bfb Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 6 Apr 2026 12:00:07 -0700 Subject: [PATCH 4/5] fix(triggers): add triggerId validation warning and JSON parse fallback for Vercel webhooks Co-Authored-By: Claude Opus 4.6 --- apps/sim/lib/webhooks/providers/vercel.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/apps/sim/lib/webhooks/providers/vercel.ts b/apps/sim/lib/webhooks/providers/vercel.ts index a4730765a1f..7f8a0f5eccc 100644 --- a/apps/sim/lib/webhooks/providers/vercel.ts +++ b/apps/sim/lib/webhooks/providers/vercel.ts @@ -51,6 +51,13 @@ export const vercelHandler: WebhookProviderHandler = { vercel_webhook: undefined, } + if (triggerId && !(triggerId in eventTypeMap)) { + logger.warn( + `[${requestId}] Unknown triggerId for Vercel: ${triggerId}, defaulting to all events`, + { triggerId, webhookId: webhook.id } + ) + } + const events = eventTypeMap[triggerId ?? ''] const notificationUrl = getNotificationUrl(webhook) @@ -107,7 +114,10 @@ export const vercelHandler: WebhookProviderHandler = { body: JSON.stringify(requestBody), }) - const responseBody = (await vercelResponse.json()) as Record + const responseBody = (await vercelResponse.json().catch(() => ({}))) as Record< + string, + unknown + > if (!vercelResponse.ok) { const errorObj = responseBody.error as Record | undefined From ee040aaf605c6ff13420337366f201aa9c6ac136 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 6 Apr 2026 12:20:10 -0700 Subject: [PATCH 5/5] fix(triggers): add paramVisibility user-only to Vercel apiKey subblock Co-Authored-By: Claude Opus 4.6 --- apps/sim/triggers/vercel/utils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/sim/triggers/vercel/utils.ts b/apps/sim/triggers/vercel/utils.ts index 44845763933..e0f016dc9f5 100644 --- a/apps/sim/triggers/vercel/utils.ts +++ b/apps/sim/triggers/vercel/utils.ts @@ -49,6 +49,7 @@ export function buildVercelExtraFields(triggerId: string): SubBlockConfig[] { description: 'Required to create the webhook in Vercel.', password: true, required: true, + paramVisibility: 'user-only', mode: 'trigger' as const, condition: { field: 'selectedTriggerId', value: triggerId }, },