@devlas/dte-sii 2.12.26 → 2.13.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.
- package/CafSolicitor.js +16 -2
- package/DTE.js +8 -2
- package/EnviadorSII.js +31 -11
- package/FolioRegistry.js +7 -2
- package/FolioService.js +62 -3
- package/README.md +62 -0
- package/SiiCertificacion.js +59 -10
- package/SiiPortalAuth.js +131 -6
- package/SiiSession.js +27 -9
- package/WsReclamo.js +7 -6
- package/cert/BoletaCert.js +29 -10
- package/cert/CertRunner.js +247 -33
- package/cert/ConfigLoader.js +4 -2
- package/cert/IntercambioCert.js +4 -1
- package/dte-sii.d.ts +48 -1
- package/index.js +2 -0
- package/package.json +1 -1
- package/utils/httpDebug.js +223 -0
- package/utils/index.js +2 -0
- package/utils/paths.js +152 -0
- package/utils/sanitize.js +48 -0
- package/utils/xml.js +4 -2
package/SiiPortalAuth.js
CHANGED
|
@@ -26,11 +26,12 @@ const https = require('https');
|
|
|
26
26
|
const http = require('http');
|
|
27
27
|
const fs = require('fs');
|
|
28
28
|
const path = require('path');
|
|
29
|
-
const os = require('os');
|
|
30
29
|
const { URL } = require('url');
|
|
31
30
|
const forge = require('node-forge');
|
|
32
31
|
const crypto = require('crypto');
|
|
33
32
|
const SiiSessionStore = require('./SiiSessionStore');
|
|
33
|
+
const { resolveDataDir } = require('./utils/paths');
|
|
34
|
+
const { registrarHttpDebug } = require('./utils/httpDebug');
|
|
34
35
|
|
|
35
36
|
function _cookieObjToStr(obj) {
|
|
36
37
|
return Object.entries(obj).map(([k, v]) => `${k}=${v}`).join('; ');
|
|
@@ -57,10 +58,12 @@ const SII_TLS_OPTS = {
|
|
|
57
58
|
// ─── Ruta del caché de sesión ─────────────────────────────────────────────────
|
|
58
59
|
// Guarda las cookies NETSCAPE_LIVEWIRE.* para reusar entre ejecuciones y evitar
|
|
59
60
|
// el error "máximo de sesiones autenticadas" del SII.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
//
|
|
62
|
+
// El directorio lo define el consumidor vía DATADIR; el fallback es os.tmpdir(),
|
|
63
|
+
// neutral respecto del sistema operativo. Antes caía a `~/AppData/Roaming/POS`,
|
|
64
|
+
// una convención de un producto Windows específico hardcodeada en una librería
|
|
65
|
+
// genérica de DTE — en Linux creaba esa ruta igual, porque Node no la valida.
|
|
66
|
+
const SESSION_CACHE_PATH = path.join(resolveDataDir(), 'sii_session_cache.json');
|
|
64
67
|
|
|
65
68
|
/**
|
|
66
69
|
* Registro global de instancias SiiPortalAuth por certHash (singleton por certificado).
|
|
@@ -196,6 +199,7 @@ class SiiPortalAuth {
|
|
|
196
199
|
|
|
197
200
|
_request(urlStr, { method = 'GET', cookieJar = {}, body = null, headers = {}, usarCert = false } = {}) {
|
|
198
201
|
return new Promise((resolve, reject) => {
|
|
202
|
+
const _t0 = Date.now();
|
|
199
203
|
const url = new URL(urlStr);
|
|
200
204
|
const isHttps = url.protocol === 'https:';
|
|
201
205
|
|
|
@@ -248,7 +252,23 @@ class SiiPortalAuth {
|
|
|
248
252
|
const encoding = /charset=utf-?8/i.test(ct)
|
|
249
253
|
? 'utf8'
|
|
250
254
|
: (isSiiHost || /iso-8859|latin-1|windows-1252/i.test(ct)) ? 'latin1' : 'utf8';
|
|
251
|
-
|
|
255
|
+
const bodyStr = buf.toString(encoding);
|
|
256
|
+
|
|
257
|
+
// Cliente HTTP independiente de SiiSession (https nativo, sin código en común):
|
|
258
|
+
// necesita su propio hook o quedarían fuera las llamadas del flujo de
|
|
259
|
+
// verificación de autorización, que hoy no dejan ningún rastro.
|
|
260
|
+
registrarHttpDebug({
|
|
261
|
+
url: urlStr,
|
|
262
|
+
method,
|
|
263
|
+
status: res.statusCode,
|
|
264
|
+
headers: res.headers,
|
|
265
|
+
body: bodyStr,
|
|
266
|
+
reqBody: body,
|
|
267
|
+
ms: Date.now() - _t0,
|
|
268
|
+
cliente: 'SiiPortalAuth',
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
resolve({ status: res.statusCode, headers: res.headers, body: bodyStr, cookieJar });
|
|
252
272
|
});
|
|
253
273
|
});
|
|
254
274
|
|
|
@@ -1036,6 +1056,111 @@ if (!fs.existsSync(SESSION_CACHE_PATH)) {
|
|
|
1036
1056
|
SiiSessionStore.delete(hash);
|
|
1037
1057
|
}
|
|
1038
1058
|
}
|
|
1059
|
+
|
|
1060
|
+
/**
|
|
1061
|
+
* Autentica en el portal SII y devuelve los datos frescos del emisor
|
|
1062
|
+
* (resolución + datos del contribuyente) ya normalizados, junto con el
|
|
1063
|
+
* cookieJar de la sesión iniciada para reutilizarla.
|
|
1064
|
+
*
|
|
1065
|
+
* Es una operación completa del portal —autenticar, consultar dos endpoints,
|
|
1066
|
+
* reintentar con sesión fresca si la cacheada quedó inservible y normalizar el
|
|
1067
|
+
* resultado—, así que vive acá y no en el consumidor: no tiene ningún
|
|
1068
|
+
* conocimiento del proyecto que la use.
|
|
1069
|
+
*
|
|
1070
|
+
* @param {Object} opts
|
|
1071
|
+
* @param {Buffer} opts.pfxBuffer - Contenido del .pfx
|
|
1072
|
+
* @param {string} [opts.pfxPassword] - Contraseña del .pfx
|
|
1073
|
+
* @param {string} [opts.rutEmpresa] - RUT de la empresa (acepta puntos). Si no se
|
|
1074
|
+
* pasa, se intenta deducir de las cookies del portal tras autenticar.
|
|
1075
|
+
* @param {Function} [opts.onAviso] - Callback para avisos no fatales (default: console.warn).
|
|
1076
|
+
* @returns {Promise<{ emisor: Object, cookieJar: Object }>}
|
|
1077
|
+
* @throws Si no puede autenticar, si el RUT es inválido o si faltan datos de resolución.
|
|
1078
|
+
*/
|
|
1079
|
+
static async obtenerEmisor({ pfxBuffer, pfxPassword = '', rutEmpresa = '', onAviso } = {}) {
|
|
1080
|
+
const aviso = onAviso || ((msg) => console.warn(msg));
|
|
1081
|
+
const auth = new SiiPortalAuth({ pfxBuffer, pfxPassword });
|
|
1082
|
+
|
|
1083
|
+
// Se resuelve antes de autenticar para poder reintentar con sesión fresca.
|
|
1084
|
+
let rutNum, dv;
|
|
1085
|
+
if (rutEmpresa) {
|
|
1086
|
+
const rutLimpio = String(rutEmpresa).replace(/\./g, '');
|
|
1087
|
+
const match = rutLimpio.match(/^(\d+)-([0-9Kk])$/);
|
|
1088
|
+
if (!match) {
|
|
1089
|
+
throw new Error(`RUT empresa inválido: "${rutEmpresa}". Formato esperado: "78206276-K"`);
|
|
1090
|
+
}
|
|
1091
|
+
rutNum = match[1];
|
|
1092
|
+
dv = match[2].toUpperCase();
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
let cookieJar = await auth.autenticar();
|
|
1096
|
+
|
|
1097
|
+
const rutDesdeCookies = (jar) => [
|
|
1098
|
+
jar['NETSCAPE_LIVEWIRE.rutm'] || jar['NETSCAPE_LIVEWIRE.rut'] || null,
|
|
1099
|
+
jar['NETSCAPE_LIVEWIRE.dvm'] || jar['NETSCAPE_LIVEWIRE.dv'] || null,
|
|
1100
|
+
];
|
|
1101
|
+
|
|
1102
|
+
if (!rutNum) {
|
|
1103
|
+
[rutNum, dv] = rutDesdeCookies(cookieJar);
|
|
1104
|
+
if (!rutNum || !dv) {
|
|
1105
|
+
throw new Error(
|
|
1106
|
+
'No se pudo determinar el RUT empresa desde las cookies del portal SII. ' +
|
|
1107
|
+
'Indica el RUT explícitamente en opts.rutEmpresa.',
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// Retry con sesión fresca: a veces la sesión cacheada pasa la validación básica
|
|
1113
|
+
// pero ad_empresa2 devuelve otra página (ej. "Actualización de Datos del
|
|
1114
|
+
// Contribuyente"). Secuencial a propósito: compartir el cookieJar en paralelo
|
|
1115
|
+
// genera una race condition — ce_consulta_muestra_e puede sobreescribir la cookie
|
|
1116
|
+
// Tivoli (TS0xxxxxxx) que ad_empresa1 dejó, y ad_empresa2 responde vacío.
|
|
1117
|
+
let datosResol, datosContrib;
|
|
1118
|
+
for (let intento = 1; intento <= 2; intento++) {
|
|
1119
|
+
try {
|
|
1120
|
+
datosResol = await auth.obtenerDatosEmpresa(rutNum, dv, cookieJar);
|
|
1121
|
+
datosContrib = await auth.obtenerDatosContribuyente(rutNum, dv, cookieJar).catch((e) => {
|
|
1122
|
+
aviso(`[SiiPortalAuth] obtenerDatosContribuyente falló (no crítico): ${e.message}`);
|
|
1123
|
+
return null;
|
|
1124
|
+
});
|
|
1125
|
+
break;
|
|
1126
|
+
} catch (e) {
|
|
1127
|
+
if (intento === 1) {
|
|
1128
|
+
aviso(`[SiiPortalAuth] Sesión cacheada inválida (${e.message}). Re-autenticando...`);
|
|
1129
|
+
SiiPortalAuth.limpiarSesionCache();
|
|
1130
|
+
cookieJar = await auth.autenticar();
|
|
1131
|
+
if (!rutEmpresa) {
|
|
1132
|
+
const [r, d] = rutDesdeCookies(cookieJar);
|
|
1133
|
+
rutNum = r || rutNum;
|
|
1134
|
+
dv = d || dv;
|
|
1135
|
+
}
|
|
1136
|
+
continue;
|
|
1137
|
+
}
|
|
1138
|
+
throw e;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
if (!datosResol?.fch_resol) {
|
|
1143
|
+
throw new Error(
|
|
1144
|
+
`No se encontraron datos de resolución para ${rutNum}-${dv} en el portal SII.\n` +
|
|
1145
|
+
'Verifica que la empresa esté habilitada para emitir DTE.',
|
|
1146
|
+
);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// fch_resol ya viene normalizada a YYYY-MM-DD desde obtenerDatosEmpresa
|
|
1150
|
+
const emisor = {
|
|
1151
|
+
rut: `${rutNum}-${dv}`,
|
|
1152
|
+
razon_social: datosContrib?.razonSocial || datosResol.razonSocial || '',
|
|
1153
|
+
giro: datosContrib?.actividades?.[0]?.descripcion || datosContrib?.glosa || '',
|
|
1154
|
+
acteco: datosContrib?.acteco || datosContrib?.actividades?.[0]?.codigo || '',
|
|
1155
|
+
direccion: datosContrib?.direccion || '',
|
|
1156
|
+
comuna: datosContrib?.comuna || '',
|
|
1157
|
+
ciudad: datosContrib?.ciudad || datosContrib?.comuna || '',
|
|
1158
|
+
fch_resol: datosResol.fch_resol,
|
|
1159
|
+
nro_resol: datosResol.nro_resol ?? 0,
|
|
1160
|
+
};
|
|
1161
|
+
|
|
1162
|
+
return { emisor, cookieJar };
|
|
1163
|
+
}
|
|
1039
1164
|
}
|
|
1040
1165
|
|
|
1041
1166
|
module.exports = SiiPortalAuth;
|
package/SiiSession.js
CHANGED
|
@@ -20,6 +20,8 @@ const {
|
|
|
20
20
|
getHost,
|
|
21
21
|
createScopedLogger,
|
|
22
22
|
} = require('./utils');
|
|
23
|
+
const { resolveDebugDir, saveDebugFile } = require('./utils/paths');
|
|
24
|
+
const { registrarHttpDebug } = require('./utils/httpDebug');
|
|
23
25
|
|
|
24
26
|
const log = createScopedLogger('SiiSession');
|
|
25
27
|
|
|
@@ -34,6 +36,8 @@ class SiiSession {
|
|
|
34
36
|
* @param {string} [options.pfxPath] - Ruta al archivo PFX
|
|
35
37
|
* @param {string} [options.pfxPassword] - Contraseña del PFX
|
|
36
38
|
* @param {Object} [options.certificado] - Instancia de Certificado
|
|
39
|
+
* @param {string} [options.debugDir] - Dónde escribir HTML de diagnóstico. Si no se
|
|
40
|
+
* provee cae a `SII_DEBUG_DIR`; si tampoco existe, no se escribe nada.
|
|
37
41
|
*/
|
|
38
42
|
constructor(options = {}) {
|
|
39
43
|
// Validar parámetros obligatorios usando validador centralizado
|
|
@@ -41,9 +45,10 @@ class SiiSession {
|
|
|
41
45
|
throw new Error('SiiSession: options.ambiente es obligatorio');
|
|
42
46
|
}
|
|
43
47
|
this.ambiente = validateAmbiente(options.ambiente);
|
|
44
|
-
|
|
48
|
+
|
|
45
49
|
// Usar host centralizado desde endpoints
|
|
46
50
|
this.baseHost = getHost(this.ambiente);
|
|
51
|
+
this.debugDir = options.debugDir || null;
|
|
47
52
|
this.cookieJar = '';
|
|
48
53
|
this.tlsOptions = null;
|
|
49
54
|
// Agent nativo con el certificado + flags TLS legacy del SII. got no reenvía
|
|
@@ -204,6 +209,7 @@ class SiiSession {
|
|
|
204
209
|
* @private
|
|
205
210
|
*/
|
|
206
211
|
async _doRequest(url, options, isPost) {
|
|
212
|
+
const _t0 = Date.now();
|
|
207
213
|
const res = await got(url, {
|
|
208
214
|
method: options.method || 'GET',
|
|
209
215
|
headers: {
|
|
@@ -257,6 +263,21 @@ class SiiSession {
|
|
|
257
263
|
bodyStr = buffer.toString('utf8');
|
|
258
264
|
}
|
|
259
265
|
|
|
266
|
+
// Punto único por el que pasa TODO el tráfico de SiiSession: cada salto de
|
|
267
|
+
// redirect, cada submit de formulario y cada reintento. Enganchar acá cubre a
|
|
268
|
+
// SiiCertificacion, CertRunner, CafSolicitor, FolioService, BoletaCert y
|
|
269
|
+
// SetsProvider sin tocar ninguno.
|
|
270
|
+
registrarHttpDebug({
|
|
271
|
+
url,
|
|
272
|
+
method: options.method || 'GET',
|
|
273
|
+
status: res.statusCode,
|
|
274
|
+
headers: res.headers,
|
|
275
|
+
body: bodyStr,
|
|
276
|
+
reqBody: options.body,
|
|
277
|
+
ms: Date.now() - _t0,
|
|
278
|
+
cliente: 'SiiSession',
|
|
279
|
+
});
|
|
280
|
+
|
|
260
281
|
return {
|
|
261
282
|
status: res.statusCode,
|
|
262
283
|
headers: res.headers,
|
|
@@ -352,14 +373,11 @@ class SiiSession {
|
|
|
352
373
|
async _tryForceCloseSessions(body) {
|
|
353
374
|
if (!body || !body.includes('superado el m')) return false;
|
|
354
375
|
|
|
355
|
-
// Guardar HTML para diagnóstico
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
fs.mkdirSync(dbgDir, { recursive: true });
|
|
361
|
-
fs.writeFileSync(path.join(dbgDir, `demasiadas-sesiones-${Date.now()}.html`), body, 'utf-8');
|
|
362
|
-
} catch (_) {}
|
|
376
|
+
// Guardar HTML para diagnóstico — solo si el consumidor definió dónde.
|
|
377
|
+
// Antes esto apuntaba a `../devlas-cloud-api-node/debug/sii-sessions`: la librería
|
|
378
|
+
// nombraba a un repo consumidor y asumía que estaba como carpeta hermana, así que
|
|
379
|
+
// en cualquier otra instalación escribía en un lugar inesperado o fallaba en silencio.
|
|
380
|
+
saveDebugFile(resolveDebugDir(this.debugDir), `demasiadas-sesiones-${Date.now()}.html`, body);
|
|
363
381
|
|
|
364
382
|
// El SII a veces incluye un form o link para cerrar sesiones anteriores
|
|
365
383
|
// Buscamos: action con "CierraAnt", "cerrar", "logout" o "Anular"
|
package/WsReclamo.js
CHANGED
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
const forge = require('node-forge');
|
|
18
|
+
const { fetchRegistrado } = require('./utils/httpDebug');
|
|
19
|
+
|
|
20
|
+
/** `fetchRegistrado` con el cliente ya fijado, para no repetirlo en cada llamada. */
|
|
21
|
+
const fetchRegistradoWs = (url, opciones) => fetchRegistrado(url, opciones, 'WsReclamo');
|
|
18
22
|
const {
|
|
19
23
|
SOAP_ENDPOINTS,
|
|
20
24
|
WSRECLAMO_ENDPOINTS,
|
|
@@ -82,14 +86,13 @@ class WsReclamo {
|
|
|
82
86
|
<soapenv:Body><getSeed/></soapenv:Body>
|
|
83
87
|
</soapenv:Envelope>`;
|
|
84
88
|
|
|
85
|
-
const res = await
|
|
89
|
+
const { response: res, text: xml } = await fetchRegistradoWs(this._seedUrl, {
|
|
86
90
|
method: 'POST',
|
|
87
91
|
headers: { 'Content-Type': 'text/xml; charset=utf-8', SOAPAction: '' },
|
|
88
92
|
body: envelope,
|
|
89
93
|
});
|
|
90
94
|
if (!res.ok) throw siiError(`Error semilla: ${res.status}`, ERROR_CODES.SII_CONNECTION_FAILED);
|
|
91
95
|
|
|
92
|
-
const xml = await res.text();
|
|
93
96
|
const semilla = extractTagContent(decodeXmlEntities(xml), 'SEMILLA');
|
|
94
97
|
if (!semilla) throw siiError('No se obtuvo semilla del SII', ERROR_CODES.SII_INVALID_RESPONSE);
|
|
95
98
|
return semilla;
|
|
@@ -114,14 +117,13 @@ class WsReclamo {
|
|
|
114
117
|
</soapenv:Body>
|
|
115
118
|
</soapenv:Envelope>`;
|
|
116
119
|
|
|
117
|
-
const res = await
|
|
120
|
+
const { response: res, text: xml } = await fetchRegistradoWs(this._tokenUrl, {
|
|
118
121
|
method: 'POST',
|
|
119
122
|
headers: { 'Content-Type': 'text/xml; charset=utf-8', SOAPAction: '' },
|
|
120
123
|
body: envelope,
|
|
121
124
|
});
|
|
122
125
|
if (!res.ok) throw siiError(`Error token: ${res.status}`, ERROR_CODES.SII_CONNECTION_FAILED);
|
|
123
126
|
|
|
124
|
-
const xml = await res.text();
|
|
125
127
|
const decoded = xml.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&');
|
|
126
128
|
const token = extractTagContent(decoded, 'TOKEN');
|
|
127
129
|
if (!token) throw siiError('No se obtuvo TOKEN del SII', ERROR_CODES.SII_AUTH_FAILED);
|
|
@@ -247,7 +249,7 @@ class WsReclamo {
|
|
|
247
249
|
</soapenv:Body>
|
|
248
250
|
</soapenv:Envelope>`;
|
|
249
251
|
|
|
250
|
-
const res = await
|
|
252
|
+
const { response: res, text: xml } = await fetchRegistradoWs(this._wsUrl, {
|
|
251
253
|
method: 'POST',
|
|
252
254
|
headers: {
|
|
253
255
|
'Content-Type': 'text/xml; charset=utf-8',
|
|
@@ -268,7 +270,6 @@ class WsReclamo {
|
|
|
268
270
|
throw siiError(`WSRECLAMO ${metodo}: HTTP ${res.status}`, ERROR_CODES.SII_CONNECTION_FAILED);
|
|
269
271
|
}
|
|
270
272
|
|
|
271
|
-
const xml = await res.text();
|
|
272
273
|
return xml;
|
|
273
274
|
}
|
|
274
275
|
|
package/cert/BoletaCert.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
const path = require('path');
|
|
24
24
|
const fs = require('fs');
|
|
25
|
+
const { registrarHttpDebug } = require('../utils/httpDebug');
|
|
25
26
|
|
|
26
27
|
class BoletaCert {
|
|
27
28
|
/**
|
|
@@ -663,14 +664,23 @@ class BoletaCert {
|
|
|
663
664
|
|
|
664
665
|
// Hacer request
|
|
665
666
|
const requestWithCert = (options, payload) => new Promise((resolve, reject) => {
|
|
667
|
+
const t0 = Date.now();
|
|
666
668
|
const req = https.request({ ...options, ...tlsOptions }, (res) => {
|
|
667
669
|
let data = '';
|
|
668
670
|
res.on('data', (chunk) => (data += chunk));
|
|
669
|
-
res.on('end', () =>
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
671
|
+
res.on('end', () => {
|
|
672
|
+
registrarHttpDebug({
|
|
673
|
+
url: `https://${options.hostname}${options.path}`,
|
|
674
|
+
method: options.method || 'GET',
|
|
675
|
+
status: res.statusCode,
|
|
676
|
+
headers: res.headers,
|
|
677
|
+
body: data,
|
|
678
|
+
reqBody: payload,
|
|
679
|
+
ms: Date.now() - t0,
|
|
680
|
+
cliente: 'BoletaCert',
|
|
681
|
+
});
|
|
682
|
+
resolve({ status: res.statusCode, text: data, headers: res.headers });
|
|
683
|
+
});
|
|
674
684
|
});
|
|
675
685
|
req.on('error', reject);
|
|
676
686
|
if (payload) req.write(payload);
|
|
@@ -784,14 +794,23 @@ class BoletaCert {
|
|
|
784
794
|
|
|
785
795
|
// Hacer request
|
|
786
796
|
const requestWithCert = (options, payload) => new Promise((resolve, reject) => {
|
|
797
|
+
const t0 = Date.now();
|
|
787
798
|
const req = https.request({ ...options, ...tlsOptions }, (res) => {
|
|
788
799
|
let data = '';
|
|
789
800
|
res.on('data', (chunk) => (data += chunk));
|
|
790
|
-
res.on('end', () =>
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
801
|
+
res.on('end', () => {
|
|
802
|
+
registrarHttpDebug({
|
|
803
|
+
url: `https://${options.hostname}${options.path}`,
|
|
804
|
+
method: options.method || 'GET',
|
|
805
|
+
status: res.statusCode,
|
|
806
|
+
headers: res.headers,
|
|
807
|
+
body: data,
|
|
808
|
+
reqBody: payload,
|
|
809
|
+
ms: Date.now() - t0,
|
|
810
|
+
cliente: 'BoletaCert',
|
|
811
|
+
});
|
|
812
|
+
resolve({ status: res.statusCode, text: data, headers: res.headers });
|
|
813
|
+
});
|
|
795
814
|
});
|
|
796
815
|
req.on('error', reject);
|
|
797
816
|
if (payload) req.write(payload);
|