Responsabilidades del lado del bot
En el lado receptor, Signal se encarga de proteger el runtime del bot antes de que ocurra cualquier mutación de dominio. SignalRegistry descubre clases listener durante el arranque, SignalValidator aplica diez reglas de transporte, NonceCache evita ataques de replay y SignalProcessor orquesta el pipeline completo. Tus handlers solo se ejecutan después de que todas esas capas aprueban el envelope entrante.
Esta separación es el beneficio arquitectónico clave. Tus handlers de dominio se mantienen enfocados en la intención de negocio, como aplicar cambios de roles, actualizar configuraciones o publicar anuncios, mientras la capa de protocolo se encarga de confianza, formato y enrutamiento. Ningún handler necesita comprobar si el webhook está autorizado o si la firma es válida, porque esas validaciones ya ocurrieron antes.
Configuración del plugin
El módulo Signal del lado del bot lee su configuración desde una entrada de plugin producida por la factory defineSignal. Esta factory se exporta desde @spraxium/signal y produce una entrada tipada SpraxiumPlugin que debe agregarse al array plugins en spraxium.config.ts. La configuración define los límites de confianza y los controles operativos del pipeline receptor.
| Opción | Tipo | Requerido | Propósito |
|---|---|---|---|
| channelId | String o lista de strings | Sí | IDs de canales privados donde se aceptan envelopes de Signal. Los mensajes de otros canales se ignoran. |
| allowedWebhookIds | Lista de strings | Sí | IDs de webhook autorizados para publicar envelopes. Los webhooks no aprobados se rechazan incluso si publican en el canal correcto. |
| hmacSecret | string | Sí | Secreto compartido para verificación de firma HMAC-SHA256. Debe coincidir exactamente con el secreto del emisor. |
| deleteAfterProcessing | boolean | No | Define si se borran los mensajes de webhook del canal después del procesamiento. Reduce ruido, pero elimina rastro de auditoría. |
| debug | boolean | No | Habilita logs de diagnóstico detallados para cada paso de validación y decisión de procesamiento. |
Patrón de configuración separada
El enfoque recomendado es extraer la configuración de Signal a un archivo dedicado dentro de config/ e importar la constante en spraxium.config.ts. Así dejas explícita la política operativa y permites que equipos de seguridad auditen los límites de Signal sin tener que analizar configuración no relacionada.
import { defineSignal } from '@spraxium/signal';
export const signalPlugin = defineSignal({
channelId: process.env.SIGNAL_CHANNEL_ID!,
allowedWebhookIds: [process.env.SIGNAL_WEBHOOK_ID!],
hmacSecret: process.env.SIGNAL_HMAC_SECRET!,
deleteAfterProcessing: true,
debug: false,
});El decorator @SignalListener
El decorator @SignalListener() marca una clase como contenedor de handlers de eventos de Signal. Cumple el mismo rol que @Listener() para eventos de Discord y @PrefixCommand() para comandos prefix. Durante el arranque, SignalRegistry escanea instancias de providers, identifica las clases decoradas con @SignalListener() y luego inspecciona cada método en busca de metadatos @OnSignal().
Internamente, @SignalListener() aplica @Injectable() automáticamente, por lo que la clase participa en inyección de dependencias sin requerir un decorator adicional. La clase debe listarse en el array providers de un módulo que importe SignalModule.
import { SignalListener, OnSignal } from '@spraxium/signal';
import type { SignalEnvelope } from '@spraxium/signal';
@SignalListener()
export class GuildConfigSignal {
@OnSignal('config.update')
async onConfigUpdate(
payload: unknown,
envelope: SignalEnvelope,
): Promise<void> {
// Domain logic anchored to envelope.guildId
}
}El decorator @OnSignal
El decorator @OnSignal() registra un método como handler de un evento específico de Signal. El primer argumento es el nombre del evento, que debe coincidir exactamente con el nombre usado por el emisor. El segundo argumento es opcional y acepta un objeto de opciones que puede incluir un schema Zod para validación de payload.
Cuando el pipeline de Signal recibe un envelope con un nombre de evento coincidente, el método decorado se invoca con dos argumentos: el payload (opcionalmente validado por Zod) y el SignalEnvelope completo. El envelope te da acceso a guildId, nonce, sentAt y otros metadatos para alcance y auditoría.
| Argumento | Tipo | Propósito |
|---|---|---|
| event | string | Nombre del evento al que responde este handler. Debe coincidir exactamente con el nombre de evento del emisor. |
| options.schema | ZodType | Schema Zod opcional para validación de payload. Si se proporciona, los payloads inválidos se rechazan antes de ejecutar el handler. |
import { SignalListener, OnSignal } from '@spraxium/signal';
import type { SignalEnvelope } from '@spraxium/signal';
import { z } from 'zod';
const ConfigUpdateSchema = z.object({
prefix: z.string().min(1).max(5),
language: z.enum(['en', 'pt', 'es']).optional(),
});
@SignalListener()
export class GuildConfigSignal {
@OnSignal('config.update', { schema: ConfigUpdateSchema })
async onConfigUpdate(
payload: z.infer<typeof ConfigUpdateSchema>,
envelope: SignalEnvelope,
): Promise<void> {
// payload is validated and typed, envelope provides guild context
}
}El nombre del evento como contrato estable
El primer argumento en @OnSignal('config.update') es la identidad del evento usada por el enrutamiento de Signal. Es la clave que mapea envelopes entrantes al listener correcto, es un contrato productor-consumidor entre código emisor y listeners del bot, y es una referencia operativa para logs, dashboards y análisis de incidentes.
Trata los nombres de evento como APIs de dominio estables. Si la semántica del payload debe cambiar de forma no retrocompatible, conviene introducir un nombre nuevo como config.update.v2 en vez de reutilizar el mismo en silencio. Renombrar un evento sin coordinar productores y listeners hace que mensajes se ignoren en silencio o se despachen al handler equivocado.
Renombrar nombres de eventos es un cambio breaking
Si renombras un evento sin actualizar tanto el emisor como el listener del bot, los mensajes se ignorarán en silencio porque el router no puede encontrar un handler que coincida con el nombre antiguo.
Cableado de módulos y listeners
La estructura de módulos para Signal sigue el patrón estándar de Spraxium. Crea un módulo de feature que importe SignalModule y registra tus clases listener en el array providers. SignalModule es un módulo global que provee los servicios base (SignalValidator, SignalRouter, SignalProcessor, SignalRegistry, NonceCache), así que solo necesitas importarlo una vez en cualquier módulo que contenga listeners.
import { Module } from '@spraxium/common';
import { SignalModule } from '@spraxium/signal';
import { GuildConfigSignal } from './listeners/guild-config.signal';
import { BillingSignal } from './listeners/billing.signal';
@Module({
imports: [SignalModule],
providers: [GuildConfigSignal, BillingSignal],
})
export class AppSignalModule {}Flujo de ciclo de vida del lado del bot
Descubrimiento y registro de listeners
Durante el arranque, SignalRegistry registra una función scanner en ModuleLoader.instanceScanners. A medida que el module loader instancia cada provider, el scanner revisa si la clase de la instancia está decorada con @SignalListener(). Para las clases que coinciden, extrae todos los métodos decorados con @OnSignal() y los registra en SignalRouter.
Suscripción a Discord messageCreate
Después del descubrimiento, el método SignalRegistry.onBoot() lee la config de Signal desde ConfigStore y suscribe un handler al evento messageCreate del cliente de Discord. Ese handler filtra mensajes entrantes y pasa al SignalProcessor solo tráfico de webhook desde canales configurados.
Pipeline de validación de envelope
Para cada mensaje webhook entrante, SignalProcessor llama a SignalValidator.validate() que ejecuta las diez reglas secuenciales. Tipo de fuente, identidad del webhook, límite de canal, parseo JSON, versión de protocolo, registro de evento, guild ID, nonce, desfase de reloj y firma HMAC se verifican en orden.
Deduplicación de nonce y despacho al handler
Después de validar, el processor consulta NonceCache para evitar reprocesar replays. Si el nonce es nuevo, resuelve el handler mediante SignalRouter, valida opcionalmente el payload con el schema Zod del handler, borra el mensaje si está configurado y finalmente invoca al handler con payload y envelope.
Apagado ordenado
Cuando el bot se apaga, SignalRegistry.onShutdown() remueve el listener de messageCreate del cliente de Discord y desregistra el scanner de ModuleLoader.instanceScanners. Esta limpieza evita fugas de memoria y listeners fantasma durante hot reloads.
Inyección de dependencias en listeners
Como @SignalListener() aplica @Injectable() automáticamente, tus clases listener participan en el sistema estándar de inyección de dependencias. Puedes inyectar services, repositorios y otros providers por parámetros del constructor, igual que en cualquier otra clase inyectable de Spraxium.
import { SignalListener, OnSignal } from '@spraxium/signal';
import type { SignalEnvelope } from '@spraxium/signal';
import { GuildSettingsService } from '../../services/guild-settings.service';
@SignalListener()
export class GuildConfigSignal {
constructor(private readonly guildSettings: GuildSettingsService) {}
@OnSignal('config.update')
async onConfigUpdate(
payload: { prefix: string },
envelope: SignalEnvelope,
): Promise<void> {
await this.guildSettings.updatePrefix(envelope.guildId, payload.prefix);
}
}Diagnóstico en modo debug
Cuando debug se establece en true en la config de Signal, el framework emite mensajes de diagnóstico detallados para cada paso del pipeline de validación y procesamiento. Cada mensaje incluye un prefijo [SignalValidator] o [SignalProcessor] e informa la regla exacta que pasó o falló, el estado del nonce, el resultado de resolución de handler y el resultado de validación Zod.
Esta salida de diagnóstico es muy valiosa durante la integración inicial y la investigación de incidentes. Aun así, conviene deshabilitarla en producción estable porque incrementa significativamente el volumen de logs en canales con tráfico frecuente de Signal. Un patrón práctico es habilitar debug solo en entornos neutral/staging o temporalmente durante el análisis de incidentes.