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
Next Next commit
refactor(webhooks): extract provider-specific logic into handler regi…
…stry (#3973)

* refactor(webhooks): extract provider-specific logic into handler registry

* fix(webhooks): address PR review feedback

- Restore original fall-through behavior for generic requireAuth with no token
- Replace `any` params with proper types in processor helper functions
- Restore array-aware initializer in processTriggerFileOutputs

* fix(webhooks): fix build error from union type indexing in processTriggerFileOutputs

Cast array initializer to Record<string, unknown> to allow string indexing
while preserving array runtime semantics for the return value.

* fix(webhooks): return 401 when requireAuth is true but no token configured

If a user explicitly sets requireAuth: true, they expect auth to be enforced.
Returning 401 when no token is configured is the correct behavior — this is
an intentional improvement over the original code which silently allowed
unauthenticated access in this case.

* refactor(webhooks): move signature validators into provider handler files

Co-locate each validate*Signature function with its provider handler,
eliminating the circular dependency where handlers imported back from
utils.server.ts. validateJiraSignature is exported from jira.ts for
shared use by confluence.ts.

* refactor(webhooks): move challenge handlers into provider files

Move handleWhatsAppVerification to providers/whatsapp.ts and
handleSlackChallenge to providers/slack.ts. Update processor.ts
imports to point to provider files.

* refactor(webhooks): move fetchAndProcessAirtablePayloads into airtable handler

Co-locate the ~400-line Airtable payload processing function with its
provider handler. Remove AirtableChange interface from utils.server.ts.

* refactor(webhooks): extract polling config functions into polling-config.ts

Move configureGmailPolling, configureOutlookPolling, configureRssPolling,
and configureImapPolling out of utils.server.ts into a dedicated module.
Update imports in deploy.ts and webhooks/route.ts.

* refactor(webhooks): decompose formatWebhookInput into per-provider formatInput methods

Move all provider-specific input formatting from the monolithic formatWebhookInput
switch statement into each provider's handler file. Delete formatWebhookInput and
all its helper functions (fetchWithDNSPinning, formatTeamsGraphNotification, Slack
file helpers, convertSquareBracketsToTwiML) from utils.server.ts. Create new handler
files for gmail, outlook, rss, imap, and calendly providers. Update webhook-execution.ts
to use handler.formatInput as the primary path with raw body passthrough as fallback.

utils.server.ts reduced from ~1600 lines to ~370 lines containing only credential-sync
functions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(webhooks): decompose provider-subscriptions into handler registry pattern

Move all provider-specific subscription create/delete logic from the monolithic
provider-subscriptions.ts into individual provider handler files via new
createSubscription/deleteSubscription methods on WebhookProviderHandler.

Replace the two massive if-else dispatch chains (11 branches each) with simple
registry lookups via getProviderHandler(). provider-subscriptions.ts reduced
from 2,337 lines to 128 lines (orchestration only).

Also migrate polling configuration (gmail, outlook, rss, imap) into provider
handlers via configurePolling() method, and challenge/verification handling
(slack, whatsapp, teams) via handleChallenge() method. Delete polling-config.ts.

Create new handler files for fathom and lemlist providers. Extract shared
subscription utilities into subscription-utils.ts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(webhooks): fix attio build error, restore imap field, remove demarcation comments

- Cast `body` to `Record<string, unknown>` in attio formatInput to fix
  type error with extractor functions
- Restore `rejectUnauthorized` field in imap configurePolling for parity
- Remove `// ---` section demarcation comments from route.ts and airtable.ts
- Update add-trigger skill to reflect handler-based architecture

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(webhooks): remove unused imports from utils.server.ts after rebase

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(webhooks): remove duplicate generic file processing from webhook-execution

The generic provider's processInputFiles handler already handles file[] field
processing via the handler.processInputFiles call. The hardcoded block from
staging was incorrectly preserved during rebase, causing double processing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(webhooks): validate auth token is set when requireAuth is enabled at deploy time

Rejects deployment with a clear error message if a generic webhook trigger
has requireAuth enabled but no authentication token configured, rather than
letting requests fail with 401 at runtime.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(webhooks): remove unintended rejectUnauthorized field from IMAP polling config

The refactored IMAP handler added a rejectUnauthorized field that was not
present in the original configureImapPolling function. This would default
to true for all existing IMAP webhooks, potentially breaking connections
to servers with self-signed certificates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(webhooks): replace crypto.randomUUID() with generateId() in ashby handler

Per project coding standards, use generateId() from @/lib/core/utils/uuid
instead of crypto.randomUUID() directly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(webhooks): standardize logger names and remove any types from providers

- Standardize logger names to WebhookProvider:X pattern across 6 providers
  (fathom, gmail, imap, lemlist, outlook, rss)
- Replace all `any` types in airtable handler with proper types:
  - Add AirtableTableChanges interface for API response typing
  - Change function params from `any` to `Record<string, unknown>`
  - Change AirtableChange fields from Record<string, any> to Record<string, unknown>
  - Change all catch blocks from `error: any` to `error: unknown`
  - Change input object from `any` to `Record<string, unknown>`

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(webhooks): remove remaining any types from deploy.ts

Replace 3 `catch (error: any)` with `catch (error: unknown)` and
1 `Record<string, any>` with `Record<string, unknown>`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
  • Loading branch information
waleedlatif1 and claude authored Apr 6, 2026
commit 5ca66c381b7af00ac81adc0fe92a8848928ffa6f
472 changes: 295 additions & 177 deletions .claude/commands/add-trigger.md

Large diffs are not rendered by default.

150 changes: 38 additions & 112 deletions apps/sim/app/api/webhooks/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,9 @@ import {
createExternalWebhookSubscription,
shouldRecreateExternalWebhookSubscription,
} from '@/lib/webhooks/provider-subscriptions'
import { getProviderHandler } from '@/lib/webhooks/providers'
import { mergeNonUserFields } from '@/lib/webhooks/utils'
import {
configureGmailPolling,
configureOutlookPolling,
configureRssPolling,
syncWebhooksForCredentialSet,
} from '@/lib/webhooks/utils.server'
import { syncWebhooksForCredentialSet } from '@/lib/webhooks/utils.server'
import { authorizeWorkflowByWorkspacePermission } from '@/lib/workflows/utils'
import { extractCredentialSetId, isCredentialSetValue } from '@/executor/constants'

Expand Down Expand Up @@ -348,7 +344,6 @@ export async function POST(request: NextRequest) {
workflowRecord.workspaceId || undefined
)

// --- Credential Set Handling ---
// For credential sets, we fan out to create one webhook per credential at save time.
// This applies to all OAuth-based triggers, not just polling ones.
// Check for credentialSetId directly (frontend may already extract it) or credential set value in credential fields
Expand Down Expand Up @@ -402,24 +397,24 @@ export async function POST(request: NextRequest) {
)
}

const needsConfiguration = provider === 'gmail' || provider === 'outlook'
const providerHandler = getProviderHandler(provider)

if (needsConfiguration) {
const configureFunc =
provider === 'gmail' ? configureGmailPolling : configureOutlookPolling
if (providerHandler.configurePolling) {
const configureErrors: string[] = []

for (const wh of syncResult.webhooks) {
if (wh.isNew) {
// Fetch the webhook data for configuration
const webhookRows = await db
.select()
.from(webhook)
.where(and(eq(webhook.id, wh.id), isNull(webhook.archivedAt)))
.limit(1)

if (webhookRows.length > 0) {
const success = await configureFunc(webhookRows[0], requestId)
const success = await providerHandler.configurePolling({
webhook: webhookRows[0],
requestId,
})
if (!success) {
configureErrors.push(
`Failed to configure webhook for credential ${wh.credentialId}`
Expand All @@ -436,7 +431,6 @@ export async function POST(request: NextRequest) {
configureErrors.length > 0 &&
configureErrors.length === syncResult.webhooks.length
) {
// All configurations failed - roll back
logger.error(`[${requestId}] All webhook configurations failed, rolling back`)
for (const wh of syncResult.webhooks) {
await db.delete(webhook).where(eq(webhook.id, wh.id))
Expand Down Expand Up @@ -488,8 +482,6 @@ export async function POST(request: NextRequest) {
}
}
}
// --- End Credential Set Handling ---

let externalSubscriptionCreated = false
const createTempWebhookData = (providerConfigOverride = resolvedProviderConfig) => ({
id: targetWebhookId || generateShortId(),
Expand Down Expand Up @@ -629,115 +621,49 @@ export async function POST(request: NextRequest) {
}
}

// --- Gmail/Outlook webhook setup (these don't require external subscriptions, configure after DB save) ---
if (savedWebhook && provider === 'gmail') {
logger.info(`[${requestId}] Gmail provider detected. Setting up Gmail webhook configuration.`)
try {
const success = await configureGmailPolling(savedWebhook, requestId)

if (!success) {
logger.error(`[${requestId}] Failed to configure Gmail polling, rolling back webhook`)
await revertSavedWebhook(savedWebhook, existingWebhook, requestId)
return NextResponse.json(
{
error: 'Failed to configure Gmail polling',
details: 'Please check your Gmail account permissions and try again',
},
{ status: 500 }
)
}

logger.info(`[${requestId}] Successfully configured Gmail polling`)
} catch (err) {
logger.error(
`[${requestId}] Error setting up Gmail webhook configuration, rolling back webhook`,
err
)
await revertSavedWebhook(savedWebhook, existingWebhook, requestId)
return NextResponse.json(
{
error: 'Failed to configure Gmail webhook',
details: err instanceof Error ? err.message : 'Unknown error',
},
{ status: 500 }
if (savedWebhook) {
const pollingHandler = getProviderHandler(provider)
if (pollingHandler.configurePolling) {
logger.info(
`[${requestId}] ${provider} provider detected. Setting up polling configuration.`
)
}
}
// --- End Gmail specific logic ---
try {
const success = await pollingHandler.configurePolling({
webhook: savedWebhook,
requestId,
})

// --- Outlook webhook setup ---
if (savedWebhook && provider === 'outlook') {
logger.info(
`[${requestId}] Outlook provider detected. Setting up Outlook webhook configuration.`
)
try {
const success = await configureOutlookPolling(savedWebhook, requestId)
if (!success) {
logger.error(
`[${requestId}] Failed to configure ${provider} polling, rolling back webhook`
)
await revertSavedWebhook(savedWebhook, existingWebhook, requestId)
return NextResponse.json(
{
error: `Failed to configure ${provider} polling`,
details: 'Please check your account permissions and try again',
},
{ status: 500 }
)
}

if (!success) {
logger.error(`[${requestId}] Failed to configure Outlook polling, rolling back webhook`)
await revertSavedWebhook(savedWebhook, existingWebhook, requestId)
return NextResponse.json(
{
error: 'Failed to configure Outlook polling',
details: 'Please check your Outlook account permissions and try again',
},
{ status: 500 }
logger.info(`[${requestId}] Successfully configured ${provider} polling`)
} catch (err) {
logger.error(
`[${requestId}] Error setting up ${provider} webhook configuration, rolling back webhook`,
err
)
}

logger.info(`[${requestId}] Successfully configured Outlook polling`)
} catch (err) {
logger.error(
`[${requestId}] Error setting up Outlook webhook configuration, rolling back webhook`,
err
)
await revertSavedWebhook(savedWebhook, existingWebhook, requestId)
return NextResponse.json(
{
error: 'Failed to configure Outlook webhook',
details: err instanceof Error ? err.message : 'Unknown error',
},
{ status: 500 }
)
}
}
// --- End Outlook specific logic ---

// --- RSS webhook setup ---
if (savedWebhook && provider === 'rss') {
logger.info(`[${requestId}] RSS provider detected. Setting up RSS webhook configuration.`)
try {
const success = await configureRssPolling(savedWebhook, requestId)

if (!success) {
logger.error(`[${requestId}] Failed to configure RSS polling, rolling back webhook`)
await revertSavedWebhook(savedWebhook, existingWebhook, requestId)
return NextResponse.json(
{
error: 'Failed to configure RSS polling',
details: 'Please try again',
error: `Failed to configure ${provider} webhook`,
details: err instanceof Error ? err.message : 'Unknown error',
},
{ status: 500 }
)
}

logger.info(`[${requestId}] Successfully configured RSS polling`)
} catch (err) {
logger.error(
`[${requestId}] Error setting up RSS webhook configuration, rolling back webhook`,
err
)
await revertSavedWebhook(savedWebhook, existingWebhook, requestId)
return NextResponse.json(
{
error: 'Failed to configure RSS webhook',
details: err instanceof Error ? err.message : 'Unknown error',
},
{ status: 500 }
)
}
}
// --- End RSS specific logic ---

if (!targetWebhookId && savedWebhook) {
try {
Expand Down
4 changes: 0 additions & 4 deletions apps/sim/app/api/webhooks/trigger/[path]/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ const {
handleSlackChallengeMock,
processWhatsAppDeduplicationMock,
processGenericDeduplicationMock,
fetchAndProcessAirtablePayloadsMock,
processWebhookMock,
executeMock,
getWorkspaceBilledAccountUserIdMock,
Expand All @@ -109,7 +108,6 @@ const {
handleSlackChallengeMock: vi.fn().mockReturnValue(null),
processWhatsAppDeduplicationMock: vi.fn().mockResolvedValue(null),
processGenericDeduplicationMock: vi.fn().mockResolvedValue(null),
fetchAndProcessAirtablePayloadsMock: vi.fn().mockResolvedValue(undefined),
processWebhookMock: vi.fn().mockResolvedValue(new Response('Webhook processed', { status: 200 })),
executeMock: vi.fn().mockResolvedValue({
success: true,
Expand Down Expand Up @@ -156,10 +154,8 @@ vi.mock('@/background/logs-webhook-delivery', () => ({
vi.mock('@/lib/webhooks/utils', () => ({
handleWhatsAppVerification: handleWhatsAppVerificationMock,
handleSlackChallenge: handleSlackChallengeMock,
verifyProviderWebhook: vi.fn().mockReturnValue(null),
processWhatsAppDeduplication: processWhatsAppDeduplicationMock,
processGenericDeduplication: processGenericDeduplicationMock,
fetchAndProcessAirtablePayloads: fetchAndProcessAirtablePayloadsMock,
processWebhook: processWebhookMock,
}))

Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/api/webhooks/trigger/[path]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ async function handleWebhookPost(
if (webhooksForPath.length === 0) {
const verificationResponse = await handlePreLookupWebhookVerification(
request.method,
body,
body as Record<string, unknown> | undefined,
requestId,
path
)
Expand Down
Loading
Loading