@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/xml.js ADDED
@@ -0,0 +1,358 @@
1
+ // Copyright (c) 2026 Devlas SpA — https://devlas.cl
2
+ // Licencia MIT. Ver archivo LICENSE para mas detalles.
3
+ /**
4
+ * Utilidades XML
5
+ *
6
+ * Funciones para manipulación de XML en DTEs del SII
7
+ *
8
+ * @module dte-sii/utils/xml
9
+ */
10
+
11
+ const { XMLParser, XMLBuilder } = require('fast-xml-parser');
12
+ const { safeSegment } = require('./sanitize');
13
+
14
+ // ============================================
15
+ // PARSERS SINGLETON (evita múltiples instancias)
16
+ // ============================================
17
+
18
+ /**
19
+ * Parser XML configurado para DTEs del SII
20
+ * Configuración estándar con atributos
21
+ */
22
+ const defaultParser = new XMLParser({
23
+ ignoreAttributes: false,
24
+ attributeNamePrefix: '@_',
25
+ trimValues: true,
26
+ parseTagValue: true,
27
+ });
28
+
29
+ /**
30
+ * Parser XML con namespaces removidos
31
+ * Útil para respuestas SOAP del SII
32
+ */
33
+ const noNsParser = new XMLParser({
34
+ ignoreAttributes: false,
35
+ attributeNamePrefix: '@_',
36
+ removeNSPrefix: true,
37
+ trimValues: true,
38
+ });
39
+
40
+ /**
41
+ * Builder XML para generar DTEs
42
+ */
43
+ const defaultBuilder = new XMLBuilder({
44
+ ignoreAttributes: false,
45
+ attributeNamePrefix: '@_',
46
+ format: false,
47
+ suppressEmptyNode: true,
48
+ });
49
+
50
+ /**
51
+ * Builder XML con formato (para debug)
52
+ */
53
+ const prettyBuilder = new XMLBuilder({
54
+ ignoreAttributes: false,
55
+ attributeNamePrefix: '@_',
56
+ format: true,
57
+ indentBy: ' ',
58
+ suppressEmptyNode: true,
59
+ });
60
+
61
+ // ============================================
62
+ // FUNCIONES DE PARSING CENTRALIZADAS
63
+ // ============================================
64
+
65
+ /**
66
+ * Parsear XML con configuración estándar
67
+ * @param {string} xml - XML a parsear
68
+ * @returns {Object} Objeto parseado
69
+ */
70
+ function parseXml(xml) {
71
+ return defaultParser.parse(xml);
72
+ }
73
+
74
+ /**
75
+ * Parsear XML removiendo namespaces (para SOAP)
76
+ * @param {string} xml - XML a parsear
77
+ * @returns {Object} Objeto parseado
78
+ */
79
+ function parseXmlNoNs(xml) {
80
+ return noNsParser.parse(xml);
81
+ }
82
+
83
+ /**
84
+ * Construir XML desde objeto
85
+ * @param {Object} obj - Objeto a convertir
86
+ * @param {boolean} [pretty=false] - Si formatear con indentación
87
+ * @returns {string} XML generado
88
+ */
89
+ function buildXml(obj, pretty = false) {
90
+ return pretty ? prettyBuilder.build(obj) : defaultBuilder.build(obj);
91
+ }
92
+
93
+ /**
94
+ * Decodificar entidades HTML en XML (común en respuestas SOAP)
95
+ * @param {string} xml - XML con entidades
96
+ * @returns {string} XML decodificado
97
+ */
98
+ function decodeXmlEntities(xml) {
99
+ return xml
100
+ .replace(/&lt;/g, '<')
101
+ .replace(/&gt;/g, '>')
102
+ .replace(/&quot;/g, '"')
103
+ .replace(/&apos;/g, "'")
104
+ .replace(/&amp;/g, '&');
105
+ }
106
+
107
+ /**
108
+ * Extraer contenido de una etiqueta XML
109
+ * @param {string} xml - XML fuente
110
+ * @param {string} tagName - Nombre de la etiqueta
111
+ * @returns {string|null} Contenido o null
112
+ */
113
+ function extractTagContent(xml, tagName) {
114
+ const regex = new RegExp(`<${tagName}>([^<]*)</${tagName}>`, 'i');
115
+ const match = xml.match(regex);
116
+ return match ? match[1] : null;
117
+ }
118
+
119
+ /**
120
+ * Extraer valor de atributo XML
121
+ * @param {string} xml - XML fuente
122
+ * @param {string} tagName - Nombre de la etiqueta
123
+ * @param {string} attrName - Nombre del atributo
124
+ * @returns {string|null} Valor o null
125
+ */
126
+ function extractAttribute(xml, tagName, attrName) {
127
+ const regex = new RegExp(`<${tagName}[^>]*${attrName}="([^"]*)"`, 'i');
128
+ const match = xml.match(regex);
129
+ return match ? match[1] : null;
130
+ }
131
+
132
+ // ============================================
133
+ // FORMATEO XML
134
+ // ============================================
135
+
136
+ /**
137
+ * Formatear valores base64 en XML con saltos de línea cada 64 caracteres
138
+ * Requerido por el SII para certificados y firmas
139
+ *
140
+ * @param {string} xml - XML a formatear
141
+ * @returns {string} - XML con base64 formateado
142
+ */
143
+ function formatBase64InXml(xml) {
144
+ const formatTag = (tagName) => {
145
+ const regex = new RegExp(`<${tagName}>([^<]+)</${tagName}>`, 'g');
146
+ return xml.replace(regex, (match, b64) => {
147
+ const clean = b64.replace(/[\r\n\s]/g, '');
148
+ const formatted = clean.replace(/(.{64})/g, '$1\n').trim();
149
+ return `<${tagName}>${formatted}</${tagName}>`;
150
+ });
151
+ };
152
+
153
+ xml = formatTag('SignatureValue');
154
+ xml = formatTag('Modulus');
155
+ xml = formatTag('X509Certificate');
156
+
157
+ return xml;
158
+ }
159
+
160
+ /**
161
+ * Convertir elementos autocerrados a etiquetas explícitas (requerido para C14N)
162
+ *
163
+ * @param {string} xml - XML a procesar
164
+ * @returns {string} - XML con tags expandidos
165
+ */
166
+ function expandSelfClosingTags(xml) {
167
+ const elements = ['CanonicalizationMethod', 'SignatureMethod', 'Transform', 'DigestMethod'];
168
+
169
+ for (const el of elements) {
170
+ const regex = new RegExp(`<${el}([^>]*?)/>`, 'g');
171
+ xml = xml.replace(regex, `<${el}$1></${el}>`);
172
+ }
173
+
174
+ return xml;
175
+ }
176
+
177
+ /**
178
+ * Normalizar valor a array
179
+ *
180
+ * @param {*} value - Valor a normalizar
181
+ * @returns {Array}
182
+ */
183
+ function normalizeArray(value) {
184
+ if (Array.isArray(value)) return value;
185
+ if (value === undefined || value === null) return [];
186
+ return [value];
187
+ }
188
+
189
+ // ============================================
190
+ // METADATA EXTRACTION
191
+ // ============================================
192
+
193
+ /**
194
+ * Extraer metadata de un XML de envío DTE
195
+ *
196
+ * @param {string} xml - XML del envío
197
+ * @returns {Object} Metadata extraída
198
+ */
199
+ function extractEnvioMetadata(xml) {
200
+ const parser = new XMLParser({
201
+ ignoreAttributes: false,
202
+ attributeNamePrefix: '@_',
203
+ trimValues: true,
204
+ parseTagValue: true,
205
+ });
206
+
207
+ try {
208
+ const data = parser.parse(xml);
209
+ const envio = data?.EnvioBOLETA || data?.EnvioDTE || data?.EnvioDTEB || data?.EnvioDTETraslado;
210
+ const setDTE = envio?.SetDTE || data?.SetDTE || {};
211
+ const caratula = setDTE?.Caratula || {};
212
+
213
+ const dtes = normalizeArray(setDTE?.DTE);
214
+ const items = dtes.map((dte) => {
215
+ const doc = dte?.Documento || dte?.DTE?.Documento || {};
216
+ const idDoc = doc?.Encabezado?.IdDoc || {};
217
+ return {
218
+ tipoDTE: idDoc?.TipoDTE?.toString?.() ?? null,
219
+ folio: idDoc?.Folio?.toString?.() ?? null,
220
+ fchEmis: idDoc?.FchEmis?.toString?.() ?? null,
221
+ };
222
+ });
223
+
224
+ return {
225
+ rutEmisor: caratula?.RutEmisor || null,
226
+ rutEnvia: caratula?.RutEnvia || null,
227
+ setId: setDTE?.['@_ID'] || setDTE?.ID || null,
228
+ items,
229
+ };
230
+ } catch (error) {
231
+ return {
232
+ rutEmisor: null,
233
+ rutEnvia: null,
234
+ setId: null,
235
+ items: [],
236
+ parseError: error.message,
237
+ };
238
+ }
239
+ }
240
+
241
+ // ============================================
242
+ // HISTÓRICO Y DEBUG
243
+ // ============================================
244
+
245
+ /**
246
+ * Guardar artefactos de envío para histórico y debug
247
+ *
248
+ * @param {Object} params - Parámetros
249
+ * @param {string} params.xml - XML del envío
250
+ * @param {string} [params.responseText] - Respuesta del SII
251
+ * @param {boolean} [params.responseOk] - Si la respuesta fue OK
252
+ * @param {number} [params.responseStatus] - Status HTTP de la respuesta
253
+ * @param {string} [params.trackId] - Track ID del envío
254
+ * @param {string} [params.ambiente] - Ambiente (certificacion/produccion)
255
+ * @param {string} [params.tipoEnvio] - Tipo de envío
256
+ * @param {string} [params.error] - Error si lo hubo
257
+ * @param {string} [params.baseDir] - Directorio base (para multi-tenant)
258
+ */
259
+ function saveEnvioArtifacts({
260
+ xml,
261
+ responseText,
262
+ responseOk,
263
+ responseStatus,
264
+ trackId,
265
+ ambiente,
266
+ tipoEnvio,
267
+ error,
268
+ baseDir,
269
+ }) {
270
+ try {
271
+ const fs = require('fs');
272
+ const path = require('path');
273
+
274
+ const meta = extractEnvioMetadata(xml);
275
+ const fecha = meta.items[0]?.fchEmis || new Date().toISOString().slice(0, 10);
276
+ const tipoDte = meta.items.length === 1 ? meta.items[0]?.tipoDTE : 'multiple';
277
+ const folio = meta.items.length === 1 ? meta.items[0]?.folio : null;
278
+
279
+ // Usar baseDir si se proporciona, sino raíz de static/nodejs como fallback
280
+ const effectiveBase = baseDir || path.resolve(__dirname, '..', '..', '..');
281
+ const historicoBaseDir = path.join(effectiveBase, 'historicos');
282
+ const rutDir = safeSegment(meta.rutEmisor || 'sin-rut');
283
+ const tipoDir = safeSegment(`dte-${tipoDte || 'sin-tipo'}`);
284
+ const fechaDir = safeSegment(fecha);
285
+ const idDir = safeSegment(trackId || meta.setId || (folio ? `folio-${folio}` : `envio-${Date.now()}`));
286
+
287
+ const historicoDir = path.join(historicoBaseDir, rutDir, tipoDir, fechaDir, idDir);
288
+ fs.mkdirSync(historicoDir, { recursive: true });
289
+
290
+ fs.writeFileSync(path.join(historicoDir, 'envio.xml'), xml, 'utf-8');
291
+ if (responseText) {
292
+ fs.writeFileSync(path.join(historicoDir, 'respuesta.xml'), responseText, 'utf-8');
293
+ }
294
+
295
+ const metadata = {
296
+ createdAt: new Date().toISOString(),
297
+ ambiente,
298
+ tipoEnvio,
299
+ rutEmisor: meta.rutEmisor,
300
+ rutEnvia: meta.rutEnvia,
301
+ setId: meta.setId,
302
+ items: meta.items,
303
+ trackId: trackId || null,
304
+ responseOk: !!responseOk,
305
+ responseStatus: responseStatus ?? null,
306
+ error: error ? String(error) : null,
307
+ parseError: meta.parseError || null,
308
+ };
309
+
310
+ fs.writeFileSync(
311
+ path.join(historicoDir, 'metadata.json'),
312
+ JSON.stringify(metadata, null, 2),
313
+ 'utf-8'
314
+ );
315
+
316
+ // Debug copy: organizar por RUT/fecha-hora de ejecución
317
+ const debugBaseDir = process.env.CERT_RUNNER_OUT_DIR
318
+ ? path.resolve(process.env.CERT_RUNNER_OUT_DIR)
319
+ : path.join(effectiveBase, 'debug');
320
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
321
+ const debugDir = path.join(debugBaseDir, rutDir, stamp);
322
+ fs.mkdirSync(debugDir, { recursive: true });
323
+ const debugPrefix = `${safeSegment(tipoEnvio)}-${safeSegment(tipoDte || 'sin-tipo')}`;
324
+ fs.writeFileSync(path.join(debugDir, `envio-${debugPrefix}.xml`), xml, 'utf-8');
325
+ if (responseText) {
326
+ fs.writeFileSync(path.join(debugDir, `respuesta-${debugPrefix}.xml`), responseText, 'utf-8');
327
+ }
328
+ } catch (saveError) {
329
+ console.warn('⚠️ No se pudo guardar histórico/debug:', saveError.message || saveError);
330
+ }
331
+ }
332
+
333
+ // ============================================
334
+ // EXPORTS
335
+ // ============================================
336
+
337
+ module.exports = {
338
+ // Parsers singleton (para uso directo si es necesario)
339
+ defaultParser,
340
+ noNsParser,
341
+ defaultBuilder,
342
+ prettyBuilder,
343
+
344
+ // Funciones de parsing
345
+ parseXml,
346
+ parseXmlNoNs,
347
+ buildXml,
348
+ decodeXmlEntities,
349
+ extractTagContent,
350
+ extractAttribute,
351
+
352
+ // Formateo
353
+ formatBase64InXml,
354
+ expandSelfClosingTags,
355
+ normalizeArray,
356
+ extractEnvioMetadata,
357
+ saveEnvioArtifacts,
358
+ };
package/utils.js ADDED
@@ -0,0 +1,4 @@
1
+ // Copyright (c) 2026 Devlas SpA — https://devlas.cl
2
+ // Licencia MIT. Ver archivo LICENSE para mas detalles.
3
+ // Alias de compatibilidad — redirige a utils/index.js
4
+ module.exports = require('./utils/index');