Spraxium logoSpraxium

Signal no lado do bot

Implemente o receptor do Signal no runtime do seu bot com fronteiras de confiança claras e handlers tipados. Aprenda a configurar o plugin, conectar módulos, usar decorators de listener, rotear eventos e entender o ciclo completo do boot ao shutdown.

Responsabilidades no lado do bot

No lado do receptor, o Signal é responsável por proteger o runtime do seu bot antes que qualquer mutação de domínio aconteça. O SignalRegistry descobre classes de listener durante o boot, o SignalValidator aplica dez regras de transporte, o NonceCache evita ataques de replay e o SignalProcessor orquestra todo o pipeline. Seus handlers só são executados depois que todas essas camadas aprovam o envelope recebido.

Essa separação é o principal benefício arquitetural. Seus handlers de domínio continuam focados na intenção de negócio, aplicando mudanças de cargo, atualizando configurações ou publicando anúncios, enquanto a camada de protocolo assume as preocupações com confiança, formato e roteamento. Nenhum handler precisa verificar se o webhook é autorizado ou se a assinatura é válida, porque essas checagens já aconteceram antes.

Configuração do plugin

O módulo Signal no lado do bot lê sua configuração a partir de uma entrada de plugin produzida pela factory defineSignal. Essa factory é exportada por @spraxium/signal e produz uma entrada tipada de SpraxiumPlugin que deve ser adicionada ao array plugins em spraxium.config.ts. A configuração define as fronteiras de confiança e os controles operacionais do pipeline receptor.

OpçãoTipoObrigatórioFinalidade
channelIdString ou lista de stringsSim

IDs dos canais privados onde envelopes do Signal são aceitos. Mensagens de outros canais são ignoradas.

allowedWebhookIdsLista de stringsSim

IDs de webhook autorizados a publicar envelopes. Webhooks não aprovados são rejeitados mesmo que postem no canal correto.

hmacSecretstringSim

Segredo compartilhado para verificação de assinatura HMAC-SHA256. Deve coincidir exatamente com o segredo do emissor.

deleteAfterProcessingbooleanNão

Define se as mensagens de webhook devem ser excluídas do canal após o processamento. Reduz ruído, mas remove a trilha de auditoria.

debugbooleanNão

Ativa logs detalhados de diagnóstico para cada etapa de validação e cada decisão de processamento.

Padrão de configuração separada

A abordagem recomendada é extrair a configuração do Signal para um arquivo dedicado dentro de config/ e importar a constante em spraxium.config.ts. Isso deixa a política operacional explícita e permite que equipes de segurança auditem os limites do Signal sem precisar percorrer configurações não relacionadas.

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,
});

O decorator @SignalListener

O decorator @SignalListener() marca uma classe como contêiner de handlers de evento do Signal. Ele cumpre o mesmo papel que @Listener() cumpre para eventos do Discord e que @PrefixCommand() cumpre para comandos prefixados. Durante o boot, o SignalRegistry percorre instâncias de providers, identifica classes decoradas com @SignalListener() e então inspeciona cada método em busca de metadados de @OnSignal().

Internamente, @SignalListener() aplica @Injectable() automaticamente, então a classe participa da injeção de dependência sem precisar de um decorator separado. A classe precisa estar listada no array providers de um módulo que importe SignalModule.

src/modules/signal/listeners/guild-config.signal.ts
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
  }
}

O decorator @OnSignal

O decorator @OnSignal() registra um método como handler de um evento específico do Signal. O primeiro argumento é o nome do evento, que precisa coincidir exatamente com o nome usado pelo emissor. O segundo argumento é opcional e aceita um objeto de opções que pode incluir um schema Zod para validação do payload.

Quando o pipeline do Signal recebe um envelope com um nome de evento compatível, o método decorado é invocado com dois argumentos: o payload, opcionalmente validado com Zod, e o SignalEnvelope completo. O envelope dá acesso a guildId, nonce, sentAt e outros metadados úteis para escopo e auditoria.

ArgumentoTipoFinalidade
eventstring

Nome do evento ao qual este handler responde. Precisa coincidir exatamente com o nome usado pelo emissor.

options.schemaZodType

Schema Zod opcional para validar o payload. Quando presente, payloads inválidos são rejeitados antes da execução do handler.

src/modules/signal/listeners/guild-config.signal.ts
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
  }
}

Nome de evento como contrato estável

O primeiro argumento em @OnSignal('config.update') é a identidade do evento usada pelo roteamento do Signal. Ele é a chave que mapeia envelopes recebidos para o listener correto, é um contrato entre o código emissor e os listeners do bot, e também serve como referência operacional em logs, dashboards e análise de incidentes.

Trate nomes de evento como APIs estáveis de domínio. Se a semântica do payload precisar mudar de forma incompatível com a versão anterior, prefira introduzir um novo nome de evento, como config.update.v2, em vez de reaproveitar o mesmo nome silenciosamente. Renomear um evento sem coordenar produtores e listeners faz com que mensagens sejam ignoradas em silêncio ou despachadas para o handler errado.

Renomear eventos é uma quebra de compatibilidade

Se você renomear um evento sem atualizar tanto o emissor quanto o listener do bot, as mensagens serão ignoradas em silêncio porque o router não conseguirá encontrar um handler compatível para o nome antigo.

Conexão de módulo e listeners

A estrutura de módulo do Signal segue o padrão usual do Spraxium. Crie um módulo de feature que importe SignalModule e registre suas classes de listener no array providers. O SignalModule é um módulo global que fornece os serviços centrais, SignalValidator, SignalRouter, SignalProcessor, SignalRegistry e NonceCache, então você só precisa importá-lo uma vez em qualquer módulo que contenha 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 {}

Fluxo de ciclo de vida no lado do bot

Descoberta e registro de listeners

Durante o boot, o SignalRegistry registra uma função de varredura em ModuleLoader.instanceScanners. À medida que o carregador de módulos instancia cada provider, o scanner verifica se a classe da instância está decorada com @SignalListener(). Para as classes compatíveis, ele extrai todos os métodos decorados com @OnSignal() e os registra no SignalRouter.

Inscrição no messageCreate do Discord

Depois da descoberta, o método SignalRegistry.onBoot() lê a configuração do Signal em ConfigStore e inscreve um handler no evento messageCreate do cliente do Discord. Esse handler filtra mensagens recebidas e encaminha apenas o tráfego de webhook vindo dos canais configurados para o SignalProcessor.

Pipeline de validação do envelope

Para cada mensagem de webhook recebida, o SignalProcessor chama SignalValidator.validate(), que executa as dez regras em sequência. Tipo de origem, identidade do webhook, limite de canal, parsing de JSON, versão do protocolo, registro do evento, guild ID, nonce, diferença de relógio e assinatura HMAC são verificados nessa ordem.

Deduplicação por nonce e despacho do handler

Depois da validação, o processor consulta o NonceCache para impedir replays. Se o nonce for novo, ele resolve o handler por meio do SignalRouter, valida o payload com o schema Zod do handler quando houver, exclui a mensagem se isso estiver configurado e invoca o handler com o payload e o envelope.

Shutdown gracioso

Quando o bot é encerrado, SignalRegistry.onShutdown() remove o listener de messageCreate do cliente do Discord e cancela o registro do scanner em ModuleLoader.instanceScanners. Essa limpeza evita vazamentos de memória e listeners fantasmas durante hot reloads.

Injeção de dependência nos listeners

Como @SignalListener() aplica @Injectable() automaticamente, suas classes de listener participam do sistema padrão de injeção de dependência. Você pode injetar services, repositórios e outros providers via parâmetros do construtor, exatamente como faria com qualquer outra classe injetável no Spraxium.

src/modules/signal/listeners/guild-config.signal.ts
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ósticos de debug

Quando debug é definido como true na configuração do Signal, o framework emite mensagens detalhadas de diagnóstico para cada etapa do pipeline de validação e processamento. Cada mensagem inclui o prefixo [SignalValidator] ou [SignalProcessor] e informa exatamente qual regra passou ou falhou, o status do nonce, o resultado da resolução do handler e o desfecho da validação com Zod.

Essa saída de diagnóstico é muito valiosa durante a integração inicial e na investigação de incidentes. Ainda assim, deve ficar desabilitada em produção estável, porque aumenta bastante o volume de logs em canais com tráfego frequente de Signal. Um padrão prático é ativar debug apenas em ambientes neutros, de staging, ou temporariamente durante a análise de um incidente.