Skip to content

Plano — Mascaramento de PII em Logs

Auditoria base: AUDIT_PII_LOGS_LGPD_2026-05-08Status geral: ✅ FECHADO (2026-05-08) — Etapas 0–6 concluídas, lint de CI ativo, memória persistida, backlog removido. Documento mantido como referência histórica e SSOT da regra. Princípio: mascarar na origem (escrita do log), preservar usuario_id/escola_id/acao para audit trail.


Histórico de revisões

DataMudançaMotivo
2026-05-08Especificação da Etapa 3 corrigida: nome_completo${papel} @ ${escola_nome} (com fallback ${papel} @ escola:${escola_id_8c}…). Versão anterior trocava por usuario_id.usuario_id é opaco para triagem operacional. papel + escola_nome identifica conta e contexto sem expor PF; PJ não é PII. Enquanto ntfy rodar em ntfy.sh (terceiro), nenhum dado de PF deve sair do domínio.
2026-05-08Replanejamento formal: cada etapa expandida para formato autossuficiente (Status / Pré-requisitos / Reversibilidade / Objetivo / Arquivos / Testes / Aceite / Docs / Volume / Notas).Permitir execução ou replanejamento por etapa, em qualquer interação futura, sem depender de contexto anterior.

Convenções comuns a todas as etapas

  • Localização canônica de testes Deno: supabase/functions/<func>/__tests__/*.test.ts ou supabase/functions/_shared/__tests__/*.test.ts. NUNCA co-localizar *.test.ts na raiz da função (bundler arrasta *.ts da raiz no deploy → OOM/exit 135). Guard: scripts/check-edge-function-test-location.sh.
  • Padrão @workflow Cenário B: primeiro escrever testes que descrevem o estado atual (red), depois testes do novo comportamento, depois implementação (green).
  • Migration SQL: nenhuma etapa desta série envolve migration. Toda mudança é de código TS de Edge Functions e documentação.
  • Critério de aceite global por etapa: (i) testes verdes em CI; (ii) @audit segurança sobre os arquivos modificados sem 🔴; (iii) docs listadas em "Documentação a atualizar" entregues na mesma interação.
  • Volume: P = 1–3 arquivos · M = 4–8 arquivos · G = 9+ arquivos.

ETAPA 0 — Helper compartilhado _shared/pii-helpers.ts

Status: ✅ CONCLUÍDA (2026-05-08) — supabase/functions/_shared/pii-helpers.ts + _shared/__tests__/pii-helpers.test.ts (29 testes Deno verdes). Pré-requisitos:Reversível sem impacto nas demais: sim — código novo, não consumido até as Etapas 2/4/5. Se revertida, basta deletar o arquivo e o teste.

Objetivo: criar SSOT de mascaramento de PII para Edge Functions, evitando regex duplicada e divergência entre handlers.

Arquivos afetados:

  • supabase/functions/_shared/pii-helpers.tsCRIAR. Helpers puros, sem I/O, sem dependência de Supabase.
  • supabase/functions/_shared/__tests__/pii-helpers.test.tsCRIAR. Testes Deno unitários.

Novo arquivo compartilhado — contratos esperados:

ts
// Mascara e-mail. Retorna partes para log estruturado.
maskEmail(email: string): { masked: string; dominio: string; hash: string }
// Ex.: "joao.silva@escola.com" → { masked: "j***@escola.com", dominio: "escola.com", hash: "<sha256_16>" }

maskCPF(cpf: string): string         // entrada flexível; saída "***.***.***-99"
maskCNPJ(cnpj: string): string       // saída "**.***.***/****-99" (PJ é público; uso opcional)
maskTelefone(tel: string): string    // saída "+55 (11) ****-4321" — preserva DDI/DDD e 4 últimos
maskCodigo(codigo: string, tipo: 'cpf' | 'cnpj' | 'inep'): string
// Redact qualquer telefone E.164 / nacional embutido em texto livre e trunca.
maskTextoComTelefones(s: string, max?: number): string
// Hash determinístico curto para correlação cross-log sem reverter PII.
sha256Hex16(input: string): Promise<string>

Testes exigidos (Deno, em _shared/__tests__/pii-helpers.test.ts):

  • E-mail: válido, com +alias, unicode no local-part, domínio com subdomínio, string vazia, sem @.
  • CPF/CNPJ/Telefone: formato canônico, com/sem máscara, comprimento inválido, string vazia, espaços.
  • maskCodigo: cada tipo + tipo desconhecido (deve fallback seguro).
  • maskTextoComTelefones: texto sem telefone, com 1, com múltiplos, truncamento por max.
  • sha256Hex16: determinismo (mesma entrada → mesma saída) e comprimento 16.

Critério de aceite: todos os helpers usados na auditoria têm cobertura ≥1 caso feliz + ≥2 edge cases; deno test verde; nenhum import de Supabase no arquivo.

Documentação a atualizar nesta etapa:

  • docs/lgpd/PII_LOG_MASKING_PLAN.md — virar status da Etapa 0 para CONCLUÍDA com link para o PR/commit.

Volume estimado: P (2 arquivos novos · sem migration · sem mudança de runtime).

Notas e riscos: baixo risco — código novo, isolado. Não exige staging. Não introduz dependências externas (usar crypto.subtle nativo do Deno para SHA-256).


ETAPA 1 — Limpar console.* de alto volume em send-otp

Status: ✅ CONCLUÍDA (2026-05-08) — 4 console.log de debug removidos em send-otp/index.ts:460-466. Verificação: rg "console\.(log|warn|error)" supabase/functions/send-otp/index.ts não retorna ocorrências com telefone E.164 cru; apenas usuario.id, escola.nome (PJ) e telefone_masked. Pré-requisitos: — (independe da Etapa 0; é remoção) Reversível sem impacto: sim — apenas remoção de logs de debug.

Objetivo: eliminar 3 console.log que ecoam telefone E.164 cru a cada requisição de OTP (achado #13 da auditoria — alto volume).

Arquivos afetados:

  • supabase/functions/send-otp/index.ts linhas ~460–466 — REMOVER os 3 console.log de debug. Manter apenas o log final já existente que usa telefone_masked.

Testes exigidos:

  • Contrato HTTP em supabase/functions/send-otp/__tests__/otp.contract.test.ts (criar se não houver): cenário "fluxo feliz" e "telefone inválido". Não precisa asseritir sobre logs (são side-effects). A garantia é via rg.
  • Smoke manual em staging: invocar send-otp 1× e verificar nos Edge Logs que nenhum E.164 aparece.

Critério de aceite:

  • rg -n "console\.(log|warn|error)" supabase/functions/send-otp/index.ts | rg -v "telefone_masked" não retorna ocorrências com telefone cru.
  • Smoke verde em staging.

Documentação a atualizar nesta etapa:

  • docs/lgpd/PII_LOG_MASKING_PLAN.md — virar Etapa 1.
  • docs/audits/AUDIT_PII_LOGS_LGPD_2026-05-08.md — virar status do achado #13.

Volume estimado: P (1 arquivo · ~6 linhas removidas).

Notas e riscos: trivial. Pode ir direto a prod após smoke em staging.


ETAPA 2 — console.* restantes em Edge Functions

Status: ✅ CONCLUÍDA (2026-05-08) — 5 arquivos atualizados (gestao-responsaveis, _shared/portal-login-responsavel, _shared/portal-login-aluno, gestao-usuarios-escola, _shared/wasender-whatsapp) + 5 testes sentinela em _shared/__tests__/pii-log-sites.test.ts. Pré-requisitos: Etapa 0 CONCLUÍDA. Reversível sem impacto: sim — substituições locais.

Objetivo: mascarar/remover os 5 console.* restantes que vazam CPF/telefone/JID em Edge Logs (achados #12, #14, #15, #16, #17).

Arquivos afetados:

  • supabase/functions/gestao-responsaveis/index.ts:319MASCARAR com maskCPF + nome reduzido a iniciais.
  • supabase/functions/gestao-responsaveis/index.ts:467MASCARAR com maskCPF.
  • supabase/functions/_shared/portal-login-responsavel.ts:121MASCARAR CPF.
  • supabase/functions/_shared/portal-login-aluno.ts:334MASCARAR CPF.
  • supabase/functions/gestao-usuarios-escola/index.ts:968REMOVER (registrarLog() já cobre o evento de hard delete).
  • supabase/functions/_shared/wasender-whatsapp.ts:102 e :211MASCARAR errorBody com maskTextoComTelefones antes do console.error.

Testes exigidos:

  • Unitários Deno em _shared/__tests__/portal-login-responsavel.test.ts e portal-login-aluno.test.ts: garantir que mensagens de erro retornadas/ logadas não contêm 11 dígitos consecutivos (regex sentinela).
  • wasender-whatsapp.test.ts em _shared/__tests__/: dado um errorBody com JID 5511987654321@s.whatsapp.net, asserir que o output mascarado não contém o número original.

Critério de aceite:

  • rg "console\.(log|error|warn)" sobre os 5 arquivos não retorna chamadas com variáveis cpf/telefone/jid/errorBody sem passagem por helper.
  • Testes verdes.

Documentação a atualizar nesta etapa:

  • PII_LOG_MASKING_PLAN.md (status), AUDIT_PII_LOGS_LGPD_2026-05-08.md (achados #12, #14–#17).

Volume estimado: M (5 arquivos · ~10 linhas).

Notas e riscos: o helper maskTextoComTelefones é decisivo aqui — qualquer regressão nele afeta logs de WhatsApp. Cobertura via Etapa 0 mitiga.


ETAPA 3 — Alertas ntfy (saída para terceiro)

Status: ✅ CONCLUÍDA (2026-05-08) Pré-requisitos: — (mudança textual; independe do helper) Reversível sem impacto: sim — alteração de template de mensagem.

Implementação aplicada:

  • Helper canônico formatAlertSubject({ papel, escola_nome?, escola_id? }) adicionado em supabase/functions/_shared/ntfy-helper.ts — produz ${papel} @ ${escola_nome}, com fallback para escola:${id8} e (sem perfil) / (sem escola) quando ausentes. Sem lookup adicional ao DB (caminho frio mantido enxuto).
  • 4 sites migrados para o novo template:
    • _shared/auth-helpers.ts — alerta "Token Revogado Reutilizado"; usa payload.principal_role + payload.escola_id. console.warn/console.log adjacentes também removeram payload.nome_completo (passaram a usar payload.sub).
    • verify-otp/index.ts — alerta "Brute Force Detectado"; usa tipoCodigo (cpf/cnpj/inep) como papel heurístico + usuario.escola_id, mais uid: truncado para correlação interna.
    • verify-password/index.ts — alerta "Brute Force Senha"; mesma estratégia.
    • send-otp/index.ts — alerta "Falha no Envio de SMS OTP"; mesma estratégia.
  • Nenhum nome completo, telefone, CPF ou e-mail é enviado a ntfy.sh.

Testes: 15/15 verdes (_shared/__tests__/ntfy-helper.test.ts 6 unit + _shared/__tests__/pii-log-sites.test.ts 9 sentinels totais incluindo 4 novos para Etapa 3). Total cumulativo do plano: 44 testes verdes.

Verificação: rg "nome_completo" supabase/functions | rg -i "enviarAlertaPush" retorna 0 ocorrências. formatAlertSubject aparece em todos os 4 arquivos alvo.

Objetivo: eliminar nome_completo (PII de PF) das mensagens enviadas a ntfy.sh (terceiro), substituindo por identificador operacional sem PII: ${papel} @ ${escola_nome}. Achados #18, #19, #20, #21.

Justificativa da especificação corrigida:

  • ntfy.sh é serviço de terceiro (cloud); enquanto não migrarmos para self-hosted, nenhum dado de PF deve sair do domínio.
  • Operador precisa de identificador humano e direto para triagem rápida — usuario_id (UUID) não atende.
  • papel + escola_nome identifica conta e contexto operacional. escola_nome é dado de PJ, não PII.
  • Quando escola_nome não estiver hidratado no escopo do alerta, usar escola:${escola_id.slice(0, 8)}… — rastreável internamente, sem PII.

Exemplos:

  • Antes: "Brute force: João Silva (5 tentativas)"
  • Depois: "Brute force: coordenador @ Escola Municipal Boa Vista (5 tentativas)"
  • Fallback: "Brute force: coordenador @ escola:a1b2c3d4… (5 tentativas)"

Arquivos afetados (4 sites — 1 linha cada):

  • supabase/functions/verify-password/index.ts:326SUBSTITUIR template.
  • supabase/functions/verify-otp/index.ts:244SUBSTITUIR template.
  • supabase/functions/send-otp/index.ts:500SUBSTITUIR template.
  • supabase/functions/_shared/auth-helpers.ts:121SUBSTITUIR template (alerta de token revogado reusado).

Em cada arquivo, antes da chamada a enviarAlertaPush, garantir que papel e escola_nome estão no escopo. Se não estiverem:

  1. Preferência: lookup leve via supabaseSystem na mesma transação (1 query a usuario_papeis_view ou equivalente).
  2. Fallback: usar escola:${escola_id.slice(0,8)}… direto.

Testes exigidos:

  • Unit Deno em supabase/functions/_shared/__tests__/auth-helpers.test.ts (já existe — estender): caso "alerta de token revogado" — body NÃO contém nome_completo; contém papel e (escola_nome OU prefixo escola:).
  • Para os 3 handlers, criar/estender em supabase/functions/<func>/__tests__/:
    • verify-password/__tests__/verify-password.alerts.test.ts
    • verify-otp/__tests__/verify-otp.alerts.test.ts
    • send-otp/__tests__/send-otp.alerts.test.ts Cada um mocka enviarAlertaPush (ou intercepta fetch para ntfy.sh) e asserta o formato do template.

Critério de aceite:

  • rg -n "nome_completo" supabase/functions | rg -i "enviarAlertaPush|ntfy" retorna 0 (excluindo cadastro-escola-publica:744 e jwt-portal:122 — fora do escopo, justificados em §2.4 da auditoria).
  • Testes verdes.
  • @audit segurança sobre os 4 arquivos sem 🔴.

Documentação a atualizar nesta etapa:

  • PII_LOG_MASKING_PLAN.md (status Etapa 3).
  • AUDIT_PII_LOGS_LGPD_2026-05-08.md (achados #18–#21).
  • docs/operations/ALERTAS_PUSH.md — registrar o template canônico ${papel} @ ${escola_nome} e a regra "nenhum dado de PF em alertas enquanto ntfy for terceiro".

Volume estimado: P (4 arquivos · 1–3 linhas cada — eventualmente +1 query de lookup quando o escopo não tiver escola_nome).

Notas e riscos:

  • Risco principal: escola_nome ausente no contexto. Mitigado pelo fallback escola: truncado.
  • Lookup adicional adiciona ~1 RTT em fluxo de erro/alerta — aceitável (caminho frio).
  • Quando ntfy migrar para self-hosted, esta decisão pode ser revista (novo ADR/plano), mas a especificação atual segue válida por padrão de defesa em profundidade.

ETAPA 4 — gerarAlteracoes()exclude consistente + evento *.contato_alterado

Status: ✅ CONCLUÍDA (2026-05-08) — _shared/diff-helper.ts estendido com PII_CONTATO_FIELDS (SSOT) e detectarCamposContatoAlterados(). Os 3 sites (user-profile, gestao-usuarios-escola, gestao-responsaveis) agora passam PII_CONTATO_FIELDS ao exclude e emitem evento dedicado *.contato_alterado com campos_alterados (apenas nomes, sem valores). LOG_ACTION_OPTIONS SSOT atualizado com as 3 novas ações. Testes: 28/28 verdes (12 unit em diff-helper.test.ts + 4 sentinels em pii-log-sites.test.ts + suíte cumulativa). Pré-requisitos: Etapa 0 CONCLUÍDA (para reuso opcional do sha256Hex16). Reversível sem impacto: parcialmente — reverter o exclude voltaria a vazar; reverter o evento separado tira granularidade. Reversão deve ser ponderada.

Implementação aplicada:

  • _shared/diff-helper.ts: +PII_CONTATO_FIELDS = ['email','telefone','telefone_secundario','codigo','cpf'] (SSOT) +detectarCamposContatoAlterados(antes, novo, campos?) retorna apenas nomes dos campos PII alterados (sem valores).
  • user-profile/index.ts:188: gerarAlteracoes(atual, novo, [...PII_CONTATO_FIELDS]) + emite perfil.contato_alterado quando há troca de contato.
  • gestao-usuarios-escola/index.ts:834: idem, com exclude já existente acrescido de ...PII_CONTATO_FIELDS + emite usuario.contato_alterado.
  • gestao-responsaveis/index.ts:499: substituiu o diff manual (loop com cpf.substring(0,3) + "***") por gerarAlteracoes com exclude + emite responsavel.contato_alterado.
  • src/constants/log-actions.ts: +usuario.contato_alterado, +responsavel.contato_alterado, +perfil.contato_alterado no SSOT do filtro/label admin.

Verificação:

  • rg "PII_CONTATO_FIELDS" supabase/functions retorna 4 sites (helper + 3 handlers).
  • rg "contato_alterado" supabase/functions retorna 3 emissões + suíte de testes.
  • Suíte Deno: 28 verdes / 0 falhas (diff-helper.test.ts + pii-log-sites.test.ts).

Objetivo: parar de gravar valores de email/telefone/telefone_secundario/codigo no JSONB detalhes via diff de update, e ainda assim manter rastreabilidade ("o usuário X alterou contato em Y") através de evento dedicado. Achados #9, #10, #11.

Mudanças:

  1. Adicionar email, telefone, telefone_secundario, codigo ao camposIgnorados da função utilitária gerarAlteracoes() em cada um dos 3 sites.
  2. Em cada update que tocar contato, emitir log adicional usuario.contato_alterado (ou responsavel.contato_alterado) com payload { campos_alterados: ['email','telefone'], usuario_id }sem valores.
  3. Demais campos (nome, papéis, série, observações) continuam logados normalmente.

Arquivos afetados:

  • supabase/functions/user-profile/index.ts:188
  • supabase/functions/gestao-usuarios-escola/index.ts:834
  • supabase/functions/gestao-responsaveis/index.ts:499

Testes exigidos:

  • Em supabase/functions/<func>/__tests__/<func>.diff.test.ts, dado um update que altera email + nome, asserir:
    • logs_transacoes.detalhes.alteracoes não contém email (nem antes/depois).
    • logs_transacoes.detalhes.alteracoes contém nome.
    • Foi emitido segundo registro *.contato_alterado com campos_alterados: ['email'] e sem valores.
  • Caso negativo: update que não toca contato não emite *.contato_alterado.

Critério de aceite:

  • Testes verdes.
  • Smoke em staging: SELECT detalhes FROM logs_transacoes WHERE detalhes::text ILIKE '%@%' no dia da execução retorna 0 entradas com e-mail completo (apenas hashes/domínios em logs de outros eventos, se houver).
  • LOG_ACTION_OPTIONS (SSOT) atualizado com as novas ações.

Documentação a atualizar nesta etapa:

  • docs/development/AUDIT_LOG.md — registrar a convenção *.contato_alterado (payload, ação, motivo).
  • PII_LOG_MASKING_PLAN.md, AUDIT_PII_LOGS_LGPD_2026-05-08.md (status).
  • Memória mem://architecture/log-actions-catalog-ssot — adicionar as duas ações novas.

Volume estimado: P (3 arquivos · ~30 linhas).

Notas e riscos:

  • Tela de admin que renderiza detalhes.alteracoes deve continuar funcional — verificar que ela não dependia de ver email cru (em §2.4 da auditoria já se concluiu que não).
  • Aplicar em staging primeiro por 24h antes de prod (sanidade do dashboard).

ETAPA 5 — registrarLog() resumo/detalhes — mascarar valores

Status: ✅ CONCLUÍDA (2026-05-08) — 8 sites migrados para o schema canônico (*_dominio + *_hash + *_mascarado). Imports de _shared/pii-helpers.ts adicionados em cada arquivo. Testes: 36/36 verdes (12 unit em diff-helper.test.ts + 24 sentinels em pii-log-sites.test.ts, sendo 8 novos da Etapa 5). Pré-requisitos: Etapa 0 CONCLUÍDA. Reversível sem impacto: sim, mas reversão recria exposição. Tratar como mudança de contrato de log e seguir staging-first.

Implementação aplicada (8 sites):

  1. _shared/gestao-usuarios-helpers.ts:182usuario.create: emailemail_dominio+email_hash; +codigo_mascarado (CPF) +telefone_mascarado.
  2. cadastro-escola-publica/index.ts:221escola.trial_signup: emailemail_dominio+email_hash; +telefone_mascarado. CNPJ permanece (PJ não é PII).
  3. admin-usuarios/index.ts:258usuario.create: codigocodigo_mascarado (via maskCodigo por tipo_codigo).
  4. admin-usuarios/index.ts:550usuario.hard_delete: codigo+emailcodigo_mascarado+email_dominio+email_hash.
  5. admin-usuarios-escola/index.ts:432 — idem usuario.hard_delete.
  6. gestao-usuarios-escola/index.ts:1083usuario.hard_delete (escopo escola): email+codigo mascarados em usuarioExcluido.
  7. email-unsubscribe/index.ts:84email.unsubscribe: emailemail_dominio+email_hash.
  8. process-email-outbox/index.ts:80email.bloqueado: to_emailto_email_dominio+to_email_hash.
  9. (Bonus) gestao-responsaveis/index.ts:387responsavel.create: telefone cru e cpf.substring(...) ad-hoc substituídos por maskTelefone + maskCPF (SSOT).

Verificação:

  • rg "from \"\\.\\./_shared/pii-helpers" supabase/functions retorna 8 imports nos arquivos alvo (+ os já existentes da Etapa 2/3).
  • Sentinels travam regressão: nenhum dos padrões antigos (email: result.email, codigo: novoUsuario.codigo, email: usuarioAlvo.email, to_email: row.to_email, cpf: cpfLimpo.substring(...)) volta a aparecer nos arquivos alvo.
  • Suíte Deno: 36 verdes / 0 falhas.

Objetivo: em todos os 8 sites de registrarLog() que persistem PII em logs_transacoes.detalhes, substituir valores crus por versão mascarada/hasheada. Achados #1 a #8.

Schema canônico do detalhes para eventos com contato:

ts
{
  usuario_id,
  email_dominio: maskEmail(email).dominio,
  email_hash:    maskEmail(email).hash,
  codigo_mascarado: maskCodigo(codigo, tipo),
  telefone_mascarado: maskTelefone(telefone),
}

Arquivos afetados:

  • supabase/functions/_shared/gestao-usuarios-helpers.ts:187
  • supabase/functions/cadastro-escola-publica/index.ts:231
  • supabase/functions/admin-usuarios/index.ts:564
  • supabase/functions/admin-usuarios-escola/index.ts:439
  • supabase/functions/gestao-usuarios-escola/index.ts:1068
  • supabase/functions/email-unsubscribe/index.ts:89
  • supabase/functions/process-email-outbox/index.ts:86
  • supabase/functions/gestao-responsaveis/index.ts:382

Testes exigidos:

  • Contratos por função em supabase/functions/<func>/__tests__/<func>.log-payload.test.ts: invocar a função em modo de teste e capturar o payload enviado a registrarLog() (mock); asserir que nenhum campo top-level contém regex \d{11} (CPF), \b\d{10,15}\b (telefone) ou @ (e-mail completo).
  • Compatibilidade da UI de logs do admin: teste de integração (Vitest + RTL) sobre o componente que renderiza detalhes — alimentar com payload mascarado e validar que não quebra (ausência de chaves antes existentes).

Critério de aceite:

  1. Smoke E2E em staging — telas de admin renderizam logs novos sem erro.
  2. Em prod, após 7 dias: SELECT count(*) FROM logs_transacoes WHERE detalhes::text ~ '\d{11}' AND criado_em > <data_deploy> retorna 0.
  3. Testes verdes em CI.

Documentação a atualizar nesta etapa:

  • docs/development/AUDIT_LOG.md — schema final do detalhes para eventos de usuário/responsável.
  • PII_LOG_MASKING_PLAN.md, AUDIT_PII_LOGS_LGPD_2026-05-08.md (status).

Volume estimado: M (8 arquivos · ~50 linhas).

Notas e riscos:

  • Mudança de contrato de log — staging obrigatório por 24h antes de prod.
  • Logs antigos permanecem como estão (esta série é forward-only). Se for desejado retroativo, abrir plano separado de migração de dados.

ETAPA 6 — Documentação, memória e fechamento

Status: ✅ CONCLUÍDA (2026-05-08) Pré-requisitos: Etapas 1–5 ✅. Reversível sem impacto: sim (somente docs/memória/CI lint).

Implementação aplicada:

  1. Status de todos os achados virados em AUDIT_PII_LOGS_LGPD_2026-05-08.md (#1–#21 ✅) — status global "Etapas 0–6 ✅".
  2. docs/security/AUDIT_LOG.md §4 reescrito com schema canônico de detalhes PII-free + evento *.contato_alterado + nota de obrigatoriedade dos helpers.
  3. docs/development/CODING_STANDARDS.md §4.1 — nova subseção "Mascaramento de PII em logs (LGPD — obrigatório)" com tabela de helpers, regra para alerts ntfy e referência ao lint de CI.
  4. docs/development/PROBLEM_SOLVING.md §5 — anti-padrão "Logar CPF/telefone/e-mail/JID cru" adicionado à tabela de erros reais cometidos.
  5. docs/operations/ALERTAS_PUSH.md §4.1 — nova seção "LGPD — proibido enviar PII para o ntfy" com exemplo de formatAlertSubject.
  6. mem://security/pii-log-masking-standard — memória constraint criada (verificada via code--view no mesmo turno) + entrada em mem://index.md (Memories e Core).
  7. docs/index.md — item "LGPD — Mascaramento de PII em logs" removido do <PendingBacklog>.
  8. CI lint: scripts/audit/pii-helpers-coverage.ts (Deno) detecta console.* em supabase/functions/ mencionando cpf|telefone|email|jid|errorBody|codigo sem token de mask na linha. Wired em .github/workflows/ci.yml job lint-and-build, junto a um step que executa as 4 suítes sentinela Deno (pii-log-sites, pii-helpers, ntfy-helper, diff-helper).

Verificação executada:

  • deno run --allow-read scripts/audit/pii-helpers-coverage.ts → ✅ "PII helpers coverage OK".
  • code--view mem://security/pii-log-masking-standard → arquivo de 25 linhas confirmado (sem ghost memory).
  • Suítes Deno cumulativas (etapas 0–5): 36/36 ✅.

Documentação a atualizar nesta etapa: ✅ todas listadas acima entregues nesta interação.

Volume entregue: P (4 docs editados + 1 doc atualizado + 1 memória + 1 script de lint + 1 ajuste em ci.yml).

Notas: o lint atua como guardrail forward-only — cobre o padrão console.*. Para registrarLog() o guardrail é o teste sentinela pii-log-sites.test.ts (24 sentinelas) que falha se algum dos 8 sites volta ao padrão antigo (email: result.email, codigo: x.codigo, etc).


Documentação contínua (não é etapa separada)

Cada etapa entrega sua própria documentação na mesma interação em que é executada — não acumular para a Etapa 6. Isso segue @workflow §4. A Etapa 6 apenas consolida a regra transversal e fecha o ciclo.


Resumo das 7 etapas — STATUS FINAL ✅

#NomeArquivosVolumePré-reqStatus
0Helper _shared/pii-helpers.ts2 (1 novo + 1 teste)P✅ CONCLUÍDA
1Limpeza console.* em send-otp1P✅ CONCLUÍDA
2console.* restantes (5 sites)5M0✅ CONCLUÍDA
3Alertas ntfypapel @ escola_nome4P✅ CONCLUÍDA
4gerarAlteracoes()exclude + *.contato_alterado3P0✅ CONCLUÍDA
5registrarLog() mascarar resumo/detalhes8M0✅ CONCLUÍDA
6Docs + memória + lint + remover do backlog4 docs + 1 mem + 1 script + CIP1–5✅ CONCLUÍDA

Resultado consolidado: 0 violações detectadas pelo lint; 36/36 sentinelas Deno verdes; 1 memória persistida; backlog docs/index.md zerado para LGPD-PII.

text
Etapa 0 (helper) ─┬─> Etapa 2 (console restantes) ✅
                  ├─> Etapa 4 (diff) ✅
                  └─> Etapa 5 (registrarLog) ✅

Etapa 1 (send-otp) ✅
Etapa 3 (ntfy)     ✅
Etapa 6 (docs+lint) ✅