// cultivaflow-webhooks/server.js
const express = require('express');
const crypto = require('crypto');
const pg = require('pg');
const fetch = require('node-fetch');
const app = express();
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
// ─── Verificador de firma ───────────────────────────────────
function verifyVercelSignature(body, signature, secret) {
const expected = crypto
.createHmac('sha1', secret)
.update(body)
.digest('hex');
try {
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
} catch { return false; }
}
// ─── Notificador Slack ──────────────────────────────────────
async function notifySlack(event, payload) {
const icons = {
'deployment.created': '🚀',
'deployment.succeeded': '✅',
'deployment.error': '🔴',
'deployment.canceled': '⏹️',
'project.created': '📁',
};
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `${icons[event] || '📌'} *${event}*`,
attachments: [{
color: event.includes('error') ? '#f85149' : '#3fb950',
fields: [
{ title: 'Deployment', value: payload.deployment?.id, short: true },
{ title: 'URL', value: payload.deployment?.url, short: true },
]
}]
})
});
}
// ─── Endpoint webhook ───────────────────────────────────────
app.post('/webhooks/vercel',
express.raw({ type: 'application/json' }),
async (req, res) => {
const sig = req.headers['x-vercel-signature'];
if (!sig) return res.status(400).send('Missing signature');
if (!verifyVercelSignature(req.body, sig, process.env.VERCEL_WEBHOOK_SECRET)) {
console.error('[SECURITY] Firma inválida rechazada');
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body.toString());
// Audit log en PostgreSQL
await pool.query(
`INSERT INTO webhook_events (id, type, payload, received_at)
VALUES ($1, $2, $3, NOW())`,
[event.id, event.type, event.payload]
);
switch (event.type) {
case 'deployment.created':
case 'deployment.succeeded':
case 'deployment.error':
await notifySlack(event.type, event.payload);
break;
case 'project.created':
console.log('Nuevo proyecto:', event.payload.project.name);
break;
default:
console.log('Evento no manejado:', event.type);
}
res.json({ received: true, id: event.id });
}
);
app.listen(3001, () =>
console.log('🎣 Webhook server en puerto 3001')
);
// app/api/webhooks/vercel/route.ts — CultivaFlow
import { NextRequest, NextResponse } from 'next/server';
import { createHmac, timingSafeEqual } from 'crypto';
import { db } from '@/lib/db';
// CRÍTICO: desactivar body parser de Next.js
export const config = { api: { bodyParser: false } };
function verify(body: Buffer, sig: string): boolean {
const expected = createHmac('sha1', process.env.VERCEL_WEBHOOK_SECRET!)
.update(body)
.digest('hex');
try {
return timingSafeEqual(
Buffer.from(sig),
Buffer.from(expected)
);
} catch { return false; }
}
export async function POST(req: NextRequest) {
const signature = req.headers.get('x-vercel-signature');
if (!signature) {
return NextResponse.json(
{ error: 'Missing signature' }, { status: 400 }
);
}
const body = Buffer.from(await req.arrayBuffer());
if (!verify(body, signature)) {
return NextResponse.json(
{ error: 'Invalid signature' }, { status: 401 }
);
}
const event: VercelWebhookEvent = JSON.parse(body.toString());
// Idempotencia: evitar procesar duplicados
const exists = await db.webhookEvents.findUnique({ where: { id: event.id } });
if (exists) return NextResponse.json({ received: true, duplicate: true });
await db.webhookEvents.create({
data: { id: event.id, type: event.type, payload: event.payload }
});
// Encola procesamiento asíncrono (no bloquear respuesta)
void processEvent(event);
return NextResponse.json({ received: true, id: event.id });
}
| Variable | Descripción | Obligatoria |
|---|---|---|
VERCEL_WEBHOOK_SECRET |
Secreto del webhook (Dashboard Vercel) | Sí |
SLACK_WEBHOOK_URL |
Incoming webhook de Slack para alertas | Sí |
DATABASE_URL |
PostgreSQL para audit log de eventos | Sí |
VERCEL_TOKEN |
API token (rollbacks automáticos) | Opcional |
| Evento | Acción CultivaFlow |
|---|---|
deployment.created |
Slack "Iniciando deploy..." |
deployment.succeeded |
Slack OK + URL + DB log |
deployment.error |
Slack urgente + abrir issue |
deployment.canceled |
DB log + limpiar recursos |
project.created |
Auto-config monitorización |
domain.created |
Verificar DNS + SSL |