Discord Webhooks — NexoBot SaaS ✓ Ed25519

Handler Express completo para recibir y verificar eventos de Discord — generado con la skill discord-webhooks

Flujo de verificacion

Discord → NexoBot API
Discord
envia evento
🔒
Verificar
Ed25519
Enrutar
por tipo
Persistir
en BD
204
Responder
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
discord-webhook-handler.js
JavaScript
// 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
terminal
bash
# 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