Release v6.0.0 — correção de contrato provada contra a API real - #46
Merged
Conversation
Probes de contrato contra a API viva carregam ids de empresa, formato da conta e sequencia de chamadas. O repositorio e publico e o campo files do package.json governa apenas o pacote npm, nao o que fica visivel no GitHub. A regra entra antes de qualquer arquivo existir em scripts/probes/: o gitignore so age sobre o que o git ainda nao rastreia.
Envelope real (status, content-type, Location, forma do corpo) com corpo sintetico: nenhum CNPJ, chave de acesso, id de empresa ou URL pre-assinada real entra no repositorio publico. A evidencia crua fica no vault. Consumidos pelos mocks de fix-consumer-invoices-contract e fix-binary-downloads-inbound, que dependiam de decisao de contrato que so a API viva responde.
… certificado
Dois bugs de contrato provados por sonda ao vivo contra a API real (2026-09-01).
Em nenhum dos dois havia divergencia de spec: o SDK e que estava errado.
1) As duas chaves da plataforma sao complementares, nao intercambiaveis — cada
uma responde 403 nos hosts da outra familia. O cliente HTTP de api.nfse.io
resolvia a chave de DADOS num host FISCAL, afetando nove recursos:
productInvoices, productInvoicesRtc, transportationInvoices,
inboundProductInvoices, municipalTaxes, certificates, stateTaxes,
taxCalculation e o lado v2 de companies.
Funcionavam por acidente: quem configurava so apiKey caia no fallback
dataApiKey -> apiKey. Quem configurava dataApiKey tomava 403.
getCteHttpClient e getNfseMainHttpClient apontavam para o MESMO host com
chaves diferentes — duas regras contraditorias. Fundidos em getNfseHttpClient.
BREAKING: cliente configurado SO com dataApiKey deixa de acessar os nove
recursos fiscais e lanca ConfigurationError no acesso, em vez de falhar com
403 na chamada.
2) uploadCertificate enviava o campo multipart 'certificate'; a API faz binding
de 'file' e respondia 400 {"errors":{"file":["The File field is required."]}}.
O metodo nunca tinha como completar.
A suite existente afirmava o comportamento errado como correto e so verificava
que o acesso nao lancava, nunca qual chave ia para o fio — por isso o bug
sobreviveu. O teste novo afirma o header por familia de host.
As rotas de entrada (/inbound/{chave}/xml, /pdf e /events/{evento}/xml,
compartilhadas por CT-e e NF-e Distribuicao) respondem com um objeto
{ publicTemporaryUri } — URL pre-assinada e temporaria. Binario NUNCA trafega
nessas rotas, e o header Accept nao altera a resposta.
Provado por sonda ao vivo em 2026-09-01 sobre chaves reais de entrada. Isso
FALSIFICA a premissa da review de 07/2026, que supunha PDF binario chegando como
string e corrompendo bytes: nao ha bytes. O plano original (trocar para
http.getBuffer) estava errado.
Os cinco metodos passam de Promise<string> para Promise<InboundFileResource>.
Nenhum chamador correto quebra — o retorno anterior ja era este objeto se
passando por string.
O envelope difere do usado por NFC-e e NF-e produto, que nomeiam o campo 'uri'.
Sao dois envelopes na mesma plataforma, entao InboundFileResource e um tipo
separado de NfeFileResource, de proposito.
Testes existentes mockavam string e passavam mesmo assim, porque so verificavam
passagem adiante. Realinhados ao formato real, mais tres testes de contrato:
xml e pdf devolvem o mesmo formato, nao se envia Accept, e o retorno nao e
Buffer nem string.
…NFC-e
Contrato conferido nas duas specs (repo e nfeio-docs, identicas nestes paths) e
provado por sonda ao vivo em 2026-09-01.
- cancel() aceita reason (query da spec) e devolve
ConsumerInvoiceCancellationResponse em vez da nota
- getItems()/getEvents() aceitam paginacao cursor (limit/startingAfter) e
devolvem envelopes proprios com hasMore; o de eventos deixa de reusar o tipo
do recurso de produto, que tem outra forma
- downloadPdf() aceita force; os tres downloads passam a devolver
ConsumerInvoiceFileResource ({ uri }) em vez de Buffer — a API devolve JSON
com URL e IGNORA o header Accept
- retrieve()/getItems()/getEvents() param de enviar environment, que a spec nao
define nessas rotas (a API tolera, mas e ruido)
list() CONTINUA exigindo environment: a sonda mostrou 400 'environment has to be
production or test' sem ele. A tarefa 2.3 previa tornar o parametro opcional 'na
direcao permissiva' — isso teria introduzido um bug. A spec e que esta errada.
Novos aliases derivados da spec em types.ts, mais teste de alinhamento
tests/types/consumer-invoice-alignment.test-d.ts pinando: uri vs
publicTemporaryUri sao envelopes distintos, items/events carregam hasMore, e o
cancelamento devolve o recurso de cancelamento.
30 dos 131 endpoints sao declarados em mais de uma spec (companies,
certificates, statetaxes, webhooks) e as copias divergem. O SOURCES.json nao
pegava: ele compara sha256 repo x docs, e este drift e *entre* specs do mesmo
lado.
O SOURCES.json ganha `sharedSections` com a fonte canonica de cada grupo, e o
validate:spec compara as copias contra ela campo a campo.
A granularidade foi escolhida por medicao, nao por gosto. Comparar a operacao
inteira -- mesmo descontando prosa -- marca 100% dos paths dos grupos A e B
como divergentes: ruido de forma (`content: {}` x ausente, `$ref` x inline,
ordem de chave). Campo a campo (`caminho -> tipo/enum`), o grupo C isola
exatamente as 24 contradicoes reais de contentType/status, sem um falso
positivo.
Quatro classes: type-mismatch e enum-mismatch falham o build, enum-subset
avisa (a copia esta atrasada), presenca de campo e informativa.
As 116 divergencias de hoje entram como baseline declarada, agrupadas por
CAUSA e nao por campo, cada uma com motivo e pendencia upstream:
74 enum-mismatch grupo A valores de enum camelCased ('sP' no lugar de
'SP', 'active' no lugar de 'Active') em
nf-produto-v2 e nf-consumidor-v2 -- defeito de
serializacao, achado nesta medicao
24 type-mismatch grupo C contentType/status como integer nas copias; o
fio manda string (sonda 2026-09-01)
18 enum-subset grupo B nf-servico-v1 atrasada em taxRegime,
legalNature e certificate.status
Baseline que deixa de reproduzir e reportada como obsoleta, para nao virar
tapete. O que falha o build e drift novo.
Canonica de webhooks e nf-consumidor-v2, nao nf-produto-v2: e a unica com o
TIPO certo (string) e a unica superset (11 campos x 10, so ela declara `id`).
discoverSpecs() do validador passa a aceitar .json -- contribuintes-v2.json,
canonica das secoes de companies, nunca tinha sido validado, enquanto o
generate-types.ts ja o processava.
Sem efeito em runtime nem na API publica: dist/index.d.ts sai byte-identico ao
build anterior, e src/generated/ nao muda uma linha que nao seja timestamp.
Testes: 17 novos, com fixture para cada classe -- inclusive a de diferenca
puramente de forma, que e o caso que invalidou as tres primeiras estrategias.
publish.yml marcava o passo de testes com `continue-on-error: true`, e um bloco
logo abaixo justificava por escrito:
"Some tests failed, but continuing with publish"
"This is expected for integration tests without API credentials"
A justificativa e falsa. Medido: sem credencial a suite da 41 passed | 4 skipped,
exit 0. Os testes de integracao PULAM, nao falham -- o guard
shouldRunIntegrationTests() cuida disso desde sempre. O continue-on-error
protegia contra um modo de falha que nao existe e, em troca, deixava passar todos
os que existem. Os tres bugs de contrato corrigidos nesta mesma versao sairam por
esse portao.
Agora publish.yml roda testes, lint, typecheck e test:types antes do build, e
qualquer um deles barra a publicacao. test:types tambem entrou no ci.yml: eram 18
assertions -- incluindo os guards de alinhamento de contrato escritos em 01/09 --
que nunca executavam. Custam 3,3s.
A suite de integracao tambem nao podia ser rodada: dotenv era devDependency e
nada carregava o .env, entao NFE_API_KEY chegava vazia e a integracao pulava
sempre, inclusive na maquina de quem tem credencial. Carregado em tests/setup.ts
(dotenv nao sobrescreve ambiente existente, entao export manual e CI continuam
vencendo o arquivo).
Com isso a execucao local foi de 742 para 779 testes -- 37 que nunca haviam
rodado -- e tres assertions apodrecidas apareceram: afirmavam
Array.isArray(companies) contra um ListResponse ({data, page}), e uma quarta lia
companies.length (undefined). Sao de antes da migracao para ListResponse.
Corrigidas. Os outros 3 arquivos de integracao passaram intactos.
O guard NAO mudou: em CI a integracao continua pulando sem
RUN_INTEGRATION_TESTS=true. Credencial de conta compartilhada nao vai para
runner. Verificado nos dois modos.
O it.skip do upload de certificado fica desligado, agora com motivo honesto no
lugar do vago: exigiria .pfx versionado (material sensivel) e CRIA empresa numa
conta compartilhada. O que ele protegeria ja esta coberto sem rede pelo unitario
que afirma o campo multipart.
Registrado tambem, em generation.test.ts, o efeito que este portao NAO cobre: a
suite roda a geracao de verdade e suja src/generated/ com ~22 linhas de carimbo.
A correcao pertence ao pipeline de geracao.
Nada em src/. Zero efeito em runtime ou API publica.
`healthCheck()` respondia `status: 'error'` SEMPRE -- com credencial valida e
API no ar -- porque enviava `pageCount: 1`:
GET /v1/companies?pageCount=1 -> 400 "pageCount must be between 1 and 50"
GET /v1/companies?pageCount=2 -> 200
GET /v1/companies -> 200
O limite inferior do servidor esta um a mais do que a propria mensagem diz.
Medido em 2026-09-02 contra api.nfe.io com chave real; o metodo agora omite o
parametro, em vez de carregar um numero magico contornando defeito alheio.
O off-by-one vai para o time de API como pendencia propria.
O teste afirma o parametro efetivamente enviado. Afirmar so `status: 'ok'`
contra um mock nao pegaria a regressao: o mock responde 200 para qualquer
query, inclusive a que a API recusa. Nao havia nenhum teste de healthCheck
ate aqui.
`getCertificateStatus` lia `{hasCertificate, expiresOn, isValid}`. A API
devolve outra coisa:
GET /v1/companies/{id}/certificate
-> { "certificates": [ { providerType, resolution, taxPayerId, thumbprint,
taxId, subject, validUntil, modifiedOn, status } ] }
Nenhum dos tres campos lidos existe, entao o metodo devolvia
`{hasCertificate: undefined}` e o `if` que calcula os derivados nunca era
verdadeiro. Isso derrubava em cascata `checkCertificateExpiration`,
`getCompaniesWithCertificates` e `getCompaniesWithExpiringCertificates` --
quatro metodos publicos. Confirmado na spec (CertificatesMetadataResource,
contribuintes-v2) e no fio em 2026-09-02.
Empresa sem certificado responde 200 com `certificates: []`, nao 404 -- 9 de
12 empresas sondadas na conta estao nesse caso.
O resumo mantem `expiresOn` em vez de renomear para `validUntil`: e o mesmo
nome que a API usa quando o certificado vem embutido no item da listagem
(CompanyCertificateV1), entao manter alinha as duas superficies em vez de
quebrar o chamador de novo. Os itens crus ficam expostos em `certificates`.
A varredura por conta deixa de fazer N+1
--------------------------------------
`getCompaniesWithCertificates` e `getCompaniesWithExpiringCertificates`
chamavam `getCertificateStatus` uma vez por empresa, em serie, sobre
`listAll()`. Enquanto o metodo estava quebrado isso era invisivel; consertado,
a conta do time (>=500 empresas) faria >=500 requisicoes sequenciais por
chamada.
Cheguei com um pool de concorrencia pronto e a sonda tornou a discussao
desnecessaria: `GET /v1/companies` ja devolve `certificate` em TODO item, com
`{thumbprint, modifiedOn, expiresOn, status}`. As duas varreduras passam a ler
dai. Zero requisicao extra, zero maquina de concorrencia. Medido ao vivo: as
duas varreduras juntas, sobre a conta inteira, em 9,8s.
Os mocks
--------
Os testes unitarios alimentavam a forma inventada e por isso passavam: o mock
validava a leitura contra a propria invencao. Reescritos contra o envelope
real, mais os casos que faltavam (lista vazia, escolha entre varios
certificados, status != Active, `validUntil` ausente).
`tests/unit/companies.test.ts` tambem stubava `getDaysUntilExpiration` em 365
dias fixos no mock de modulo -- todo teste de vencimento media a constante do
mock, nao a data. O mock agora e parcial: so o que depende de um .pfx de
verdade fica falso; a aritmetica de datas e real.
E `tests/integration/certificates.integration.test.ts` afirma o contrato
contra a API real, inclusive que os campos antigos NAO existem na resposta.
Um mock nao consegue afirmar o nome de um campo que so a API sabe.
…lote
`downloadPdf(companyId)` e `downloadXml(companyId)` sem o id montavam
`/serviceinvoices/pdf` e `/serviceinvoices/xml`. O servidor casa a rota `/{id}`
e le o sufixo como identificador:
GET /v1/companies/{id}/serviceinvoices/pdf
-> 404 "service invoice with id (pdf) was not found"
A rota em lote nao existe: nao esta na spec nf-servico-v1 nem no nfeio-docs.
O comentario `// Bulk download for company (returns ZIP)` descrevia uma rota
inventada. Medido em 2026-09-02.
BREAKING CHANGE: `invoiceId` passa a ser obrigatorio nos dois metodos. Toda
chamada afetada ja falhava em runtime; agora falha na compilacao do consumidor.
Para varias notas, itere sobre os ids. Nota em MIGRATION.md.
Documentacao
------------
README, docs/downloads.md e docs/API.md prometiam "ZIP com todas as notas",
com exemplo pronto para copiar -- em quatro lugares, incluindo um bullet de
features. Removidos, com a medicao no lugar.
Testes
------
Havia DUAS copias dos testes de download (tests/unit/service-invoices.test.ts
e tests/unit/core/resources/service-invoices.test.ts), as duas afirmando o
caminho em lote. Elas afirmavam o caminho que o SDK montava, nunca que a API
o servisse -- passavam verdes contra uma rota inexistente.
Ressalva: `npm run typecheck` NAO teria pego isso. O tsconfig limita o include
a `src/**/*` e ainda exclui `**/*.test.ts`, entao nenhum arquivo de teste e
verificado. As chamadas sobreviventes so apareceram quando a suite rodou, e uma
montou `/serviceinvoices/undefined/pdf`. Lacuna inventariada.
Duas correcoes que sairam da mesma medicao.
1) Accept dos downloads por chave de acesso
-------------------------------------------
`productInvoiceQuery.downloadPdf/downloadXml` mandavam so o tipo binario.
No caminho feliz isso funciona -- e sempre funcionou, ao contrario do que o
diagnostico anterior registrou. O problema e o caminho de ERRO: o servidor nao
tem formatter de erro para `application/pdf` e responde 406 com corpo vazio,
apagando a mensagem. Medido em 2026-09-02 contra nfe.api.nfe.io:
.pdf + "application/pdf" chave real: 200 %PDF-1.4 (7623 bytes)
inexistente: 406, corpo vazio
.pdf + "application/pdf, application/json;q=0.9" chave real: 200 %PDF-1.4 (MESMOS bytes)
inexistente: 400 "access key is not valid"
O tipo binario continua em primeiro, entao o caminho feliz nao muda -- mesmo
status, mesmo content-type, mesmo numero de bytes. O `Buffer` de retorno segue
valendo porque o content-type de sucesso nao mudou.
2) O extrator de mensagem de erro ignorava dois envelopes reais
---------------------------------------------------------------
Com o Accept corrigido o erro chegava com corpo JSON -- e o SDK jogava a
mensagem fora assim mesmo. `extractErrorMessage` so lia
`message`/`error`/`detail`/`details`. A plataforma usa quatro envelopes:
"pageCount must be between 1 and 50" string JSON crua
{"code":40001,"message":"environment has to be ..."} campo message
{"errors":[{"message":"access key is not valid"}]} lista (hosts de consulta)
{"title":"...","errors":{"file":["The File field ..."]}} ProblemDetails/ModelState
Nos dois ultimos o chamador recebia `HTTP 400 error` -- literalmente o status
que ele ja tinha. Foi assim que `The File field is required.` ficou invisivel
enquanto o upload de certificado nao funcionava.
Os quatro envelopes viraram teste, mais os casos de borda (lista vazia,
varias mensagens, corpo sem nada aproveitavel).
E `tests/integration/setup.ts` passa a repassar `NFE_DATA_API_KEY`: sem a chave
de dados os hosts de consulta respondem 403, e nenhum teste de integracao podia
tocar neles.
Quatro metodos publicos apontam para rotas declaradas na OpenAPI que a
plataforma NAO serve:
GET /v2/companies/{id}/municipaltaxes/{mtid}/series/{serie}
PATCH /v2/companies/{id}/municipaltaxes/{mtid}/updateprefecture
GET /v1/consumerinvoices/coupon/{chave}
GET /v1/consumerinvoices/coupon/{chave}.xml
Ate aqui o chamador recebia `NotFoundError` generico, indistinguivel de "esse
dado nao existe" -- e ia procurar defeito nos proprios dados.
Como a distincao foi feita (2026-09-02)
---------------------------------------
Comparando com um path inventado no mesmo host: 404 de corpo vazio, sem
content-type, byte a byte igual ao do path inventado, e roteamento, nao "nao
encontrado". Confirmacao independente: rota servida responde 401 SEM
credencial; as de cima respondem 404 sem credencial -- o middleware de
autenticacao nem chega a rodar. Alem disso, 90 dias de log de gateway nao tem
um unico 200 em `consumerinvoices/coupon`.
A escolha
---------
Nao remover os metodos (apagaria a informacao de que a rota existe na spec e
nao e servida) e nao bloquear no cliente (congelaria a medicao de hoje no
codigo, e ninguem lembraria de tirar o guard). A requisicao sai; SO o 404 e
enriquecido, preservando a classe do erro para nao quebrar `instanceof`. Se a
rota subir, o 200 passa intacto -- e ha teste afirmando exatamente isso.
legalPeople / naturalPeople NAO estavam quebrados
-------------------------------------------------
O diagnostico de julho registrou os 14 metodos como "400 em toda chamada". A
sonda tinha usado a empresa do .env, de id com 32 caracteres; a rota valida o
company_id como ObjectId de 24 hex. Sobre 50 empresas da mesma conta:
30 com id de 24 hex -> 200
19 com id de 32 chars -> 400 "company id is not valid"
Um id de 24 hex sintetico responde `404 "Company not found."`: o validador de
formato passa e a busca e que falha. E limite do servidor -- nao ha conversao
possivel, e validar localmente so antecipa a recusa com mensagem pior.
Documentado no JSDoc dos dois recursos, com teste de integracao afirmando as
duas metades para a proxima leitura nao repetir a generalizacao.
CHANGELOG (pt-BR) da frente inteira: os cinco bugs corrigidos, os quatro metodos depreciados por rota nao servida, a quebra do download de NFS-e, e a correcao de registro dos DOIS que nao estavam quebrados (productInvoiceQuery e os 14 de legalPeople/naturalPeople). Fixture `tests/fixtures/live-contracts/unreachable-methods.json` guarda a medicao, inclusive o metodo de discriminacao -- comparar com path inventado no mesmo host, e conferir a resposta SEM credencial. Foi o que separou "rota nao servida" de "registro nao encontrado", e "metodo quebrado" de "entrada invalida". Chave de acesso, CNPJ, thumbprint e id de empresa reais nao entram: os valores sao sinteticos ou <redigido>. Verificacao: typecheck limpo, lint 0 erros, test:types 18/18, build ok, dependencies segue vazio. Suite em modo CI 766 passed | 54 skipped; local 813 passed | 7 skipped, 50 arquivos, nenhum falho.
Duas superficies que nao sao codigo, mas que o usuario le como contrato.
1) Toda requisicao mentia sobre quem era
----------------------------------------
`src/core/http/client.ts` fixava `packageVersion = '3.0.0'` com um
`// TODO: Read from package.json`. O User-Agent saia como `@nfe-io/sdk@3.0.0`:
nome de pacote que NAO existe (o publicado e `nfe-io`) e versao tres majors
atras. Medido nos logs de gateway, 30 dias:
93.995 requisicoes | 23 variantes de User-Agent | 5 majors de Node
| 1 unica versao de SDK reportada
As 23 variantes diferem so no Node e na plataforma. O User-Agent e o unico
sinal de adocao que a plataforma tem, e nao trazia informacao nenhuma sobre a
versao -- nao dava para saber quem migrou nem correlacionar incidente com
versao. A partir da proxima release, da.
O valor divergia em quatro lugares: 3.0.0 no User-Agent, 5.1.0 em
PACKAGE_VERSION e VERSION, 5.2.0 no package.json. E PACKAGE_NAME -- constante
PUBLICA -- dizia `@nfe-io/sdk`.
Agora ha fonte unica: `src/version.ts`, gerado do package.json por
`scripts/generate-version.ts` (ligado ao `npm run generate`). Gerado, e nao
lido em runtime, porque `require('../package.json')` quebra em bundle e acopla
o runtime ao layout do pacote. Nenhum literal de versao sobrou em `src/`, e
`tests/unit/version.test.ts` falha se algum voltar: a geracao e a conveniencia,
o teste e a garantia. Conferido no artefato construido -- `dist/` ja sai
`nfe-io@5.2.0`.
Nove exemplos de JSDoc mandavam importar de '@nfe-io/sdk'. Corrigidos; a skill
nao precisa mais avisar que o JSDoc mente.
2) A documentacao ensinava o wiring que a API recusa
-----------------------------------------------------
`docs/multi-host-routing.md` dizia que productInvoices, productInvoicesRtc,
stateTaxes, municipalTaxes, certificates, transportationInvoices e
inboundProductInvoices usavam a chave DE DADOS em api.nfse.io. E host FISCAL:
responde 403 a chave de dados. Era o mesmo defeito corrigido no roteamento
interno em b50bb74, ainda ensinado como se fosse o certo -- quem seguisse a
tabela reintroduzia o bug na propria aplicacao. Faltavam tambem taxCalculation
e o lado v2 de companies. Tabela refeita a partir do codigo.
A nota de fallback deixou de sugerir que as chaves sao alternativas: sao
complementares, cada uma responde 403 no territorio da outra.
3) Exemplos que nao compilavam
-------------------------------
README documentava `addresses.lookupByTerm()` e `addresses.search()`, removidos
na v5. A skill publicada chamava `uploadCertificate(companyId, certBuffer,
'password')`, mas a assinatura recebe um objeto. A skill tambem listava
taxCalculation entre os recursos de dataApiKey (usa a principal) e apresentava
consumerInvoiceQuery e municipalTaxes.getSeries/updatePrefecture como
funcionais -- sao rotas que a plataforma nao serve.
`tests/unit/docs-drift.test.ts` passa a falhar quando README, docs/ ou a skill
citam metodo que nao existe. Casa NOME de metodo, nao assinatura: conferir
assinatura exigiria compilar cada exemplo, e isso e change propria. Mesmo assim
pega os dois casos desta rodada -- verificado reintroduzindo o trecho antigo.
4) Um teste travava o bug no lugar
-----------------------------------
`tests/unit/http-client.test.ts` afirmava que o User-Agent continha
'@nfe-io/sdk'. Quem consertasse o nome quebrava a suite. Corrigido para afirmar
o nome real e negar o antigo.
Verificacao: typecheck limpo, lint 0 erros, test:types 18/18, build ok.
Suite em modo CI: 776 passed | 54 skipped (era 766).
Nenhum era chamado por package.json, workflow ou documentacao, e os tres
carregavam fatos que deixaram de valer ha duas majors:
scripts/release.sh tag e commit `v3.0.0` fixos; `git push origin v3`
(branch que nao existe; a mainline e master);
`npm view @nfe-io/sdk` (pacote que nao existe);
nao faz bump de versao nenhum
scripts/release.ps1 mesmo v3.0.0 fixo; "Alguns testes podem falhar
(tests/core.test.ts - arquivo legado)" e "107/122
testes principais estao passando" -- a suite tem 830
RELEASE_COMMANDS.sh mesmo v3.0.0 fixo, na RAIZ do repo (o mais
descobrivel dos tres); apontava para o release.sh;
"Package renamed: nfe -> @nfe-io/sdk", que e o
inverso do que aconteceu
Quem rodasse qualquer um deles hoje criaria a tag errada. O .ps1 ainda
normalizava teste falhando no release -- a mesma premissa do
`continue-on-error` que a change enforce-release-gate acabou de remover.
Nada se perde: `publish.yml` ja faz tudo que eles faziam (testes, lint,
typecheck, test:types, build, verificacao dos artefatos de dist, dry-run) e
publica com provenance ao criar a release no GitHub. As tres ultimas releases
(v5.0.0, v5.1.0, v5.2.0) passaram por PR `release/vX.Y.Z`, nao por estes
scripts.
O `RELEASE_COMMANDS.sh` nao foi nomeado no pedido, mas e da mesma familia,
ficaria apontando para um arquivo apagado e e o unico dos tres na raiz.
Nenhum deles ia para o npm: `files` do package.json publica so dist, README,
CHANGELOG, MIGRATION e skills.
O passo "Check package.json version" so LIA a versao para uma saida. Criar a
release `v6.0.0` com o package.json ainda em `5.2.0` passava por ele sem
reclamar; o `npm publish` acabaria falhando por versao ja publicada, mas por
acidente -- e depois de instalar, testar, lintar e buildar tudo.
Pior no caso inverso, que nao tem rede de seguranca nenhuma: package.json em
`6.0.0` com a release criada como `v5.2.0` publica 6.0.0 no npm enquanto a
release, o CHANGELOG e as notas dizem 5.2.0.
Agora o passo compara e reprova, e subiu para logo depois do setup do Node --
falha em segundos, antes do `npm ci`. Vale para os dois gatilhos:
`release: created` traz a tag em `github.event.release.tag_name`,
`workflow_dispatch` em `inputs.tag`. O prefixo `v` e opcional.
Conferido com valores simulados: v6.0.0/6.0.0 passa, 6.0.0 sem prefixo passa,
pre-release (v6.0.0-rc.1) passa, package.json desatualizado reprova, tag
desatualizada reprova, tag vazia reprova.
Sai tambem uma referencia morta no resumo da release: o bloco condicional lia
`steps.tests.outcome`, mas o passo de testes perdeu o `id: tests` junto com o
`continue-on-error` na change enforce-release-gate. A condicao nunca era
verdadeira, e o texto repetia a justificativa falsa que aquela change removeu
("Some tests failed during CI (expected for integration tests)").
Revisao de seguranca automatizada apontou injecao de comando no passo que
acabei de adicionar, e o apontamento procede.
`${{ github.event.release.tag_name }}` dentro de um bloco `run:` e substituido
ANTES de o bash parsear a linha. Um valor com metacaractere deixa de ser dado e
vira comando. Nao e teorico -- `git check-ref-format` aceita `;`, `$(...)`,
crase, `&&` e `|` em nome de tag (so o espaco e recusado), e o `inputs.tag` do
`workflow_dispatch` nao valida coisa alguma.
O que estava em risco: este job tem `id-token: write` e `secrets.NPM_TOKEN`.
Injecao aqui exfiltra a credencial que publica um pacote que clientes instalam
-- vetor de supply chain. A barreira e ter permissao de escrita no repo, mas
conta de contribuidor comprometida e exatamente como esse tipo de ataque
comeca, e o custo de fechar e zero.
Correcao: os valores passam por `env:` e o script le variavel de ambiente. O
bash recebe o conteudo como dado e nunca o reparseia. Mesmo tratamento no
"Create GitHub Release Summary", que interpolava
`steps.package-version.outputs.version` em tres pontos -- risco menor, porque o
valor vem do package.json e ja passou pela checagem, mas e a mesma classe.
Defesa em profundidade: a tag agora precisa casar
`^v?[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$` antes de qualquer comparacao.
Alem de barrar valor hostil, pega tag digitada errada de graca.
Verificado com payload real: `v6.0.0;touch ...`, `v6.0.0$(touch ...)`,
`v6.0.0\`touch ...\`` e `v6.0.0 && touch ...` pelo dispatch -- os quatro
reprovam e o arquivo nao e criado. Comportamento normal intacto: v6.0.0, 6.0.0
sem prefixo, pre-release, dispatch por inputs.tag, package.json desatualizado e
tag vazia continuam com o mesmo veredito de antes.
Nenhuma interpolacao `${{ }}` sobrou dentro de bloco `run:` nos dois workflows.
Major de correcao de contrato. Nenhuma funcionalidade nova: sao bugs provados por sonda ao vivo contra a API real entre 01 e 03/09, a maioria em metodos que nunca puderam funcionar. E major porque nove pontos da superficie publica mudam de tipo ou assinatura -- medido comparando o dist/index.d.ts com o construido a partir da v5.2.0 publicada, nao por estimativa: serviceInvoices.downloadPdf/downloadXml invoiceId? -> obrigatorio inbound.getXml/getPdf/getEventXml string -> InboundFileResource transportationInvoices.download* string -> InboundFileResource consumerInvoices.download* Buffer -> ConsumerInvoiceFileResource consumerInvoices.getItems/getEvents environment? -> options?, retorno novo consumerInvoices.cancel retorno novo companies.getCertificateStatus inline -> CertificateStatusSummary PACKAGE_NAME '@nfe-io/sdk' -> 'nfe-io' wiring de credencial ConfigurationError em vez de 403 Zero metodos removidos. Na pratica quase ninguem precisa mexer: as quebras sao em superficies que ja estavam quebradas -- metodos que so respondiam 404, retornos que vinham undefined, tipos que mentiam sobre o conteudo. Dois tipos vazavam ------------------ `CertificateStatusSummary` e `ConsumerInvoicePageOptions` apareciam em assinatura publica e NAO estavam na lista de exports: o chamador nao conseguia nomear o retorno de `getCertificateStatus()` nem o argumento de `getItems()`/`getEvents()`. Exportados. Achado ao montar o guia de migracao -- escrever a migracao e o que faz alguem ler a superficie do lado de fora. O bump em si ------------ package.json 5.2.0 -> 6.0.0, e `src/version.ts` acompanhou sozinho via `npm run generate:version`. Simulado o portao do publish.yml contra este estado: tag `v6.0.0` passa, `v5.2.0` reprova. CHANGELOG com secao [6.0.0] datada, MIGRATION.md com o roteiro completo v5 -> v6 (sete quebras, quatro depreciados e a restricao de id que e do servidor), e o badge do README apontando para a nova secao. Verificacao: typecheck limpo, lint 0 erros, test:types 18/18, build ok, 776 passed | 54 skipped.
Arquivo temporario, para ser apagado depois de abrir o PR. Fica versionado para voce revisar o texto antes de publicar, e para o comando de abertura (`gh pr create --body-file`) ter de onde ler.
📋 OpenAPI Spec Validation✅ All specs validated and types generated successfully Specs processed:
Generated types available as artifact in |
📋 OpenAPI Spec Validation✅ All specs validated and types generated successfully Specs processed:
Generated types available as artifact in |
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.
O que é
Major de correção de contrato. Nenhuma funcionalidade nova: são bugs provados por sonda
ao vivo contra a API real entre 01 e 03/09, a maioria em métodos que nunca puderam
funcionar.
Em nenhum deles a especificação era a culpada — o SDK é que estava errado. Evidência
versionada em
tests/fixtures/live-contracts/, com dado sensível redigido.Medidas comparando o
dist/index.d.tsdesta branch com o construído a partir da v5.2.0publicada — não estimadas.
serviceInvoices.downloadPdf/downloadXmlinvoiceId?: stringinvoiceId: stringinboundProductInvoices.getXml/getPdf/getEventXmlPromise<string>Promise<InboundFileResource>transportationInvoices.downloadXml/downloadEventXmlPromise<string>Promise<InboundFileResource>consumerInvoices.downloadPdf/Xml/RejectionXmlBuffer/NfeFileResourceConsumerInvoiceFileResourceconsumerInvoices.getItems/getEventsenvironment?options?, retorno próprioconsumerInvoices.cancelConsumerInvoiceCancellationResponsecompanies.getCertificateStatusCertificateStatusSummaryPACKAGE_NAME'@nfe-io/sdk''nfe-io'403na chamadaConfigurationErrorna horaZero métodos removidos. Só uma quebra interrompe compilação de código que funcionava:
o
invoiceIdobrigatório. As outras oito são em superfícies que já estavam quebradas —métodos que só respondiam 404, retornos que vinham
undefined, tipos que mentiam sobre oque continham.
Roteiro completo em
MIGRATION.md.Os bugs, por ordem de gravidade
Nove recursos fiscais usavam a credencial errada. O cliente de
api.nfse.ioresolvia achave de dados num host fiscal, que responde
403. Só funcionavam por acidente, viao fallback
dataApiKey → apiKey. Quem configuravadataApiKey— o que a documentaçãorecomenda — tomava
403.companies.getCertificateStatus()lia uma forma que a API nunca devolveu. Retornava{hasCertificate: undefined}para toda empresa, e derrubava em cascata outros três métodos.O mesmo bug está no
client-phpe noclient-ruby, por cópia — há change aberta nos dois.healthCheck()respondiaerrorsempre. EnviavapageCount: 1, que a API recusa com400 "pageCount must be between 1 and 50"— o limite inferior do servidor está um a mais doque a própria mensagem diz. O método existe para dizer se a integração está de pé.
Downloads devolviam objeto tipado como texto. As rotas de entrada respondem
{ publicTemporaryUri }; binário nunca trafegou nelas.O erro da API não chegava ao chamador.
extractErrorMessagelia dois dos quatroenvelopes que a plataforma usa. Nos outros dois o chamador recebia
HTTP 400 error— ostatus que já tinha. Foi assim que
The File field is required.ficou invisível enquanto oupload de certificado não funcionava.
Toda requisição mentia sobre a versão. O User-Agent saía
@nfe-io/sdk@3.0.0— pacoteinexistente, versão três majors atrás. 93.995 requisições em 30 dias, e nenhuma
informação de versão chegando à plataforma.
O que ficou melhor além das correções
publish.ymltinhacontinue-on-error: trueno passo de testes, com justificativa escrita que era falsa. Ostrês bugs corrigidos em 01/09 saíram por esse portão.
.env, então ela pulavasempre. Execução local foi de 742 para 813 testes; quatro assertions apodrecidas
apareceram e foram corrigidas.
validate:specdetecta drift entre cópias da mesma seção — 30 dos 131 endpoints sãodeclarados em mais de uma spec e as cópias divergiram.
package.jsonprecisam concordar antes de publicar.passam a quebrar a suíte. Ambas verificadas por mutação.
Correções de registro
Dois métodos que o diagnóstico anterior dava como quebrados não estavam — a amostra é
que era a exceção:
productInvoiceQuery.downloadPdfdevolve200e%PDF-1.4com chave real. O406medido antes vinha de chave inexistente.
legalPeople/naturalPeople(14 métodos) respondem200. O400vinha de empresa comid de 32 caracteres; a rota aceita só
ObjectIdde 24 hex — limite do servidor.Upstream
Nove issues abertas em
nfe/docsa partir desta rodada —nfe/docs#335,#336,#337,#338,#345,#346,#347,#348,#349— mais um comentário emnfe/docs#343.A
#349cobre as páginas publicadas do SDK Node e está atribuída a@andrenfe; as outrasseguem sem dono.
Verificação
Depois do merge
v6.0.0no GitHub — é o que dispara opublish.yml.package.json, roda testes/lint/typecheck/test:types/build,verifica os artefatos e publica com provenance.
nfeio-docsprecisa de PR próprio — rastreado emnfe/docs#349: a página públicadocs/desenvolvedores/bibliotecas/nodejs/multi-host-routing.mdtem a mesma tabela erradade credencial que este PR corrige aqui, e mais quatro classes de divergência.
.github/PULL_REQUEST_v6.0.0.md).