@devlas/dte-sii 2.12.20 → 2.12.22

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
@@ -244,6 +244,19 @@ class CafSolicitor {
244
244
  this.session.saveSession(this.sessionPath);
245
245
  }
246
246
 
247
+ // Rechazo duro por falta de Verificación de Actividades: debe detectarse ANTES de
248
+ // _processMultiStepFlow. La página de rechazo también contiene el string
249
+ // "of_solicita_folios_dcto" dentro del JS changeRegregion(), lo que hace que
250
+ // _processMultiStepFlow la confunda con el paso 2 normal y siga de largo el flujo
251
+ // multi-paso en vez de detectar el rechazo aquí, terminando en errorCode UNKNOWN.
252
+ if (response.body && response.body.includes('no registra Verificacion de Actividades')) {
253
+ return {
254
+ success: false,
255
+ errorCode: 'VERIFICACION_ACTIVIDADES_PENDIENTE',
256
+ error: 'SII: El RUT no tiene registrada la Verificación de Actividades para este tipo de documento. Solicítala en el sitio SII → Peticiones Administrativas → Verificación de actividad, o en oficinas del SII.',
257
+ };
258
+ }
259
+
247
260
  // Procesar flujo multi-paso del SII
248
261
  response = await this._processMultiStepFlow(response, rut, dv, tipoDte, cantidad, debugDir);
249
262
 
@@ -269,6 +282,16 @@ class CafSolicitor {
269
282
  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.' };
270
283
  }
271
284
 
285
+ // Rechazo por Verificación de Actividades detectado en un paso posterior del flujo
286
+ // multi-paso (ver guarda defensiva en _processMultiStepFlow).
287
+ if (response.body && response.body.includes('no registra Verificacion de Actividades')) {
288
+ return {
289
+ success: false,
290
+ errorCode: 'VERIFICACION_ACTIVIDADES_PENDIENTE',
291
+ error: 'SII: El RUT no tiene registrada la Verificación de Actividades para este tipo de documento. Solicítala en el sitio SII → Peticiones Administrativas → Verificación de actividad, o en oficinas del SII.',
292
+ };
293
+ }
294
+
272
295
  if (response.body && response.body.includes('Autenticaci')) {
273
296
  return { success: false, errorCode: 'SESSION_EXPIRED', error: 'El SII devolvió página de autenticación' };
274
297
  }
@@ -375,6 +398,13 @@ class CafSolicitor {
375
398
  */
376
399
  async _processMultiStepFlow(response, rut, dv, tipoDte, cantidad, debugDir) {
377
400
  let currentHtml = response.body || '';
401
+
402
+ // Defensivo: mismo rechazo por Verificación de Actividades puede aparecer si el SII
403
+ // lo entrega recién en un paso posterior en vez del response inicial de solicitar().
404
+ if (currentHtml.includes('no registra Verificacion de Actividades')) {
405
+ return response; // solicitar() detectará el rechazo en response.body
406
+ }
407
+
378
408
  const realFormAction = SiiSession.extractFormAction(currentHtml);
379
409
 
380
410
  // Si el <form> real ya apunta a of_confirma_folio, el SII preseleccionó
@@ -447,7 +477,11 @@ class CafSolicitor {
447
477
  */
448
478
  async _processStep3(response, rut, dv, tipoDte, cantidad, debugDir) {
449
479
  let currentHtml = response.body || '';
450
-
480
+
481
+ if (currentHtml.includes('no registra Verificacion de Actividades')) {
482
+ return response; // solicitar() detectará el rechazo en response.body
483
+ }
484
+
451
485
  const formAction3 = SiiSession.extractFormAction(currentHtml) || '/cvc_cgi/dte/of_confirma_folio';
452
486
  const inputs3 = SiiSession.extractInputValues(currentHtml);
453
487
 
package/EnviadorSII.js CHANGED
@@ -1174,7 +1174,11 @@ class EnviadorSII {
1174
1174
  log.log('XML Length:', xml.length, 'bytes');
1175
1175
 
1176
1176
  const [rutNum, dv] = rutEmisor.split('-');
1177
- const [rutEnviaNum, dvEnvia] = this.certificado.rut.split('-');
1177
+ // Fallback a rutEmisor si el certificado no expone RUT (ver
1178
+ // docs/EXTRACCION_RUT_CERTIFICADO.md) — evita un TypeError por .split
1179
+ // sobre null cuando el PFX no tiene el RUT en ningún campo conocido.
1180
+ const rutEnvia = this.certificado.rut || rutEmisor;
1181
+ const [rutEnviaNum, dvEnvia] = rutEnvia.split('-');
1178
1182
 
1179
1183
  const boundary = '----WebKitFormBoundary' + Math.random().toString(36).substring(2);
1180
1184
 
@@ -214,7 +214,7 @@ class BoletaCert {
214
214
 
215
215
  const envioBoleta = new EnvioBOLETA({
216
216
  rutEmisor: this.emisor.rut,
217
- rutEnvia: this.certificado.rut,
217
+ rutEnvia: this.certificado.rut || this.emisor.rut,
218
218
  fchResol: this.resolucion.fecha,
219
219
  nroResol: this.resolucion.numero,
220
220
  certificado: this.certificado,
@@ -228,7 +228,7 @@ class BoletaCert {
228
228
  // Carátula para certificación
229
229
  envioBoleta.setCaratula({
230
230
  RutEmisor: this.emisor.rut,
231
- RutEnvia: this.certificado.rut,
231
+ RutEnvia: this.certificado.rut || this.emisor.rut,
232
232
  RutReceptor: '60803000-K', // SII en certificación
233
233
  FchResol: this.resolucion.fecha,
234
234
  NroResol: this.resolucion.numero,
@@ -291,7 +291,7 @@ class BoletaCert {
291
291
  // Establecer carátula
292
292
  consumoFolio.setCaratula({
293
293
  RutEmisor: this.emisor.rut,
294
- RutEnvia: this.certificado.rut,
294
+ RutEnvia: this.certificado.rut || this.emisor.rut,
295
295
  FchResol: this.resolucion.fecha,
296
296
  NroResol: this.resolucion.numero,
297
297
  SecEnvio: options?.secEnvio || 1,
@@ -358,7 +358,7 @@ class BoletaCert {
358
358
 
359
359
  consumoFolio.setCaratula({
360
360
  RutEmisor: this.emisor.rut,
361
- RutEnvia: this.certificado.rut,
361
+ RutEnvia: this.certificado.rut || this.emisor.rut,
362
362
  FchResol: this.resolucion.fecha,
363
363
  NroResol: this.resolucion.numero,
364
364
  SecEnvio: options.secEnvio,
package/package.json CHANGED
@@ -1,12 +1,15 @@
1
1
  {
2
2
  "name": "@devlas/dte-sii",
3
- "version": "2.12.20",
3
+ "version": "2.12.22",
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",
7
7
  "license": "MIT",
8
8
  "author": "Devlas SpA <ti@devlas.cl> (https://devlas.cl)",
9
9
  "homepage": "https://github.com/devlas-cl/dte-sii",
10
+ "scripts": {
11
+ "test": "node test/pfx.test.js"
12
+ },
10
13
  "repository": {
11
14
  "type": "git",
12
15
  "url": "https://github.com/devlas-cl/dte-sii.git"
package/utils/pfx.js CHANGED
@@ -179,11 +179,48 @@ function extractSubjectFields(cert) {
179
179
  }
180
180
 
181
181
  /**
182
- * Extraer RUT del certificado (desde serialNumber, OU o CN)
182
+ * Extraer RUT desde el Subject Alternative Name (SAN) con OID
183
+ * 1.3.6.1.4.1.8321.1 — reservado bajo la normativa chilena de firma
184
+ * electrónica (Ley 19.799). A diferencia de serialNumber/OU/CN, que son
185
+ * convención de cada CA, este OID es consistente entre CAs (verificado
186
+ * contra Acepta, IDOK y Signapis — ver docs/EXTRACCION_RUT_CERTIFICADO.md).
187
+ * @param {forge.pki.Certificate} cert - Certificado
188
+ * @returns {string|null} RUT o null
189
+ */
190
+ function extractRutFromSan(cert) {
191
+ const san = cert.getExtension('subjectAltName');
192
+ if (!san || !san.altNames) return null;
193
+
194
+ for (const alt of san.altNames) {
195
+ if (alt.type !== 0) continue; // 0 = otherName
196
+ try {
197
+ const oid = forge.asn1.derToOid(alt.value[0].value);
198
+ if (oid !== '1.3.6.1.4.1.8321.1') continue;
199
+
200
+ const inner = alt.value[1] && alt.value[1].value && alt.value[1].value[0];
201
+ if (!inner || !inner.value) continue;
202
+
203
+ const clean = inner.value.replace(/\./g, '').toUpperCase();
204
+ if (/^\d{7,8}-[\dK]$/.test(clean)) {
205
+ return clean;
206
+ }
207
+ } catch {
208
+ // OID no parseable en este altName — seguir con el resto y caer al fallback
209
+ }
210
+ }
211
+
212
+ return null;
213
+ }
214
+
215
+ /**
216
+ * Extraer RUT del certificado (desde SAN OID, serialNumber, OU o CN)
183
217
  * @param {forge.pki.Certificate} cert - Certificado
184
218
  * @returns {string|null} RUT o null
185
219
  */
186
220
  function extractRutFromCertificate(cert) {
221
+ const sanRut = extractRutFromSan(cert);
222
+ if (sanRut) return sanRut;
223
+
187
224
  const subject = extractSubjectFields(cert);
188
225
 
189
226
  // Intentar desde serialNumber (más confiable)