Slack
AlphaMensagens do Slack dos seus clientes entrando e saindo do seu produto
Seu produto passa a ler e responder mensagens no Slack de cada cliente, com o workspace dele conectado por OAuth, sem que você construa um app Slack, um verificador de assinatura e uma fila de entrega do zero.
- Produtos de assistente ou copiloto que precisam conversar com o usuário onde ele já está
- Plataformas B2B cujo cliente quer receber alerta operacional no canal do time dele
- Times que já usam o WPP ou o WPP Business da Catalisa e querem o Slack como mais um canal
- Um app Slack próprio com OAuth, verificação de assinatura e gestão de token por workspace
- Assinatura de uma ferramenta de automação (Zapier, Make) só para levar mensagem de Slack ao seu backend
- Camada caseira de filtro de ruído para não inundar seu sistema com toda mensagem do workspace
- Uma plataforma de notificação multicanal (isso é o Webhooks Engine somado aos BBs de canal)
- Um bot pronto ou um framework de conversa — o building block só transporta a mensagem
- Um arquivo de mensagens do Slack — nada de conteúdo de conversa é persistido aqui
- Um substituto do Slack Connect ou do Slack Enterprise Grid
10 endpoints em 5 recursos.
Resumo executivo
O Slack conecta o workspace de cada cliente ao seu produto nos dois sentidos. O usuário autoriza uma vez pelo botão do Slack, e a partir daí as mensagens que interessam — DMs, menções e os canais que ele escolheu observar — chegam ao seu backend por webhook, e o seu produto pode responder no mesmo lugar.
Na prática: um assistente que já atende pelo WhatsApp passa a atender pelo Slack sem nova arquitetura. O usuário manda "qual o status da proposta 4471?" numa DM, o evento chega ao seu endpoint em segundos, e a resposta volta pelo POST /messages/send como se tivesse sido digitada pelo próprio usuário.
Está em alpha. São 8 endpoints, e a lista do que falta é longa e honesta: não há como desconectar um workspace pela API, não há listar nem apagar dispatcher, o token não é renovado, e o app Slack ainda depende de aprovação da Slack para ser instalado fora do workspace de desenvolvimento. Ler a §15 antes de prometer qualquer coisa a cliente não é recomendação, é requisito.
| Atributo | Valor |
|---|---|
| Identificador | slack |
| Categoria | Comunicação |
| Escopo | Tenant (exige organizationId no token) |
| Porta (standalone) | 3029 |
| Path alias | @slack |
| Prefixo HTTP | /slack/api/v1/slack — sim, slack aparece duas vezes |
| Status | Alpha desde 2026-06 |
| Depende de | PostgreSQL (schema slack), IAM, Webhooks Engine, Slack Platform |
Atenção ao prefixo. O app Hono usa basePath('/slack') e os routers são montados em /api/v1/slack/.... A rota completa fica /slack/api/v1/slack/messages/send, com o nome do módulo repetido. É o que o código faz hoje. Toda rota da §9 está na forma completa e real.
O problema
negócioO cenário. Seu produto precisa falar com o usuário dentro do Slack da empresa dele. Não o seu Slack — o dele. E não um workspace, mas o de cada cliente que você atende, cada um com seu próprio consentimento, seus próprios canais e sua própria política de segurança.
O que trava hoje.
- Um app Slack de verdade é um projeto, não um endpoint. Para instalar em mais de um workspace, o app precisa de distribuição pública ativada, o que a Slack condiciona a uma lista de requisitos: "Your app must support SSL for all of the following URLs", fluxo de onboarding para múltiplos workspaces, e pedir só as permissões necessárias, "admins screen these during approval" (Slack — Distributing Slack apps, consulta em 2026-08-16). Depois disso vem OAuth por workspace e um token por instalação para guardar e cifrar.
- A janela de resposta é de três segundos. A Slack é explícita: você deve "respond to the event request with an HTTP 2xx within three seconds", e falhando isso ela reenvia três vezes — "nearly immediately", depois de 1 minuto e depois de 5 minutos (Slack — Events API, consulta em 2026-08-16). Qualquer processamento síncrono dentro do handler vira mensagem duplicada em produção.
- Verificação de assinatura é obrigatória e fácil de errar. A Slack assina cada requisição, e a verificação exige o corpo bruto — reserializar o JSON antes de calcular o HMAC quebra a comparação de forma silenciosa. Some a isso a janela de tolerância de tempo contra reenvio de requisição capturada.
- O volume de um workspace inunda qualquer backend. Ligar o Events API sem filtro significa receber toda mensagem de todo canal, incluindo entrada e saída de gente, edição, remoção e mensagem de outros bots. Filtrar isso é código, e é código que precisa rodar antes da fila, não depois.
- Ferramenta de automação não resolve multi-tenant. Zapier e Make são ótimos para um fluxo. Para cem clientes com cem workspaces, cada um com seus canais, o modelo de cobrança por tarefa e a ausência de contexto de tenant transformam a solução no problema.
O custo de não resolver. A soma é: um app distribuído aprovado, OAuth por workspace, cifra e ciclo de vida de token por instalação, verificação de assinatura com corpo bruto, ack em menos de três segundos com processamento assíncrono, e filtro de ruído. Nada disso é a sua regra de negócio, e tudo isso precisa existir antes do primeiro cliente ver valor.
Posto em forma de lista de tarefas, é o que você entrega antes da primeira funcionalidade visível:
| Peça obrigatória | Por que ela existe | É regra de negócio sua? |
|---|---|---|
| App Slack distribuído e aprovado | Sem distribuição pública, o app só instala no seu próprio workspace | Não |
| OAuth por workspace | Cada cliente autoriza o próprio workspace, com o próprio consentimento | Não |
| Cifra e ciclo de vida de token por instalação | Um token por instalação, guardado em repouso e mantido válido | Não |
| Verificação de assinatura sobre o corpo bruto | Rota pública sem token da plataforma; a assinatura é a única defesa | Não |
| Ack em menos de 3 segundos, com processamento assíncrono | Exigência da Slack; falhar nela produz mensagem duplicada | Não |
| Filtro de ruído antes da fila | Sem ele, o workspace inteiro entra no seu backend | Não |
Proposta de valor
negócio| Antes | Depois |
|---|---|
| Construir e manter um app Slack distribuído por conta própria | Um app configurado uma vez, com o OAuth por workspace já implementado |
| Token de instalação guardado como der | AES-256-GCM no banco, envelope cifrado, nunca devolvido pela API |
| Handler que processa dentro dos 3 segundos e reenvia duplicado | Ack imediato e processamento disparado fora do caminho da resposta |
| Todo evento do workspace chegando ao seu backend | DM, menção e canais escolhidos — o resto é descartado antes da entrega |
| Uma integração de entrega por canal (WhatsApp, Slack, ...) | O mesmo Webhooks Engine, com assinatura, retry e log, para todos |
O building block é bidirecional, e vale ver os dois sentidos separados antes de entrar em cada ganho:
flowchart LR
subgraph Entrada["Entrada — o usuário fala"]
U1["Usuário escreve<br/>no Slack"] --> EV["POST /events<br/>assinatura + ack 3s"]
EV --> FIL["shouldForward<br/>filtro de ruído"]
FIL --> WE["Webhooks Engine"]
WE --> BE["Seu backend"]
end
subgraph Saida["Saída — o produto responde"]
BE2["Seu backend"] --> SEND["POST /messages/send"]
SEND --> SL["chat.postMessage<br/>as_user true"]
SL --> U2["Mensagem no canal,<br/>com identidade humana"]
endOAuth por workspace, pronto
POST /connections/connect devolve a URL de autorização com o state cifrado; o callback troca o código por token, identifica o time e grava a conexão. O integrador não escreve OAuth.
Ack primeiro, trabalho depois
O handler de eventos verifica a assinatura, responde 200 e dispara a ingestão sem esperar. É a arquitetura que a janela de três segundos da Slack exige, e ela já está feita.
Filtro de ruído no lugar certo
A função shouldForward descarta subtipos de ruído (bot_message, message_changed, message_deleted, channel_join, channel_leave), deixa passar toda DM e todo grupo de DM, deixa passar qualquer mensagem que mencione o usuário conectado, e para canais comuns só entrega os que estão em watchedChannels. O que sai daqui é sinal.
Entrega pela infraestrutura que já existe
O dispatcher cria uma assinatura no Webhooks Engine — a mesma que o WPP e o WPP Business usam — com timeout de 10 segundos e três tentativas em backoff exponencial. Assinatura, retry e log de entrega não são reimplementados aqui.
Casos de uso reais
negócioCaso 1 — Um assistente ganha o Slack como canal sem nova arquitetura Cenário ilustrativo
Produto de assistente para times de operações que já atendia por WhatsApp usando o building block WPP. Os clientes corporativos pediram Slack.
A primeira estimativa do time para "colocar Slack" foi de seis a oito semanas: app distribuído, OAuth por workspace, verificação de assinatura, ack em três segundos, filtro e uma fila de entrega própria. Nenhuma dessas semanas produziria uma linha de valor para o usuário — o assistente em si já existia.
O cliente conecta o workspace por POST /slack/api/v1/slack/connections/connect. Um POST /slack/api/v1/slack/dispatchers aponta para o mesmo endpoint que já recebia os eventos do WPP, com watchedChannels definindo os canais observados. O evento chega como slack.v1.message.received com o mesmo envelope de dispatcher que o time já sabia consumir. A resposta sai por POST /slack/api/v1/slack/messages/send.
flowchart LR WA["WhatsApp<br/>via BB wpp"] --> WE["Webhooks Engine"] SK["Slack<br/>via BB slack"] --> WE WE --> CONS["Consumidor único<br/>já existente<br/>/hooks/mensagens"] CONS --> ASS["Assistente<br/>regra de produto"] ASS -->|"resposta"| OUT1["POST /messages/send<br/>Slack"] ASS -->|"resposta"| OUT2["Envio WPP"]
O canal novo reaproveita o consumidor existente. O trabalho que sobra é de produto — o que o assistente responde no Slack —, não de plataforma.
Caso 2 — Um alerta operacional chega no canal do cliente, não numa caixa de e-mail Cenário ilustrativo
Plataforma de logística que avisa quando uma entrega ultrapassa a janela combinada.
Os avisos iam por e-mail para uma lista. Ninguém lia, porque a decisão sobre a entrega atrasada é tomada no canal #operacao-sp do Slack, onde o time já está. O e-mail chegava, era encaminhado para o Slack por uma pessoa, e o tempo de resposta era o tempo dessa pessoa.
O cliente conecta o workspace e a plataforma passa a postar em #operacao-sp por POST /messages/send. Como a mensagem é enviada com o token do usuário que instalou o app (as_user: true), ela aparece com identidade humana no canal, não como bot anônimo — o que muda a taxa de leitura.
sequenceDiagram autonumber participant P as "Plataforma de logística" participant S as "BB Slack" participant SL as "Slack — canal #operacao-sp" participant T as "Time de operação" participant WE as "Webhooks Engine" P->>S: "POST /messages/send — entrega 4471 fora da janela" S->>SL: "chat.postMessage as_user true" SL-->>T: "Aviso aparece com identidade humana" T->>SL: "Responde no canal — abrir tratativa" SL->>S: "POST /events — assinatura verificada" S->>S: "shouldForward — canal está em watchedChannels" S->>WE: "slack.v1.message.received" WE->>P: "Entrega assinada — vira registro de tratativa"
O aviso nasce onde a decisão acontece. E, como o mesmo dispatcher observa o canal, a resposta que o time escreve volta ao produto e vira registro de tratativa.
Caso 3 — O filtro de ruído é o que torna a ingestão viável Cenário ilustrativo
Cliente com um workspace de 400 pessoas e cerca de 90 canais ativos.
Um piloto anterior, feito com uma ferramenta de automação genérica, ligou o Events API sem filtro. O backend recebeu tudo: entrada e saída de gente em canal, edição de mensagem, mensagem de outros bots, thread de canal irrelevante. O volume derrubou a fila no primeiro dia útil, e o custo por tarefa da ferramenta estourou o orçamento do piloto na primeira semana.
O shouldForward roda antes da entrega: descarta os cinco subtipos de ruído, entrega toda DM e MPIM, entrega qualquer texto contendo <@USER_ID> do usuário conectado, e para canais comuns exige que o canal esteja em watchedChannels. Os canais observados são ajustados por PATCH /dispatchers/:id sem recriar nada.
flowchart LR
WS["Workspace<br/>400 pessoas · 90 canais"] --> EV["Todo evento do<br/>Events API"]
EV --> F{"shouldForward<br/>roda no BB,<br/>antes da entrega"}
F -->|"bot, editada, apagada,<br/>entrada e saída de canal"| D["Descartado<br/>não trafega, não é cobrado"]
F -->|"canal comum fora<br/>de watchedChannels"| D
F -->|"DM e grupo de DM"| E["Entregue ao backend"]
F -->|"menção ao usuário conectado"| E
F -->|"canal em watchedChannels"| EAtenção. A ferramenta genérica do piloto filtrava depois de receber — e cobrava por tudo que recebeu. Aqui o filtro roda dentro do processo do building block, antes da fila e antes de qualquer entrega. Em workspace grande, essa diferença de posição é a diferença entre o projeto caber e não caber no orçamento.
Do workspace inteiro, chega ao backend a fração que tem endereço: conversa direta, menção explícita e os canais que alguém escolheu observar.
Caso 4 — Distribuir um app Slack tem um portão, e ele é público Referência de mercado
Quem planeja "integrar Slack" costuma orçar a API e esquecer o processo de distribuição.
A própria Slack documenta que instalar em outros workspaces exige OAuth 2.0, SSL em todas as URLs e uma checklist de distribuição pública, e que "apps intended for commercial distribution should be submitted and approved for listing in the Slack Marketplace" (Slack — Distributing Slack apps, consulta em 2026-08-16). Distribuição não listada é possível para clientes iniciais, mas escopos de histórico de conversa passam por escrutínio, e o administrador de cada workspace corporativo ainda pode exigir aprovação individual.
O building block implementa o lado técnico inteiro — OAuth v2 com state cifrado, verificação de assinatura, ack de três segundos, escopos de usuário declarados e documentados. O runbook `docs/slack/SETUP.md` descreve o processo de criação do app, o manifesto pronto e a política de aprovação por tipo de workspace.
flowchart LR A["Lado técnico<br/>o BB resolve"] --> A1["OAuth v2 com state cifrado"] A --> A2["Verificação de assinatura"] A --> A3["Ack de três segundos"] A --> A4["Escopos declarados<br/>e documentados"] B["Portão externo<br/>ninguém resolve pelo cliente"] --> B1["Revisão do app pela Slack"] B --> B2["Aprovação do administrador<br/>de cada workspace"] B --> B3["Enquanto não sai, o alcance é<br/>o workspace de instalação — §15"]
O que resta ao cliente é a parte que ninguém pode fazer por ele: o processo de aprovação junto à Slack e ao administrador do workspace. Ressalva honesta: enquanto a revisão do app não sai, o alcance é o workspace de instalação. Está em §15.
Mercado e diferenciais
negócioPanorama. Colocar Slack dentro de um produto tem três caminhos. Construir o app direto na Slack Platform dá controle total e cobra em engenharia: distribuição, OAuth por workspace, ciclo de vida de token, assinatura, ack de três segundos. Usar uma ferramenta de automação — Zapier, Make, n8n — resolve um fluxo em uma tarde e cobra por execução, com o fluxo vivendo fora do seu código. Contratar uma plataforma de notificação — Knock, Courier — resolve muito bem o caminho de saída e quase não toca o de entrada.
O Slack da Catalisa fica num quarto lugar, estreito de propósito: é o transporte bidirecional por workspace, dentro de uma plataforma que o cliente já usa para identidade e entrega de eventos. Não compete em amplitude com o Zapier nem em orquestração com o Knock.
flowchart TD P["Colocar Slack<br/>dentro do produto"] --> C1["Slack Platform direta"] P --> C2["Automação<br/>Zapier · Make · n8n"] P --> C3["Notificação<br/>Knock · Courier"] P --> C4["Catalisa Slack"] C1 --> R1["Controle total.<br/>Cobra em engenharia"] C2 --> R2["Um fluxo numa tarde.<br/>Cobra por execução,<br/>fluxo fora do seu código"] C3 --> R3["Saída muito bem resolvida.<br/>Quase não toca a entrada"] C4 --> R4["Transporte bidirecional<br/>por workspace, dentro da<br/>plataforma já contratada"]
| Critério | Catalisa Slack | Slack API direta | Zapier | n8n | Knock |
|---|---|---|---|---|---|
| Ingestão de mensagem do usuário | Sim, com filtro de ruído | Você constrói | Sim, por trigger | Sim | Não é o foco |
| Envio de mensagem | Sim, como o usuário conectado | Sim | Sim | Sim | Sim, como bot |
| Multi-tenant por workspace | Sim, ligado à organização do IAM | Você constrói | Por conta conectada | Você constrói | Por tenant do produto |
| Modelo de preço | Em definição | Sem licença de API | Por tarefa (Free 100/mês; Pro a partir de US$ 19,99/mês) | Cloud a partir de 20 €/mês; self-host sem licença | Free 10 mil msg; Starter US$ 250/mês |
| Onde o fluxo vive | No seu código | No seu código | Na conta Zapier | No n8n | No Knock |
| Filtro de ruído embutido | Sim | Você constrói | Você configura | Você configura | Não se aplica |
| Retry e assinatura na entrega | Via Webhooks Engine | Você constrói | Interno | Interno | Interno |
| Preferências por usuário e agrupamento | Não | Não | Não | Não | Sim, é o forte deles |
| Block Kit, modais, Socket Mode | Não (§15) | Sim | Parcial | Parcial | Parcial |
| Maturidade | Alpha (§15) | Produção | Produção | Produção | Produção |
Preços consultados nas páginas públicas em 2026-08-16: Zapier, n8n, Knock, Courier, Make. Variam por região, plano e negociação.
Nossos diferenciais
- O filtro roda antes da entrega, não depois.
shouldForwardé uma função pura, testada, que decide no processo do building block. Uma ferramenta de automação recebe tudo e filtra depois — e cobra por tudo que recebeu. Em workspace grande, essa diferença é a viabilidade do projeto. - O canal reaproveita a infraestrutura de entrega dos outros canais. O dispatcher do Slack é o mesmo padrão do WPP e do WPP Business, sobre o mesmo Webhooks Engine. Quem já consome WhatsApp consome Slack com o mesmo verificador de assinatura e a mesma semântica de retry.
- A mensagem sai com identidade humana. O envio usa o token do usuário que instalou o app, com
as_user: true. Em canal operacional, mensagem de bot anônimo é ignorada e mensagem com rosto é lida. - O token do workspace fica na plataforma do cliente, cifrado. AES-256-GCM sob chave mestra própria do módulo, no banco da própria Catalisa, sem terceiro no meio do caminho entre o Slack e o produto.
Quando escolher o concorrente.
flowchart TD
Q1{"O problema é notificação de saída<br/>orquestrada — preferências, agrupamento,<br/>digest, fallback entre canais?"}
Q1 -->|Sim| K["Knock ou Courier.<br/>Este BB não resolve — não é o que ele é"]
Q1 -->|Não| Q2{"Quem precisa da integração<br/>é time de negócio, não de engenharia?"}
Q2 -->|Sim| Z["Zapier. Entrega numa tarde o que<br/>aqui exige endpoint HTTP e deploy"]
Q2 -->|Não| Q3{"Precisa de Block Kit, modais,<br/>slash commands ou Socket Mode?"}
Q3 -->|Sim| SP["Slack Platform direta.<br/>Nada disso está aqui — ver §15"]
Q3 -->|Não| Q4{"Você atende um workspace só,<br/>o seu?"}
Q4 -->|Sim| DIR["Construa direto. Mais simples que<br/>adotar um building block em alpha"]
Q4 -->|Não| CAT["Catalisa Slack: ingestão bidirecional,<br/>muitos workspaces de muitos clientes,<br/>na plataforma que já entrega WhatsApp"]Em prosa: se o problema é notificação de saída bem orquestrada — preferências por usuário, agrupamento, digest, múltiplos canais com fallback —, o Knock e o Courier resolvem hoje e este building block não resolve nunca, porque não é o que ele é. Se o time que precisa da integração é de negócio e não de engenharia, o Zapier entrega em uma tarde o que aqui exige um endpoint HTTP e um deploy. Se você precisa de Block Kit, modais, slash commands ou Socket Mode, a Slack Platform direta é o único caminho — nada disso está aqui (§15). E se você atende um workspace, o seu, construir direto é mais simples que adotar um building block em alpha. O Slack da Catalisa ganha num caso específico: ingestão bidirecional, muitos workspaces de muitos clientes, dentro de uma plataforma que já entrega WhatsApp pelo mesmo caminho.
Modelo de cobrança e ROI
negócioUnidade de cobrança. Precificação em definição. O building block está em alpha e não tem preço fechado. Não há valor a informar, e não vamos inventar um.
O que dispara custo.
| Driver | Por que pesa |
|---|---|
| Workspaces conectados | Cada conexão é um token cifrado para guardar e um conjunto de dispatchers para manter |
| Mensagens ingeridas | Cada evento aceito pelo filtro vira uma entrega pelo Webhooks Engine, com retry se falhar |
| Mensagens enviadas | Cada POST /messages/send é uma chamada chat.postMessage, sujeita ao limite da Slack |
Comparação de custo — cenário: 20 clientes com 20 workspaces, cerca de 30 mil mensagens ingeridas e 10 mil enviadas por mês, depois do filtro de ruído.
| Catalisa Slack | Zapier | n8n Cloud | Knock | |
|---|---|---|---|---|
| Base de cálculo | Em definição | Por tarefa | Por execução de workflow | Por mensagem |
| Ordem de grandeza mensal | A definir | 40 mil tarefas/mês fica bem acima da faixa de entrada de US$ 19,99 | 40 mil execuções exigem o plano Business, na casa de 667 €/mês | 40 mil mensagens cabem no Starter de US$ 250/mês |
| Ingestão bidirecional | Sim | Sim | Sim | Não é o foco |
| Filtro antes de contar | Sim | Não — conta o que entra | Não | Não se aplica |
| App Slack distribuído | Você ainda precisa aprovar o seu (§15) | Zapier usa o app deles | Você constrói | Knock usa o app deles |
Estimativas montadas sobre as tabelas públicas consultadas em 2026-08-16 e citadas na §5. São ordens de grandeza para orientar conversa, não proposta comercial. Os valores da Catalisa não estão definidos.
ROI. O retorno não está na linha de licença — para volume baixo, quase tudo é barato. Está em duas coisas.
| Fonte do retorno | O que acontece | Ordem de grandeza |
|---|---|---|
| Trabalho de plataforma que não é feito | App distribuído, OAuth por workspace, cifra de token, verificação de assinatura, ack de três segundos e filtro | Semanas de engenharia que não produzem valor visível ao usuário |
| Efeito do filtro sobre quem cobra por evento | Contar tudo o que o workspace produz versus contar só o que tem endereço | Uma ordem de grandeza, em workspace grande |
A primeira é o trabalho de plataforma que não é feito: app distribuído, OAuth por workspace, cifra de token, verificação de assinatura, ack de três segundos e filtro somam semanas que não produzem valor visível ao usuário. A segunda é o efeito do filtro no modelo de custo de qualquer ferramenta que cobre por evento: em workspace grande, a diferença entre contar tudo e contar o que tem endereço é de uma ordem de grandeza.
Arquitetura
Camadas e caminho da requisição
flowchart TD
HTTP["HTTP"] --> APP
subgraph APP["Hono app — basePath /slack"]
R1["/api/v1/slack/connections<br/>connectionRouter · 3 rotas"]
R2["/api/v1/slack/dispatchers<br/>dispatcherRouter · 2 rotas"]
R3["/api/v1/slack/messages<br/>messagesRouter · 1 rota"]
R4["/api/v1/slack/channels<br/>channelsRouter · 1 rota"]
R5["/api/v1/slack/events<br/>slackEventsRouter · 1 rota — chamada pela Slack"]
R6["/health e /connected<br/>página de retorno pós-OAuth"]
end
APP --> SVC
subgraph SVC["services/"]
S1["SlackConnectionService<br/>OAuth v2, state cifrado, upsert"]
S2["SlackEventsService<br/>assinatura, filtro de ruído, fan-out"]
S3["SlackSendService<br/>chat.postMessage como o usuário"]
S4["SlackChannelsService<br/>conversations.list"]
S5["SlackDispatcherService<br/>cria assinatura no Webhooks Engine"]
end
SVC --> REPO["repositories/ Prisma<br/>schema slack · 2 modelos"]
SVC --> WC["@slack/web-api WebClient<br/>api.slack.com"]O ciclo completo — autorizar, cifrar, ligar a entrega, receber push
Este é o fluxo ponta a ponta, das quatro etapas que o integrador percorre uma vez por workspace. O state do OAuth e o token de usuário usam o mesmo esquema AES-256-GCM do building block Calendar — iv:authTag:ciphertext em hex, IV aleatório de 12 bytes por operação — só que sob chave mestra própria do módulo.
sequenceDiagram
autonumber
participant U as "Usuário do cliente"
participant APP as "Seu produto"
participant BB as "BB Slack"
participant DB as "PostgreSQL — schema slack"
participant SL as "Slack Platform"
participant WE as "Webhooks Engine"
rect rgb(238, 244, 255)
note over U,SL: "1. Autorizar"
APP->>BB: "POST /connections/connect"
BB->>BB: "encryptCredentials — organizationId, userId, redirectUrl"
BB-->>APP: "authorizationUrl com state cifrado e user_scope"
APP-->>U: "Redireciona para slack.com/oauth/v2/authorize"
U->>SL: "Autoriza o app no workspace"
SL-->>BB: "GET /connections/callback — code e state"
end
rect rgb(240, 248, 240)
note over BB,DB: "2. Guardar a credencial cifrada"
BB->>BB: "decryptCredentials do state — falha vira Invalid callback state"
BB->>SL: "oauth.v2.access — troca code por token de usuário"
SL-->>BB: "authed_user.access_token, team.id, scopes"
BB->>BB: "encryptCredentials — AES-256-GCM, IV de 12 bytes"
BB->>DB: "upsert em slack_connections — chave org + teamId + slackUserId"
BB-->>U: "Redireciona para o redirectUrl do state"
end
rect rgb(255, 248, 236)
note over APP,WE: "3. Ligar a entrega — o equivalente ao sincronizar"
APP->>BB: "POST /dispatchers — endpointUrl e watchedChannels"
BB->>WE: "createSubscription — filtra slack.v1.message.received"
WE-->>BB: "subscriptionId"
BB-->>APP: "secret de 32 bytes, entregue uma única vez"
end
rect rgb(252, 240, 244)
note over U,APP: "4. Receber o push da Slack"
U->>SL: "Escreve uma DM ou menciona o usuário conectado"
SL->>BB: "POST /events — x-slack-signature e timestamp"
BB->>BB: "Lê o corpo bruto e verifica HMAC-SHA256 em tempo constante"
BB-->>SL: "200 ok em menos de 3 segundos"
BB->>DB: "Resolve conexão por teamId e authorizations[0]"
BB->>BB: "shouldForward — filtro de ruído"
BB->>WE: "emitSlackPublic — slack.v1.message.received"
WE->>APP: "Entrega assinada, com retry"
endAtenção. Não existe passo de sincronização por varredura, como num BB de calendário: a Slack empurra o evento. O que a etapa 3 liga é o caminho de saída da entrega, não uma sincronização periódica. E o BB não renova token — ver §15.
Caminho de uma mensagem que entra, em detalhe
flowchart TD
U["Usuário escreve no Slack"] --> P["POST /slack/api/v1/slack/events<br/>rota pública — sem token da plataforma"]
P --> A1["1. Lê o corpo BRUTO como texto,<br/>antes de qualquer parse"]
A1 --> A2{"2. x-slack-signature confere<br/>contra SLACK_SIGNING_SECRET?<br/>HMAC-SHA256 de v0:timestamp:corpo<br/>timestamp dentro de 5 minutos?"}
A2 -->|Não| E401["401 invalid signature"]
A2 -->|Sim| A3{"3. O corpo é JSON válido?"}
A3 -->|Não| E400["400 invalid json"]
A3 -->|Sim| A4{"4. type é url_verification?"}
A4 -->|Sim| CH["200 com o challenge em texto puro"]
A4 -->|Não| A5{"5. event_callback de<br/>message ou app_mention?"}
A5 -->|Sim| FF["Dispara ingestEvent SEM aguardar"]
A5 -->|Não| ACK
FF --> ACK["6. Responde 200 imediatamente<br/>janela de 3s da Slack"]
FF --> ING
subgraph ING["SlackEventsService.ingestEvent"]
I1["1. Conexão por team_id e authorizations[0]"]
I2["2. Dispatchers ACTIVE da conexão"]
I3["3. União dos watchedChannels"]
I4["4. shouldForward — descarta bot_message,<br/>message_changed, message_deleted,<br/>channel_join e channel_leave;<br/>passa im, mpim, menção e canal observado"]
I5["5. emitSlackPublic<br/>slack.v1.message.received"]
I1 --> I2 --> I3 --> I4 --> I5
end
ING --> WE["Webhooks Engine<br/>assinatura · retry · log"]
WE --> CLI["Endpoint do cliente"]Decisões não óbvias.
- A assinatura é verificada em toda requisição, inclusive no
url_verification. Muita implementação libera o desafio de verificação antes de checar assinatura, porque é o primeiro POST que a Slack manda. Aqui não: semSLACK_SIGNING_SECRETconfigurado, ou com assinatura inválida, a resposta é401mesmo para o desafio. Custa um passo a mais na configuração e fecha um caminho em que qualquer um posta na sua rota pública. - O corpo é lido como texto antes de qualquer parse.
await c.req.text()vem primeiro, e o HMAC é calculado sobre esses bytes. Fazer o parse e reserializar muda espaçamento e ordem de chave, e a assinatura deixa de bater — de forma silenciosa e intermitente, que é o pior modo de falhar. - A comparação usa
timingSafeEqual, com verificação de comprimento antes. Comparação de string com===vaza informação por tempo de execução. E otimingSafeEqualdo Node lança exceção com buffers de tamanhos diferentes, por isso o comprimento é conferido antes. - Tolerância de 5 minutos no timestamp. Requisição capturada e reenviada depois disso é rejeitada. Cinco minutos é a folga que absorve relógio dessincronizado sem abrir janela de repetição relevante.
- A ingestão é disparada e esquecida. O
void service.ingestEvent(...)sai do caminho da resposta de propósito: a Slack exige2xxem três segundos e reenvia três vezes se não receber (Slack — Events API, 2026-08-16). Processar antes de responder produz mensagem duplicada sob carga. O preço é que uma falha na ingestão vira log dewarn, não erro HTTP — o cliente do Slack não fica sabendo. - Erro de ingestão é sempre não fatal. Conexão não encontrada, dispatcher ausente ou falha de publicação não derrubam a resposta. Evento de workspace que ninguém conectou é ignorado em silêncio, o que é o comportamento certo para uma rota pública.
- O app OAuth é global, não por organização.
SLACK_CLIENT_ID,SLACK_CLIENT_SECRETeSLACK_CALLBACK_URLvêm do ambiente e são registrados uma vez no container. É uma diferença deliberada em relação ao Calendar, que guarda credenciais OAuth por tenant: no Slack existe um app da plataforma, e cada cliente instala esse app no workspace dele. O trade-off é que a tela de consentimento leva a marca do app da plataforma, não a do cliente. - O envio usa
as_user: true. A mensagem sai com a identidade do usuário que instalou, não de um bot. É intencional — o token pedido éuser_scope, não bot token — e tem consequência: o app precisa do escopochat:writede usuário, e a mensagem conta como escrita por aquela pessoa. - O filtro considera a união dos
watchedChannelsde todos os dispatchers ativos. Se um dispatcher observa#ae outro observa#b, a mensagem de#apassa pelo filtro e é publicada uma vez; a separação por dispatcher acontece no Webhooks Engine, pelos filtros da assinatura.
Monolito vs. standalone. Em monolito, o Slack é resolvido pelo container TypeDI e o SlackDispatcherService fala com o Webhooks Engine por chamada local. Em standalone — o modo de produção — sobe como serviço próprio (porta 3029 no compose local) e o dispatcher usa a facade remota: sem MODULE_WEBHOOKS_ENGINE_URL configurado, POST /dispatchers responde 503. Está anotado no próprio stack de deploy.
Conceitos e modelo de dados
Glossário
| Termo | Significa |
|---|---|
| Connection | O vínculo entre um usuário da plataforma, uma organização e um workspace do Slack. Guarda o token cifrado. Único por (organizationId, teamId, slackUserId). |
| Team | O workspace do Slack. O teamId (T…) é como a Slack o identifica em todo evento. |
| Slack user id | O identificador do usuário no workspace (U…). Quem instalou o app e em nome de quem as mensagens saem. |
| Dispatcher | A regra de encaminhamento: para qual endpoint entregar e quais canais observar. Cria uma assinatura no Webhooks Engine. |
| Watched channels | Lista de channelId observados. Só afeta canais comuns — DM e menção passam sempre. |
| Signing secret | Segredo do app Slack usado para verificar a assinatura de cada requisição recebida. Global, do ambiente. |
| Envelope cifrado | JSON {accessToken, refreshToken} cifrado com AES-256-GCM e gravado como uma string. |
| Evento público | slack.v1.message.received, publicado no barramento e entregue pelo Webhooks Engine. |
Modelo de dados — schema slack no PostgreSQL.
| Modelo Prisma | Tabela | Propósito | Campos-chave |
|---|---|---|---|
SlackConnection | slack.slack_connections | Workspace conectado por um usuário | teamId, teamName, slackUserId, accessToken (envelope cifrado), scopes[], status, único (organizationId, teamId, slackUserId) |
SlackDispatcher | slack.slack_dispatchers | Regra de encaminhamento | endpointUrl, customHeaders, watchedChannels[], subscriptionId, status |
São dois modelos e um relacionamento — a conexão é o dono, o dispatcher é a regra pendurada nela:
erDiagram
SlackConnection ||--o{ SlackDispatcher : "tem"
SlackConnection {
uuid id PK
uuid organizationId "do JWT, nunca do corpo"
uuid userId "usuário da plataforma"
string teamId "workspace T..."
string teamName
string slackUserId "usuário U... que instalou"
string accessToken "envelope AES-256-GCM"
string_array scopes
enum status "só ACTIVE é atribuído hoje"
}
SlackDispatcher {
uuid id PK
uuid connectionId FK
uuid organizationId
string endpointUrl "para onde o Webhooks Engine entrega"
json customHeaders "inclui x-webhook-secret"
string_array watchedChannels "só afeta canal comum"
string subscriptionId "assinatura no Webhooks Engine"
enum status "só ACTIVE é atribuído hoje"
}Enumerações
| Enum | Valores |
|---|---|
SlackConnectionStatus | ACTIVE e demais estados do schema. Só ACTIVE é atribuído hoje |
SlackDispatcherStatus | ACTIVE e demais estados do schema. Só ACTIVE é atribuído hoje |
Escopos de usuário solicitados
| Escopo | Para quê |
|---|---|
im:history · im:read | Ler e listar DMs |
mpim:history · mpim:read | Ler e listar grupos de DM |
channels:history · channels:read | Ler e listar canais públicos dos quais o usuário participa |
groups:history · groups:read | Ler e listar canais privados dos quais o usuário participa |
users:read | Resolver nome e perfil |
chat:write | Postar como o usuário |
O token de usuário só alcança as DMs daquele usuário e os canais em que ele já está. Não há caminho para ler conversa privada de terceiros.
Decisão do filtro de ruído
As quatro perguntas são avaliadas nessa ordem, e a primeira que responde decide. É uma função pura, shouldForward, testada isoladamente:
flowchart TD
EV["Evento chega"] --> Q1{"subtype está em bot_message,<br/>message_changed, message_deleted,<br/>channel_join ou channel_leave?"}
Q1 -->|Sim| DROP1["DESCARTA"]
Q1 -->|Não| Q2{"channelType é im ou mpim?"}
Q2 -->|Sim| OK1["ENTREGA — DM ou grupo de DM"]
Q2 -->|Não| Q3{"O texto contém a menção<br/>ao slackUserId da conexão?"}
Q3 -->|Sim| OK2["ENTREGA — menção"]
Q3 -->|Não| Q4{"channelId está na união dos<br/>watchedChannels dos dispatchers ACTIVE?"}
Q4 -->|Sim| OK3["ENTREGA — canal observado"]
Q4 -->|Não| DROP2["DESCARTA"]Ciclo de vida da conexão
O caminho até ACTIVE passa por três etapas, e não há saída dele pela API hoje:
stateDiagram-v2
[*] --> Solicitada : "POST /connections/connect<br/>gera state cifrado com<br/>organizationId, userId e redirectUrl"
Solicitada --> Consentida : "usuário autoriza em<br/>slack.com/oauth/v2/authorize"
Consentida --> ACTIVE : "GET /connections/callback (rota pública)<br/>decifra o state, oauth.v2.access,<br/>cifra o token, faz upsert"
Solicitada --> Falha : "state adulterado ou<br/>chave mestra trocada"
Consentida --> Falha : "resposta do OAuth sem<br/>authed_user.access_token"
Falha --> [*] : "400 Invalid callback state"
ACTIVE --> ACTIVE : "reconectar faz upsert na mesma<br/>linha e limpa deletedAt"
note right of ACTIVE
Não há endpoint de desconexão — ver §15.
Não há renovação de token — ver §15.
Os demais valores do enum
SlackConnectionStatus existem no schema
mas nenhum caminho de código os atribui.
end noteReferência da API
Prefixo real: /slack/api/v1/slack. O nome do módulo aparece duas vezes porque o app usa basePath('/slack') e os routers são montados sob /api/v1/slack. Em standalone, a base é https://slack.stg.catalisa.app.
Salvo indicação de rota pública, toda rota exige authMiddleware, o requirePermission(...) da tabela e requireOrganization — token sem organizationId recebe 403 com Organization context required.
Conexões — /slack/api/v1/slack/connections
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /slack/api/v1/slack/connections/connect | Devolve a URL de autorização do Slack | SLACK_WRITE |
GET | /slack/api/v1/slack/connections/callback | Callback OAuth. Rota pública — sem authMiddleware | Pública |
GET | /slack/api/v1/slack/connections | Lista as conexões do usuário do token | SLACK_READ |
Dispatchers — /slack/api/v1/slack/dispatchers
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /slack/api/v1/slack/dispatchers | Cria dispatcher e a assinatura no Webhooks Engine. 201 | SLACK_WRITE |
PATCH | /slack/api/v1/slack/dispatchers/:id | Atualiza apenas watchedChannels | SLACK_WRITE |
Mensagens e canais
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /slack/api/v1/slack/messages/send | Envia mensagem como o usuário conectado | SLACK_WRITE |
GET | /slack/api/v1/slack/channels?teamId=&slackUserId= | Lista canais, DMs e grupos da conexão | SLACK_READ |
Eventos do Slack — rota pública
| Método | Rota | Descrição | Permissão |
|---|---|---|---|
POST | /slack/api/v1/slack/events | Recebe url_verification e event_callback da Slack | Pública, autenticada por x-slack-signature |
Saúde e retorno de OAuth
| Método | Rota | Descrição |
|---|---|---|
GET | /slack/health | Sonda de vida e versão do build |
GET | /slack/connected | Página HTML estática de confirmação, para usar como redirectUrl pós-OAuth |
POST /slack/api/v1/slack/connections/connect
Request
{ "redirectUrl": "https://app.suaempresa.com.br/integracoes/slack/pronto" }{ "redirectUrl": "https://app.suaempresa.com.br/integracoes/slack/pronto" }| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
redirectUrl | string (URL) | Sim | Para onde o usuário volta depois do callback. Viaja cifrado no state |
Resposta 200
{
"authorizationUrl": "https://slack.com/oauth/v2/authorize?client_id=...&user_scope=im%3Ahistory%2C...&state=..."
}{
"authorizationUrl": "https://slack.com/oauth/v2/authorize?client_id=...&user_scope=im%3Ahistory%2C...&state=..."
}Note o user_scope: o fluxo pede token de usuário, não de bot.
Erros
| Status | Quando |
|---|---|
400 | redirectUrl ausente ou não é URL válida |
403 | Sem SLACK_WRITE ou sem organização no token |
500 | SLACK_CREDENTIAL_MASTER_KEY ausente ou malformada — a cifra do state falha |
POST /slack/api/v1/slack/dispatchers
Cria a regra de encaminhamento e, junto com ela, a assinatura no Webhooks Engine.
Request
{
"connectionId": "8f14e45f-ceea-467a-9e1a-2b3c4d5e6f70",
"teamId": "T01ABCDEF",
"slackUserId": "U01ABCDEF",
"endpointUrl": "https://api.suaempresa.com.br/hooks/slack",
"watchedChannels": ["C01OPERACAO", "C02SUPORTE"],
"customHeaders": { "X-Origem": "slack" }
}{
"connectionId": "8f14e45f-ceea-467a-9e1a-2b3c4d5e6f70",
"teamId": "T01ABCDEF",
"slackUserId": "U01ABCDEF",
"endpointUrl": "https://api.suaempresa.com.br/hooks/slack",
"watchedChannels": ["C01OPERACAO", "C02SUPORTE"],
"customHeaders": { "X-Origem": "slack" }
}| Campo | Tipo | Obrigatório | Descrição |
|---|---|---|---|
connectionId | string (UUID) | Sim | Conexão à qual o dispatcher pertence |
teamId | string | Sim | Workspace. Compõe o nome da assinatura |
slackUserId | string | Sim | Usuário conectado. Compõe o nome da assinatura |
endpointUrl | string (URL) | Sim | Para onde o Webhooks Engine entrega |
watchedChannels | string[] | Não | Canais comuns observados. Padrão [] — só DM e menção passam |
customHeaders | object | Não | Cabeçalhos extras em cada entrega |
Resposta 201
{
"data": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"secret": "c0ffee…",
"subscriptionId": "aaaa-bbbb-cccc-dddd"
}
}{
"data": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"secret": "c0ffee…",
"subscriptionId": "aaaa-bbbb-cccc-dddd"
}
}O
secretde 32 bytes é gerado aqui e enviado ao Webhooks Engine como o cabeçalhox-webhook-secretde toda entrega. Ele aparece uma vez, nesta resposta. Não há endpoint para recuperá-lo.
A assinatura criada filtra slack.v1.message.received e slack.v1.message.sent, com timeout de 10 s e até 3 tentativas em backoff exponencial (1 s, 2 s, 4 s). O segundo evento é declarado mas não é emitido hoje — ver §15.
Erros
| Status | Quando |
|---|---|
400 | Corpo reprovado no Zod |
403 | Sem SLACK_WRITE ou sem organização |
503 | Em standalone, MODULE_WEBHOOKS_ENGINE_URL não configurado |
POST /slack/api/v1/slack/messages/send
Request
{
"teamId": "T01ABCDEF",
"slackUserId": "U01ABCDEF",
"channelId": "C01OPERACAO",
"text": "Entrega 4471 fora da janela. Quer que eu abra a tratativa?"
}{
"teamId": "T01ABCDEF",
"slackUserId": "U01ABCDEF",
"channelId": "C01OPERACAO",
"text": "Entrega 4471 fora da janela. Quer que eu abra a tratativa?"
}| Campo | Tipo | Obrigatório | Restrições |
|---|---|---|---|
teamId | string | Sim | Mínimo 1 caractere |
slackUserId | string | Sim | Mínimo 1 caractere |
channelId | string | Sim | ID do canal, DM ou grupo (C…, D…, G…) |
text | string | Sim | 1 a 40.000 caracteres |
A tripla (organizationId do token, teamId, slackUserId) resolve a conexão e o token — o organizationId nunca vem do corpo.
Resposta 200
{ "data": { "correlationId": "1756742400.123456" } }{ "data": { "correlationId": "1756742400.123456" } }O correlationId é o ts da mensagem no Slack. É o que identifica a mensagem e o que se usa como thread_ts para responder em thread.
Erros
| Status | Quando |
|---|---|
400 | Corpo reprovado no Zod, ou a Slack devolveu erro (Slack error: channel_not_found, not_in_channel, ...) |
403 | Sem SLACK_WRITE ou sem organização |
404 | Não há conexão para essa tripla, ou ela foi excluída |
500 | Falha na chamada à Slack, ou o token não decifra |
POST /slack/api/v1/slack/events
Rota pública, chamada pela Slack. Configure-a como Request URL em Event Subscriptions.
Cabeçalhos exigidos
| Cabeçalho | Uso |
|---|---|
x-slack-request-timestamp | Época em segundos. Desvio maior que 5 minutos é recusado |
x-slack-signature | v0= seguido do HMAC-SHA256 de v0:{timestamp}:{corpo bruto} com o SLACK_SIGNING_SECRET |
Respostas
| Situação | Resposta |
|---|---|
Assinatura inválida, ausente, fora da janela, ou SLACK_SIGNING_SECRET não configurado | 401 {"error":"invalid signature"} |
| Corpo não é JSON | 400 {"error":"invalid json"} |
type: url_verification | 200 com o challenge em texto puro |
type: url_verification sem challenge string | 400 {"error":"missing challenge"} |
event_callback de message ou app_mention | 200 {"ok":true} — ingestão dispara em paralelo |
| Qualquer outro tipo | 200 {"ok":true} |
Payload publicado quando o evento passa no filtro — tipo slack.v1.message.received:
{
"originalPayload": {
"teamId": "T01ABCDEF",
"slackUserId": "U01ABCDEF",
"channelId": "C01OPERACAO",
"channelType": "channel",
"ts": "1756742400.123456",
"threadTs": null,
"senderId": "U09XYZ",
"senderName": null,
"text": "qual o status da proposta 4471?",
"subtype": null,
"files": []
}
}{
"originalPayload": {
"teamId": "T01ABCDEF",
"slackUserId": "U01ABCDEF",
"channelId": "C01OPERACAO",
"channelType": "channel",
"ts": "1756742400.123456",
"threadTs": null,
"senderId": "U09XYZ",
"senderName": null,
"text": "qual o status da proposta 4471?",
"subtype": null,
"files": []
}
}Início rápido
Do zero à primeira mensagem trafegando. Os comandos não foram executados ao escrever esta documentação — conferem com o código e os schemas Zod, mas trate-os como referência.
Pré-requisito: o app Slack precisa existir e estar configurado. O passo a passo completo, com manifesto pronto, está em `docs/slack/SETUP.md`. Sem SLACK_CLIENT_ID, SLACK_CLIENT_SECRET, SLACK_SIGNING_SECRET, SLACK_CALLBACK_URL e SLACK_CREDENTIAL_MASTER_KEY, nada abaixo funciona.
São oito passos, e o oitavo é o que fecha o ciclo nos dois sentidos:
flowchart LR P1["1. Autenticar<br/>no IAM"] --> P2["2. Conferir que<br/>/events dá 401"] P2 --> P3["3. Conectar<br/>o workspace"] P3 --> P4["4. Verificar<br/>a conexão"] P4 --> P5["5. Descobrir<br/>os canais"] P5 --> P6["6. Criar<br/>o dispatcher"] P6 --> P7["7. Enviar<br/>uma mensagem"] P7 --> P8["8. Fechar o ciclo —<br/>escrever uma DM"]
1. Autenticar no IAM
TOKEN=$(curl -s -X POST https://iam.stg.catalisa.app/iam/api/v1/users/login \
-H "Content-Type: application/json" \
-d '{"email":"admin@catalisa.app","password":"password123",
"organizationId":"b0000000-0000-0000-0000-000000000001"}' | jq -r .accessToken)
BASE=https://slack.stg.catalisa.app/slack/api/v1/slackTOKEN=$(curl -s -X POST https://iam.stg.catalisa.app/iam/api/v1/users/login \
-H "Content-Type: application/json" \
-d '{"email":"admin@catalisa.app","password":"password123",
"organizationId":"b0000000-0000-0000-0000-000000000001"}' | jq -r .accessToken)
BASE=https://slack.stg.catalisa.app/slack/api/v1/slackConfira que o token chegou antes de seguir — echo $TOKEN precisa imprimir um JWT, não null:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI…eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI…2. Confirmar que a rota de eventos está protegida
curl -s -o /dev/null -w "%{http_code}\n" -X POST "$BASE/events" \
-H "Content-Type: application/json" -d '{"type":"url_verification","challenge":"x"}'curl -s -o /dev/null -w "%{http_code}\n" -X POST "$BASE/events" \
-H "Content-Type: application/json" -d '{"type":"url_verification","challenge":"x"}'Deve responder 401. Se responder 200, o SLACK_SIGNING_SECRET não está fazendo o que deveria — pare e corrija antes de seguir.
3. Conectar o workspace
curl -s -X POST "$BASE/connections/connect" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"redirectUrl":"https://slack.stg.catalisa.app/slack/connected"}' \
| jq -r .authorizationUrlcurl -s -X POST "$BASE/connections/connect" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"redirectUrl":"https://slack.stg.catalisa.app/slack/connected"}' \
| jq -r .authorizationUrlhttps://slack.com/oauth/v2/authorize?client_id=…&user_scope=im%3Ahistory%2C…&redirect_uri=…&state=…https://slack.com/oauth/v2/authorize?client_id=…&user_scope=im%3Ahistory%2C…&redirect_uri=…&state=…Abra a URL, autorize no workspace, e o callback grava a conexão e redireciona.
4. Verificar a conexão
curl -s "$BASE/connections" -H "Authorization: Bearer $TOKEN" | jqcurl -s "$BASE/connections" -H "Authorization: Bearer $TOKEN" | jq{
"data": [
{ "id": "8f14e45f-…", "teamId": "T01ABCDEF", "teamName": "Acme",
"slackUserId": "U01ABCDEF", "status": "ACTIVE" }
]
}{
"data": [
{ "id": "8f14e45f-…", "teamId": "T01ABCDEF", "teamName": "Acme",
"slackUserId": "U01ABCDEF", "status": "ACTIVE" }
]
}Guarde id, teamId e slackUserId — os três aparecem nas chamadas seguintes.
5. Descobrir os canais
curl -s -G "$BASE/channels" -H "Authorization: Bearer $TOKEN" \
--data-urlencode "teamId=T01ABCDEF" \
--data-urlencode "slackUserId=U01ABCDEF" | jqcurl -s -G "$BASE/channels" -H "Authorization: Bearer $TOKEN" \
--data-urlencode "teamId=T01ABCDEF" \
--data-urlencode "slackUserId=U01ABCDEF" | jq{ "data": [ { "id": "C01OPERACAO", "name": "operacao-sp", "isPrivate": false } ] }{ "data": [ { "id": "C01OPERACAO", "name": "operacao-sp", "isPrivate": false } ] }6. Criar o dispatcher
curl -s -X POST "$BASE/dispatchers" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"connectionId": "8f14e45f-…",
"teamId": "T01ABCDEF",
"slackUserId": "U01ABCDEF",
"endpointUrl": "https://api.suaempresa.com.br/hooks/slack",
"watchedChannels": ["C01OPERACAO"]
}' | jqcurl -s -X POST "$BASE/dispatchers" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{
"connectionId": "8f14e45f-…",
"teamId": "T01ABCDEF",
"slackUserId": "U01ABCDEF",
"endpointUrl": "https://api.suaempresa.com.br/hooks/slack",
"watchedChannels": ["C01OPERACAO"]
}' | jq{
"data": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"secret": "c0ffee…",
"subscriptionId": "aaaa-bbbb-cccc-dddd"
}
}{
"data": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"secret": "c0ffee…",
"subscriptionId": "aaaa-bbbb-cccc-dddd"
}
}Anote o secret — ele não volta nunca mais.
7. Enviar uma mensagem
curl -s -X POST "$BASE/messages/send" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"teamId":"T01ABCDEF","slackUserId":"U01ABCDEF",
"channelId":"C01OPERACAO","text":"Integração ativa."}' | jqcurl -s -X POST "$BASE/messages/send" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"teamId":"T01ABCDEF","slackUserId":"U01ABCDEF",
"channelId":"C01OPERACAO","text":"Integração ativa."}' | jq{ "data": { "correlationId": "1756742400.123456" } }{ "data": { "correlationId": "1756742400.123456" } }8. Fechar o ciclo
Escreva uma DM para o usuário conectado, ou mencione-o em #operacao-sp. Seu endpoint recebe slack.v1.message.received em segundos.
Credenciais de staging. Nunca cole segredo de produção em documentação ou script — ver AMBIENTES.md.
Receitas
Verificar a assinatura da entrega no seu endpoint
O Webhooks Engine assina a entrega, e o dispatcher acrescenta o x-webhook-secret gerado na criação. Confira os dois, e confira o segredo em tempo constante.
flowchart LR
D["Entrega chega<br/>no seu endpoint"] --> V1{"Assinatura RSA do<br/>Webhooks Engine confere?"}
V1 -->|Não| R1["401 — defesa principal"]
V1 -->|Sim| V2{"x-webhook-secret confere<br/>em tempo constante?"}
V2 -->|Não| R2["401 — segunda camada"]
V2 -->|Sim| ACK["Responda 200 primeiro"]
ACK --> FILA["Só então enfileire<br/>o trabalho"]import { timingSafeEqual } from 'node:crypto'
app.post('/hooks/slack', express.raw({ type: 'application/json' }), (req, res) => {
const recebido = Buffer.from(req.header('x-webhook-secret') ?? '')
const esperado = Buffer.from(process.env.SLACK_DISPATCHER_SECRET)
if (recebido.length !== esperado.length || !timingSafeEqual(recebido, esperado)) {
return res.sendStatus(401)
}
// Responda rápido. Enfileire o trabalho.
res.sendStatus(200)
const { originalPayload } = req.body
fila.add({ canal: 'slack', ...originalPayload })
})import { timingSafeEqual } from 'node:crypto'
app.post('/hooks/slack', express.raw({ type: 'application/json' }), (req, res) => {
const recebido = Buffer.from(req.header('x-webhook-secret') ?? '')
const esperado = Buffer.from(process.env.SLACK_DISPATCHER_SECRET)
if (recebido.length !== esperado.length || !timingSafeEqual(recebido, esperado)) {
return res.sendStatus(401)
}
// Responda rápido. Enfileire o trabalho.
res.sendStatus(200)
const { originalPayload } = req.body
fila.add({ canal: 'slack', ...originalPayload })
})Armadilhas.
- Responda antes de processar. O timeout do dispatcher é de 10 segundos; passar disso conta como falha e gera nova tentativa.
- A entrega não é exatamente-uma-vez. Três tentativas em backoff significam que o mesmo
tspode chegar duas vezes. Usets+channelIdcomo chave de idempotência. - A assinatura RSA do Webhooks Engine é a defesa principal; o
x-webhook-secreté a segunda camada. Ver Webhooks Engine.
Ajustar os canais observados sem recriar nada
curl -s -X PATCH "$BASE/dispatchers/$DISPATCHER_ID" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"watchedChannels":["C01OPERACAO","C02SUPORTE","C03LOGISTICA"]}'curl -s -X PATCH "$BASE/dispatchers/$DISPATCHER_ID" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"watchedChannels":["C01OPERACAO","C02SUPORTE","C03LOGISTICA"]}'{ "ok": true }{ "ok": true }Armadilhas.
- O corpo substitui a lista inteira. Não há acrescentar nem remover — mande sempre o conjunto completo.
- Só
watchedChannelsé atualizável. Para mudarendpointUrloucustomHeaders, crie outro dispatcher; não háDELETEpara o antigo (§15). - Tirar um canal da lista não para DM nem menção. Esses dois passam sempre, por desenho do filtro.
- O usuário conectado precisa estar no canal. Canal fora da participação dele não gera evento, por mais que esteja na lista.
Responder dentro da thread da mensagem recebida
O POST /messages/send não aceita thread_ts hoje (§15). A alternativa que funciona:
# O ts recebido no evento identifica a mensagem original
curl -s -X POST "$BASE/messages/send" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"teamId\":\"$TEAM\",\"slackUserId\":\"$USER\",
\"channelId\":\"$CANAL\",
\"text\":\"Sobre a proposta 4471: em análise desde ontem.\"}"# O ts recebido no evento identifica a mensagem original
curl -s -X POST "$BASE/messages/send" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d "{\"teamId\":\"$TEAM\",\"slackUserId\":\"$USER\",
\"channelId\":\"$CANAL\",
\"text\":\"Sobre a proposta 4471: em análise desde ontem.\"}"Armadilhas.
- A resposta vai para o canal, não para a thread. Em canal movimentado, ela se perde do contexto. Se thread é requisito do seu produto, isso é bloqueante hoje.
- O
correlationIdque volta é otsda sua mensagem, e é o que você guarda para correlacionar resposta com pedido. - O limite de 40.000 caracteres é do schema; a Slack tem os limites de renderização dela. Mensagem muito longa é truncada ou rejeitada pelo lado deles.
Diagnosticar por que nada chega ao seu endpoint
Em ordem, do mais comum ao mais raro:
| Verificação | Como | O que significa falhar |
|---|---|---|
| A Request URL do app aponta para a rota certa? | Console do Slack, Event Subscriptions | Deve ser {BASE_PUBLICA}/slack/api/v1/slack/events, com o slack duplicado |
| A Slack consegue verificar a URL? | O console mostra Verified | 401 aqui quase sempre é SLACK_SIGNING_SECRET divergente entre o app e o ambiente |
| Os eventos de usuário estão assinados? | Event Subscriptions → eventos de usuário | Faltam message.im, message.mpim, message.channels, message.groups, app_mention |
| A conexão existe? | GET /connections | Se a lista está vazia, o OAuth não concluiu |
| Há dispatcher ativo? | Não há endpoint de listagem (§15) — consulte slack.slack_dispatchers | Sem dispatcher ACTIVE, ingestEvent sai antes de publicar |
| O canal está observado? | PATCH /dispatchers/:id | Canal comum fora de watchedChannels é descartado. Teste com DM, que passa sempre |
| A mensagem tem subtipo de ruído? | — | Mensagem editada, apagada, de bot ou de entrada/saída é descartada por desenho |
| O Webhooks Engine está alcançável? | Log do serviço | Em standalone, sem MODULE_WEBHOOKS_ENGINE_URL a criação do dispatcher já teria dado 503 |
Atalho: teste sempre com DM primeiro. DM ignora
watchedChannelse elimina metade das causas possíveis em uma tentativa.
Integração com outros building blocks
| Building block | Como se relaciona | Obrigatório |
|---|---|---|
| IAM | Emite o token com organizationId e as permissões SLACK_* | Sim |
| Webhooks Engine | Entrega os eventos com assinatura, retry e log. O dispatcher cria a assinatura | Sim, para dispatcher |
| WPP | Mesmo padrão de dispatcher. Quem consome WhatsApp consome Slack com o mesmo verificador | Não |
| WPP Business | Idem, pelo canal Meta Cloud API | Não |
| Calendar | Padrão irmão de OAuth por conexão, com credenciais cifradas — com uma diferença importante (abaixo) | Não |
| Audit Trail | Registro de quem conectou workspace e criou dispatcher | Não |
O padrão de dispatcher da casa
Slack, WPP e WPP Business resolvem o mesmo problema: pegar evento de um canal de mensagem e entregar ao sistema do cliente com garantia. Os três usam o mesmo Webhooks Engine por baixo.
flowchart TD WA["WhatsApp<br/>Baileys"] --> BB1["BB wpp<br/>wpp.v1.*"] MC["WhatsApp<br/>Meta Cloud"] --> BB2["BB wpp-business<br/>wpp-biz.*"] SL["Slack"] --> BB3["BB slack<br/>slack.v1.*"] BB1 --> WE BB2 --> WE BB3 --> WE WE["Webhooks Engine<br/>assinatura RSA · retry ·<br/>log de entrega · filtros"] --> EP["Endpoint único do cliente"]
O argumento comercial: um consumidor, três canais. O cliente escreve o verificador de assinatura e o tratamento de idempotência uma vez.
Onde o Slack difere dos irmãos, e vale saber antes de integrar:
wpp / wpp-business | slack | |
|---|---|---|
| Modos de dispatcher | BASIC e ADVANCED, com condições JSONPath | Um modo só |
| Filtro por padrão glob de evento | Sim | Não — um tipo de evento só |
| Listar, apagar e pausar dispatcher | Sim | Não (§15) |
| Histórico de entregas por API | Sim | Não (§15) |
| Retentativa manual de entrega | Sim | Não (§15) |
| Namespace | wpp.v1.* · wpp-biz.* | slack.v1.* |
A referência completa do padrão está em `docs/wpp/dispatchers.md`. O Slack é a versão mínima dele.
A diferença em relação ao Calendar.
flowchart TD
subgraph CAL["BB Calendar — app por organização"]
C1["CalendarProviderConfig<br/>client_id e client_secret<br/>cifrados por tenant"] --> C2["Tela de consentimento<br/>leva a marca do cliente"]
C2 --> C3["Cada cliente passa pela<br/>verificação do Google sozinho"]
end
subgraph SLK["BB Slack — app da plataforma"]
S1["SLACK_CLIENT_ID e SLACK_CLIENT_SECRET<br/>vindos do ambiente, uma vez"] --> S2["Tela de consentimento<br/>leva a marca da plataforma"]
S2 --> S3["Aprovação do app é uma só<br/>e é gargalo compartilhado — §15"]
end
COM["Em comum: state cifrado e<br/>token AES-256-GCM no banco"] --> CAL
COM --> SLKOs dois fazem OAuth com state cifrado e token AES-256-GCM no banco, mas o app OAuth mora em lugares diferentes: o Calendar guarda client_id e client_secret por organização, então a tela de consentimento leva a marca do cliente; o Slack usa um app da plataforma, vindo do ambiente, que cada cliente instala no workspace dele. Vem do modelo da própria Slack, em que um app distribuído é instalado em muitos workspaces. A consequência prática: no Calendar cada cliente passa pela verificação do Google sozinho; no Slack a aprovação do app é uma só, da plataforma, e é gargalo compartilhado (§15).
Configuração e operação
Variáveis de ambiente
| Variável | Descrição | Obrigatória | Padrão |
|---|---|---|---|
SLACK_CLIENT_ID | Client ID do app Slack | Sim | — |
SLACK_CLIENT_SECRET | Client Secret do app Slack | Sim | — |
SLACK_SIGNING_SECRET | Signing Secret. Sem ele, POST /events recusa tudo com 401 | Sim | — |
SLACK_CALLBACK_URL | {BASE_PUBLICA}/slack/api/v1/slack/connections/callback. Idêntico ao cadastrado no app | Sim | — |
SLACK_CREDENTIAL_MASTER_KEY | Chave AES-256-GCM. 64 caracteres hex (32 bytes). openssl rand -hex 32 | Sim | — |
MODULE_WEBHOOKS_ENGINE_URL | URL interna do Webhooks Engine. Sem ela, POST /dispatchers dá 503 em standalone | Em standalone | '' |
MODULE_SLACK_URL | URL interna do serviço | Em standalone | '' |
MODULE_SLACK_PORT | Porta em standalone | Não | 3029 |
DATABASE_URL | PostgreSQL, schema slack | Sim | — |
JWT_SECRET | Compartilhado com o IAM, mínimo 44 caracteres | Sim | — |
Cuidado na primeira subida. SLACK_CLIENT_ID, SLACK_CLIENT_SECRET e SLACK_CALLBACK_URL são opcionais no schema de configuração e, quando ausentes, viram string vazia no container — o serviço sobe e só falha na primeira tentativa de conexão. A SLACK_CREDENTIAL_MASTER_KEY é lida direto de process.env, fora do schema de configuração, e valida o formato apenas no momento do uso. Confirme as cinco variáveis logo após o deploy chamando POST /connections/connect: se ela devolve uma authorizationUrl com o client_id certo, está tudo no lugar.
Dependências de infraestrutura
| Dependência | Para quê |
|---|---|
| PostgreSQL | Schema slack, 2 tabelas |
| IAM | Emissão e validação do token |
| Webhooks Engine | Assinatura, retry e log de entrega dos dispatchers |
| Slack Platform | OAuth, chat.postMessage, conversations.list, Events API |
Limites e quotas
| Limite | Valor | Origem |
|---|---|---|
| Tamanho do texto enviado | 1 a 40.000 caracteres | Schema Zod |
| Janela de ack do Events API | 3 segundos, com 3 reenvios (imediato, 1 min, 5 min) | Slack — Events API, 2026-08-16 |
| Tolerância do timestamp da assinatura | 5 minutos | verifySlackSignature |
chat.postMessage | Tier especial — cerca de 1 mensagem por segundo por canal, com limite de workspace | Slack — Rate limits, 2026-08-16 |
conversations.list | Tier 2 — 20+ requisições por minuto | Slack — Rate limits, 2026-08-16 |
| Timeout de entrega do dispatcher | 10 segundos | SlackDispatcherService |
| Tentativas de entrega | 3, backoff 1 s → 2 s → 4 s | SlackDispatcherService |
Não há proteção de taxa própria no envio. Se o seu produto disparar rajada em um canal, o limite da Slack será atingido e o
chat.postMessagefalhará. Controle a cadência do seu lado (§15).
Catálogo de erros
| Status | Código | Significa | O que fazer |
|---|---|---|---|
400 | VALIDATION | Corpo ou query reprovados no Zod | Confira contra a §9 |
400 | VALIDATION | teamId and slackUserId query params are required | Faltam os dois na listagem de canais |
400 | VALIDATION | Slack error: <código> | Erro do lado da Slack: channel_not_found, not_in_channel, invalid_auth. O código original vem junto |
400 | VALIDATION | Invalid callback state | state adulterado, fluxo expirado, ou a chave mestra mudou entre connect e callback |
400 | VALIDATION | Slack OAuth response missing required fields | A resposta veio sem authed_user.access_token, sem authed_user.id ou sem team.id — quase sempre escopo de usuário não concedido |
401 | — | invalid signature em POST /events | SLACK_SIGNING_SECRET divergente, timestamp fora dos 5 minutos, ou corpo alterado por proxy |
403 | — | Organization context required | Autentique com token que carregue organizationId |
403 | FORBIDDEN | Falta SLACK_READ ou SLACK_WRITE | Confira o papel e as permissões contratadas |
404 | NOT_FOUND | SlackConnection inexistente ou excluída | Confira teamId e slackUserId; a organização vem do token |
500 | INTERNAL | Failed to decrypt Slack token | A SLACK_CREDENTIAL_MASTER_KEY mudou. Rotação invalida todas as conexões |
500 | INTERNAL | Slack postMessage failed / Slack conversations.list failed | Falha de rede ou indisponibilidade da Slack |
503 | — | POST /dispatchers | MODULE_WEBHOOKS_ENGINE_URL ausente em standalone |
Observabilidade.
GET /slack/healthresponde vida e versão do build. Não testa banco, nem Slack, nem Webhooks Engine.- A ingestão loga em
debugquando um evento é filtrado (Slack event noise-filtered — not forwarding) e quando não há dispatcher ativo. É o primeiro lugar a olhar quando "não chega nada". - Falha de ingestão vira
warncomSlack event ingest error (non-fatal)— nunca erro HTTP, porque a resposta à Slack já saiu. - O histórico de entrega vive no Webhooks Engine, não aqui. Este building block não expõe endpoint de log de entrega (§15).
Segurança e compliance
Isolamento entre tenants. O organizationId vem do claim assinado do JWT, nunca do corpo. Cada router define requireOrganization e devolve 403 quando falta. Nos caminhos de envio e de listagem de canais, a organização compõe a chave de busca da conexão: getDecryptedToken(organizationId, teamId, slackUserId) bate na chave única (organizationId, teamId, slackUserId). Passar teamId de outro cliente não resolve conexão nenhuma — retorna 404.
Na ingestão o caminho é o inverso, e precisa ser: o evento vem da Slack sem token da plataforma, então a organização é derivada da conexão encontrada por (teamId, slackUserId), e é ela que vai no emitSlackPublic. A entrega herda o tenant do dono do workspace.
flowchart LR
subgraph SAI["Saída — a organização vem do token"]
T["JWT assinado<br/>claim organizationId"] --> K["Chave de busca<br/>org + teamId + slackUserId"]
K --> CN["Conexão encontrada"]
K -.->|"teamId de outro cliente<br/>não resolve nada"| NF["404"]
end
subgraph ENT["Entrada — a organização é derivada da conexão"]
SLK["Evento da Slack<br/>sem token da plataforma"] --> BU["Busca por<br/>teamId + authorizations[0]"]
BU --> CN2["Conexão encontrada"]
CN2 --> ORG["organizationId da conexão<br/>vai no emitSlackPublic"]
ORG --> DEL["A entrega herda o tenant<br/>do dono do workspace"]
endA rota pública se defende por assinatura. POST /events não tem autenticação da plataforma — a Slack não carrega o nosso token. A defesa é o HMAC-SHA256 sobre v0:{timestamp}:{corpo bruto} com o SLACK_SIGNING_SECRET, comparado em tempo constante, com tolerância de 5 minutos no timestamp contra reenvio de requisição capturada. A verificação vale para toda requisição, inclusive o desafio de verificação de URL: sem segredo configurado, a resposta é 401, e não um caminho aberto.
Como as credenciais OAuth são protegidas — evidência no código.
| O quê | Como | Onde |
|---|---|---|
| Token de usuário do Slack | AES-256-GCM, IV aleatório de 12 bytes por operação, authTag verificado na decifragem. Formato iv:authTag:ciphertext, tudo em hex | src/slack/utils/crypto.ts |
| Envelope gravado | JSON {accessToken, refreshToken} cifrado antes de ir ao banco, nunca em claro | slack-connection.service.ts |
state do OAuth em trânsito | Mesmo esquema. organizationId, userId e redirectUrl viajam cifrados dentro do state | connect e handleCallback |
| Chave mestra | SLACK_CREDENTIAL_MASTER_KEY, 64 hex (32 bytes), validada por regex a cada uso. Vive em SOPS, nunca no repositório | getMasterKey() |
| Segredo do dispatcher | 32 bytes aleatórios, entregues uma vez na criação e repassados ao Webhooks Engine como cabeçalho | slack-dispatcher.service.ts |
Nada de token volta pela API. GET /connections seleciona campo a campo — id, teamId, teamName, slackUserId, status — e a coluna accessToken não está na projeção. Não existe endpoint que devolva token. O secret do dispatcher aparece uma única vez, na resposta da criação.
Chave por módulo. SLACK_CREDENTIAL_MASTER_KEY é distinta de CALENDAR_CREDENTIAL_MASTER_KEY, EMAIL_CREDENTIAL_MASTER_KEY e das demais. Comprometer uma não entrega as outras. O custo é operacional: rotacionar invalida todas as conexões, porque não há versionamento de chave nem recifragem gradual. Rotação é evento planejado com reconexão de todos os usuários.
O escopo do token limita o alcance, e isso é a garantia mais forte. O fluxo pede user_scope, não bot token. Um token de usuário alcança apenas as DMs daquele usuário e os canais em que ele já está. Não existe caminho técnico para ler conversa privada de terceiros no workspace, mesmo com o token em mãos.
O caminho do conteúdo da mensagem, e onde ele para, é o que decide a conversa de retenção:
flowchart LR SL["Texto da mensagem<br/>no Slack"] --> BB["BB Slack<br/>atravessa o processo"] BB -.->|"NÃO persiste —<br/>não há tabela de mensagem"| X["schema slack"] BB --> BUS["Barramento de eventos"] BUS --> WE["Webhooks Engine<br/>log de entrega guarda o payload,<br/>e o payload carrega o texto"] WE --> CLI["Endpoint do cliente"] WE --> RET["Aqui é onde a política de<br/>retenção precisa ser definida"]
Que conteúdo de mensagem é armazenado. Nenhum. Não há tabela de mensagem no schema slack. O texto atravessa o processo — chega no evento, passa pelo filtro, é publicado no barramento e entregue — sem persistência neste building block. O que fica no banco é:
| Dado | Onde | Natureza |
|---|---|---|
teamId, teamName, slackUserId | slack_connections | Identificadores do workspace e do usuário |
| Token OAuth | slack_connections | Credencial, cifrada |
scopes[] | slack_connections | Metadado da autorização |
endpointUrl, customHeaders, watchedChannels[] | slack_dispatchers | Configuração de entrega |
userId, organizationId | Ambas | Identificadores internos |
Onde o conteúdo pode ficar retido, então. No Webhooks Engine, cujo log de entrega guarda o payload — e o payload carrega o texto da mensagem. Se a sua política proíbe reter conteúdo de conversa fora do Slack, essa é a política de retenção a definir, e ela é do Webhooks Engine, não daqui.
LGPD. Mensagem é dado pessoal, e o conteúdo pode ser sensível — conversa de RH, discussão jurídica, dado de saúde num canal de benefícios. Cinco pontos:
- Base legal e finalidade. O consentimento OAuth do usuário no Slack não é a base legal do seu tratamento. Documente a finalidade e peça só os escopos que usa.
- O administrador do workspace é parte interessada. Em workspace corporativo com aprovação de app ligada, o administrador aprova a instalação — e essa aprovação é parte do registro de conformidade do seu cliente.
- Minimização por filtro. O filtro de ruído reduz o que sai do Slack. É controle de privacidade além de controle de custo: mensagem descartada não trafega, não é entregue e não é logada.
- Minimização por não armazenar. Nenhum conteúdo é persistido aqui. Preserve isso: não crie cache de mensagem sem revisar retenção.
- Eliminação. Não há endpoint de desconexão (§15). Remover uma conexão exige intervenção no banco, e isso não revoga o token no Slack — a autorização segue ativa até o usuário removê-la na conta dele. Atender pedido de eliminação exige os dois passos, manualmente.
Autenticação e permissões.
| Permissão | Concede |
|---|---|
SLACK_READ | Listar conexões e canais |
SLACK_WRITE | Conectar workspace, criar e atualizar dispatcher, enviar mensagem |
Não há permissão de administrador separada: quem tem SLACK_WRITE cria dispatcher, e dispatcher define para onde o conteúdo do workspace é entregue. Trate SLACK_WRITE como permissão elevada ao montar os papéis.
Limitações conhecidas
Este building block está em alpha. A lista abaixo é longa de propósito.
| Limitação | Impacto | Situação |
|---|---|---|
| Não há desconectar workspace | Não existe DELETE /connections/:id. Remover uma conexão exige mexer no banco, e isso não revoga o token no Slack | Lacuna conhecida, registrada em `docs/slack/SETUP.md`. Relevante para LGPD (§14) |
| O token não é renovado | Não há refreshAccessToken. O manifesto recomendado desliga a rotação de token da Slack. Se a rotação for ligada no app, a conexão expira e só volta com nova autorização | Depende de a rotação permanecer desligada |
| Não há listar, apagar ou pausar dispatcher | Só POST e PATCH. Descobrir os dispatchers de uma conexão exige consulta ao banco; dispatcher errado não tem como ser removido pela API | Roadmap — os irmãos WPP têm tudo isso |
| Sem histórico de entrega pela API | O log vive no Webhooks Engine; este BB não expõe rota equivalente à GET /:id/delivery-logs do WPP | Roadmap |
| Sem retentativa manual de entrega | Depois das 3 tentativas automáticas, não há como reprocessar pela API do Slack BB | Roadmap |
slack.v1.message.sent nunca é emitido | O dispatcher registra a assinatura com esse filtro, mas nenhum código o publica. POST /messages/send não gera evento. Assinar esse tipo não entrega nada | Reservado, não implementado — mesmo padrão do wpp.v1.message.status.updated |
| Sem resposta em thread | slackSendSchema não aceita thread_ts. Toda resposta vai ao canal, fora do contexto da thread | Roadmap. Bloqueante para alguns produtos |
| Sem Block Kit, anexo, arquivo ou reação | O envio é text puro. Sem botão, modal, slash command ou Socket Mode | Não implementado. Se precisa disso, use a Slack Platform direta |
| Sem controle de taxa no envio | Nenhuma proteção contra o limite de aproximadamente 1 mensagem por segundo por canal do chat.postMessage. Rajada gera erro da Slack | Controle a cadência no seu lado |
conversations.list sem paginação | A listagem de canais faz uma chamada e devolve o que veio. Workspace grande não vem completo | Não implementado |
| A distribuição do app depende de revisão da Slack | Os escopos de histórico (*:history) disparam revisão antes da distribuição fora do workspace de instalação. Enquanto ela não sai, o alcance é o workspace de desenvolvimento | Bloqueio externo. O plano B — modelo de bot sem escopos de histórico — está descrito em `docs/slack/SETUP.md` |
| O app OAuth é global, não por tenant | A tela de consentimento leva a marca do app da plataforma, não a do cliente. Diferente do Calendar | Por desenho, seguindo o modelo de app distribuído da Slack |
SLACK_CREDENTIAL_MASTER_KEY fora do schema de configuração | É lida direto de process.env e validada só no uso. Ausência ou formato errado não impedem o boot — quebram a primeira conexão | Verifique após o deploy (§13) |
| Sem facade de módulo | Não há slack.facade.ts. Outros building blocks não conseguem chamar o Slack por HTTP em standalone | Só necessário quando outro BB precisar |
/health não checa dependência | Vida e versão apenas | Suficiente para liveness, insuficiente para readiness |
| Enums maiores que o comportamento | SlackConnectionStatus e SlackDispatcherStatus têm estados que nenhum caminho de código atribui — só ACTIVE é usado | Schema à frente da implementação |
| Prefixo de rota duplicado | A rota real é /slack/api/v1/slack/... | Não corrigido |
Perguntas frequentes
Isso está pronto para produção?
Não como produto completo. O caminho principal — conectar, ingerir com filtro, entregar por webhook, enviar mensagem — funciona e é coberto por testes unitários. O que falta é ciclo de vida: desconectar, listar e apagar dispatcher, renovar token, responder em thread. Para um piloto controlado, com um cliente e acompanhamento, dá para usar. Para vender como capacidade fechada, leia a §15 inteira antes.
Vocês guardam as mensagens do meu Slack?
Neste building block, não. Não existe tabela de mensagem. O texto atravessa o processo e é entregue ao seu endpoint sem ser persistido aqui. Mas o log de entrega do Webhooks Engine guarda o payload, e o payload tem o texto. Se conteúdo de conversa não pode ficar retido fora do Slack, a política de retenção a definir é a de lá.
O app consegue ler conversa privada de qualquer pessoa do workspace?
Não. O token é de usuário, e alcança as DMs daquele usuário e os canais em que ele já está. Não existe caminho técnico para ler DM de terceiros. Ainda assim, em workspace corporativo, a instalação costuma passar por aprovação do administrador — e deve mesmo.
Por que preciso de um app Slack meu se o building block já existe?
Você não precisa de um app seu: o Slack BB usa um app da plataforma, configurado por ambiente, que cada cliente instala no workspace dele. É diferente do Calendar, onde cada organização registra o próprio app OAuth. A contrapartida é que a tela de consentimento leva a marca da plataforma, e a aprovação do app junto à Slack é única e compartilhada (§15).
A mensagem aparece como bot ou como pessoa?
Como pessoa — a que instalou o app. O envio usa as_user: true com o token de usuário. Em canal operacional isso muda a taxa de leitura, e também significa que a mensagem é atribuída àquela pessoa no histórico do workspace. Deixe isso claro para quem conecta.
Recebo todas as mensagens do workspace?
Não, e é de propósito. O filtro entrega DM, grupo de DM, qualquer mensagem que mencione o usuário conectado, e canais comuns só se estiverem em watchedChannels. Mensagem de bot, editada, apagada e de entrada ou saída de canal é descartada antes de qualquer entrega. O diagrama da decisão está em §8.
Consigo responder dentro da thread?
Hoje não. O POST /messages/send não aceita thread_ts e a resposta vai para o canal (§15). Se thread é requisito, é bloqueante — planeje em torno disso ou aguarde.
Como sei se o dispatcher está entregando?
Pelo Webhooks Engine, que guarda o log com status, código HTTP e tentativas. Este building block não expõe rota de log de entrega (§15). Para saber se o problema é antes da entrega, os logs em debug do serviço dizem quando um evento foi filtrado ou quando não havia dispatcher ativo.
Qual a diferença entre este BB e o WPP?
O canal, e o grau de maturidade. O WPP fala WhatsApp por Baileys, o WPP Business fala WhatsApp pela Meta Cloud API, e este fala Slack. Os três entregam pelo mesmo Webhooks Engine, com a mesma semântica de assinatura e retry — por isso um consumidor serve para os três. O Slack é a versão mínima do padrão: um modo de dispatcher, sem condições JSONPath, sem listar ou apagar dispatcher, sem retentativa manual. A tabela de diferenças está em §12.
O que acontece se eu rotacionar a SLACK_CREDENTIAL_MASTER_KEY?
Todas as conexões param de decifrar, com Failed to decrypt Slack token, e todos os usuários precisam reconectar. Não há versionamento de chave nem recifragem gradual. Guarde a chave em SOPS e trate como segredo de longa duração.
Por que a rota tem slack duas vezes?
Porque o app usa basePath('/slack') e os routers são montados sob /api/v1/slack. A rota efetiva é /slack/api/v1/slack/events. Atenção especial nessa: é a Request URL que você cadastra no console do Slack, e errar o prefixo é a causa mais comum de a verificação de URL falhar.
Padrão: PADRAO-DOCUMENTACAO.md · Índice: INDEX.md · Runbook: docs/slack/SETUP.md