Por qué existe Signal en arquitecturas de bots
Signal fue diseñado para equipos que operan bots en entornos reales donde múltiples servicios externos necesitan informar cambios de dominio al bot sin crear dependencias rígidas en runtime. En lugar de conectar cada API, worker o panel de administración directamente con los internals del bot, publicas eventos estandarizados mediante webhooks de Discord y dejas que el bot los consuma por una ruta segura. Esto mantiene limpios los límites de responsabilidad: los sistemas externos se enfocan en producir eventos de negocio y el bot se enfoca en reaccionar.
El protocolo usa la infraestructura de webhooks de Discord como capa de transporte. Los servicios externos envían envelopes JSON firmados a un canal privado de Discord, y el módulo Signal del bot escucha los mensajes nuevos de ese canal. Este diseño evita exponer APIs internas del servicio, evita montar servidores de transporte personalizados y aprovecha la confiabilidad y las garantías de entrega ya existentes en Discord. La separación entre productor y consumidor es la razón principal por la que Signal escala mejor que handlers de webhook ad hoc dispersos por módulos de features.
Dos paquetes, un protocolo
El ecosistema de Signal está compuesto por dos paquetes. @spraxium/signal
es el receptor del lado del bot que valida y despacha eventos.
@spraxium/signal-client
es el emisor liviano usado por servicios externos para publicar envelopes
firmados.
Visión general de la arquitectura
El ecosistema de Signal se divide en un receptor (el bot) y uno o más emisores (servicios externos). El emisor construye un envelope firmado, lo publica como mensaje de webhook en un canal privado de Discord y el listener del bot lo recoge. El receptor valida el envelope con un pipeline estricto de diez reglas antes de despachar el evento a un handler registrado.
Esta arquitectura mantiene limpio el proceso del bot. No expone puertos HTTP entrantes para ingesta de eventos. En su lugar, aprovecha el evento messageCreate de Discord, al que el bot ya está suscrito. El módulo Signal solo añade un filtro que identifica mensajes de webhook de fuentes aprobadas y los enruta al pipeline de validación.
| Componente | Paquete | Responsabilidad |
|---|---|---|
| SignalClient | @spraxium/signal-client | Construye envelopes, los firma con HMAC y los entrega por webhook POST. |
| SignalModule | @spraxium/signal | Módulo global que integra el pipeline receptor en el runtime del bot. |
| SignalValidator | @spraxium/signal | Ejecuta diez reglas secuenciales de validación sobre mensajes entrantes. |
| SignalProcessor | @spraxium/signal | Orquesta validación, deduplicación por nonce, parsing con Zod y despacho a handlers. |
| SignalRouter | @spraxium/signal | Mapea nombres de evento a funciones handler registradas. |
| SignalRegistry | @spraxium/signal | Descubre clases @SignalListener, registra handlers y conecta el listener de messageCreate. |
| NonceCache | @spraxium/signal | Rastrea nonces procesados con un TTL de 60 segundos para protección ante replays. |
| HmacService | @spraxium/signal | Calcula y verifica firmas HMAC-SHA256 con comparación segura frente a timing attacks. |
Estructura de carpetas recomendada
El contrato del envelope
Cada mensaje de Signal sigue una única forma de envelope. Esta estructura estandarizada asegura que la validación de transporte ocurra antes de ejecutar cualquier código de negocio. El envelope es un objeto JSON con siete campos, cada uno con un rol específico en el pipeline de validación y despacho.
| Campo | Tipo | Finalidad |
|---|---|---|
| v | 1 | Versión del protocolo usada para control de compatibilidad. Siempre |
| event | string | Nombre del evento de dominio que se mapea a un listener del bot (por ejemplo,
|
| guildId | string | Contexto del servidor objetivo para despliegues de bot multi-guild. |
| nonce | string | UUID único de este envelope, usado para deduplicación de replays. |
| sentAt | number | Timestamp Unix en milisegundos cuando se creó el envelope. |
| signature | string | Digest hex HMAC-SHA256 para verificación de integridad y confianza de origen. |
| payload | unknown | Datos de dominio libres que se entregan al handler resuelto en el bot. |
{
"v": 1,
"event": "config.update",
"guildId": "123456789012345678",
"nonce": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"sentAt": 1712937600000,
"signature": "8f14e45fceea167a5a36dedd4bea2543",
"payload": {
"prefix": "!",
"language": "en"
}
}Modelo de seguridad HMAC
La capa HMAC es el límite de seguridad que le indica al bot si un envelope fue producido por un emisor autorizado y si el contenido se mantuvo intacto en tránsito. La firma se calcula concatenando los campos nonce, sentAt y payload en un solo string (${nonce}.${sentAt}.${JSON.stringify(payload)}), y luego aplicando HMAC-SHA256 con una clave secreta compartida.
En el lado del bot se realiza el mismo cálculo y el resultado se compara con la firma recibida usando una función de comparación segura por tiempo. Si los valores no coinciden, el envelope se rechaza antes de ejecutar cualquier lógica de negocio. Esto protege frente a tres categorías de ataque: manipulación del payload en tránsito, replay con datos modificados y envelopes forjados por servicios no autorizados.
En términos prácticos, HMAC permite que tu equipo confíe en automatizaciones que vienen de dashboards externos, sistemas de facturación o backoffices de moderación sin exponer APIs internas del servicio al proceso del bot. El secreto compartido debe ser el mismo en emisor y receptor, y nunca debe commitearse al control de versiones.
El secreto compartido debe coincidir exactamente
El valor hmacSecret en defineSignal del lado del bot
debe ser idéntico al valor hmacSecret en el constructor de
SignalClient. Si difieren aunque sea por un carácter,
todos los envelopes se rechazarán como no confiables.
Pipeline de validación de diez reglas
Antes de que se ejecute cualquier handler de dominio, cada mensaje entrante pasa por un pipeline estricto de diez reglas implementado en SignalValidator. Cada regla se evalúa en secuencia, y la primera falla detiene el pipeline de inmediato. Este diseño fail-fast garantiza que los mensajes inválidos consuman el mínimo tiempo de procesamiento.
| Regla | Verificación | Se rechaza si |
|---|---|---|
| R1 | Origen del webhook | El mensaje no tiene la propiedad webhookId. |
| R2 | Identidad del webhook | El webhookId no está en el array allowedWebhookIds. |
| R3 | Límite de canal | El canal del mensaje no coincide con el channelId configurado. |
| R4 | Parseo de JSON | El contenido del mensaje no es JSON válido o no es un objeto. |
| R5 | Versión del protocolo | El campo v no es exactamente 1. |
| R6 | Registro del evento | El nombre event no está registrado en ningún handler del bot. |
| R7 | Validez de guild ID | El guildId está vacío o no es string. |
| R8 | Validez de nonce | El nonce está vacío o no es string. |
| R9 | Desfase de reloj | El timestamp sentAt se desvía más de 5 minutos del reloj del bot. |
| R10 | Firma HMAC | La signature no coincide con el digest HMAC-SHA256 recalculado. |
Pipeline completo de procesamiento
Después de que el validador aprueba un envelope, SignalProcessor continúa con tres pasos adicionales antes de ejecutar el handler de dominio. Entender el pipeline completo te ayuda a depurar problemas de entrega y a razonar sobre el orden de operaciones durante el análisis de incidentes.
Validación de diez reglas
SignalValidator revisa cada mensaje entrante contra diez reglas secuenciales. La primera falla rechaza el mensaje y deja una entrada de diagnóstico cuando el modo debug está habilitado. Solo los mensajes que superan las diez reglas pasan a la siguiente etapa.
Deduplicación de nonce
NonceCache verifica si ese nonce ya fue visto en los últimos 60 segundos. Si ya existía, el mensaje se trata como replay y se descarta en silencio. Si es nuevo, el nonce se registra con un timestamp. Las entradas expiradas se desalojan automáticamente en la siguiente comprobación.
Resolución de handler y enrutamiento
SignalRouter busca el handler registrado para ese nombre de evento. Si no existe handler, el mensaje se registra como no enrutable y se descarta. Esto no debería ocurrir porque la regla R6 ya verificó que el evento estaba registrado, pero esta validación del router agrega defensa en profundidad.
Validación de payload con Zod
Si el handler resuelto tiene un schema Zod adjunto mediante las opciones de @OnSignal, el processor ejecuta safeParse sobre el payload. Si la validación falla, el mensaje se rechaza con un log de diagnóstico. Si no hay schema configurado, el payload crudo se pasa sin cambios.
Borrado opcional del mensaje
Si deleteAfterProcessing está habilitado en la config de Signal, el processor intenta borrar el mensaje de webhook del canal de Discord. Los fallos de borrado se registran como warnings, pero no bloquean la ejecución del handler.
Ejecución del handler
El processor invoca el handler resuelto con el payload validado y el envelope completo. Los errores lanzados por el handler se capturan, se registran y no rompen el pipeline de Signal. Los demás eventos continúan procesándose normalmente.
Casos de uso prácticos
| Caso de uso | Ajuste | Por qué funciona bien |
|---|---|---|
| Sistema de facturación notificando cambios de roles | Fuerte | Los sistemas de negocio emiten eventos mientras el bot aplica cambios de roles del servidor de forma segura y asíncrona. |
Backoffice de moderación publicando acciones de enforcement | Fuerte | El backoffice permanece desacoplado y el bot maneja de forma independiente la ejecución orientada a Discord. |
| Panel de admin en Next.js usando Server Actions | Fuerte | Las Server Actions publican eventos firmados directamente, manteniendo simple la lógica de UI y asíncronas las operaciones del bot. |
| Pipeline CI/CD disparando anuncios del bot | Fuerte | Los pasos del pipeline emiten eventos tras despliegues exitosos y el bot publica notas de versión formateadas. |
| Workflows síncronos de request-response | Débil | Signal es asíncrono y unidireccional por diseño. Los contratos HTTP directos son mejores para respuestas inmediatas. |
Cuándo evitar Signal
Si el productor y el consumidor viven en el mismo grafo de módulos y no necesitas límites asíncronos, las llamadas directas a servicios son más simples de razonar y más fáciles de trazar. Signal debería adoptarse cuando sus garantías operativas, su modelo de seguridad y sus beneficios de desacoplamiento sean realmente útiles, no como opción por defecto para cualquier interacción interna. Para bucles de feedback síncronos orientados al usuario donde el llamador necesita respuesta inmediata, endpoints HTTP o llamadas directas a la API de Discord siguen siendo la mejor elección.