@devlas/dte-sii 2.9.4 → 2.9.5
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 +37 -0
- package/SiiPortalAuth.js +117 -1
- package/SiiSessionStore.js +24 -0
- package/package.json +1 -1
package/CafSolicitor.js
CHANGED
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
|
|
15
15
|
const fs = require('fs');
|
|
16
16
|
const path = require('path');
|
|
17
|
+
const crypto = require('crypto');
|
|
17
18
|
const SiiSession = require('./SiiSession');
|
|
19
|
+
const SiiPortalAuth = require('./SiiPortalAuth');
|
|
20
|
+
const SiiSessionStore = require('./SiiSessionStore');
|
|
18
21
|
const { splitRut } = require('./utils/rut');
|
|
19
22
|
|
|
20
23
|
/**
|
|
@@ -70,6 +73,40 @@ class CafSolicitor {
|
|
|
70
73
|
pfxPath: options.pfxPath,
|
|
71
74
|
pfxPassword: options.pfxPassword,
|
|
72
75
|
});
|
|
76
|
+
|
|
77
|
+
// Intentar reutilizar cookies del store compartido (SiiPortalAuth o sesión previa).
|
|
78
|
+
// También calcula certHash para escribir de vuelta al store cuando este SiiSession autentique.
|
|
79
|
+
let certHash = null;
|
|
80
|
+
try {
|
|
81
|
+
const pfxBuffer = fs.readFileSync(options.pfxPath);
|
|
82
|
+
const { certPem } = SiiPortalAuth._extractPems(pfxBuffer, options.pfxPassword);
|
|
83
|
+
certHash = crypto.createHash('sha1').update(certPem).digest('hex').slice(0, 12);
|
|
84
|
+
|
|
85
|
+
const existingCookies = SiiPortalAuth.getCookieStringForPfx(pfxBuffer, options.pfxPassword);
|
|
86
|
+
if (existingCookies) {
|
|
87
|
+
this.session.cookieJar = existingCookies;
|
|
88
|
+
console.log('[CafSolicitor] 🔗 Sesión SII pre-cargada desde store compartido — sin auth extra');
|
|
89
|
+
} else {
|
|
90
|
+
console.log('[CafSolicitor] 🔑 Sin sesión previa — SiiSession hará su propio auth');
|
|
91
|
+
}
|
|
92
|
+
} catch (_e) {
|
|
93
|
+
console.warn('[CafSolicitor] No se pudo leer PFX para seed de cookies:', _e.message);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Monkey-patch: al autenticar, escribe las cookies al store compartido
|
|
97
|
+
// para que SiiPortalAuth pueda reutilizarlas (dirección inversa de la unificación).
|
|
98
|
+
if (certHash && typeof this.session.loginWithCertificate === 'function') {
|
|
99
|
+
const _origLogin = this.session.loginWithCertificate.bind(this.session);
|
|
100
|
+
this.session.loginWithCertificate = async (...args) => {
|
|
101
|
+
const result = await _origLogin(...args);
|
|
102
|
+
if (this.session.cookieJar && certHash) {
|
|
103
|
+
SiiSessionStore.set(certHash, this.session.cookieJar);
|
|
104
|
+
console.log('[CafSolicitor] 🔗 Sesión post-login escrita al store compartido (hash=' + certHash + ')');
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
73
110
|
_sessionRegistry.set(sessionKey, this.session);
|
|
74
111
|
console.log('[CafSolicitor] 🔑 Nueva sesión SII registrada para', sessionKey);
|
|
75
112
|
}
|
package/SiiPortalAuth.js
CHANGED
|
@@ -30,6 +30,20 @@ const os = require('os');
|
|
|
30
30
|
const { URL } = require('url');
|
|
31
31
|
const forge = require('node-forge');
|
|
32
32
|
const crypto = require('crypto');
|
|
33
|
+
const SiiSessionStore = require('./SiiSessionStore');
|
|
34
|
+
|
|
35
|
+
function _cookieObjToStr(obj) {
|
|
36
|
+
return Object.entries(obj).map(([k, v]) => `${k}=${v}`).join('; ');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function _parseCookieStr(str) {
|
|
40
|
+
const obj = {};
|
|
41
|
+
for (const pair of (str || '').split('; ')) {
|
|
42
|
+
const eq = pair.indexOf('=');
|
|
43
|
+
if (eq > 0) obj[pair.slice(0, eq).trim()] = pair.slice(eq + 1).trim();
|
|
44
|
+
}
|
|
45
|
+
return obj;
|
|
46
|
+
}
|
|
33
47
|
|
|
34
48
|
// ─── Opciones TLS comunes para SII ────────────────────────────────────────────
|
|
35
49
|
const SII_TLS_OPTS = {
|
|
@@ -48,6 +62,14 @@ const SESSION_CACHE_PATH = path.join(
|
|
|
48
62
|
'sii_session_cache.json'
|
|
49
63
|
);
|
|
50
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Registro global de instancias SiiPortalAuth por certHash (singleton por certificado).
|
|
67
|
+
* Evita abrir múltiples sesiones concurrentes en el portal SII para el mismo certificado.
|
|
68
|
+
* Mismo patrón que CafSolicitor._sessionRegistry.
|
|
69
|
+
* @type {Map<string, SiiPortalAuth>}
|
|
70
|
+
*/
|
|
71
|
+
const _instanceRegistry = new Map();
|
|
72
|
+
|
|
51
73
|
/**
|
|
52
74
|
* Clase principal de autenticación con el portal SII
|
|
53
75
|
*/
|
|
@@ -67,8 +89,16 @@ class SiiPortalAuth {
|
|
|
67
89
|
// Huella para identificar de qué cert es la sesión cacheada
|
|
68
90
|
this._certHash = crypto.createHash('sha1').update(certPem).digest('hex').slice(0, 12);
|
|
69
91
|
|
|
92
|
+
// Reutilizar instancia existente en memoria si ya existe para este certificado.
|
|
93
|
+
// Esto evita abrir múltiples sesiones paralelas en el portal SII.
|
|
94
|
+
if (_instanceRegistry.has(this._certHash)) {
|
|
95
|
+
return _instanceRegistry.get(this._certHash);
|
|
96
|
+
}
|
|
97
|
+
|
|
70
98
|
this._agentePlano = new https.Agent(SII_TLS_OPTS);
|
|
71
99
|
this._agenteCert = new https.Agent({ ...SII_TLS_OPTS, cert: certPem, key: keyPem });
|
|
100
|
+
|
|
101
|
+
_instanceRegistry.set(this._certHash, this);
|
|
72
102
|
}
|
|
73
103
|
|
|
74
104
|
/**
|
|
@@ -170,12 +200,29 @@ class SiiPortalAuth {
|
|
|
170
200
|
async autenticar() {
|
|
171
201
|
const TARGET = 'https://misiir.sii.cl/cgi_misii/siihome.cgi';
|
|
172
202
|
|
|
173
|
-
// ──
|
|
203
|
+
// ── 1a. Store compartido (cubre sesiones de CafSolicitor/SiiSession) ────────
|
|
204
|
+
const storedStr = SiiSessionStore.get(this._certHash);
|
|
205
|
+
if (storedStr) {
|
|
206
|
+
const cookieObj = _parseCookieStr(storedStr);
|
|
207
|
+
const validaStore = await this._validarSesion(cookieObj);
|
|
208
|
+
if (validaStore) {
|
|
209
|
+
console.log('[SiiPortalAuth] Reutilizando sesión SII desde store compartido');
|
|
210
|
+
this._cachedCookieJar = cookieObj;
|
|
211
|
+
SiiPortalAuth._guardarSesionCache(this._certHash, cookieObj);
|
|
212
|
+
return cookieObj;
|
|
213
|
+
}
|
|
214
|
+
SiiSessionStore.delete(this._certHash);
|
|
215
|
+
console.warn('[SiiPortalAuth] Sesión del store compartido expirada, borrando...');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ── 1b. Caché en disco ───────────────────────────────────────────────────
|
|
174
219
|
const cached = SiiPortalAuth._cargarSesionCache(this._certHash);
|
|
175
220
|
if (cached) {
|
|
176
221
|
const valida = await this._validarSesion(cached);
|
|
177
222
|
if (valida) {
|
|
178
223
|
console.log('[SiiPortalAuth] Reutilizando sesión SII cacheada');
|
|
224
|
+
this._cachedCookieJar = cached;
|
|
225
|
+
SiiSessionStore.set(this._certHash, _cookieObjToStr(cached));
|
|
179
226
|
return cached;
|
|
180
227
|
}
|
|
181
228
|
console.warn('[SiiPortalAuth] Sesión cacheada expirada, re-autenticando...');
|
|
@@ -216,6 +263,8 @@ class SiiPortalAuth {
|
|
|
216
263
|
}
|
|
217
264
|
|
|
218
265
|
SiiPortalAuth._guardarSesionCache(this._certHash, cookieJar);
|
|
266
|
+
SiiSessionStore.set(this._certHash, _cookieObjToStr(cookieJar));
|
|
267
|
+
this._cachedCookieJar = cookieJar;
|
|
219
268
|
return cookieJar;
|
|
220
269
|
}
|
|
221
270
|
|
|
@@ -606,6 +655,73 @@ if (!fs.existsSync(SESSION_CACHE_PATH)) {
|
|
|
606
655
|
|
|
607
656
|
return { resumen, detalles: detallesArr.flat() };
|
|
608
657
|
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Retorna las cookies de sesión activas para un PFX dado, en formato string para SiiSession.
|
|
661
|
+
* Busca primero en el registry en memoria, luego en el caché a disco.
|
|
662
|
+
* Retorna null si no hay sesión previa (no hace auth nueva).
|
|
663
|
+
*
|
|
664
|
+
* Usado por CafSolicitor para reutilizar la sesión de SiiPortalAuth y evitar
|
|
665
|
+
* abrir una segunda sesión paralela en el portal SII para el mismo certificado.
|
|
666
|
+
*
|
|
667
|
+
* @param {Buffer} pfxBuffer - Buffer del archivo PFX
|
|
668
|
+
* @param {string} pfxPassword - Contraseña del PFX
|
|
669
|
+
* @returns {string|null} Cookies en formato "KEY=val; KEY2=val2" o null
|
|
670
|
+
*/
|
|
671
|
+
static getCookieStringForPfx(pfxBuffer, pfxPassword) {
|
|
672
|
+
try {
|
|
673
|
+
const { certPem } = SiiPortalAuth._extractPems(pfxBuffer, pfxPassword);
|
|
674
|
+
const certHash = crypto.createHash('sha1').update(certPem).digest('hex').slice(0, 12);
|
|
675
|
+
|
|
676
|
+
// 0. Store compartido (cubre tanto SiiPortalAuth como SiiSession/CafSolicitor)
|
|
677
|
+
const storedStr = SiiSessionStore.get(certHash);
|
|
678
|
+
if (storedStr) {
|
|
679
|
+
console.log('[SiiPortalAuth] 🔗 getCookieStringForPfx: cookies desde store compartido (hash=' + certHash + ')');
|
|
680
|
+
return storedStr;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// 1. Registry en memoria (más rápido, no hace I/O)
|
|
684
|
+
const instance = _instanceRegistry.get(certHash);
|
|
685
|
+
if (instance?._cachedCookieJar) {
|
|
686
|
+
const str = Object.entries(instance._cachedCookieJar).map(([k, v]) => `${k}=${v}`).join('; ');
|
|
687
|
+
console.log('[SiiPortalAuth] 🔗 getCookieStringForPfx: cookies desde registry en memoria (hash=' + certHash + ')');
|
|
688
|
+
return str;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// 2. Caché en disco (sobrevive reinicios dentro del mismo deploy)
|
|
692
|
+
const fileCookies = SiiPortalAuth._cargarSesionCache(certHash);
|
|
693
|
+
if (fileCookies) {
|
|
694
|
+
const str = Object.entries(fileCookies).map(([k, v]) => `${k}=${v}`).join('; ');
|
|
695
|
+
console.log('[SiiPortalAuth] 🔗 getCookieStringForPfx: cookies desde caché en disco (hash=' + certHash + ')');
|
|
696
|
+
return str;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
console.log('[SiiPortalAuth] getCookieStringForPfx: sin sesión previa para hash=' + certHash);
|
|
700
|
+
} catch (_e) {
|
|
701
|
+
console.warn('[SiiPortalAuth] getCookieStringForPfx: error leyendo PFX —', _e.message);
|
|
702
|
+
}
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Cierra todas las instancias SiiPortalAuth en caché (logout en el portal SII).
|
|
708
|
+
* Llamar durante el shutdown del proceso junto a CafSolicitor.closeAllSessions().
|
|
709
|
+
*/
|
|
710
|
+
static async closeAllSessions() {
|
|
711
|
+
for (const [hash, instance] of _instanceRegistry) {
|
|
712
|
+
try {
|
|
713
|
+
const logoutUrls = [
|
|
714
|
+
'https://herculesr.sii.cl/cgi_AUT2000/autLogout.cgi',
|
|
715
|
+
'https://www.sii.cl/AUT2000/autLogout.cgi',
|
|
716
|
+
];
|
|
717
|
+
for (const url of logoutUrls) {
|
|
718
|
+
await instance._request(url, { method: 'GET' }).catch(() => {});
|
|
719
|
+
}
|
|
720
|
+
} catch (_e) { /* ignorar errores de red al apagar */ }
|
|
721
|
+
_instanceRegistry.delete(hash);
|
|
722
|
+
SiiSessionStore.delete(hash);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
609
725
|
}
|
|
610
726
|
|
|
611
727
|
module.exports = SiiPortalAuth;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Copyright (c) 2026 Devlas SpA — https://devlas.cl
|
|
2
|
+
// Licencia MIT. Ver archivo LICENSE para mas detalles.
|
|
3
|
+
/**
|
|
4
|
+
* SiiSessionStore.js
|
|
5
|
+
*
|
|
6
|
+
* Registro compartido en memoria de cookies de sesión del portal SII.
|
|
7
|
+
* Permite que SiiPortalAuth y CafSolicitor/SiiSession usen la misma sesión
|
|
8
|
+
* portal sin abrir conexiones duplicadas, independiente del orden de ejecución.
|
|
9
|
+
*
|
|
10
|
+
* Clave: cert hash (SHA1[:12] del PEM del certificado) — único por certificado PFX.
|
|
11
|
+
* Valor: cookie string "KEY=val; KEY2=val2" — formato que acepta SiiSession.cookieJar.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const _store = new Map();
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
get(certHash) { return _store.get(certHash) ?? null; },
|
|
20
|
+
set(certHash, cookieString) { _store.set(certHash, cookieString); },
|
|
21
|
+
delete(certHash) { _store.delete(certHash); },
|
|
22
|
+
entries() { return _store.entries(); },
|
|
23
|
+
clear() { _store.clear(); },
|
|
24
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devlas/dte-sii",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.5",
|
|
4
4
|
"description": "Facturación y boletas electrónicas para el SII de Chile. Genera, timbra, firma y envía DTEs, libros electrónicos y automatiza la certificación.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "dte-sii.d.ts",
|