@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 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
- // ── 1. Intentar reusar sesión cacheada ───────────────────────────────────
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.4",
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",