@devlas/dte-sii 2.5.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/BoletaService.js +109 -0
- package/CAF.js +173 -0
- package/CafSolicitor.js +380 -0
- package/Certificado.js +123 -0
- package/ConsumoFolio.js +376 -0
- package/DTE.js +399 -0
- package/EnviadorSII.js +1304 -0
- package/Envio.js +196 -0
- package/FolioRegistry.js +553 -0
- package/FolioService.js +703 -0
- package/LICENSE +27 -0
- package/LibroBase.js +134 -0
- package/LibroCompraVenta.js +205 -0
- package/LibroGuia.js +225 -0
- package/README.md +239 -0
- package/Signer.js +94 -0
- package/SiiCertificacion.js +1189 -0
- package/SiiPortalAuth.js +460 -0
- package/SiiSession.js +499 -0
- package/cert/BoletaCert.js +731 -0
- package/cert/CertFolioHelper.js +185 -0
- package/cert/CertRunner.js +2658 -0
- package/cert/ConfigLoader.js +133 -0
- package/cert/IntercambioCert.js +429 -0
- package/cert/LibroCompras.js +359 -0
- package/cert/LibroGuias.js +171 -0
- package/cert/LibroVentas.js +153 -0
- package/cert/MuestrasImpresas.js +676 -0
- package/cert/SetBase.js +321 -0
- package/cert/SetBasico.js +413 -0
- package/cert/SetCompra.js +472 -0
- package/cert/SetExenta.js +490 -0
- package/cert/SetGuia.js +283 -0
- package/cert/SetParser.js +1184 -0
- package/cert/SetsProvider.js +499 -0
- package/cert/Simulacion.js +521 -0
- package/cert/comunaOficina.js +460 -0
- package/cert/index.js +124 -0
- package/cert/types.js +330 -0
- package/dte-sii.d.ts +458 -0
- package/index.js +428 -0
- package/package.json +48 -0
- package/utils/c14n.js +275 -0
- package/utils/calculo.js +396 -0
- package/utils/config.js +276 -0
- package/utils/constants.js +302 -0
- package/utils/emisor.js +174 -0
- package/utils/endpoints.js +225 -0
- package/utils/error.js +235 -0
- package/utils/index.js +339 -0
- package/utils/logger.js +239 -0
- package/utils/pfx.js +203 -0
- package/utils/receptor.js +218 -0
- package/utils/referencia.js +169 -0
- package/utils/resolucion.js +119 -0
- package/utils/rut.js +169 -0
- package/utils/sanitize.js +124 -0
- package/utils/tokenCache.js +214 -0
- package/utils/xml.js +358 -0
- package/utils.js +4 -0
package/SiiSession.js
ADDED
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
// Copyright (c) 2026 Devlas SpA — https://devlas.cl
|
|
2
|
+
// Licencia MIT. Ver archivo LICENSE para mas detalles.
|
|
3
|
+
/**
|
|
4
|
+
* SiiSession.js - Manejo de sesiones HTTP con el SII
|
|
5
|
+
*
|
|
6
|
+
* Proporciona autenticación con certificado digital y manejo de cookies
|
|
7
|
+
* para interactuar con los servicios web del SII.
|
|
8
|
+
*
|
|
9
|
+
* @module SiiSession
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const got = require('got');
|
|
13
|
+
const {
|
|
14
|
+
loadPfxFromBuffer,
|
|
15
|
+
loadPfxFromFile,
|
|
16
|
+
createTlsOptions,
|
|
17
|
+
validateAmbiente,
|
|
18
|
+
getHost,
|
|
19
|
+
createScopedLogger,
|
|
20
|
+
} = require('./utils');
|
|
21
|
+
|
|
22
|
+
const log = createScopedLogger('SiiSession');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Clase para manejar sesiones HTTP con el SII
|
|
26
|
+
*/
|
|
27
|
+
class SiiSession {
|
|
28
|
+
/**
|
|
29
|
+
* @param {Object} options - Opciones de configuración
|
|
30
|
+
* @param {string} options.ambiente - 'certificacion' o 'produccion' (OBLIGATORIO)
|
|
31
|
+
* @param {Buffer|string} [options.pfxBuffer] - Buffer del archivo PFX
|
|
32
|
+
* @param {string} [options.pfxPath] - Ruta al archivo PFX
|
|
33
|
+
* @param {string} [options.pfxPassword] - Contraseña del PFX
|
|
34
|
+
* @param {Object} [options.certificado] - Instancia de Certificado
|
|
35
|
+
*/
|
|
36
|
+
constructor(options = {}) {
|
|
37
|
+
// Validar parámetros obligatorios usando validador centralizado
|
|
38
|
+
if (!options.ambiente) {
|
|
39
|
+
throw new Error('SiiSession: options.ambiente es obligatorio');
|
|
40
|
+
}
|
|
41
|
+
this.ambiente = validateAmbiente(options.ambiente);
|
|
42
|
+
|
|
43
|
+
// Usar host centralizado desde endpoints
|
|
44
|
+
this.baseHost = getHost(this.ambiente);
|
|
45
|
+
this.cookieJar = '';
|
|
46
|
+
this.tlsOptions = null;
|
|
47
|
+
|
|
48
|
+
// Configurar TLS desde certificado
|
|
49
|
+
if (options.certificado) {
|
|
50
|
+
this._configureTlsFromCertificado(options.certificado);
|
|
51
|
+
} else if (options.pfxBuffer && options.pfxPassword) {
|
|
52
|
+
this._configureTlsFromBuffer(options.pfxBuffer, options.pfxPassword);
|
|
53
|
+
} else if (options.pfxPath && options.pfxPassword) {
|
|
54
|
+
this._configureTlsFromFile(options.pfxPath, options.pfxPassword);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Configura TLS desde una instancia de Certificado
|
|
60
|
+
* @private
|
|
61
|
+
*/
|
|
62
|
+
_configureTlsFromCertificado(certificado) {
|
|
63
|
+
try {
|
|
64
|
+
this.tlsOptions = {
|
|
65
|
+
key: certificado.getPrivateKeyPEM(),
|
|
66
|
+
cert: certificado.getCertificatePEM(),
|
|
67
|
+
certificate: certificado.getCertificatePEM(),
|
|
68
|
+
rejectUnauthorized: false,
|
|
69
|
+
};
|
|
70
|
+
} catch (error) {
|
|
71
|
+
log.error('Error configurando TLS desde certificado:', error.message);
|
|
72
|
+
this.tlsOptions = null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Configura TLS desde un buffer PFX usando utilidad centralizada
|
|
78
|
+
* @private
|
|
79
|
+
*/
|
|
80
|
+
_configureTlsFromBuffer(pfxBuffer, password) {
|
|
81
|
+
try {
|
|
82
|
+
const pfxData = loadPfxFromBuffer(pfxBuffer, password);
|
|
83
|
+
this.tlsOptions = createTlsOptions(pfxData);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
log.error('Error configurando TLS desde PFX:', error.message);
|
|
86
|
+
this.tlsOptions = null;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Configura TLS desde un archivo PFX usando utilidad centralizada
|
|
92
|
+
* @private
|
|
93
|
+
*/
|
|
94
|
+
_configureTlsFromFile(pfxPath, password) {
|
|
95
|
+
try {
|
|
96
|
+
const pfxData = loadPfxFromFile(pfxPath, password);
|
|
97
|
+
this.tlsOptions = createTlsOptions(pfxData);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
log.error('Error configurando TLS desde archivo PFX:', error.message);
|
|
100
|
+
this.tlsOptions = null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Parsea un RUT en sus componentes
|
|
106
|
+
* @param {string} rutCompleto - RUT con formato XX.XXX.XXX-X o XXXXXXXX-X
|
|
107
|
+
* @returns {{rut: string, dv: string}}
|
|
108
|
+
*/
|
|
109
|
+
static parseRut(rutCompleto) {
|
|
110
|
+
const clean = String(rutCompleto || '').replace(/\./g, '').toUpperCase();
|
|
111
|
+
const [rut, dv] = clean.split('-');
|
|
112
|
+
return { rut, dv };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Codifica campos para form-urlencoded
|
|
117
|
+
* @param {Object} fields - Campos a codificar
|
|
118
|
+
* @returns {string}
|
|
119
|
+
*/
|
|
120
|
+
static formEncode(fields) {
|
|
121
|
+
return Object.entries(fields)
|
|
122
|
+
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v ?? '')}`)
|
|
123
|
+
.join('&');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Combina cookies existentes con nuevas del header Set-Cookie
|
|
128
|
+
* @private
|
|
129
|
+
*/
|
|
130
|
+
_mergeCookies(current, setCookieHeader) {
|
|
131
|
+
const jar = new Map();
|
|
132
|
+
const addCookie = (cookieStr) => {
|
|
133
|
+
const [pair] = cookieStr.split(';');
|
|
134
|
+
const [name, value] = pair.split('=');
|
|
135
|
+
if (name) jar.set(name.trim(), (value || '').trim());
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
if (current) {
|
|
139
|
+
current.split(';').forEach((c) => addCookie(c));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (Array.isArray(setCookieHeader)) {
|
|
143
|
+
setCookieHeader.forEach((c) => addCookie(c));
|
|
144
|
+
} else if (setCookieHeader) {
|
|
145
|
+
addCookie(setCookieHeader);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return Array.from(jar.entries())
|
|
149
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
150
|
+
.join('; ');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Realiza una petición HTTP
|
|
155
|
+
* @param {string} url - URL de destino
|
|
156
|
+
* @param {Object} options - Opciones de la petición
|
|
157
|
+
* @returns {Promise<Object>}
|
|
158
|
+
*/
|
|
159
|
+
async request(url, options = {}) {
|
|
160
|
+
const res = await got(url, {
|
|
161
|
+
method: options.method || 'GET',
|
|
162
|
+
headers: {
|
|
163
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
164
|
+
...(this.cookieJar ? { Cookie: this.cookieJar } : {}),
|
|
165
|
+
...(options.headers || {}),
|
|
166
|
+
},
|
|
167
|
+
body: options.body,
|
|
168
|
+
followRedirect: false,
|
|
169
|
+
throwHttpErrors: false,
|
|
170
|
+
https: this.tlsOptions || { rejectUnauthorized: false },
|
|
171
|
+
responseType: 'buffer', // Obtener como buffer para manejar encoding
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
this.cookieJar = this._mergeCookies(this.cookieJar, res.headers['set-cookie']);
|
|
175
|
+
|
|
176
|
+
// Detectar encoding del Content-Type y convertir correctamente
|
|
177
|
+
let bodyStr;
|
|
178
|
+
const contentType = res.headers['content-type'] || '';
|
|
179
|
+
const buffer = res.body;
|
|
180
|
+
|
|
181
|
+
// El SII de Chile usa ISO-8859-1 para TODO su contenido (HTML, XML, text/plain, octet-stream, etc.)
|
|
182
|
+
// Forzar ISO-8859-1 para cualquier respuesta de sii.cl que no especifique UTF-8
|
|
183
|
+
const isSiiUrl = url.includes('sii.cl');
|
|
184
|
+
const hasUtf8 = contentType.toLowerCase().includes('utf-8');
|
|
185
|
+
const forceIso = contentType.toLowerCase().includes('iso-8859-1') ||
|
|
186
|
+
contentType.toLowerCase().includes('latin1');
|
|
187
|
+
|
|
188
|
+
// Aplicar ISO-8859-1 si:
|
|
189
|
+
// 1. El Content-Type especifica ISO-8859-1/latin1, O
|
|
190
|
+
// 2. Es una URL del SII y NO especifica UTF-8 (incluyendo octet-stream, text/*, xml, etc.)
|
|
191
|
+
if (forceIso || (isSiiUrl && !hasUtf8)) {
|
|
192
|
+
// SII usa ISO-8859-1, convertir cada byte a su codepoint Unicode correspondiente
|
|
193
|
+
// ISO-8859-1 es un subconjunto directo de Unicode (codepoints 0-255)
|
|
194
|
+
bodyStr = '';
|
|
195
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
196
|
+
bodyStr += String.fromCharCode(buffer[i]);
|
|
197
|
+
}
|
|
198
|
+
} else {
|
|
199
|
+
bodyStr = buffer.toString('utf8');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
status: res.statusCode,
|
|
204
|
+
headers: res.headers,
|
|
205
|
+
body: bodyStr,
|
|
206
|
+
rawBody: res.body, // Buffer original por si se necesita
|
|
207
|
+
url: res.url,
|
|
208
|
+
cookieJar: this.cookieJar,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Sigue redirecciones HTTP
|
|
214
|
+
* @param {Object} initial - Respuesta inicial
|
|
215
|
+
* @param {number} maxRedirects - Máximo de redirecciones
|
|
216
|
+
* @returns {Promise<Object>}
|
|
217
|
+
*/
|
|
218
|
+
async followRedirects(initial, maxRedirects = 8) {
|
|
219
|
+
let response = initial;
|
|
220
|
+
let redirects = 0;
|
|
221
|
+
let lastLocationUrl = null;
|
|
222
|
+
|
|
223
|
+
while ([301, 302, 303, 307, 308].includes(response.status) && response.headers.location && redirects < maxRedirects) {
|
|
224
|
+
const nextUrl = new URL(response.headers.location, response.url).toString();
|
|
225
|
+
lastLocationUrl = nextUrl;
|
|
226
|
+
response = await this.request(nextUrl, { method: 'GET' });
|
|
227
|
+
redirects += 1;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return { response, lastLocationUrl };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Realiza login con certificado digital
|
|
235
|
+
* @param {string} lastLocationUrl - URL de redirección del login
|
|
236
|
+
* @returns {Promise<Object>}
|
|
237
|
+
*/
|
|
238
|
+
async loginWithCertificate(lastLocationUrl) {
|
|
239
|
+
if (!lastLocationUrl || !this.tlsOptions) {
|
|
240
|
+
return { success: false, response: null };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const locationUrl = new URL(lastLocationUrl);
|
|
244
|
+
const referencia = locationUrl.search ? decodeURIComponent(locationUrl.search.slice(1)) : '';
|
|
245
|
+
|
|
246
|
+
if (!referencia) {
|
|
247
|
+
return { success: false, response: null };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const loginUrl = `https://herculesr.sii.cl/cgi_AUT2000/CAutInicio.cgi?${referencia}`;
|
|
251
|
+
const loginBody = SiiSession.formEncode({ referencia });
|
|
252
|
+
|
|
253
|
+
const loginResponse = await this.request(loginUrl, {
|
|
254
|
+
method: 'POST',
|
|
255
|
+
headers: {
|
|
256
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
257
|
+
'Content-Length': Buffer.byteLength(loginBody),
|
|
258
|
+
Referer: loginUrl,
|
|
259
|
+
},
|
|
260
|
+
body: loginBody,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const redirected = await this.followRedirects(loginResponse);
|
|
264
|
+
return { success: true, response: redirected.response };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Asegura una sesión autenticada para acceder a una página
|
|
269
|
+
* @param {string} targetPath - Ruta del recurso
|
|
270
|
+
* @returns {Promise<Object>}
|
|
271
|
+
*/
|
|
272
|
+
async ensureSession(targetPath) {
|
|
273
|
+
const targetUrl = `https://${this.baseHost}${targetPath}`;
|
|
274
|
+
let response = await this.request(targetUrl, { method: 'GET' });
|
|
275
|
+
|
|
276
|
+
const redirected = await this.followRedirects(response);
|
|
277
|
+
response = redirected.response;
|
|
278
|
+
|
|
279
|
+
// Detectar bloqueo por demasiadas sesiones
|
|
280
|
+
if (response.body && response.body.includes('superado el m')) {
|
|
281
|
+
const errorMsg = 'SII: Demasiadas sesiones abiertas. Espera ~30 min o cierra sesión manualmente en el portal SII.';
|
|
282
|
+
console.error(`\n❌ ${errorMsg}\n`);
|
|
283
|
+
throw new Error(errorMsg);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Si requiere autenticación
|
|
287
|
+
if (response.body && response.body.includes('Autenticaci')) {
|
|
288
|
+
const certLogin = await this.loginWithCertificate(redirected.lastLocationUrl);
|
|
289
|
+
if (certLogin.success && certLogin.response) {
|
|
290
|
+
response = certLogin.response;
|
|
291
|
+
|
|
292
|
+
// Verificar si el login resultó en bloqueo por sesiones
|
|
293
|
+
if (response.body && response.body.includes('superado el m')) {
|
|
294
|
+
const errorMsg = 'SII: Demasiadas sesiones abiertas. Espera ~30 min o cierra sesión manualmente en el portal SII.';
|
|
295
|
+
console.error(`\n❌ ${errorMsg}\n`);
|
|
296
|
+
throw new Error(errorMsg);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Si hay redirección a af_anular1
|
|
302
|
+
if (response.body && response.body.includes('/cvc_cgi/dte/af_anular1')) {
|
|
303
|
+
const continued = await this.request(targetUrl, { method: 'GET' });
|
|
304
|
+
const continuedResult = await this.followRedirects(continued);
|
|
305
|
+
return continuedResult.response;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return response;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Envía un formulario HTTP
|
|
313
|
+
* @param {string} action - URL o path de acción
|
|
314
|
+
* @param {Object} fields - Campos del formulario
|
|
315
|
+
* @param {string} [referer] - URL de referencia
|
|
316
|
+
* @returns {Promise<Object>}
|
|
317
|
+
*/
|
|
318
|
+
async submitForm(action, fields, referer = null) {
|
|
319
|
+
const url = new URL(action, `https://${this.baseHost}`).toString();
|
|
320
|
+
const body = SiiSession.formEncode(fields);
|
|
321
|
+
|
|
322
|
+
return this.request(url, {
|
|
323
|
+
method: 'POST',
|
|
324
|
+
headers: {
|
|
325
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
326
|
+
'Content-Length': Buffer.byteLength(body),
|
|
327
|
+
...(referer ? { Referer: referer } : {}),
|
|
328
|
+
},
|
|
329
|
+
body,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Resetea la sesión
|
|
335
|
+
*/
|
|
336
|
+
reset() {
|
|
337
|
+
this.cookieJar = '';
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Retorna el host base según ambiente
|
|
342
|
+
* @returns {string}
|
|
343
|
+
*/
|
|
344
|
+
getBaseHost() {
|
|
345
|
+
return this.baseHost;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Guarda la sesión actual en un archivo JSON
|
|
350
|
+
* @param {string} filePath - Ruta del archivo donde guardar
|
|
351
|
+
*/
|
|
352
|
+
saveSession(filePath) {
|
|
353
|
+
const fs = require('fs');
|
|
354
|
+
const sessionData = {
|
|
355
|
+
cookieJar: this.cookieJar,
|
|
356
|
+
baseHost: this.baseHost,
|
|
357
|
+
savedAt: Date.now(),
|
|
358
|
+
expiresAt: Date.now() + (25 * 60 * 1000), // 25 minutos de validez
|
|
359
|
+
};
|
|
360
|
+
fs.writeFileSync(filePath, JSON.stringify(sessionData, null, 2), 'utf8');
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Carga una sesión desde un archivo JSON
|
|
365
|
+
* @param {string} filePath - Ruta del archivo de sesión
|
|
366
|
+
* @returns {boolean} - true si la sesión fue cargada exitosamente y es válida
|
|
367
|
+
*/
|
|
368
|
+
loadSession(filePath) {
|
|
369
|
+
const fs = require('fs');
|
|
370
|
+
try {
|
|
371
|
+
if (!fs.existsSync(filePath)) {
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
375
|
+
|
|
376
|
+
// Verificar que la sesión no haya expirado
|
|
377
|
+
if (data.expiresAt && Date.now() > data.expiresAt) {
|
|
378
|
+
console.log('Sesión SII expirada, se requiere nuevo login');
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Verificar que el host coincida
|
|
383
|
+
if (data.baseHost && data.baseHost !== this.baseHost) {
|
|
384
|
+
console.log('Host SII no coincide, se requiere nuevo login');
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
this.cookieJar = data.cookieJar || '';
|
|
389
|
+
console.log('Sesión SII cargada desde archivo');
|
|
390
|
+
return true;
|
|
391
|
+
} catch (err) {
|
|
392
|
+
console.log('Error cargando sesión SII:', err.message);
|
|
393
|
+
return false;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Verifica si existe una sesión guardada y es válida
|
|
399
|
+
* @param {string} filePath - Ruta del archivo de sesión
|
|
400
|
+
* @returns {boolean}
|
|
401
|
+
*/
|
|
402
|
+
static isSessionValid(filePath) {
|
|
403
|
+
const fs = require('fs');
|
|
404
|
+
try {
|
|
405
|
+
if (!fs.existsSync(filePath)) {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
409
|
+
return data.expiresAt && Date.now() < data.expiresAt;
|
|
410
|
+
} catch {
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Elimina el archivo de sesión
|
|
417
|
+
* @param {string} filePath - Ruta del archivo de sesión
|
|
418
|
+
*/
|
|
419
|
+
static clearSession(filePath) {
|
|
420
|
+
const fs = require('fs');
|
|
421
|
+
try {
|
|
422
|
+
if (fs.existsSync(filePath)) {
|
|
423
|
+
fs.unlinkSync(filePath);
|
|
424
|
+
}
|
|
425
|
+
} catch {
|
|
426
|
+
// Ignorar errores
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// Utilidades de parsing HTML
|
|
432
|
+
SiiSession.extractFormAction = function(html) {
|
|
433
|
+
const match = html.match(/<form[^>]*action="([^"]+)"/i);
|
|
434
|
+
return match ? match[1] : null;
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
SiiSession.extractFormActionByName = function(html, formName) {
|
|
438
|
+
if (!formName) return SiiSession.extractFormAction(html);
|
|
439
|
+
const regex = new RegExp(`<form[^>]*name="${formName}"[^>]*action="([^"]+)"`, 'i');
|
|
440
|
+
const match = String(html || '').match(regex);
|
|
441
|
+
return match ? match[1] : null;
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
SiiSession.extractInputValues = function(html) {
|
|
445
|
+
const inputs = {};
|
|
446
|
+
const regex = /<input[^>]+>/gi;
|
|
447
|
+
const matches = html.match(regex) || [];
|
|
448
|
+
matches.forEach((tag) => {
|
|
449
|
+
const nameMatch = tag.match(/name\s*=\s*"([^"]+)"/i);
|
|
450
|
+
const valueMatch = tag.match(/value\s*=\s*"([^"]*)"/i);
|
|
451
|
+
if (nameMatch) {
|
|
452
|
+
inputs[nameMatch[1]] = valueMatch ? valueMatch[1] : '';
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
return inputs;
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
SiiSession.extractFormInputsByName = function(html, formName = null) {
|
|
459
|
+
const formRegex = formName
|
|
460
|
+
? new RegExp(`<form[^>]*name="${formName}"[^>]*>[\\s\\S]*?<\\/form>`, 'i')
|
|
461
|
+
: /<form[^>]*>[\s\S]*?<\/form>/i;
|
|
462
|
+
const match = String(html || '').match(formRegex);
|
|
463
|
+
if (!match) return {};
|
|
464
|
+
return SiiSession.extractInputValues(match[0]);
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
SiiSession.extractInputTags = function(html) {
|
|
468
|
+
const tags = [];
|
|
469
|
+
const regex = /<input[^>]+>/gi;
|
|
470
|
+
const matches = html.match(regex) || [];
|
|
471
|
+
matches.forEach((tag) => {
|
|
472
|
+
const nameMatch = tag.match(/name\s*=\s*"([^"]+)"/i);
|
|
473
|
+
const valueMatch = tag.match(/value\s*=\s*"([^"]*)"/i);
|
|
474
|
+
const typeMatch = tag.match(/type\s*=\s*"([^"]+)"/i);
|
|
475
|
+
const onClickMatch = tag.match(/onClick\s*=\s*"([^"]+)"/i);
|
|
476
|
+
tags.push({
|
|
477
|
+
name: nameMatch ? nameMatch[1] : null,
|
|
478
|
+
value: valueMatch ? valueMatch[1] : null,
|
|
479
|
+
type: typeMatch ? typeMatch[1] : null,
|
|
480
|
+
onClick: onClickMatch ? onClickMatch[1] : null,
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
return tags;
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
SiiSession.stripHtml = function(value) {
|
|
487
|
+
return String(value || '')
|
|
488
|
+
.replace(/<[^>]+>/g, ' ')
|
|
489
|
+
.replace(/ /gi, ' ')
|
|
490
|
+
.replace(/\s+/g, ' ')
|
|
491
|
+
.trim();
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
SiiSession.parseIntFromText = function(value) {
|
|
495
|
+
const match = String(value || '').match(/(\d{1,})/);
|
|
496
|
+
return match ? Number(match[1]) : null;
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
module.exports = SiiSession;
|