Pular para conteúdo

v0.9.2 — hotfix: o roster da API deixa de devolver e-mail, e de quebrar com quem saiu

Hotfix, e não release de development. Nasce de main (v0.9.1) e carrega um conserto, com uma linha de dependência de teste. Volta para main e para development por merge commit (AGENTS.md §12).

Proposto em 2026-09-25, sob a regra de que segurança vem primeiro (AGENTS.md §14.0): exposição ativa em produção entra na primeira release possível. O momento do delivery é da pessoa mantenedora: todo merge em main é deploy.

O que estava no ar, desde a v0.9.0

GET /api/v1/teams/:id/members, a rota do roster da API pública:

# Defeito Severidade Consequência
N1 um vínculo marcado como equívoco saía com mistake.por, o e-mail de quem o marcou alta qualquer token com alcance sobre a equipe lia o e-mail de quem administra. O schema OpenAPI da própria rota diz que e-mail não sai
N2 um vínculo encerrado saía com ended_at igual à tupla {:declarado, <e-mail>, …}, que o Jason não serializa média a rota respondia 500 para toda equipe com alguém que saiu, e o e-mail de quem declarou a saída ia para o log de erro

Achados em 2026-09-25 pela revisão de segurança da implementação do servidor MCP (specs/062-servidor-mcp/seguranca-revisao-da-implementacao.md), que viu o mesmo defeito na API. Nenhum teste pegava: a fixture só tinha vínculo observado e vigente.

O conserto

  • ended_at passa a ser a data, que é o que o schema já declarava;
  • end_origin, campo novo: declared, no_longer_observed ou declared_without_author. A data de uma saída pela coleta é quando a plataforma parou de ver a pessoa, e não quando ela saiu;
  • mistake fica com reason e at, sem o autor;
  • lazy_html 0.1.12 → 0.1.13, dependência só de teste, com a EEF-CVE-2026-92106 (LOW), que fazia o mix hex.audit sair 1.

Por que PATCH

Grau Carrega? Por quê
MAJOR não nada que o contrato prometia deixa de valer
MINOR não não há nada novo que quem usa passe a conseguir fazer
PATCH sim faz a rota cumprir o que o schema já dizia: e-mail não sai, e ended_at é uma data. O end_origin não é funcionalidade nova, é a informação que a tupla quebrada carregava, agora serializável

O que muda para quem integra, e fica dito: mistake.por some, e ele nunca devia ter saído. Quem lia ended_at recebia 500, e passa a receber uma data.

Os três riscos de sempre

Risco Veredito Medido como
migração destrutiva nenhuma migração git diff origin/main -- priv/repo/migrations vazio
variável obrigatória nova nenhuma config/runtime.exs e compose.yaml fora do diff
mudança em tela em uso nenhuma tela: só a resposta JSON de uma rota da API o diff toca team_controller.ex, api_schemas.ex, um teste e uma linha do mix.lock

A evidência, sobre o código de produção

Numa worktree tirada de main:

Comando Código
mix test test/the_band_web/api/roster_sem_email_test.exs 0, 3 testes
a mesma, com o team_controller.ex de main 3/3 reprovam, o primeiro com o 500
mix test test/the_band_web/api/ test/the_band_web/controllers/api/ 0, 145 testes
mix hex.audit · mix deps.audit 0 · 0
MIX_ENV=test mix compile --warnings-as-errors · mix format --check-formatted 0 · 0

A suíte inteira é o CI do PR.

O que o conserto não desfaz: quem pode ter lido

O e-mail que já saiu não volta. O que se pode saber é quem leu, e o registro de leitura permite saber tudo: a rota /members (#934) e o registro (#936) foram a produção juntos, na v0.9.0. Conferido em 2026-09-25 com git merge-base --is-ancestor sobre as tags: nenhum dos dois está na v0.8.0, e os dois estão na v0.9.0. Não houve leitura dessa rota em produção sem registro.

A consulta, para rodar na base de produção (ato de quem opera, pelo agente deploy-producao ou pela pessoa mantenedora; nenhum agente desta sessão acessa produção):

-- Quem leu /members de uma equipe que já tinha um equívoco marcado no momento da leitura.
-- Só essas leituras podem ter levado o e-mail (N1). As do N2 deram 500, e 500 não é gravado.
SELECT r.token_public_id,
       r.target_id              AS team_id,
       count(*)                 AS leituras,
       min(r.occurred_at)       AS primeira,
       max(r.occurred_at)       AS ultima
FROM api_access_reads r
WHERE r.route = '/api/v1/teams/:id/members'
  AND EXISTS (
    SELECT 1
    FROM eo_team_memberships m
    WHERE m.team_id = r.target_id
      AND m.invalidated_at IS NOT NULL
      AND m.invalidated_at <= r.occurred_at
  )
GROUP BY r.token_public_id, r.target_id
ORDER BY ultima DESC;

Como ler o resultado:

  • nenhuma linha: nenhum token leu o e-mail, e o N1 fica sem consequência;
  • com linhas: cada token_public_id é uma credencial que recebeu o e-mail de quem marcou o equívoco. A tela de tokens, /api-tokens, diz a quem pertence cada uma. Se alguma não for reconhecida, revogue-a com o motivo suspeita de vazamento, e avise quem teve o e-mail exposto.

E o log: os 500 do N2 escreveram o e-mail de quem declarou a saída no log de erro da aplicação. Quem tem acesso ao log de produção teve acesso a ele. É o mesmo grupo que opera, mas fica dito.

Depois do merge

  1. o CD publica a imagem v0.9.2 e entrega no Dokploy;
  2. /version tem de responder 0.9.2;
  3. a consulta acima, em produção;
  4. back-merge de main para development, por merge commit: development já tem o conserto (#982), e recebe a versão e esta nota.