Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
cc6b80c
refactor(webhooks): extract provider-specific logic into handler regi…
waleedlatif1 Apr 5, 2026
ffa5864
fix(webhooks): address PR review feedback
waleedlatif1 Apr 5, 2026
403e32f
fix(webhooks): fix build error from union type indexing in processTri…
waleedlatif1 Apr 5, 2026
5d9b95a
fix(webhooks): return 401 when requireAuth is true but no token confi…
waleedlatif1 Apr 5, 2026
7b6b50b
refactor(webhooks): move signature validators into provider handler f…
waleedlatif1 Apr 5, 2026
1f92950
refactor(webhooks): move challenge handlers into provider files
waleedlatif1 Apr 5, 2026
46a1ea0
refactor(webhooks): move fetchAndProcessAirtablePayloads into airtabl…
waleedlatif1 Apr 5, 2026
adf13bc
refactor(webhooks): extract polling config functions into polling-con…
waleedlatif1 Apr 5, 2026
ced7d14
refactor(webhooks): decompose formatWebhookInput into per-provider fo…
waleedlatif1 Apr 5, 2026
0d2f78b
refactor(webhooks): decompose provider-subscriptions into handler reg…
waleedlatif1 Apr 5, 2026
3ad355e
fix(webhooks): fix attio build error, restore imap field, remove dema…
waleedlatif1 Apr 5, 2026
8bcf450
fix(webhooks): remove unused imports from utils.server.ts after rebase
waleedlatif1 Apr 5, 2026
60610b7
fix(webhooks): remove duplicate generic file processing from webhook-…
waleedlatif1 Apr 5, 2026
1478de1
fix(webhooks): validate auth token is set when requireAuth is enabled…
waleedlatif1 Apr 5, 2026
78e6de5
fix(webhooks): remove unintended rejectUnauthorized field from IMAP p…
waleedlatif1 Apr 5, 2026
7afed0d
fix(webhooks): replace crypto.randomUUID() with generateId() in ashby…
waleedlatif1 Apr 5, 2026
220aa91
refactor(webhooks): standardize logger names and remove any types fro…
waleedlatif1 Apr 5, 2026
98b4586
refactor(webhooks): remove remaining any types from deploy.ts
waleedlatif1 Apr 6, 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
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