diff --git a/apps/sim/app/api/webhooks/route.ts b/apps/sim/app/api/webhooks/route.ts index 61f8d30c54d..22468160cb5 100644 --- a/apps/sim/app/api/webhooks/route.ts +++ b/apps/sim/app/api/webhooks/route.ts @@ -33,6 +33,7 @@ import { shouldRecreateExternalWebhookSubscription, } from '@/lib/webhooks/provider-subscriptions' import { getProviderHandler } from '@/lib/webhooks/providers' +import { WebhookDeploymentConfigurationError } from '@/lib/webhooks/providers/errors' import { mergeNonUserFields } from '@/lib/webhooks/utils' import { findConflictingWebhookPathOwner } from '@/lib/webhooks/utils.server' import { listAccessibleWorkspaceRowsForUser } from '@/lib/workspaces/utils' @@ -573,13 +574,20 @@ export const POST = withRouteHandler(async (request: NextRequest) => { } } } catch (err) { - logger.error(`[${requestId}] Error creating external webhook subscription`, err) + const isConfigurationError = err instanceof WebhookDeploymentConfigurationError + if (isConfigurationError) { + logger.warn(`[${requestId}] Rejected webhook trigger configuration`, { + error: err.message, + }) + } else { + logger.error(`[${requestId}] Error creating external webhook subscription`, err) + } return NextResponse.json( { error: 'Failed to create external webhook subscription', details: getErrorMessage(err, 'Unknown error'), }, - { status: 500 } + { status: isConfigurationError ? 400 : 500 } ) } } else { diff --git a/apps/sim/lib/webhooks/providers/airtable.ts b/apps/sim/lib/webhooks/providers/airtable.ts index 63f609e2ee3..0c9a2ccbc03 100644 --- a/apps/sim/lib/webhooks/providers/airtable.ts +++ b/apps/sim/lib/webhooks/providers/airtable.ts @@ -1,6 +1,7 @@ import { db } from '@sim/db' import { account, webhook } from '@sim/db/schema' import { createLogger } from '@sim/logger' +import { toStringOrNull } from '@sim/utils/coerce' import { toRecord } from '@sim/utils/object' import { eq } from 'drizzle-orm' import { validateAirtableId } from '@/lib/core/security/input-validation' @@ -14,6 +15,7 @@ import { getNotificationUrl, getProviderConfig, } from '@/lib/webhooks/provider-subscription-utils' +import { WebhookDeploymentConfigurationError } from '@/lib/webhooks/providers/errors' import type { DeleteSubscriptionContext, FormatInputContext, @@ -435,30 +437,30 @@ export const airtableHandler: WebhookProviderHandler = { }: SubscriptionContext): Promise { try { const config = getProviderConfig(webhookRecord) - const { baseId, tableId, includeCellValuesInFieldIds, credentialId } = config as { - baseId?: string - tableId?: string + const { includeCellValuesInFieldIds, credentialId } = config as { includeCellValuesInFieldIds?: string credentialId?: string } + const baseId = toStringOrNull(config.baseId) + const tableId = toStringOrNull(config.tableId) if (!baseId || !tableId) { logger.warn(`[${requestId}] Missing baseId or tableId for Airtable webhook creation.`, { webhookId: webhookRecord.id, }) - throw new Error( + throw new WebhookDeploymentConfigurationError( 'Base ID and Table ID are required to create Airtable webhook. Please provide valid Airtable base and table IDs.' ) } const baseIdValidation = validateAirtableId(baseId, 'app', 'baseId') if (!baseIdValidation.isValid) { - throw new Error(baseIdValidation.error) + throw new WebhookDeploymentConfigurationError(baseIdValidation.error) } const tableIdValidation = validateAirtableId(tableId, 'tbl', 'tableId') if (!tableIdValidation.isValid) { - throw new Error(tableIdValidation.error) + throw new WebhookDeploymentConfigurationError(tableIdValidation.error) } const accessToken = credentialId @@ -535,6 +537,7 @@ export const airtableHandler: WebhookProviderHandler = { ) return { providerConfigUpdates: { externalId: responseBody.id } } } catch (error: unknown) { + if (error instanceof WebhookDeploymentConfigurationError) throw error const err = error as Error logger.error( `[${requestId}] Exception during Airtable webhook creation for webhook ${webhookRecord.id}.`, diff --git a/apps/sim/lib/webhooks/providers/ashby.ts b/apps/sim/lib/webhooks/providers/ashby.ts index 18315250edf..c42c042414f 100644 --- a/apps/sim/lib/webhooks/providers/ashby.ts +++ b/apps/sim/lib/webhooks/providers/ashby.ts @@ -6,6 +6,7 @@ import { isRecordLike, omit, toRecord } from '@sim/utils/object' import { NextResponse } from 'next/server' import { isPayloadSizeLimitError, readResponseJsonWithLimit } from '@/lib/core/utils/stream-limits' import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' +import { WebhookDeploymentConfigurationError } from '@/lib/webhooks/providers/errors' import type { AuthContext, DeleteSubscriptionContext, @@ -235,13 +236,15 @@ export const ashbyHandler: WebhookProviderHandler = { } if (!apiKey) { - throw new Error( + throw new WebhookDeploymentConfigurationError( 'Ashby API Key is required. Please provide your API Key with apiKeysWrite permission in the trigger configuration.' ) } if (!triggerId) { - throw new Error('Trigger ID is required to create Ashby webhook.') + throw new WebhookDeploymentConfigurationError( + 'Trigger ID is required to create Ashby webhook.' + ) } const { ASHBY_TRIGGER_ACTION_MAP } = await import('@/triggers/ashby/utils') @@ -322,6 +325,7 @@ export const ashbyHandler: WebhookProviderHandler = { ) return { providerConfigUpdates: { externalId, secretToken } } } catch (error: unknown) { + if (error instanceof WebhookDeploymentConfigurationError) throw error const err = error as Error logger.error( `[${ctx.requestId}] Exception during Ashby webhook creation for webhook ${ctx.webhook.id}.`, diff --git a/apps/sim/lib/webhooks/providers/attio.ts b/apps/sim/lib/webhooks/providers/attio.ts index c8fde94a4cd..5b3fb473ce8 100644 --- a/apps/sim/lib/webhooks/providers/attio.ts +++ b/apps/sim/lib/webhooks/providers/attio.ts @@ -9,6 +9,7 @@ import { getNotificationUrl, getProviderConfig, } from '@/lib/webhooks/provider-subscription-utils' +import { WebhookDeploymentConfigurationError } from '@/lib/webhooks/providers/errors' import type { AuthContext, DeleteSubscriptionContext, @@ -116,7 +117,7 @@ export const attioHandler: WebhookProviderHandler = { logger.warn(`[${requestId}] Missing credentialId for Attio webhook creation.`, { webhookId: webhookRecord.id, }) - throw new Error( + throw new WebhookDeploymentConfigurationError( 'Attio account connection required. Please connect your Attio account in the trigger configuration and try again.' ) } @@ -222,6 +223,7 @@ export const attioHandler: WebhookProviderHandler = { return { providerConfigUpdates: { externalId: webhookId, webhookSecret: secret || '' } } } catch (error: unknown) { + if (error instanceof WebhookDeploymentConfigurationError) throw error const message = toError(error).message logger.error( `[${requestId}] Exception during Attio webhook creation for webhook ${webhookRecord.id}.`, diff --git a/apps/sim/lib/webhooks/providers/bitbucket.ts b/apps/sim/lib/webhooks/providers/bitbucket.ts index 2f61d11825c..3ded1288f57 100644 --- a/apps/sim/lib/webhooks/providers/bitbucket.ts +++ b/apps/sim/lib/webhooks/providers/bitbucket.ts @@ -11,6 +11,7 @@ import { getNotificationUrl, getProviderConfig, } from '@/lib/webhooks/provider-subscription-utils' +import { WebhookDeploymentConfigurationError } from '@/lib/webhooks/providers/errors' import type { DeleteSubscriptionContext, EventMatchContext, @@ -89,7 +90,7 @@ async function resolveBitbucketAccessToken( requestId: string ): Promise { if (!credentialId) { - throw new Error( + throw new WebhookDeploymentConfigurationError( 'Bitbucket account connection required. Connect or reconnect Bitbucket in the trigger configuration and try again.' ) } @@ -112,7 +113,9 @@ function readRequiredConfigString( ): string { const value = config[key] if (typeof value !== 'string' || !value.trim()) { - throw new Error(`Bitbucket ${label} is required to manage the repository webhook.`) + throw new WebhookDeploymentConfigurationError( + `Bitbucket ${label} is required to manage the repository webhook.` + ) } return value.trim() } diff --git a/apps/sim/lib/webhooks/providers/calendly.ts b/apps/sim/lib/webhooks/providers/calendly.ts index d574d6e9534..83445bf7cfc 100644 --- a/apps/sim/lib/webhooks/providers/calendly.ts +++ b/apps/sim/lib/webhooks/providers/calendly.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' +import { WebhookDeploymentConfigurationError } from '@/lib/webhooks/providers/errors' import type { DeleteSubscriptionContext, FormatInputContext, @@ -37,7 +38,7 @@ export const calendlyHandler: WebhookProviderHandler = { logger.warn(`[${ctx.requestId}] Missing apiKey for Calendly webhook creation.`, { webhookId: ctx.webhook.id, }) - throw new Error( + throw new WebhookDeploymentConfigurationError( 'Personal Access Token is required to create Calendly webhook. Please provide your Calendly Personal Access Token.' ) } @@ -46,7 +47,7 @@ export const calendlyHandler: WebhookProviderHandler = { logger.warn(`[${ctx.requestId}] Missing organization URI for Calendly webhook creation.`, { webhookId: ctx.webhook.id, }) - throw new Error( + throw new WebhookDeploymentConfigurationError( 'Organization URI is required to create Calendly webhook. Please provide your Organization URI from the "Get Current User" operation.' ) } @@ -55,7 +56,9 @@ export const calendlyHandler: WebhookProviderHandler = { logger.warn(`[${ctx.requestId}] Missing triggerId for Calendly webhook creation.`, { webhookId: ctx.webhook.id, }) - throw new Error('Trigger ID is required to create Calendly webhook') + throw new WebhookDeploymentConfigurationError( + 'Trigger ID is required to create Calendly webhook' + ) } const notificationUrl = getNotificationUrl(ctx.webhook) @@ -152,6 +155,7 @@ export const calendlyHandler: WebhookProviderHandler = { ) return { providerConfigUpdates: { externalId: webhookId } } } catch (error: unknown) { + if (error instanceof WebhookDeploymentConfigurationError) throw error const err = error as Error logger.error( `[${ctx.requestId}] Exception during Calendly webhook creation for webhook ${ctx.webhook.id}.`, diff --git a/apps/sim/lib/webhooks/providers/checkr.ts b/apps/sim/lib/webhooks/providers/checkr.ts index 5bfb6b8b180..2896636da71 100644 --- a/apps/sim/lib/webhooks/providers/checkr.ts +++ b/apps/sim/lib/webhooks/providers/checkr.ts @@ -8,6 +8,7 @@ import { and, eq, isNull, ne } from 'drizzle-orm' import { NextResponse } from 'next/server' import { isPayloadSizeLimitError, readResponseJsonWithLimit } from '@/lib/core/utils/stream-limits' import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' +import { WebhookDeploymentConfigurationError } from '@/lib/webhooks/providers/errors' import type { AuthContext, DeleteSubscriptionContext, @@ -198,7 +199,9 @@ export const checkrHandler: WebhookProviderHandler = { const providerConfig = getProviderConfig(ctx.webhook) const apiKey = typeof providerConfig.apiKey === 'string' ? providerConfig.apiKey.trim() : '' if (!apiKey) { - throw new Error('Checkr API key is required to register the webhook.') + throw new WebhookDeploymentConfigurationError( + 'Checkr API key is required to register the webhook.' + ) } const notificationUrl = getNotificationUrl(ctx.webhook) diff --git a/apps/sim/lib/webhooks/providers/clickup.ts b/apps/sim/lib/webhooks/providers/clickup.ts index 1f97de6ee3f..a411b4be055 100644 --- a/apps/sim/lib/webhooks/providers/clickup.ts +++ b/apps/sim/lib/webhooks/providers/clickup.ts @@ -9,6 +9,7 @@ import { getNotificationUrl, getProviderConfig, } from '@/lib/webhooks/provider-subscription-utils' +import { WebhookDeploymentConfigurationError } from '@/lib/webhooks/providers/errors' import type { DeleteSubscriptionContext, EventMatchContext, @@ -46,7 +47,7 @@ async function resolveClickUpAccessToken( requestId: string ): Promise { if (!credentialId) { - throw new Error( + throw new WebhookDeploymentConfigurationError( 'ClickUp account connection required. Please connect your ClickUp account in the trigger configuration and try again.' ) } @@ -72,7 +73,9 @@ function parseOptionalNumericId(value: unknown, label: string): number | undefin if (!raw) return undefined const parsed = Number(raw) if (!Number.isInteger(parsed)) { - throw new Error(`ClickUp ${label} must be a whole number. Received: ${raw}`) + throw new WebhookDeploymentConfigurationError( + `ClickUp ${label} must be a whole number. Received: ${raw}` + ) } return parsed } @@ -154,7 +157,7 @@ export const clickupHandler: WebhookProviderHandler = { const workspaceId = parseOptionalStringId(config.triggerWorkspaceId) if (!workspaceId) { - throw new Error( + throw new WebhookDeploymentConfigurationError( 'ClickUp workspace is required. Please select a workspace in the trigger configuration and try again.' ) } @@ -271,6 +274,7 @@ export const clickupHandler: WebhookProviderHandler = { return { providerConfigUpdates: { externalId, webhookSecret: secret } } } catch (error: unknown) { + if (error instanceof WebhookDeploymentConfigurationError) throw error const message = toError(error).message logger.error( `[${requestId}] Exception during ClickUp webhook creation for webhook ${webhookRecord.id}.`, diff --git a/apps/sim/lib/webhooks/providers/emailbison.ts b/apps/sim/lib/webhooks/providers/emailbison.ts index 43dc7c9f999..cdbcce6dfcd 100644 --- a/apps/sim/lib/webhooks/providers/emailbison.ts +++ b/apps/sim/lib/webhooks/providers/emailbison.ts @@ -7,6 +7,7 @@ import { validateUrlWithDNS, } from '@/lib/core/security/input-validation.server' import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' +import { WebhookDeploymentConfigurationError } from '@/lib/webhooks/providers/errors' import type { DeleteSubscriptionContext, EventMatchContext, @@ -127,15 +128,15 @@ export const emailBisonHandler: WebhookProviderHandler = { const triggerId = providerConfig.triggerId as string | undefined if (!apiKey?.trim()) { - throw new Error('Email Bison API Key is required.') + throw new WebhookDeploymentConfigurationError('Email Bison API Key is required.') } if (!apiBaseUrl?.trim()) { - throw new Error('Email Bison Instance URL is required.') + throw new WebhookDeploymentConfigurationError('Email Bison Instance URL is required.') } if (!triggerId) { - throw new Error('Email Bison trigger ID is required.') + throw new WebhookDeploymentConfigurationError('Email Bison trigger ID is required.') } const { getEmailBisonEventTypeForTrigger } = await import('@/triggers/emailbison/utils') diff --git a/apps/sim/lib/webhooks/providers/errors.ts b/apps/sim/lib/webhooks/providers/errors.ts index 77fe674ed81..1ce9f67921a 100644 --- a/apps/sim/lib/webhooks/providers/errors.ts +++ b/apps/sim/lib/webhooks/providers/errors.ts @@ -1,5 +1,5 @@ export class WebhookDeploymentConfigurationError extends Error { - constructor(message: string) { + constructor(message?: string) { super(message) this.name = 'WebhookDeploymentConfigurationError' } diff --git a/apps/sim/lib/webhooks/providers/fathom.ts b/apps/sim/lib/webhooks/providers/fathom.ts index 341f50c4592..5299edadeb2 100644 --- a/apps/sim/lib/webhooks/providers/fathom.ts +++ b/apps/sim/lib/webhooks/providers/fathom.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import { validateAlphanumericId } from '@/lib/core/security/input-validation' import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' +import { WebhookDeploymentConfigurationError } from '@/lib/webhooks/providers/errors' import type { DeleteSubscriptionContext, SubscriptionContext, @@ -27,7 +28,7 @@ export const fathomHandler: WebhookProviderHandler = { logger.warn(`[${requestId}] Missing apiKey for Fathom webhook creation.`, { webhookId: webhook.id, }) - throw new Error( + throw new WebhookDeploymentConfigurationError( 'Fathom API Key is required. Please provide your API key in the trigger configuration.' ) } @@ -108,6 +109,7 @@ export const fathomHandler: WebhookProviderHandler = { return { providerConfigUpdates: { externalId: responseBody.id } } } catch (error: unknown) { + if (error instanceof WebhookDeploymentConfigurationError) throw error const err = error as Error logger.error( `[${requestId}] Exception during Fathom webhook creation for webhook ${webhook.id}.`, diff --git a/apps/sim/lib/webhooks/providers/fivetran.ts b/apps/sim/lib/webhooks/providers/fivetran.ts index 1069e383676..f17ded3337c 100644 --- a/apps/sim/lib/webhooks/providers/fivetran.ts +++ b/apps/sim/lib/webhooks/providers/fivetran.ts @@ -8,6 +8,7 @@ import { generateRandomHex } from '@sim/utils/random' import { NextResponse } from 'next/server' import { isPayloadSizeLimitError, readResponseJsonWithLimit } from '@/lib/core/utils/stream-limits' import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' +import { WebhookDeploymentConfigurationError } from '@/lib/webhooks/providers/errors' import type { AuthContext, DeleteSubscriptionContext, @@ -228,7 +229,9 @@ export const fivetranHandler: WebhookProviderHandler = { const providerConfig = getProviderConfig(ctx.webhook) const credentials = readCredentials(providerConfig) if (!credentials) { - throw new Error('A Fivetran API key and secret are required to register the webhook.') + throw new WebhookDeploymentConfigurationError( + 'A Fivetran API key and secret are required to register the webhook.' + ) } const triggerId = readString(providerConfig, 'triggerId') diff --git a/apps/sim/lib/webhooks/providers/gitlab.ts b/apps/sim/lib/webhooks/providers/gitlab.ts index 41d71d758e1..cbb99aed691 100644 --- a/apps/sim/lib/webhooks/providers/gitlab.ts +++ b/apps/sim/lib/webhooks/providers/gitlab.ts @@ -5,6 +5,7 @@ import { isRecordLike, toRecord } from '@sim/utils/object' import { NextResponse } from 'next/server' import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server' import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' +import { WebhookDeploymentConfigurationError } from '@/lib/webhooks/providers/errors' import type { AuthContext, DeleteSubscriptionContext, @@ -182,15 +183,22 @@ export const gitlabHandler: WebhookProviderHandler = { const triggerId = config.triggerId as string | undefined const host = config.host as string | undefined - if (!accessToken) - throw new Error('GitLab Personal Access Token is required to create the webhook.') - if (!projectId) throw new Error('GitLab Project ID is required to create the webhook.') + if (!accessToken) { + throw new WebhookDeploymentConfigurationError( + 'GitLab Personal Access Token is required to create the webhook.' + ) + } + if (!projectId) { + throw new WebhookDeploymentConfigurationError( + 'GitLab Project ID is required to create the webhook.' + ) + } try { getGitLabApiBase(host) } catch (error) { if (error instanceof UnsafeGitLabHostError) { - throw new Error( + throw new WebhookDeploymentConfigurationError( 'GitLab host is invalid. Provide a domain like gitlab.example.com (no protocol, path, or credentials).' ) } diff --git a/apps/sim/lib/webhooks/providers/grain.ts b/apps/sim/lib/webhooks/providers/grain.ts index 4270b6babe5..8a7820694d4 100644 --- a/apps/sim/lib/webhooks/providers/grain.ts +++ b/apps/sim/lib/webhooks/providers/grain.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import { NextResponse } from 'next/server' import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' +import { WebhookDeploymentConfigurationError } from '@/lib/webhooks/providers/errors' import type { DeleteSubscriptionContext, EventFilterContext, @@ -141,7 +142,7 @@ async function createLegacyV1Subscription(params: { webhookId, triggerId, }) - throw new Error( + throw new WebhookDeploymentConfigurationError( 'Grain view ID is required. Please provide the Grain view ID from GET /_/public-api/views in the trigger configuration.' ) } @@ -303,7 +304,7 @@ export const grainHandler: WebhookProviderHandler = { logger.warn(`[${requestId}] Missing apiKey for Grain webhook creation.`, { webhookId: webhook.id, }) - throw new Error( + throw new WebhookDeploymentConfigurationError( 'Grain API Key is required. Please provide your Grain access token in the trigger configuration.' ) } @@ -352,6 +353,7 @@ export const grainHandler: WebhookProviderHandler = { webhookId: webhook.id as string, }) } catch (error: unknown) { + if (error instanceof WebhookDeploymentConfigurationError) throw error const err = error as Error logger.error( `[${requestId}] Exception during Grain webhook creation for webhook ${webhook.id}.`, diff --git a/apps/sim/lib/webhooks/providers/granola.ts b/apps/sim/lib/webhooks/providers/granola.ts index b8dfb7dcb9f..70905343420 100644 --- a/apps/sim/lib/webhooks/providers/granola.ts +++ b/apps/sim/lib/webhooks/providers/granola.ts @@ -2,6 +2,7 @@ import { createLogger } from '@sim/logger' import { toRecordOrNull } from '@sim/utils/object' import { NextResponse } from 'next/server' import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' +import { WebhookDeploymentConfigurationError } from '@/lib/webhooks/providers/errors' import type { AuthContext, DeleteSubscriptionContext, @@ -215,7 +216,7 @@ export const granolaHandler: WebhookProviderHandler = { logger.warn(`[${requestId}] Missing apiKey for Granola webhook creation.`, { webhookId: webhook.id, }) - throw new Error( + throw new WebhookDeploymentConfigurationError( 'Granola API Key is required. Please provide your Granola API key in the trigger configuration.' ) } diff --git a/apps/sim/lib/webhooks/providers/instantly.ts b/apps/sim/lib/webhooks/providers/instantly.ts index ff8089a867e..5b62fad2c42 100644 --- a/apps/sim/lib/webhooks/providers/instantly.ts +++ b/apps/sim/lib/webhooks/providers/instantly.ts @@ -5,6 +5,7 @@ import { generateShortId } from '@sim/utils/id' import { isRecordLike, toRecord, toRecordOrNull } from '@sim/utils/object' import { NextResponse } from 'next/server' import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' +import { WebhookDeploymentConfigurationError } from '@/lib/webhooks/providers/errors' import type { AuthContext, DeleteSubscriptionContext, @@ -127,11 +128,11 @@ export const instantlyHandler: WebhookProviderHandler = { const campaignId = optionalId(providerConfig.triggerCampaignId) if (!apiKey?.trim()) { - throw new Error('Instantly API Key is required.') + throw new WebhookDeploymentConfigurationError('Instantly API Key is required.') } if (!triggerId) { - throw new Error('Instantly trigger ID is required.') + throw new WebhookDeploymentConfigurationError('Instantly trigger ID is required.') } const { getInstantlySubscriptionEventTypeForTrigger } = await import( diff --git a/apps/sim/lib/webhooks/providers/jotform.ts b/apps/sim/lib/webhooks/providers/jotform.ts index 39de88e8a9b..97ba5d5e1a9 100644 --- a/apps/sim/lib/webhooks/providers/jotform.ts +++ b/apps/sim/lib/webhooks/providers/jotform.ts @@ -4,6 +4,7 @@ import { getErrorMessage } from '@sim/utils/errors' import { isRecordLike, toRecord } from '@sim/utils/object' import { and, eq, isNull, ne } from 'drizzle-orm' import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' +import { WebhookDeploymentConfigurationError } from '@/lib/webhooks/providers/errors' import type { DeleteSubscriptionContext, FormatInputContext, @@ -167,7 +168,7 @@ export const jotformHandler: WebhookProviderHandler = { const credentials = readCredentials(ctx.webhook) if ('error' in credentials) { logger.warn(`[${ctx.requestId}] ${credentials.error}`, { webhookId: ctx.webhook.id }) - throw new Error(credentials.error) + throw new WebhookDeploymentConfigurationError(credentials.error) } const notificationUrl = getNotificationUrl(ctx.webhook) diff --git a/apps/sim/lib/webhooks/providers/lemlist.ts b/apps/sim/lib/webhooks/providers/lemlist.ts index 24759354a36..46fdfab2d54 100644 --- a/apps/sim/lib/webhooks/providers/lemlist.ts +++ b/apps/sim/lib/webhooks/providers/lemlist.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import { validateAlphanumericId } from '@/lib/core/security/input-validation' import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' +import { WebhookDeploymentConfigurationError } from '@/lib/webhooks/providers/errors' import type { DeleteSubscriptionContext, SubscriptionContext, @@ -23,7 +24,7 @@ export const lemlistHandler: WebhookProviderHandler = { logger.warn(`[${requestId}] Missing apiKey for Lemlist webhook creation.`, { webhookId: webhook.id, }) - throw new Error( + throw new WebhookDeploymentConfigurationError( 'Lemlist API Key is required. Please provide your Lemlist API Key in the trigger configuration.' ) } @@ -108,6 +109,7 @@ export const lemlistHandler: WebhookProviderHandler = { return { providerConfigUpdates: { externalId: responseBody._id } } } catch (error: unknown) { + if (error instanceof WebhookDeploymentConfigurationError) throw error const err = error as Error logger.error( `[${requestId}] Exception during Lemlist webhook creation for webhook ${webhook.id}.`, diff --git a/apps/sim/lib/webhooks/providers/linear.ts b/apps/sim/lib/webhooks/providers/linear.ts index 5b9af23aaff..5b5d09a341b 100644 --- a/apps/sim/lib/webhooks/providers/linear.ts +++ b/apps/sim/lib/webhooks/providers/linear.ts @@ -6,6 +6,7 @@ import { generateId } from '@sim/utils/id' import { isRecordLike, toRecord } from '@sim/utils/object' import { NextResponse } from 'next/server' import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' +import { WebhookDeploymentConfigurationError } from '@/lib/webhooks/providers/errors' import type { AuthContext, DeleteSubscriptionContext, @@ -183,7 +184,7 @@ export const linearHandler: WebhookProviderHandler = { const apiKey = config.apiKey as string | undefined if (!apiKey) { logger.warn(`[${ctx.requestId}] Missing API key for Linear webhook ${ctx.webhook.id}`) - throw new Error( + throw new WebhookDeploymentConfigurationError( 'Linear API key is required. Please provide a valid API key in the trigger configuration.' ) } diff --git a/apps/sim/lib/webhooks/providers/linq.ts b/apps/sim/lib/webhooks/providers/linq.ts index 56ac42570fd..5337dee92d3 100644 --- a/apps/sim/lib/webhooks/providers/linq.ts +++ b/apps/sim/lib/webhooks/providers/linq.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import { NextResponse } from 'next/server' import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' +import { WebhookDeploymentConfigurationError } from '@/lib/webhooks/providers/errors' import type { AuthContext, DeleteSubscriptionContext, @@ -102,7 +103,7 @@ export const linqHandler: WebhookProviderHandler = { logger.warn(`[${requestId}] Missing apiKey for Linq webhook creation.`, { webhookId: webhook.id, }) - throw new Error( + throw new WebhookDeploymentConfigurationError( 'Linq API Key is required. Please provide your Linq API Key in the trigger configuration.' ) } diff --git a/apps/sim/lib/webhooks/providers/microsoft-teams.ts b/apps/sim/lib/webhooks/providers/microsoft-teams.ts index fa9e9b632d1..1d441b991b5 100644 --- a/apps/sim/lib/webhooks/providers/microsoft-teams.ts +++ b/apps/sim/lib/webhooks/providers/microsoft-teams.ts @@ -20,6 +20,7 @@ import { getNotificationUrl, getProviderConfig, } from '@/lib/webhooks/provider-subscription-utils' +import { WebhookDeploymentConfigurationError } from '@/lib/webhooks/providers/errors' import type { AuthContext, DeleteSubscriptionContext, @@ -603,14 +604,14 @@ export const microsoftTeamsHandler: WebhookProviderHandler = { if (!credentialId) { logger.warn(`[${requestId}] Missing credentialId for Teams chat subscription ${webhook.id}`) - throw new Error( + throw new WebhookDeploymentConfigurationError( 'Microsoft Teams credentials are required. Please connect your Microsoft account in the trigger configuration.' ) } if (!chatId) { logger.warn(`[${requestId}] Missing chatId for Teams chat subscription ${webhook.id}`) - throw new Error( + throw new WebhookDeploymentConfigurationError( 'Chat ID is required to create a Teams subscription. Please provide a valid chat ID.' ) } diff --git a/apps/sim/lib/webhooks/providers/monday.ts b/apps/sim/lib/webhooks/providers/monday.ts index 3d9ebafc470..9b10e78d1cf 100644 --- a/apps/sim/lib/webhooks/providers/monday.ts +++ b/apps/sim/lib/webhooks/providers/monday.ts @@ -8,6 +8,7 @@ import { getNotificationUrl, getProviderConfig, } from '@/lib/webhooks/provider-subscription-utils' +import { WebhookDeploymentConfigurationError } from '@/lib/webhooks/providers/errors' import type { DeleteSubscriptionContext, FormatInputContext, @@ -73,19 +74,21 @@ export const mondayHandler: WebhookProviderHandler = { if (!triggerId) { logger.warn(`[${ctx.requestId}] Missing triggerId for Monday webhook ${ctx.webhook.id}`) - throw new Error('Trigger type is required for Monday.com webhook creation.') + throw new WebhookDeploymentConfigurationError( + 'Trigger type is required for Monday.com webhook creation.' + ) } if (!boardId) { logger.warn(`[${ctx.requestId}] Missing boardId for Monday webhook ${ctx.webhook.id}`) - throw new Error( + throw new WebhookDeploymentConfigurationError( 'Board ID is required. Please provide a valid Monday.com board ID in the trigger configuration.' ) } const boardIdValidation = validateMondayNumericId(boardId, 'boardId') if (!boardIdValidation.isValid) { - throw new Error(boardIdValidation.error!) + throw new WebhookDeploymentConfigurationError(boardIdValidation.error!) } const { MONDAY_EVENT_TYPE_MAP } = await import('@/triggers/monday/utils') diff --git a/apps/sim/lib/webhooks/providers/pagerduty.ts b/apps/sim/lib/webhooks/providers/pagerduty.ts index fe15d0d475c..9f3d318d4e9 100644 --- a/apps/sim/lib/webhooks/providers/pagerduty.ts +++ b/apps/sim/lib/webhooks/providers/pagerduty.ts @@ -3,6 +3,7 @@ import { createLogger } from '@sim/logger' import { safeCompare } from '@sim/security/compare' import { toRecord } from '@sim/utils/object' import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' +import { WebhookDeploymentConfigurationError } from '@/lib/webhooks/providers/errors' import type { DeleteSubscriptionContext, EventMatchContext, @@ -145,8 +146,11 @@ export const pagerdutyHandler: WebhookProviderHandler = { const apiKey = config.apiKey as string | undefined const triggerId = config.triggerId as string | undefined - if (!apiKey) - throw new Error('PagerDuty API Key is required to create the webhook subscription.') + if (!apiKey) { + throw new WebhookDeploymentConfigurationError( + 'PagerDuty API Key is required to create the webhook subscription.' + ) + } const { getPagerDutyEvents } = await import('@/triggers/pagerduty/utils') const res = await fetch(`${PAGERDUTY_API_BASE}/webhook_subscriptions`, { diff --git a/apps/sim/lib/webhooks/providers/plane.ts b/apps/sim/lib/webhooks/providers/plane.ts index 8bbf999e555..ff23a291f5a 100644 --- a/apps/sim/lib/webhooks/providers/plane.ts +++ b/apps/sim/lib/webhooks/providers/plane.ts @@ -5,6 +5,7 @@ import { getErrorMessage } from '@sim/utils/errors' import { isRecordLike, toRecord } from '@sim/utils/object' import { secureFetchWithValidation } from '@/lib/core/security/input-validation.server' import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' +import { WebhookDeploymentConfigurationError } from '@/lib/webhooks/providers/errors' import type { WebhookProviderHandler } from '@/lib/webhooks/providers/types' import { buildFallbackDeliveryFingerprint, @@ -40,8 +41,16 @@ async function planeWebhookRequest( ) { const slug = config.triggerWorkspaceSlug const apiKey = config.triggerApiKey - if (typeof slug !== 'string' || !slug.trim() || typeof apiKey !== 'string') { - throw new Error('Plane workspace slug and API token are required for automatic registration') + if ( + typeof slug !== 'string' || + !slug.trim() || + typeof apiKey !== 'string' || + !apiKey.trim() || + /[\r\n]/.test(apiKey.trim()) + ) { + throw new WebhookDeploymentConfigurationError( + 'Plane workspace slug and API token are required for automatic registration' + ) } const baseUrl = typeof config.triggerBaseUrl === 'string' ? config.triggerBaseUrl : undefined return secureFetchWithValidation( diff --git a/apps/sim/lib/webhooks/providers/resend.ts b/apps/sim/lib/webhooks/providers/resend.ts index d3045a5ab96..e532540123c 100644 --- a/apps/sim/lib/webhooks/providers/resend.ts +++ b/apps/sim/lib/webhooks/providers/resend.ts @@ -1,6 +1,7 @@ import { createLogger } from '@sim/logger' import { NextResponse } from 'next/server' import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' +import { WebhookDeploymentConfigurationError } from '@/lib/webhooks/providers/errors' import type { AuthContext, DeleteSubscriptionContext, @@ -117,7 +118,7 @@ export const resendHandler: WebhookProviderHandler = { logger.warn(`[${requestId}] Missing apiKey for Resend webhook creation.`, { webhookId: webhook.id, }) - throw new Error( + throw new WebhookDeploymentConfigurationError( 'Resend API Key is required. Please provide your Resend API Key in the trigger configuration.' ) } @@ -203,6 +204,7 @@ export const resendHandler: WebhookProviderHandler = { }, } } catch (error: unknown) { + if (error instanceof WebhookDeploymentConfigurationError) throw error const err = error as Error logger.error( `[${requestId}] Exception during Resend webhook creation for webhook ${webhook.id}.`, diff --git a/apps/sim/lib/webhooks/providers/revenuecat.ts b/apps/sim/lib/webhooks/providers/revenuecat.ts index 54ed8c5e499..14ff48b7010 100644 --- a/apps/sim/lib/webhooks/providers/revenuecat.ts +++ b/apps/sim/lib/webhooks/providers/revenuecat.ts @@ -3,6 +3,7 @@ import { safeCompare } from '@sim/security/compare' import { generateId } from '@sim/utils/id' import { NextResponse } from 'next/server' import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' +import { WebhookDeploymentConfigurationError } from '@/lib/webhooks/providers/errors' import type { AuthContext, DeleteSubscriptionContext, @@ -81,13 +82,15 @@ export const revenueCatHandler: WebhookProviderHandler = { } if (!apiKey) { - throw new Error( + throw new WebhookDeploymentConfigurationError( 'RevenueCat Secret API key is required to create the webhook. Provide a v2 Secret API key with the project_configuration:integrations:read_write permission.' ) } if (!projectId) { - throw new Error('RevenueCat Project ID is required to create the webhook.') + throw new WebhookDeploymentConfigurationError( + 'RevenueCat Project ID is required to create the webhook.' + ) } const { REVENUECAT_TRIGGER_TO_API_EVENT_TYPE } = await import('@/triggers/revenuecat/utils') diff --git a/apps/sim/lib/webhooks/providers/rootly.ts b/apps/sim/lib/webhooks/providers/rootly.ts index a0f99798541..79bbc026f8a 100644 --- a/apps/sim/lib/webhooks/providers/rootly.ts +++ b/apps/sim/lib/webhooks/providers/rootly.ts @@ -4,6 +4,7 @@ import { hmacSha256Hex } from '@sim/security/hmac' import { generateId } from '@sim/utils/id' import { NextResponse } from 'next/server' import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' +import { WebhookDeploymentConfigurationError } from '@/lib/webhooks/providers/errors' import type { AuthContext, DeleteSubscriptionContext, @@ -144,7 +145,7 @@ export const rootlyHandler: WebhookProviderHandler = { } if (!apiKey) { - throw new Error( + throw new WebhookDeploymentConfigurationError( 'Rootly API key is required. Please provide your Rootly API key in the trigger configuration.' ) } @@ -221,6 +222,7 @@ export const rootlyHandler: WebhookProviderHandler = { ) return { providerConfigUpdates: { externalId, webhookSecret: signingSecret } } } catch (error: unknown) { + if (error instanceof WebhookDeploymentConfigurationError) throw error const err = error as Error logger.error( `[${ctx.requestId}] Exception during Rootly webhook creation for webhook ${ctx.webhook.id}.`, diff --git a/apps/sim/lib/webhooks/providers/telegram.ts b/apps/sim/lib/webhooks/providers/telegram.ts index 41a2e03adc3..afd30d9fa93 100644 --- a/apps/sim/lib/webhooks/providers/telegram.ts +++ b/apps/sim/lib/webhooks/providers/telegram.ts @@ -7,6 +7,7 @@ import { NextResponse } from 'next/server' import { getEffectiveDecryptedEnv } from '@/lib/environment/utils' import { resolveBackgroundWebhookEnv } from '@/lib/webhooks/env-resolver' import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' +import { WebhookDeploymentConfigurationError } from '@/lib/webhooks/providers/errors' import type { AuthContext, DeleteSubscriptionContext, @@ -147,7 +148,7 @@ export const telegramHandler: WebhookProviderHandler = { if (!botToken) { logger.warn(`[${ctx.requestId}] Missing botToken for Telegram webhook ${ctx.webhook.id}`) - throw new Error( + throw new WebhookDeploymentConfigurationError( 'Bot token is required to create a Telegram webhook. Please provide a valid Telegram bot token.' ) } diff --git a/apps/sim/lib/webhooks/providers/typeform.ts b/apps/sim/lib/webhooks/providers/typeform.ts index aa4f2181c02..492e3c83981 100644 --- a/apps/sim/lib/webhooks/providers/typeform.ts +++ b/apps/sim/lib/webhooks/providers/typeform.ts @@ -4,6 +4,7 @@ import { hmacSha256Base64 } from '@sim/security/hmac' import { getErrorMessage } from '@sim/utils/errors' import { toRecord } from '@sim/utils/object' import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' +import { WebhookDeploymentConfigurationError } from '@/lib/webhooks/providers/errors' import type { DeleteSubscriptionContext, FormatInputContext, @@ -74,14 +75,14 @@ export const typeformHandler: WebhookProviderHandler = { if (!formId) { logger.warn(`[${ctx.requestId}] Missing formId for Typeform webhook ${ctx.webhook.id}`) - throw new Error( + throw new WebhookDeploymentConfigurationError( 'Form ID is required to create a Typeform webhook. Please provide a valid form ID.' ) } if (!apiKey) { logger.warn(`[${ctx.requestId}] Missing apiKey for Typeform webhook ${ctx.webhook.id}`) - throw new Error( + throw new WebhookDeploymentConfigurationError( 'Personal Access Token is required to create a Typeform webhook. Please provide your Typeform API key.' ) } diff --git a/apps/sim/lib/webhooks/providers/vercel.ts b/apps/sim/lib/webhooks/providers/vercel.ts index 93319493703..2bafc09f8e3 100644 --- a/apps/sim/lib/webhooks/providers/vercel.ts +++ b/apps/sim/lib/webhooks/providers/vercel.ts @@ -4,6 +4,7 @@ import { safeCompare } from '@sim/security/compare' import { isRecordLike } from '@sim/utils/object' import { NextResponse } from 'next/server' import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' +import { WebhookDeploymentConfigurationError } from '@/lib/webhooks/providers/errors' import type { AuthContext, DeleteSubscriptionContext, @@ -86,7 +87,7 @@ export const vercelHandler: WebhookProviderHandler = { const filterProjectIds = providerConfig.filterProjectIds as string | undefined if (!apiKey) { - throw new Error( + throw new WebhookDeploymentConfigurationError( 'Vercel Access Token is required. Please provide your access token in the trigger configuration.' ) } @@ -199,6 +200,7 @@ export const vercelHandler: WebhookProviderHandler = { }, } } catch (error: unknown) { + if (error instanceof WebhookDeploymentConfigurationError) throw error const err = error as Error logger.error( `[${requestId}] Exception during Vercel webhook creation for webhook ${webhook.id}.`, diff --git a/apps/sim/lib/webhooks/providers/webflow.ts b/apps/sim/lib/webhooks/providers/webflow.ts index dfbea875859..2a806ce70d0 100644 --- a/apps/sim/lib/webhooks/providers/webflow.ts +++ b/apps/sim/lib/webhooks/providers/webflow.ts @@ -7,6 +7,7 @@ import { getNotificationUrl, getProviderConfig, } from '@/lib/webhooks/provider-subscription-utils' +import { WebhookDeploymentConfigurationError } from '@/lib/webhooks/providers/errors' import type { DeleteSubscriptionContext, EventFilterContext, @@ -38,19 +39,23 @@ export const webflowHandler: WebhookProviderHandler = { logger.warn(`[${requestId}] Missing siteId for Webflow webhook creation.`, { webhookId: webhookRecord.id, }) - throw new Error('Site ID is required to create Webflow webhook') + throw new WebhookDeploymentConfigurationError( + 'Site ID is required to create Webflow webhook' + ) } const siteIdValidation = validateAlphanumericId(siteId, 'siteId', 100) if (!siteIdValidation.isValid) { - throw new Error(siteIdValidation.error) + throw new WebhookDeploymentConfigurationError(siteIdValidation.error) } if (!triggerId) { logger.warn(`[${requestId}] Missing triggerId for Webflow webhook creation.`, { webhookId: webhookRecord.id, }) - throw new Error('Trigger type is required to create Webflow webhook') + throw new WebhookDeploymentConfigurationError( + 'Trigger type is required to create Webflow webhook' + ) } const accessToken = credentialId @@ -126,6 +131,7 @@ export const webflowHandler: WebhookProviderHandler = { return { providerConfigUpdates: { externalId: responseBody.id || responseBody._id } } } catch (error: unknown) { + if (error instanceof WebhookDeploymentConfigurationError) throw error const err = error as Error logger.error( `[${requestId}] Exception during Webflow webhook creation for webhook ${webhookRecord.id}.`, diff --git a/apps/sim/lib/webhooks/providers/zendesk.ts b/apps/sim/lib/webhooks/providers/zendesk.ts index 452eec0ef37..c231c654578 100644 --- a/apps/sim/lib/webhooks/providers/zendesk.ts +++ b/apps/sim/lib/webhooks/providers/zendesk.ts @@ -4,6 +4,7 @@ import { safeCompare } from '@sim/security/compare' import { toRecord } from '@sim/utils/object' import { NextResponse } from 'next/server' import { getNotificationUrl, getProviderConfig } from '@/lib/webhooks/provider-subscription-utils' +import { WebhookDeploymentConfigurationError } from '@/lib/webhooks/providers/errors' import type { AuthContext, DeleteSubscriptionContext, @@ -174,14 +175,26 @@ export const zendeskHandler: WebhookProviderHandler = { const apiToken = config.apiToken as string | undefined const triggerId = config.triggerId as string | undefined - if (!subdomain) throw new Error('Zendesk subdomain is required to create the webhook.') + if (!subdomain) { + throw new WebhookDeploymentConfigurationError( + 'Zendesk subdomain is required to create the webhook.' + ) + } if (!isValidZendeskSubdomain(subdomain)) { - throw new Error( + throw new WebhookDeploymentConfigurationError( 'Zendesk subdomain must contain only letters, numbers, and hyphens (e.g. "yourcompany").' ) } - if (!email) throw new Error('Zendesk admin email is required to create the webhook.') - if (!apiToken) throw new Error('Zendesk API token is required to create the webhook.') + if (!email) { + throw new WebhookDeploymentConfigurationError( + 'Zendesk admin email is required to create the webhook.' + ) + } + if (!apiToken) { + throw new WebhookDeploymentConfigurationError( + 'Zendesk API token is required to create the webhook.' + ) + } const { getZendeskSubscriptions } = await import('@/triggers/zendesk/utils') const apiBase = zendeskApiBase(subdomain) diff --git a/apps/sim/lib/workflows/executor/resume-execution.ts b/apps/sim/lib/workflows/executor/resume-execution.ts index 2f523769258..4a4d6e6311b 100644 --- a/apps/sim/lib/workflows/executor/resume-execution.ts +++ b/apps/sim/lib/workflows/executor/resume-execution.ts @@ -8,6 +8,7 @@ import { } from '@/lib/billing/core/attribution' import { getJobQueue, shouldExecuteInline } from '@/lib/core/async-jobs' import type { AsyncExecutionCorrelation } from '@/lib/core/async-jobs/types' +import { logFailureOnce, markFailureKind } from '@/lib/core/errors/failure-log' import { toTriggerMaxDurationSeconds } from '@/lib/core/execution-limits' import type { OrchestrationRequestContext } from '@/lib/core/orchestration/types' import { generateRequestId } from '@/lib/core/utils/request' @@ -434,16 +435,14 @@ export async function executeResumeWorkflow({ return { kind: 'started', executionId: enqueueResult.resumeExecutionId } } catch (error) { if (error instanceof ResumeWorkflowExecutionError) throw error - logger.error( - 'Resume request failed', - projectResolvedSecretDiagnosticError(error, undefined, { - workflowId, - executionId, - contextId, - }) - ) const statusCode = isRecordLike(error) && typeof error.statusCode === 'number' ? error.statusCode : undefined + if (statusCode !== undefined && statusCode < 500) markFailureKind(error, 'user') + logFailureOnce(logger, 'Resume request failed', error, { + executionId, + metadata: () => + projectResolvedSecretDiagnosticError(error, undefined, { workflowId, contextId }), + }) if (statusCode !== undefined) { throw new ResumeWorkflowExecutionError(statusCode, toError(error).message, statusCode < 500) }