@devlas/dte-sii 2.11.0 → 2.12.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/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/package.json +1 -1
- package/test-qdetestlibro.js +174 -174
- package/test-muestras.js +0 -180
package/CafSolicitor.js
CHANGED
|
@@ -197,7 +197,13 @@ class CafSolicitor {
|
|
|
197
197
|
async solicitar({ tipoDte, cantidad = 1 }) {
|
|
198
198
|
const { numero: rut, dv } = splitRut(this.rutEmisor);
|
|
199
199
|
const debugDir = this._getDebugDir(tipoDte);
|
|
200
|
-
|
|
200
|
+
|
|
201
|
+
// Rate limiting: mínimo 1001ms entre solicitudes para no saturar el portal SII.
|
|
202
|
+
const _now = Date.now();
|
|
203
|
+
const _elapsed = _now - CafSolicitor._lastSolicitudAt;
|
|
204
|
+
if (_elapsed < 1001) await new Promise(r => setTimeout(r, 1001 - _elapsed));
|
|
205
|
+
CafSolicitor._lastSolicitudAt = Date.now();
|
|
206
|
+
|
|
201
207
|
console.log('─'.repeat(60));
|
|
202
208
|
console.log(`[CafSolicitor] Solicitando CAF tipo ${tipoDte} x${cantidad}`);
|
|
203
209
|
console.log(` RUT: ${this.rutEmisor} | Ambiente: ${this.ambiente}`);
|
|
@@ -233,14 +239,27 @@ class CafSolicitor {
|
|
|
233
239
|
// Guardar respuesta final
|
|
234
240
|
this._saveDebug(debugDir, 'caf-final.html', response.body || '');
|
|
235
241
|
|
|
242
|
+
// Detectar bloqueo WAAP/firewall del SII antes de cualquier check de negocio
|
|
243
|
+
if (response.status === 403 ||
|
|
244
|
+
(response.body && (
|
|
245
|
+
response.body.includes('acceso restringido') ||
|
|
246
|
+
response.body.toLowerCase().includes('recaptcha')
|
|
247
|
+
))) {
|
|
248
|
+
return { success: false, errorCode: 'WAAP_BLOCKED', error: 'IP bloqueada por el firewall del SII. Espera antes de reintentar.' };
|
|
249
|
+
}
|
|
250
|
+
|
|
236
251
|
// Verificar si obtuvimos el CAF
|
|
237
252
|
if (response.body && response.body.includes('<AUTORIZACION')) {
|
|
238
253
|
const cafPath = this._saveCafOrganized(response.body, tipoDte);
|
|
239
254
|
return { success: true, cafPath, xml: response.body, maxAutor: this._lastMaxAutor ?? cantidad };
|
|
240
255
|
}
|
|
241
256
|
|
|
257
|
+
if (response.body && response.body.includes('NO SE AUTORIZA')) {
|
|
258
|
+
return { success: false, errorCode: 'TIMBRAJE_BLOQUEADO', error: 'SII: No se autoriza timbraje. Folios acumulados excesivos o situaciones tributarias pendientes. Revisa el portal SII → Factura Electrónica → Solicitud de Timbraje.' };
|
|
259
|
+
}
|
|
260
|
+
|
|
242
261
|
if (response.body && response.body.includes('Autenticaci')) {
|
|
243
|
-
return { success: false, error: 'El SII devolvió página de autenticación' };
|
|
262
|
+
return { success: false, errorCode: 'SESSION_EXPIRED', error: 'El SII devolvió página de autenticación' };
|
|
244
263
|
}
|
|
245
264
|
|
|
246
265
|
// Detectar error de MAX_AUTOR: "La cantidad de documentos a timbrar debe ser menor o igual al máximo autorizado"
|
|
@@ -255,7 +274,7 @@ class CafSolicitor {
|
|
|
255
274
|
this.runStamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
256
275
|
return this.solicitar({ tipoDte, cantidad: 1 });
|
|
257
276
|
}
|
|
258
|
-
return { success: false, error: 'Cantidad de folios excede el máximo que SII autoriza por solicitud para este tipo de documento (MAX_AUTOR). Verifica el estado de timbraje de tu empresa en el portal SII.' };
|
|
277
|
+
return { success: false, errorCode: 'MAX_AUTOR_EXCEEDED', error: 'Cantidad de folios excede el máximo que SII autoriza por solicitud para este tipo de documento (MAX_AUTOR). Verifica el estado de timbraje de tu empresa en el portal SII.' };
|
|
259
278
|
}
|
|
260
279
|
|
|
261
280
|
// Detectar rango ya autorizado — el CAF existe en SII pero no fue capturado
|
|
@@ -300,16 +319,17 @@ class CafSolicitor {
|
|
|
300
319
|
const rangoStr = desde != null && hasta != null ? ` ${desde}-${hasta}` : '';
|
|
301
320
|
return {
|
|
302
321
|
success: false,
|
|
322
|
+
errorCode: 'RANGO_YA_AUTORIZADO',
|
|
303
323
|
rangoYaAutorizado: desde != null && hasta != null ? { folioDesde: desde, folioHasta: hasta } : null,
|
|
304
324
|
error: `SII: Ya existe CAF autorizado para el rango${rangoStr}. El XML fue aprobado por SII en una solicitud previa pero no se pudo recuperar automáticamente. Descárgalo manualmente desde el portal SII (Factura Electrónica → Solicitud de Timbraje).`,
|
|
305
325
|
};
|
|
306
326
|
}
|
|
307
327
|
|
|
308
|
-
return { success: false, error: 'No se obtuvo CAF en la respuesta' };
|
|
328
|
+
return { success: false, errorCode: 'UNKNOWN', error: 'No se obtuvo CAF en la respuesta' };
|
|
309
329
|
|
|
310
330
|
} catch (err) {
|
|
311
331
|
console.error(`[CafSolicitor] Error: ${err.message}`);
|
|
312
|
-
return { success: false, error: err.message };
|
|
332
|
+
return { success: false, errorCode: 'NETWORK_ERROR', error: err.message };
|
|
313
333
|
}
|
|
314
334
|
}
|
|
315
335
|
|
|
@@ -335,6 +355,12 @@ class CafSolicitor {
|
|
|
335
355
|
currentHtml = response.body || '';
|
|
336
356
|
this._saveDebug(debugDir, 'step2.html', currentHtml);
|
|
337
357
|
|
|
358
|
+
// Rechazo duro antes del check de COD_DOCTO: la página de rechazo también contiene
|
|
359
|
+
// "COD_DOCTO" en su JavaScript, lo que causaría un POST innecesario con datos vacíos.
|
|
360
|
+
if (currentHtml.includes('NO SE AUTORIZA')) {
|
|
361
|
+
return response; // solicitar() detectará el bloqueo en response.body
|
|
362
|
+
}
|
|
363
|
+
|
|
338
364
|
// Selección de tipo de documento
|
|
339
365
|
if (currentHtml.includes('COD_DOCTO')) {
|
|
340
366
|
const selectInputs = SiiSession.extractInputValues(currentHtml);
|
|
@@ -553,4 +579,6 @@ class CafSolicitor {
|
|
|
553
579
|
}
|
|
554
580
|
}
|
|
555
581
|
|
|
582
|
+
CafSolicitor._lastSolicitudAt = 0; // ms timestamp of last solicitar() call — for rate limiting
|
|
583
|
+
|
|
556
584
|
module.exports = CafSolicitor;
|
package/LICENSE
CHANGED
|
@@ -1,27 +1,27 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Devlas SpA — https://devlas.cl
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
22
|
-
|
|
23
|
-
---
|
|
24
|
-
|
|
25
|
-
Esta librería implementa el protocolo XML del Servicio de Impuestos Internos (SII)
|
|
26
|
-
de Chile tal como está documentado públicamente por el propio SII.
|
|
27
|
-
Inspirada conceptualmente en LibreDTE de SASCO SpA (https://libredte.cl).
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Devlas SpA — https://devlas.cl
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
Esta librería implementa el protocolo XML del Servicio de Impuestos Internos (SII)
|
|
26
|
+
de Chile tal como está documentado públicamente por el propio SII.
|
|
27
|
+
Inspirada conceptualmente en LibreDTE de SASCO SpA (https://libredte.cl).
|
package/SiiSession.js
CHANGED
|
@@ -157,10 +157,57 @@ class SiiSession {
|
|
|
157
157
|
* @returns {Promise<Object>}
|
|
158
158
|
*/
|
|
159
159
|
async request(url, options = {}) {
|
|
160
|
+
const isPost = (options.method || 'GET').toUpperCase() === 'POST';
|
|
161
|
+
const RETRY_DELAYS = [2000, 4000, 8000];
|
|
162
|
+
const RETRYABLE_CODES = new Set(['ETIMEDOUT', 'ECONNRESET', 'ECONNREFUSED', 'ENOTFOUND', 'TimeoutError']);
|
|
163
|
+
const RETRYABLE_STATUS = new Set([502, 503, 504]);
|
|
164
|
+
|
|
165
|
+
let lastErr;
|
|
166
|
+
for (let attempt = 0; attempt <= RETRY_DELAYS.length; attempt++) {
|
|
167
|
+
if (attempt > 0) {
|
|
168
|
+
const delay = RETRY_DELAYS[attempt - 1];
|
|
169
|
+
log.warn(`[SiiSession] request timeout/5xx — reintentando en ${delay / 1000}s (intento ${attempt}/${RETRY_DELAYS.length})...`);
|
|
170
|
+
await new Promise(r => setTimeout(r, delay));
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
const result = await this._doRequest(url, options, isPost);
|
|
174
|
+
if (RETRYABLE_STATUS.has(result.status)) {
|
|
175
|
+
lastErr = new Error(`HTTP ${result.status}`);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
return result;
|
|
179
|
+
} catch (err) {
|
|
180
|
+
const code = err.code || err.constructor?.name || '';
|
|
181
|
+
if (RETRYABLE_CODES.has(code)) { lastErr = err; continue; }
|
|
182
|
+
throw err; // error no retriable — propagar inmediatamente
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
throw lastErr;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Realiza la petición HTTP sin retry. Llamado desde request().
|
|
190
|
+
* @private
|
|
191
|
+
*/
|
|
192
|
+
async _doRequest(url, options, isPost) {
|
|
160
193
|
const res = await got(url, {
|
|
161
194
|
method: options.method || 'GET',
|
|
162
195
|
headers: {
|
|
163
|
-
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
196
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
|
|
197
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
|
198
|
+
'Accept-Language': 'es-419,es-US;q=0.9,es;q=0.8,en;q=0.7',
|
|
199
|
+
'sec-ch-ua': '"Chromium";v="122", "Not(A:Brand";v="24", "Google Chrome";v="122"',
|
|
200
|
+
'sec-ch-ua-mobile': '?0',
|
|
201
|
+
'sec-ch-ua-platform': '"Windows"',
|
|
202
|
+
'Sec-Fetch-Dest': 'document',
|
|
203
|
+
'Sec-Fetch-Mode': 'navigate',
|
|
204
|
+
'Sec-Fetch-Site': 'same-origin',
|
|
205
|
+
'Sec-Fetch-User': '?1',
|
|
206
|
+
'Upgrade-Insecure-Requests': '1',
|
|
207
|
+
...(isPost ? {
|
|
208
|
+
'Cache-Control': 'max-age=0',
|
|
209
|
+
'Origin': `https://${this.baseHost}`,
|
|
210
|
+
} : {}),
|
|
164
211
|
...(this.cookieJar ? { Cookie: this.cookieJar } : {}),
|
|
165
212
|
...(options.headers || {}),
|
|
166
213
|
},
|
|
@@ -168,29 +215,21 @@ class SiiSession {
|
|
|
168
215
|
followRedirect: false,
|
|
169
216
|
throwHttpErrors: false,
|
|
170
217
|
https: this.tlsOptions || { rejectUnauthorized: false },
|
|
171
|
-
responseType: 'buffer',
|
|
218
|
+
responseType: 'buffer',
|
|
219
|
+
timeout: { request: options.timeoutMs ?? 20000 },
|
|
172
220
|
});
|
|
173
221
|
|
|
174
222
|
this.cookieJar = this._mergeCookies(this.cookieJar, res.headers['set-cookie']);
|
|
175
|
-
|
|
176
|
-
// Detectar encoding del Content-Type y convertir correctamente
|
|
177
|
-
let bodyStr;
|
|
223
|
+
|
|
178
224
|
const contentType = res.headers['content-type'] || '';
|
|
179
225
|
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
226
|
const isSiiUrl = url.includes('sii.cl');
|
|
184
227
|
const hasUtf8 = contentType.toLowerCase().includes('utf-8');
|
|
185
|
-
const forceIso = contentType.toLowerCase().includes('iso-8859-1') ||
|
|
228
|
+
const forceIso = contentType.toLowerCase().includes('iso-8859-1') ||
|
|
186
229
|
contentType.toLowerCase().includes('latin1');
|
|
187
|
-
|
|
188
|
-
|
|
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.)
|
|
230
|
+
|
|
231
|
+
let bodyStr;
|
|
191
232
|
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
233
|
bodyStr = '';
|
|
195
234
|
for (let i = 0; i < buffer.length; i++) {
|
|
196
235
|
bodyStr += String.fromCharCode(buffer[i]);
|
|
@@ -198,12 +237,12 @@ class SiiSession {
|
|
|
198
237
|
} else {
|
|
199
238
|
bodyStr = buffer.toString('utf8');
|
|
200
239
|
}
|
|
201
|
-
|
|
240
|
+
|
|
202
241
|
return {
|
|
203
242
|
status: res.statusCode,
|
|
204
243
|
headers: res.headers,
|
|
205
244
|
body: bodyStr,
|
|
206
|
-
rawBody: res.body,
|
|
245
|
+
rawBody: res.body,
|
|
207
246
|
url: res.url,
|
|
208
247
|
cookieJar: this.cookieJar,
|
|
209
248
|
};
|