@useinvio/nfse-sdk 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +115 -56
  2. package/dist/agent.d.ts +86 -0
  3. package/dist/agent.js +235 -0
  4. package/dist/agent.js.map +1 -0
  5. package/dist/certificate.d.ts +96 -0
  6. package/dist/certificate.js +218 -0
  7. package/dist/certificate.js.map +1 -0
  8. package/dist/certificateValidation.d.ts +11 -6
  9. package/dist/certificateValidation.js +23 -12
  10. package/dist/certificateValidation.js.map +1 -1
  11. package/dist/client.d.ts +4 -2
  12. package/dist/client.js +5 -4
  13. package/dist/client.js.map +1 -1
  14. package/dist/dpsJsonSchema.d.ts +17 -0
  15. package/dist/dpsJsonSchema.js +184 -0
  16. package/dist/dpsJsonSchema.js.map +1 -0
  17. package/dist/emissaoNota.d.ts +4 -4
  18. package/dist/emissaoNota.js +4 -3
  19. package/dist/emissaoNota.js.map +1 -1
  20. package/dist/fiscalDomains.d.ts +22 -0
  21. package/dist/fiscalDomains.js +99 -0
  22. package/dist/fiscalDomains.js.map +1 -0
  23. package/dist/fiscalValidation.js +21 -53
  24. package/dist/fiscalValidation.js.map +1 -1
  25. package/dist/index.d.ts +7 -2
  26. package/dist/index.js +4 -2
  27. package/dist/index.js.map +1 -1
  28. package/dist/jsonSchemaCheck.d.ts +12 -0
  29. package/dist/jsonSchemaCheck.js +70 -0
  30. package/dist/jsonSchemaCheck.js.map +1 -0
  31. package/dist/loadPfx.d.ts +2 -0
  32. package/dist/loadPfx.js +2 -1
  33. package/dist/loadPfx.js.map +1 -1
  34. package/dist/sefinClient.d.ts +10 -4
  35. package/dist/sefinClient.js +22 -4
  36. package/dist/sefinClient.js.map +1 -1
  37. package/dist/signXml.d.ts +3 -3
  38. package/dist/signXml.js +8 -6
  39. package/dist/signXml.js.map +1 -1
  40. package/dist/xmlInspection.js +10 -9
  41. package/dist/xmlInspection.js.map +1 -1
  42. package/dist/xsdValidation.d.ts +17 -5
  43. package/dist/xsdValidation.js +75 -67
  44. package/dist/xsdValidation.js.map +1 -1
  45. package/examples/README.md +1 -1
  46. package/examples/client-resource.ts +3 -5
  47. package/examples/emit-homologation.ts +3 -4
  48. package/examples/handle-errors.ts +3 -3
  49. package/llms.txt +15 -5
  50. package/package.json +9 -4
package/README.md CHANGED
@@ -40,11 +40,11 @@ O SDK cuida da camada protocolar: certificado A1, montagem de DPS, validações
40
40
  - [Enviar evento](#enviar-evento-ex-cancelamento)
41
41
  - [Série e número da DPS](#série-e-número-da-dps)
42
42
  - [Ambientes](#ambientes)
43
+ - [Agentes de IA e MCP](#agentes-de-ia-e-mcp)
43
44
  - [Referência da API](#referência-da-api)
44
45
  - [Desenvolvimento](#desenvolvimento)
45
46
  - [Publicação no npm](#publicação-no-npm)
46
- - [Divulgação e exemplos](#divulgação-e-exemplos)
47
- - [Assistente GPT](#assistente-gpt)
47
+ - [Exemplos](#exemplos)
48
48
 
49
49
  ---
50
50
 
@@ -54,7 +54,7 @@ O SDK cuida da camada protocolar: certificado A1, montagem de DPS, validações
54
54
  npm install @useinvio/nfse-sdk
55
55
  ```
56
56
 
57
- > Node.js ≥ 20 é obrigatório.
57
+ > Node.js ≥ 20 é obrigatório. Não precisa de `xmllint` nem de binários nativos: a validação XSD roda em WebAssembly.
58
58
 
59
59
  ---
60
60
 
@@ -63,9 +63,10 @@ npm install @useinvio/nfse-sdk
63
63
  O caminho mais comum: emitir uma nota a partir de JSON declarativo.
64
64
 
65
65
  ```ts
66
- import { emitirNfse, loadPfx, EmitirNotaError } from '@useinvio/nfse-sdk';
66
+ import { emitirNfse, EmitirNotaError, NfseCertificate } from '@useinvio/nfse-sdk';
67
67
 
68
- const pfx = loadPfx('./certificado.pfx', process.env.PFX_PASSWORD!);
68
+ // NFSE_CERT_PATH=./certificado.pfx e NFSE_CERT_PASSWORD=... (veja Certificado A1)
69
+ const certificado = NfseCertificate.fromEnv();
69
70
 
70
71
  const nota = {
71
72
  ambiente: 'restrita' as const,
@@ -103,7 +104,7 @@ const nota = {
103
104
  };
104
105
 
105
106
  try {
106
- const resultado = await emitirNfse(nota, pfx);
107
+ const resultado = await emitirNfse(nota, certificado);
107
108
  console.log(resultado.chaveAcesso); // chave de 50 dígitos
108
109
  console.log(resultado.nfseXml); // XML autorizado pela SEFIN
109
110
  } catch (error) {
@@ -133,8 +134,8 @@ try {
133
134
  | Situação | O que usar |
134
135
  |---|---|
135
136
  | Quer uma API de cliente com `client.invoices.create(...)` | `new NfseClient(...)` |
136
- | Outro sistema já gera o XML da DPS | `emitirNfse(xmlString, pfx)` |
137
- | Você monta os dados e quer que a SDK construa o XML | `emitirNfse(notaJson, pfx)` |
137
+ | Outro sistema já gera o XML da DPS | `emitirNfse(xmlString, certificado)` |
138
+ | Você monta os dados e quer que a SDK construa o XML | `emitirNfse(notaJson, certificado)` |
138
139
  | Precisa inspecionar ou salvar o XML antes de enviar | `buildDpsFromJson(nota)` |
139
140
  | Fluxo customizado (assinar, compactar ou enviar separadamente) | funções de baixo nível |
140
141
 
@@ -150,14 +151,11 @@ O cliente usa os mesmos campos do JSON declarativo (`prestador`, `servico` e
150
151
  cada chamada.
151
152
 
152
153
  ```ts
153
- import { NfseClient } from '@useinvio/nfse-sdk';
154
+ import { NfseCertificate, NfseClient } from '@useinvio/nfse-sdk';
154
155
 
155
156
  const client = new NfseClient({
156
157
  environment: 'sandbox',
157
- certificate: {
158
- content: process.env.NFSE_CERTIFICATE!, // PFX em base64
159
- password: process.env.NFSE_CERTIFICATE_PASSWORD!,
160
- },
158
+ certificate: NfseCertificate.fromEnv(),
161
159
  defaults: {
162
160
  prestador: {
163
161
  cnpj: '12345678000195',
@@ -206,9 +204,8 @@ console.log(invoice.nfseXml);
206
204
  ```
207
205
 
208
206
  `environment: 'sandbox'` é alias de `restrita`, e `production` é alias de
209
- `producao`. O campo `certificate.content` aceita o conteúdo binário do PFX em
210
- `Buffer` ou o PFX codificado em base64, formato comum para variáveis de
211
- ambiente.
207
+ `producao`. `certificate` aceita um `NfseCertificate` ou, por compatibilidade,
208
+ `{ content, password }` com o PFX em `Buffer` ou base64.
212
209
 
213
210
  Para inspecionar a DPS gerada sem assinar nem enviar:
214
211
 
@@ -235,7 +232,7 @@ const dps = client.invoices.buildDpsJson({
235
232
 
236
233
  Divergencias entre `vServ` e a conversao cambial geram avisos, preservando o valor explicito. O mesmo vale para o valor estrangeiro do bloco de comercio exterior. Todos os valores monetarios sao arredondados para duas casas, inclusive `comExt.vServMoeda`; valores de servico que arredondam para zero sao rejeitados. Leia `warnings` antes da emissao quando quiser apresentar esses avisos ao usuario.
237
234
 
238
- `prepararNota` verifica a vigencia do certificado antes de assinar; `transmitirNotaPreparada` verifica novamente antes do envio. `assertCertificateValidity(pfx, now?)` expoe essa verificacao. A checagem de titularidade continua explicita via `assertCertificateForProvider(pfx, documento, now?)`; o SDK nao resolve representacao fiscal.
235
+ `prepararNota` verifica a vigencia do certificado antes de assinar; `transmitirNotaPreparada` verifica novamente antes do envio. `assertCertificateValidity(certificado, now?)` expoe essa verificacao. A checagem de titularidade continua explicita via `assertCertificateForProvider(certificado, documento, now?)`; o SDK nao resolve representacao fiscal.
239
236
 
240
237
  O ambiente para JSON segue `options.ambiente` → `request.ambiente` → `restrita`, tanto no XML quanto no destino. XML externo usa `tpAmb` e rejeita uma opcao divergente. Notas preparadas tambem precisam ter o mesmo ambiente no XML assinado e no envio.
241
238
 
@@ -249,23 +246,66 @@ Veja [as correcoes e mudancas de comportamento](docs/CORRECOES_EMISSAO.md) e os
249
246
 
250
247
  ## Certificado A1
251
248
 
252
- ### A partir de arquivo
249
+ Use `NfseCertificate`. Ele lê o `.pfx`/`.p12` uma vez, mantém a chave privada fora do objeto e descarta a senha e os bytes do arquivo. `console.log(certificado)` e `JSON.stringify(certificado)` mostram só CNPJ/CPF, validade e fingerprint.
250
+
251
+ ### Variáveis de ambiente (recomendado)
252
+
253
+ ```bash
254
+ NFSE_CERT_PATH=./certificado.pfx # ou NFSE_CERT_BASE64=<PFX em base64>
255
+ NFSE_CERT_PASSWORD=sua-senha # ou NFSE_CERT_PASSWORD_FILE=/run/secrets/nfse_cert_password
256
+ ```
253
257
 
254
258
  ```ts
255
- import { loadPfx } from '@useinvio/nfse-sdk';
259
+ import { NfseCertificate } from '@useinvio/nfse-sdk';
256
260
 
257
- const pfx = loadPfx('./certificado.pfx', process.env.PFX_PASSWORD!);
261
+ const certificado = NfseCertificate.fromEnv();
262
+ console.log(certificado.cnpj, certificado.notAfter, certificado.daysUntilExpiry());
258
263
  ```
259
264
 
260
- ### A partir de buffer (ex.: certificado descriptografado do banco)
265
+ `NFSE_CERT_PASSWORD_FILE` lê a senha de um arquivo, o formato de secrets do Docker e do Kubernetes. Nunca versione o `.pfx` nem a senha.
266
+
267
+ ### Outras origens
261
268
 
262
269
  ```ts
263
- import { loadPfxFromBuffer } from '@useinvio/nfse-sdk';
270
+ NfseCertificate.fromFile('./certificado.pfx', senha);
271
+ NfseCertificate.fromBase64(pfxBase64, senha); // aceita quebras de linha e data: URLs
272
+ NfseCertificate.fromPfx(pfxBuffer, senha); // ex.: PFX vindo do seu cofre de segredos
273
+ NfseCertificate.fromPem({ key, cert, passphrase }); // par extraído com openssl
274
+ ```
275
+
276
+ ### Vários emitentes (multi-tenant)
277
+
278
+ Crie um `NfseCertificate` por emitente e reaproveite-o. Cada certificado tem o próprio agente mTLS, então conexões nunca são compartilhadas entre emitentes. Guarde o PFX cifrado (KMS, Vault, Secrets Manager) e a senha em um segredo separado, e descriptografe só ao criar o objeto.
264
279
 
265
- const pfx = loadPfxFromBuffer(pfxBuffer, password);
280
+ ```ts
281
+ import { assertCertificateForProvider, NfseCertificate } from '@useinvio/nfse-sdk';
282
+
283
+ const cache = new Map<string, NfseCertificate>();
284
+
285
+ async function certificadoDoEmitente(cnpj: string): Promise<NfseCertificate> {
286
+ const cached = cache.get(cnpj);
287
+ if (cached?.isValidAt()) return cached;
288
+ const { pfx, senha } = await cofre.buscarCertificado(cnpj); // seu secret manager
289
+ const certificado = NfseCertificate.fromPfx(pfx, senha);
290
+ assertCertificateForProvider(certificado, cnpj);
291
+ cache.set(cnpj, certificado);
292
+ return certificado;
293
+ }
266
294
  ```
267
295
 
268
- O objeto `pfx` retornado contém chave privada e certificado em PEM, pronto para assinar e fazer mTLS.
296
+ ### Erros de certificado
297
+
298
+ Todos lançam `CertificateValidationError` com um `code` estável:
299
+
300
+ | `code` | Quando |
301
+ |---|---|
302
+ | `CERTIFICATE_INVALID_PASSWORD` | Senha incorreta. |
303
+ | `CERTIFICATE_UNREADABLE` | O conteúdo não é um PFX/PEM válido. |
304
+ | `CERTIFICATE_MISSING`, `CERTIFICATE_PASSWORD_MISSING` | Variáveis de ambiente ausentes em `fromEnv`. |
305
+ | `CERTIFICATE_NOT_YET_VALID`, `CERTIFICATE_EXPIRED` | Fora da vigência. Verificado antes de assinar e antes de enviar. |
306
+ | `CERTIFICATE_HOLDER_MISMATCH` | `assertCertificateForProvider` com CPF/CNPJ diferente do titular. |
307
+
308
+ > `loadPfx` e `loadPfxFromBuffer` continuam funcionando, mas retornam a chave privada em PEM dentro de um objeto comum. Prefira `NfseCertificate`.
269
309
 
270
310
  ---
271
311
 
@@ -274,11 +314,11 @@ O objeto `pfx` retornado contém chave privada e certificado em PEM, pronto para
274
314
  Use quando outro sistema (ERP, sistema legado) já gera o XML da DPS no layout nacional.
275
315
 
276
316
  ```ts
277
- import { emitirNfse, loadPfx } from '@useinvio/nfse-sdk';
317
+ import { emitirNfse, NfseCertificate } from '@useinvio/nfse-sdk';
278
318
 
279
- const pfx = loadPfx('./certificado.pfx', process.env.PFX_PASSWORD!);
319
+ const certificado = NfseCertificate.fromEnv();
280
320
 
281
- const resultado = await emitirNfse(dpsXml, pfx, { ambiente: 'restrita' });
321
+ const resultado = await emitirNfse(dpsXml, certificado, { ambiente: 'restrita' });
282
322
 
283
323
  console.log(resultado.chaveAcesso);
284
324
  console.log(resultado.nfseXml);
@@ -296,7 +336,7 @@ Para a referência completa de campos aceitos no JSON, consulte [JSON_MAPPING.md
296
336
  O builder valida formatos e combinações mínimas antes de gerar XML, mas não escolhe códigos fiscais por você. Campos como `tributacaoMunicipal` e `totTrib` precisam ser informados explicita[...]
297
337
 
298
338
  ```ts
299
- import { emitirNfse, loadPfx, type DpsJsonRequest } from '@useinvio/nfse-sdk';
339
+ import { emitirNfse, NfseCertificate, type DpsJsonRequest } from '@useinvio/nfse-sdk';
300
340
 
301
341
  const nota: DpsJsonRequest = {
302
342
  ambiente: 'restrita',
@@ -333,8 +373,8 @@ const nota: DpsJsonRequest = {
333
373
  },
334
374
  };
335
375
 
336
- const pfx = loadPfx('./certificado.pfx', process.env.PFX_PASSWORD!);
337
- const resultado = await emitirNfse(nota, pfx);
376
+ const certificado = NfseCertificate.fromEnv();
377
+ const resultado = await emitirNfse(nota, certificado);
338
378
  ```
339
379
 
340
380
  > `pTotTribFed`, `pTotTribEst` e `pTotTribMun` são percentuais com duas casas decimais (ex.: `"5.00"`), diferentes das alíquotas com quatro casas (`pAliq`).
@@ -365,7 +405,7 @@ console.log(xml); // XML não assinado da DPS
365
405
  import { EmitirNotaError, emitirNfse } from '@useinvio/nfse-sdk';
366
406
 
367
407
  try {
368
- await emitirNfse(nota, pfx);
408
+ await emitirNfse(nota, certificado);
369
409
  } catch (error) {
370
410
  if (!(error instanceof EmitirNotaError)) throw error;
371
411
 
@@ -387,7 +427,7 @@ try {
387
427
  ```ts
388
428
  import { consultarNfse } from '@useinvio/nfse-sdk';
389
429
 
390
- const resposta = await consultarNfse(chaveAcesso, pfx, 'restrita');
430
+ const resposta = await consultarNfse(chaveAcesso, certificado, 'restrita');
391
431
 
392
432
  if (resposta.status === 200) {
393
433
  console.log(resposta.body);
@@ -403,10 +443,10 @@ if (resposta.status === 200) {
403
443
  ```ts
404
444
  import { enviarEvento, gzipBase64, signEnveloped } from '@useinvio/nfse-sdk';
405
445
 
406
- const signedEventXml = signEnveloped(pedRegXml, pedRegId, 'infPedReg', pfx);
446
+ const signedEventXml = signEnveloped(pedRegXml, pedRegId, 'infPedReg', certificado);
407
447
  const pedRegXmlGZipB64 = gzipBase64(signedEventXml);
408
448
 
409
- const resposta = await enviarEvento(pedRegXmlGZipB64, pfx, chaveAcesso, 'restrita');
449
+ const resposta = await enviarEvento(pedRegXmlGZipB64, certificado, chaveAcesso, 'restrita');
410
450
  ```
411
451
 
412
452
  ---
@@ -463,6 +503,34 @@ Leia o guia completo em [docs/METRICS.md](./docs/METRICS.md).
463
503
 
464
504
  ---
465
505
 
506
+ ## Agentes de IA e MCP
507
+
508
+ O subpath `@useinvio/nfse-sdk/agent` expõe ferramentas neutras de framework para servidores MCP e agentes (OpenAI, LangChain, Vercel AI SDK). Cada ferramenta traz `name`, `description`, `inputSchema`/`outputSchema` em JSON Schema, `annotations` no formato MCP e um handler `run`.
509
+
510
+ | Ferramenta | Efeito | Certificado |
511
+ | --- | --- | --- |
512
+ | `validate_dps` | Valida a requisição; nenhum efeito colateral. | Não |
513
+ | `build_dps_xml` | Gera o XML não assinado e confere contra o XSD v1.01. | Não |
514
+ | `get_nfse` | Consulta uma NFS-e pela chave de acesso. | Sim |
515
+
516
+ ```ts
517
+ import { nfseAgentTools } from '@useinvio/nfse-sdk/agent';
518
+
519
+ const context = { pfx: certificado, ambiente: 'restrita' as const }; // configuração do host
520
+ for (const tool of nfseAgentTools) {
521
+ // registre tool.name, tool.description, tool.inputSchema e tool.annotations no seu framework
522
+ // e chame tool.run(argumentos, context) quando o modelo usar a ferramenta
523
+ }
524
+ ```
525
+
526
+ Certificado e ambiente ficam no `context` do host, nunca na entrada da ferramenta: o modelo não escolhe `producao` nem vê material de chave. Uma requisição com `ambiente` diferente do configurado gera `AMBIENTE_NOT_ALLOWED`. Falhas esperadas retornam `issues` com `code`, `path`, `severity`, `source` e `suggestion`; falhas de infraestrutura (rede, motor XSD) rejeitam a promise. `toAgentIssues(error)` converte erros do SDK (`DpsFiscalValidationError`, `DpsXsdValidationError`, `CertificateValidationError`, `EmitirNotaError`) para esse mesmo formato.
527
+
528
+ `DPS_JSON_REQUEST_SCHEMA`, exportado também pela raiz do pacote, descreve o `DpsJsonRequest` canônico em JSON Schema 2020-12 com um subconjunto portável de palavras-chave. Ele cobre formatos e domínios de códigos, mas não as regras entre campos (regime × `totTrib`, nacional × exportação); `validateDpsJsonRequest` continua sendo a autoridade. Atalhos legados (`emissao.vServ`, `comExt`) e campos rejeitados pela validação ficam fora do schema; `validate_dps` aponta campos desconhecidos como `UNSUPPORTED_FIELD` em vez de ignorá-los.
529
+
530
+ A emissão ainda não é exposta como ferramenta: ela exigirá confirmação explícita antes do envio.
531
+
532
+ ---
533
+
466
534
  ## Referência da API
467
535
 
468
536
  ### Funções principais
@@ -470,9 +538,9 @@ Leia o guia completo em [docs/METRICS.md](./docs/METRICS.md).
470
538
  | Função | Descrição |
471
539
  |---|---|
472
540
  | `new NfseClient(options)` | Cria cliente com ambiente, certificado e defaults no mesmo formato do JSON declarativo. |
473
- | `emitirNfse(input, pfx, options?)` | Emite uma NFS-e a partir de XML ou JSON. Retorna `ResultadoEmissaoNota`. |
474
- | `consultarNfse(chaveAcesso, pfx, ambiente?)` | Consulta uma NFS-e pela chave de acesso. |
475
- | `enviarEvento(xmlGzipB64, pfx, chaveAcesso, ambiente?)` | Envia evento fiscal (cancelamento, etc.). |
541
+ | `emitirNfse(input, certificado, options?)` | Emite uma NFS-e a partir de XML ou JSON. Retorna `ResultadoEmissaoNota`. |
542
+ | `consultarNfse(chaveAcesso, certificado, ambiente?)` | Consulta uma NFS-e pela chave de acesso. |
543
+ | `enviarEvento(xmlGzipB64, certificado, chaveAcesso, ambiente?)` | Envia evento fiscal (cancelamento, etc.). |
476
544
  | `buildDpsFromJson(nota)` | Monta o XML da DPS sem assinar nem enviar. |
477
545
  | `setSefinRequestObserver(observer)` | Registra um hook para observar latência/status das chamadas à SEFIN. |
478
546
  | `createSefinLatencyTracker(options?)` | Agrega amostras recentes e pode calcular percentis quando `includePercentiles` estiver habilitado. |
@@ -481,17 +549,18 @@ Leia o guia completo em [docs/METRICS.md](./docs/METRICS.md).
481
549
 
482
550
  | Função | Descrição |
483
551
  |---|---|
484
- | `assertCertificateValidity(pfx, now?)` | Verifica vigencia do certificado; aplicada na preparacao e no envio. |
485
- | `assertCertificateForProvider(pfx, documento, now?)` | Verifica explicitamente vigencia e correspondencia do titular. |
486
- | `loadPfx(path, password)` | Carrega certificado A1 a partir de arquivo. |
487
- | `loadPfxFromBuffer(buffer, password)` | Carrega certificado A1 a partir de buffer. |
552
+ | `assertCertificateValidity(certificado, now?)` | Verifica vigencia do certificado; aplicada na preparacao e no envio. |
553
+ | `assertCertificateForProvider(certificado, documento, now?)` | Verifica explicitamente vigencia e correspondencia do titular. |
554
+ | `NfseCertificate.fromEnv()` / `fromFile()` / `fromBase64()` / `fromPfx()` / `fromPem()` | Carrega o certificado A1 sem expor a chave privada. |
555
+ | `loadPfx(path, password)` | Legado: retorna a chave em PEM. Prefira `NfseCertificate`. |
556
+ | `loadPfxFromBuffer(buffer, password)` | Legado: retorna a chave em PEM. Prefira `NfseCertificate`. |
488
557
 
489
558
  ### Funções de baixo nível
490
559
 
491
560
  | Função | Descrição |
492
561
  |---|---|
493
- | `signDps(xml, dpsId, pfx)` | Assina XML de DPS com XMLDSIG. |
494
- | `signEnveloped(xml, id, ref, pfx)` | Assina XML genérico (eventos). |
562
+ | `signDps(xml, dpsId, certificado)` | Assina XML de DPS com XMLDSIG. |
563
+ | `signEnveloped(xml, id, ref, certificado)` | Assina XML genérico (eventos). |
495
564
  | `verifyDps(xml, certPem)` | Verifica assinatura de uma DPS. |
496
565
  | `gzipBase64(xml)` | Compacta XML em GZip e codifica em Base64. |
497
566
  | `gunzipBase64(b64)` | Descompacta resposta GZip/Base64. |
@@ -537,7 +606,7 @@ npm run build # compila para dist/
537
606
  npm test # build + tipos + testes unitários
538
607
  ```
539
608
 
540
- `npm test` também valida uma DPS gerada contra os XSD oficiais em `schemas/nfse/v1.01/Schemas/1.01`. Para compatibilidade com `xmllint`, o teste corrige em uma cópia temporária a âncora regex[...]
609
+ `npm test` também valida as DPS geradas contra os XSD oficiais em `schemas/nfse/v1.01/Schemas/1.01` com o motor embutido (libxml2 em WebAssembly). Se o `xmllint` estiver instalado, como no CI, os testes também o usam como verificação cruzada.
541
610
 
542
611
  ### Uso local em outro projeto
543
612
 
@@ -567,19 +636,9 @@ O pacote usa `publishConfig.access = public`, necessário para publicar um pacot
567
636
 
568
637
  ---
569
638
 
570
- ## Divulgação e exemplos
571
-
572
- - Exemplos copiáveis ficam em [`examples/`](./examples).
573
- - Material de lançamento, mensagens para redes e prompt de landing page ficam em [`docs/launch.md`](./docs/launch.md).
574
-
575
- ---
576
-
577
- ## Assistente GPT
578
-
579
- Para criar um GPT que ajude outros devs a implementar e documentar esta SDK, use:
639
+ ## Exemplos
580
640
 
581
- - [`docs/gpt-assistant.md`](./docs/gpt-assistant.md): configuracao completa do GPT, instrucoes copiaveis, starters e testes de preview.
582
- - [`docs/gpt-knowledge.md`](./docs/gpt-knowledge.md): resumo compacto para subir como arquivo de Knowledge.
641
+ Exemplos copiáveis ficam em [`examples/`](./examples).
583
642
 
584
643
  ---
585
644
 
@@ -0,0 +1,86 @@
1
+ import { type JsonSchema } from './dpsJsonSchema.js';
2
+ import type { Ambiente } from './config.js';
3
+ import type { DpsJsonRequest } from './dpsJson.js';
4
+ import type { CertificateInput } from './certificate.js';
5
+ export { DPS_JSON_REQUEST_SCHEMA } from './dpsJsonSchema.js';
6
+ export type { JsonSchema } from './dpsJsonSchema.js';
7
+ export type NfseAgentIssueSource = 'sdk' | 'invio' | 'schema' | 'xsd' | 'sefin' | 'certificate' | 'environment';
8
+ /** One shape for validation, XSD, certificate and SEFIN problems. */
9
+ export interface NfseAgentIssue {
10
+ code: string;
11
+ /** Dotted path inside the request (`emissao.` omitted, as in `DpsValidationIssue`); `$` when global. */
12
+ path: string;
13
+ severity: 'error' | 'warning';
14
+ source: NfseAgentIssueSource;
15
+ message: string;
16
+ suggestion?: string;
17
+ }
18
+ export interface NfseAgentContext {
19
+ /** Host-owned A1 certificate (`NfseCertificate` recommended). Required only by tools that talk to SEFIN. */
20
+ pfx?: CertificateInput;
21
+ /** Host-owned environment. Defaults to `restrita`; tool input cannot override it. */
22
+ ambiente?: Ambiente;
23
+ }
24
+ /** Same hints as MCP `ToolAnnotations`. */
25
+ export interface NfseAgentToolAnnotations {
26
+ readOnlyHint: boolean;
27
+ destructiveHint: boolean;
28
+ idempotentHint: boolean;
29
+ openWorldHint: boolean;
30
+ }
31
+ export interface NfseAgentTool<Input = any, Output = any> {
32
+ name: string;
33
+ title: string;
34
+ description: string;
35
+ inputSchema: JsonSchema;
36
+ outputSchema: JsonSchema;
37
+ annotations: NfseAgentToolAnnotations;
38
+ requiresCertificate: boolean;
39
+ /**
40
+ * Expected failures (invalid input, rejection, missing certificate) resolve
41
+ * with `issues`. Infrastructure failures (XSD engine, network) reject.
42
+ */
43
+ run(input: Input, context?: NfseAgentContext): Promise<Output>;
44
+ }
45
+ export interface DpsRequestToolInput {
46
+ request: Omit<DpsJsonRequest, 'ambiente'>;
47
+ }
48
+ export interface ValidateDpsToolOutput {
49
+ valid: boolean;
50
+ schemaVersion: string;
51
+ ambiente: Ambiente;
52
+ issues: NfseAgentIssue[];
53
+ warnings: NfseAgentIssue[];
54
+ }
55
+ export interface BuildDpsXmlToolOutput {
56
+ ok: boolean;
57
+ ambiente: Ambiente;
58
+ dpsId?: string;
59
+ xml?: string;
60
+ issues: NfseAgentIssue[];
61
+ warnings: NfseAgentIssue[];
62
+ }
63
+ export interface GetNfseToolInput {
64
+ chaveAcesso: string;
65
+ }
66
+ export interface GetNfseToolOutput {
67
+ ok: boolean;
68
+ ambiente: Ambiente;
69
+ chaveAcesso: string;
70
+ status?: number;
71
+ nfseXml?: string;
72
+ body?: unknown;
73
+ issues: NfseAgentIssue[];
74
+ }
75
+ /**
76
+ * Converts known SDK errors to `NfseAgentIssue[]`. Returns `undefined` for
77
+ * anything else (transport, XSD engine, programming errors) so the caller can rethrow.
78
+ */
79
+ export declare function toAgentIssues(error: unknown): NfseAgentIssue[] | undefined;
80
+ /** `DPS_JSON_REQUEST_SCHEMA` without `ambiente`, which belongs to the host. */
81
+ export declare const DPS_AGENT_REQUEST_SCHEMA: JsonSchema;
82
+ export declare const validateDpsTool: NfseAgentTool<DpsRequestToolInput, ValidateDpsToolOutput>;
83
+ export declare const buildDpsXmlTool: NfseAgentTool<DpsRequestToolInput, BuildDpsXmlToolOutput>;
84
+ export declare const getNfseTool: NfseAgentTool<GetNfseToolInput, GetNfseToolOutput>;
85
+ /** All tools, in the order an agent typically needs them. */
86
+ export declare const nfseAgentTools: readonly NfseAgentTool[];
package/dist/agent.js ADDED
@@ -0,0 +1,235 @@
1
+ /**
2
+ * Framework-neutral tool definitions for MCP servers and LLM agents.
3
+ *
4
+ * Each tool carries a JSON Schema for input and output, MCP-style annotations
5
+ * and a `run` handler. Adapters (MCP, OpenAI, LangChain, Vercel AI SDK) only
6
+ * need to map these fields. Certificate and environment are host
7
+ * configuration passed in `NfseAgentContext`, never tool input, so a model
8
+ * cannot select production or see key material.
9
+ */
10
+ import { CertificateValidationError } from './certificateValidation.js';
11
+ import { DEFAULT_AMBIENTE } from './config.js';
12
+ import { buildDpsFromJson } from './dpsJson.js';
13
+ import { DPS_JSON_REQUEST_SCHEMA } from './dpsJsonSchema.js';
14
+ import { EmitirNotaError } from './emissaoNota.js';
15
+ import { DpsFiscalValidationError, validateDpsJsonRequest } from './fiscalValidation.js';
16
+ import { gunzipBase64 } from './gzipB64.js';
17
+ import { checkJsonSchema } from './jsonSchemaCheck.js';
18
+ import { consultarNfse, extrairErros } from './sefinClient.js';
19
+ import { DpsXsdValidationError, validateDpsXmlAgainstXsd } from './xsdValidation.js';
20
+ export { DPS_JSON_REQUEST_SCHEMA } from './dpsJsonSchema.js';
21
+ function fromValidationIssue(issue) {
22
+ return { ...issue };
23
+ }
24
+ function sefinIssue(erro) {
25
+ const authorization = erro.Codigo === 'INVALID_AUTHORIZATION_RESPONSE';
26
+ return {
27
+ code: erro.Codigo,
28
+ path: '$',
29
+ severity: 'error',
30
+ source: authorization ? 'sdk' : 'sefin',
31
+ message: erro.Complemento ? `${erro.Descricao} (${erro.Complemento})` : erro.Descricao,
32
+ ...(authorization ? { suggestion: 'Consulte a nota pela chave ou concilie a DPS antes de reenviar.' } : {}),
33
+ };
34
+ }
35
+ /**
36
+ * Converts known SDK errors to `NfseAgentIssue[]`. Returns `undefined` for
37
+ * anything else (transport, XSD engine, programming errors) so the caller can rethrow.
38
+ */
39
+ export function toAgentIssues(error) {
40
+ if (error instanceof DpsFiscalValidationError)
41
+ return error.issues.map(fromValidationIssue);
42
+ if (error instanceof DpsXsdValidationError) {
43
+ return error.details.map((message) => ({ code: 'XSD_INVALID', path: '$', severity: 'error', source: 'xsd', message }));
44
+ }
45
+ if (error instanceof CertificateValidationError) {
46
+ return [{ code: error.code, path: '$', severity: 'error', source: 'certificate', message: error.message }];
47
+ }
48
+ if (error instanceof EmitirNotaError) {
49
+ const issues = error.erros.map(sefinIssue);
50
+ return issues.length ? issues : [{ code: 'SEFIN_REJECTED', path: '$', severity: 'error', source: 'sefin', message: error.message }];
51
+ }
52
+ return undefined;
53
+ }
54
+ function resolveAmbiente(context) {
55
+ return context?.ambiente ?? DEFAULT_AMBIENTE;
56
+ }
57
+ function stripEmissao(path) {
58
+ return path.replace(/^emissao\./, '');
59
+ }
60
+ /** Fields the schema does not accept are reported instead of silently ignored by the builder. */
61
+ function unsupportedFieldIssues(request) {
62
+ return checkJsonSchema(DPS_JSON_REQUEST_SCHEMA, request)
63
+ .filter(({ keyword }) => keyword === 'additionalProperties')
64
+ .map(({ path }) => ({
65
+ code: 'UNSUPPORTED_FIELD',
66
+ path: stripEmissao(path),
67
+ severity: 'error',
68
+ source: 'schema',
69
+ message: `${path} nao e um campo suportado pelo schema da DPS`,
70
+ suggestion: 'Remova o campo ou use o nome canonico descrito no schema.',
71
+ }));
72
+ }
73
+ function environmentIssue(request, ambiente) {
74
+ const requested = request?.ambiente;
75
+ if (requested === undefined || requested === ambiente)
76
+ return [];
77
+ return [{
78
+ code: 'AMBIENTE_NOT_ALLOWED',
79
+ path: 'ambiente',
80
+ severity: 'error',
81
+ source: 'environment',
82
+ message: `O ambiente e definido pelo host (${ambiente}); a requisicao pediu ${String(requested)}.`,
83
+ suggestion: 'Remova o campo ambiente da requisicao.',
84
+ }];
85
+ }
86
+ function validateForAgent(input, context) {
87
+ const ambiente = resolveAmbiente(context);
88
+ const request = input?.request;
89
+ const hostIssues = environmentIssue(request, ambiente);
90
+ const withAmbiente = request && typeof request === 'object' && !Array.isArray(request) ? { ...request, ambiente } : request;
91
+ const report = validateDpsJsonRequest(withAmbiente);
92
+ const structural = report.issues.some((issue) => issue.code === 'INVALID_STRUCTURE');
93
+ const issues = [
94
+ ...hostIssues,
95
+ ...(structural ? [] : unsupportedFieldIssues(withAmbiente)),
96
+ ...report.issues.map(fromValidationIssue),
97
+ ];
98
+ return { ambiente, report, issues, warnings: report.warnings.map(fromValidationIssue), request: withAmbiente };
99
+ }
100
+ const { $schema: _draft, ...requestSchema } = DPS_JSON_REQUEST_SCHEMA;
101
+ const { ambiente: _ambiente, ...requestProperties } = requestSchema.properties;
102
+ /** `DPS_JSON_REQUEST_SCHEMA` without `ambiente`, which belongs to the host. */
103
+ export const DPS_AGENT_REQUEST_SCHEMA = { ...requestSchema, properties: requestProperties };
104
+ const DPS_REQUEST_INPUT_SCHEMA = {
105
+ type: 'object',
106
+ properties: { request: DPS_AGENT_REQUEST_SCHEMA },
107
+ required: ['request'],
108
+ additionalProperties: false,
109
+ };
110
+ const ISSUE_SCHEMA = {
111
+ type: 'object',
112
+ properties: {
113
+ code: { type: 'string', description: 'Codigo estavel do problema, ex.: TOT_TRIB_REQUIRED ou codigo SEFIN.' },
114
+ path: { type: 'string', description: 'Campo afetado; $ quando global.' },
115
+ severity: { type: 'string', enum: ['error', 'warning'] },
116
+ source: { type: 'string', enum: ['sdk', 'invio', 'schema', 'xsd', 'sefin', 'certificate', 'environment'] },
117
+ message: { type: 'string' },
118
+ suggestion: { type: 'string' },
119
+ },
120
+ required: ['code', 'path', 'severity', 'source', 'message'],
121
+ additionalProperties: false,
122
+ };
123
+ const ISSUES = { type: 'array', items: ISSUE_SCHEMA };
124
+ const AMBIENTE = { type: 'string', enum: ['restrita', 'producao'] };
125
+ export const validateDpsTool = {
126
+ name: 'validate_dps',
127
+ title: 'Validar DPS da NFS-e',
128
+ description: 'Valida uma requisicao de DPS (NFS-e Nacional) sem certificado, sem rede e sem efeitos colaterais. ' +
129
+ 'Retorna issues com code, path, message e suggestion; corrija os campos indicados e valide novamente ate valid=true. ' +
130
+ 'Nao invente codigos fiscais (cTribNac, regime, tributacao): pergunte ao usuario quando faltarem.',
131
+ inputSchema: DPS_REQUEST_INPUT_SCHEMA,
132
+ outputSchema: {
133
+ type: 'object',
134
+ properties: { valid: { type: 'boolean' }, schemaVersion: { type: 'string' }, ambiente: AMBIENTE, issues: ISSUES, warnings: ISSUES },
135
+ required: ['valid', 'schemaVersion', 'ambiente', 'issues', 'warnings'],
136
+ additionalProperties: false,
137
+ },
138
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
139
+ requiresCertificate: false,
140
+ async run(input, context) {
141
+ const { ambiente, report, issues, warnings } = validateForAgent(input, context);
142
+ return { valid: issues.length === 0, schemaVersion: report.schemaVersion, ambiente, issues, warnings };
143
+ },
144
+ };
145
+ export const buildDpsXmlTool = {
146
+ name: 'build_dps_xml',
147
+ title: 'Gerar XML da DPS',
148
+ description: 'Valida a requisicao e gera o XML nao assinado da DPS, conferido contra o XSD oficial v1.01. ' +
149
+ 'Nao assina nem transmite. Use para revisar a nota antes de pedir a emissao ao usuario.',
150
+ inputSchema: DPS_REQUEST_INPUT_SCHEMA,
151
+ outputSchema: {
152
+ type: 'object',
153
+ properties: {
154
+ ok: { type: 'boolean' },
155
+ ambiente: AMBIENTE,
156
+ dpsId: { type: 'string', description: 'Id de 45 caracteres da DPS.' },
157
+ xml: { type: 'string', description: 'XML da DPS nao assinado.' },
158
+ issues: ISSUES,
159
+ warnings: ISSUES,
160
+ },
161
+ required: ['ok', 'ambiente', 'issues', 'warnings'],
162
+ additionalProperties: false,
163
+ },
164
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
165
+ requiresCertificate: false,
166
+ async run(input, context) {
167
+ const { ambiente, issues, warnings, request } = validateForAgent(input, context);
168
+ if (issues.length)
169
+ return { ok: false, ambiente, issues, warnings };
170
+ try {
171
+ const built = buildDpsFromJson(request);
172
+ validateDpsXmlAgainstXsd(built.xml);
173
+ return { ok: true, ambiente, dpsId: built.id, xml: built.xml, issues: [], warnings };
174
+ }
175
+ catch (error) {
176
+ const mapped = toAgentIssues(error);
177
+ if (!mapped)
178
+ throw error;
179
+ return { ok: false, ambiente, issues: mapped, warnings };
180
+ }
181
+ },
182
+ };
183
+ export const getNfseTool = {
184
+ name: 'get_nfse',
185
+ title: 'Consultar NFS-e',
186
+ description: 'Consulta uma NFS-e na SEFIN pela chave de acesso de 50 digitos, no ambiente configurado pelo host. ' +
187
+ 'Use tambem para conciliar uma emissao com resposta ambigua antes de qualquer reenvio.',
188
+ inputSchema: {
189
+ type: 'object',
190
+ properties: { chaveAcesso: { type: 'string', pattern: '^\\d{50}$', description: 'Chave de acesso da NFS-e, 50 digitos.' } },
191
+ required: ['chaveAcesso'],
192
+ additionalProperties: false,
193
+ },
194
+ outputSchema: {
195
+ type: 'object',
196
+ properties: {
197
+ ok: { type: 'boolean' },
198
+ ambiente: AMBIENTE,
199
+ chaveAcesso: { type: 'string' },
200
+ status: { type: 'number', description: 'Status HTTP da SEFIN.' },
201
+ nfseXml: { type: 'string', description: 'XML da NFS-e quando retornado.' },
202
+ body: { description: 'Corpo da resposta da SEFIN, sem o XML compactado.' },
203
+ issues: ISSUES,
204
+ },
205
+ required: ['ok', 'ambiente', 'chaveAcesso', 'issues'],
206
+ additionalProperties: false,
207
+ },
208
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
209
+ requiresCertificate: true,
210
+ async run(input, context) {
211
+ const ambiente = resolveAmbiente(context);
212
+ const chaveAcesso = String(input?.chaveAcesso ?? '');
213
+ const fail = (issue) => ({ ok: false, ambiente, chaveAcesso, issues: [issue] });
214
+ if (!/^\d{50}$/.test(chaveAcesso)) {
215
+ return fail({ code: 'CHAVE_ACESSO_INVALID_FORMAT', path: 'chaveAcesso', severity: 'error', source: 'sdk', message: 'chaveAcesso deve ter 50 digitos numericos.' });
216
+ }
217
+ if (!context?.pfx) {
218
+ return fail({ code: 'CERTIFICATE_REQUIRED', path: '$', severity: 'error', source: 'certificate', message: 'O host nao configurou um certificado A1 para consultar a SEFIN.' });
219
+ }
220
+ const { status, body } = await consultarNfse(chaveAcesso, context.pfx, ambiente);
221
+ const { nfseXmlGZipB64, NfseXmlGZipB64, ...rest } = (body && typeof body === 'object' && !Array.isArray(body) ? body : { value: body });
222
+ const compressed = nfseXmlGZipB64 ?? NfseXmlGZipB64;
223
+ if (status < 200 || status >= 300) {
224
+ const issues = extrairErros(body).map(sefinIssue);
225
+ return { ok: false, ambiente, chaveAcesso, status, body: rest, issues: issues.length ? issues : [
226
+ { code: `HTTP_${status}`, path: '$', severity: 'error', source: 'sefin', message: `A SEFIN respondeu HTTP ${status}.` },
227
+ ] };
228
+ }
229
+ const nfseXml = typeof compressed === 'string' ? gunzipBase64(compressed) : undefined;
230
+ return { ok: true, ambiente, chaveAcesso, status, ...(nfseXml ? { nfseXml } : {}), body: rest, issues: [] };
231
+ },
232
+ };
233
+ /** All tools, in the order an agent typically needs them. */
234
+ export const nfseAgentTools = [validateDpsTool, buildDpsXmlTool, getNfseTool];
235
+ //# sourceMappingURL=agent.js.map