/** * Approval Step — send approval request, pause pipeline until callback. * Config: { channel_id: 1, message: "...", options: ["Approve","Reject","Escalate"], timeout_min: 60 } */ import { registerStepExecutor } from '../pipeline-engine'; import { postgresClient } from '../postgres-client'; import { PipelineStep, PipelineContext, StepExecutorResult, NotificationChannel } from '../../types/pipeline'; async function executeApproval( step: PipelineStep, context: PipelineContext, executionId: number ): Promise { const channelId = Number(step.config.channel_id); const message = step.config.message || 'Approval required'; const options = step.config.options || ['Approve', 'Reject']; const timeoutMin = Number(step.config.timeout_min) || 60; const expiresAt = new Date(Date.now() + timeoutMin * 60 * 1000); // Create approval request record const result = await postgresClient.query<{ id: number }>( `INSERT INTO approval_requests (execution_id, step_order, channel_id, message, options, status, expires_at) VALUES ($1, $2, $3, $4, $5, 'pending', $6) RETURNING id`, [executionId, step.step_order, channelId || null, message, JSON.stringify(options), expiresAt] ); const approvalId = result.rows[0].id; const callbackUrl = `${process.env.WEBHOOK_BASE_URL || ''}/api/pipelines/approval/${approvalId}`; console.log(`[PIPELINE:approval] Created approval #${approvalId}, callback: ${callbackUrl}`); // Send notification with approval buttons if channel is configured if (channelId) { const chResult = await postgresClient.query( `SELECT * FROM notification_channels WHERE id = $1 AND is_active = true`, [channelId] ); if (chResult.rows.length > 0) { const channel = chResult.rows[0]; await sendApprovalNotification(channel, message, options, approvalId, callbackUrl, context); } } // Return waiting — pipeline will pause here return { success: true, waiting: true, output: { approval_id: approvalId, callback_url: callbackUrl }, }; } async function sendApprovalNotification( channel: NotificationChannel, message: string, options: string[], approvalId: number, callbackUrl: string, context: PipelineContext ): Promise { try { if (channel.channel_type === 'teams') { const actions = options.map(opt => ({ type: 'Action.OpenUrl', title: opt, url: `${callbackUrl}?response=${encodeURIComponent(opt)}`, })); const card = { type: 'message', attachments: [{ contentType: 'application/vnd.microsoft.card.adaptive', content: { type: 'AdaptiveCard', $schema: 'http://adaptivecards.io/schemas/adaptive-card.json', version: '1.4', body: [ { type: 'TextBlock', text: 'Approval Required', weight: 'bolder', size: 'medium' }, { type: 'TextBlock', text: message, wrap: true }, { type: 'TextBlock', text: `Approval #${approvalId}`, size: 'small', isSubtle: true }, ], actions, }, }], }; await fetch(channel.config.webhook_url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(card), }); } else if (channel.channel_type === 'telegram') { const keyboard = { inline_keyboard: [options.map(opt => ({ text: opt, callback_data: JSON.stringify({ approval_id: approvalId, response: opt }), }))], }; await fetch(`https://api.telegram.org/bot${channel.config.bot_token}/sendMessage`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ chat_id: channel.config.chat_id, text: `🔔 *Approval Required*\n\n${message}\n\n_Approval #${approvalId}_`, parse_mode: 'Markdown', reply_markup: keyboard, }), }); } else if (channel.channel_type === 'ntfy') { const serverUrl = channel.config.server_url || 'https://ntfy.sh'; const headers: Record = { 'Title': 'Approval Required', 'Priority': 'high', 'Tags': 'warning', 'Actions': options.map(opt => `http, ${opt}, ${callbackUrl}?response=${encodeURIComponent(opt)}, method=POST` ).join('; '), }; if (channel.config.auth_token) { headers['Authorization'] = `Bearer ${channel.config.auth_token}`; } await fetch(`${serverUrl}/${channel.config.topic}`, { method: 'POST', headers, body: message, }); } } catch (err) { console.error(`[PIPELINE:approval] Failed to send notification:`, err); } } registerStepExecutor('approval', executeApproval);