diff --git a/apps/sim/app/api/cron/renew-subscriptions/route.ts b/apps/sim/app/api/cron/renew-subscriptions/route.ts index 2f58f5faa3..501fdfdc43 100644 --- a/apps/sim/app/api/cron/renew-subscriptions/route.ts +++ b/apps/sim/app/api/cron/renew-subscriptions/route.ts @@ -1,6 +1,6 @@ import { db } from '@sim/db' import { webhook as webhookTable, workflow as workflowTable } from '@sim/db/schema' -import { and, eq } from 'drizzle-orm' +import { and, eq, or } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { verifyCronAuth } from '@/lib/auth/internal' import { createLogger } from '@/lib/logs/console/logger' @@ -35,7 +35,15 @@ export async function GET(request: NextRequest) { }) .from(webhookTable) .innerJoin(workflowTable, eq(webhookTable.workflowId, workflowTable.id)) - .where(and(eq(webhookTable.isActive, true), eq(webhookTable.provider, 'microsoftteams'))) + .where( + and( + eq(webhookTable.isActive, true), + or( + eq(webhookTable.provider, 'microsoft-teams'), + eq(webhookTable.provider, 'microsoftteams') + ) + ) + ) logger.info( `Found ${webhooksWithWorkflows.length} active Teams webhooks, checking for expiring subscriptions` diff --git a/apps/sim/app/api/webhooks/route.ts b/apps/sim/app/api/webhooks/route.ts index 86fd3ff644..5f0d58a27e 100644 --- a/apps/sim/app/api/webhooks/route.ts +++ b/apps/sim/app/api/webhooks/route.ts @@ -137,7 +137,7 @@ export async function POST(request: NextRequest) { const isCredentialBased = credentialBasedProviders.includes(provider) // Treat Microsoft Teams chat subscription as credential-based for path generation purposes const isMicrosoftTeamsChatSubscription = - provider === 'microsoftteams' && + provider === 'microsoft-teams' && typeof providerConfig === 'object' && providerConfig?.triggerId === 'microsoftteams_chat_subscription' @@ -297,7 +297,7 @@ export async function POST(request: NextRequest) { } } - if (provider === 'microsoftteams') { + if (provider === 'microsoft-teams') { const { createTeamsSubscription } = await import('@/lib/webhooks/webhook-helpers') logger.info(`[${requestId}] Creating Teams subscription before saving to database`) try { diff --git a/apps/sim/app/api/webhooks/test/route.ts b/apps/sim/app/api/webhooks/test/route.ts index 8467f53063..3de2e19b1b 100644 --- a/apps/sim/app/api/webhooks/test/route.ts +++ b/apps/sim/app/api/webhooks/test/route.ts @@ -441,7 +441,7 @@ export async function GET(request: NextRequest) { }) } - case 'microsoftteams': { + case 'microsoft-teams': { const hmacSecret = providerConfig.hmacSecret if (!hmacSecret) { diff --git a/apps/sim/app/api/webhooks/trigger/[path]/route.ts b/apps/sim/app/api/webhooks/trigger/[path]/route.ts index 4f8704489d..0de3c1a632 100644 --- a/apps/sim/app/api/webhooks/trigger/[path]/route.ts +++ b/apps/sim/app/api/webhooks/trigger/[path]/route.ts @@ -1,7 +1,5 @@ import { type NextRequest, NextResponse } from 'next/server' -import { v4 as uuidv4 } from 'uuid' import { createLogger } from '@/lib/logs/console/logger' -import { LoggingSession } from '@/lib/logs/execution/logging-session' import { generateRequestId } from '@/lib/utils' import { checkRateLimits, @@ -139,34 +137,10 @@ export async function POST( if (foundWebhook.blockId) { const blockExists = await blockExistsInDeployment(foundWorkflow.id, foundWebhook.blockId) if (!blockExists) { - logger.warn( + logger.info( `[${requestId}] Trigger block ${foundWebhook.blockId} not found in deployment for workflow ${foundWorkflow.id}` ) - - const executionId = uuidv4() - const loggingSession = new LoggingSession(foundWorkflow.id, executionId, 'webhook', requestId) - - const actorUserId = foundWorkflow.workspaceId - ? (await import('@/lib/workspaces/utils')).getWorkspaceBilledAccountUserId( - foundWorkflow.workspaceId - ) || foundWorkflow.userId - : foundWorkflow.userId - - await loggingSession.safeStart({ - userId: actorUserId, - workspaceId: foundWorkflow.workspaceId || '', - variables: {}, - }) - - await loggingSession.safeCompleteWithError({ - error: { - message: `Trigger block not deployed. The webhook trigger (block ${foundWebhook.blockId}) is not present in the deployed workflow. Please redeploy the workflow.`, - stackTrace: undefined, - }, - traceSpans: [], - }) - - return new NextResponse('Trigger block not deployed', { status: 404 }) + return new NextResponse('Trigger block not found in deployment', { status: 404 }) } } diff --git a/apps/sim/background/webhook-execution.ts b/apps/sim/background/webhook-execution.ts index 14e8ca40e2..7698616b59 100644 --- a/apps/sim/background/webhook-execution.ts +++ b/apps/sim/background/webhook-execution.ts @@ -112,7 +112,9 @@ export async function executeWebhookJob(payload: WebhookExecutionPayload) { const idempotencyKey = IdempotencyService.createWebhookIdempotencyKey( payload.webhookId, - payload.headers + payload.headers, + payload.body, + payload.provider ) const runOperation = async () => { diff --git a/apps/sim/executor/handlers/trigger/trigger-handler.ts b/apps/sim/executor/handlers/trigger/trigger-handler.ts index 85fbffb4ab..1b53cd8641 100644 --- a/apps/sim/executor/handlers/trigger/trigger-handler.ts +++ b/apps/sim/executor/handlers/trigger/trigger-handler.ts @@ -55,7 +55,7 @@ export class TriggerBlockHandler implements BlockHandler { } } - if (provider === 'microsoftteams') { + if (provider === 'microsoft-teams') { const providerData = (starterOutput as any)[provider] || webhookData[provider] || {} const payloadSource = providerData?.message?.raw || webhookData.payload || {} return { diff --git a/apps/sim/lib/idempotency/service.ts b/apps/sim/lib/idempotency/service.ts index e20dc6dbfe..31495c2a99 100644 --- a/apps/sim/lib/idempotency/service.ts +++ b/apps/sim/lib/idempotency/service.ts @@ -4,6 +4,7 @@ import { idempotencyKey } from '@sim/db/schema' import { and, eq } from 'drizzle-orm' import { createLogger } from '@/lib/logs/console/logger' import { getRedisClient } from '@/lib/redis' +import { extractProviderIdentifierFromBody } from '@/lib/webhooks/provider-utils' const logger = createLogger('IdempotencyService') @@ -451,13 +452,25 @@ export class IdempotencyService { /** * Create an idempotency key from a webhook payload following RFC best practices - * Standard webhook headers (webhook-id, x-webhook-id, etc.) + * Checks both headers and body for unique identifiers to prevent duplicate executions + * + * @param webhookId - The webhook database ID + * @param headers - HTTP headers from the webhook request + * @param body - Parsed webhook body (optional, used for provider-specific identifiers) + * @param provider - Provider name for body extraction (optional) + * @returns A unique idempotency key for this webhook event */ - static createWebhookIdempotencyKey(webhookId: string, headers?: Record): string { + static createWebhookIdempotencyKey( + webhookId: string, + headers?: Record, + body?: any, + provider?: string + ): string { const normalizedHeaders = headers ? Object.fromEntries(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v])) : undefined + // Check standard webhook headers first const webhookIdHeader = normalizedHeaders?.['webhook-id'] || normalizedHeaders?.['x-webhook-id'] || @@ -470,7 +483,22 @@ export class IdempotencyService { return `${webhookId}:${webhookIdHeader}` } + // Check body for provider-specific unique identifiers + if (body && provider) { + const bodyIdentifier = extractProviderIdentifierFromBody(provider, body) + + if (bodyIdentifier) { + return `${webhookId}:${bodyIdentifier}` + } + } + + // No unique identifier found - generate random UUID + // This means duplicate detection will not work for this webhook const uniqueId = randomUUID() + logger.warn('No unique identifier found, duplicate executions may occur', { + webhookId, + provider, + }) return `${webhookId}:${uniqueId}` } } diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index d26cfa7f16..ff131effc8 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -906,6 +906,24 @@ export function parseProvider(provider: OAuthProvider): ProviderConfig { featureType: 'sharepoint', } } + if (provider === 'microsoft-teams' || provider === 'microsoftteams') { + return { + baseProvider: 'microsoft', + featureType: 'microsoft-teams', + } + } + if (provider === 'microsoft-excel') { + return { + baseProvider: 'microsoft', + featureType: 'microsoft-excel', + } + } + if (provider === 'microsoft-planner') { + return { + baseProvider: 'microsoft', + featureType: 'microsoft-planner', + } + } // Handle compound providers (e.g., 'google-email' -> { baseProvider: 'google', featureType: 'email' }) const [base, feature] = provider.split('-') diff --git a/apps/sim/lib/webhooks/processor.ts b/apps/sim/lib/webhooks/processor.ts index 1b76610096..5d58b36d06 100644 --- a/apps/sim/lib/webhooks/processor.ts +++ b/apps/sim/lib/webhooks/processor.ts @@ -250,7 +250,7 @@ export async function verifyProviderAuth( const rawProviderConfig = (foundWebhook.providerConfig as Record) || {} const providerConfig = resolveProviderConfigEnvVars(rawProviderConfig, decryptedEnvVars) - if (foundWebhook.provider === 'microsoftteams') { + if (foundWebhook.provider === 'microsoft-teams') { if (providerConfig.hmacSecret) { const authHeader = request.headers.get('authorization') @@ -556,7 +556,7 @@ export async function checkRateLimits( traceSpans: [], }) - if (foundWebhook.provider === 'microsoftteams') { + if (foundWebhook.provider === 'microsoft-teams') { return NextResponse.json( { type: 'message', @@ -634,7 +634,7 @@ export async function checkUsageLimits( traceSpans: [], }) - if (foundWebhook.provider === 'microsoftteams') { + if (foundWebhook.provider === 'microsoft-teams') { return NextResponse.json( { type: 'message', @@ -783,7 +783,7 @@ export async function queueWebhookExecution( // For Microsoft Teams Graph notifications, extract unique identifiers for idempotency if ( - foundWebhook.provider === 'microsoftteams' && + foundWebhook.provider === 'microsoft-teams' && body?.value && Array.isArray(body.value) && body.value.length > 0 @@ -835,7 +835,7 @@ export async function queueWebhookExecution( ) } - if (foundWebhook.provider === 'microsoftteams') { + if (foundWebhook.provider === 'microsoft-teams') { const providerConfig = (foundWebhook.providerConfig as Record) || {} const triggerId = providerConfig.triggerId as string | undefined @@ -886,7 +886,7 @@ export async function queueWebhookExecution( } catch (error: any) { logger.error(`[${options.requestId}] Failed to queue webhook execution:`, error) - if (foundWebhook.provider === 'microsoftteams') { + if (foundWebhook.provider === 'microsoft-teams') { return NextResponse.json( { type: 'message', diff --git a/apps/sim/lib/webhooks/provider-utils.ts b/apps/sim/lib/webhooks/provider-utils.ts new file mode 100644 index 0000000000..2ae0681e94 --- /dev/null +++ b/apps/sim/lib/webhooks/provider-utils.ts @@ -0,0 +1,85 @@ +/** + * Provider-specific unique identifier extractors for webhook idempotency + */ + +function extractSlackIdentifier(body: any): string | null { + if (body.event_id) { + return body.event_id + } + + if (body.event?.ts && body.team_id) { + return `${body.team_id}:${body.event.ts}` + } + + return null +} + +function extractTwilioIdentifier(body: any): string | null { + return body.MessageSid || body.CallSid || null +} + +function extractStripeIdentifier(body: any): string | null { + if (body.id && body.object === 'event') { + return body.id + } + return null +} + +function extractHubSpotIdentifier(body: any): string | null { + if (Array.isArray(body) && body.length > 0 && body[0]?.eventId) { + return String(body[0].eventId) + } + return null +} + +function extractLinearIdentifier(body: any): string | null { + if (body.action && body.data?.id) { + return `${body.action}:${body.data.id}` + } + return null +} + +function extractJiraIdentifier(body: any): string | null { + if (body.webhookEvent && (body.issue?.id || body.project?.id)) { + return `${body.webhookEvent}:${body.issue?.id || body.project?.id}` + } + return null +} + +function extractMicrosoftTeamsIdentifier(body: any): string | null { + if (body.value && Array.isArray(body.value) && body.value.length > 0) { + const notification = body.value[0] + if (notification.subscriptionId && notification.resourceData?.id) { + return `${notification.subscriptionId}:${notification.resourceData.id}` + } + } + return null +} + +function extractAirtableIdentifier(body: any): string | null { + if (body.cursor && typeof body.cursor === 'string') { + return body.cursor + } + return null +} + +const PROVIDER_EXTRACTORS: Record string | null> = { + slack: extractSlackIdentifier, + twilio: extractTwilioIdentifier, + twilio_voice: extractTwilioIdentifier, + stripe: extractStripeIdentifier, + hubspot: extractHubSpotIdentifier, + linear: extractLinearIdentifier, + jira: extractJiraIdentifier, + 'microsoft-teams': extractMicrosoftTeamsIdentifier, + airtable: extractAirtableIdentifier, +} + +export function extractProviderIdentifierFromBody(provider: string, body: any): string | null { + if (!body || typeof body !== 'object') { + return null + } + + const extractor = PROVIDER_EXTRACTORS[provider] + return extractor ? extractor(body) : null +} diff --git a/apps/sim/lib/webhooks/utils.server.ts b/apps/sim/lib/webhooks/utils.server.ts index d21867e189..47aacdba32 100644 --- a/apps/sim/lib/webhooks/utils.server.ts +++ b/apps/sim/lib/webhooks/utils.server.ts @@ -133,7 +133,7 @@ async function formatTeamsGraphNotification( input: 'Teams notification received', webhook: { data: { - provider: 'microsoftteams', + provider: 'microsoft-teams', path: foundWebhook?.path || '', providerConfig: foundWebhook?.providerConfig || {}, payload: body, @@ -397,7 +397,7 @@ async function formatTeamsGraphNotification( }, webhook: { data: { - provider: 'microsoftteams', + provider: 'microsoft-teams', path: foundWebhook?.path || '', providerConfig: foundWebhook?.providerConfig || {}, payload: body, @@ -446,7 +446,7 @@ async function formatTeamsGraphNotification( }, webhook: { data: { - provider: 'microsoftteams', + provider: 'microsoft-teams', path: foundWebhook?.path || '', providerConfig: foundWebhook?.providerConfig || {}, payload: body, @@ -818,7 +818,7 @@ export async function formatWebhookInput( } } - if (foundWebhook.provider === 'microsoftteams') { + if (foundWebhook.provider === 'microsoft-teams') { // Check if this is a Microsoft Graph change notification if (body?.value && Array.isArray(body.value) && body.value.length > 0) { return await formatTeamsGraphNotification(body, foundWebhook, foundWorkflow, request) @@ -875,7 +875,7 @@ export async function formatWebhookInput( webhook: { data: { - provider: 'microsoftteams', + provider: 'microsoft-teams', path: foundWebhook.path, providerConfig: foundWebhook.providerConfig, payload: body, @@ -1653,7 +1653,7 @@ export function verifyProviderWebhook( break } - case 'microsoftteams': + case 'microsoft-teams': break case 'generic': if (providerConfig.requireAuth) { diff --git a/apps/sim/lib/webhooks/webhook-helpers.ts b/apps/sim/lib/webhooks/webhook-helpers.ts index c1ccf976b7..9d9d6775a1 100644 --- a/apps/sim/lib/webhooks/webhook-helpers.ts +++ b/apps/sim/lib/webhooks/webhook-helpers.ts @@ -623,7 +623,7 @@ export async function cleanupExternalWebhook( ): Promise { if (webhook.provider === 'airtable') { await deleteAirtableWebhook(webhook, workflow, requestId) - } else if (webhook.provider === 'microsoftteams') { + } else if (webhook.provider === 'microsoft-teams') { await deleteTeamsSubscription(webhook, workflow, requestId) } else if (webhook.provider === 'telegram') { await deleteTelegramWebhook(webhook, requestId)