β‘ Generated with: webhooks-agentes-gestionados-claude Β· hookdeck/webhook-skills
8
Eventos CMA cubiertos
3
Integraciones activas
5min
Ventana anti-replay
0
Vulnerabilidades (grep+LLM)
Flujo de verificaciΓ³n
Anthropic
envia POST
envia POST
β
express.raw()
cuerpo crudo
cuerpo crudo
β
Verificar
HMAC-SHA256
HMAC-SHA256
β
Anti-replay
< 5 min
< 5 min
β
Switch
event.data.type
event.data.type
β
Update
jobs DB
jobs DB
β
Notificar
Slack
Slack
β
200 OK
{received: true}
{received: true}
webhook-handler.js
π
webhook-handler.js
JavaScript Β· Node 20
π Copiar
β ProducciΓ³n
// Generated with: claude-managed-agents-webhooks skill // https://github.com/hookdeck/webhook-skills // Cliente: CULTIVA IA Platform β orquestaciΓ³n de agentes SEO/contenido const express = require('express'); const crypto = require('crypto'); const https = require('https'); const { Pool } = require('pg'); const app = express(); const db = new Pool({ connectionString: process.env.DATABASE_URL }); const PORT = process.env.PORT || 3000; // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ // 1. VerificaciΓ³n de firma Standard Webhooks (HMAC-SHA256) // Spec: https://www.standardwebhooks.com/ // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ function verifyClaudeSignature(payload, webhookId, webhookTimestamp, webhookSignature, secret) { if (!webhookId || !webhookTimestamp || !webhookSignature || !webhookSignature.includes(',')) { return false; } // ProtecciΓ³n anti-replay: rechazar eventos con mΓ‘s de 5 min de antigΓΌedad const currentTime = Math.floor(Date.now() / 1000); const timestampDiff = currentTime - parseInt(webhookTimestamp); if (timestampDiff > 300 || timestampDiff < -300) { return false; } // Contenido firmado: "{webhookId}.{webhookTimestamp}.{rawBody}" const payloadStr = payload instanceof Buffer ? payload.toString('utf8') : payload; const signedContent = `${webhookId}.${webhookTimestamp}.${payloadStr}`; // whsec_ es prefijo de una clave base64 de 32 bytes const secretKey = secret.startsWith('whsec_') ? secret.slice(6) : secret; const secretBytes = Buffer.from(secretKey, 'base64'); const expectedSig = crypto .createHmac('sha256', secretBytes) .update(signedContent, 'utf8') .digest('base64'); // Soporta mΓΊltiples firmas "v1,sig1 v1,sig2" (rotaciΓ³n de claves) return webhookSignature.split(' ').some(pair => { const [version, signature] = pair.split(','); if (version !== 'v1' || !signature) return false; try { return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expectedSig) ); } catch { return false; } }); } // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ // 2. Helpers: PostgreSQL jobs + Slack notifications // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ async function updateJobStatus(sessionId, status, meta = {}) { await db.query( `UPDATE jobs SET status = $1, updated_at = NOW(), meta = meta || $2 WHERE cma_session_id = $3`, [status, JSON.stringify(meta), sessionId] ); } async function notifySlack(message, color = 'good') { const body = JSON.stringify({ attachments: [{ color: color, text: message, footer: 'CULTIVA IA Β· CMA Webhooks' }] }); await new Promise((resolve, reject) => { const url = new URL(process.env.SLACK_WEBHOOK_URL); const req = https.request({ hostname: url.hostname, path: url.pathname, method: 'POST', headers: { 'Content-Type': 'application/json' } }, res => res.on('data', () => {}).on('end', resolve)); req.on('error', reject); req.end(body); }); } // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ // 3. Endpoint webhook β DEBE usar express.raw() // Si usas express.json() el body se parsea y la firma falla // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ app.post( '/webhooks/claude-managed-agents', express.raw({ type: 'application/json' }), async (req, res) => { const webhookId = req.headers['webhook-id']; const webhookTimestamp = req.headers['webhook-timestamp']; const webhookSignature = req.headers['webhook-signature']; if (!verifyClaudeSignature( req.body, webhookId, webhookTimestamp, webhookSignature, process.env.ANTHROPIC_WEBHOOK_SIGNING_KEY )) { console.warn('[CMA] Firma invΓ‘lida β posible replay o clave incorrecta'); return res.status(400).json({ error: 'Invalid signature' }); } const event = JSON.parse(req.body.toString()); const eventType = event.data?.type; // β data.type, NO top-level type const sessionId = event.data?.id; console.log(`[CMA] ${eventType} β ${sessionId}`); switch (eventType) { // Agente terminΓ³ su turno, espera input (Γ©xito mΓ‘s frecuente) case 'session.status_idled': await updateJobStatus(sessionId, 'idled'); await notifySlack( `:white_check_mark: Agente inactivo y listo\n*SesiΓ³n:* ${sessionId}`, 'good' ); break; // Error terminal β requiere intervenciΓ³n manual case 'session.status_terminated': await updateJobStatus(sessionId, 'terminated', { terminated_at: new Date().toISOString() }); await notifySlack( `:x: SesiΓ³n terminada con error\n*SesiΓ³n:* ${sessionId}`, 'danger' ); break; // Agente coordinador abriΓ³ un subagente case 'session.thread_created': console.log(`[CMA] Nuevo hilo multiagente: ${sessionId}`); await updateJobStatus(sessionId, 'running_multiagent'); break; // Credencial OAuth de vault no pudo refrescarse β alerta ops case 'vault_credential.refresh_failed': await notifySlack( `:warning: Vault credential refresh FAILED\n*ID:* ${sessionId}\nRevisa el vault en Anthropic Console`, 'warning' ); break; case 'session.status_run_started': await updateJobStatus(sessionId, 'running'); break; case 'session.thread_idled': case 'session.thread_terminated': console.log(`[CMA] Thread lifecycle: ${eventType} β ${sessionId}`); break; default: console.log(`[CMA] Evento no manejado: ${eventType}`); } return res.status(200).json({ received: true }); } ); // Health check app.get('/health', (_req, res) => res.json({ status: 'ok', service: 'cultiva-cma-webhooks' })); app.listen(PORT, () => console.log(`[CMA] Webhook handler escuchando en :${PORT}/webhooks/claude-managed-agents`) );
Eventos manejados
session.status_idled
El agente terminΓ³ su turno y espera input del usuario o aprobaciΓ³n de herramienta.
π jobs.status = idled π¬ Slack OK
session.status_terminated
La sesiΓ³n alcanzΓ³ un error terminal. Requiere intervenciΓ³n manual.
π jobs.status = terminated π΄ Slack DANGER
session.thread_created
El coordinador abriΓ³ un hilo de subagente (arquitectura multiagente).
π jobs.status = running_multiagent
vault_credential.refresh_failed
OAuth MCP no pudo refrescar el token. La herramienta quedarΓ‘ sin acceso.
β Slack WARNING al equipo ops
session.status_run_started
El agente iniciΓ³ ejecuciΓ³n; se activa en cada transiciΓ³n a estado running.
π jobs.status = running
session.thread_idled / thread_terminated
Ciclo de vida de hilos secundarios. Log estructurado para trazabilidad.
π console.log estructurado
Variables de entorno
π
ANTHROPIC_WEBHOOK_SIGNING_KEY
whsec_[base64-32bytes] β Console β Webhooks
π€
ANTHROPIC_API_KEY
sk-ant-api03-... β para client.beta.sessions.retrieve()
π
DATABASE_URL
postgres://user:pass@host:5432/cultiva_platform
π¬
SLACK_WEBHOOK_URL
https://hooks.slack.com/services/T.../B.../...
Desarrollo local
π»Terminalbash
# Tunnel sin cuenta (Hookdeck CLI) npx hookdeck-cli listen 3000 claude-managed-agents \ --path /webhooks/claude-managed-agents # Resultado: URL pΓΊblica β https://events.hookdeck.com/e/src_... # Pegar esa URL en Anthropic Console β Settings β Webhooks β Add endpoint