feat(triggers): add Zoom webhook triggers (#3992)

* feat(triggers): add Zoom webhook triggers with challenge-response and signature verification

Add 6 Zoom webhook triggers (meeting started/ended, participant joined/left, recording completed, generic webhook) with full Zoom protocol support including endpoint.url_validation challenge-response handling and x-zm-signature HMAC-SHA256 verification.

* fix(triggers): use webhook.isActive instead of non-existent deletedAt column

* fix(triggers): address PR review feedback for Zoom webhooks

- Add 30s timestamp freshness check to prevent replay attacks
- Return null from handleChallenge when no secret token found instead of responding with empty-key HMAC
- Remove all `as any` casts from output builder functions

* lint

* fix(triggers): harden Zoom webhook security per PR review

- verifyAuth now fails closed (401) when secretToken is missing
- handleChallenge DB query filters by provider='zoom' to avoid cross-provider leaks
- handleChallenge verifies x-zm-signature before responding to prevent HMAC oracle

* fix(triggers): rename type to meeting_type to avoid TriggerOutput type collision

* fix(triggers): make challenge signature verification mandatory, not optional

* fix(triggers): fail closed on unknown trigger IDs and update Zoom landing page data

- isZoomEventMatch now returns false for unrecognized trigger IDs
- Update integrations.json with 6 Zoom triggers

* fix(triggers): add missing id fields to Zoom trigger entries in integrations.json

* fix(triggers): increase Zoom timestamp tolerance to 300s per Zoom docs
This commit is contained in:
Waleed
2026-04-06 13:39:43 -07:00
committed by GitHub
parent cd5cee3033
commit 18a7868bb3
13 changed files with 782 additions and 6 deletions

View File

@@ -5528,6 +5528,11 @@
"name": "HubSpot Contact Deleted",
"description": "Trigger workflow when a contact is deleted in HubSpot"
},
{
"id": "hubspot_contact_merged",
"name": "HubSpot Contact Merged",
"description": "Trigger workflow when contacts are merged in HubSpot"
},
{
"id": "hubspot_contact_privacy_deleted",
"name": "HubSpot Contact Privacy Deleted",
@@ -5538,6 +5543,11 @@
"name": "HubSpot Contact Property Changed",
"description": "Trigger workflow when any property of a contact is updated in HubSpot"
},
{
"id": "hubspot_contact_restored",
"name": "HubSpot Contact Restored",
"description": "Trigger workflow when a deleted contact is restored in HubSpot"
},
{
"id": "hubspot_company_created",
"name": "HubSpot Company Created",
@@ -5548,11 +5558,21 @@
"name": "HubSpot Company Deleted",
"description": "Trigger workflow when a company is deleted in HubSpot"
},
{
"id": "hubspot_company_merged",
"name": "HubSpot Company Merged",
"description": "Trigger workflow when companies are merged in HubSpot"
},
{
"id": "hubspot_company_property_changed",
"name": "HubSpot Company Property Changed",
"description": "Trigger workflow when any property of a company is updated in HubSpot"
},
{
"id": "hubspot_company_restored",
"name": "HubSpot Company Restored",
"description": "Trigger workflow when a deleted company is restored in HubSpot"
},
{
"id": "hubspot_conversation_creation",
"name": "HubSpot Conversation Creation",
@@ -5588,11 +5608,21 @@
"name": "HubSpot Deal Deleted",
"description": "Trigger workflow when a deal is deleted in HubSpot"
},
{
"id": "hubspot_deal_merged",
"name": "HubSpot Deal Merged",
"description": "Trigger workflow when deals are merged in HubSpot"
},
{
"id": "hubspot_deal_property_changed",
"name": "HubSpot Deal Property Changed",
"description": "Trigger workflow when any property of a deal is updated in HubSpot"
},
{
"id": "hubspot_deal_restored",
"name": "HubSpot Deal Restored",
"description": "Trigger workflow when a deleted deal is restored in HubSpot"
},
{
"id": "hubspot_ticket_created",
"name": "HubSpot Ticket Created",
@@ -5603,13 +5633,28 @@
"name": "HubSpot Ticket Deleted",
"description": "Trigger workflow when a ticket is deleted in HubSpot"
},
{
"id": "hubspot_ticket_merged",
"name": "HubSpot Ticket Merged",
"description": "Trigger workflow when tickets are merged in HubSpot"
},
{
"id": "hubspot_ticket_property_changed",
"name": "HubSpot Ticket Property Changed",
"description": "Trigger workflow when any property of a ticket is updated in HubSpot"
},
{
"id": "hubspot_ticket_restored",
"name": "HubSpot Ticket Restored",
"description": "Trigger workflow when a deleted ticket is restored in HubSpot"
},
{
"id": "hubspot_webhook",
"name": "HubSpot Webhook (All Events)",
"description": "Trigger workflow on any HubSpot webhook event"
}
],
"triggerCount": 18,
"triggerCount": 27,
"authType": "oauth",
"category": "tools",
"integrationType": "crm",
@@ -10263,8 +10308,39 @@
}
],
"operationCount": 35,
"triggers": [],
"triggerCount": 0,
"triggers": [
{
"id": "salesforce_record_created",
"name": "Salesforce Record Created",
"description": "Trigger workflow when a Salesforce record is created"
},
{
"id": "salesforce_record_updated",
"name": "Salesforce Record Updated",
"description": "Trigger workflow when a Salesforce record is updated"
},
{
"id": "salesforce_record_deleted",
"name": "Salesforce Record Deleted",
"description": "Trigger workflow when a Salesforce record is deleted"
},
{
"id": "salesforce_opportunity_stage_changed",
"name": "Salesforce Opportunity Stage Changed",
"description": "Trigger workflow when an opportunity stage changes"
},
{
"id": "salesforce_case_status_changed",
"name": "Salesforce Case Status Changed",
"description": "Trigger workflow when a case status changes"
},
{
"id": "salesforce_webhook",
"name": "Salesforce Webhook (All Events)",
"description": "Trigger workflow on any Salesforce webhook event"
}
],
"triggerCount": 6,
"authType": "oauth",
"category": "tools",
"integrationType": "crm",
@@ -12856,8 +12932,39 @@
}
],
"operationCount": 10,
"triggers": [],
"triggerCount": 0,
"triggers": [
{
"id": "zoom_meeting_started",
"name": "Meeting Started",
"description": "Triggered when a Zoom meeting starts"
},
{
"id": "zoom_meeting_ended",
"name": "Meeting Ended",
"description": "Triggered when a Zoom meeting ends"
},
{
"id": "zoom_participant_joined",
"name": "Participant Joined",
"description": "Triggered when a participant joins a Zoom meeting"
},
{
"id": "zoom_participant_left",
"name": "Participant Left",
"description": "Triggered when a participant leaves a Zoom meeting"
},
{
"id": "zoom_recording_completed",
"name": "Recording Completed",
"description": "Triggered when a Zoom cloud recording is completed"
},
{
"id": "zoom_webhook",
"name": "Generic Webhook",
"description": "Triggered on any Zoom webhook event"
}
],
"triggerCount": 6,
"authType": "oauth",
"category": "tools",
"integrationType": "communication",

View File

@@ -117,7 +117,7 @@ export async function parseWebhookBody(
}
/** Providers that implement challenge/verification handling, checked before webhook lookup. */
const CHALLENGE_PROVIDERS = ['slack', 'microsoft-teams', 'whatsapp'] as const
const CHALLENGE_PROVIDERS = ['slack', 'microsoft-teams', 'whatsapp', 'zoom'] as const
export async function handleProviderChallenges(
body: unknown,

View File

@@ -38,6 +38,7 @@ 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'
import { zoomHandler } from '@/lib/webhooks/providers/zoom'
const logger = createLogger('WebhookProviderRegistry')
@@ -78,6 +79,7 @@ const PROVIDER_HANDLERS: Record<string, WebhookProviderHandler> = {
vercel: vercelHandler,
webflow: webflowHandler,
whatsapp: whatsappHandler,
zoom: zoomHandler,
}
/**

View File

@@ -0,0 +1,166 @@
import crypto from 'crypto'
import { db, webhook } from '@sim/db'
import { createLogger } from '@sim/logger'
import { and, eq } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { NextResponse } from 'next/server'
import { safeCompare } from '@/lib/core/security/encryption'
import type {
AuthContext,
EventMatchContext,
WebhookProviderHandler,
} from '@/lib/webhooks/providers/types'
const logger = createLogger('WebhookProvider:Zoom')
/**
* Validate Zoom webhook signature using HMAC-SHA256.
* Zoom sends `x-zm-signature` as `v0=<hex>` and `x-zm-request-timestamp`.
* The message to hash is `v0:{timestamp}:{rawBody}`.
*/
function validateZoomSignature(
secretToken: string,
signature: string,
timestamp: string,
body: string
): boolean {
try {
if (!secretToken || !signature || !timestamp || !body) {
return false
}
const nowSeconds = Math.floor(Date.now() / 1000)
const requestSeconds = Number.parseInt(timestamp, 10)
if (Number.isNaN(requestSeconds) || Math.abs(nowSeconds - requestSeconds) > 300) {
return false
}
const message = `v0:${timestamp}:${body}`
const computedHash = crypto.createHmac('sha256', secretToken).update(message).digest('hex')
const expectedSignature = `v0=${computedHash}`
return safeCompare(expectedSignature, signature)
} catch (err) {
logger.error('Zoom signature validation error', err)
return false
}
}
export const zoomHandler: WebhookProviderHandler = {
verifyAuth({ request, rawBody, requestId, providerConfig }: AuthContext) {
const secretToken = providerConfig.secretToken as string | undefined
if (!secretToken) {
logger.warn(
`[${requestId}] Zoom webhook missing secretToken in providerConfig — rejecting request`
)
return new NextResponse('Unauthorized - Zoom secret token not configured', { status: 401 })
}
const signature = request.headers.get('x-zm-signature')
const timestamp = request.headers.get('x-zm-request-timestamp')
if (!signature || !timestamp) {
logger.warn(`[${requestId}] Zoom webhook missing signature or timestamp header`)
return new NextResponse('Unauthorized - Missing Zoom signature', { status: 401 })
}
if (!validateZoomSignature(secretToken, signature, timestamp, rawBody)) {
logger.warn(`[${requestId}] Zoom webhook signature verification failed`)
return new NextResponse('Unauthorized - Invalid Zoom signature', { status: 401 })
}
return null
},
async matchEvent({ webhook: wh, workflow, body, requestId, providerConfig }: EventMatchContext) {
const triggerId = providerConfig.triggerId as string | undefined
const obj = body as Record<string, unknown>
const event = obj.event as string | undefined
if (triggerId) {
const { isZoomEventMatch } = await import('@/triggers/zoom/utils')
if (!isZoomEventMatch(triggerId, event || '')) {
logger.debug(
`[${requestId}] Zoom event mismatch for trigger ${triggerId}. Event: ${event}. Skipping execution.`,
{
webhookId: wh.id,
workflowId: workflow.id,
triggerId,
receivedEvent: event,
}
)
return false
}
}
return true
},
/**
* Handle Zoom endpoint URL validation challenges.
* Zoom sends an `endpoint.url_validation` event with a `plainToken` that must
* be hashed with the app's secret token and returned alongside the original token.
*/
async handleChallenge(body: unknown, request: NextRequest, requestId: string, path: string) {
const obj = body as Record<string, unknown> | null
if (obj?.event !== 'endpoint.url_validation') {
return null
}
const payload = obj.payload as Record<string, unknown> | undefined
const plainToken = payload?.plainToken as string | undefined
if (!plainToken) {
return null
}
logger.info(`[${requestId}] Zoom URL validation request received for path: ${path}`)
// Look up the webhook record to get the secret token from providerConfig
let secretToken = ''
try {
const webhooks = await db
.select()
.from(webhook)
.where(
and(eq(webhook.path, path), eq(webhook.provider, 'zoom'), eq(webhook.isActive, true))
)
if (webhooks.length > 0) {
const config = webhooks[0].providerConfig as Record<string, unknown> | null
secretToken = (config?.secretToken as string) || ''
}
} catch (err) {
logger.warn(`[${requestId}] Failed to look up webhook secret for Zoom validation`, err)
return null
}
if (!secretToken) {
logger.warn(
`[${requestId}] No secret token configured for Zoom URL validation on path: ${path}`
)
return null
}
// Verify the challenge request's signature to prevent HMAC oracle attacks
const signature = request.headers.get('x-zm-signature')
const timestamp = request.headers.get('x-zm-request-timestamp')
if (!signature || !timestamp) {
logger.warn(`[${requestId}] Zoom challenge request missing signature headers — rejecting`)
return null
}
const rawBody = JSON.stringify(body)
if (!validateZoomSignature(secretToken, signature, timestamp, rawBody)) {
logger.warn(`[${requestId}] Zoom challenge request failed signature verification`)
return null
}
const hashForValidate = crypto
.createHmac('sha256', secretToken)
.update(plainToken)
.digest('hex')
return NextResponse.json({
plainToken,
encryptedToken: hashForValidate,
})
},
}

View File

@@ -243,6 +243,14 @@ import {
webflowFormSubmissionTrigger,
} from '@/triggers/webflow'
import { whatsappWebhookTrigger } from '@/triggers/whatsapp'
import {
zoomMeetingEndedTrigger,
zoomMeetingStartedTrigger,
zoomParticipantJoinedTrigger,
zoomParticipantLeftTrigger,
zoomRecordingCompletedTrigger,
zoomWebhookTrigger,
} from '@/triggers/zoom'
export const TRIGGER_REGISTRY: TriggerRegistry = {
slack_webhook: slackWebhookTrigger,
@@ -451,4 +459,10 @@ export const TRIGGER_REGISTRY: TriggerRegistry = {
intercom_contact_created: intercomContactCreatedTrigger,
intercom_user_created: intercomUserCreatedTrigger,
intercom_webhook: intercomWebhookTrigger,
zoom_meeting_started: zoomMeetingStartedTrigger,
zoom_meeting_ended: zoomMeetingEndedTrigger,
zoom_participant_joined: zoomParticipantJoinedTrigger,
zoom_participant_left: zoomParticipantLeftTrigger,
zoom_recording_completed: zoomRecordingCompletedTrigger,
zoom_webhook: zoomWebhookTrigger,
}

View File

@@ -0,0 +1,6 @@
export { zoomMeetingEndedTrigger } from './meeting_ended'
export { zoomMeetingStartedTrigger } from './meeting_started'
export { zoomParticipantJoinedTrigger } from './participant_joined'
export { zoomParticipantLeftTrigger } from './participant_left'
export { zoomRecordingCompletedTrigger } from './recording_completed'
export { zoomWebhookTrigger } from './webhook'

View File

@@ -0,0 +1,37 @@
import { ZoomIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import type { TriggerConfig } from '@/triggers/types'
import {
buildMeetingOutputs,
zoomSecretTokenField,
zoomSetupInstructions,
zoomTriggerOptions,
} from '@/triggers/zoom/utils'
/**
* Zoom Meeting Ended Trigger
*/
export const zoomMeetingEndedTrigger: TriggerConfig = {
id: 'zoom_meeting_ended',
name: 'Zoom Meeting Ended',
provider: 'zoom',
description: 'Trigger workflow when a Zoom meeting ends',
version: '1.0.0',
icon: ZoomIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'zoom_meeting_ended',
triggerOptions: zoomTriggerOptions,
setupInstructions: zoomSetupInstructions('meeting_ended'),
extraFields: [zoomSecretTokenField('zoom_meeting_ended')],
}),
outputs: buildMeetingOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
},
}

View File

@@ -0,0 +1,40 @@
import { ZoomIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import type { TriggerConfig } from '@/triggers/types'
import {
buildMeetingOutputs,
zoomSecretTokenField,
zoomSetupInstructions,
zoomTriggerOptions,
} from '@/triggers/zoom/utils'
/**
* Zoom Meeting Started Trigger
*
* Primary trigger - includes the dropdown for selecting trigger type.
*/
export const zoomMeetingStartedTrigger: TriggerConfig = {
id: 'zoom_meeting_started',
name: 'Zoom Meeting Started',
provider: 'zoom',
description: 'Trigger workflow when a Zoom meeting starts',
version: '1.0.0',
icon: ZoomIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'zoom_meeting_started',
triggerOptions: zoomTriggerOptions,
includeDropdown: true,
setupInstructions: zoomSetupInstructions('meeting_started'),
extraFields: [zoomSecretTokenField('zoom_meeting_started')],
}),
outputs: buildMeetingOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
},
}

View File

@@ -0,0 +1,37 @@
import { ZoomIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import type { TriggerConfig } from '@/triggers/types'
import {
buildParticipantOutputs,
zoomSecretTokenField,
zoomSetupInstructions,
zoomTriggerOptions,
} from '@/triggers/zoom/utils'
/**
* Zoom Participant Joined Trigger
*/
export const zoomParticipantJoinedTrigger: TriggerConfig = {
id: 'zoom_participant_joined',
name: 'Zoom Participant Joined',
provider: 'zoom',
description: 'Trigger workflow when a participant joins a Zoom meeting',
version: '1.0.0',
icon: ZoomIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'zoom_participant_joined',
triggerOptions: zoomTriggerOptions,
setupInstructions: zoomSetupInstructions('participant_joined'),
extraFields: [zoomSecretTokenField('zoom_participant_joined')],
}),
outputs: buildParticipantOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
},
}

View File

@@ -0,0 +1,37 @@
import { ZoomIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import type { TriggerConfig } from '@/triggers/types'
import {
buildParticipantOutputs,
zoomSecretTokenField,
zoomSetupInstructions,
zoomTriggerOptions,
} from '@/triggers/zoom/utils'
/**
* Zoom Participant Left Trigger
*/
export const zoomParticipantLeftTrigger: TriggerConfig = {
id: 'zoom_participant_left',
name: 'Zoom Participant Left',
provider: 'zoom',
description: 'Trigger workflow when a participant leaves a Zoom meeting',
version: '1.0.0',
icon: ZoomIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'zoom_participant_left',
triggerOptions: zoomTriggerOptions,
setupInstructions: zoomSetupInstructions('participant_left'),
extraFields: [zoomSecretTokenField('zoom_participant_left')],
}),
outputs: buildParticipantOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
},
}

View File

@@ -0,0 +1,37 @@
import { ZoomIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import type { TriggerConfig } from '@/triggers/types'
import {
buildRecordingOutputs,
zoomSecretTokenField,
zoomSetupInstructions,
zoomTriggerOptions,
} from '@/triggers/zoom/utils'
/**
* Zoom Recording Completed Trigger
*/
export const zoomRecordingCompletedTrigger: TriggerConfig = {
id: 'zoom_recording_completed',
name: 'Zoom Recording Completed',
provider: 'zoom',
description: 'Trigger workflow when a Zoom cloud recording is completed',
version: '1.0.0',
icon: ZoomIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'zoom_recording_completed',
triggerOptions: zoomTriggerOptions,
setupInstructions: zoomSetupInstructions('recording_completed'),
extraFields: [zoomSecretTokenField('zoom_recording_completed')],
}),
outputs: buildRecordingOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
},
}

View File

@@ -0,0 +1,256 @@
import type { SubBlockConfig } from '@/blocks/types'
import type { TriggerOutput } from '@/triggers/types'
/**
* Maps trigger IDs to the Zoom webhook event names they should match.
*/
const ZOOM_TRIGGER_EVENT_MAP: Record<string, string[]> = {
zoom_meeting_started: ['meeting.started'],
zoom_meeting_ended: ['meeting.ended'],
zoom_participant_joined: ['meeting.participant_joined'],
zoom_participant_left: ['meeting.participant_left'],
zoom_recording_completed: ['recording.completed'],
}
/**
* Checks whether a Zoom webhook payload matches the configured trigger type.
* Returns true for the generic `zoom_webhook` trigger (accepts all events).
*/
export function isZoomEventMatch(triggerId: string, event: string): boolean {
if (triggerId === 'zoom_webhook') {
return true
}
const allowedEvents = ZOOM_TRIGGER_EVENT_MAP[triggerId]
if (!allowedEvents) {
return false
}
return allowedEvents.includes(event)
}
/**
* Dropdown options for the Zoom trigger type selector.
*/
export const zoomTriggerOptions = [
{ label: 'Meeting Started', id: 'zoom_meeting_started' },
{ label: 'Meeting Ended', id: 'zoom_meeting_ended' },
{ label: 'Participant Joined', id: 'zoom_participant_joined' },
{ label: 'Participant Left', id: 'zoom_participant_left' },
{ label: 'Recording Completed', id: 'zoom_recording_completed' },
{ label: 'Generic Webhook (All Events)', id: 'zoom_webhook' },
]
type ZoomEventType =
| 'meeting_started'
| 'meeting_ended'
| 'participant_joined'
| 'participant_left'
| 'recording_completed'
| 'generic'
/**
* Generates setup instructions HTML for Zoom triggers.
*/
export function zoomSetupInstructions(eventType: ZoomEventType): string {
const eventNames: Record<ZoomEventType, string> = {
meeting_started: 'meeting.started',
meeting_ended: 'meeting.ended',
participant_joined: 'meeting.participant_joined',
participant_left: 'meeting.participant_left',
recording_completed: 'recording.completed',
generic: 'your desired event type(s)',
}
const instructions = [
'Copy the <strong>Webhook URL</strong> above.',
'Go to the <a href="https://marketplace.zoom.us/" target="_blank" rel="noopener noreferrer">Zoom Marketplace</a> and open your app (or create a new Webhook Only app).',
"Copy the <strong>Secret Token</strong> from your Zoom app's <strong>Features</strong> page and paste it in the <strong>Secret Token</strong> field above.",
'Click <strong>"Save Configuration"</strong> above to activate the trigger.',
'Navigate to <strong>Features > Event Subscriptions</strong> and click <strong>Add Event Subscription</strong>.',
'Enter a subscription name and paste the webhook URL into the <strong>Event notification endpoint URL</strong> field.',
'Click <strong>Validate</strong> to verify the endpoint.',
`Click <strong>Add Events</strong> and select the <strong>${eventNames[eventType]}</strong> event type.`,
'Save the event subscription in Zoom.',
]
return instructions
.map(
(instruction, index) =>
`<div class="mb-3"><strong>${index + 1}.</strong> ${instruction}</div>`
)
.join('')
}
/**
* Creates the secret token field subBlock for a Zoom trigger.
*/
export function zoomSecretTokenField(triggerId: string): SubBlockConfig {
return {
id: 'secretToken',
title: 'Secret Token',
type: 'short-input',
placeholder: 'Enter your Zoom app Secret Token',
description:
"Found in your Zoom app's Features page. Required for endpoint validation and webhook signature verification.",
password: true,
required: true,
mode: 'trigger',
condition: {
field: 'selectedTriggerId',
value: triggerId,
},
}
}
/**
* Builds outputs for meeting lifecycle events (started/ended).
*/
export function buildMeetingOutputs(): Record<string, TriggerOutput> {
return {
event: {
type: 'string',
description: 'The webhook event type (e.g., meeting.started)',
},
event_ts: {
type: 'number',
description: 'Unix timestamp in milliseconds when the event occurred',
},
payload: {
account_id: {
type: 'string',
description: 'Zoom account ID',
},
object: {
type: 'object',
description: 'Meeting details',
properties: {
id: { type: 'number', description: 'Meeting ID' },
uuid: { type: 'string', description: 'Meeting UUID' },
topic: { type: 'string', description: 'Meeting topic' },
meeting_type: {
type: 'number',
description: 'Meeting type (1=instant, 2=scheduled, etc.)',
},
host_id: { type: 'string', description: 'Host user ID' },
start_time: { type: 'string', description: 'Meeting start time (ISO 8601)' },
end_time: {
type: 'string',
description: 'Meeting end time (ISO 8601, present on meeting.ended)',
},
timezone: { type: 'string', description: 'Meeting timezone' },
duration: { type: 'number', description: 'Meeting duration in minutes' },
},
},
},
}
}
/**
* Builds outputs for participant events (joined/left).
*/
export function buildParticipantOutputs(): Record<string, TriggerOutput> {
return {
event: {
type: 'string',
description: 'The webhook event type (e.g., meeting.participant_joined)',
},
event_ts: {
type: 'number',
description: 'Unix timestamp in milliseconds when the event occurred',
},
payload: {
account_id: {
type: 'string',
description: 'Zoom account ID',
},
object: {
type: 'object',
description: 'Meeting details',
properties: {
id: { type: 'number', description: 'Meeting ID' },
uuid: { type: 'string', description: 'Meeting UUID' },
topic: { type: 'string', description: 'Meeting topic' },
host_id: { type: 'string', description: 'Host user ID' },
participant: {
type: 'object',
description: 'Participant details',
properties: {
id: { type: 'string', description: 'Participant user ID' },
user_id: { type: 'string', description: 'Participant user ID (16-digit)' },
user_name: { type: 'string', description: 'Participant display name' },
email: { type: 'string', description: 'Participant email' },
join_time: { type: 'string', description: 'Time participant joined (ISO 8601)' },
leave_time: {
type: 'string',
description: 'Time participant left (ISO 8601, present on participant_left)',
},
},
},
},
},
},
}
}
/**
* Builds outputs for recording completed events.
*/
export function buildRecordingOutputs(): Record<string, TriggerOutput> {
return {
event: {
type: 'string',
description: 'The webhook event type (recording.completed)',
},
event_ts: {
type: 'number',
description: 'Unix timestamp in milliseconds when the event occurred',
},
payload: {
account_id: {
type: 'string',
description: 'Zoom account ID',
},
object: {
type: 'object',
description: 'Recording details',
properties: {
id: { type: 'number', description: 'Meeting ID' },
uuid: { type: 'string', description: 'Meeting UUID' },
topic: { type: 'string', description: 'Meeting topic' },
host_id: { type: 'string', description: 'Host user ID' },
host_email: { type: 'string', description: 'Host email' },
start_time: { type: 'string', description: 'Recording start time (ISO 8601)' },
duration: { type: 'number', description: 'Recording duration in minutes' },
total_size: { type: 'number', description: 'Total recording size in bytes' },
recording_count: { type: 'number', description: 'Number of recording files' },
share_url: { type: 'string', description: 'URL to share the recording' },
recording_files: {
type: 'json',
description: 'Array of recording file objects with download URLs',
},
},
},
},
}
}
/**
* Builds outputs for generic webhook (any event type).
*/
export function buildGenericOutputs(): Record<string, TriggerOutput> {
return {
event: {
type: 'string',
description: 'The webhook event type (e.g., meeting.started, recording.completed)',
},
event_ts: {
type: 'number',
description: 'Unix timestamp in milliseconds when the event occurred',
},
payload: {
type: 'json',
description: 'Complete webhook payload (structure varies by event type)',
},
}
}

View File

@@ -0,0 +1,37 @@
import { ZoomIcon } from '@/components/icons'
import { buildTriggerSubBlocks } from '@/triggers'
import type { TriggerConfig } from '@/triggers/types'
import {
buildGenericOutputs,
zoomSecretTokenField,
zoomSetupInstructions,
zoomTriggerOptions,
} from '@/triggers/zoom/utils'
/**
* Generic Zoom webhook trigger that accepts any event type.
*/
export const zoomWebhookTrigger: TriggerConfig = {
id: 'zoom_webhook',
name: 'Zoom Webhook (All Events)',
provider: 'zoom',
description: 'Trigger workflow on any Zoom webhook event',
version: '1.0.0',
icon: ZoomIcon,
subBlocks: buildTriggerSubBlocks({
triggerId: 'zoom_webhook',
triggerOptions: zoomTriggerOptions,
setupInstructions: zoomSetupInstructions('generic'),
extraFields: [zoomSecretTokenField('zoom_webhook')],
}),
outputs: buildGenericOutputs(),
webhook: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
},
}