skill: slack-webhooks-verificacion-eventos • cliente: Cultiva IA Agency • stack: Node.js + Express
// Generated with: slack-webhooks skill — Cultiva IA Agency // Endpoint: POST /webhooks/slack en api.cultivaia.com const crypto = require('crypto'); const express = require('express'); const app = express(); // ─── Verificacion HMAC-SHA256 ────────────────────────────────────────────── function verifySlackRequest(rawBody, signatureHeader, timestampHeader, signingSecret) { if (!signatureHeader || !timestampHeader || !signingSecret) return false; const timestamp = parseInt(timestampHeader, 10); if (Number.isNaN(timestamp)) return false; // Proteccion anti-replay: rechazar peticiones con mas de 5 minutos if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > 60 * 5) return false; // Slack firma la cadena literal: "v0:" + timestamp + ":" + raw body const basestring = `v0:${timestamp}:${rawBody}`; const expected = 'v0=' + crypto .createHmac('sha256', signingSecret) .update(basestring, 'utf8') .digest('hex'); try { // timingSafeEqual previene timing attacks return crypto.timingSafeEqual( Buffer.from(signatureHeader), Buffer.from(expected) ); } catch { return false; } } // ─── Handlers de eventos Cultiva IA ─────────────────────────────────────── async function handleAppMention(event) { console.log(`[@cultiva-bot] Mencion en #${event.channel} por ${event.user}: "${event.text}"`); // Enqueue tarea pesada (no bloquear los 3s) — ej. bull/pg await taskQueue.add('mention-response', { event }); } async function handleReactionAdded(event) { if (event.reaction === 'white_check_mark') { console.log(`[Tareas] Completada por ${event.user} en msg ${event.item.ts}`); await db.markTaskComplete(event.item.ts, event.user); } } async function handleTeamJoin(event) { console.log(`[Bienvenida] Nuevo miembro: ${event.user.id}`); await slack.chat.postMessage({ channel: event.user.id, text: `Bienvenido/a a Cultiva IA! Soy @cultiva-bot. Escribe /ayuda para empezar.` }); } // ─── Endpoint principal ──────────────────────────────────────────────────── app.post('/webhooks/slack', express.raw({ type: 'application/json' }), // CRITICO: body RAW para verificacion async (req, res) => { const signature = req.headers['x-slack-signature']; const timestamp = req.headers['x-slack-request-timestamp']; const rawBody = req.body.toString('utf8'); if (!verifySlackRequest(rawBody, signature, timestamp, process.env.SLACK_SIGNING_SECRET)) { return res.status(401).send('Firma invalida'); } const payload = JSON.parse(rawBody); // Challenge de verificacion inicial (configuracion del endpoint en Slack) if (payload.type === 'url_verification') { return res.status(200).json({ challenge: payload.challenge }); } // Responder 200 inmediatamente — Slack requiere respuesta en < 3 segundos res.status(200).send('OK'); if (payload.type === 'event_callback') { const event = payload.event; switch (event.type) { case 'app_mention': await handleAppMention(event); break; case 'reaction_added': await handleReactionAdded(event); break; case 'team_join': await handleTeamJoin(event); break; case 'app_home_opened': console.log(`[Home] Abierto por ${event.user}`); break; default: console.log(`[Webhook] Evento no manejado: ${event.type}`); } } } ); app.listen(3000, () => console.log('cultiva-bot escuchando en :3000'));
| Header | Descripcion | Uso |
|---|---|---|
| X-Slack-Signature | Firma HMAC-SHA256 v0=<hex> |
Verificar autenticidad |
| X-Slack-Request-Timestamp | Unix epoch incluido en el basestring | Anti-replay (≤5 min) |
| X-Slack-Retry-Num | Intento de reintento (1, 2 o 3) | Detectar duplicados |
| X-Slack-Retry-Reason | http_timeout / http_error |
Diagnosticar timeouts |
express.raw({ type: 'application/json' }) y parsear manualmente tras verificar.
X-Slack-Retry-Num
para evitar procesamiento duplicado (idempotencia).
=== o !==.
Un atacante puede medir el tiempo de respuesta para adivinar el secret caracter por caracter.
crypto.timingSafeEqual() garantiza tiempo constante
independientemente del numero de caracteres que coincidan.
# Exponer el servidor local a Slack sin cuenta ni configuracion de red npx hookdeck-cli listen 3000 slack --path /webhooks/slack # Output: # Signing Secret sk_cultiva_abc123xyz789 # Forwarding https://events.hookdeck.com/e/src_xxx → localhost:3000/webhooks/slack # # Pegar esta URL en: Slack App → Event Subscriptions → Request URL # Slack enviara el challenge url_verification automaticamente # Variable de entorno necesaria SLACK_SIGNING_SECRET=sk_cultiva_abc123xyz789 # Slack App → Basic Information → App Credentials