@devlas/dte-sii 2.12.26 → 2.13.1
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 +135 -15
- 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/SiiCertificacion.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
const SiiSession = require('./SiiSession.js');
|
|
19
19
|
const { STEPS, emitProgress } = require('./utils/progress');
|
|
20
|
+
const { resolveArtifactDir } = require('./utils/paths');
|
|
20
21
|
|
|
21
22
|
/** Decodifica entidades HTML latinas (el portal SII las usa en vez de UTF-8 crudo) y limpia
|
|
22
23
|
* tags/whitespace, para poder aplicar regex de texto sobre las respuestas de forma confiable. */
|
|
@@ -83,11 +84,18 @@ class SiiCertificacion {
|
|
|
83
84
|
|
|
84
85
|
this.rutEmpresa = options.rutEmpresa.replace(/\./g, '');
|
|
85
86
|
this.dvEmpresa = options.dvEmpresa.toUpperCase();
|
|
86
|
-
|
|
87
|
+
|
|
88
|
+
// Un solo lugar donde se decide dónde escribir. Antes había 7 call sites que
|
|
89
|
+
// recalculaban la ruta a mano, y no coincidían entre sí: cuatro usaban
|
|
90
|
+
// `../../debug/cert-v2` y tres `../../debug`, así que archivos de la misma
|
|
91
|
+
// operación quedaban repartidos en dos carpetas distintas.
|
|
92
|
+
this.debugDir = resolveArtifactDir(options.debugDir, options.debugDir ? '' : 'cert-v2');
|
|
93
|
+
|
|
87
94
|
this.session = new SiiSession({
|
|
88
95
|
pfxPath: options.pfxPath,
|
|
89
96
|
pfxPassword: options.pfxPassword,
|
|
90
97
|
ambiente: 'certificacion',
|
|
98
|
+
debugDir: this.debugDir,
|
|
91
99
|
});
|
|
92
100
|
|
|
93
101
|
// Reutilización de sesión: cargar desde archivo y guardar automáticamente tras cada login
|
|
@@ -632,7 +640,7 @@ class SiiCertificacion {
|
|
|
632
640
|
{
|
|
633
641
|
const fs = require('fs');
|
|
634
642
|
const path = require('path');
|
|
635
|
-
const debugDir =
|
|
643
|
+
const debugDir = this.debugDir;
|
|
636
644
|
if (!fs.existsSync(debugDir)) {
|
|
637
645
|
fs.mkdirSync(debugDir, { recursive: true });
|
|
638
646
|
}
|
|
@@ -789,7 +797,7 @@ class SiiCertificacion {
|
|
|
789
797
|
// Guardar respuesta de pe_avance3 para debug
|
|
790
798
|
const fs = require('fs');
|
|
791
799
|
const path = require('path');
|
|
792
|
-
const debugDir =
|
|
800
|
+
const debugDir = this.debugDir;
|
|
793
801
|
if (!fs.existsSync(debugDir)) {
|
|
794
802
|
fs.mkdirSync(debugDir, { recursive: true });
|
|
795
803
|
}
|
|
@@ -822,6 +830,38 @@ class SiiCertificacion {
|
|
|
822
830
|
errorMsg = 'Respuesta inválida al declarar avance';
|
|
823
831
|
}
|
|
824
832
|
|
|
833
|
+
// ── Datos inconsistentes: error DEFINITIVO, no "todavía no" ────────────
|
|
834
|
+
//
|
|
835
|
+
// El SII contesta 200 con una página normal que dice, entre los antecedentes del
|
|
836
|
+
// set, "FECHA NO CORRESPONDE AL ENVIO". No es un error de sesión ni de contenido,
|
|
837
|
+
// así que caía en el camino de éxito: la verificación posterior encontraba los
|
|
838
|
+
// campos vacíos, el bucle reintentaba 10 veces y después el polling seguía hasta
|
|
839
|
+
// agotar el timeout de la etapa. Minutos quemados esperando algo imposible, y sin
|
|
840
|
+
// que el mensaje real del SII llegara nunca al usuario.
|
|
841
|
+
//
|
|
842
|
+
// Esta ruta la comparten declarar avance, libros y simulación, así que detectarlo
|
|
843
|
+
// acá los cubre a los tres.
|
|
844
|
+
//
|
|
845
|
+
// La causa habitual es la zona horaria: los runners arman las fechas con
|
|
846
|
+
// `new Date().getDate()`, que usa la TZ del proceso. Corriendo en UTC, entre las
|
|
847
|
+
// 20:00 y las 00:00 de Chile ya es el día siguiente y se declara con la fecha de
|
|
848
|
+
// mañana. Por eso `TZ=America/Santiago` es obligatorio (ver CLAUDE.md).
|
|
849
|
+
const _inconsistencia = body.replace(/<[^>]*>/g, ' ')
|
|
850
|
+
.match(/((?:FECHA|RUT|FOLIO|NUMERO|N[UÚ]MERO)[^.<]{0,40}?NO\s+(?:CORRESPONDE|COINCIDE)[^.<]{0,40})/i);
|
|
851
|
+
if (_inconsistencia) {
|
|
852
|
+
const _detalle = _inconsistencia[1].replace(/\s+/g, ' ').trim();
|
|
853
|
+
return {
|
|
854
|
+
success: false,
|
|
855
|
+
datoInconsistente: true,
|
|
856
|
+
error: `El SII rechazó la declaración: ${_detalle}. Los datos declarados no coinciden con lo que el SII tiene registrado del envío; corrígelos y vuelve a declarar (esperar no lo resuelve).`,
|
|
857
|
+
status: declareResponse.status,
|
|
858
|
+
rawHtml: body,
|
|
859
|
+
formHtml,
|
|
860
|
+
setsDeclarados: Object.keys(sets),
|
|
861
|
+
formDataSent: formData,
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
|
|
825
865
|
// Éxito si el form se envió sin errores de sesión/contenido.
|
|
826
866
|
// El estado del envío (ERRORES O REPAROS / EN REVISION / REVISADO CONFORME)
|
|
827
867
|
// NO determina el éxito de la declaración — eso se resuelve vía polling.
|
|
@@ -843,7 +883,7 @@ class SiiCertificacion {
|
|
|
843
883
|
{
|
|
844
884
|
const fs = require('fs');
|
|
845
885
|
const path = require('path');
|
|
846
|
-
const debugDir =
|
|
886
|
+
const debugDir = this.debugDir;
|
|
847
887
|
if (!fs.existsSync(debugDir)) fs.mkdirSync(debugDir, { recursive: true });
|
|
848
888
|
fs.writeFileSync(path.join(debugDir, 'pe_avance3_response.html'), body, 'utf8');
|
|
849
889
|
|
|
@@ -868,7 +908,7 @@ class SiiCertificacion {
|
|
|
868
908
|
{
|
|
869
909
|
const fs = require('fs');
|
|
870
910
|
const path = require('path');
|
|
871
|
-
const debugDir =
|
|
911
|
+
const debugDir = this.debugDir;
|
|
872
912
|
if (!fs.existsSync(debugDir)) fs.mkdirSync(debugDir, { recursive: true });
|
|
873
913
|
fs.writeFileSync(path.join(debugDir, 'pe_avance2_verify.html'), verifyHtml, 'utf8');
|
|
874
914
|
|
|
@@ -1027,7 +1067,7 @@ class SiiCertificacion {
|
|
|
1027
1067
|
if (process.env.DEBUG_SII) {
|
|
1028
1068
|
const fs = require('fs');
|
|
1029
1069
|
const path = require('path');
|
|
1030
|
-
const debugDir =
|
|
1070
|
+
const debugDir = this.debugDir;
|
|
1031
1071
|
if (!fs.existsSync(debugDir)) {
|
|
1032
1072
|
fs.mkdirSync(debugDir, { recursive: true });
|
|
1033
1073
|
}
|
|
@@ -1053,7 +1093,7 @@ class SiiCertificacion {
|
|
|
1053
1093
|
if (process.env.DEBUG_SII) {
|
|
1054
1094
|
const fs = require('fs');
|
|
1055
1095
|
const path = require('path');
|
|
1056
|
-
const debugDir =
|
|
1096
|
+
const debugDir = this.debugDir;
|
|
1057
1097
|
if (!fs.existsSync(debugDir)) {
|
|
1058
1098
|
fs.mkdirSync(debugDir, { recursive: true });
|
|
1059
1099
|
}
|
|
@@ -1201,7 +1241,7 @@ class SiiCertificacion {
|
|
|
1201
1241
|
try {
|
|
1202
1242
|
const fs = require('fs');
|
|
1203
1243
|
const path = require('path');
|
|
1204
|
-
const debugDir =
|
|
1244
|
+
const debugDir = this.debugDir;
|
|
1205
1245
|
if (!fs.existsSync(debugDir)) fs.mkdirSync(debugDir, { recursive: true });
|
|
1206
1246
|
fs.writeFileSync(path.join(debugDir, filename), body || '', 'utf8');
|
|
1207
1247
|
} catch (_e) { /* debug best-effort, no bloquear el flujo real */ }
|
|
@@ -1475,6 +1515,13 @@ class SiiCertificacion {
|
|
|
1475
1515
|
esReparos: upper.includes('REPAROS'),
|
|
1476
1516
|
porRealizar: upper.includes('POR REALIZAR'),
|
|
1477
1517
|
esAnulado: upper.includes('ANULADO'),
|
|
1518
|
+
// Estados que NO se resuelven esperando: el dato declarado no cuadra con lo
|
|
1519
|
+
// que el SII tiene registrado, así que hay que corregirlo y volver a declarar.
|
|
1520
|
+
// Sin esta categoría caían en el limbo (ni conforme, ni en revisión, ni
|
|
1521
|
+
// rechazado) y el polling seguía hasta agotar el timeout de la etapa.
|
|
1522
|
+
// Caso real: "FECHA NO CORRESPONDE AL ENVIO" cuando el proceso corre en UTC
|
|
1523
|
+
// y declara con la fecha del día siguiente (ver TZ en docker-compose.yml).
|
|
1524
|
+
datoInconsistente: /NO CORRESPONDE|NO COINCIDE|FECHA INVALIDA/i.test(upper),
|
|
1478
1525
|
};
|
|
1479
1526
|
}
|
|
1480
1527
|
}
|
|
@@ -1540,7 +1587,9 @@ class SiiCertificacion {
|
|
|
1540
1587
|
: result.estados;
|
|
1541
1588
|
|
|
1542
1589
|
const todosConformes = Object.values(estadosRelevantes).every(e => e.esConforme);
|
|
1543
|
-
|
|
1590
|
+
// `datoInconsistente` cuenta como rechazo: seguir esperando no lo arregla.
|
|
1591
|
+
const algunoRechazado = Object.values(estadosRelevantes)
|
|
1592
|
+
.some(e => e.esRechazado || e.datoInconsistente);
|
|
1544
1593
|
|
|
1545
1594
|
if (onProgress) {
|
|
1546
1595
|
onProgress({ intento, maxIntentos, estado: 'resultado', estados: estadosRelevantes });
|
|
@@ -1559,7 +1608,7 @@ class SiiCertificacion {
|
|
|
1559
1608
|
// sin decir cuál set ni con qué estado. Se nombran solo los rechazados
|
|
1560
1609
|
// para no ahogar el dato entre los que sí pasaron.
|
|
1561
1610
|
const detalle = Object.values(estadosRelevantes)
|
|
1562
|
-
.filter(e => e.esRechazado)
|
|
1611
|
+
.filter(e => e.esRechazado || e.datoInconsistente)
|
|
1563
1612
|
.map(e => `${e.nombre}: ${e.estado}`)
|
|
1564
1613
|
.join('; ');
|
|
1565
1614
|
return {
|
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);
|