mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-28 03:00:29 -04:00
feat(triggers): add Vercel webhook triggers with automatic registration (#3988)
* feat(triggers): add Vercel webhook triggers with automatic registration * fix(triggers): add Vercel webhook signature verification and expand generic events * fix(triggers): validate Vercel webhook ID before storing to prevent orphaned webhooks * fix(triggers): add triggerId validation warning and JSON parse fallback for Vercel webhooks Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(triggers): add paramVisibility user-only to Vercel apiKey subblock Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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: [
|
||||
|
||||
@@ -35,6 +35,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'
|
||||
|
||||
@@ -74,6 +75,7 @@ const PROVIDER_HANDLERS: Record<string, WebhookProviderHandler> = {
|
||||
twilio: twilioHandler,
|
||||
twilio_voice: twilioVoiceHandler,
|
||||
typeform: typeformHandler,
|
||||
vercel: vercelHandler,
|
||||
webflow: webflowHandler,
|
||||
whatsapp: whatsappHandler,
|
||||
}
|
||||
|
||||
226
apps/sim/lib/webhooks/providers/vercel.ts
Normal file
226
apps/sim/lib/webhooks/providers/vercel.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
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,
|
||||
FormatInputContext,
|
||||
FormatInputResult,
|
||||
SubscriptionContext,
|
||||
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<SubscriptionResult | undefined> {
|
||||
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<string, string[] | undefined> = {
|
||||
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,
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
logger.info(`[${requestId}] Creating Vercel webhook`, {
|
||||
triggerId,
|
||||
events,
|
||||
hasTeamId: !!teamId,
|
||||
hasProjectIds: !!filterProjectIds,
|
||||
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<string, unknown> = {
|
||||
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',
|
||||
],
|
||||
}
|
||||
|
||||
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().catch(() => ({}))) as Record<
|
||||
string,
|
||||
unknown
|
||||
>
|
||||
|
||||
if (!vercelResponse.ok) {
|
||||
const errorObj = responseBody.error as Record<string, unknown> | 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)
|
||||
}
|
||||
|
||||
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: externalId }
|
||||
)
|
||||
|
||||
return {
|
||||
providerConfigUpdates: {
|
||||
externalId,
|
||||
webhookSecret: (responseBody.secret as string) || '',
|
||||
},
|
||||
}
|
||||
} 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<void> {
|
||||
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<FormatInputResult> {
|
||||
const body = ctx.body as Record<string, unknown>
|
||||
const payload = (body.payload || {}) as Record<string, unknown>
|
||||
|
||||
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,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -226,6 +226,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,
|
||||
@@ -395,6 +405,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,
|
||||
|
||||
37
apps/sim/triggers/vercel/deployment_canceled.ts
Normal file
37
apps/sim/triggers/vercel/deployment_canceled.ts
Normal file
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
40
apps/sim/triggers/vercel/deployment_created.ts
Normal file
40
apps/sim/triggers/vercel/deployment_created.ts
Normal file
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
37
apps/sim/triggers/vercel/deployment_error.ts
Normal file
37
apps/sim/triggers/vercel/deployment_error.ts
Normal file
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
37
apps/sim/triggers/vercel/deployment_ready.ts
Normal file
37
apps/sim/triggers/vercel/deployment_ready.ts
Normal file
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
37
apps/sim/triggers/vercel/domain_created.ts
Normal file
37
apps/sim/triggers/vercel/domain_created.ts
Normal file
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
13
apps/sim/triggers/vercel/index.ts
Normal file
13
apps/sim/triggers/vercel/index.ts
Normal file
@@ -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'
|
||||
37
apps/sim/triggers/vercel/project_created.ts
Normal file
37
apps/sim/triggers/vercel/project_created.ts
Normal file
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
37
apps/sim/triggers/vercel/project_removed.ts
Normal file
37
apps/sim/triggers/vercel/project_removed.ts
Normal file
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
226
apps/sim/triggers/vercel/utils.ts
Normal file
226
apps/sim/triggers/vercel/utils.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
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 <strong>Vercel Dashboard > Settings > Tokens</strong>.',
|
||||
`Click <strong>"Save Configuration"</strong> to automatically create the webhook in Vercel for <strong>${eventType}</strong> events.`,
|
||||
'The webhook will be automatically deleted when you remove this trigger.',
|
||||
]
|
||||
|
||||
return instructions
|
||||
.map(
|
||||
(instruction, index) =>
|
||||
`<div class="mb-3"><strong>${index + 1}.</strong> ${instruction}</div>`
|
||||
)
|
||||
.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,
|
||||
paramVisibility: 'user-only',
|
||||
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<string, TriggerOutput> {
|
||||
return {
|
||||
...coreOutputs,
|
||||
...deploymentOutputs,
|
||||
} as Record<string, TriggerOutput>
|
||||
}
|
||||
|
||||
/**
|
||||
* Build outputs for project events
|
||||
*/
|
||||
export function buildProjectOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
...coreOutputs,
|
||||
...projectOutputs,
|
||||
} as Record<string, TriggerOutput>
|
||||
}
|
||||
|
||||
/**
|
||||
* Build outputs for domain events
|
||||
*/
|
||||
export function buildDomainOutputs(): Record<string, TriggerOutput> {
|
||||
return {
|
||||
...coreOutputs,
|
||||
...domainOutputs,
|
||||
} as Record<string, TriggerOutput>
|
||||
}
|
||||
|
||||
/**
|
||||
* Build outputs for the generic webhook (all events)
|
||||
*/
|
||||
export function buildVercelOutputs(): Record<string, TriggerOutput> {
|
||||
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<string, TriggerOutput>
|
||||
}
|
||||
38
apps/sim/triggers/vercel/webhook.ts
Normal file
38
apps/sim/triggers/vercel/webhook.ts
Normal file
@@ -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',
|
||||
},
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user