import crypto from 'crypto'; import { storage } from '../storage'; import { generateBotServiceToken } from '../utils/jwt'; import { sendWebhook } from '../utils/webhook'; import type { BotSubscription } from '@shared/schema'; import { decrypt } from '../crypto'; export interface WebhookEvent { event: string; timestamp: string; organizationId: number; data: any; } export class WebhookService { async dispatchEvent(event: WebhookEvent): Promise { const { organizationId, event: eventType, data } = event; try { let subscriptions: BotSubscription[] = []; if (data.formId) { const formSubs = await storage.getBotSubscriptionsByForm(data.formId, organizationId); subscriptions = [...subscriptions, ...formSubs.filter(s => (s.events as string[]).includes(eventType))]; } if (data.statusId) { const statusSubs = await storage.getBotSubscriptionsByStatus(data.statusId, organizationId); subscriptions = [...subscriptions, ...statusSubs.filter(s => (s.events as string[]).includes(eventType))]; } if (data.transitionId) { const transitionSubs = await storage.getBotSubscriptionsByTransition(data.transitionId, organizationId); subscriptions = [...subscriptions, ...transitionSubs.filter(s => (s.events as string[]).includes(eventType))]; } const uniqueBotIds = Array.from(new Set(subscriptions.map(s => s.botId))); for (const botId of uniqueBotIds) { const bot = await storage.getBot(botId, organizationId); if (!bot || !bot.webhookUrl || !bot.webhookEnabled || !bot.isActive) { continue; } // Generate short-lived access token for the bot const botAccessToken = generateBotServiceToken(bot.id, organizationId); const payload = { ...event, bot: { id: bot.id, name: bot.name, accessToken: botAccessToken // Short-lived token (10 min) for API callbacks } }; let signature = ''; if (bot.webhookSecret) { const secret = decrypt(bot.webhookSecret); signature = crypto .createHmac('sha256', secret) .update(JSON.stringify(payload)) .digest('hex'); } await sendWebhook( bot.webhookUrl, payload, { 'X-Webhook-Signature': signature, 'X-Bot-Id': bot.id.toString(), 'X-Event-Type': eventType, }, { taskId: data.taskId, organizationId, botId: bot.id, eventType, } ); } } catch (error) { console.error('Webhook dispatch error:', error); } } async dispatchStatusChange( organizationId: number, taskId: number, formId: number, fromStatusId: number, toStatusId: number, transitionId: number, userId: number ): Promise { const task = await storage.getTask(taskId, organizationId); const user = await storage.getUser(userId); await this.dispatchEvent({ event: 'status_change', timestamp: new Date().toISOString(), organizationId, data: { taskId, formId, task, fromStatusId, toStatusId, transitionId, statusId: toStatusId, user: user ? { id: user.id, firstName: user.firstName, lastName: user.lastName, email: user.email } : null } }); } async dispatchComment( organizationId: number, taskId: number, formId: number, message: any, authorId: number ): Promise { const task = await storage.getTask(taskId, organizationId); const author = await storage.getUser(authorId); await this.dispatchEvent({ event: 'comment', timestamp: new Date().toISOString(), organizationId, data: { taskId, formId, task, message, author: author ? { id: author.id, firstName: author.firstName, lastName: author.lastName, email: author.email } : null } }); } async dispatchFieldChange( organizationId: number, taskId: number, formId: number, fieldId: number, fieldCode: string, oldValue: any, newValue: any, userId: number ): Promise { const task = await storage.getTask(taskId, organizationId); const user = await storage.getUser(userId); await this.dispatchEvent({ event: 'field_change', timestamp: new Date().toISOString(), organizationId, data: { taskId, formId, task, fieldId, fieldCode, oldValue, newValue, user: user ? { id: user.id, firstName: user.firstName, lastName: user.lastName, email: user.email } : null } }); } } export const webhookService = new WebhookService();