mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
fix(triggers): dedup + not surfacing deployment status log (#2033)
* fix(triggers): dedup + not surfacing deployment status log
* fix ms teams
* change to microsoftteams
* Revert "change to microsoftteams"
This reverts commit 217f808641.
* fix
* fix
* fix provider name
* fix oauth for msteams
This commit is contained in:
committed by
GitHub
parent
00d9b45a22
commit
98908dbfb9
@@ -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`
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -441,7 +441,7 @@ export async function GET(request: NextRequest) {
|
||||
})
|
||||
}
|
||||
|
||||
case 'microsoftteams': {
|
||||
case 'microsoft-teams': {
|
||||
const hmacSecret = providerConfig.hmacSecret
|
||||
|
||||
if (!hmacSecret) {
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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, 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'] ||
|
||||
@@ -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}`
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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('-')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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<string, any>) || {}
|
||||
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',
|
||||
|
||||
85
apps/sim/lib/webhooks/provider-utils.ts
Normal file
85
apps/sim/lib/webhooks/provider-utils.ts
Normal file
@@ -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
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -623,7 +623,7 @@ export async function cleanupExternalWebhook(
|
||||
): Promise<void> {
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user