import crypto from 'crypto'; import { DatabaseService } from './DatabaseService'; import { ComposeService } from './ComposeService'; import { FileSystemService } from './FileSystemService'; import { NodeRegistry } from './NodeRegistry'; export class WebhookService { private static instance: WebhookService; public static getInstance(): WebhookService { if (!WebhookService.instance) { WebhookService.instance = new WebhookService(); } return WebhookService.instance; } public generateSecret(): string { return crypto.randomBytes(32).toString('hex'); } public validateSignature(payload: string, secret: string, signature: string): boolean { // Expect format: sha256= const parts = signature.split('='); if (parts.length !== 2 || parts[0] !== 'sha256') return false; const expected = crypto .createHmac('sha256', secret) .update(payload) .digest('hex'); return crypto.timingSafeEqual( Buffer.from(expected, 'hex'), Buffer.from(parts[1], 'hex') ); } public async execute(webhookId: number, action: string, triggerSource: string | null): Promise<{ success: boolean; error?: string; duration_ms: number }> { const db = DatabaseService.getInstance(); const webhook = db.getWebhook(webhookId); if (!webhook) throw new Error('Webhook not found'); const defaultNodeId = NodeRegistry.getInstance().getDefaultNodeId(); // Validate the stack still exists const stacks = await FileSystemService.getInstance(defaultNodeId).getStacks(); if (!stacks.includes(webhook.stack_name)) { const error = `Stack "${webhook.stack_name}" not found`; db.addWebhookExecution({ webhook_id: webhookId, action, status: 'failure', trigger_source: triggerSource, duration_ms: 0, error, executed_at: Date.now(), }); return { success: false, error, duration_ms: 0 }; } const startTime = Date.now(); try { const compose = ComposeService.getInstance(defaultNodeId); switch (action) { case 'deploy': await compose.deployStack(webhook.stack_name); break; case 'restart': await compose.runCommand(webhook.stack_name, 'restart'); break; case 'stop': await compose.runCommand(webhook.stack_name, 'stop'); break; case 'start': await compose.runCommand(webhook.stack_name, 'start'); break; case 'pull': await compose.updateStack(webhook.stack_name); break; default: throw new Error(`Unknown action: ${action}`); } const duration_ms = Date.now() - startTime; db.addWebhookExecution({ webhook_id: webhookId, action, status: 'success', trigger_source: triggerSource, duration_ms, error: null, executed_at: Date.now(), }); return { success: true, duration_ms }; } catch (err) { const duration_ms = Date.now() - startTime; const error = (err as Error).message || 'Unknown error'; db.addWebhookExecution({ webhook_id: webhookId, action, status: 'failure', trigger_source: triggerSource, duration_ms, error, executed_at: Date.now(), }); return { success: false, error, duration_ms }; } } public maskSecret(secret: string): string { if (secret.length <= 8) return '••••••••'; return '••••••••' + secret.slice(-4); } }