@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 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', // Obtener como buffer para manejar encoding
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
- // 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.)
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, // Buffer original por si se necesita
245
+ rawBody: res.body,
207
246
  url: res.url,
208
247
  cookieJar: this.cookieJar,
209
248
  };