node-sped-nfe 1.1.0 → 1.1.2

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.
@@ -1,436 +0,0 @@
1
- import { XMLParser, XMLBuilder } from "fast-xml-parser";
2
- import https from "https";
3
- import { spawnSync, SpawnSyncReturns } from "child_process"
4
- import tmp from "tmp"
5
- import crypto from "crypto";
6
- import { urlEventos } from "./eventos.js"
7
- import fs from "fs"
8
- import path from 'path';
9
- import { fileURLToPath } from 'url';
10
- import pem from 'pem';
11
-
12
-
13
-
14
- const __filename = fileURLToPath(import.meta.url);
15
- const __dirname = path.dirname(__filename);
16
-
17
- class Tools {
18
- #cert: {
19
- pfx: string;
20
- senha: string;
21
- };
22
- #xmlTools: {
23
- XMLBuilder: XMLBuilder;
24
- XMLParser: XMLParser;
25
- } = {
26
- XMLBuilder: {} as XMLBuilder,
27
- XMLParser: {} as XMLParser
28
- };
29
- #pem: {
30
- key: string;
31
- cert: string;
32
- ca: string[]; // <- define que pode ser uma lista de strings
33
- } = {
34
- key: "", // A chave privada extraída do PKCS#12, em formato PEM
35
- cert: "", // O certificado extraído, em formato PEM
36
- ca: [] // Uma lista de certificados da cadeia (se houver), ou null
37
- };
38
- #config: {
39
- mod: string;
40
- xmllint: string;
41
- UF: string;
42
- tpAmb: number;
43
- CSC: string;
44
- CSCid: string;
45
- versao: string;
46
- timeout: number;
47
- };
48
-
49
- constructor(config = { mod: "", xmllint: 'xmllint', UF: '', tpAmb: 2, CSC: "", CSCid: "", versao: "4.00", timeout: 30 }, certificado = { pfx: "", senha: "" }) {
50
- if (typeof config != "object") throw "Tools({config},{}): Config deve ser um objecto!";
51
- if (typeof config.UF == "undefined") throw "Tools({...,UF:?},{}): UF não definida!";
52
- if (typeof config.tpAmb == "undefined") throw "Tools({...,tpAmb:?},{}): tpAmb não definida!";
53
- if (typeof config.versao == "undefined") throw "Tools({...,versao:?},{}): versao não definida!";
54
-
55
- if (typeof config.timeout == "undefined") config.timeout = 30;
56
- if (typeof config.xmllint == "undefined") config.xmllint = 'xmllint';
57
-
58
- //Configurar certificado
59
- this.#config = config;
60
- this.#cert = certificado;
61
- this.#xmlTools.XMLBuilder = new XMLBuilder({
62
- ignoreAttributes: false,
63
- attributeNamePrefix: "@",
64
- });
65
- this.#xmlTools.XMLParser = new XMLParser({
66
- ignoreAttributes: false,
67
- attributeNamePrefix: "@",
68
- parseTagValue: false, // Evita conversão automática de valores
69
- });
70
- }
71
-
72
- sefazEnviaLote(xml: string, data: any = { idLote: 1, indSinc: 0, compactar: false }): Promise<string> {
73
- return new Promise(async (resvol, reject) => {
74
- if (typeof data.idLote == "undefined") data.idLote = 1;
75
- if (typeof data.indSinc == "undefined") data.indSinc = 0;
76
- if (typeof data.compactar == "undefined") data.compactar = false;
77
-
78
- await this.#certTools();
79
- let jsonXmlLote = {
80
- "soap:Envelope": {
81
- "@xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
82
- "@xmlns:xsd": "http://www.w3.org/2001/XMLSchema",
83
- "@xmlns:soap": "http://www.w3.org/2003/05/soap-envelope",
84
- "soap:Body": {
85
- "nfeDadosMsg": {
86
- "@xmlns": "http://www.portalfiscal.inf.br/nfe/wsdl/NFeAutorizacao4",
87
- "enviNFe": {
88
- ...{
89
- "@xmlns": "http://www.portalfiscal.inf.br/nfe",
90
- "@versao": "4.00",
91
- "idLote": data.idLote,
92
- "indSinc": data.indSinc,
93
- },
94
- ...(await this.xml2json(xml))
95
- }
96
- }
97
- }
98
- },
99
- }
100
- let xmlLote = await this.json2xml(jsonXmlLote);
101
- try {
102
- let tempUF = urlEventos(this.#config.UF, this.#config.versao);
103
- const req = https.request(tempUF[`mod${this.#config.mod}`][(this.#config.tpAmb == 1 ? "producao" : "homologacao")].NFeAutorizacao, {
104
- ...{
105
- method: 'POST',
106
- headers: {
107
- 'Content-Type': 'application/soap+xml; charset=utf-8',
108
- 'Content-Length': xmlLote.length,
109
- },
110
- rejectUnauthorized: false
111
- },
112
- ...this.#pem
113
- }, (res) => {
114
- let data = '';
115
-
116
- res.on('data', (chunk) => {
117
- data += chunk;
118
- });
119
-
120
- res.on('end', () => {
121
- resvol(data);
122
- });
123
- });
124
-
125
- req.setTimeout(this.#config.timeout * 1000, () => {
126
- reject({
127
- name: 'TimeoutError',
128
- message: 'The operation was aborted due to timeout'
129
- });
130
- req.destroy(); // cancela a requisição
131
- });
132
- req.on('error', (erro) => {
133
- reject(erro);
134
- });
135
- req.write(xmlLote);
136
- req.end();
137
- } catch (erro) {
138
- reject(erro);
139
- }
140
- })
141
- }
142
-
143
- async xmlSign(xmlJSON: string, data: any = { tag: "infNFe" }): Promise<string> {
144
- return new Promise(async (resvol, reject) => {
145
- if (data.tag === undefined) data.tag = "infNFe";
146
-
147
- //Obter a tag que ira ser assinada
148
- let xml = await this.xml2json(xmlJSON) as any;
149
- let tempPem = await this.#certTools() as any;
150
-
151
- const sign = crypto.createSign('RSA-SHA1'); // Correção: Alterado para RSA-SHA1
152
- let signedInfo = {
153
- "SignedInfo": {
154
- "@xmlns": "http://www.w3.org/2000/09/xmldsig#",
155
- "CanonicalizationMethod": {
156
- "@Algorithm": "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"
157
- },
158
- "SignatureMethod": {
159
- "@Algorithm": "http://www.w3.org/2000/09/xmldsig#rsa-sha1" // Mantém SHA1
160
- },
161
- "Reference": {
162
- "@URI": `#${xml.NFe.infNFe['@Id']}`,
163
- "Transforms": {
164
- "Transform": [
165
- {
166
- "@Algorithm": "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
167
- },
168
- {
169
- "@Algorithm": "http://www.w3.org/TR/2001/REC-xml-c14n-20010315"
170
- },
171
- ]
172
- },
173
- "DigestMethod": {
174
- "@Algorithm": "http://www.w3.org/2000/09/xmldsig#sha1" // Mantém SHA1
175
- },
176
- "DigestValue": crypto.createHash('sha1').update(await this.json2xml({ infNFe: xml.NFe.infNFe }), 'utf8') // Mantém Hash SHA1
177
- .digest('base64')
178
-
179
- }
180
- }
181
- }
182
- sign.update(await this.json2xml(signedInfo), 'utf8');
183
- xml.NFe.Signature = {
184
- ...signedInfo,
185
- ...{
186
- "SignatureValue": sign.sign(tempPem.key, 'base64'),
187
- "KeyInfo": {
188
- "X509Data": {
189
- "X509Certificate": tempPem.cert.replace(/-----BEGIN CERTIFICATE-----/g, '').replace(/-----END CERTIFICATE-----/g, '').replace(/\r\n/g, '')
190
- }
191
- },
192
- "@xmlns": "http://www.w3.org/2000/09/xmldsig#"
193
- }
194
- }
195
-
196
-
197
- if (xml.NFe.infNFe.ide.mod == 65) {
198
- xml.NFe.infNFeSupl.qrCode = this.#gerarQRCodeNFCe(xml.NFe, "2", this.#config.CSCid, this.#config.CSC)
199
- }
200
-
201
- this.json2xml(xml).then(async res => {
202
- this.#xmlValido(res, `nfe_v${this.#config.versao}`);
203
- resvol(res);
204
- }).catch(err => {
205
- reject(err)
206
- })
207
- })
208
- }
209
-
210
- //Gerar QRCode da NFCe
211
- #gerarQRCodeNFCe(NFe: any, versaoQRCode: string = "2", idCSC: string, CSC: string): string {
212
- let s = '|',
213
- concat,
214
- hash;
215
- if (NFe.infNFe.ide.tpEmis == 1) {
216
- concat = [NFe.infNFe['@Id'].replace("NFe", ""), versaoQRCode, NFe.infNFe.ide.tpAmb, Number(idCSC)].join(s);
217
- } else {
218
- let hexDigestValue = Buffer.from(NFe.Signature.SignedInfo.Reference.DigestValue).toString('hex');
219
- concat = [NFe.infNFe['@Id'].replace("NFe", ""), versaoQRCode, NFe.infNFe.ide.tpAmb, NFe.infNFe.ide.dhEmi, NFe.infNFe.total.ICMSTot.vNF, hexDigestValue, Number(idCSC)].join(s);
220
- }
221
- hash = crypto.createHash('sha1').update(concat + CSC).digest('hex');
222
- return NFe.infNFeSupl.qrCode + '?p=' + concat + s + hash;
223
- }
224
-
225
- async xml2json(xml: string): Promise<object> {
226
- return new Promise((resvol, reject) => {
227
- resvol(this.#xmlTools.XMLParser.parse(xml))
228
- })
229
- }
230
-
231
- async json2xml(obj: object): Promise<string> {
232
- return new Promise((resvol, reject) => {
233
- resvol(this.#xmlTools.XMLBuilder.build(obj))
234
- })
235
- }
236
-
237
- //Obter certificado
238
- async getCertificado(): Promise<object> {
239
- return new Promise(async (resvol, reject) => {
240
- this.#certTools().then(resvol).catch(reject)
241
- })
242
- }
243
-
244
- //Consulta NFe
245
- consultarNFe(chNFe: string): Promise<string> {
246
- return new Promise(async (resolve, reject) => {
247
- if (!chNFe || chNFe.length !== 44) {
248
- return reject("consultarNFe(chNFe) -> chave inválida!");
249
- }
250
-
251
- if (typeof this.#config.UF === "undefined") throw "consultarNFe({...UF}) -> não definido!";
252
- if (typeof this.#config.tpAmb === "undefined") throw "consultarNFe({...tpAmb}) -> não definido!";
253
- if (typeof this.#config.mod === "undefined") throw "consultarNFe({...mod}) -> não definido!";
254
-
255
- let consSitNFe = {
256
- "@xmlns": "http://www.portalfiscal.inf.br/nfe",
257
- "@versao": "4.00",
258
- "tpAmb": this.#config.tpAmb,
259
- "xServ": "CONSULTAR",
260
- "chNFe": chNFe
261
- };
262
-
263
- let xmlObj = {
264
- "soap:Envelope": {
265
- "@xmlns:soap": "http://www.w3.org/2003/05/soap-envelope",
266
- "@xmlns:nfe": "http://www.portalfiscal.inf.br/nfe/wsdl/NFeConsultaProtocolo4",
267
- "soap:Body": {
268
- "nfe:nfeDadosMsg": {
269
- "consSitNFe": consSitNFe
270
- }
271
- }
272
- }
273
- };
274
-
275
- try {
276
- const builder = new XMLBuilder({
277
- ignoreAttributes: false,
278
- attributeNamePrefix: "@"
279
- });
280
-
281
- // Validação do XML interno (opcional)
282
- this.#xmlValido(builder.build({ consSitNFe }), `consSitNFe_v${this.#config.versao}`);
283
-
284
- const xml = builder.build(xmlObj);
285
-
286
- let tempUF = urlEventos(this.#config.UF, this.#config.versao);
287
-
288
- const url = tempUF[`mod${this.#config.mod}`][(this.#config.tpAmb == 1 ? "producao" : "homologacao")].NFeConsultaProtocolo;
289
-
290
- const req = https.request(url, {
291
- method: 'POST',
292
- headers: {
293
- 'Content-Type': 'application/soap+xml; charset=utf-8',
294
- 'Content-Length': xml.length,
295
- },
296
- rejectUnauthorized: false,
297
- ...await this.#certTools()
298
- }, (res) => {
299
- let data = '';
300
- res.on('data', (chunk) => data += chunk);
301
- res.on('end', () => resolve(data));
302
- });
303
-
304
- req.setTimeout(this.#config.timeout * 1000, () => {
305
- reject({
306
- name: 'TimeoutError',
307
- message: 'The operation was aborted due to timeout'
308
- });
309
- req.destroy(); // cancela a requisição
310
- });
311
- req.on('error', (err) => reject(err));
312
- req.write(xml);
313
- req.end();
314
- } catch (err) {
315
- reject(err);
316
- }
317
- });
318
- }
319
-
320
- //Consulta status sefaz
321
- async sefazStatus(): Promise<string> {
322
- return new Promise(async (resvol, reject) => {
323
-
324
- if (typeof this.#config.UF == "undefined") throw "sefazStatus({...UF}) -> não definido!";
325
- if (typeof this.#config.tpAmb == "undefined") throw "sefazStatus({...tpAmb}) -> não definido!";
326
- if (typeof this.#config.mod == "undefined") throw "sefazStatus({...mod}) -> não definido!";
327
-
328
- let tempUF = urlEventos(this.#config.UF, this.#config.versao);
329
-
330
- //Separado para validar o corpo da consulta
331
- let consStatServ = {
332
- "@versao": "4.00",
333
- "@xmlns": "http://www.portalfiscal.inf.br/nfe",
334
- "tpAmb": this.#config.tpAmb,
335
- "cUF": tempUF.cUF,
336
- "xServ": "STATUS"
337
- }
338
-
339
- let xmlObj = {
340
- "soap:Envelope": {
341
- "@xmlns:soap": "http://www.w3.org/2003/05/soap-envelope",
342
- "@xmlns:nfe": "http://www.portalfiscal.inf.br/nfe/wsdl/NFeStatusServico4",
343
- "soap:Body": {
344
- "nfe:nfeDadosMsg": {
345
- consStatServ
346
- }
347
- }
348
- }
349
- }
350
-
351
- try {
352
- let tempBuild = new XMLBuilder({
353
- ignoreAttributes: false,
354
- attributeNamePrefix: "@"
355
- });
356
-
357
- //Validação
358
- this.#xmlValido(tempBuild.build({ consStatServ }), `consStatServ_v${this.#config.versao}`);
359
- let tempUF = urlEventos(this.#config.UF, this.#config.versao);
360
- let xml = tempBuild.build(xmlObj);
361
- const req = https.request(tempUF[`mod${this.#config.mod}`][(this.#config.tpAmb == 1 ? "producao" : "homologacao")].NFeStatusServico, {
362
- ...{
363
- method: 'POST',
364
- headers: {
365
- 'Content-Type': 'application/soap+xml; charset=utf-8',
366
- 'Content-Length': xml.length,
367
- },
368
- rejectUnauthorized: false
369
- },
370
- ...await this.#certTools()
371
- }, (res) => {
372
- let data = '';
373
-
374
- res.on('data', (chunk) => {
375
- data += chunk;
376
- });
377
-
378
- res.on('end', () => {
379
- resvol(data);
380
- });
381
- });
382
-
383
- req.setTimeout(this.#config.timeout * 1000, () => {
384
- reject({
385
- name: 'TimeoutError',
386
- message: 'The operation was aborted due to timeout'
387
- });
388
- req.destroy(); // cancela a requisição
389
- });
390
- req.on('error', (erro) => {
391
- reject(erro);
392
- });
393
-
394
- req.write(xml);
395
- req.end();
396
- } catch (erro) {
397
- reject(erro);
398
- }
399
- })
400
- }
401
-
402
-
403
- //Validar XML da NFe, somente apos assinar
404
- async #xmlValido(xml: string, xsd: string) {
405
- const xmlFile = tmp.fileSync({ mode: 0o644, prefix: 'xml-', postfix: '.xml' });
406
- fs.writeFileSync(xmlFile.name, xml, { encoding: 'utf8' });
407
- const schemaPath = path.resolve(__dirname, `../../schemas/${xsd}.xsd`);
408
- const verif: SpawnSyncReturns<string> = spawnSync(
409
- this.#config.xmllint,
410
- ['--noout', '--schema', schemaPath, xmlFile.name],
411
- { encoding: 'utf8' }
412
- );
413
-
414
- xmlFile.removeCallback();
415
-
416
- // Aqui, usamos o operador de encadeamento opcional (?.)
417
- if (verif.error) {
418
- throw new Error("Biblioteca xmllint não encontrada!");
419
- } else if (!verif.stderr.includes(".xml validates")) {
420
- throw new Error(verif.stderr);
421
- }
422
- return 1;
423
- }
424
-
425
- #certTools(): Promise<object> {
426
- return new Promise(async (resvol, reject) => {
427
- if (this.#pem.key != "") resvol(this.#pem);
428
- pem.readPkcs12(this.#cert.pfx, { p12Password: this.#cert.senha }, (err, myPem) => {
429
- if (err) return reject(err); // <-- importante!
430
- this.#pem = myPem;
431
- resvol(this.#pem);
432
- });
433
- })
434
- }
435
- }
436
- export { Tools }