Logotipo Hedhog

Receba novidades do Hedhog no seu e-mail

Novos lançamentos, receitas e breaking changes — sem spam.

Submódulo inbox

webhooks

Recepção pública de webhooks do Resend para eventos de entrega/engajamento e e-mails de entrada (recebidos), verificada por assinatura HMAC em vez do guard de papel/auth da plataforma.

Caminho de origem: libraries/inbox/src/message (message.controller.ts: InboxWebhookController), resend-integration.service.ts

Arquivo do módulo: message.module.ts

Introdução

O submódulo Webhooks é o `InboxWebhookController`, montado em `/inbox/webhook` e decorado com `@Public()` — ele é declarado no mesmo arquivo que `MessageController` e compartilha o `message.module.ts`, mas é registrado como seu próprio controller com seu próprio requisito de auth (ausente). O `route.yaml` ainda lista relações de papel (`admin`, `admin-inbox`, `inbox-owner`) para todas as suas três rotas, mas essa configuração não tem efeito em runtime: o guard de auth do NestJS verifica os metadados de `@Public()` antes de avaliar qualquer relação de papel e faz um curto-circuito, pulando a verificação de papel inteiramente para um controller marcado como `@Public()`. Na prática, essas três rotas ficam abertas a qualquer chamador — as entradas em role.yaml/route.yaml para elas são vestigiais e não devem ser lidas como o controle de acesso real. A autenticação efetiva deste controller acontece uma camada abaixo, dentro de `ResendIntegrationService.assertValidSignature`: todo corpo de webhook é verificado contra uma assinatura HMAC-SHA256 calculada a partir da configuração `inbox.resend.webhook-secret`, suportando tanto os headers nativos do Resend `x-resend-signature`/`x-resend-timestamp` quanto os headers no formato Svix `svix-id`/`svix-timestamp`/`svix-signature` (os webhooks do Resend são baseados em Svix), com uma comparação timing-safe e tolerante a hex ou base64. Uma requisição com assinatura ausente ou inválida é rejeitada com um 401 antes de qualquer processamento de payload; uma configuração `inbox.resend.webhook-secret` ausente rejeita toda requisição de imediato.

`POST /inbox/webhook` é um endpoint unificado que inspeciona o formato do payload para decidir como roteá-lo: `isInboundWebhookPayload` trata o corpo como um e-mail de entrada (recebido) se seu campo `type`/`event`/`name` contiver `email.received`, `inbound` ou `received`, ou se o objeto `data` do payload tiver campos `from`/`to`/`subject` mesmo sem uma string de tipo correspondente; caso contrário, se `body.type` for uma string, ele é roteado como um evento de entrega/engajamento; o fallback final (nenhum tipo reconhecível) também roteia como entrada. `POST /inbox/webhook/events` e `POST /inbox/webhook/inbound` pulam essa detecção e sempre roteiam para o handler respectivo, que é o ponto de integração mais seguro quando a configuração do endpoint do Resend pode especificar URLs separadas por categoria de webhook.

Ambos os handlers não processam o payload inline — eles chamam `IntegrationDeveloperApiService.publishEvent` para colocar o corpo bruto na outbox de Integration da plataforma (`inbox.resend.webhook.event.received` / `inbox.resend.webhook.inbound.received`) e retornam `{ accepted: true, outboxEventId }` imediatamente. `ResendIntegrationService.onModuleInit` inscreve seus próprios consumidores nesses mesmos nomes de evento, de modo que os efeitos colaterais reais — atualizar o status de `mail_event`/`inbox_message` para eventos de entrega, ou criar uma mensagem/thread de entrada totalmente nova — rodam de forma assíncrona e são retentados pelo subsistema de Integration independentemente do ciclo original de requisição/resposta HTTP.

O processamento de entrada (`processInboundWebhook`) é o caminho mais complexo do módulo: ele resolve a `inbox_account` de destino correspondendo ao e-mail do destinatário "to", deduplica por `provider_message_id`, faz o threading da nova mensagem usando uma cascata de três estratégias — primeiro pelos tokens de header `In-Reply-To`/`References` contra o `provider_message_id` de uma mensagem de saída anterior, depois pelo assunto normalizado mais o remetente aparecendo como destinatário de uma mensagem de saída anterior na mesma conta, e então apenas pelo assunto normalizado — e, quando um token de API está configurado, busca o corpo HTML/texto completo e os anexos na API do Resend por `email_id` (recorrendo a quaisquer campos de corpo presentes no payload do webhook, ou fazendo o parse de um blob MIME bruto com `mailparser`, se nenhum token estiver definido). Os anexos são baixados e reenviados por meio do serviço de File da plataforma para `inbox_attachment` em regime de melhor esforço (falhas de upload são logadas, não lançadas), e todo proprietário/membro da conta é notificado via `NotificationService` assim que a mensagem é persistida.

Endpoints HTTP

3 endpoints

Recepção unificada de webhooks. Inspeciona o payload para decidir se é um evento de entrega/engajamento ou um e-mail de entrada (recebido) e então o publica na outbox de Integration para processamento assíncrono.

Body

FieldTypeRequiredNotes
(raw body)Record<string, any>yesResend webhook payload; shape depends on event vs. inbound

Sempre roteia como um webhook de evento de entrega/engajamento, independentemente do formato do payload.

Body

FieldTypeRequiredNotes
(raw body)Record<string, any>yesResend delivery/engagement event payload (sent, delivered, bounced, complained, opened, clicked, failed, delivery_delayed)

Sempre roteia como um webhook de entrada (e-mail recebido), independentemente do formato do payload.

Body

FieldTypeRequiredNotes
(raw body)Record<string, any>yesResend inbound (received email) payload