@devlas/dte-sii 2.5.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.
Files changed (60) hide show
  1. package/BoletaService.js +109 -0
  2. package/CAF.js +173 -0
  3. package/CafSolicitor.js +380 -0
  4. package/Certificado.js +123 -0
  5. package/ConsumoFolio.js +376 -0
  6. package/DTE.js +399 -0
  7. package/EnviadorSII.js +1304 -0
  8. package/Envio.js +196 -0
  9. package/FolioRegistry.js +553 -0
  10. package/FolioService.js +703 -0
  11. package/LICENSE +27 -0
  12. package/LibroBase.js +134 -0
  13. package/LibroCompraVenta.js +205 -0
  14. package/LibroGuia.js +225 -0
  15. package/README.md +239 -0
  16. package/Signer.js +94 -0
  17. package/SiiCertificacion.js +1189 -0
  18. package/SiiPortalAuth.js +460 -0
  19. package/SiiSession.js +499 -0
  20. package/cert/BoletaCert.js +731 -0
  21. package/cert/CertFolioHelper.js +185 -0
  22. package/cert/CertRunner.js +2658 -0
  23. package/cert/ConfigLoader.js +133 -0
  24. package/cert/IntercambioCert.js +429 -0
  25. package/cert/LibroCompras.js +359 -0
  26. package/cert/LibroGuias.js +171 -0
  27. package/cert/LibroVentas.js +153 -0
  28. package/cert/MuestrasImpresas.js +676 -0
  29. package/cert/SetBase.js +321 -0
  30. package/cert/SetBasico.js +413 -0
  31. package/cert/SetCompra.js +472 -0
  32. package/cert/SetExenta.js +490 -0
  33. package/cert/SetGuia.js +283 -0
  34. package/cert/SetParser.js +1184 -0
  35. package/cert/SetsProvider.js +499 -0
  36. package/cert/Simulacion.js +521 -0
  37. package/cert/comunaOficina.js +460 -0
  38. package/cert/index.js +124 -0
  39. package/cert/types.js +330 -0
  40. package/dte-sii.d.ts +458 -0
  41. package/index.js +428 -0
  42. package/package.json +48 -0
  43. package/utils/c14n.js +275 -0
  44. package/utils/calculo.js +396 -0
  45. package/utils/config.js +276 -0
  46. package/utils/constants.js +302 -0
  47. package/utils/emisor.js +174 -0
  48. package/utils/endpoints.js +225 -0
  49. package/utils/error.js +235 -0
  50. package/utils/index.js +339 -0
  51. package/utils/logger.js +239 -0
  52. package/utils/pfx.js +203 -0
  53. package/utils/receptor.js +218 -0
  54. package/utils/referencia.js +169 -0
  55. package/utils/resolucion.js +119 -0
  56. package/utils/rut.js +169 -0
  57. package/utils/sanitize.js +124 -0
  58. package/utils/tokenCache.js +214 -0
  59. package/utils/xml.js +358 -0
  60. package/utils.js +4 -0
package/utils/rut.js ADDED
@@ -0,0 +1,169 @@
1
+ // Copyright (c) 2026 Devlas SpA — https://devlas.cl
2
+ // Licencia MIT. Ver archivo LICENSE para mas detalles.
3
+ /**
4
+ * Utilidades de RUT
5
+ *
6
+ * Funciones para manipulación y validación de RUT chileno
7
+ *
8
+ * @module dte-sii/utils/rut
9
+ */
10
+
11
+ // ============================================
12
+ // FORMATEO
13
+ // ============================================
14
+
15
+ /**
16
+ * Formatear RUT chileno (quitar puntos, mayúsculas)
17
+ *
18
+ * @param {string} rut - RUT a formatear (ej: "12.345.678-K")
19
+ * @returns {string} - RUT formateado (ej: "12345678-K")
20
+ */
21
+ function formatRut(rut) {
22
+ if (!rut) return '';
23
+ return rut.replace(/\./g, '').toUpperCase();
24
+ }
25
+
26
+ /**
27
+ * Limpiar RUT (solo números y K)
28
+ *
29
+ * @param {string} rut - RUT con cualquier formato
30
+ * @returns {string} - RUT limpio (ej: "12345678K")
31
+ */
32
+ function cleanRut(rut) {
33
+ if (!rut) return '';
34
+ return rut.replace(/[^0-9kK]/g, '').toUpperCase();
35
+ }
36
+
37
+ /**
38
+ * Separar RUT en número y dígito verificador
39
+ *
40
+ * @param {string} rut - RUT en cualquier formato
41
+ * @returns {{ numero: string, dv: string }}
42
+ */
43
+ function splitRut(rut) {
44
+ const cleaned = cleanRut(rut);
45
+ if (cleaned.length < 2) {
46
+ return { numero: cleaned, dv: '' };
47
+ }
48
+ return {
49
+ numero: cleaned.slice(0, -1),
50
+ dv: cleaned.slice(-1),
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Formatear RUT con puntos y guión
56
+ *
57
+ * @param {string} rut - RUT sin formato
58
+ * @returns {string} - RUT formateado (ej: "12.345.678-K")
59
+ */
60
+ function formatRutWithDots(rut) {
61
+ const { numero, dv } = splitRut(rut);
62
+ if (!numero) return '';
63
+
64
+ // Agregar puntos cada 3 dígitos desde la derecha
65
+ const formatted = numero.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
66
+ return `${formatted}-${dv}`;
67
+ }
68
+
69
+ /**
70
+ * Formatear RUT para uso en SII (solo con guión)
71
+ *
72
+ * @param {string} rut - RUT en cualquier formato
73
+ * @returns {string} - RUT formateado (ej: "12345678-K")
74
+ */
75
+ function formatRutSii(rut) {
76
+ const { numero, dv } = splitRut(rut);
77
+ if (!numero) return '';
78
+ return `${numero}-${dv}`;
79
+ }
80
+
81
+ // ============================================
82
+ // VALIDACIÓN
83
+ // ============================================
84
+
85
+ /**
86
+ * Calcular dígito verificador de RUT
87
+ *
88
+ * @param {string|number} rutSinDV - RUT sin dígito verificador
89
+ * @returns {string} - Dígito verificador (0-9 o K)
90
+ */
91
+ function calcularDV(rutSinDV) {
92
+ const rut = parseInt(String(rutSinDV).replace(/\D/g, ''), 10);
93
+ if (isNaN(rut) || rut <= 0) return '';
94
+
95
+ let suma = 0;
96
+ let multiplicador = 2;
97
+ const rutStr = rut.toString();
98
+
99
+ for (let i = rutStr.length - 1; i >= 0; i--) {
100
+ suma += parseInt(rutStr[i]) * multiplicador;
101
+ multiplicador = multiplicador === 7 ? 2 : multiplicador + 1;
102
+ }
103
+
104
+ const resto = suma % 11;
105
+ const dv = 11 - resto;
106
+
107
+ if (dv === 11) return '0';
108
+ if (dv === 10) return 'K';
109
+ return dv.toString();
110
+ }
111
+
112
+ /**
113
+ * Validar RUT chileno
114
+ *
115
+ * @param {string} rut - RUT a validar
116
+ * @returns {boolean} - true si el RUT es válido
117
+ */
118
+ function validarRut(rut) {
119
+ const { numero, dv } = splitRut(rut);
120
+ if (!numero || numero.length < 7 || numero.length > 8) return false;
121
+ if (!dv) return false;
122
+
123
+ const dvCalculado = calcularDV(numero);
124
+ return dvCalculado === dv.toUpperCase();
125
+ }
126
+
127
+ /**
128
+ * Validar RUT y retornar formateado si es válido
129
+ *
130
+ * @param {string} rut - RUT a validar
131
+ * @returns {{ valid: boolean, rut: string|null, error?: string }}
132
+ */
133
+ function validateAndFormatRut(rut) {
134
+ if (!rut || typeof rut !== 'string') {
135
+ return { valid: false, rut: null, error: 'RUT no proporcionado' };
136
+ }
137
+
138
+ const { numero, dv } = splitRut(rut);
139
+
140
+ if (!numero || numero.length < 7) {
141
+ return { valid: false, rut: null, error: 'RUT muy corto' };
142
+ }
143
+
144
+ if (numero.length > 8) {
145
+ return { valid: false, rut: null, error: 'RUT muy largo' };
146
+ }
147
+
148
+ const dvCalculado = calcularDV(numero);
149
+ if (dvCalculado !== dv.toUpperCase()) {
150
+ return { valid: false, rut: null, error: 'Dígito verificador inválido' };
151
+ }
152
+
153
+ return { valid: true, rut: formatRutSii(rut) };
154
+ }
155
+
156
+ // ============================================
157
+ // EXPORTS
158
+ // ============================================
159
+
160
+ module.exports = {
161
+ formatRut,
162
+ cleanRut,
163
+ splitRut,
164
+ formatRutWithDots,
165
+ formatRutSii,
166
+ calcularDV,
167
+ validarRut,
168
+ validateAndFormatRut,
169
+ };
@@ -0,0 +1,124 @@
1
+ // Copyright (c) 2026 Devlas SpA — https://devlas.cl
2
+ // Licencia MIT. Ver archivo LICENSE para mas detalles.
3
+ /**
4
+ * Utilidades de Sanitización
5
+ *
6
+ * Funciones para sanitizar y formatear texto para DTEs del SII
7
+ *
8
+ * @module dte-sii/utils/sanitize
9
+ */
10
+
11
+ // ============================================
12
+ // SANITIZACIÓN DE TEXTO
13
+ // ============================================
14
+
15
+ /**
16
+ * Sanitizar texto para DTE SII
17
+ * Elimina caracteres que causan problemas de firma XML
18
+ *
19
+ * @param {string} text - Texto a sanitizar
20
+ * @returns {string} - Texto sanitizado
21
+ */
22
+ function sanitizeSiiText(text) {
23
+ if (text === undefined || text === null) return '';
24
+ return String(text)
25
+ .replace(/[''´`]/g, '') // Eliminar apóstrofes
26
+ .replace(/[""]/g, '') // Eliminar comillas tipográficas
27
+ .replace(/"/g, '') // Eliminar comillas dobles ASCII
28
+ .trim();
29
+ }
30
+
31
+ /**
32
+ * Truncar texto a longitud máxima (preservando palabras completas si es posible)
33
+ *
34
+ * @param {string} text - Texto a truncar
35
+ * @param {number} maxLen - Longitud máxima
36
+ * @param {boolean} [preserveWords=false] - Si preservar palabras completas
37
+ * @returns {string}
38
+ */
39
+ function truncateText(text, maxLen, preserveWords = false) {
40
+ const sanitized = sanitizeSiiText(text);
41
+ if (sanitized.length <= maxLen) return sanitized;
42
+
43
+ if (preserveWords) {
44
+ const truncated = sanitized.substring(0, maxLen);
45
+ const lastSpace = truncated.lastIndexOf(' ');
46
+ if (lastSpace > maxLen * 0.7) {
47
+ return truncated.substring(0, lastSpace).trim();
48
+ }
49
+ }
50
+
51
+ return sanitized.substring(0, maxLen);
52
+ }
53
+
54
+ /**
55
+ * Sanitizar giro para receptor (máximo 40 caracteres)
56
+ *
57
+ * @param {string} giro - Giro del receptor
58
+ * @returns {string}
59
+ */
60
+ function sanitizeGiroRecep(giro) {
61
+ return truncateText(giro, 40);
62
+ }
63
+
64
+ /**
65
+ * Sanitizar razón social (máximo 100 caracteres)
66
+ *
67
+ * @param {string} razonSocial - Razón social
68
+ * @returns {string}
69
+ */
70
+ function sanitizeRazonSocial(razonSocial) {
71
+ return truncateText(razonSocial, 100);
72
+ }
73
+
74
+ /**
75
+ * Sanitizar nombre de ítem (máximo 80 caracteres)
76
+ *
77
+ * @param {string} nombre - Nombre del ítem
78
+ * @returns {string}
79
+ */
80
+ function sanitizeNombreItem(nombre) {
81
+ return truncateText(nombre, 80);
82
+ }
83
+
84
+ /**
85
+ * Sanitizar descripción de ítem (máximo 1000 caracteres)
86
+ *
87
+ * @param {string} descripcion - Descripción del ítem
88
+ * @returns {string}
89
+ */
90
+ function sanitizeDescripcionItem(descripcion) {
91
+ return truncateText(descripcion, 1000);
92
+ }
93
+
94
+ /**
95
+ * Crear segmento seguro para nombres de archivo/directorio
96
+ *
97
+ * @param {*} value - Valor a convertir
98
+ * @param {string} [fallback='sin-valor'] - Valor por defecto
99
+ * @returns {string}
100
+ */
101
+ function safeSegment(value, fallback = 'sin-valor') {
102
+ const raw = value === undefined || value === null ? '' : String(value);
103
+ const cleaned = raw
104
+ .replace(/\./g, '')
105
+ .replace(/[^a-zA-Z0-9._-]/g, '-')
106
+ .replace(/-+/g, '-')
107
+ .replace(/^-|-$/g, '')
108
+ .slice(0, 80);
109
+ return cleaned || fallback;
110
+ }
111
+
112
+ // ============================================
113
+ // EXPORTS
114
+ // ============================================
115
+
116
+ module.exports = {
117
+ sanitizeSiiText,
118
+ truncateText,
119
+ sanitizeGiroRecep,
120
+ sanitizeRazonSocial,
121
+ sanitizeNombreItem,
122
+ sanitizeDescripcionItem,
123
+ safeSegment,
124
+ };
@@ -0,0 +1,214 @@
1
+ // Copyright (c) 2026 Devlas SpA — https://devlas.cl
2
+ // Licencia MIT. Ver archivo LICENSE para mas detalles.
3
+ /**
4
+ * Token Cache
5
+ *
6
+ * Cache de tokens SII para evitar solicitudes innecesarias
7
+ * Los tokens tienen validez de ~60 minutos
8
+ *
9
+ * @module dte-sii/utils/tokenCache
10
+ */
11
+
12
+ const { getConfigSection } = require('./config');
13
+
14
+ // ============================================
15
+ // ALMACENAMIENTO EN MEMORIA
16
+ // ============================================
17
+
18
+ /**
19
+ * Estructura del cache:
20
+ * {
21
+ * 'certificacion:rest:78206276-K': { token: 'xxx', expiresAt: Date },
22
+ * 'produccion:soap:78206276-K': { token: 'yyy', expiresAt: Date },
23
+ * }
24
+ */
25
+ const tokenStore = new Map();
26
+
27
+ // ============================================
28
+ // FUNCIONES PRINCIPALES
29
+ // ============================================
30
+
31
+ /**
32
+ * Generar clave única para el cache
33
+ * @param {string} ambiente - 'certificacion' o 'produccion'
34
+ * @param {string} tipo - 'rest' o 'soap'
35
+ * @param {string} rutEmisor - RUT del emisor
36
+ * @returns {string} Clave del cache
37
+ */
38
+ function generateCacheKey(ambiente, tipo, rutEmisor) {
39
+ return `${ambiente}:${tipo}:${rutEmisor}`;
40
+ }
41
+
42
+ /**
43
+ * Obtener token del cache si existe y no ha expirado
44
+ * @param {string} ambiente - 'certificacion' o 'produccion'
45
+ * @param {string} tipo - 'rest' o 'soap'
46
+ * @param {string} rutEmisor - RUT del emisor
47
+ * @returns {string|null} Token cacheado o null
48
+ */
49
+ function getCachedToken(ambiente, tipo, rutEmisor) {
50
+ const config = getConfigSection('tokenCache');
51
+
52
+ if (!config?.enabled) {
53
+ return null;
54
+ }
55
+
56
+ const key = generateCacheKey(ambiente, tipo, rutEmisor);
57
+ const cached = tokenStore.get(key);
58
+
59
+ if (!cached) {
60
+ return null;
61
+ }
62
+
63
+ // Verificar expiración
64
+ if (Date.now() >= cached.expiresAt) {
65
+ tokenStore.delete(key);
66
+ return null;
67
+ }
68
+
69
+ return cached.token;
70
+ }
71
+
72
+ /**
73
+ * Guardar token en cache
74
+ * @param {string} ambiente - 'certificacion' o 'produccion'
75
+ * @param {string} tipo - 'rest' o 'soap'
76
+ * @param {string} rutEmisor - RUT del emisor
77
+ * @param {string} token - Token a cachear
78
+ * @param {number} [ttlMinutes] - TTL personalizado (opcional)
79
+ */
80
+ function setCachedToken(ambiente, tipo, rutEmisor, token, ttlMinutes) {
81
+ const config = getConfigSection('tokenCache');
82
+
83
+ if (!config?.enabled) {
84
+ return;
85
+ }
86
+
87
+ const ttl = ttlMinutes ?? config.ttlMinutes ?? 55;
88
+ const expiresAt = Date.now() + (ttl * 60 * 1000);
89
+
90
+ const key = generateCacheKey(ambiente, tipo, rutEmisor);
91
+ tokenStore.set(key, {
92
+ token,
93
+ expiresAt,
94
+ createdAt: Date.now(),
95
+ ambiente,
96
+ tipo,
97
+ rutEmisor,
98
+ });
99
+ }
100
+
101
+ /**
102
+ * Invalidar token específico
103
+ * @param {string} ambiente - 'certificacion' o 'produccion'
104
+ * @param {string} tipo - 'rest' o 'soap'
105
+ * @param {string} rutEmisor - RUT del emisor
106
+ */
107
+ function invalidateToken(ambiente, tipo, rutEmisor) {
108
+ const key = generateCacheKey(ambiente, tipo, rutEmisor);
109
+ tokenStore.delete(key);
110
+ }
111
+
112
+ /**
113
+ * Invalidar todos los tokens de un ambiente
114
+ * @param {string} ambiente - 'certificacion' o 'produccion'
115
+ */
116
+ function invalidateAmbiente(ambiente) {
117
+ for (const key of tokenStore.keys()) {
118
+ if (key.startsWith(`${ambiente}:`)) {
119
+ tokenStore.delete(key);
120
+ }
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Invalidar todos los tokens de un emisor
126
+ * @param {string} rutEmisor - RUT del emisor
127
+ */
128
+ function invalidateEmisor(rutEmisor) {
129
+ for (const key of tokenStore.keys()) {
130
+ if (key.endsWith(`:${rutEmisor}`)) {
131
+ tokenStore.delete(key);
132
+ }
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Limpiar todo el cache
138
+ */
139
+ function clearTokenCache() {
140
+ tokenStore.clear();
141
+ }
142
+
143
+ /**
144
+ * Obtener estadísticas del cache
145
+ * @returns {Object} Estadísticas
146
+ */
147
+ function getTokenCacheStats() {
148
+ const now = Date.now();
149
+ let active = 0;
150
+ let expired = 0;
151
+
152
+ for (const [key, value] of tokenStore.entries()) {
153
+ if (now >= value.expiresAt) {
154
+ expired++;
155
+ } else {
156
+ active++;
157
+ }
158
+ }
159
+
160
+ return {
161
+ total: tokenStore.size,
162
+ active,
163
+ expired,
164
+ entries: Array.from(tokenStore.entries()).map(([key, value]) => ({
165
+ key,
166
+ ambiente: value.ambiente,
167
+ tipo: value.tipo,
168
+ rutEmisor: value.rutEmisor,
169
+ createdAt: new Date(value.createdAt).toISOString(),
170
+ expiresAt: new Date(value.expiresAt).toISOString(),
171
+ isExpired: now >= value.expiresAt,
172
+ remainingMinutes: Math.max(0, Math.round((value.expiresAt - now) / 60000)),
173
+ })),
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Limpiar tokens expirados (limpieza manual)
179
+ * @returns {number} Número de tokens eliminados
180
+ */
181
+ function pruneExpiredTokens() {
182
+ const now = Date.now();
183
+ let pruned = 0;
184
+
185
+ for (const [key, value] of tokenStore.entries()) {
186
+ if (now >= value.expiresAt) {
187
+ tokenStore.delete(key);
188
+ pruned++;
189
+ }
190
+ }
191
+
192
+ return pruned;
193
+ }
194
+
195
+ // ============================================
196
+ // EXPORTS
197
+ // ============================================
198
+
199
+ module.exports = {
200
+ // Operaciones principales
201
+ getCachedToken,
202
+ setCachedToken,
203
+
204
+ // Invalidación
205
+ invalidateToken,
206
+ invalidateAmbiente,
207
+ invalidateEmisor,
208
+ clearTokenCache,
209
+
210
+ // Utilidades
211
+ getTokenCacheStats,
212
+ pruneExpiredTokens,
213
+ generateCacheKey,
214
+ };