Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 10 additions & 2 deletions apps/sim/app/api/cron/renew-subscriptions/route.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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`
Expand Down
4 changes: 2 additions & 2 deletions apps/sim/app/api/webhooks/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/app/api/webhooks/test/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,7 +441,7 @@ export async function GET(request: NextRequest) {
})
}

case 'microsoftteams': {
case 'microsoft-teams': {
const hmacSecret = providerConfig.hmacSecret

if (!hmacSecret) {
Expand Down
30 changes: 2 additions & 28 deletions apps/sim/app/api/webhooks/trigger/[path]/route.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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 })
}
}

Expand Down
4 changes: 3 additions & 1 deletion apps/sim/background/webhook-execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/sim/executor/handlers/trigger/trigger-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
32 changes: 30 additions & 2 deletions apps/sim/lib/idempotency/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down Expand Up @@ -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, string>): string {
static createWebhookIdempotencyKey(
webhookId: string,
headers?: Record<string, string>,
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'] ||
Expand All @@ -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}`
}
}
Expand Down
18 changes: 18 additions & 0 deletions apps/sim/lib/oauth/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('-')
Expand Down
12 changes: 6 additions & 6 deletions apps/sim/lib/webhooks/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ export async function verifyProviderAuth(
const rawProviderConfig = (foundWebhook.providerConfig as Record<string, any>) || {}
const providerConfig = resolveProviderConfigEnvVars(rawProviderConfig, decryptedEnvVars)

if (foundWebhook.provider === 'microsoftteams') {
if (foundWebhook.provider === 'microsoft-teams') {
if (providerConfig.hmacSecret) {
const authHeader = request.headers.get('authorization')

Expand Down Expand Up @@ -556,7 +556,7 @@ export async function checkRateLimits(
traceSpans: [],
})

if (foundWebhook.provider === 'microsoftteams') {
if (foundWebhook.provider === 'microsoft-teams') {
return NextResponse.json(
{
type: 'message',
Expand Down Expand Up @@ -634,7 +634,7 @@ export async function checkUsageLimits(
traceSpans: [],
})

if (foundWebhook.provider === 'microsoftteams') {
if (foundWebhook.provider === 'microsoft-teams') {
return NextResponse.json(
{
type: 'message',
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -835,7 +835,7 @@ export async function queueWebhookExecution(
)
}

if (foundWebhook.provider === 'microsoftteams') {
if (foundWebhook.provider === 'microsoft-teams') {
const providerConfig = (foundWebhook.providerConfig as Record<string, any>) || {}
const triggerId = providerConfig.triggerId as string | undefined

Expand Down Expand Up @@ -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',
Expand Down
85 changes: 85 additions & 0 deletions apps/sim/lib/webhooks/provider-utils.ts
Original file line number Diff line number Diff line change
@@ -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, (body: any) => 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
}
12 changes: 6 additions & 6 deletions apps/sim/lib/webhooks/utils.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -397,7 +397,7 @@ async function formatTeamsGraphNotification(
},
webhook: {
data: {
provider: 'microsoftteams',
provider: 'microsoft-teams',
path: foundWebhook?.path || '',
providerConfig: foundWebhook?.providerConfig || {},
payload: body,
Expand Down Expand Up @@ -446,7 +446,7 @@ async function formatTeamsGraphNotification(
},
webhook: {
data: {
provider: 'microsoftteams',
provider: 'microsoft-teams',
path: foundWebhook?.path || '',
providerConfig: foundWebhook?.providerConfig || {},
payload: body,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -875,7 +875,7 @@ export async function formatWebhookInput(

webhook: {
data: {
provider: 'microsoftteams',
provider: 'microsoft-teams',
path: foundWebhook.path,
providerConfig: foundWebhook.providerConfig,
payload: body,
Expand Down Expand Up @@ -1653,7 +1653,7 @@ export function verifyProviderWebhook(

break
}
case 'microsoftteams':
case 'microsoft-teams':
break
case 'generic':
if (providerConfig.requireAuth) {
Expand Down
Loading