Files
simstudioai--sim/apps/sim/lib/webhooks/providers/microsoft-teams.ts
T
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

870 lines
30 KiB
TypeScript

import { db } from '@sim/db'
import { account } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { safeCompare } from '@sim/security/compare'
import { hmacSha256Base64 } from '@sim/security/hmac'
import { toError } from '@sim/utils/errors'
import { isRecordLike } from '@sim/utils/object'
import { eq } from 'drizzle-orm'
import { type NextRequest, NextResponse } from 'next/server'
import { isMicrosoftContentUrl } from '@/lib/core/security/input-validation'
import {
type SecureFetchResponse,
secureFetchWithPinnedIP,
validateUrlWithDNS,
} from '@/lib/core/security/input-validation.server'
import { sanitizeUrlForLog } from '@/lib/core/utils/logging'
import {
getCredentialOwner,
getNotificationUrl,
getProviderConfig,
} from '@/lib/webhooks/provider-subscription-utils'
import type {
AuthContext,
DeleteSubscriptionContext,
EventFilterContext,
FormatInputContext,
FormatInputResult,
SubscriptionContext,
SubscriptionResult,
WebhookProviderHandler,
} from '@/lib/webhooks/providers/types'
import { refreshAccessTokenIfNeeded, resolveOAuthAccountId } from '@/app/api/auth/oauth/utils'
const logger = createLogger('WebhookProvider:MicrosoftTeams')
function validateMicrosoftTeamsSignature(
hmacSecret: string,
signature: string,
body: string
): boolean {
try {
if (!hmacSecret || !signature || !body) {
return false
}
if (!signature.startsWith('HMAC ')) {
return false
}
const providedSignature = signature.substring(5)
const secretBytes = Buffer.from(hmacSecret, 'base64')
const computedHash = hmacSha256Base64(body, secretBytes)
return safeCompare(computedHash, providedSignature)
} catch (error) {
logger.error('Error validating Microsoft Teams signature:', error)
return false
}
}
/**
* Derive a stable per-delivery identifier from a Teams webhook body.
*
* Graph change notifications (chat subscriptions) are keyed by
* `subscriptionId:messageId`. Outgoing webhook activities (channel
* @mentions) carry no `value` array — they're keyed by the Bot Framework
* Activity's own unique `id` instead.
*/
function extractNotificationKey(body: unknown): string | null {
if (!isRecordLike(body)) {
return null
}
const value = body.value
if (Array.isArray(value) && value.length > 0) {
const notification = value[0]
if (!isRecordLike(notification)) {
return null
}
const subscriptionId = notification.subscriptionId
const resourceData = notification.resourceData
const messageId = isRecordLike(resourceData) ? resourceData.id : undefined
if (typeof subscriptionId === 'string' && typeof messageId === 'string') {
return `${subscriptionId}:${messageId}`
}
return null
}
const activityId = body.id
return typeof activityId === 'string' && activityId ? activityId : null
}
async function fetchWithDNSPinning(
url: string,
accessToken: string,
requestId: string
): Promise<SecureFetchResponse | null> {
try {
const urlValidation = await validateUrlWithDNS(url, 'contentUrl')
if (!urlValidation.isValid) {
logger.warn(`[${requestId}] Invalid content URL: ${urlValidation.error}`, { url })
return null
}
const headers: Record<string, string> = {}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
const response = await secureFetchWithPinnedIP(url, urlValidation.resolvedIP!, { headers })
return response
} catch (error) {
logger.error(`[${requestId}] Error fetching URL with DNS pinning`, {
error: toError(error).message,
url: sanitizeUrlForLog(url),
})
return null
}
}
/**
* Format Microsoft Teams Graph change notification
*/
async function formatTeamsGraphNotification(
body: Record<string, unknown>,
foundWebhook: Record<string, unknown>,
foundWorkflow: { id: string; userId: string }
): Promise<unknown> {
const notification = (body.value as unknown[])?.[0] as Record<string, unknown> | undefined
if (!notification) {
logger.warn('Received empty Teams notification body')
return null
}
const changeType = (notification.changeType as string) || 'created'
const resource = (notification.resource as string) || ''
const subscriptionId = (notification.subscriptionId as string) || ''
let chatId: string | null = null
let messageId: string | null = null
const fullMatch = resource.match(/chats\/([^/]+)\/messages\/([^/]+)/)
if (fullMatch) {
chatId = fullMatch[1]
messageId = fullMatch[2]
}
if (!chatId || !messageId) {
const quotedMatch = resource.match(/chats\('([^']+)'\)\/messages\('([^']+)'\)/)
if (quotedMatch) {
chatId = quotedMatch[1]
messageId = quotedMatch[2]
}
}
if (!chatId || !messageId) {
const collectionMatch = resource.match(/chats\/([^/]+)\/messages$/)
const rdId = ((body?.value as unknown[])?.[0] as Record<string, unknown>)?.resourceData as
| Record<string, unknown>
| undefined
const rdIdValue = rdId?.id as string | undefined
if (collectionMatch && rdIdValue) {
chatId = collectionMatch[1]
messageId = rdIdValue
}
}
if (
(!chatId || !messageId) &&
((body?.value as unknown[])?.[0] as Record<string, unknown>)?.resourceData
) {
const resourceData = ((body.value as unknown[])[0] as Record<string, unknown>)
.resourceData as Record<string, unknown>
const odataId = resourceData['@odata.id']
if (typeof odataId === 'string') {
const odataMatch = odataId.match(/chats\('([^']+)'\)\/messages\('([^']+)'\)/)
if (odataMatch) {
chatId = odataMatch[1]
messageId = odataMatch[2]
}
}
}
if (!chatId || !messageId) {
logger.warn('Could not resolve chatId/messageId from Teams notification', {
resource,
hasResourceDataId: Boolean(
((body?.value as unknown[])?.[0] as Record<string, unknown>)?.resourceData
),
valueLength: Array.isArray(body?.value) ? (body.value as unknown[]).length : 0,
keys: Object.keys(body || {}),
})
return {
from: null,
message: { raw: body },
activity: body,
conversation: null,
}
}
const resolvedChatId = chatId as string
const resolvedMessageId = messageId as string
const providerConfig = (foundWebhook?.providerConfig as Record<string, unknown>) || {}
const credentialId = providerConfig.credentialId
const includeAttachments = providerConfig.includeAttachments !== false
let message: Record<string, unknown> | null = null
const rawAttachments: Array<{ name: string; data: Buffer; contentType: string; size: number }> =
[]
let accessToken: string | null = null
if (!credentialId) {
logger.error('Missing credentialId for Teams chat subscription', {
chatId: resolvedChatId,
messageId: resolvedMessageId,
webhookId: foundWebhook?.id,
blockId: foundWebhook?.blockId,
providerConfig,
})
} else {
try {
const resolved = await resolveOAuthAccountId(credentialId as string)
if (!resolved) {
logger.error('Teams credential could not be resolved', { credentialId })
} else {
const rows = await db
.select()
.from(account)
.where(eq(account.id, resolved.accountId))
.limit(1)
if (rows.length === 0) {
logger.error('Teams credential not found', { credentialId, chatId: resolvedChatId })
} else {
const effectiveUserId = rows[0].userId
accessToken = await refreshAccessTokenIfNeeded(
resolved.accountId,
effectiveUserId,
'teams-graph-notification'
)
}
}
if (accessToken) {
const msgUrl = `https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(resolvedChatId)}/messages/${encodeURIComponent(resolvedMessageId)}`
const res = await fetch(msgUrl, { headers: { Authorization: `Bearer ${accessToken}` } })
if (res.ok) {
message = (await res.json()) as Record<string, unknown>
if (includeAttachments && (message?.attachments as unknown[] | undefined)?.length) {
const attachments = Array.isArray(message?.attachments)
? (message.attachments as Record<string, unknown>[])
: []
for (const att of attachments) {
try {
const contentUrl =
typeof att?.contentUrl === 'string' ? (att.contentUrl as string) : undefined
const contentTypeHint =
typeof att?.contentType === 'string' ? (att.contentType as string) : undefined
let attachmentName = (att?.name as string) || 'teams-attachment'
if (!contentUrl) continue
let parsedContentUrl: URL
try {
parsedContentUrl = new URL(contentUrl)
} catch {
continue
}
const contentHost = parsedContentUrl.hostname.toLowerCase()
let buffer: Buffer | null = null
let mimeType = 'application/octet-stream'
const isOneDriveShareLink =
contentHost === '1drv.ms' ||
contentHost === '1drv.com' ||
contentHost === 'microsoftpersonalcontent.com' ||
contentHost.endsWith('.microsoftpersonalcontent.com')
if (isMicrosoftContentUrl(contentUrl) && !isOneDriveShareLink) {
try {
const directRes = await fetchWithDNSPinning(
contentUrl,
accessToken,
'teams-attachment'
)
if (directRes?.ok) {
const arrayBuffer = await directRes.arrayBuffer()
buffer = Buffer.from(arrayBuffer)
mimeType =
directRes.headers.get('content-type') ||
contentTypeHint ||
'application/octet-stream'
} else if (directRes) {
const encodedUrl = Buffer.from(contentUrl)
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '')
const graphUrl = `https://graph.microsoft.com/v1.0/shares/u!${encodedUrl}/driveItem/content`
const graphRes = await fetch(graphUrl, {
headers: { Authorization: `Bearer ${accessToken}` },
redirect: 'follow',
})
if (graphRes.ok) {
const arrayBuffer = await graphRes.arrayBuffer()
buffer = Buffer.from(arrayBuffer)
mimeType =
graphRes.headers.get('content-type') ||
contentTypeHint ||
'application/octet-stream'
} else {
continue
}
}
} catch {
continue
}
} else if (isOneDriveShareLink) {
try {
let shareToken: string | null = null
if (contentHost === '1drv.ms') {
const lastSegment = parsedContentUrl.pathname.split('/').pop()
if (lastSegment) shareToken = lastSegment
} else if (parsedContentUrl.searchParams.has('resid')) {
shareToken = parsedContentUrl.searchParams.get('resid')
}
if (!shareToken) {
const base64Url = Buffer.from(contentUrl, 'utf-8')
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '')
shareToken = `u!${base64Url}`
} else if (!shareToken.startsWith('u!')) {
const base64Url = Buffer.from(shareToken, 'utf-8')
.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=+$/, '')
shareToken = `u!${base64Url}`
}
const metadataUrl = `https://graph.microsoft.com/v1.0/shares/${shareToken}/driveItem`
const metadataRes = await fetch(metadataUrl, {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
})
if (!metadataRes.ok) {
const directUrl = `https://graph.microsoft.com/v1.0/shares/${shareToken}/driveItem/content`
const directRes = await fetch(directUrl, {
headers: { Authorization: `Bearer ${accessToken}` },
redirect: 'follow',
})
if (directRes.ok) {
const arrayBuffer = await directRes.arrayBuffer()
buffer = Buffer.from(arrayBuffer)
mimeType =
directRes.headers.get('content-type') ||
contentTypeHint ||
'application/octet-stream'
} else {
continue
}
} else {
const metadata = (await metadataRes.json()) as Record<string, unknown>
const downloadUrl = metadata['@microsoft.graph.downloadUrl'] as
| string
| undefined
if (downloadUrl) {
const downloadRes = await fetchWithDNSPinning(
downloadUrl,
'',
'teams-onedrive-download'
)
if (downloadRes?.ok) {
const arrayBuffer = await downloadRes.arrayBuffer()
buffer = Buffer.from(arrayBuffer)
const fileInfo = metadata.file as Record<string, unknown> | undefined
mimeType =
downloadRes.headers.get('content-type') ||
(fileInfo?.mimeType as string | undefined) ||
contentTypeHint ||
'application/octet-stream'
if (metadata.name && metadata.name !== attachmentName) {
attachmentName = metadata.name as string
}
} else {
continue
}
} else {
continue
}
}
} catch {
continue
}
} else {
try {
const ares = await fetchWithDNSPinning(
contentUrl,
accessToken,
'teams-attachment-generic'
)
if (ares?.ok) {
const arrayBuffer = await ares.arrayBuffer()
buffer = Buffer.from(arrayBuffer)
mimeType =
ares.headers.get('content-type') ||
contentTypeHint ||
'application/octet-stream'
}
} catch {
continue
}
}
if (!buffer) continue
const size = buffer.length
rawAttachments.push({
name: attachmentName,
data: buffer,
contentType: mimeType,
size,
})
} catch {
/* skip attachment on error */
}
}
}
}
}
} catch (error) {
logger.error('Failed to fetch Teams message', {
error,
chatId: resolvedChatId,
messageId: resolvedMessageId,
})
}
}
if (!message) {
logger.warn('No message data available for Teams notification', {
chatId: resolvedChatId,
messageId: resolvedMessageId,
hasCredential: !!credentialId,
})
return {
message_id: resolvedMessageId,
chat_id: resolvedChatId,
from_name: '',
text: '',
created_at: '',
attachments: [],
}
}
const messageText = (message.body as Record<string, unknown>)?.content || ''
const from = ((message.from as Record<string, unknown>)?.user as Record<string, unknown>) || {}
const createdAt = (message.createdDateTime as string) || ''
return {
message_id: resolvedMessageId,
chat_id: resolvedChatId,
from_name: (from.displayName as string) || '',
text: messageText,
created_at: createdAt,
attachments: rawAttachments,
}
}
export const microsoftTeamsHandler: WebhookProviderHandler = {
handleChallenge(_body: unknown, request: NextRequest, requestId: string, path: string) {
const url = new URL(request.url)
const validationToken = url.searchParams.get('validationToken')
if (validationToken) {
logger.info(`[${requestId}] Microsoft Graph subscription validation for path: ${path}`)
return new NextResponse(validationToken, {
status: 200,
headers: { 'Content-Type': 'text/plain' },
})
}
return null
},
verifyAuth({ webhook, request, rawBody, requestId, providerConfig }: AuthContext) {
if (providerConfig.triggerId !== 'microsoftteams_chat_subscription') {
const hmacSecret = providerConfig.hmacSecret as string | undefined
if (!hmacSecret) {
logger.error(
`[${requestId}] Microsoft Teams outgoing webhook missing configured HMAC secret`
)
return new NextResponse('Unauthorized - Missing HMAC secret configuration', {
status: 401,
})
}
const authHeader = request.headers.get('authorization')
if (!authHeader || !authHeader.startsWith('HMAC ')) {
logger.warn(
`[${requestId}] Microsoft Teams outgoing webhook missing HMAC authorization header`
)
return new NextResponse('Unauthorized - Missing HMAC signature', { status: 401 })
}
if (!validateMicrosoftTeamsSignature(hmacSecret, authHeader, rawBody)) {
logger.warn(`[${requestId}] Microsoft Teams HMAC signature verification failed`)
return new NextResponse('Unauthorized - Invalid HMAC signature', { status: 401 })
}
}
if (providerConfig.triggerId === 'microsoftteams_chat_subscription') {
const expectedClientState = String(webhook.id ?? '')
if (!expectedClientState) {
logger.warn(
`[${requestId}] Microsoft Teams chat subscription webhook missing id for clientState verification`
)
return new NextResponse('Unauthorized - Invalid clientState', { status: 401 })
}
let notifications: unknown[] = []
try {
const parsed = JSON.parse(rawBody) as Record<string, unknown>
if (Array.isArray(parsed?.value)) {
notifications = parsed.value
}
} catch {
notifications = []
}
if (notifications.length === 0) {
logger.warn(
`[${requestId}] Microsoft Teams chat subscription notification missing value array`
)
return new NextResponse('Unauthorized - Invalid notification payload', { status: 401 })
}
for (const notification of notifications) {
const clientState = (notification as Record<string, unknown>)?.clientState
if (typeof clientState !== 'string' || !safeCompare(clientState, expectedClientState)) {
logger.warn(
`[${requestId}] Microsoft Teams chat subscription clientState verification failed`
)
return new NextResponse('Unauthorized - Invalid clientState', { status: 401 })
}
}
}
return null
},
formatErrorResponse(error: string, status: number) {
return NextResponse.json({ type: 'message', text: error }, { status })
},
enrichHeaders({ body }: EventFilterContext, headers: Record<string, string>) {
const key = extractNotificationKey(body)
if (key) {
headers['x-teams-notification-id'] = key
}
},
extractIdempotencyId(body: unknown) {
return extractNotificationKey(body)
},
formatSuccessResponse(providerConfig: Record<string, unknown>) {
if (providerConfig.triggerId === 'microsoftteams_chat_subscription') {
return new NextResponse(null, { status: 202 })
}
return NextResponse.json({ type: 'message', text: 'Sim' })
},
formatQueueErrorResponse() {
return NextResponse.json(
{ type: 'message', text: 'Webhook processing failed' },
{ status: 500 }
)
},
async createSubscription({
webhook,
workflow,
userId,
requestId,
request,
}: SubscriptionContext): Promise<SubscriptionResult | undefined> {
const config = getProviderConfig(webhook)
if (config.triggerId !== 'microsoftteams_chat_subscription') {
return undefined
}
const credentialId = config.credentialId as string | undefined
const chatId = config.chatId as string | undefined
if (!credentialId) {
logger.warn(`[${requestId}] Missing credentialId for Teams chat subscription ${webhook.id}`)
throw new Error(
'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(
'Chat ID is required to create a Teams subscription. Please provide a valid chat ID.'
)
}
const credentialOwner = await getCredentialOwner(credentialId, requestId)
const accessToken = credentialOwner
? await refreshAccessTokenIfNeeded(
credentialOwner.accountId,
credentialOwner.userId,
requestId
)
: null
if (!accessToken) {
logger.error(`[${requestId}] Failed to get access token for Teams subscription ${webhook.id}`)
throw new Error(
'Failed to authenticate with Microsoft Teams. Please reconnect your Microsoft account and try again.'
)
}
const existingSubscriptionId = config.externalSubscriptionId as string | undefined
if (existingSubscriptionId) {
try {
const checkRes = await fetch(
`https://graph.microsoft.com/v1.0/subscriptions/${existingSubscriptionId}`,
{ method: 'GET', headers: { Authorization: `Bearer ${accessToken}` } }
)
if (checkRes.ok) {
const existingPayload = await checkRes.json()
logger.info(
`[${requestId}] Teams subscription ${existingSubscriptionId} already exists for webhook ${webhook.id}`
)
return {
providerConfigUpdates: {
externalSubscriptionId: existingSubscriptionId,
subscriptionExpiration: existingPayload.expirationDateTime,
},
}
}
} catch {
logger.debug(`[${requestId}] Existing subscription check failed, will create new one`)
}
}
const notificationUrl = getNotificationUrl(webhook)
const resource = `/chats/${chatId}/messages`
const maxLifetimeMinutes = 4230
const expirationDateTime = new Date(Date.now() + maxLifetimeMinutes * 60 * 1000).toISOString()
const body = {
changeType: 'created,updated',
notificationUrl,
lifecycleNotificationUrl: notificationUrl,
resource,
includeResourceData: false,
expirationDateTime,
clientState: webhook.id,
}
try {
const res = await fetch('https://graph.microsoft.com/v1.0/subscriptions', {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
const payload = await res.json()
if (!res.ok) {
const errorMessage =
payload.error?.message || payload.error?.code || 'Unknown Microsoft Graph API error'
logger.error(
`[${requestId}] Failed to create Teams subscription for webhook ${webhook.id}`,
{
status: res.status,
error: payload.error,
}
)
let userFriendlyMessage = 'Failed to create Teams subscription'
if (res.status === 401 || res.status === 403) {
userFriendlyMessage =
'Authentication failed. Please reconnect your Microsoft Teams account and ensure you have the necessary permissions.'
} else if (res.status === 404) {
userFriendlyMessage =
'Chat not found. Please verify that the Chat ID is correct and that you have access to the specified chat.'
} else if (errorMessage && errorMessage !== 'Unknown Microsoft Graph API error') {
userFriendlyMessage = `Teams error: ${errorMessage}`
}
throw new Error(userFriendlyMessage)
}
logger.info(
`[${requestId}] Successfully created Teams subscription ${payload.id} for webhook ${webhook.id}`
)
return {
providerConfigUpdates: {
externalSubscriptionId: payload.id as string,
subscriptionExpiration: payload.expirationDateTime as string,
},
}
} catch (error: unknown) {
if (
error instanceof Error &&
(error.message.includes('credentials') ||
error.message.includes('Chat ID') ||
error.message.includes('authenticate'))
) {
throw error
}
logger.error(
`[${requestId}] Error creating Teams subscription for webhook ${webhook.id}`,
error
)
throw new Error(
error instanceof Error
? error.message
: 'Failed to create Teams subscription. Please try again.'
)
}
},
async deleteSubscription({
webhook,
workflow,
requestId,
strict,
}: DeleteSubscriptionContext): Promise<void> {
try {
const config = getProviderConfig(webhook)
if (config.triggerId !== 'microsoftteams_chat_subscription') {
return
}
const externalSubscriptionId = config.externalSubscriptionId as string | undefined
const credentialId = config.credentialId as string | undefined
if (!externalSubscriptionId || !credentialId) {
logger.info(`[${requestId}] No external subscription to delete for webhook ${webhook.id}`)
if (strict) throw new Error('Missing Teams subscription cleanup configuration')
return
}
const credentialOwner = await getCredentialOwner(credentialId, requestId)
const accessToken = credentialOwner
? await refreshAccessTokenIfNeeded(
credentialOwner.accountId,
credentialOwner.userId,
requestId
)
: null
if (!accessToken) {
logger.warn(
`[${requestId}] Could not get access token to delete Teams subscription for webhook ${webhook.id}`
)
if (strict) throw new Error('Missing Teams access token for subscription deletion')
return
}
const res = await fetch(
`https://graph.microsoft.com/v1.0/subscriptions/${externalSubscriptionId}`,
{
method: 'DELETE',
headers: { Authorization: `Bearer ${accessToken}` },
}
)
if (res.ok || res.status === 404) {
logger.info(
`[${requestId}] Successfully deleted Teams subscription ${externalSubscriptionId} for webhook ${webhook.id}`
)
} else {
const errorBody = await res.text()
logger.warn(
`[${requestId}] Failed to delete Teams subscription ${externalSubscriptionId} for webhook ${webhook.id}. Status: ${res.status}`
)
if (strict) throw new Error(`Failed to delete Teams subscription: ${res.status}`)
}
} catch (error) {
logger.error(
`[${requestId}] Error deleting Teams subscription for webhook ${webhook.id}`,
error
)
if (strict) throw error
}
},
async formatInput({
body,
webhook,
workflow,
requestId,
}: FormatInputContext): Promise<FormatInputResult> {
const b = body as Record<string, unknown>
const value = b?.value as unknown[] | undefined
if (value && Array.isArray(value) && value.length > 0) {
const result = await formatTeamsGraphNotification(b, webhook, workflow)
return { input: result }
}
const messageText = (b?.text as string) || ''
const messageId = (b?.id as string) || ''
const timestamp = (b?.timestamp as string) || (b?.localTimestamp as string) || ''
const from = (b?.from || {}) as Record<string, unknown>
const conversation = (b?.conversation || {}) as Record<string, unknown>
const channelData = (b?.channelData || {}) as Record<string, unknown>
const channelDataTeam = (channelData.team || {}) as Record<string, unknown>
const channelDataChannel = (channelData.channel || {}) as Record<string, unknown>
const channelDataTenant = (channelData.tenant || {}) as Record<string, unknown>
return {
input: {
from: {
id: (from.id || '') as string,
name: (from.name || '') as string,
aadObjectId: (from.aadObjectId || '') as string,
},
message: {
raw: {
attachments: b?.attachments || [],
channelData: {
team: { id: (channelDataTeam.id || '') as string },
tenant: { id: (channelDataTenant.id || '') as string },
channel: { id: (channelDataChannel.id || '') as string },
teamsTeamId: (channelData.teamsTeamId || channelDataTeam.id || '') as string,
teamsChannelId: (channelData.teamsChannelId || channelDataChannel.id || '') as string,
},
conversation: b?.conversation || {},
text: messageText,
messageType: (b?.type || 'message') as string,
channelId: (b?.channelId || '') as string,
timestamp,
},
},
activity: b || {},
conversation: {
id: (conversation.id || '') as string,
name: (conversation.name || '') as string,
isGroup: (conversation.isGroup || false) as boolean,
tenantId: (conversation.tenantId || '') as string,
aadObjectId: (conversation.aadObjectId || '') as string,
conversationType: (conversation.conversationType || '') as string,
},
},
}
},
}