@devlas/dte-sii 2.11.0 → 2.12.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 +33 -5
- package/LICENSE +27 -27
- package/SiiSession.js +56 -17
- package/WsReclamo.js +434 -434
- package/cert/comunaOficina.js +458 -458
- package/cert/index.js +122 -122
- package/cert/types.js +328 -328
- package/dte-sii.d.ts +3 -0
- package/package.json +1 -1
- package/test-qdetestlibro.js +174 -174
- package/utils/pfx.js +57 -21
- package/test-muestras.js +0 -180
package/test-qdetestlibro.js
CHANGED
|
@@ -1,174 +1,174 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
/**
|
|
3
|
-
* test-qdetestlibro.js
|
|
4
|
-
*
|
|
5
|
-
* Prueba los endpoints del portal SII para consultar libros electrónicos:
|
|
6
|
-
* 1. QEstLibro — lista todos los libros del año y extrae los Códigos
|
|
7
|
-
* 2. QDetEstLibro — detalle de cada envío (TrackId, estado, etc.)
|
|
8
|
-
*
|
|
9
|
-
* Uso:
|
|
10
|
-
* node test-qdetestlibro.js [year=2026] [periodo=2026-04]
|
|
11
|
-
*
|
|
12
|
-
* Requiere que la sesión exista o la crea automáticamente.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
const path = require('path');
|
|
16
|
-
const fs = require('fs');
|
|
17
|
-
const SiiCertificacion = require('./SiiCertificacion.js');
|
|
18
|
-
|
|
19
|
-
// ─── Configuración ────────────────────────────────────────────────────────────
|
|
20
|
-
const PFX_PATH = path.resolve(__dirname, '../devlas-cloud-api-node/secret/19925444-8.pfx');
|
|
21
|
-
const PFX_PASS = 'Lsr12345';
|
|
22
|
-
const RUT_EMPRESA = '78206276';
|
|
23
|
-
const DV_EMPRESA = 'K';
|
|
24
|
-
const SESSION_PATH = path.resolve(__dirname, '../devlas-cloud-api-node/debug/cert-v2/session.json');
|
|
25
|
-
|
|
26
|
-
const YEAR = process.argv.find(a => a.startsWith('year='))?.split('=')[1] || '2026';
|
|
27
|
-
const PERIODO = process.argv.find(a => a.startsWith('periodo='))?.split('=')[1] || null; // null = todos
|
|
28
|
-
|
|
29
|
-
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
30
|
-
async function main() {
|
|
31
|
-
console.log('=== test-qdetestlibro.js ===');
|
|
32
|
-
console.log('PFX:', PFX_PATH);
|
|
33
|
-
console.log('RUT:', `${RUT_EMPRESA}-${DV_EMPRESA}`);
|
|
34
|
-
console.log('Year:', YEAR, '| Filtro período:', PERIODO || '(todos)');
|
|
35
|
-
|
|
36
|
-
const cert = new SiiCertificacion({
|
|
37
|
-
pfxPath: PFX_PATH,
|
|
38
|
-
pfxPassword: PFX_PASS,
|
|
39
|
-
rutEmpresa: RUT_EMPRESA,
|
|
40
|
-
dvEmpresa: DV_EMPRESA,
|
|
41
|
-
sessionPath: SESSION_PATH,
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
// ── 1. Asegurar sesión portal en subsistema /cgi_dte/UPL/ ─────────────────
|
|
45
|
-
// DTEauth?7 es la página del formulario de búsqueda de libros.
|
|
46
|
-
// CSESSIONID es el token de sesión del portal SII. Lo necesitamos válido.
|
|
47
|
-
console.log('\n[1] Autenticando en /cgi_dte/UPL/DTEauth?7...');
|
|
48
|
-
// Parchar request para capturar Set-Cookie crudos de DTEauth?7
|
|
49
|
-
const origRequest = cert.session.request.bind(cert.session);
|
|
50
|
-
let lastRawHeaders = null;
|
|
51
|
-
cert.session.request = async function(url, opts) {
|
|
52
|
-
const resp = await origRequest(url, opts);
|
|
53
|
-
if (url.includes('DTEauth')) {
|
|
54
|
-
lastRawHeaders = resp.headers;
|
|
55
|
-
}
|
|
56
|
-
return resp;
|
|
57
|
-
};
|
|
58
|
-
const authResp = await cert.session.ensureSession('/cgi_dte/UPL/DTEauth?7');
|
|
59
|
-
cert.session.request = origRequest; // restaurar
|
|
60
|
-
console.log(' Status DTEauth:', authResp?.status);
|
|
61
|
-
if (lastRawHeaders) {
|
|
62
|
-
const sc = lastRawHeaders['set-cookie'];
|
|
63
|
-
console.log(' Set-Cookie DTEauth?7:', JSON.stringify(sc));
|
|
64
|
-
}
|
|
65
|
-
console.log(' CSESSIONID en jar:', cert.session.cookieJar?.match(/CSESSIONID=[^;]+/)?.[0] || '(none)');
|
|
66
|
-
fs.writeFileSync(path.join(__dirname, 'test-output', 'dteauth7.html'), authResp?.body || '', 'latin1');
|
|
67
|
-
|
|
68
|
-
// ── TEST DIRECTO: llamar QDetEstLibro sin QEstLibro de por medio ──────────
|
|
69
|
-
console.log('\n[TEST DIRECTO] QDetEstLibro sin pasar por QEstLibro...');
|
|
70
|
-
const urlDetDirect = `https://maullin.sii.cl/cgi_dte/UPL/QDetEstLibro` +
|
|
71
|
-
`?Codigo=COMPRA-772220&rutC=78206276&dvC=K&periodo=2026-04`;
|
|
72
|
-
const directResp = await cert.session.request(urlDetDirect);
|
|
73
|
-
console.log(' Status:', directResp.status);
|
|
74
|
-
const directBody = directResp.body;
|
|
75
|
-
if (directBody.includes('SESION HA EXPIRADO')) {
|
|
76
|
-
console.warn(' [WARN] TAMBIÉN falla sin QEstLibro — es el CSESSIONID o la sesión');
|
|
77
|
-
} else if (directBody.includes('AUTORIZADO')) {
|
|
78
|
-
console.warn(' [WARN] Error de autorización');
|
|
79
|
-
console.log(directBody.slice(0, 500));
|
|
80
|
-
} else {
|
|
81
|
-
console.log(' OK! Funciona directamente');
|
|
82
|
-
console.log(directBody.slice(0, 1000));
|
|
83
|
-
}
|
|
84
|
-
fs.writeFileSync(path.join(__dirname, 'test-output', 'qdetestlibro-direct.html'), directBody, 'latin1');
|
|
85
|
-
|
|
86
|
-
// ── 2. Llamar QEstLibro ────────────────────────────────────────────────────
|
|
87
|
-
const urlLista = `https://maullin.sii.cl/cgi_dte/UPL/QEstLibro` +
|
|
88
|
-
`?rutCompany=${RUT_EMPRESA}&dvCompany=${DV_EMPRESA}&TrackId=&year=${YEAR}&month=00&tipo=TODOS`;
|
|
89
|
-
|
|
90
|
-
console.log('\n[2] GET', urlLista);
|
|
91
|
-
const listaResp = await cert.session.request(urlLista);
|
|
92
|
-
console.log(' Status:', listaResp.status);
|
|
93
|
-
console.log(' Set-Cookie headers:', listaResp.headers?.['set-cookie'] || '(none)');
|
|
94
|
-
console.log(' Cookies post-QEstLibro:', cert.session.cookieJar?.slice(0, 250) + '...');
|
|
95
|
-
|
|
96
|
-
if (listaResp.body.includes('SESION HA EXPIRADO')) {
|
|
97
|
-
console.error(' [ERR] Sesión expirada en QEstLibro — revisar ensureSession');
|
|
98
|
-
process.exit(1);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Guardar HTML para inspección
|
|
102
|
-
const htmlListaPath = path.join(__dirname, 'test-output', 'qestlibro.html');
|
|
103
|
-
fs.mkdirSync(path.dirname(htmlListaPath), { recursive: true });
|
|
104
|
-
fs.writeFileSync(htmlListaPath, listaResp.body, 'latin1');
|
|
105
|
-
console.log(' HTML guardado en:', htmlListaPath);
|
|
106
|
-
|
|
107
|
-
// ── 3. Parsear la tabla: extraer Código → periodo → operación ──────────────
|
|
108
|
-
// El HTML tiene links tipo: QDetEstLibro?Codigo=VENTA-772219&rutC=...&periodo=2026-04
|
|
109
|
-
// href sin comillas: href=QDetEstLibro?Codigo=X&rutC=Y&dvC=Z&periodo=PPPP>Ver
|
|
110
|
-
// el > cierra el atributo, por eso lo excluimos del grupo de captura
|
|
111
|
-
const linkRegex = /QDetEstLibro\?Codigo=([^&"'\s>]+)&rutC=[^&"'\s>]+&dvC=[^&"'\s>]+&periodo=([^&"'\s>]+)/gi;
|
|
112
|
-
const codigos = {}; // { '2026-04': { VENTA: 'VENTA-772219', COMPRA: 'COMPRA-XXXXX' } }
|
|
113
|
-
let m;
|
|
114
|
-
while ((m = linkRegex.exec(listaResp.body)) !== null) {
|
|
115
|
-
const codigo = m[1];
|
|
116
|
-
const periodo = m[2];
|
|
117
|
-
if (PERIODO && periodo !== PERIODO) continue;
|
|
118
|
-
codigos[periodo] = codigos[periodo] || {};
|
|
119
|
-
const tipoMatch = /^(VENTA|COMPRA|GUIAS?)/i.exec(codigo);
|
|
120
|
-
const tipo = tipoMatch ? tipoMatch[1].toUpperCase() : codigo;
|
|
121
|
-
codigos[periodo][tipo] = codigo;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
console.log('\n Códigos encontrados:');
|
|
125
|
-
if (Object.keys(codigos).length === 0) {
|
|
126
|
-
console.log(' (ninguno — revisar HTML en test-output/qestlibro.html)');
|
|
127
|
-
}
|
|
128
|
-
for (const [p, ops] of Object.entries(codigos)) {
|
|
129
|
-
for (const [op, cod] of Object.entries(ops)) {
|
|
130
|
-
console.log(` ${p} / ${op} → ${cod}`);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// ── 4. Llamar QDetEstLibro para cada código ────────────────────────────────
|
|
135
|
-
// Usamos las MISMAS cookies que obtuvimos de DTEauth?7 (paso 1).
|
|
136
|
-
// NO re-autenticamos: si DTEauth?7 usa tokens one-shot, un segundo call
|
|
137
|
-
// lo consumiría sin beneficio. Las cookies NETSCAPE_LIVEWIRE persisten.
|
|
138
|
-
for (const [periodo, ops] of Object.entries(codigos)) {
|
|
139
|
-
for (const [operacion, codigo] of Object.entries(ops)) {
|
|
140
|
-
const urlDet = `https://maullin.sii.cl/cgi_dte/UPL/QDetEstLibro` +
|
|
141
|
-
`?Codigo=${encodeURIComponent(codigo)}&rutC=${RUT_EMPRESA}&dvC=${DV_EMPRESA}&periodo=${periodo}`;
|
|
142
|
-
|
|
143
|
-
const refererQEstLibro = `https://maullin.sii.cl/cgi_dte/UPL/QEstLibro` +
|
|
144
|
-
`?rutCompany=${RUT_EMPRESA}&dvCompany=${DV_EMPRESA}&TrackId=&year=${YEAR}&month=00&tipo=TODOS`;
|
|
145
|
-
|
|
146
|
-
console.log(`\n[3] QDetEstLibro ${periodo} ${operacion} (${codigo})`);
|
|
147
|
-
console.log(' GET', urlDet);
|
|
148
|
-
|
|
149
|
-
const detResp = await cert.session.request(urlDet, {
|
|
150
|
-
headers: { Referer: refererQEstLibro },
|
|
151
|
-
});
|
|
152
|
-
console.log(' Status:', detResp.status);
|
|
153
|
-
|
|
154
|
-
const outPath = path.join(__dirname, 'test-output', `qdetestlibro-${periodo}-${operacion}.html`);
|
|
155
|
-
fs.writeFileSync(outPath, detResp.body, 'latin1');
|
|
156
|
-
|
|
157
|
-
if (detResp.body.includes('SESION HA EXPIRADO')) {
|
|
158
|
-
console.warn(' [WARN] Sesión expirada — revisar cookies / DTEauth');
|
|
159
|
-
console.log('\n--- BODY (500 chars) ---\n', detResp.body.slice(0, 500));
|
|
160
|
-
} else {
|
|
161
|
-
console.log(' HTML guardado en:', outPath);
|
|
162
|
-
console.log('\n--- BODY ---\n', detResp.body);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
console.log('\n=== Fin ===');
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
main().catch(e => {
|
|
171
|
-
console.error('[FATAL]', e.message);
|
|
172
|
-
console.error(e.stack);
|
|
173
|
-
process.exit(1);
|
|
174
|
-
});
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* test-qdetestlibro.js
|
|
4
|
+
*
|
|
5
|
+
* Prueba los endpoints del portal SII para consultar libros electrónicos:
|
|
6
|
+
* 1. QEstLibro — lista todos los libros del año y extrae los Códigos
|
|
7
|
+
* 2. QDetEstLibro — detalle de cada envío (TrackId, estado, etc.)
|
|
8
|
+
*
|
|
9
|
+
* Uso:
|
|
10
|
+
* node test-qdetestlibro.js [year=2026] [periodo=2026-04]
|
|
11
|
+
*
|
|
12
|
+
* Requiere que la sesión exista o la crea automáticamente.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const SiiCertificacion = require('./SiiCertificacion.js');
|
|
18
|
+
|
|
19
|
+
// ─── Configuración ────────────────────────────────────────────────────────────
|
|
20
|
+
const PFX_PATH = path.resolve(__dirname, '../devlas-cloud-api-node/secret/19925444-8.pfx');
|
|
21
|
+
const PFX_PASS = 'Lsr12345';
|
|
22
|
+
const RUT_EMPRESA = '78206276';
|
|
23
|
+
const DV_EMPRESA = 'K';
|
|
24
|
+
const SESSION_PATH = path.resolve(__dirname, '../devlas-cloud-api-node/debug/cert-v2/session.json');
|
|
25
|
+
|
|
26
|
+
const YEAR = process.argv.find(a => a.startsWith('year='))?.split('=')[1] || '2026';
|
|
27
|
+
const PERIODO = process.argv.find(a => a.startsWith('periodo='))?.split('=')[1] || null; // null = todos
|
|
28
|
+
|
|
29
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
30
|
+
async function main() {
|
|
31
|
+
console.log('=== test-qdetestlibro.js ===');
|
|
32
|
+
console.log('PFX:', PFX_PATH);
|
|
33
|
+
console.log('RUT:', `${RUT_EMPRESA}-${DV_EMPRESA}`);
|
|
34
|
+
console.log('Year:', YEAR, '| Filtro período:', PERIODO || '(todos)');
|
|
35
|
+
|
|
36
|
+
const cert = new SiiCertificacion({
|
|
37
|
+
pfxPath: PFX_PATH,
|
|
38
|
+
pfxPassword: PFX_PASS,
|
|
39
|
+
rutEmpresa: RUT_EMPRESA,
|
|
40
|
+
dvEmpresa: DV_EMPRESA,
|
|
41
|
+
sessionPath: SESSION_PATH,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// ── 1. Asegurar sesión portal en subsistema /cgi_dte/UPL/ ─────────────────
|
|
45
|
+
// DTEauth?7 es la página del formulario de búsqueda de libros.
|
|
46
|
+
// CSESSIONID es el token de sesión del portal SII. Lo necesitamos válido.
|
|
47
|
+
console.log('\n[1] Autenticando en /cgi_dte/UPL/DTEauth?7...');
|
|
48
|
+
// Parchar request para capturar Set-Cookie crudos de DTEauth?7
|
|
49
|
+
const origRequest = cert.session.request.bind(cert.session);
|
|
50
|
+
let lastRawHeaders = null;
|
|
51
|
+
cert.session.request = async function(url, opts) {
|
|
52
|
+
const resp = await origRequest(url, opts);
|
|
53
|
+
if (url.includes('DTEauth')) {
|
|
54
|
+
lastRawHeaders = resp.headers;
|
|
55
|
+
}
|
|
56
|
+
return resp;
|
|
57
|
+
};
|
|
58
|
+
const authResp = await cert.session.ensureSession('/cgi_dte/UPL/DTEauth?7');
|
|
59
|
+
cert.session.request = origRequest; // restaurar
|
|
60
|
+
console.log(' Status DTEauth:', authResp?.status);
|
|
61
|
+
if (lastRawHeaders) {
|
|
62
|
+
const sc = lastRawHeaders['set-cookie'];
|
|
63
|
+
console.log(' Set-Cookie DTEauth?7:', JSON.stringify(sc));
|
|
64
|
+
}
|
|
65
|
+
console.log(' CSESSIONID en jar:', cert.session.cookieJar?.match(/CSESSIONID=[^;]+/)?.[0] || '(none)');
|
|
66
|
+
fs.writeFileSync(path.join(__dirname, 'test-output', 'dteauth7.html'), authResp?.body || '', 'latin1');
|
|
67
|
+
|
|
68
|
+
// ── TEST DIRECTO: llamar QDetEstLibro sin QEstLibro de por medio ──────────
|
|
69
|
+
console.log('\n[TEST DIRECTO] QDetEstLibro sin pasar por QEstLibro...');
|
|
70
|
+
const urlDetDirect = `https://maullin.sii.cl/cgi_dte/UPL/QDetEstLibro` +
|
|
71
|
+
`?Codigo=COMPRA-772220&rutC=78206276&dvC=K&periodo=2026-04`;
|
|
72
|
+
const directResp = await cert.session.request(urlDetDirect);
|
|
73
|
+
console.log(' Status:', directResp.status);
|
|
74
|
+
const directBody = directResp.body;
|
|
75
|
+
if (directBody.includes('SESION HA EXPIRADO')) {
|
|
76
|
+
console.warn(' [WARN] TAMBIÉN falla sin QEstLibro — es el CSESSIONID o la sesión');
|
|
77
|
+
} else if (directBody.includes('AUTORIZADO')) {
|
|
78
|
+
console.warn(' [WARN] Error de autorización');
|
|
79
|
+
console.log(directBody.slice(0, 500));
|
|
80
|
+
} else {
|
|
81
|
+
console.log(' OK! Funciona directamente');
|
|
82
|
+
console.log(directBody.slice(0, 1000));
|
|
83
|
+
}
|
|
84
|
+
fs.writeFileSync(path.join(__dirname, 'test-output', 'qdetestlibro-direct.html'), directBody, 'latin1');
|
|
85
|
+
|
|
86
|
+
// ── 2. Llamar QEstLibro ────────────────────────────────────────────────────
|
|
87
|
+
const urlLista = `https://maullin.sii.cl/cgi_dte/UPL/QEstLibro` +
|
|
88
|
+
`?rutCompany=${RUT_EMPRESA}&dvCompany=${DV_EMPRESA}&TrackId=&year=${YEAR}&month=00&tipo=TODOS`;
|
|
89
|
+
|
|
90
|
+
console.log('\n[2] GET', urlLista);
|
|
91
|
+
const listaResp = await cert.session.request(urlLista);
|
|
92
|
+
console.log(' Status:', listaResp.status);
|
|
93
|
+
console.log(' Set-Cookie headers:', listaResp.headers?.['set-cookie'] || '(none)');
|
|
94
|
+
console.log(' Cookies post-QEstLibro:', cert.session.cookieJar?.slice(0, 250) + '...');
|
|
95
|
+
|
|
96
|
+
if (listaResp.body.includes('SESION HA EXPIRADO')) {
|
|
97
|
+
console.error(' [ERR] Sesión expirada en QEstLibro — revisar ensureSession');
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Guardar HTML para inspección
|
|
102
|
+
const htmlListaPath = path.join(__dirname, 'test-output', 'qestlibro.html');
|
|
103
|
+
fs.mkdirSync(path.dirname(htmlListaPath), { recursive: true });
|
|
104
|
+
fs.writeFileSync(htmlListaPath, listaResp.body, 'latin1');
|
|
105
|
+
console.log(' HTML guardado en:', htmlListaPath);
|
|
106
|
+
|
|
107
|
+
// ── 3. Parsear la tabla: extraer Código → periodo → operación ──────────────
|
|
108
|
+
// El HTML tiene links tipo: QDetEstLibro?Codigo=VENTA-772219&rutC=...&periodo=2026-04
|
|
109
|
+
// href sin comillas: href=QDetEstLibro?Codigo=X&rutC=Y&dvC=Z&periodo=PPPP>Ver
|
|
110
|
+
// el > cierra el atributo, por eso lo excluimos del grupo de captura
|
|
111
|
+
const linkRegex = /QDetEstLibro\?Codigo=([^&"'\s>]+)&rutC=[^&"'\s>]+&dvC=[^&"'\s>]+&periodo=([^&"'\s>]+)/gi;
|
|
112
|
+
const codigos = {}; // { '2026-04': { VENTA: 'VENTA-772219', COMPRA: 'COMPRA-XXXXX' } }
|
|
113
|
+
let m;
|
|
114
|
+
while ((m = linkRegex.exec(listaResp.body)) !== null) {
|
|
115
|
+
const codigo = m[1];
|
|
116
|
+
const periodo = m[2];
|
|
117
|
+
if (PERIODO && periodo !== PERIODO) continue;
|
|
118
|
+
codigos[periodo] = codigos[periodo] || {};
|
|
119
|
+
const tipoMatch = /^(VENTA|COMPRA|GUIAS?)/i.exec(codigo);
|
|
120
|
+
const tipo = tipoMatch ? tipoMatch[1].toUpperCase() : codigo;
|
|
121
|
+
codigos[periodo][tipo] = codigo;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
console.log('\n Códigos encontrados:');
|
|
125
|
+
if (Object.keys(codigos).length === 0) {
|
|
126
|
+
console.log(' (ninguno — revisar HTML en test-output/qestlibro.html)');
|
|
127
|
+
}
|
|
128
|
+
for (const [p, ops] of Object.entries(codigos)) {
|
|
129
|
+
for (const [op, cod] of Object.entries(ops)) {
|
|
130
|
+
console.log(` ${p} / ${op} → ${cod}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── 4. Llamar QDetEstLibro para cada código ────────────────────────────────
|
|
135
|
+
// Usamos las MISMAS cookies que obtuvimos de DTEauth?7 (paso 1).
|
|
136
|
+
// NO re-autenticamos: si DTEauth?7 usa tokens one-shot, un segundo call
|
|
137
|
+
// lo consumiría sin beneficio. Las cookies NETSCAPE_LIVEWIRE persisten.
|
|
138
|
+
for (const [periodo, ops] of Object.entries(codigos)) {
|
|
139
|
+
for (const [operacion, codigo] of Object.entries(ops)) {
|
|
140
|
+
const urlDet = `https://maullin.sii.cl/cgi_dte/UPL/QDetEstLibro` +
|
|
141
|
+
`?Codigo=${encodeURIComponent(codigo)}&rutC=${RUT_EMPRESA}&dvC=${DV_EMPRESA}&periodo=${periodo}`;
|
|
142
|
+
|
|
143
|
+
const refererQEstLibro = `https://maullin.sii.cl/cgi_dte/UPL/QEstLibro` +
|
|
144
|
+
`?rutCompany=${RUT_EMPRESA}&dvCompany=${DV_EMPRESA}&TrackId=&year=${YEAR}&month=00&tipo=TODOS`;
|
|
145
|
+
|
|
146
|
+
console.log(`\n[3] QDetEstLibro ${periodo} ${operacion} (${codigo})`);
|
|
147
|
+
console.log(' GET', urlDet);
|
|
148
|
+
|
|
149
|
+
const detResp = await cert.session.request(urlDet, {
|
|
150
|
+
headers: { Referer: refererQEstLibro },
|
|
151
|
+
});
|
|
152
|
+
console.log(' Status:', detResp.status);
|
|
153
|
+
|
|
154
|
+
const outPath = path.join(__dirname, 'test-output', `qdetestlibro-${periodo}-${operacion}.html`);
|
|
155
|
+
fs.writeFileSync(outPath, detResp.body, 'latin1');
|
|
156
|
+
|
|
157
|
+
if (detResp.body.includes('SESION HA EXPIRADO')) {
|
|
158
|
+
console.warn(' [WARN] Sesión expirada — revisar cookies / DTEauth');
|
|
159
|
+
console.log('\n--- BODY (500 chars) ---\n', detResp.body.slice(0, 500));
|
|
160
|
+
} else {
|
|
161
|
+
console.log(' HTML guardado en:', outPath);
|
|
162
|
+
console.log('\n--- BODY ---\n', detResp.body);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
console.log('\n=== Fin ===');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
main().catch(e => {
|
|
171
|
+
console.error('[FATAL]', e.message);
|
|
172
|
+
console.error(e.stack);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
});
|
package/utils/pfx.js
CHANGED
|
@@ -19,7 +19,8 @@ const { certError, ERROR_CODES } = require('./error');
|
|
|
19
19
|
* @property {forge.pki.PrivateKey} privateKey - Clave privada
|
|
20
20
|
* @property {forge.pki.Certificate} certificate - Certificado
|
|
21
21
|
* @property {string} privateKeyPem - Clave privada en formato PEM
|
|
22
|
-
* @property {string} certificatePem - Certificado en formato PEM
|
|
22
|
+
* @property {string} certificatePem - Certificado hoja en formato PEM (solo leaf, para firma XML)
|
|
23
|
+
* @property {string} certificateChainPem - Cadena completa PEM: hoja → intermedios → raíz (para TLS mTLS)
|
|
23
24
|
* @property {Object} subject - Campos del subject del certificado
|
|
24
25
|
* @property {string} rut - RUT extraído del certificado (si existe)
|
|
25
26
|
* @property {string} cn - Common Name del certificado
|
|
@@ -40,7 +41,7 @@ function loadPfxFromBuffer(pfxBuffer, password) {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
let p12Asn1, p12;
|
|
43
|
-
|
|
44
|
+
|
|
44
45
|
try {
|
|
45
46
|
p12Asn1 = forge.asn1.fromDer(pfxBuffer.toString('binary'));
|
|
46
47
|
} catch (err) {
|
|
@@ -56,38 +57,74 @@ function loadPfxFromBuffer(pfxBuffer, password) {
|
|
|
56
57
|
throw certError(`Error descifrando PFX: ${err.message}`, ERROR_CODES.CERT_INVALID, { originalError: err });
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
// Extraer
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
// Extraer todos los certificados y la clave privada
|
|
61
|
+
const certBagsRaw = p12.getBags({ bagType: forge.pki.oids.certBag })[forge.pki.oids.certBag] || [];
|
|
62
|
+
let keyBagRaw = p12.getBags({ bagType: forge.pki.oids.pkcs8ShroudedKeyBag })[forge.pki.oids.pkcs8ShroudedKeyBag] || [];
|
|
63
|
+
// Fallback: keyBag sin cifrado (formato legacy)
|
|
64
|
+
if (!keyBagRaw.length) {
|
|
65
|
+
keyBagRaw = p12.getBags({ bagType: forge.pki.oids.keyBag })[forge.pki.oids.keyBag] || [];
|
|
66
|
+
}
|
|
65
67
|
|
|
66
|
-
if (!
|
|
68
|
+
if (!certBagsRaw.length) {
|
|
67
69
|
throw certError('No se encontró certificado en el archivo PFX', ERROR_CODES.CERT_INVALID);
|
|
68
70
|
}
|
|
69
71
|
|
|
70
|
-
if (!
|
|
72
|
+
if (!keyBagRaw.length) {
|
|
71
73
|
throw certError('No se encontró clave privada en el archivo PFX', ERROR_CODES.CERT_INVALID);
|
|
72
74
|
}
|
|
73
75
|
|
|
74
|
-
const
|
|
75
|
-
const privateKey =
|
|
76
|
+
const allCerts = certBagsRaw.map(b => b.cert).filter(Boolean);
|
|
77
|
+
const privateKey = keyBagRaw[0].key;
|
|
78
|
+
|
|
79
|
+
// ── Identificar el certificado hoja (leaf) ───────────────────────────────
|
|
80
|
+
// Estrategia 1: cert cuya clave pública coincide con la clave privada (RSA)
|
|
81
|
+
let leafCert = null;
|
|
82
|
+
try {
|
|
83
|
+
const pubFromKey = forge.pki.setRsaPublicKey(privateKey.n, privateKey.e);
|
|
84
|
+
if (pubFromKey) {
|
|
85
|
+
const pubPem = forge.pki.publicKeyToPem(pubFromKey);
|
|
86
|
+
leafCert = allCerts.find(c => forge.pki.publicKeyToPem(c.publicKey) === pubPem) ?? null;
|
|
87
|
+
}
|
|
88
|
+
} catch (_) { /* EC u otro tipo de clave */ }
|
|
89
|
+
|
|
90
|
+
// Estrategia 2: primer cert cuyo basicConstraints no marca cA=true
|
|
91
|
+
if (!leafCert) {
|
|
92
|
+
leafCert = allCerts.find(c => {
|
|
93
|
+
const bc = c.getExtension('basicConstraints');
|
|
94
|
+
return !bc || bc.cA !== true;
|
|
95
|
+
}) ?? allCerts[0];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── Ordenar cadena: hoja → intermedios → raíz ────────────────────────────
|
|
99
|
+
// SII (palena.sii.cl) requiere la cadena completa en el TLS handshake mutuo.
|
|
100
|
+
// Sin los intermedios, el servidor devuelve SSL alert 48 (unknown_ca).
|
|
101
|
+
const remaining = allCerts.filter(c => c !== leafCert);
|
|
102
|
+
const chain = [leafCert];
|
|
103
|
+
let current = leafCert;
|
|
104
|
+
while (remaining.length > 0) {
|
|
105
|
+
const issuerHash = current.issuer.hash;
|
|
106
|
+
const idx = remaining.findIndex(c => c.subject.hash === issuerHash);
|
|
107
|
+
if (idx < 0) break;
|
|
108
|
+
current = remaining.splice(idx, 1)[0];
|
|
109
|
+
if (chain.includes(current)) break;
|
|
110
|
+
chain.push(current);
|
|
111
|
+
}
|
|
112
|
+
chain.push(...remaining);
|
|
76
113
|
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
const rut = extractRutFromCertificate(certificate);
|
|
114
|
+
const subject = extractSubjectFields(leafCert);
|
|
115
|
+
const rut = extractRutFromCertificate(leafCert);
|
|
80
116
|
|
|
81
117
|
return {
|
|
82
118
|
privateKey,
|
|
83
|
-
certificate,
|
|
119
|
+
certificate: leafCert,
|
|
84
120
|
privateKeyPem: forge.pki.privateKeyToPem(privateKey),
|
|
85
|
-
certificatePem: forge.pki.certificateToPem(
|
|
121
|
+
certificatePem: forge.pki.certificateToPem(leafCert),
|
|
122
|
+
certificateChainPem: chain.map(c => forge.pki.certificateToPem(c)).join(''),
|
|
86
123
|
subject,
|
|
87
124
|
rut,
|
|
88
125
|
cn: subject.CN || null,
|
|
89
|
-
notBefore:
|
|
90
|
-
notAfter:
|
|
126
|
+
notBefore: leafCert.validity.notBefore,
|
|
127
|
+
notAfter: leafCert.validity.notAfter,
|
|
91
128
|
};
|
|
92
129
|
}
|
|
93
130
|
|
|
@@ -186,8 +223,7 @@ function getDaysUntilExpiry(notAfter) {
|
|
|
186
223
|
function createTlsOptions(pfxData) {
|
|
187
224
|
return {
|
|
188
225
|
key: pfxData.privateKeyPem,
|
|
189
|
-
cert: pfxData.certificatePem,
|
|
190
|
-
certificate: pfxData.certificatePem,
|
|
226
|
+
cert: pfxData.certificateChainPem || pfxData.certificatePem,
|
|
191
227
|
rejectUnauthorized: false,
|
|
192
228
|
};
|
|
193
229
|
}
|
package/test-muestras.js
DELETED
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* test-muestras.js
|
|
3
|
-
*
|
|
4
|
-
* Genera PDFs de muestras impresas desde los XMLs de certificación y los guarda
|
|
5
|
-
* en ./test-output/. Ejecutar desde la raíz del repositorio dte-sii:
|
|
6
|
-
*
|
|
7
|
-
* node test-muestras.js
|
|
8
|
-
* node test-muestras.js --open (abre cada PDF al generarlo)
|
|
9
|
-
* node test-muestras.js --set guia (solo ese set: basico|exenta|guia|compra)
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
'use strict';
|
|
13
|
-
|
|
14
|
-
const fs = require('fs');
|
|
15
|
-
const path = require('path');
|
|
16
|
-
const { execSync } = require('child_process');
|
|
17
|
-
const MuestrasImpresas = require('./cert/MuestrasImpresas');
|
|
18
|
-
|
|
19
|
-
// ── Configuración ──────────────────────────────────────────────────────────────
|
|
20
|
-
|
|
21
|
-
const SETS_DIR = path.resolve(
|
|
22
|
-
__dirname,
|
|
23
|
-
'../devlas-cloud-api-node/debug/cert-v2/sets-prueba'
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
const OUT_DIR = path.join(__dirname, 'test-output');
|
|
27
|
-
|
|
28
|
-
const SETS = [
|
|
29
|
-
{ file: 'envio-set-basico.xml', label: 'Básico (33 + 56 + 61)' },
|
|
30
|
-
{ file: 'envio-set-exenta.xml', label: 'Exenta (34)' },
|
|
31
|
-
{ file: 'envio-set-guia.xml', label: 'Guía (52)' },
|
|
32
|
-
{ file: 'envio-set-compra.xml', label: 'Compra (46)' },
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
// ── Argumentos CLI ────────────────────────────────────────────────────────────
|
|
36
|
-
|
|
37
|
-
const ARGS = process.argv.slice(2);
|
|
38
|
-
const AUTO_OPEN = ARGS.includes('--open');
|
|
39
|
-
const FILTER = ARGS.find(a => a.startsWith('--set'))
|
|
40
|
-
? ARGS[ARGS.indexOf(ARGS.find(a => a.startsWith('--set'))) + 1]
|
|
41
|
-
: null;
|
|
42
|
-
|
|
43
|
-
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
44
|
-
|
|
45
|
-
function openPdf(filePath) {
|
|
46
|
-
try {
|
|
47
|
-
if (process.platform === 'win32') {
|
|
48
|
-
execSync(`start "" "${filePath}"`);
|
|
49
|
-
} else if (process.platform === 'darwin') {
|
|
50
|
-
execSync(`open "${filePath}"`);
|
|
51
|
-
} else {
|
|
52
|
-
execSync(`xdg-open "${filePath}"`);
|
|
53
|
-
}
|
|
54
|
-
} catch { /* silenciar si no hay visor */ }
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function pad(str, len) {
|
|
58
|
-
return String(str).padEnd(len);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
62
|
-
|
|
63
|
-
async function main() {
|
|
64
|
-
fs.mkdirSync(OUT_DIR, { recursive: true });
|
|
65
|
-
|
|
66
|
-
const muestra = new MuestrasImpresas({
|
|
67
|
-
emisor: {
|
|
68
|
-
RUTEmisor: '78206276-K',
|
|
69
|
-
RznSoc: 'DEVLAS SPA',
|
|
70
|
-
GiroEmis: 'ACTIVIDADES DE PROGRAMACION INFORMATICA',
|
|
71
|
-
DirOrigen: 'AV. ESC. AGRÍCOLA 1710, Bloque G, Depto. 706',
|
|
72
|
-
CmnaOrigen: 'MACUL',
|
|
73
|
-
},
|
|
74
|
-
siiOficina: 'S.I.I. - SANTIAGO CENTRO',
|
|
75
|
-
resolucion: 'Res. Ex. SII N° 0 del 2026',
|
|
76
|
-
debugDir: OUT_DIR,
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
const sets = FILTER
|
|
80
|
-
? SETS.filter(s => s.file.includes(FILTER))
|
|
81
|
-
: SETS;
|
|
82
|
-
|
|
83
|
-
if (!sets.length) {
|
|
84
|
-
console.error(`[!] No se encontró ningún set que coincida con: ${FILTER}`);
|
|
85
|
-
process.exit(1);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
let totalDocs = 0;
|
|
89
|
-
let totalPdfs = 0;
|
|
90
|
-
let errores = 0;
|
|
91
|
-
|
|
92
|
-
console.log('\n' + '═'.repeat(60));
|
|
93
|
-
console.log(' TEST — generarPDFBuffer (pdf-lib, sin Chromium)');
|
|
94
|
-
console.log('═'.repeat(60));
|
|
95
|
-
console.log(` Salida: ${OUT_DIR}`);
|
|
96
|
-
console.log(` Auto-open: ${AUTO_OPEN ? 'sí' : 'no (agrega --open)'}`);
|
|
97
|
-
console.log('');
|
|
98
|
-
|
|
99
|
-
for (const set of sets) {
|
|
100
|
-
const xmlPath = path.join(SETS_DIR, set.file);
|
|
101
|
-
|
|
102
|
-
if (!fs.existsSync(xmlPath)) {
|
|
103
|
-
console.warn(` [!] No encontrado: ${xmlPath}`);
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
console.log(`─── ${set.label} `);
|
|
108
|
-
|
|
109
|
-
const xml = fs.readFileSync(xmlPath, 'utf8');
|
|
110
|
-
let docs;
|
|
111
|
-
try {
|
|
112
|
-
docs = muestra.parseEnvioDTE(xml);
|
|
113
|
-
} catch (e) {
|
|
114
|
-
console.error(` [ERR] parseEnvioDTE: ${e.message}`);
|
|
115
|
-
errores++;
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
console.log(` ${docs.length} documento(s) parseados`);
|
|
120
|
-
console.log('');
|
|
121
|
-
|
|
122
|
-
for (const doc of docs) {
|
|
123
|
-
totalDocs++;
|
|
124
|
-
|
|
125
|
-
const baseName = `muestra-${doc.tipoDte}-${doc.folio}`;
|
|
126
|
-
const label = `tipo ${pad(doc.tipoDte, 3)} folio ${pad(doc.folio, 6)}`;
|
|
127
|
-
|
|
128
|
-
// ── Ejemplar tributario ──────────────────────────────────────────────────
|
|
129
|
-
try {
|
|
130
|
-
const t0 = Date.now();
|
|
131
|
-
const buf = await muestra.generarPDFBuffer(doc, { cedible: false });
|
|
132
|
-
const ms = Date.now() - t0;
|
|
133
|
-
const out = path.join(OUT_DIR, `${baseName}.pdf`);
|
|
134
|
-
fs.writeFileSync(out, buf);
|
|
135
|
-
totalPdfs++;
|
|
136
|
-
console.log(` ✓ ${label} → ${baseName}.pdf (${buf.length} bytes, ${ms}ms)`);
|
|
137
|
-
if (AUTO_OPEN) openPdf(out);
|
|
138
|
-
} catch (e) {
|
|
139
|
-
errores++;
|
|
140
|
-
console.error(` ✗ ${label} → ERROR: ${e.message}`);
|
|
141
|
-
if (e.stack) console.error(e.stack.split('\n').slice(1, 4).map(l => ' ' + l).join('\n'));
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// ── Copia cedible (si aplica) ────────────────────────────────────────────
|
|
145
|
-
const CEDIBLES = new Set([33, 34, 43, 46, 52]);
|
|
146
|
-
if (CEDIBLES.has(doc.tipoDte)) {
|
|
147
|
-
const esGuiaInterna = doc.tipoDte === 52 && [5, 6].includes(doc.indTraslado);
|
|
148
|
-
if (!esGuiaInterna) {
|
|
149
|
-
try {
|
|
150
|
-
const t0 = Date.now();
|
|
151
|
-
const buf = await muestra.generarPDFBuffer(doc, { cedible: true });
|
|
152
|
-
const ms = Date.now() - t0;
|
|
153
|
-
const out = path.join(OUT_DIR, `${baseName}-cedible.pdf`);
|
|
154
|
-
fs.writeFileSync(out, buf);
|
|
155
|
-
totalPdfs++;
|
|
156
|
-
console.log(` ✓ ${label} → ${baseName}-cedible.pdf (${buf.length} bytes, ${ms}ms)`);
|
|
157
|
-
if (AUTO_OPEN) openPdf(out);
|
|
158
|
-
} catch (e) {
|
|
159
|
-
errores++;
|
|
160
|
-
console.error(` ✗ ${label} (cedible) → ERROR: ${e.message}`);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
console.log('');
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
console.log('═'.repeat(60));
|
|
169
|
-
console.log(` Documentos procesados : ${totalDocs}`);
|
|
170
|
-
console.log(` PDFs generados : ${totalPdfs}`);
|
|
171
|
-
if (errores) console.log(` Errores : ${errores}`);
|
|
172
|
-
console.log(`\n Archivos en: ${OUT_DIR}`);
|
|
173
|
-
console.log('═'.repeat(60) + '\n');
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
main().catch(e => {
|
|
177
|
-
console.error('\n[FATAL]', e.message);
|
|
178
|
-
console.error(e.stack);
|
|
179
|
-
process.exit(1);
|
|
180
|
-
});
|