Referencia de código lista para producción · CULTIVA IA · Proyecto: automatizacion-clientes
x-webhook-signatureexpress.json() primero, la firma siempre fallara porque el body ya fue transformado.
const crypto = require('crypto'); const express = require('express'); const app = express(); // ——— Verificacion de firma HMAC-SHA256 ——— function verifyCursorWebhook(rawBody, signatureHeader, secret) { if (!signatureHeader || !secret) return false; // Cursor envia: sha256=<hex> const [algorithm, signature] = signatureHeader.split('='); if (algorithm !== 'sha256') return false; const expected = crypto .createHmac('sha256', secret) .update(rawBody) .digest('hex'); try { // timingSafeEqual previene timing attacks return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); } catch { return false; } } // ——— Endpoint POST /webhooks/cursor ——— app.post( '/webhooks/cursor', express.raw({ type: 'application/json' }), // CRITICO: raw body (req, res) => { const signature = req.headers['x-webhook-signature']; const webhookId = req.headers['x-webhook-id']; const event = req.headers['x-webhook-event']; const secret = process.env.CURSOR_WEBHOOK_SECRET; // 1. Verificar firma if (!verifyCursorWebhook(req.body, signature, secret)) { console.error(`[CULTIVA] Firma invalida — webhook rechazado (id: ${webhookId})`); return res.status(401).json({ error: 'Firma invalida' }); } // 2. Parsear payload DESPUES de verificar const payload = JSON.parse(req.body.toString()); console.log(`[CULTIVA] Evento recibido: ${event} (id: ${webhookId})`); // 3. Manejar statusChange if (event === 'statusChange') { const { id, status, summary, target } = payload; if (status === 'FINISHED') { console.log(`[CULTIVA] Agente ${id} FINALIZADO. PR: ${target?.prUrl}`); console.log(`[CULTIVA] Resumen: ${summary}`); // TODO: actualizar dashboard, notificar Slack success } else if (status === 'ERROR') { console.error(`[CULTIVA] Agente ${id} con ERROR — notificando al lead tecnico`); // TODO: notifySlack({ text: `ERROR en agente ${id}`, channel: '#alertas-ia' }); } } // 4. Responder inmediatamente (antes de procesamiento largo) return res.json({ received: true }); } ); app.listen(3000, () => console.log('[CULTIVA] Servidor webhooks en :3000'));
import hmac import hashlib import json import os from fastapi import FastAPI, Request, HTTPException from fastapi.responses import JSONResponse app = FastAPI(title="CULTIVA IA — Cursor Webhooks") def verify_cursor_webhook( body: bytes, signature_header: str, secret: str ) -> bool: """Verifica HMAC-SHA256 con comparacion timing-safe.""" if not signature_header or not secret: return False parts = signature_header.split("=") if len(parts) != 2 or parts[0] != "sha256": return False signature = parts[1] expected = hmac.new( secret.encode(), body, hashlib.sha256 ).hexdigest() # hmac.compare_digest previene timing attacks return hmac.compare_digest(signature, expected) @app.post("/webhooks/cursor") async def cursor_webhook(request: Request): body = await request.body() sig = request.headers.get("x-webhook-signature", "") wh_id = request.headers.get("x-webhook-id", "") event = request.headers.get("x-webhook-event", "") secret = os.getenv("CURSOR_WEBHOOK_SECRET", "") if not verify_cursor_webhook(body, sig, secret): print(f"[CULTIVA] Firma invalida (id={wh_id})") raise HTTPException(status_code=401, detail="Firma invalida") payload = json.loads(body) print(f"[CULTIVA] Evento: {event} (id={wh_id})") if event == "statusChange": status = payload.get("status") agent = payload.get("id") summary = payload.get("summary", "") pr_url = payload.get("target", {}).get("prUrl") if status == "FINISHED": print(f"[CULTIVA] {agent} FINALIZADO. PR={pr_url}") print(f"[CULTIVA] Resumen: {summary}") # await notify_slack_success(agent, pr_url, summary) elif status == "ERROR": print(f"[CULTIVA] ERROR en agente {agent}") # await notify_slack_error(agent) return JSONResponse({"received": True})
# Signing secret desde Cursor Dashboard CURSOR_WEBHOOK_SECRET=cltv_whsec_a1b2c3d4e5f6g7h8i9j0
{
"event": "statusChange",
"timestamp": "2026-06-15T09:14:00.000Z",
"id": "agent_cultiva_7f2a1d",
"status": "FINISHED",
"source": {
"repository": "github.com/cultiva-ia/automatizacion",
"ref": "main"
},
"target": {
"url": "github.com/cultiva-ia/automatizacion/pull/42",
"branchName": "cursor/fix-migracion-db",
"prUrl": "github.com/.../pull/42"
},
"summary": "Actualizo 4 migraciones SQL y corrio tests"
}
| Cabecera | Ejemplo | Descripcion | Obligatorio |
|---|---|---|---|
x-webhook-signature |
sha256=3a4b5c… |
HMAC-SHA256 del raw body. Siempre empieza por sha256= |
Si |
x-webhook-id |
wh_01J3X… |
Identificador unico del webhook para idempotencia | Si |
x-webhook-event |
statusChange |
Tipo de evento. Actualmente solo statusChange |
Si |
Content-Type |
application/json |
El body siempre es JSON serializado | Siempre |
| Status | Significado | Accion recomendada |
|---|---|---|
FINISHED |
Agente completo con exito | Leer summary y target.prUrl, actualizar dashboard, notificar equipo |
ERROR |
El agente encontro un error irrecuperable | Alertar al lead tecnico (Slack), registrar en log, revisar PR si existe |
npx hookdeck-cli listen 3000 cursor --path /webhooks/cursor para obtener una URL publica que reenvie eventos a tu servidor local. Incluye UI de inspeccion de peticiones.