Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
5ca66c3
refactor(webhooks): extract provider-specific logic into handler regi…
waleedlatif1 Apr 6, 2026
925be3d
feat(triggers): add Salesforce webhook triggers (#3982)
waleedlatif1 Apr 6, 2026
c9b45f4
feat(triggers): add HubSpot merge, restore, and generic webhook trigg…
waleedlatif1 Apr 6, 2026
62a7700
feat(integrations): add Sixtyfour AI integration (#3981)
waleedlatif1 Apr 6, 2026
796384a
feat(triggers): add Resend webhook triggers with auto-registration (#…
waleedlatif1 Apr 6, 2026
62ea0f1
feat(triggers): add Gong webhook triggers for call events (#3984)
waleedlatif1 Apr 6, 2026
590f376
feat(triggers): add Intercom webhook triggers (#3990)
waleedlatif1 Apr 6, 2026
7ea0693
feat(triggers): add Greenhouse webhook triggers (#3985)
waleedlatif1 Apr 6, 2026
21e5b5c
feat(triggers): add Notion webhook triggers (#3989)
waleedlatif1 Apr 6, 2026
c18f023
feat(analytics): add Google Tag Manager and Google Analytics for host…
waleedlatif1 Apr 6, 2026
8b1d749
feat(triggers): add Vercel webhook triggers with automatic registrati…
waleedlatif1 Apr 6, 2026
cd5cee3
feat(landing): add PostHog tracking for CTA clicks, demo requests, an…
waleedlatif1 Apr 6, 2026
18a7868
feat(triggers): add Zoom webhook triggers (#3992)
waleedlatif1 Apr 6, 2026
5ea63f1
feat(triggers): add Linear v2 triggers with automatic webhook registr…
waleedlatif1 Apr 6, 2026
7e0794c
fix(signup): show multiple signup errors at once (#3987)
TheodoreSpeaks Apr 6, 2026
58571fe
fix(hitl): fix stream endpoint, pause persistence, and resume page (#…
waleedlatif1 Apr 6, 2026
2164cef
fix(mothership): fix url keeping markdown hash on resource switch (#3…
TheodoreSpeaks Apr 7, 2026
8c8c627
feat(block): Conditionally hide impersonateUser field from block, add…
TheodoreSpeaks Apr 7, 2026
25b4a3f
feat(posthog): Add posthog log for signup failed (#3998)
TheodoreSpeaks Apr 7, 2026
df2c47a
fix(copilot): fix copilot running workflow stuck on 10mb error (#3999)
TheodoreSpeaks Apr 7, 2026
8df3f20
fix(blocks): allow tool expansion in disabled mode, improve child dep…
waleedlatif1 Apr 7, 2026
5eb494d
fix(secrets): secrets/integrations component code cleanup (#4003)
icecrasher321 Apr 7, 2026
606477e
feat(home): add folders to resource menu (#4000)
waleedlatif1 Apr 7, 2026
cc8c9e8
feat(home): add double-enter to send top queued message (#4005)
waleedlatif1 Apr 7, 2026
c52834b
fix(subflows): make edges inside subflows directly clickable (#3969)
waleedlatif1 Apr 7, 2026
609ba61
fix(sockets): joining currently deleted workflow (#4004)
icecrasher321 Apr 7, 2026
89ae738
feat(folders): soft-delete folders and show in Recently Deleted (#4001)
waleedlatif1 Apr 7, 2026
64c6cd9
fix(webhooks): harden audited provider triggers (#3997)
waleedlatif1 Apr 7, 2026
8e11c32
fix(resource-menu): consistent height between 1 result and no results…
waleedlatif1 Apr 7, 2026
1e00a06
fix(home): simplify enter-to-send queued message to single press (#4008)
waleedlatif1 Apr 7, 2026
7793583
fix(secrets): restore unsaved-changes guard for settings tab navigati…
waleedlatif1 Apr 7, 2026
68df732
refactor(triggers): consolidate v2 Linear triggers into same files as…
waleedlatif1 Apr 7, 2026
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
Prev Previous commit
Next Next commit
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
  • Loading branch information
waleedlatif1 authored Apr 6, 2026
commit 18a7868bb33ba2f51e169e47e3944d05fc9ba15f
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 @@ -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",
Expand Down Expand Up @@ -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",
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 @@ -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')

Expand Down Expand Up @@ -78,6 +79,7 @@ const PROVIDER_HANDLERS: Record<string, WebhookProviderHandler> = {
vercel: vercelHandler,
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 @@ -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,
Expand Down Expand Up @@ -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,
}
Loading
Loading