Flujo de verificacion
Discord → NexoBot APIDiscord
envia evento
envia evento
→
Verificar
Ed25519
Ed25519
→
Enrutar
por tipo
por tipo
→
Persistir
en BD
en BD
→
Responder
204 vacio
204 vacio
Eventos manejados por NexoBot
3 tipos de negocio criticos
APPLICATION_AUTHORIZED
Usuario instala la app NexoBot en su servidor de Discord.
+ Crear workspace en BD
APPLICATION_DEAUTHORIZED
Usuario elimina la app o revoca el acceso.
✗ Marcar cuenta como cancelada
ENTITLEMENT_CREATE
Nuevo pago del plan Premium NexoBot Pro.
★ Activar plan Pro en usuario
Codigo generado — discord-webhook-handler.js
Node.js + Express// Generated with: discord-webhooks skill // Cliente: NexoBot SaaS — handler de eventos Discord para la API Express // https://github.com/hookdeck/webhook-skills const express = require('express'); const { verifyKey } = require('discord-interactions'); const db = require('./db'); // conexion PostgreSQL de NexoBot const router = express.Router(); // ─── MIDDLEWARE: verificacion Ed25519 ──────────────────────────────────────── // CRITICO: necesitamos el body RAW (bytes), no parseado. // Discord firma: X-Signature-Timestamp + body_raw router.post( '/webhooks/discord', express.raw({ type: 'application/json' }), async (req, res) => { const signature = req.headers['x-signature-ed25519']; const timestamp = req.headers['x-signature-timestamp']; const publicKey = process.env.DISCORD_PUBLIC_KEY; // Si faltan cabeceras obligatorias → rechazar inmediatamente if (!signature || !timestamp) { return res.status(401).send('Missing signature headers'); } // Verificacion criptografica (discord-interactions v4 es async) const isValid = await verifyKey(req.body, signature, timestamp, publicKey); if (!isValid) { return res.status(401).send('Invalid request signature'); } const payload = JSON.parse(req.body.toString()); // ─── PING (type 0): validacion inicial del endpoint ────────────────────── // Discord envia este PING cuando registramos la URL en el Developer Portal. // Debemos responder 204 vacio; sin esto el registro falla. if (payload.type === 0) { return res.status(204).send(); } // ─── EVENT (type 1): payload real de negocio ───────────────────────────── if (payload.type === 1) { const event = payload.event; switch (event.type) { // Usuario instala NexoBot → crear workspace case 'APPLICATION_AUTHORIZED': { const { user, guild } = event.data; await db.query( `INSERT INTO workspaces (discord_user_id, guild_id, installed_at) VALUES ($1, $2, NOW()) ON CONFLICT (discord_user_id) DO UPDATE SET guild_id = $2`, [user?.id, guild?.id] ); console.info(`[NexoBot] Workspace creado para usuario ${user?.id}`); break; } // Usuario elimina la app → cancelar cuenta case 'APPLICATION_DEAUTHORIZED': { const { user } = event.data; await db.query( `UPDATE workspaces SET status = 'cancelled', cancelled_at = NOW() WHERE discord_user_id = $1`, [user?.id] ); console.info(`[NexoBot] Cuenta cancelada para usuario ${user?.id}`); break; } // Pago de suscripcion Premium → activar plan Pro case 'ENTITLEMENT_CREATE': { const { id: entitlementId, user_id, sku_id } = event.data; await db.query( `UPDATE workspaces SET plan = 'pro', entitlement_id = $1, plan_activated_at = NOW() WHERE discord_user_id = $2`, [entitlementId, user_id] ); console.info(`[NexoBot] Plan Pro activado — entitlement ${entitlementId} sku ${sku_id}`); break; } default: console.warn(`[NexoBot] Evento no manejado: ${event.type}`); } } // Discord siempre espera un 204 al final (aunque el evento no sea conocido) res.status(204).send(); } ); module.exports = router;
Variables de entorno
DISCORD_PUBLIC_KEY
a3f9b2c1d0e...
Developer Portal → General Info
DATABASE_URL
postgresql://nexo...
Conexion NexoBot BD
PORT
3000
Local dev
Puntos clave de seguridad
express.raw()
El body debe ser bytes crudos, no parseado — la firma criptografica falla si el body fue modificado.
Timestamp replay
discord-interactions valida que el timestamp no sea demasiado antiguo, previniendo ataques de replay.
PING obligatorio
Responder 204 al type 0 es requisito para registrar el endpoint en el Developer Portal.
204 siempre
Discord no necesita body en la respuesta. Siempre 204 vacio, incluso en eventos no manejados.
Desarrollo local con hookdeck-cli
Tunnel sin cuenta necesaria# 1. Instalar dependencia npm install discord-interactions # 2. Montar el tunel (sin registrarse) npx hookdeck-cli listen 3000 nexobot-discord --path /webhooks/discord # Output esperado: # ✓ Forwarding: https://xxxxx.hookdeck.io/webhooks/discord → localhost:3000/webhooks/discord # 3. Pegar esa URL en Discord Developer Portal # App → Webhooks → Endpoint URL → Guardar # Discord enviara el PING → tu handler responde 204 → endpoint registrado