@devlas/dte-sii 2.12.9 → 2.12.12
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 +17 -0
- package/SiiSession.js +38 -3
- package/package.json +1 -1
- package/utils/pfx.js +57 -1
package/CafSolicitor.js
CHANGED
|
@@ -224,6 +224,8 @@ class CafSolicitor {
|
|
|
224
224
|
// parsear <form action> + hidden fields → submit con esos campos + los propios.
|
|
225
225
|
const authResponse = await this.session.ensureSession('/cvc_cgi/dte/of_solicita_folios');
|
|
226
226
|
|
|
227
|
+
this._saveDebug(debugDir, 'step0-ensureSession.html', authResponse.body || '');
|
|
228
|
+
|
|
227
229
|
let response;
|
|
228
230
|
if (this._requiresAuthentication(authResponse.body)) {
|
|
229
231
|
// Sesión sigue inválida tras ensureSession — el chequeo de más abajo
|
|
@@ -235,6 +237,8 @@ class CafSolicitor {
|
|
|
235
237
|
response = await this.session.submitForm(formAction, { ...hiddenFields, ...fields });
|
|
236
238
|
}
|
|
237
239
|
|
|
240
|
+
this._saveDebug(debugDir, 'step1-submit.html', response.body || '');
|
|
241
|
+
|
|
238
242
|
// Guardar sesión para reutilización
|
|
239
243
|
if (this.sessionPath) {
|
|
240
244
|
this.session.saveSession(this.sessionPath);
|
|
@@ -371,6 +375,19 @@ class CafSolicitor {
|
|
|
371
375
|
*/
|
|
372
376
|
async _processMultiStepFlow(response, rut, dv, tipoDte, cantidad, debugDir) {
|
|
373
377
|
let currentHtml = response.body || '';
|
|
378
|
+
const realFormAction = SiiSession.extractFormAction(currentHtml);
|
|
379
|
+
|
|
380
|
+
// Si el <form> real ya apunta a of_confirma_folio, el SII preseleccionó
|
|
381
|
+
// COD_DOCTO server-side (coincide con el tipoDte enviado en el paso 1) y no
|
|
382
|
+
// hace falta pasar por of_solicita_folios_dcto. Ese endpoint solo se dispara
|
|
383
|
+
// vía JS (changeRegregion) si el usuario cambia el <select> a mano — su sola
|
|
384
|
+
// presencia como string dentro de un <script> no significa que sea el
|
|
385
|
+
// próximo paso real. Ir directo a _processStep3, que ya arma COD_DOCTO/
|
|
386
|
+
// CANT_DOCTOS explícitamente (extractInputValues no lee <select>, así que
|
|
387
|
+
// dejarlo entrar a la rama de abajo pierde el tipo de documento).
|
|
388
|
+
if (realFormAction && realFormAction.includes('of_confirma_folio')) {
|
|
389
|
+
return this._processStep3(response, rut, dv, tipoDte, cantidad, debugDir);
|
|
390
|
+
}
|
|
374
391
|
|
|
375
392
|
// Paso 2: of_solicita_folios_dcto
|
|
376
393
|
if (currentHtml.includes('of_solicita_folios_dcto')) {
|
package/SiiSession.js
CHANGED
|
@@ -14,6 +14,8 @@ const {
|
|
|
14
14
|
loadPfxFromBuffer,
|
|
15
15
|
loadPfxFromFile,
|
|
16
16
|
createTlsOptions,
|
|
17
|
+
createTlsAgent,
|
|
18
|
+
createTlsAgentFromPem,
|
|
17
19
|
validateAmbiente,
|
|
18
20
|
getHost,
|
|
19
21
|
createScopedLogger,
|
|
@@ -44,6 +46,10 @@ class SiiSession {
|
|
|
44
46
|
this.baseHost = getHost(this.ambiente);
|
|
45
47
|
this.cookieJar = '';
|
|
46
48
|
this.tlsOptions = null;
|
|
49
|
+
// Agent nativo con el certificado + flags TLS legacy del SII. got no reenvía
|
|
50
|
+
// secureOptions/maxVersion a través de su opción `https` bajo ningún nombre de
|
|
51
|
+
// clave, así que estos flags solo se pueden aplicar vía un https.Agent nativo.
|
|
52
|
+
this.tlsAgent = null;
|
|
47
53
|
|
|
48
54
|
// Configurar TLS desde certificado
|
|
49
55
|
if (options.certificado) {
|
|
@@ -61,15 +67,19 @@ class SiiSession {
|
|
|
61
67
|
*/
|
|
62
68
|
_configureTlsFromCertificado(certificado) {
|
|
63
69
|
try {
|
|
70
|
+
const keyPem = certificado.getPrivateKeyPEM();
|
|
71
|
+
const certPem = certificado.getCertificatePEM();
|
|
64
72
|
this.tlsOptions = {
|
|
65
|
-
key:
|
|
66
|
-
cert:
|
|
67
|
-
certificate:
|
|
73
|
+
key: keyPem,
|
|
74
|
+
cert: certPem,
|
|
75
|
+
certificate: certPem,
|
|
68
76
|
rejectUnauthorized: false,
|
|
69
77
|
};
|
|
78
|
+
this.tlsAgent = createTlsAgentFromPem(keyPem, certPem);
|
|
70
79
|
} catch (error) {
|
|
71
80
|
log.error('Error configurando TLS desde certificado:', error.message);
|
|
72
81
|
this.tlsOptions = null;
|
|
82
|
+
this.tlsAgent = null;
|
|
73
83
|
}
|
|
74
84
|
}
|
|
75
85
|
|
|
@@ -81,9 +91,11 @@ class SiiSession {
|
|
|
81
91
|
try {
|
|
82
92
|
const pfxData = loadPfxFromBuffer(pfxBuffer, password);
|
|
83
93
|
this.tlsOptions = createTlsOptions(pfxData);
|
|
94
|
+
this.tlsAgent = createTlsAgent(pfxData);
|
|
84
95
|
} catch (error) {
|
|
85
96
|
log.error('Error configurando TLS desde PFX:', error.message);
|
|
86
97
|
this.tlsOptions = null;
|
|
98
|
+
this.tlsAgent = null;
|
|
87
99
|
}
|
|
88
100
|
}
|
|
89
101
|
|
|
@@ -95,9 +107,11 @@ class SiiSession {
|
|
|
95
107
|
try {
|
|
96
108
|
const pfxData = loadPfxFromFile(pfxPath, password);
|
|
97
109
|
this.tlsOptions = createTlsOptions(pfxData);
|
|
110
|
+
this.tlsAgent = createTlsAgent(pfxData);
|
|
98
111
|
} catch (error) {
|
|
99
112
|
log.error('Error configurando TLS desde archivo PFX:', error.message);
|
|
100
113
|
this.tlsOptions = null;
|
|
114
|
+
this.tlsAgent = null;
|
|
101
115
|
}
|
|
102
116
|
}
|
|
103
117
|
|
|
@@ -215,6 +229,11 @@ class SiiSession {
|
|
|
215
229
|
followRedirect: false,
|
|
216
230
|
throwHttpErrors: false,
|
|
217
231
|
https: this.tlsOptions || { rejectUnauthorized: false },
|
|
232
|
+
// got remapea `https` a un set fijo de claves y NO reenvía secureOptions/
|
|
233
|
+
// maxVersion bajo ningún nombre — sin el Agent nativo aquí, el certificado
|
|
234
|
+
// cliente se autentica con handshake TLS incompleto contra el SII (ver
|
|
235
|
+
// utils/pfx.js createTlsAgent).
|
|
236
|
+
...(this.tlsAgent ? { agent: { https: this.tlsAgent } } : {}),
|
|
218
237
|
responseType: 'buffer',
|
|
219
238
|
timeout: { request: options.timeoutMs ?? 20000 },
|
|
220
239
|
});
|
|
@@ -437,6 +456,22 @@ class SiiSession {
|
|
|
437
456
|
}
|
|
438
457
|
}
|
|
439
458
|
|
|
459
|
+
// Pantalla "ESCOJA COMO DESEA INGRESAR" — aparece cuando el certificado está
|
|
460
|
+
// habilitado para representación electrónica de otros RUT. No es un fallo de
|
|
461
|
+
// autenticación (aunque su <title> genérico "Autenticación" coincida con el
|
|
462
|
+
// check de más arriba y por eso el caller podría malinterpretarlo como tal):
|
|
463
|
+
// hay que seguir el link "Continuar" (GET al mismo recurso) para llegar al
|
|
464
|
+
// formulario real.
|
|
465
|
+
if (response.body && response.body.includes('ESCOJA COMO DESEA INGRESAR')) {
|
|
466
|
+
const continuarMatch = response.body.match(/<a\s+href="([^"]+)"[^>]*>\s*Continuar\s*<\/a>/i);
|
|
467
|
+
if (continuarMatch) {
|
|
468
|
+
const continuarUrl = new URL(continuarMatch[1], targetUrl).toString();
|
|
469
|
+
const continuado = await this.request(continuarUrl, { method: 'GET' });
|
|
470
|
+
const continuadoResult = await this.followRedirects(continuado);
|
|
471
|
+
response = continuadoResult.response;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
440
475
|
// Si hay redirección a af_anular1
|
|
441
476
|
if (response.body && response.body.includes('/cvc_cgi/dte/af_anular1')) {
|
|
442
477
|
const continued = await this.request(targetUrl, { method: 'GET' });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devlas/dte-sii",
|
|
3
|
-
"version": "2.12.
|
|
3
|
+
"version": "2.12.12",
|
|
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",
|
package/utils/pfx.js
CHANGED
|
@@ -10,9 +10,24 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
const fs = require('fs');
|
|
13
|
+
const https = require('https');
|
|
14
|
+
const crypto = require('crypto');
|
|
13
15
|
const forge = require('node-forge');
|
|
14
16
|
const { certError, ERROR_CODES } = require('./error');
|
|
15
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Flags TLS que la infraestructura legacy del SII requiere para completar la
|
|
20
|
+
* autenticación con certificado cliente (mismo set que SiiPortalAuth.SII_TLS_OPTS).
|
|
21
|
+
* Sin `maxVersion: 'TLSv1.2'` + renegociación insegura habilitada, el handshake
|
|
22
|
+
* de client-cert contra los servidores del SII no se completa correctamente.
|
|
23
|
+
*/
|
|
24
|
+
const SII_LEGACY_TLS_FLAGS = {
|
|
25
|
+
maxVersion: 'TLSv1.2',
|
|
26
|
+
secureOptions:
|
|
27
|
+
crypto.constants.SSL_OP_ALLOW_UNSAFE_LEGACY_RENEGOTIATION |
|
|
28
|
+
crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT,
|
|
29
|
+
};
|
|
30
|
+
|
|
16
31
|
/**
|
|
17
32
|
* Resultado de cargar un PFX
|
|
18
33
|
* @typedef {Object} PfxData
|
|
@@ -217,17 +232,56 @@ function getDaysUntilExpiry(notAfter) {
|
|
|
217
232
|
|
|
218
233
|
/**
|
|
219
234
|
* Crear opciones TLS desde datos PFX
|
|
235
|
+
*
|
|
236
|
+
* IMPORTANTE: `got` (usado por SiiSession) remapea su opción `https` a un set
|
|
237
|
+
* fijo de claves y solo reconoce `certificate` para el certificado cliente —
|
|
238
|
+
* NO `cert` (ver got/dist/source/core/index.js, "HTTPS options remapping").
|
|
239
|
+
* Se incluyen ambas claves (`cert` para consumidores que usan `https` nativo,
|
|
240
|
+
* `certificate` para got) para que el certificado se envíe en ambos casos.
|
|
241
|
+
*
|
|
220
242
|
* @param {PfxData} pfxData - Datos del PFX
|
|
221
243
|
* @returns {Object} Opciones TLS para https/got
|
|
222
244
|
*/
|
|
223
245
|
function createTlsOptions(pfxData) {
|
|
246
|
+
const certPem = pfxData.certificateChainPem || pfxData.certificatePem;
|
|
224
247
|
return {
|
|
225
248
|
key: pfxData.privateKeyPem,
|
|
226
|
-
cert:
|
|
249
|
+
cert: certPem,
|
|
250
|
+
certificate: certPem,
|
|
227
251
|
rejectUnauthorized: false,
|
|
228
252
|
};
|
|
229
253
|
}
|
|
230
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Crea un https.Agent nativo con el certificado cliente + los flags TLS legacy
|
|
257
|
+
* que el SII requiere. `got` no reenvía `secureOptions`/`maxVersion` a través de
|
|
258
|
+
* su opción `https` bajo ningún nombre de clave — la única forma de aplicarlos
|
|
259
|
+
* es con un Agent nativo pasado como `agent.https` en las opciones de got.
|
|
260
|
+
*
|
|
261
|
+
* @param {PfxData} pfxData - Datos del PFX
|
|
262
|
+
* @returns {https.Agent}
|
|
263
|
+
*/
|
|
264
|
+
function createTlsAgent(pfxData) {
|
|
265
|
+
return createTlsAgentFromPem(pfxData.privateKeyPem, pfxData.certificateChainPem || pfxData.certificatePem);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Crea un https.Agent nativo desde un par key/cert PEM ya extraído (ej. desde
|
|
270
|
+
* una instancia de Certificado), con los mismos flags legacy que createTlsAgent.
|
|
271
|
+
*
|
|
272
|
+
* @param {string} keyPem
|
|
273
|
+
* @param {string} certPem
|
|
274
|
+
* @returns {https.Agent}
|
|
275
|
+
*/
|
|
276
|
+
function createTlsAgentFromPem(keyPem, certPem) {
|
|
277
|
+
return new https.Agent({
|
|
278
|
+
...SII_LEGACY_TLS_FLAGS,
|
|
279
|
+
key: keyPem,
|
|
280
|
+
cert: certPem,
|
|
281
|
+
rejectUnauthorized: false,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
231
285
|
module.exports = {
|
|
232
286
|
loadPfxFromBuffer,
|
|
233
287
|
loadPfxFromFile,
|
|
@@ -236,4 +290,6 @@ module.exports = {
|
|
|
236
290
|
isCertificateExpired,
|
|
237
291
|
getDaysUntilExpiry,
|
|
238
292
|
createTlsOptions,
|
|
293
|
+
createTlsAgent,
|
|
294
|
+
createTlsAgentFromPem,
|
|
239
295
|
};
|