Por que o Signal existe em arquiteturas de bots
Signal foi criado para equipes que operam bots em cenários reais, nos quais vários serviços externos precisam avisar o bot sobre mudanças de domínio sem criar dependências rígidas em tempo de execução. Em vez de acoplar cada API, worker ou painel administrativo diretamente aos internals do bot, você publica eventos padronizados por webhooks do Discord e deixa o bot consumi-los por um único caminho seguro. Isso mantém os limites de responsabilidade claros, porque os sistemas externos ficam focados em produzir eventos de negócio, enquanto o bot fica focado em reagir a eles.
O protocolo usa a própria infraestrutura de webhooks do Discord como camada de transporte. Serviços externos enviam envelopes JSON assinados para um canal privado do Discord, e o módulo Signal do bot escuta as novas mensagens desse canal. Esse desenho evita expor APIs internas do serviço, evita subir servidores de transporte sob medida e aproveita a confiabilidade e as garantias de entrega que o Discord já oferece. Essa separação entre produtor e consumidor é o principal motivo de o Signal escalar melhor do que handlers de webhook improvisados espalhados pelos módulos da aplicação.
Dois pacotes, um protocolo
O ecossistema do Signal é composto por dois pacotes. @spraxium/signal
é o receptor no lado do bot, responsável por validar e despachar eventos.
@spraxium/signal-client
é o emissor leve usado por serviços externos para publicar envelopes
assinados.
Visão geral da arquitetura
O ecossistema do Signal se divide em um receptor, o bot, e um ou mais emissores, que são os serviços externos. O emissor monta um envelope assinado, publica esse envelope como mensagem de webhook em um canal privado do Discord, e o listener do bot o captura. O receptor valida o envelope por meio de um pipeline rígido de dez regras antes de despachar o evento para um handler registrado.
Essa arquitetura mantém o processo do bot limpo. Ele nunca expõe portas HTTP de entrada para ingestão de eventos. Em vez disso, aproveita o evento messageCreate do Discord, ao qual o bot já está inscrito. O módulo Signal apenas adiciona um filtro que identifica mensagens de webhook vindas de fontes aprovadas e as encaminha para o pipeline de validação.
| Componente | Pacote | Responsabilidade |
|---|---|---|
| SignalClient | @spraxium/signal-client | Monta envelopes, assina com HMAC e entrega via POST de webhook. |
| SignalModule | @spraxium/signal | Módulo global que conecta o pipeline do receptor ao runtime do bot. |
| SignalValidator | @spraxium/signal | Executa dez regras sequenciais de validação sobre mensagens de entrada. |
| SignalProcessor | @spraxium/signal | Orquestra validação, deduplicação por nonce, parsing com Zod e despacho do handler. |
| SignalRouter | @spraxium/signal | Mapeia nomes de evento para funções de handler registradas. |
| SignalRegistry | @spraxium/signal | Descobre classes com @SignalListener, registra handlers e conecta o listener de messageCreate. |
| NonceCache | @spraxium/signal | Rastreia nonces já processados com TTL de 60 segundos para proteção contra replay. |
| HmacService | @spraxium/signal | Calcula e verifica assinaturas HMAC-SHA256 com comparação segura contra timing attacks. |
Estrutura de pastas recomendada
O contrato do envelope
Toda mensagem do Signal segue um único formato de envelope. Essa estrutura padronizada garante que a validação de transporte aconteça antes de qualquer código de negócio ser executado. O envelope é um objeto JSON com sete campos, e cada um cumpre um papel específico no pipeline de validação e despacho.
| Campo | Tipo | Finalidade |
|---|---|---|
| v | 1 | Versão do protocolo usada para controle de compatibilidade. Na versão atual, é sempre 1. |
| event | string | Nome do evento de domínio que mapeia para um listener no bot, como config.update. |
| guildId | string | Contexto da guild de destino em implantações de bot com múltiplas guilds. |
| nonce | string | UUID exclusivo deste envelope, usado para deduplicação contra replay. |
| sentAt | number | Timestamp Unix em milissegundos de quando o envelope foi criado. |
| signature | string | Digest hexadecimal HMAC-SHA256 usado para verificar integridade e confiança na origem. |
| payload | unknown | Dados de domínio livres, entregues ao handler do bot que for resolvido. |
{
"v": 1,
"event": "config.update",
"guildId": "123456789012345678",
"nonce": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"sentAt": 1712937600000,
"signature": "8f14e45fceea167a5a36dedd4bea2543",
"payload": {
"prefix": "!",
"language": "en"
}
}Modelo de segurança com HMAC
A camada de HMAC é a fronteira de segurança que informa ao bot se um envelope foi produzido por um emissor autorizado e se o conteúdo permaneceu intacto durante o trajeto. A assinatura é calculada concatenando os campos nonce, sentAt e payload em uma única string (${nonce}.${sentAt}.${JSON.stringify(payload)}) e depois aplicando HMAC-SHA256 com uma chave secreta compartilhada.
No lado do bot, o mesmo cálculo é executado e o resultado é comparado com a assinatura recebida por meio de uma função de comparação segura contra timing attacks. Se os valores não coincidirem, o envelope é rejeitado antes de qualquer lógica de negócio ser executada. Isso protege contra três categorias de ataque: adulteração do payload em trânsito, replay com dados alterados e envelopes forjados por serviços não autorizados.
Na prática, o HMAC permite que sua equipe confie em automações vindas de dashboards externos, sistemas de billing ou backoffices de moderação sem expor APIs internas de serviço ao processo do bot. O segredo compartilhado precisa ser o mesmo nos dois lados, emissor e receptor, e nunca deve ser commitado no controle de versão.
O segredo compartilhado precisa ser idêntico
O valor de hmacSecret em defineSignal, no lado do bot,
precisa ser idêntico ao valor de hmacSecret passado no
construtor de SignalClient. Se houver diferença, mesmo que seja
de um único caractere, todo envelope será rejeitado como não confiável.
Pipeline de validação em dez regras
Antes que qualquer handler de domínio seja executado, toda mensagem recebida passa por um pipeline rígido de validação em dez regras, implementado em SignalValidator. Cada regra é avaliada em sequência, e a primeira falha interrompe o pipeline imediatamente. Esse desenho fail-fast garante que mensagens inválidas consumam o mínimo possível de processamento.
| Regra | Verificação | Rejeita quando |
|---|---|---|
| R1 | Origem do webhook | A mensagem não possui a propriedade webhookId. |
| R2 | Identidade do webhook | O valor de webhookId não está no array allowedWebhookIds. |
| R3 | Limite de canal | O canal da mensagem não corresponde ao channelId configurado. |
| R4 | Parsing de JSON | O conteúdo da mensagem não é um JSON válido ou não é um objeto. |
| R5 | Versão do protocolo | O campo v não é exatamente 1. |
| R6 | Registro do evento | O nome em event não está registrado em nenhum handler do bot. |
| R7 | Validade do guild ID | O valor de guildId está vazio ou não é uma string. |
| R8 | Validade do nonce | O valor de nonce está vazio ou não é uma string. |
| R9 | Diferença de relógio | O timestamp em sentAt se desvia mais de 5 minutos do relógio do bot. |
| R10 | Assinatura HMAC | O valor de signature não corresponde ao digest HMAC-SHA256 recalculado. |
Pipeline completo de processamento
Depois que o validador aprova um envelope, o SignalProcessor segue pelas demais etapas até a execução do handler de domínio. Entender o fluxo completo ajuda a depurar problemas de entrega e a raciocinar sobre a ordem das operações durante a análise de incidentes.
Validação em dez regras
O SignalValidator verifica cada mensagem recebida contra dez regras em sequência. A primeira falha rejeita a mensagem e registra uma entrada de diagnóstico quando o modo debug está habilitado. Só mensagens que passam por todas as dez regras seguem para a próxima etapa.
Deduplicação por nonce
O NonceCache verifica se esse nonce já apareceu nos últimos 60 segundos. Se já apareceu, a mensagem é tratada como replay e descartada em silêncio. Se for novo, o nonce é registrado com timestamp. Entradas expiradas são removidas automaticamente na próxima verificação.
Resolução e roteamento do handler
O SignalRouter procura o handler registrado para esse nome de evento. Se nenhum handler existir, a mensagem é registrada como sem rota e descartada. Isso não deveria acontecer porque a regra R6 já confirmou que o evento foi registrado, mas a checagem no router adiciona uma camada extra de defesa.
Validação do payload com Zod
Se o handler resolvido tiver um schema Zod associado nas opções de @OnSignal, o processor executa safeParse sobre o payload. Se a validação falhar, a mensagem é rejeitada com um log de diagnóstico. Se não houver schema configurado, o payload bruto segue sem alterações.
Exclusão opcional da mensagem
Se deleteAfterProcessing estiver habilitado na configuração do Signal, o processor tenta excluir a mensagem de webhook do canal do Discord. Falhas na exclusão são registradas como warning, mas não impedem a execução do handler.
Execução do handler
O processor invoca o handler resolvido com o payload validado e o envelope completo. Erros lançados pelo handler são capturados, registrados e não derrubam o pipeline do Signal. Os demais eventos continuam sendo processados normalmente.
Casos de uso práticos
| Caso de uso | Aderência | Por que funciona bem |
|---|---|---|
| Sistema de billing notificando atualizações de cargo | Alta | Sistemas de negócio emitem eventos enquanto o bot aplica alterações de cargo na guild de forma segura e assíncrona. |
Backoffice de moderação publicando ações de enforcement | Alta | O backoffice permanece desacoplado e o bot cuida da execução voltada ao Discord de forma independente. |
| Painel administrativo em Next.js usando Server Actions | Alta | Server Actions publicam eventos assinados diretamente, mantendo a lógica de UI simples e a operação do bot assíncrona. |
| Pipeline de CI/CD disparando anúncios do bot | Alta | Etapas do pipeline emitem eventos após deploys bem-sucedidos e o bot publica notas de release formatadas. |
| Fluxos síncronos de request-response | Baixa | O Signal é assíncrono e unidirecional por definição. Contratos HTTP diretos funcionam melhor para respostas imediatas. |
Quando evitar o Signal
Se produtor e consumidor vivem no mesmo grafo de módulos e você não precisa de fronteiras assíncronas, chamadas diretas de serviço são mais simples de entender e mais fáceis de rastrear. O Signal deve ser adotado quando suas garantias operacionais, seu modelo de segurança e os benefícios de desacoplamento realmente fazem diferença, não como padrão para toda interação interna. Para loops síncronos de feedback voltados ao usuário, em que o chamador precisa de resposta imediata, endpoints HTTP ou chamadas diretas à API do Discord continuam sendo escolhas melhores.