# @devlas/dte-sii

**Librería de facturación y boletas electrónicas para el SII de Chile.**

Genera, timbra, firma y envía facturas electrónicas, boletas electrónicas, libros contables y automatiza el proceso de certificación ante el SII.

> Desarrollada por [Estudio Devlas](https://devlas.cl), de forma independiente y abierta.
>
> Hecha en Chile, para quienes facturan en Chile. Si te ha sido útil, puedes [apoyar su desarrollo](https://github.com/sponsors/devlas-cl).

---

## Instalación

```bash
npm install @devlas/dte-sii
```

---

## Tabla de contenidos

- [Tipos de DTE soportados](#tipos-de-dte-soportados)
- [Uso rápido](#uso-rápido)
- [Flujo completo: Factura Electrónica (tipo 33)](#flujo-completo-factura-electrónica-tipo-33)
- [Boletas electrónicas](#boletas-electrónicas)
- [Libros electrónicos y RCOF](#libros-electrónicos-y-rcof)
- [Gestión de folios](#gestión-de-folios)
- [Sesión y autenticación con el SII](#sesión-y-autenticación-con-el-sii)
- [Descargar el XML completo desde el portal (Respaldo MIPYME)](#descargar-el-xml-completo-desde-el-portal-respaldo-mipyme)
- [Aceptación y reclamo de DTE (WsReclamo)](#aceptación-y-reclamo-de-dte-wsreclamo)
- [Estados SII: Interpretación de respuestas](#estados-sii-interpretación-de-respuestas)
- [Manejo de errores](#manejo-de-errores)
- [Configuración global y reintentos](#configuración-global-y-reintentos)
- [Utilidades](#utilidades)
- [Uso desde proyectos ESM (interop)](#uso-desde-proyectos-esm-interop)
- [TypeScript](#typescript)
- [Referencia de clases](#referencia-de-clases)
- [Estructura de archivos](#estructura-de-archivos)
- [Certificación SII](#certificación-sii)
- [Depuración: captura de llamadas al SII](#depuración-captura-de-llamadas-al-sii)
- [Ambientes](#ambientes)
- [Contribuir](#contribuir)
- [Seguridad](#seguridad)
- [Apoyar el proyecto](#apoyar-el-proyecto)
- [Licencia](#licencia)

---

## Tipos de DTE soportados

| Tipo | Documento |
|------|-----------|
| `33` | Factura Electrónica |
| `34` | Factura No Afecta o Exenta Electrónica |
| `39` | Boleta Electrónica Afecta |
| `41` | Boleta Electrónica Exenta |
| `43` | Liquidación Factura |
| `46` | Factura de Compra |
| `52` | Guía de Despacho |
| `56` | Nota de Débito |
| `61` | Nota de Crédito |

---

## Uso rápido

```javascript
const { Certificado, CAF, DTE, EnvioDTE, EnviadorSII } = require('@devlas/dte-sii')
const fs = require('fs')

const cert = new Certificado(fs.readFileSync('empresa.pfx'), 'contraseña')
const caf  = new CAF(fs.readFileSync('caf_33.xml', 'utf8'))

const dte = new DTE({
  Encabezado: {
    IdDoc:   { TipoDTE: 33, Folio: 1 },
    Emisor:  { RUTEmisor: '76543210-K', RznSoc: 'Mi Empresa SpA', GiroEmis: 'Software', DirOrigen: 'Av. Ejemplo 123', CmnaOrigen: 'Santiago', Acteco: 620200 },
    Receptor: { RUTRecep: '12345678-9', RznSocRecep: 'Cliente SA', GiroRecep: 'Comercio', DirRecep: 'Calle 456', CmnaRecep: 'Providencia' },
  },
  Detalle: [
    { NmbItem: 'Servicio de desarrollo', QtyItem: 1, PrcItem: 100000 },
  ],
})

dte.generarXML().timbrar(caf).firmar(cert)

const envio = new EnvioDTE({ certificado: cert })
envio.agregar(dte)
envio.setCaratula({ RutEmisor: '76543210-K', RutReceptor: '60803000-K', FchResol: '2024-01-15', NroResol: 123 })
envio.generar()

const enviador = new EnviadorSII(cert, 'produccion') // o 'certificacion'
const resultado = await enviador.enviarDteSoap(envio)
console.log('TrackID:', resultado.trackId)
```

---

## Flujo completo: Factura Electrónica (tipo 33)

### 1. Cargar certificado y CAF

```javascript
const { Certificado, CAF } = require('@devlas/dte-sii')
const fs = require('fs')

const cert = new Certificado(fs.readFileSync('empresa.pfx'), 'clave_pfx')
// cert.getPrivateKeyPem()   → PEM de la llave privada
// cert.getCertificatePem()  → PEM del certificado público

const caf = new CAF(fs.readFileSync('caf_33.xml', 'utf8'))
// caf.getRutEmisor()        → RUT del emisor
// caf.getTipoDTE()          → 33
// caf.getFolioDesde()       → primer folio autorizado
// caf.getFolioHasta()       → último folio autorizado
// caf.isFolioValido(folio)  → boolean
```

### 2. Crear el DTE

```javascript
const { DTE } = require('@devlas/dte-sii')

// Formato simplificado (calcula totales automáticamente)
const dte = new DTE({
  tipo: 33,
  folio: 1,
  emisor: {
    rut: '76543210-K', razonSocial: 'Mi Empresa SpA',
    giro: 'Desarrollo de software', direccion: 'Av. Ejemplo 123',
    comuna: 'Santiago', actividadEconomica: 620200,
  },
  receptor: {
    rut: '12345678-9', razonSocial: 'Cliente SA',
    giro: 'Comercio', direccion: 'Calle 456', comuna: 'Providencia',
  },
  items: [
    { nombre: 'Licencia anual', cantidad: 1, precio: 100000 },
    { nombre: 'Soporte técnico', cantidad: 3, precio: 15000 },
  ],
  resolucion: { fecha: '2024-01-15', numero: 123 },
})

// O bien formato estructurado con XML SII estándar
const dte = new DTE({
  Encabezado: { IdDoc: { TipoDTE: 33, Folio: 1 }, Emisor: { ... }, Receptor: { ... }, Totales: { ... } },
  Detalle: [ { NmbItem: 'Producto', QtyItem: 2, PrcItem: 50000 } ],
  Referencia: [ { TpoDocRef: 61, FolioRef: 5, RazonRef: 'Anula factura' } ], // opcional
})

// Generar XML → timbrar → firmar (chainable)
dte.generarXML().timbrar(caf).firmar(cert)

// Obtener el XML final
console.log(dte.getXML())
```

### 3. Crear sobre y enviar

```javascript
const { EnvioDTE, EnviadorSII } = require('@devlas/dte-sii')

const envio = new EnvioDTE({ certificado: cert })
envio.agregar(dte)
envio.setCaratula({
  RutEmisor:   '76543210-K',
  RutReceptor: '60803000-K', // RUT del SII para envíos propios
  FchResol:    '2024-01-15',
  NroResol:    123,
})
envio.generar()

const enviador = new EnviadorSII(cert, 'produccion')
const resultado = await enviador.enviarDteSoap(envio)
// resultado.trackId   → ID para consultar el estado
// resultado.estado    → 'EPR', 'REC', etc.
// resultado.glosa     → mensaje SII
```

### 4. Consultar estado del envío

```javascript
// Estado del sobre (EnvioDTE)
const estadoSobre = await enviador.consultarEstado({
  trackId: resultado.trackId,
  rutEmisor: '76543210-K',
})
// estadoSobre.esExitoso / esIntermedio / esRechazado
// estadoSobre.codigo   → 'EPR', 'RPR', 'RSC', etc.

// Estado de un DTE individual
const estadoDte = await enviador.consultarEstadoDte({
  rutEmisor:   '76543210-K',
  rutReceptor: '12345678-9',
  tipoDte:     33,
  folio:       1,
  fechaEmision: '2024-06-15',
  montoDte:    145000,
})
// estadoDte.codigo → 'DOK', 'DNK', 'FAU', etc.
```

---

## Boletas electrónicas

> **Recomendado para CERTIFICAR boletas: usar `CertRunner` + `BoletaCert`, no el camino manual.**
>
> Para el proceso de certificación de boletas ante el SII, use el orquestador de
> alto nivel (`require('@devlas/dte-sii/cert')` → `CertRunner` / `BoletaCert`). Ese
> camino descarga el set, genera y firma las boletas, arma el `EnvioBOLETA` + RCOF,
> obtiene el TrackId y declara el cumplimiento. Es el flujo probado end-to-end.
>
> El camino manual de bajo nivel (`DTE` + `EnvioBOLETA` + `EnviadorSII`) que se
> muestra más abajo funciona, pero es fácil romperlo. **Error frecuente:** "optimizar"
> quitando los `xmlns` del `<DTE>` por parecer redundantes con el sobre. NO lo haga:
> el `xmlns="http://www.sii.cl/SiiDte"` del `<DTE>` es parte del documento firmado
> (se hereda al `<Documento>` al calcular el DigestValue). Si lo quita, el SII
> recalcula el digest con el namespace y no coincide → rechazo por "firma inválida".
> Ver [Certificación SII](#certificación-sii).

> **Diferencia crítica por ambiente**
>
> - **Producción** → usar la API **REST** del SII (`enviarBoleta`) con los datos reales de resolución de la empresa.
> - **Certificación** → la API REST **no funciona** para el proceso de certificación SII. Usar **SOAP** (`enviarDteSoap`) con resolución `NroResol: 0` y la fecha de resolución de certificación que entrega el SII.
>
> Usar los datos de empresa incorrectos para el ambiente (por ejemplo, datos de producción en certificación) provoca rechazo inmediato del SII.

---

### Datos de resolución por ambiente

| Campo | Certificación | Producción |
|-------|--------------|------------|
| `NroResol` | `0` (siempre cero en cert.) | Número real de resolución SII |
| `FchResol` | Fecha entregada por el SII al iniciar certificación | Fecha real de la resolución |
| Método de envío | `enviarDteSoap` (SOAP) | `enviarBoleta` (REST) |

Para obtener la fecha y número de resolución de producción automáticamente desde el portal SII, ver [`SiiPortalAuth`](#sesión-y-autenticación-con-el-sii).

---

### Certificación: SOAP (obligatorio)

```javascript
const { DTE, CAF, Certificado, EnvioBOLETA, EnviadorSII } = require('@devlas/dte-sii')
const fs = require('fs')

const cert = new Certificado(fs.readFileSync('empresa_cert.pfx'), 'clave')
const caf  = new CAF(fs.readFileSync('caf_39_cert.xml', 'utf8'))

const dte = new DTE({ tipo: 39, folio: 1, emisor: { rut: '76543210-K', ... }, items: [ ... ] })
dte.generarXML().timbrar(caf).firmar(cert)

const envio = new EnvioBOLETA({ certificado: cert })
envio.agregar(dte)
envio.setCaratula({
  RutEmisor: '76543210-K',
  FchResol:  '2019-10-18',  // fecha de resolución de certificación (entregada por el SII)
  NroResol:  0,             // siempre 0 en certificación
})
envio.generar()

// SOAP - único método que funciona para certificación de boletas
const enviador = new EnviadorSII(cert, 'certificacion')
const resultado = await enviador.enviarDteSoap(envio)
console.log('TrackID:', resultado.trackId)
```

### Producción: REST (recomendado)

```javascript
const { DTE, CAF, Certificado, EnvioBOLETA, EnviadorSII } = require('@devlas/dte-sii')
const fs = require('fs')

const cert = new Certificado(fs.readFileSync('empresa_prod.pfx'), 'clave')
const caf  = new CAF(fs.readFileSync('caf_39_prod.xml', 'utf8'))

const dte = new DTE({ tipo: 39, folio: 1, emisor: { rut: '76543210-K', ... }, items: [ ... ] })
dte.generarXML().timbrar(caf).firmar(cert)

const envio = new EnvioBOLETA({ certificado: cert })
envio.agregar(dte)
envio.setCaratula({
  RutEmisor: '76543210-K',
  FchResol:  '2024-01-15',  // fecha real de resolución SII de la empresa
  NroResol:  123,           // número real de resolución SII de la empresa
})
envio.generar()

// REST - método estándar para producción
const enviador = new EnviadorSII(cert, 'produccion')
const resultado = await enviador.enviarBoleta(envio)
console.log('TrackID:', resultado.trackId)
```

### Flujo con BoletaService

`BoletaService` simplifica la creación de boletas individuales. Aplica el mismo criterio de ambiente: usar `enviarDteSoap` para certificación y `enviarBoleta` para producción una vez que el servicio retorne el sobre.

```javascript
const { BoletaService } = require('@devlas/dte-sii')
const fs = require('fs')

const service = new BoletaService()
service.cargarCertificado(fs.readFileSync('empresa.pfx'), 'clave_pfx')
service.cargarCAF(fs.readFileSync('caf_39.xml', 'utf8'))

const boleta = await service.crearBoleta({
  folio:      1,
  emisor:     { rut: '76543210-K', razonSocial: 'Mi Empresa', giro: 'Software', ... },
  items:      [{ nombre: 'Producto', cantidad: 1, precioConIva: 10000 }],
  resolucion: {
    fecha:  process.env.SII_AMBIENTE === 'certificacion' ? '2019-10-18' : '2024-01-15',
    numero: process.env.SII_AMBIENTE === 'certificacion' ? 0           : 123,
  },
})
```

---

## Libros electrónicos y RCOF

### LibroCompraVenta

```javascript
const { LibroCompraVenta, Certificado } = require('@devlas/dte-sii')

const libro = new LibroCompraVenta()
libro.setCaratula({
  RutEmisorLibro: '76543210-K',
  RutEnvia:       '76543210-K',
  PeriodoTributario: '2024-06',
  FchResol: '2024-01-15', NroResol: 123,
  TipoOperacion: 'VENTA',  // o 'COMPRA'
  TipoLibro: 'MENSUAL',
  TipoEnvio: 'TOTAL',
  FolioNotificacion: 0,
})
libro.setResumen({ /* datos del resumen */ })
libro.setDetalle([ /* array de documentos */ ])
libro.generar().firmar(cert)

const enviador = new EnviadorSII(cert, 'produccion')
await enviador.enviarLibroSoap(libro)
```

### ConsumoFolio (RCOF)

El RCOF es obligatorio para boletas y debe enviarse **antes de las 08:00 del día siguiente**.

```javascript
const { ConsumoFolio, CAF, Certificado } = require('@devlas/dte-sii')

const rcof = new ConsumoFolio()
rcof.setCaratula({
  RutEmisor:  '76543210-K',
  FchResol:   '2024-01-15',
  NroResol:   0,
  FchInicio:  '2024-06-15',
  FchFinal:   '2024-06-15',
  SecEnvio:   1,
  TmstFirmaEnv: new Date().toISOString(),
})
rcof.agregar(dte, caf)
rcof.generar().firmar(cert)

const enviador = new EnviadorSII(cert, 'produccion')
await enviador.enviarRcofSoap(rcof)
```

---

## Gestión de folios

Los folios son el recurso más crítico del ciclo de facturación: sin folio válido no hay DTE. La librería provee tres capas que se complementan:

| Capa | Clase | Responsabilidad |
|------|-------|-----------------|
| **Local** | `FolioRegistry` | Asigna y persiste folios desde un CAF ya descargado |
| **SII** | `FolioService` | Consulta, solicita y anula rangos de folios ante el SII |
| **Automática** | `CafSolicitor` | Descarga el XML del CAF nuevo directamente desde el portal SII |

El flujo típico de producción combina las tres: `FolioRegistry` asigna folios del CAF activo; cuando el rango se agota, `CafSolicitor` solicita un CAF nuevo al SII sin intervención humana; si quedan folios sin usar de un CAF anterior, `FolioService` los anula para mantener la contabilidad en orden.

---

### FolioRegistry: registro local

`FolioRegistry` mantiene un JSON en disco que registra qué folios están reservados, usados o pendientes de confirmación. Previene la doble asignación incluso ante reinicios del proceso.

```javascript
const { FolioRegistry, CAF, createCafFingerprint } = require('@devlas/dte-sii')
const fs = require('fs')

const registry    = new FolioRegistry()                    // persiste en disco (JSON)
const cafXml      = fs.readFileSync('caf_33.xml', 'utf8')
const caf         = new CAF(cafXml)
const fingerprint = createCafFingerprint(cafXml)           // hash único del CAF

// Reservar el siguiente folio disponible del rango del CAF
const folio = registry.reserveNextFolio({
  rutEmisor:      '76543210-K',
  tipoDte:        caf.getTipoDTE(),
  folioDesde:     caf.getFolioDesde(),
  folioHasta:     caf.getFolioHasta(),
  ambiente:       'produccion',
  cafFingerprint: fingerprint,
})

// ... generar y enviar el DTE ...

// Marcar folio como enviado al recibir trackId del SII
registry.markFolioSent({
  rutEmisor: '76543210-K', tipoDte: 33, folio,
  folioDesde: caf.getFolioDesde(), folioHasta: caf.getFolioHasta(),
  ambiente: 'produccion', cafFingerprint: fingerprint,
  trackId: '0245283324',
})
```

`resolveCafPath` busca automáticamente el CAF más reciente disponible con folios libres, evitando la necesidad de hardcodear rutas:

```javascript
const { resolveCafPath } = require('@devlas/dte-sii')

const cafPath = resolveCafPath({
  tipoDte:       33,
  rutEmisor:     '76543210-K',
  requiredCount: 1,          // necesito al menos 1 folio disponible
  ambiente:      'produccion',
})
const cafXml = fs.readFileSync(cafPath, 'utf8')
```

---

### FolioService: consulta, solicitud y anulación ante el SII

`FolioService` se comunica directamente con el SII para operar sobre folios: consultar el estado actual, solicitar un nuevo rango o anular folios no utilizados.

```javascript
const { FolioService, Certificado } = require('@devlas/dte-sii')

const service = new FolioService({
  ambiente:    'produccion',
  rutEmisor:   '76543210-K',
  certificado: new Certificado(fs.readFileSync('empresa.pfx'), 'clave'),
})

// Consultar cuántos folios quedan y cuál fue el último emitido
const info = await service.consultarFolios({ tipoDte: 33 })
console.log('Último folio final:', info.ultimoFolioFinal)
console.log('Folios disponibles:', info.foliosDisponibles)

// Solicitar un nuevo rango de folios al SII
await service.solicitar({ tipoDte: 33, cantidad: 100 })

// Anular folios que nunca se usaron (evita descuadres en el SII)
await service.anularFolios({
  tipoDte:    33,
  folioDesde: 50,
  folioHasta: 60,
  motivo:     'Folios no utilizados por cambio de CAF',
})
```

---

### CafSolicitor: obtención automática de CAF

`CafSolicitor` automatiza la descarga del XML del CAF desde el portal SII usando el certificado PFX, sin intervención manual. Es la pieza que cierra el ciclo de reposición automática de folios.

```javascript
const { CafSolicitor, Certificado } = require('@devlas/dte-sii')

const solicitor = new CafSolicitor({
  certificado: new Certificado(fs.readFileSync('empresa.pfx'), 'clave'),
  ambiente:    'produccion',
})

const cafXml = await solicitor.solicitar({ tipoDte: 33, cantidad: 200 })
fs.writeFileSync('caf_33_nuevo.xml', cafXml)
```

---

### Ciclo completo automatizado

El siguiente patrón implementa reposición y limpieza de folios sin intervención humana. Se recomienda ejecutarlo como un job periódico o al detectar que el CAF activo está por agotarse.

```javascript
const {
  FolioRegistry, FolioService, CafSolicitor, CAF,
  Certificado, createCafFingerprint, resolveCafPath,
} = require('@devlas/dte-sii')
const fs   = require('fs')
const path = require('path')

const CAF_DIR    = path.join(__dirname, 'cafs')
const RUT        = '76543210-K'
const AMBIENTE   = 'produccion'
const TIPO_DTE   = 33
const UMBRAL     = 10   // solicitar nuevo CAF cuando queden menos de N folios
const CANTIDAD   = 200  // folios a solicitar

const cert      = new Certificado(fs.readFileSync('empresa.pfx'), 'clave')
const registry  = new FolioRegistry()
const service   = new FolioService({ ambiente: AMBIENTE, rutEmisor: RUT, certificado: cert })
const solicitor = new CafSolicitor({ certificado: cert, ambiente: AMBIENTE })

async function gestionarFolios() {
  // 1. Consultar estado actual en el SII
  const info = await service.consultarFolios({ tipoDte: TIPO_DTE })
  console.log(`Folios disponibles: ${info.foliosDisponibles}`)

  // 2. Solicitar nuevo CAF si quedan pocos folios
  if (info.foliosDisponibles < UMBRAL) {
    console.log('Solicitando nuevo CAF...')
    const cafXml  = await solicitor.solicitar({ tipoDte: TIPO_DTE, cantidad: CANTIDAD })
    const archivo = path.join(CAF_DIR, `caf_${TIPO_DTE}_${Date.now()}.xml`)
    fs.writeFileSync(archivo, cafXml)
    console.log(`Nuevo CAF guardado en: ${archivo}`)
  }

  // 3. Detectar folios reservados pero nunca enviados (caídos en error)
  //    y anularlos en el SII para mantener la contabilidad limpia
  const pendientes = registry.getFoliosPendientes({ rutEmisor: RUT, tipoDte: TIPO_DTE, ambiente: AMBIENTE })
  for (const rango of pendientes) {
    console.log(`Anulando folios caídos: ${rango.desde}-${rango.hasta}`)
    await service.anularFolios({
      tipoDte:    TIPO_DTE,
      folioDesde: rango.desde,
      folioHasta: rango.hasta,
      motivo:     'Folios reservados no emitidos por error de sistema',
    })
  }
}

// Ejecutar al inicio y luego cada hora
gestionarFolios().catch(console.error)
setInterval(() => gestionarFolios().catch(console.error), 60 * 60 * 1000)
```

**Puntos clave del ciclo automatizado:**

- `FolioRegistry` detecta folios que fueron reservados pero cuyo DTE nunca se envió exitosamente (por crash, timeout, etc.)
- `FolioService.anularFolios` limpia esos folios en el SII, previniendo descuadres en libros y RCOF
- `CafSolicitor` descarga el nuevo CAF directamente, sin necesidad de acceder al portal SII manualmente
- `resolveCafPath` hace que el código de emisión siempre use el CAF vigente, sin cambiar rutas hardcodeadas

---

## Sesión y autenticación con el SII

### SiiPortalAuth: autenticación al portal

Obtiene datos de empresa (nro_resol, fch_resol) directamente desde el portal SII usando el certificado PFX. Usa el patrón Singleton por certificado para evitar el límite de sesiones del SII.

```javascript
const { SiiPortalAuth, Certificado } = require('@devlas/dte-sii')

const cert  = new Certificado(fs.readFileSync('empresa.pfx'), 'clave')
const auth  = new SiiPortalAuth(cert)

await auth.autenticar()
const datos = await auth.obtenerDatosEmpresa()
// datos.nro_resol   → número de resolución
// datos.fch_resol   → fecha de resolución (AAAA-MM-DD)
// datos.razon_social, datos.giro, etc.

// Reutilizar sesión entre componentes (evita múltiples logins)
const cookies = await SiiPortalAuth.getCookieStringForPfx(cert)
```

#### Caché de sesión: un mapa por certificado

Un login contra `zeusr.sii.cl` es un handshake con certificado, **caro y contado por el SII**,
que bloquea el RUT por *"máximo de sesiones autenticadas"*. Por eso las cookies se cachean en
disco, en `$DATADIR/sii_session_cache.json`, con un TTL de 90 minutos.

Desde **2.16.0 el caché es un mapa por huella de certificado**. Antes guardaba una sola sesión,
así que en un servidor multi-tenant cada certificado invalidaba al anterior y **todos**
re-autenticaban en cada pasada: el costo crecía lineal con la base de clientes.

- El formato viejo se **migra**, no se descarta.
- Poda automática: expiradas primero, y tope de 200 entradas.
- Escritura atómica y relectura previa, para que dos réplicas sobre el mismo volumen no se
  borren las sesiones entre sí.

```javascript
SiiPortalAuth.limpiarSesionCache(certHash)   // borra una
SiiPortalAuth.limpiarSesionCache()           // borra todas
```

> ⚠️ **En un servidor, apunta `DATADIR` a un volumen persistente.** Sin eso el caché vive en el
> filesystem del contenedor y se pierde en cada redeploy, forzando un re-login de toda la base.

#### Varias réplicas: sesión y estado compartidos

Con un solo proceso basta el archivo y el mutex de siempre. Con varias réplicas hay que compartir
dos cosas, y cada una tiene su puerto (ver `SiiSessionPorts.js`):

| Puerto | Qué guarda | Adaptadores incluidos |
|---|---|---|
| `SessionStore` y `SessionLock` | La sesión del portal por certificado y quién la usa ahora | `MemorySessionStore`, `MemorySessionLock` y, por defecto, archivo y mutex en el proceso |
| `StateStore` | Folios anulados, período de libros, totales LTC y folios usados en certificación | `MemoryStateStore`, `FileStateStore` (por defecto, en `stateDir`) |

```javascript
SiiPortalAuth.configurarSesion({ store, lock, estado })  // los tres son opcionales
SiiPortalAuth.restablecerSesion()                        // vuelve a archivo y mutex en proceso
```

Con un `SessionStore` configurado, `SiiSession` (y con ella `FolioService`, `CafSolicitor` y
`SiiCertificacion`) toma el lock del certificado, **reutiliza la sesión guardada en vez de
autenticarse otra vez** y la guarda al terminar. `SiiSession` y `SiiPortalAuth` identifican el
certificado con la misma huella, así que hay **una sola sesión por certificado**. No hace falta
`sessionPath`.

`StateStore` es un almacén de documentos JSON por clave (`load`, `save`, `remove`). Un consumidor
que no configure nada sigue leyendo y escribiendo los mismos archivos de antes.

> `FolioRegistry` (el control local de folios de `FolioService.getNextFolio`) sigue en archivo: su
> API es síncrona. Un servicio que lleve ese control en su propia base no lo usa.

#### Reintento ante fallas de red/TLS

`autenticar()` reintenta 3 veces con espera progresiva (1s, 2s, 4s) ante errores de transporte.

> ⚠️ El SII devuelve **`EPROTO` de forma intermitente** al abrir la conexión TLS con certificado
> (`rsa_pss ... last octet invalid`), y reintentando con el **mismo** certificado funciona. **No
> es señal de certificado vencido ni no habilitado**, aunque lo parezca. Interpretarlo así marca
> como rotos certificados que están sanos.

El **límite de sesiones nunca se reintenta** (cada intento empeora el bloqueo) y se distingue por
`err.code === 'SII_LIMITE_SESIONES'`.

Dos códigos más indican un trámite pendiente del contribuyente, no un problema de la
librería ni de la conexión:

| `err.code` | Qué significa | Qué hacer |
|---|---|---|
| `CERTIFICADO_NO_HABILITADO` | El certificado nunca se habilitó como método de autenticación. El SII responde 200 con un `alert()` de JavaScript, no con un error HTTP. No se reintenta. | En sii.cl: Clave Tributaria, Cambiar clave, y habilitar la autenticación con Certificado Digital. |
| `EMPRESA_NO_AUTORIZADA` | La empresa no está autorizada a operar en ese ambiente. Aparece al leer los datos del contribuyente. | Terminar la certificación (en producción no queda autorizada antes) o correr la Postulación/Enrolamiento. |

`CafSolicitor.solicitar()` devuelve `EMPRESA_NO_AUTORIZADA` en `errorCode` por el mismo motivo.

### SiiSession: sesiones HTTP autenticadas

```javascript
const { SiiSession, Certificado } = require('@devlas/dte-sii')

const session = new SiiSession(new Certificado(fs.readFileSync('empresa.pfx'), 'clave'))
await session.loginWithCertificate()
const resp = await session.request('GET', 'https://herculesr.sii.cl/...')
```

---

## RCV del portal: qué función usar para cada caso

El portal del SII expone el Registro de Compras y Ventas por servicios distintos, y cada uno
entrega cosas diferentes. Ninguno reemplaza a los otros:

| Necesito | Función | Servicio del portal |
|---|---|---|
| Listar cada documento del período (folio, fecha, montos), sin boletas | `obtenerDetalleDtes()` | `consemitidosinternetui` |
| Totales oficiales por tipo de documento, incluidas las boletas (agregadas por mes), y estado contable | `obtenerResumenRegistro()` | `consdcvinternetui` |
| Clasificación de un documento de compra y cambio de su tipo de compra | `obtenerDetalleDocumentoCompra()` y relacionadas | `complementoscvui` |
| XML completo del documento (ítems, referencias, TED) | `descargarRespaldoMipyme()` | Respaldo MIPYME |

Puntos a tener en cuenta:

- `obtenerDetalleDtes()` **no ve boletas**: consulta un sistema del SII que nunca las indexó. Si la
  usa para verificar boletas obtendrá 0 aunque el SII las haya aceptado. Use
  `obtenerResumenRegistro()`.
- `obtenerResumenRegistro()` entrega **totales por tipo de documento, no documentos individuales**.
  Su detalle por documento (`getDetalleCompra`, `getDetalleVenta`) exige reCAPTCHA v3 y esta
  librería no lo automatiza.
- Por eso `obtenerDetalleDtes()` sigue siendo la única vía de listado por documento sin captcha.
  No está obsoleta, tiene esa limitación.
- Solo `obtenerDetalleDtes()` informa el evento del receptor (`dehOrdenEvento`: 2 acuse de recibo,
  3 aceptación, 4 reclamo). No se verificó si `consdcvinternetui` entrega un equivalente.

---

## Descargar el XML completo desde el portal (Respaldo MIPYME)

`descargarRespaldoMipyme()` baja el **XML firmado completo** de los DTE emitidos o recibidos
desde el "Respaldo de archivos MIPYME" del portal (`www1.sii.cl/cgi-bin/Portal001`).

Es la única vía que entrega el **documento entero**: detalle línea por línea, `CdgItem` del
proveedor, referencias y TED. `obtenerDetalleDtes()` solo trae metadatos y
`obtenerResumenRegistro()` solo totales mensuales.

**Requiere únicamente el certificado digital**, no estar certificado como emisor.

> 🔴 **Existe SOLO en producción: no hay ambiente de certificación.** Medido sobre
> `/cgi-bin/Portal001/lista_documentos.cgi`: `www1.sii.cl` responde 200, `maullin.sii.cl`
> redirige a `Error404` y `www4c.sii.cl` da 404 (para contrastar, una ruta real de maullin
> redirige al login de certificación, no a un 404).
>
> Consecuencia para cualquier consumidor: con el resto del sistema apuntando a maullin, este
> método **igual lee documentos reales del contribuyente**. Es de solo lectura contra el SII,
> pero escribe facturas reales en la base del entorno que lo llame. Un entorno de desarrollo
> necesita una puerta explícita; no alcanza con mirar la variable de ambiente del DTE, porque
> esta función no tiene ambientes.

```javascript
const auth = new SiiPortalAuth({ pfxBuffer, pfxPassword })

const { total, tramos } = await auth.descargarRespaldoMipyme('76543210', '6', {
  origen: 'RCP',            // 'ENV' emitidos | 'RCP' recibidos
  desde:  '2026-01-01',
  hasta:  '2026-08-18',
  tipoDoc: '',              // vacío = todos
  reintentos: 3,
})
// tramos: [{ desde, hasta, total, xml }, ...] — un XML por tramo
```

### Modo streaming (`onTramo`) — obligatorio para históricos grandes

Sin `onTramo` **todos los XML quedan en memoria hasta el final**: ~6,4 KB por documento, o sea
unos 7 MB para 1.100 documentos, y crece lineal.

```javascript
await auth.descargarRespaldoMipyme(rut, dv, {
  origen: 'RCP', desde, hasta,
  onTramo: async (t) => { await guardar(t.xml) },   // se persiste y se suelta
})
// con onTramo, los tramos del resultado vienen SIN `xml`
```

### Qué hay que saber del portal

| | |
|---|---|
| **Tope de 20 por descarga** | Es del servidor, no cosmético. Con 21 devuelve **HTML de error**, no un XML recortado. El método trocea el rango solo, del **más reciente al más viejo**. |
| **Un día con más de 20** | No se puede partir más por fecha: se lanza error explícito. Tiene salida cortando por `TPO_DOC` (un tipo de DTE por consulta). ⚠️ **No** usar `FOLIO`/`FOLIOHASTA`: borran `FEC_HASTA` en silencio y devuelven otro conjunto. |
| **Encoding** | El XML viene en **ISO-8859-1**. Leerlo como utf8 rompe los acentos. |
| **Captcha** | Hoy va vacío, pero el SII puede encenderlo sin avisar → `RESPALDO_CAPTCHA`. |
| **Alcance** | Solo lo registrado en el sistema de facturación **gratuito** del SII. Un comercio que ya migró a otro sistema no encuentra ahí sus documentos nuevos. |

> 🔴 **El portal viejo comunica sus rechazos por `alert()` de JavaScript, con HTTP 200** — no en
> el HTML visible ni en el `<title>`, que dice otra cosa. Y la página **válida** trae además un
> `//alert(...)` **comentado**. Clasificar por título, limpiar los `<script>` antes de parsear, o
> creerle al alert comentado: las tres cosas producen diagnósticos falsos.

Los errores traen **`err.mensajePortal`** con el texto exacto del SII. **Mostrar ese texto, no
una traducción propia.**

⚠️ **`SIN_DATOS` no es lo mismo que `INDETERMINADO`.** El primero es un veredicto definitivo
("acá no hay nada") y el consumidor puede cerrar ese período para siempre. Los otros dos
significan "no se pudo concluir" y "no pudimos preguntar": tratarlos igual cerró en falso un
período que ya tenía 24 documentos bajados.

| código | qué pasó | ¿reintentar? |
|---|---|---|
| `RESPALDO_SIN_DATOS` | el RUT no tiene información en MIPYME | no |
| `RESPALDO_CAPTCHA` | el SII encendió el captcha | no |
| `RESPALDO_RECHAZADO` | rechazo con un texto que no conocemos; llega literal | no |
| `RESPALDO_INDETERMINADO` | **varios** mensajes del template, sin veredicto único; llegan en `err.mensajesPortal` | sí, más tarde |
| `RESPALDO_SIN_EMPRESA` | página de ingreso sin ningún alert; causa no determinada | no |

Contrato completo, respuestas reales y los errores que cuestan tiempo (es **POST** no GET;
`ORIGEN=ENV` no `EMI`; el listado es obligatorio antes de la descarga) en
la sección de arriba.

---

## Aceptación y reclamo de DTE (WsReclamo)

`WsReclamo` implementa el web service `WSRECLAMO` del SII (v1.2) para registrar eventos de aceptación/rechazo de DTE por parte del receptor.

> `WsReclamo` no se re-exporta desde `index.js`. Importar directamente:

```javascript
const WsReclamo = require('@devlas/dte-sii/WsReclamo')
const { Certificado } = require('@devlas/dte-sii')

const ws = new WsReclamo(new Certificado(fs.readFileSync('empresa.pfx'), 'clave'), 'produccion')

// Consultar historial de eventos de un DTE
const eventos = await ws.listarEventosHistDoc({
  rutEmisor:   '76543210-K',
  tipoDTE:     33,
  folio:       1,
  rutReceptor: '12345678-9',
})

// Consultar estado desde la perspectiva del receptor
const estado = await ws.consultarEstadoReceptor({ ... })

// Registrar aceptación (ACD) o reclamo (RCD)
await ws.ingresarAceptacion({
  rutEmisor: '76543210-K', tipoDTE: 33, folio: 1,
  accion: 'ACD', // ACD=Aceptado, RCD=Reclamado, ERM=Otorga Mercaderías
})
```

---

## Estados SII: Interpretación de respuestas

`EnviadorSII` clasifica automáticamente cada código en `esExitoso`, `esIntermedio` o `esRechazado`.

### QueryEstUp - Estado del sobre de envío

| Código | Descripción | `esExitoso` | `esIntermedio` | `esRechazado` |
|--------|-------------|:-----------:|:--------------:|:-------------:|
| `EPR`  | Envío Procesado | ✓ | | |
| `RPR`  | Procesado con Reparos | ✓ | | |
| `REC` / `SOK` / `FOK` / `CRT` / `PRD` / `PDR` | En proceso de validación | | ✓ | |
| `RSC`  | Error en Schema XML | | | ✓ |
| `RFR`  | Error en Firma Digital | | | ✓ |
| `RCT`  | Error en Carátula | | | ✓ |

### QueryEstDte - Estado del DTE individual

| Código | Descripción | Clasificación |
|--------|-------------|---------------|
| `DOK`  | Datos coinciden | ✓ Exitoso |
| `DNK`  | Datos no coinciden | ~ Intermedio |
| `FAU`  | Folio no autorizado | ✗ Rechazado |
| `FNA`  | Emisor no habilitado | ✗ Rechazado |
| `FAN` / `AND` / `ANC` | Anulado | ✗ Rechazado |
| `EMP`  | Empresa sin autorización | ✗ Rechazado |

### Códigos de error de consulta (-1 a -14)

Los valores negativos son **errores del servidor de consulta del SII**, no rechazo del documento. Resultan en `esIntermedio = true` y pueden reintentarse.

---

## Manejo de errores

Todos los errores operacionales lanzan `DteSiiError` con las propiedades:

| Propiedad | Descripción |
|-----------|-------------|
| `code`    | Código de error (`CERT_ERROR`, `DTE_ERROR`, `SII_ERROR`, etc.) |
| `message` | Mensaje descriptivo |
| `cause`   | Error original (si aplica) |

```javascript
const { DteSiiError } = require('@devlas/dte-sii')

try {
  await enviador.enviarDteSoap(envio)
} catch (err) {
  if (err instanceof DteSiiError) {
    console.error(`[${err.code}] ${err.message}`)
  }
}
```

---

## Configuración global y reintentos

```javascript
const { configure, configureRetry } = require('@devlas/dte-sii')

// Configuración global (aplicar al inicio de la app)
configure({
  ambiente:          'produccion',  // 'produccion' | 'certificacion'
  defaultRutEmisor:  '76543210-K',
  tokenCacheTtlMs:   300_000,       // 5 minutos (default)
})

// Lógica de reintentos para llamadas al SII
configureRetry({
  maxAttempts:     3,
  initialDelayMs:  1_000,
  backoffFactor:   2,
  retryOn:         ['SII_TIMEOUT', 'SII_SERVER_ERROR'],
})
```

---

## Utilidades

Accesibles como named exports o mediante el namespace `utils`:

```javascript
const {
  // RUT
  formatRut, validarRut, calcularDV, splitRut,
  // Sanitización
  sanitizeSiiText, sanitizeRazonSocial, sanitizeNombreItem,
  // XML
  parseXml, parseXmlNoNs, buildXml, formatBase64InXml,
  // Cálculos monetarios
  calcularTotalesDesdeItems, calcularMontoItem, buildDetalle,
  // Construcción de entidades
  buildEmisor, normalizeEmisor, validarEmisor,
  buildReceptor, normalizeReceptor, RECEPTOR_CONSUMIDOR_FINAL,
  buildDocReferencia, buildReferenciasNcNd,
  createResolucion, createResolucionCertificacion,
  // Constantes
  TIPOS_DTE, TIPOS_BOLETA, NOMBRES_DTE, TASA_IVA,
  // Folios
  createCafFingerprint, findLatestCaf, resolveCafPath,
  // Endpoints SII
  SOAP_ENDPOINTS, REST_ENDPOINTS, getHost,
  // Token cache
  getCachedToken, setCachedToken, pruneExpiredTokens,
  // Logging
  logger, configureLogger, createScopedLogger,
} = require('@devlas/dte-sii')
```

### Tabla de referencia rápida

| Función / Constante | Descripción |
|---------------------|-------------|
| `formatRut(rut)` | Formatea RUT chileno con puntos y guion |
| `validarRut(rut)` | Valida dígito verificador |
| `calcularDV(rut)` | Calcula dígito verificador de un RUT |
| `sanitizeSiiText(str)` | Elimina caracteres no aceptados por el SII |
| `sanitizeRazonSocial(str)` | Sanitiza razones sociales |
| `sanitizeNombreItem(str)` | Sanitiza nombres de ítems |
| `calcularTotalesDesdeItems(items)` | Calcula MntNeto, IVA, MntTotal |
| `buildEmisor(data)` | Construye nodo `<Emisor>` |
| `buildReceptor(data)` | Construye nodo `<Receptor>` |
| `RECEPTOR_CONSUMIDOR_FINAL` | Objeto receptor para boletas sin RUT receptor |
| `TASA_IVA` | `0.19` (IVA Chile) |
| `TIPOS_DTE` | `{ FACTURA: 33, FACTURA_EXENTA: 34, ... }` |
| `NOMBRES_DTE` | Mapeo código → nombre legible |
| `createCafFingerprint(xml)` | Hash único de un CAF (para FolioRegistry) |
| `findLatestCaf(tipoDte, dir)` | Busca el CAF más reciente en un directorio |
| `resolveCafPath(opts)` | Resuelve ruta de CAF con validación de folios disponibles |
| `getHost(ambiente)` | URL base del host SII según ambiente |

---

## Uso desde proyectos ESM (interop)

Esta librería es **CommonJS**. Para usarla desde un proyecto ESM (Node.js nativo o `"type": "module"`):

```javascript
// Node.js ESM puro
import { createRequire } from 'module'
const require = createRequire(import.meta.url)
const { Certificado, CAF, DTE, EnviadorSII } = require('@devlas/dte-sii')
```

**TypeScript con ESM** :

```typescript
import { createRequire } from 'module'
const _require = createRequire(import.meta.url)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { Certificado, CAF, DTE } = _require('@devlas/dte-sii') as Record<string, new (...a: any[]) => any>

// WsReclamo se importa directamente (no está en index.js):
const WsReclamo = _require('@devlas/dte-sii/WsReclamo')
```

---

## TypeScript

La librería incluye tipos completos en `dte-sii.d.ts`. Interfaces principales:

```typescript
import type {
  // Entidades
  Emisor, Receptor, DetalleItem, Totales,
  // DTEs
  DteDatos, DteSimplificado,
  // Configuración
  GlobalConfig, RetryConfig, TokenCacheConfig,
  // Errores
  DteSiiError,
} from '@devlas/dte-sii'
```

---

## Referencia de clases

### Clases principales

| Clase | Archivo | Descripción |
|-------|---------|-------------|
| `Certificado` | `Certificado.js` | Carga y gestiona certificados digitales PFX/P12; valida expiración |
| `CAF` | `CAF.js` | Parsea CAF XML; valida rango de folios; firma TED |
| `DTE` | `DTE.js` | Genera, timbra y firma documentos tributarios (todos los tipos) |
| `Signer` | `Signer.js` | Firma XML-DSig compatible con SII (C14N + RSA-SHA1) |
| `EnvioDTE` | `Envio.js` | Sobre XML para facturas, guías y notas |
| `EnvioBOLETA` | `Envio.js` | Sobre XML para boletas electrónicas |
| `EnviadorSII` | `EnviadorSII.js` | Comunicación SOAP/REST con el SII; caché de tokens; reintentos |

### Servicios y gestión de folios

| Clase | Archivo | Descripción |
|-------|---------|-------------|
| `BoletaService` | `BoletaService.js` | Flujo simplificado para crear boletas electrónicas |
| `FolioRegistry` | `FolioRegistry.js` | Registro local JSON de folios reservados/enviados |
| `FolioService` | `FolioService.js` | Consulta, solicita y anula folios ante el SII |
| `CafSolicitor` | `CafSolicitor.js` | Solicitud automatizada de CAF al SII |
| `SiiSession` | `SiiSession.js` | Sesiones HTTP autenticadas con certificado (cookie jar) |
| `SiiPortalAuth` | `SiiPortalAuth.js` | Autenticación al portal SII; datos de empresa; RCV; **respaldo MIPYME (XML completo)**; caché de sesión por certificado |

### Libros y reportes

| Clase | Archivo | Descripción |
|-------|---------|-------------|
| `ConsumoFolio` | `ConsumoFolio.js` | RCOF (Resumen Consumo de Folios) para boletas |
| `LibroCompraVenta` | `LibroCompraVenta.js` | Libro electrónico de compras/ventas |
| `LibroGuia` | `LibroGuia.js` | Libro electrónico de guías de despacho |

### Web services complementarios

| Clase | Archivo | Descripción |
|-------|---------|-------------|
| `WsReclamo` | `WsReclamo.js` | WS WSRECLAMO v1.2: aceptación, reclamo e historial de eventos de DTE |

---

## Estructura de archivos

```
dte-sii/
├── index.js              <- Punto de entrada; re-exporta todas las clases públicas
├── dte-sii.d.ts          <- Definiciones TypeScript (979 líneas)
│
├── Certificado.js        <- PFX/P12 loader
├── CAF.js                <- CAF parser
├── DTE.js                <- Document builder (XML, TED, firma)
├── Signer.js             <- XML-DSig
├── Envio.js              <- EnvioDTE + EnvioBOLETA
├── EnviadorSII.js        <- SOAP/REST con SII; caché tokens; reintentos
│
├── BoletaService.js      <- Flujo simplificado de boletas
├── SiiPortalAuth.js      <- Autenticación portal SII (Singleton)
├── SiiSession.js         <- Sesiones HTTP autenticadas
├── SiiSessionStore.js    <- Persistencia de sesiones
├── CafSolicitor.js       <- Solicitud automatizada de CAF
│
├── FolioRegistry.js      <- Registro local de folios (JSON)
├── FolioService.js       <- Gestión de folios ante el SII
│
├── LibroBase.js          <- Clase base para libros electrónicos
├── LibroCompraVenta.js   <- Libro compras/ventas
├── LibroGuia.js          <- Libro guías de despacho
├── ConsumoFolio.js       <- RCOF
│
├── WsReclamo.js          <- WS aceptación/reclamo de DTE
│
├── utils/                <- 20 módulos de utilidades (RUT, XML, cálculos, etc.)
├── cert/                 <- 19 módulos para automatización de certificación SII
└── docs/                 <- PDFs y XSDs oficiales del SII
```

---

## Certificación SII

El directorio `cert/` contiene los helpers necesarios para ejecutar el proceso de certificación ante el SII. Orquestado por `CertRunner`, incluye:

- Generación de sets básicos, de compra, exentos y guías
- Libros de compras, ventas y guías para certificación
- Envío de boletas de certificación
- Intercambio de DTE entre contribuyentes (simulación)
- Generación de muestras impresas

```javascript
// Uso desde un proyecto ESM
const { CertFolioHelper } = require('@devlas/dte-sii')
```

---

## Artefactos de la certificación entre etapas

La certificación es una cadena de etapas que se pasan archivos por disco (estructuras, resultados,
XML de los envíos). Si cada etapa corre en un proceso o una réplica distinta, o el disco no persiste
entre despliegues, esos archivos se pierden. Para eso están `hidratarArtefactos` y `volcarArtefactos`:
antes de cada etapa se **hidrata** un directorio temporal desde un almacén y, al terminar, se **vuelca**
lo nuevo o cambiado. El runner de la etapa no cambia: sigue leyendo y escribiendo archivos.

```js
const { hidratarArtefactos, volcarArtefactos } = require('@devlas/dte-sii')

const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cert-'))
const previos = await hidratarArtefactos({ store: miAlmacen, dir })

// ... corre la etapa con `dir` como directorio de trabajo ...

await volcarArtefactos({
  store: miAlmacen,
  dir,
  previos,
  aceptar: (clave) => !clave.startsWith('http/'), // qué archivos vale la pena conservar
})
```

La librería no sabe dónde viven los datos: define el puerto `ArtefactosStore` (`listar`, `leer`,
`escribir` y opcionalmente `borrar`) y cada consumidor aporta el suyo (una tabla de base de datos,
Redis, S3). `MemoryArtefactosStore` sirve de referencia y para tests.

Puntos a tener en cuenta:

- **Se preserva la fecha de modificación** de archivos y carpetas. Hay flujos que eligen "la corrida
  más reciente" por mtime; si al hidratar todo quedara con la hora de hoy, se perdería el orden.
- **Nunca se escribe fuera del directorio de trabajo**, aunque el almacén devuelva claves con `..`,
  rutas absolutas o barras invertidas.
- Solo se guarda texto: `.xml` y `.json` por defecto (`extensiones`), con un tope de 4 MB por archivo
  (`maxBytes`). Los archivos que superan el tope o no cumplen se informan en `omitidos`.
- `aceptar` es obligatorio al volcar: la librería no decide por ti qué archivos conservar.
- `transformar` y `restaurar` permiten guardar el contenido cifrado (por ejemplo, un archivo con
  cookies de sesión) y recuperarlo.

---

## Depuración: captura de llamadas al SII

Cuando el SII rechaza algo, el motivo viene en el HTML o el XML que devuelve, y sin ese
cuerpo guardado no hay forma de saber qué pasó. `utils/httpDebug.js` graba **todas** las
llamadas HTTP de la librería.

Está **apagado por defecto**: solo actúa si defines `SII_HTTP_DEBUG_DIR`. Sin esa variable
el costo es una comparación por request, así que se puede dejar el código como está en
producción.

```bash
SII_HTTP_DEBUG_DIR=/tmp/sii-debug node tu-script.js
```

Deja en ese directorio:

```
001-POST-DTEUpload-200.html          <- respuesta (cabecera con URL, status, ms, cliente)
001-POST-DTEUpload-200-request.txt   <- cuerpo enviado, si supera 2000 caracteres
index.jsonl                          <- una línea por llamada, para grep/jq
```

```bash
# ¿Qué llamadas hizo y cuánto tardó cada una?
jq -r '"\(.n) \(.method) \(.url) -> \(.status) \(.ms)ms [\(.cliente)]"' /tmp/sii-debug/index.jsonl
```

Cubre los cinco clientes HTTP de la librería, que son independientes entre sí: `SiiSession`,
`SiiPortalAuth`, `EnviadorSII`, `CertRunner`/`BoletaCert` y `WsReclamo`.

**Se redactan** `set-cookie`, `cookie`, `authorization` y `<RSASK>` (la llave privada RSA del
CAF). Aun así, el resto del contenido son documentos tributarios: trata ese directorio como
material sensible y púrgalo.

Para dirigir la captura por etapa, redefine la variable antes de cada bloque: se lee en cada
llamada, no una sola vez al cargar el módulo.

### Varios procesos o réplicas, y el índice

Cada línea de `index.jsonl` trae, además de la URL, el estado y la duración:

- `fallo`: `true` si el estado HTTP es 400 o superior, o si no hubo estado (error de red). Permite
  separar lo que conviene conservar más tiempo sin abrir cada archivo.
- `proceso`: identificador del proceso que hizo la llamada (pid y momento de arranque).

El contador `NNN` del nombre de archivo es por proceso y vuelve a empezar en cada arranque, así
que dos arranques, o dos réplicas que escriben en un mismo almacén, generan los mismos nombres y se
sobrescriben. Para evitarlo, define `SII_HTTP_DEBUG_PREFIJO` con un valor distinto por proceso o
réplica (por ejemplo su nombre): los archivos pasan a llamarse `<prefijo>-001-POST-DTEUpload-200.html`.
Solo se aceptan letras, números, punto, guion y guion bajo. Sin definirla, los nombres son los de
siempre.

### Archivo de envíos

Cuando se envía un documento, la librería también guarda una copia del XML enviado y de la respuesta
(`historicos/<rut>/dte-<tipo>/<fecha>/<trackId>/` y una copia de depuración en `debug/`). Si tu
aplicación ya conserva el XML y la respuesta por su cuenta, por ejemplo en su base de datos, puedes
evitar tener el mismo dato dos veces con `SII_ARCHIVAR_ENVIOS=0` (también `false`, `no` u `off`). Sin
definirla se archiva como siempre. Con esa opción la captura HTTP sigue registrando el intercambio.

---

## Ambientes

| Ambiente | Constante | Descripción |
|----------|-----------|-------------|
| `'certificacion'` | - | Apunta a `maullin.sii.cl` - para pruebas y certificación |
| `'produccion'` | - | Apunta a `palena.sii.cl` - producción real |

> Siempre verifica la variable de entorno `SII_AMBIENTE` (o el parámetro `ambiente`) antes de ejecutar código DTE para evitar envíos accidentales a producción.

### `TZ` es obligatoria

Define `TZ=America/Santiago` en el proceso que use esta librería.

`CertRunner` construye fechas con la hora local al declarar avance y libros. Con el proceso
en UTC, entre las ~20:00 y medianoche de Chile genera la fecha del **día siguiente**: un
envío registrado el día 11 se declara como del 12 y el SII responde *"FECHA NO CORRESPONDE
AL ENVIO"*, dejando el flujo esperando algo que nunca va a llegar.

> Para comprobarlo no sirve `date`: dentro de un contenedor sin `tzdata` **miente**. Usa
> `node -e "console.log(new Date().toString())"`.

### Acentos en el timbre (TED)

El lector de PDF417 del SII pierde los bytes ≥ 128: `Cajón` llega como `Cajnn` y el timbre no
valida. Por eso `DTE` normaliza a ASCII `RznSocRecep` y `NmbItem` **solo dentro del TED**,
antes de firmarlo.

El cuerpo del DTE conserva las tildes: el documento impreso y el XML que recibe el receptor
se ven correctos. Si generas el PDF417 por tu cuenta, respeta esa misma regla.

---

## Contribuir

Los PRs son bienvenidos. Antes de escribir código, lee
[CONTRIBUTING.md](CONTRIBUTING.md).

Esta librería emite documentos tributarios reales, así que el estándar de evidencia
es más alto que en una librería común. En resumen: un PR por cambio, evidencia
declarada por nivel (lectura, test, artefacto adjunto, respuesta del SII), y trazar
el radio de impacto, porque un valor que entra acá sale por siete lugares distintos
y el código de barras firmado es uno de ellos.

```bash
npm test        # suite completa, sin red ni SII
npm run scan    # barrido de datos reales (este repo es público)
npm run types   # verificación de dte-sii.d.ts
```

Las tres corren en CI sobre cada PR, en Node 18, 20 y 22.

Si trabajas con un agente de código, el repo trae la skill
[`pr-dte-sii`](.claude/skills/pr-dte-sii/) con estas reglas en formato ejecutable.

## Seguridad

Si encuentras un problema de seguridad, **no abras un issue público**. Ver
[SECURITY.md](SECURITY.md) para el canal privado y el alcance.

## Apoyar el proyecto

La librería es y seguirá siendo MIT, completa y sin funciones reservadas. Mantenerla
implica seguir los cambios de esquemas y servicios del SII, sostener los tests y
responder issues. Si tu empresa la usa y quieres ayudar a que siga al día, puedes
hacerlo en [GitHub Sponsors](https://github.com/sponsors/devlas-cl).

El apoyo es voluntario y no da prioridad sobre otros usuarios en el desarrollo.

<!-- sponsors --><!-- sponsors -->

## Licencia

MIT - Copyright (c) 2026 [Devlas SpA](https://devlas.cl)

Implementa el protocolo XML público del SII de Chile.
Inspirada conceptualmente en [LibreDTE de SASCO SpA](https://libredte.cl).
