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_atpassa a ser a data, que é o que o schema já declarava;end_origin, campo novo:declared,no_longer_observedoudeclared_without_author. A data de uma saída pela coleta é quando a plataforma parou de ver a pessoa, e não quando ela saiu;mistakefica comreasoneat, sem o autor;lazy_html0.1.12 → 0.1.13, dependência só de teste, com aEEF-CVE-2026-92106(LOW), que fazia omix hex.auditsair 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 motivosuspeita 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¶
- o CD publica a imagem
v0.9.2e entrega no Dokploy; /versiontem de responder0.9.2;- a consulta acima, em produção;
- back-merge de
mainparadevelopment, por merge commit:developmentjá tem o conserto (#982), e recebe a versão e esta nota.