diff --git a/apps/sim/app/(landing)/integrations/data/integrations.json b/apps/sim/app/(landing)/integrations/data/integrations.json index 189e0a3b243..cfc7d0d365b 100644 --- a/apps/sim/app/(landing)/integrations/data/integrations.json +++ b/apps/sim/app/(landing)/integrations/data/integrations.json @@ -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", @@ -10217,8 +10262,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", @@ -12810,8 +12886,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", diff --git a/apps/sim/lib/webhooks/processor.ts b/apps/sim/lib/webhooks/processor.ts index 38f6cc81bbc..4288d0287d6 100644 --- a/apps/sim/lib/webhooks/processor.ts +++ b/apps/sim/lib/webhooks/processor.ts @@ -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, diff --git a/apps/sim/lib/webhooks/providers/registry.ts b/apps/sim/lib/webhooks/providers/registry.ts index 1bea27ebc15..153f2c350e8 100644 --- a/apps/sim/lib/webhooks/providers/registry.ts +++ b/apps/sim/lib/webhooks/providers/registry.ts @@ -35,6 +35,7 @@ import type { WebhookProviderHandler } from '@/lib/webhooks/providers/types' import { verifyTokenAuth } from '@/lib/webhooks/providers/utils' 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') @@ -72,6 +73,7 @@ const PROVIDER_HANDLERS: Record = { typeform: typeformHandler, webflow: webflowHandler, whatsapp: whatsappHandler, + zoom: zoomHandler, } /** diff --git a/apps/sim/lib/webhooks/providers/zoom.ts b/apps/sim/lib/webhooks/providers/zoom.ts new file mode 100644 index 00000000000..ed33b2bac2e --- /dev/null +++ b/apps/sim/lib/webhooks/providers/zoom.ts @@ -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=` 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 + 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 | null + if (obj?.event !== 'endpoint.url_validation') { + return null + } + + const payload = obj.payload as Record | 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 | 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, + }) + }, +} diff --git a/apps/sim/triggers/registry.ts b/apps/sim/triggers/registry.ts index eea378bfb99..79f8e64a641 100644 --- a/apps/sim/triggers/registry.ts +++ b/apps/sim/triggers/registry.ts @@ -212,6 +212,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, @@ -395,4 +403,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, } diff --git a/apps/sim/triggers/zoom/index.ts b/apps/sim/triggers/zoom/index.ts new file mode 100644 index 00000000000..b6aad68878b --- /dev/null +++ b/apps/sim/triggers/zoom/index.ts @@ -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' diff --git a/apps/sim/triggers/zoom/meeting_ended.ts b/apps/sim/triggers/zoom/meeting_ended.ts new file mode 100644 index 00000000000..6d11d590d22 --- /dev/null +++ b/apps/sim/triggers/zoom/meeting_ended.ts @@ -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', + }, + }, +} diff --git a/apps/sim/triggers/zoom/meeting_started.ts b/apps/sim/triggers/zoom/meeting_started.ts new file mode 100644 index 00000000000..b1e559f532b --- /dev/null +++ b/apps/sim/triggers/zoom/meeting_started.ts @@ -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', + }, + }, +} diff --git a/apps/sim/triggers/zoom/participant_joined.ts b/apps/sim/triggers/zoom/participant_joined.ts new file mode 100644 index 00000000000..573adbbe113 --- /dev/null +++ b/apps/sim/triggers/zoom/participant_joined.ts @@ -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', + }, + }, +} diff --git a/apps/sim/triggers/zoom/participant_left.ts b/apps/sim/triggers/zoom/participant_left.ts new file mode 100644 index 00000000000..d88e325448a --- /dev/null +++ b/apps/sim/triggers/zoom/participant_left.ts @@ -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', + }, + }, +} diff --git a/apps/sim/triggers/zoom/recording_completed.ts b/apps/sim/triggers/zoom/recording_completed.ts new file mode 100644 index 00000000000..bbae2eb6340 --- /dev/null +++ b/apps/sim/triggers/zoom/recording_completed.ts @@ -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', + }, + }, +} diff --git a/apps/sim/triggers/zoom/utils.ts b/apps/sim/triggers/zoom/utils.ts new file mode 100644 index 00000000000..20e316f0400 --- /dev/null +++ b/apps/sim/triggers/zoom/utils.ts @@ -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 = { + 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 = { + 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 Webhook URL above.', + 'Go to the Zoom Marketplace and open your app (or create a new Webhook Only app).', + "Copy the Secret Token from your Zoom app's Features page and paste it in the Secret Token field above.", + 'Click "Save Configuration" above to activate the trigger.', + 'Navigate to Features > Event Subscriptions and click Add Event Subscription.', + 'Enter a subscription name and paste the webhook URL into the Event notification endpoint URL field.', + 'Click Validate to verify the endpoint.', + `Click Add Events and select the ${eventNames[eventType]} event type.`, + 'Save the event subscription in Zoom.', + ] + + return instructions + .map( + (instruction, index) => + `
${index + 1}. ${instruction}
` + ) + .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 { + 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 { + 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 { + 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 { + 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)', + }, + } +} diff --git a/apps/sim/triggers/zoom/webhook.ts b/apps/sim/triggers/zoom/webhook.ts new file mode 100644 index 00000000000..b7695c2fa52 --- /dev/null +++ b/apps/sim/triggers/zoom/webhook.ts @@ -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', + }, + }, +}