Repository navigation
fix(auth,http,programs): ciclo de QA da borda — unicidade de CPF, uuidV4 estrito, magic-byte no logo; Hurl substitui o Bruno (ADR-0074) - #1037
Merged
Conversation
O scripts/e2e/bruno-all.sh era o unico lugar que chamava bru run e estava muito desatualizado; foi substituido pela abordagem de QA + pen-test por arquivos .rest. Remove o script e todas as referencias: o npm-script test:integration:all, os READMEs, a rule api-collections.md (verify block + enforced_by), as clausulas do ADR-0038.yaml que apontavam para o script (enforced_by -> []), e os dois testes que o citavam (migrate-boot-inversion e block-bash-file-io). Assisted-by: Claude-Code:claude-opus-4-8
…m contracts/:id) z.uuid() do Zod v4 aceitava UUID bem-formado nao-v4 (nil, max, v1/v3/v5); o dominio rejeita via isUuidV4, e o handler de contracts nao mapeava contract-id-invalid, caindo no default 500 (viola a invariante nunca-500). Introduz um schema uuidV4() compartilhado em src/shared/http (z.uuidv4 estrito, com teste de paridade com o isUuidV4 do dominio) e troca as 11 ocorrencias de z.uuid() da borda, preservando os modificadores. Defesa em profundidade: mapeia contract-id-invalid / amendment-id-invalid / document-id-invalid para 400 no contracts. Efeito medido: UUID nao-v4 passa a devolver 400 uniforme em toda rota :id. Assisted-by: Claude-Code:claude-opus-4-8
O upload de logo (POST /api/v1/programs/:id/logo) confiava no Content-Type declarado e nao conferia os bytes reais — aceitava conteudo arbitrario rotulado como imagem (provado ao vivo: HTML como image/png -> 200). As rotas-irmas (foto de usuario, documentos fiscais) ja checavam magic-byte; o logo ficou de fora. Adiciona imageMagicBytesMatch (jpeg/png/webp) inline no use case uploadProgramLogo, retornando logo-content-mismatch -> 422, consistente com o photo-content-mismatch da foto. Assisted-by: Claude-Code:claude-opus-4-8
…movida ADR-0074, que supersede o ADR-0034 (adocao do @usebruno/cli) e o ADR-0038 (Bruno obrigatorio via CLI + autoria .bru). O gatilho foi o critério do proprio ADR-0038: "um .bru escrito e nao executado e cobertura ilusoria". Quando o runner unico (scripts/e2e/bruno-all.sh) caiu em 05/10 por estar desatualizado, nada o substituiu — e os 266 .bru viraram exatamente o que aquele ADR proibia. Somado a isso, o formato guarda o encadeamento em JavaScript (bru.setVar) e as assercoes em chai, o que exige o runtime do Bruno para saber se o arquivo esta correto. Hurl 8.0.1 resolve os dois pontos: [Captures] nativo (sem JavaScript), --variables-file (o arquivo nasce SEM segredo), --report-junit para CI e `hurlfmt --check` como gate de sintaxe que roda sem servidor. A colecao foi convertida: 241 requests em 23 arquivos, 24 [Captures] e 77 [Asserts] traduzidos do JS, hurlfmt verde nos 23. BREAKING CHANGE: api-collections/ (266 .bru) e @usebruno/cli saem do repositorio. A remocao da devDependency levou 317 pacotes, e com eles as DUAS excecoes de supply-chain que o Bruno exigia (protobufjs em allowBuilds, semver@5.7.2||6.3.1 em trustPolicyExclude) — removidas depois de `pnpm why` provar que nenhuma outra cadeia as trazia. Excecao de politica sobrevive a dependencia que a justificou se ninguem varrer. Os fixtures binarios de upload (sample.jpg/pdf) migram de api-collections/ para tests/fixtures/http-edge/, versionado e neutro: as pastas de saida da conversao sao gitignored e guardar fixture dentro delas tiraria o arquivo do git. Limites medidos, nao supostos: [Captures] NAO cruzam arquivos (capture em a.hurl + uso em b.hurl, mesmo comando e --jobs 1, falha); a suite exige o seed E2E (8 perfis) e hoje roda 82/241, o resto parando em variavel indefinida; --jobs 1 e obrigatorio porque --test paraleliza por default e o rate limit de login e compartilhado. Zero 500 nos 82 requests executados. As colecoes convertidas ficam fora do git por ora (uso interno): o .hurl e versionavel quando virar gate, porque nao carrega segredo; o .http, nao, porque so funciona com o token colado dentro do arquivo. O .gitignore tambem passa a cobrir .cockpit/ (estado de workspace da IDE). Registro historico (handbook/specs, tickets, CHANGELOG) NAO foi reescrito (ADR-0057 §5) e ainda descreve o Bruno: era verdade quando foi escrito. Assisted-by: Claude-Code:claude-opus-5
O ciclo de QA de 05/10 provou que dois usuarios podiam ser criados com o MESMO cpf e e-mails diferentes — ambos 201. Como cpf e documento, isso e duplicacao de identidade: `auth_user` tinha uniqueIndex em email e legacy_id, nenhum em cpf, e `createUserByAdmin` so checava findByEmail. A garantia passa a viver no banco (`auth_user_cpf_idx`, migration 0010). cpf segue nullable e o InnoDB permite multiplos NULL num UNIQUE, entao quem nasce sem perfil (register/OIDC) nao colide — propriedade coberta por teste. Os DOIS adapters que inserem em auth_user distinguem o 1062 pelo nome do indice na sqlMessage: `user-repository.drizzle` devolve `cpf-already-registered` (409 na borda, em create e update) e `provisioned-user-store.drizzle` idem, para a ETL poder tratar. Sem a distincao, dado duplicado se disfarcaria de falha de infra (`provisioned-user-store-unavailable`) e o usuario legado NAO seria migrado — defeito que a propria migration criava. Em `provisionLegacyUser`, cpf duplicado DEGRADA para null + warning e re-tenta, a mesma politica que ja valia para cpf invalido (Evans p.82, reconstituicao): um campo de perfil opcional nao vale o registro inteiro. O id e gerado uma vez e reusado no retry — reconstruir nao troca identidade. Testes: o contrato do port ganha CA7 (duplicado -> erro nomeado), CA8 (dois NULL coexistem) e CA9 (upsert do proprio registro nao e conflito), valendo para as duas implementacoes; CA7 provado invertido (removida a checagem, acusa). O gate de schema varre TODAS as migrations, nao a 0000, porque o indice nasceu na 0010. `user-repository.drizzle.test` passa a honrar AUTH_DATABASE_URL com fallback no helper canonico, como contracts e financial ja faziam. Efeito colateral medido e corrigido: 18 testes de rota ficaram vermelhos (`409 !== 201`) porque as fixtures reusavam um cpf fixo para criar N usuarios. A correcao e a fixture respeitar o invariante novo, nunca afrouxar o assert: `tests/support/synthetic-cpf.ts` deriva cpf valido por modulo 11 de um seed. Gerar em vez de listar numeros e deliberado — fixture e o caminho por onde dado de cadastro real entra num repositorio publico. Verificado contra MySQL 8.4 real (nao so fake): replay das 7 migrations em banco limpo, e o contrato do UserRepository com MYSQL_INTEGRATION=1 passando 11/11. Assisted-by: Claude-Code:claude-opus-5
`.tmp/` e onde script de medicao/conversao tem de viver: no scratchpad da sessao o ESM resolve pelo path do ARQUIVO e os imports do projeto dao MODULE_NOT_FOUND. Mas o diretorio nao entra em tsconfig algum, e o projectService type-aware do ESLint reprova com `was not found by the project service` — qualquer script ad-hoc ali travava o `lint` e, com ele, o gate do hook Stop. A regua ja existia para os irmaos da mesma categoria (descartavel, gitignored, fora de tsconfig): `tools/bugs-scripts/**` e `tests/reports/**` ja estavam na lista de ignores, com esse motivo escrito. `.tmp/` ficou de fora — existia num lugar e faltava no outro. Assisted-by: Claude-Code:claude-opus-5
… 3a UNIQUE da ETL Tres correcoes em `src/` apontadas pela revisao da branch. 1. O nome do indice era string magica em tres arquivos. `AUTH_USER_UNIQUE_INDEX` (schemas/mysql.ts) passa a ser a fonte unica dos tres nomes, e os dois adapters a consomem. Nao e cosmetica: o errno 1062 nao diz QUAL unicidade quebrou — quem distingue e o nome do indice dentro da sqlMessage, e e por ele que o adapter escolhe entre erro nomeado, idempotencia e falha de infra. Com o nome escrito a mao em cada lugar, um `drizzle-kit generate` que o renomeasse passava no gate e quebrava a traducao EM SILENCIO: o 409 do cpf duplicado viraria 500 e a ETL leria dado duplicado como banco indisponivel. O gate novo (schema-hardening, CA10) cobra a PROPRIEDADE, nao a contagem: cada valor da constante existe em alguma migration, nenhum adapter repete o literal, e os adapters de fato consomem a constante (guarda contra verde por vacuidade). A assercao que fecha e a NEGATIVA, e ela foi provada invertida — reintroduzido o literal, o gate acusa. 2. `provisioned-user-store` tratava DUAS das tres UNIQUE que o INSERT alcanca. O `auth_user_email_idx` caia em `provisioned-user-store-unavailable`: dado duplicado disfarcado de falha de infra, o mesmo defeito que o commit anterior corrigiu para o cpf. Agora devolve `email-already-registered`, que PROPAGA (email nao se degrada — e obrigatorio e e a identidade do login), diferente do cpf, que o use case anula e re-tenta. O comentario do arquivo dizia "as duas de auth_user"; eram tres. 3. `upload-program-logo`: o `default` do switch de magic-byte passa a ser fail-CLOSED. Hoje e inalcancavel (a allowlist roda antes), mas protege o dia em que um MIME novo entrar na allowlist sem a assinatura — com `return true` aquele MIME aceitaria conteudo falsificado em silencio (CWE-434), que e o defeito que a funcao veio corrigir.⚠️ A funcao irma em `auth/adapters/http/photo-upload.ts` FICA fail-open, de proposito: lá o magic-byte roda na BORDA e a allowlist so no use case, entao fechar o default trocaria `photo-type-unsupported` por `photo-content-mismatch` — mudanca de contrato observavel, nao endurecimento. A assimetria esta documentada nos dois arquivos; e ela a razao de as duas copias NAO terem sido fundidas num modulo compartilhado nesta passada: unificar exige alinhar a ORDEM primeiro. Nao mexido, com motivo medido: `uuidV4()` em response schema. A revisao sugeriu que contrariava o principio do proprio arquivo, mas o `git blame` mostra o oposto — o comentario de `financial/.../schemas.ts` JA dizia "IDs proprios seguem z.uuid()" antes, e o commit anterior so trocou por `uuidV4()`. A politica (refs estrangeiras nao revalidam, ids proprios revalidam) e pre-existente e deliberada. Risco residual medido: toda fonte de id e `randomUUID()`, os 16 UUID de seed das migrations sao v4 e o banco local tem zero nao-v4. Assisted-by: Claude-Code:claude-opus-5
… verde O `audit (produção — blocking)` estava vermelho com 16 vulnerabilidades (10 high) em quatro pacotes. Nao era regressao deste PR: toda a mudanca anterior em package.json foi a REMOCAO de uma devDependency. O audit so nao rodava na dev desde 10/09, e as advisories sairam nesse intervalo — este PR foi o primeiro run depois delas, e revelou o estado em vez de causa-lo. - `fastify` 5.12.1 -> 5.12.5 (patch, dentro do major 5). - `nodemailer` 9.1.1 -> 10.0.15 (major). O unico breaking do 10.0.0 e exigir Node.js >= 20; estamos em 24. `createTransport`, `sendMail` e `verify()` nao mudaram de assinatura, e e toda a superficie que o adapter usa. - `fast-uri` piso 3.1.6 -> 3.1.7 (resolve 3.1.8) e `brace-expansion` 5.0.9 -> 5.0.11 (resolve 5.0.12), nos overrides. Os dois overrides eram a parte interessante: eles estavam SEGURANDO a versao vulneravel, nao excluindo-a. O comentario do proprio pnpm-workspace.yaml previa isso por escrito — "o piso fixado para a advisory do dia vira o teto quando sai a proxima" — e se cumpriu pela QUINTA vez no fast-uri e pela QUARTA no brace-expansion. Faixa (`>=X <4`) nao empurra ninguem: o piso e ele proprio uma versao aceitavel, entao `@fastify/ajv-compiler` seguia resolvendo 3.1.6 mesmo com o fastify em 5.12.5. Expulsar exige SUBIR o piso acima da vulneravel. O historico de reincidencia fica no comentario; o piso vigente e o novo. Verificacao, com a ressalva de onde ela alcanca: - `pnpm run audit` -> "No known vulnerabilities found" (era 16). - Gate completo verde: 11931 testes, fail 0. - O boot do Fastify esta exercitado de verdade: 190 arquivos de teste montam o app por `fastify.inject`. O `createTransport` real do nodemailer 10 roda em `nodemailer-require-tls.test.ts`, nos dois branches (pool e nao-pool). - NAO houve boot contra SMTP real (o OrbStack estava desligado e subir container exige OK do dono): o envio de ponta a ponta segue coberto so por fake.⚠️ `@types/nodemailer` FICA em `^8`: nao existe `@types/nodemailer@10` — a ultima publicada e 8.0.2. O nodemailer 10 migrou para TypeScript mas declara `types: null` e manteve, de proposito, compatibilidade com o layout do @types (esta no changelog do 10.0.0). Quem tentar "alinhar" os majors nao vai achar o pacote.⚠️ Divida registrada, nao corrigida aqui: o adapter importa subpath profundo (`nodemailer/lib/smtp-pool/index.js`). Esse caminho NAO existe mais no v10 — o pacote publica so `dist/`, e o `exports` mapeia `./lib/*`. Funciona hoje porque sao `import type`, apagados em runtime, e o tipo vem do @types. Virar import de valor quebraria. Trocar para o especificador do `exports` e assunto proprio. Assisted-by: Claude-Code:claude-opus-5
GabrielAderaldo
added a commit
that referenced
this pull request
Oct 6, 2026
A branch do toolchain estava parada à espera da dev. Merge, não rebase: ela está publicada e tem o PR #1035 aberto. Cinco conflitos, dos treze arquivos que os dois lados tocaram: - `scripts/e2e/bruno-all.sh` (delete/modify) — remoção aceita. O Bruno saiu do projeto inteiro pelo ADR-0074, e o runner não se ressuscita. - `eslint.config.js` (delete/modify, invertido) — remoção aceita. A branch retira o ESLint pelo ADR-0072; a dev havia acrescentado `.tmp/**` aos ignores. A entrada foi portada para `ignorePatterns` do `.oxlintrc.json`, ao lado dos irmãos de mesma categoria (`tools/bugs-scripts/**`, `tests/reports/**`): descartável, gitignored, fora de qualquer tsconfig. Sem ela, script ad-hoc em `.tmp/` volta a travar o lint. - `package.json` — os dois lados removeram coisas diferentes e a união remove ambas: sai `test:integration:all` (dev, apontava para o runner do Bruno) e sai `@usebruno/cli` (dev), sai `@typescript/native-preview` (branch, ADR-0072). Preservadas as flags que o TS 7 tornou redundantes, já retiradas pela branch. - `pnpm-lock.yaml` — lado da dev aceito e regenerado com `pnpm install`, como manda o lockfile. Coexistem as deps do toolchain (oxlint, oxlint-tsgolint, typescript 7.0.2) e o que a dev trouxe: fastify 5.12.5, nodemailer 10.0.15, zero `usebruno`, zero `typescript-eslint`, zero `native-preview`. - `handbook/architecture/adr/README.md` — conflito aditivo: 0072 e 0073 da branch, 0074 da dev. Os três preservados em ordem; 0034 e 0038 seguem `Superseded by 0074`. Um vermelho veio do merge e foi corrigido aqui: `node-flags-not-redundant.test.ts` listava `scripts/e2e/bruno-all.sh` em `INVOCATION_POINTS`, e o arquivo deixou de existir. As três falhas tinham essa causa única — o `readSource` estourando ENOENT derrubava junto os dois testes que apenas iteram a mesma lista, que é exatamente o cenário descrito no docstring do gate. Nenhum dos dois lados produz esse vermelho sozinho: a lista é artefato do ADR-0072, o arquivo foi removido pelo ADR-0074. Gate verde: typecheck, format:check, lint (1017 arquivos, 154 regras, 0 erros) e 11963 testes com fail 0, 20 skipped. Assisted-by: Claude-Code:claude-opus-5
This was referenced Oct 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fecha o ciclo de QA/pen-test da borda HTTP de 05–06/10 e troca a ferramenta da suíte de coleções.
O que entra
1. Unicidade de CPF em
auth_user(achado F7)Dois usuários podiam ser criados com o mesmo CPF e e-mails diferentes — ambos 201. CPF é documento, então isso é duplicação de identidade: a tabela tinha UNIQUE em
emailelegacy_id, nenhum emcpf, e o use case só checavafindByEmail.A garantia passa a viver no banco (
auth_user_cpf_idx, migration 0010).cpfsegue nullable e o InnoDB permite múltiplos NULL num UNIQUE, então quem nasce sem perfil (register/OIDC) não colide — propriedade coberta por teste.Os três índices UNIQUE que um INSERT em
auth_useralcança têm desfechos diferentes, e os dois adapters distinguem pelo nome do índice dentro do errno 1062:legacy_idcpfemailSem essa distinção, dado duplicado se disfarçaria de falha de infra (
unavailable) e o usuário legado não seria migrado — defeito que a própria migration criava.2.
uuidV4()estrito na borda (corrige 500 emcontracts/:id)z.uuid()do Zod v4 aceitava UUID bem-formado não-v4 (nil, max, v1/v3/v5); o domínio rejeitava viaisUuidV4e o handler não mapeava o erro, caindo no default 500 — violação da invariante "nunca 500". SchemauuidV4()compartilhado emsrc/shared/http, com teste de paridade contra o VO do domínio, nas 11 ocorrências da borda. UUID não-v4 passa a devolver 400 uniforme em toda rota:id.3. Magic-byte no upload de logo (CWE-434)
POST /api/v1/programs/:id/logoconfiava noContent-Typedeclarado: conteúdo arbitrário rotulado como imagem era aceito. As rotas-irmãs já conferiam assinatura; o logo ficou de fora. Agora valida jpeg/png/webp e devolve 422logo-content-mismatch.4. Hurl substitui o Bruno;
api-collections/é removida (ADR-0074)Ver o ADR para a decisão completa. Em resumo: o gatilho foi o critério do próprio ADR-0038 — "um
.bruescrito e não executado é cobertura ilusória". Quando o runner único caiu em 05/10, nada o substituiu, e os 266.bruviraram exatamente o que aquele ADR proibia.Hurl 8.0.1 resolve os dois limites do formato anterior:
[Captures]nativo (o encadeamento sai do JavaScript),--variables-file(o arquivo nasce sem segredo),--report-junitpara CI ehurlfmt --checkcomo gate de sintaxe que roda sem servidor. A coleção foi convertida — 241 requests em 23 arquivos, com 24[Captures]e 77[Asserts]traduzidos do JS.BREAKING CHANGE:
api-collections/(266.bru) e@usebruno/clisaem do repositório. A remoção da devDependency levou 317 pacotes e, com eles, as duas exceções de supply-chain que o Bruno exigia (protobufjsemallowBuilds,semver@5.7.2 || 6.3.1emtrustPolicyExclude) — removidas depois depnpm whyprovar que nenhuma outra cadeia as trazia.Verificação
format→typecheck→lint→test). Final: 11931 testes,fail 0, 19 skipped e 5 todo pré-existentes.UserRepositorycomMYSQL_INTEGRATION=1passando 11/11 — inclusive a propriedade de múltiplos NULL no UNIQUE, que um fake só pode imitar./security-review: zero vulnerabilidades novas. Verificado em particular que asqlMessagedo 1062 (que carrega o valor duplicado) não chega a log — osreturnde erro nomeado precedem ostderr.writenos dois adapters, e CPF/e-mail são PII./code-reviewem esforço alto: achados desrc/tratados no último commit.Limites conhecidos, declarados
.hurlé versionável quando virar gate, porque não carrega segredo.[Captures]do Hurl não cruzam arquivos (medido). Ou o arquivo é autocontido, ou o token vem de--variables-file.--jobs 1é obrigatório:--testparaleliza por default e o rate limit de login é compartilhado.ALTER TABLE ADD CONSTRAINT UNIQUEfalha com 1062, e o replay em banco limpo é cego para isso (banco limpo não tem linhas). A premissa do F7 é justamente que duplicatas eram aceitas — então o pré-voo em ambiente com dado é necessário, e foi decisão do dono assumir esse risco, não um esquecimento.handbook/specs,tickets,CHANGELOG) não foi reescrito (ADR-0057 §5) e ainda descreve o Bruno: era verdade quando foi escrito.Decisões de arquivo
api-collections/paratests/fixtures/http-edge/(versionado e neutro), comgit mvpara preservar histórico.Superseded by ADR-0074, com a cadeia fechada nos YAMLs. O §5 do ADR-0074 reafirma por inteiro o que o 0038 estabeleceu e continua valendo — norma corrente não se apoia em documento superseded..claude/rules/api-collections.mdesupply-chain.mdatualizadas: a segunda afirmava que@usebruno/cliera devDependency pinada, que deixou de ser verdade.Follow-up registrado
Achados do code-review que ficaram fora de
src/e seguem abertos: o docblock dosynthetic-cpfpromete unicidade que o módulo quebra nas bordas da faixa, e os fixtures movidos não têm consumidor versionado enquanto a suíte Hurl estiver fora do git.