Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
117 changes: 112 additions & 5 deletions apps/sim/app/(landing)/integrations/data/integrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/lib/webhooks/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/lib/webhooks/providers/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down Expand Up @@ -72,6 +73,7 @@ const PROVIDER_HANDLERS: Record<string, WebhookProviderHandler> = {
typeform: typeformHandler,
webflow: webflowHandler,
whatsapp: whatsappHandler,
zoom: zoomHandler,
}

/**
Expand Down
166 changes: 166 additions & 0 deletions apps/sim/lib/webhooks/providers/zoom.ts
Original file line number Diff line number Diff line change
@@ -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,
})
},
}
14 changes: 14 additions & 0 deletions apps/sim/triggers/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
}
Loading
Loading