@lapyme/arca 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1387 @@
1
+ // src/errors.ts
2
+ var ArcaError = class extends Error {
3
+ code;
4
+ name = "ArcaError";
5
+ constructor(message, code = "ARCA_ERROR", options) {
6
+ super(message, options);
7
+ this.code = code;
8
+ }
9
+ };
10
+ var ArcaConfigurationError = class extends ArcaError {
11
+ name = "ArcaConfigurationError";
12
+ constructor(message, options) {
13
+ super(message, "ARCA_CONFIGURATION_ERROR", options);
14
+ }
15
+ };
16
+ var ArcaTransportError = class extends ArcaError {
17
+ name = "ArcaTransportError";
18
+ statusCode;
19
+ responseBody;
20
+ constructor(message, options) {
21
+ super(message, "ARCA_TRANSPORT_ERROR", options);
22
+ this.statusCode = options?.statusCode;
23
+ this.responseBody = options?.responseBody;
24
+ }
25
+ };
26
+ var ArcaSoapFaultError = class extends ArcaError {
27
+ name = "ArcaSoapFaultError";
28
+ faultCode;
29
+ detail;
30
+ constructor(message, options) {
31
+ super(message, "ARCA_SOAP_FAULT", options);
32
+ this.faultCode = options?.faultCode;
33
+ this.detail = options?.detail;
34
+ }
35
+ };
36
+ var ArcaServiceError = class extends ArcaError {
37
+ name = "ArcaServiceError";
38
+ serviceCode;
39
+ detail;
40
+ constructor(message, options) {
41
+ super(message, "ARCA_SERVICE_ERROR", options);
42
+ this.serviceCode = options?.serviceCode;
43
+ this.detail = options?.detail;
44
+ }
45
+ };
46
+
47
+ // src/config.ts
48
+ var ARCA_ENVIRONMENTS = ["production", "test"];
49
+ var ARCA_ENV_VARIABLES = {
50
+ taxId: "ARCA_TAX_ID",
51
+ certificatePem: "ARCA_CERTIFICATE_PEM",
52
+ privateKeyPem: "ARCA_PRIVATE_KEY_PEM",
53
+ environment: "ARCA_ENVIRONMENT",
54
+ wsaaCacheMode: "ARCA_WSAA_CACHE_MODE",
55
+ wsaaCacheDirectory: "ARCA_WSAA_CACHE_DIRECTORY"
56
+ };
57
+ var PRIVATE_KEY_PEM_PREFIXES = [
58
+ "-----BEGIN PRIVATE KEY-----",
59
+ "-----BEGIN RSA PRIVATE KEY-----",
60
+ "-----BEGIN ENCRYPTED PRIVATE KEY-----"
61
+ ];
62
+ function resolveArcaEnvironment(production) {
63
+ return production ? "production" : "test";
64
+ }
65
+ function createArcaClientConfigFromEnv(options = {}) {
66
+ const env = options.env ?? process.env;
67
+ const variableNames = {
68
+ ...ARCA_ENV_VARIABLES,
69
+ ...options.variableNames
70
+ };
71
+ const environmentInput = readEnv(env, variableNames.environment);
72
+ const environmentValue = normalizeEnvironmentValue(environmentInput);
73
+ const cacheModeInput = readEnv(env, variableNames.wsaaCacheMode);
74
+ const cacheModeValue = normalizeCacheMode(cacheModeInput);
75
+ const config = {
76
+ taxId: readEnv(env, variableNames.taxId) ?? "",
77
+ certificatePem: readEnv(env, variableNames.certificatePem) ?? "",
78
+ privateKeyPem: readEnv(env, variableNames.privateKeyPem) ?? "",
79
+ environment: environmentValue ?? environmentInput ?? options.defaultEnvironment ?? "test"
80
+ };
81
+ if (cacheModeValue === "disk" || cacheModeInput === "disk") {
82
+ config.wsaa = {
83
+ cache: {
84
+ mode: cacheModeValue ?? cacheModeInput,
85
+ directory: readEnv(env, variableNames.wsaaCacheDirectory) ?? ""
86
+ }
87
+ };
88
+ } else if (cacheModeValue === "memory" || cacheModeInput !== void 0) {
89
+ config.wsaa = {
90
+ cache: {
91
+ mode: cacheModeValue ?? cacheModeInput
92
+ }
93
+ };
94
+ }
95
+ assertArcaClientConfig(config);
96
+ return normalizeArcaClientConfig(config);
97
+ }
98
+ function assertArcaClientConfig(config) {
99
+ const invalidFields = [];
100
+ const normalized = normalizeArcaClientConfig(config);
101
+ if (!/^\d{11}$/.test(normalized.taxId)) {
102
+ invalidFields.push("taxId");
103
+ }
104
+ if (!normalized.certificatePem.startsWith("-----BEGIN CERTIFICATE-----")) {
105
+ invalidFields.push("certificatePem");
106
+ }
107
+ if (!PRIVATE_KEY_PEM_PREFIXES.some(
108
+ (prefix) => normalized.privateKeyPem.startsWith(prefix)
109
+ )) {
110
+ invalidFields.push("privateKeyPem");
111
+ }
112
+ if (!ARCA_ENVIRONMENTS.includes(normalized.environment)) {
113
+ invalidFields.push("environment");
114
+ }
115
+ const cacheConfig = config.wsaa?.cache;
116
+ if (cacheConfig) {
117
+ const cacheMode = cacheConfig.mode?.trim().toLowerCase() ?? "memory";
118
+ if (cacheMode !== "memory" && cacheMode !== "disk") {
119
+ invalidFields.push("wsaa.cache.mode");
120
+ }
121
+ if (cacheMode === "disk" && (!("directory" in cacheConfig) || cacheConfig.directory.trim().length < 1)) {
122
+ invalidFields.push("wsaa.cache.directory");
123
+ }
124
+ }
125
+ if (invalidFields.length > 0) {
126
+ throw new ArcaConfigurationError(
127
+ `Missing or invalid ARCA client config fields: ${invalidFields.join(", ")}`
128
+ );
129
+ }
130
+ }
131
+ var ARCA_WSAA_CONFIG = {
132
+ namespace: "http://wsaa.view.sua.dvadac.desein.afip.gov",
133
+ endpoint: {
134
+ production: "https://wsaa.afip.gov.ar/ws/services/LoginCms",
135
+ test: "https://wsaahomo.afip.gov.ar/ws/services/LoginCms"
136
+ },
137
+ soapVersion: "1.1",
138
+ soapActionBase: "",
139
+ usesEmptySoapAction: true
140
+ };
141
+ var ARCA_SERVICE_CONFIG = {
142
+ wsaa: ARCA_WSAA_CONFIG,
143
+ wsfe: {
144
+ namespace: "http://ar.gov.afip.dif.FEV1/",
145
+ endpoint: {
146
+ production: "https://servicios1.afip.gov.ar/wsfev1/service.asmx",
147
+ test: "https://wswhomo.afip.gov.ar/wsfev1/service.asmx"
148
+ },
149
+ soapVersion: "1.2",
150
+ soapActionBase: "http://ar.gov.afip.dif.FEV1/",
151
+ useLegacyTlsSecurityLevel0: true
152
+ },
153
+ wsmtxca: {
154
+ namespace: "http://impl.service.wsmtxca.afip.gov.ar/service/",
155
+ endpoint: {
156
+ production: "https://serviciosjava.afip.gov.ar/wsmtxca/services/MTXCAService",
157
+ test: "https://fwshomo.afip.gov.ar/wsmtxca/services/MTXCAService"
158
+ },
159
+ soapVersion: "1.1",
160
+ soapActionBase: "http://impl.service.wsmtxca.afip.gov.ar/service/"
161
+ },
162
+ "padron-a5": {
163
+ namespace: "http://a5.soap.ws.server.puc.sr/",
164
+ endpoint: {
165
+ production: "https://aws.afip.gov.ar/sr-padron/webservices/personaServiceA5",
166
+ test: "https://awshomo.afip.gov.ar/sr-padron/webservices/personaServiceA5"
167
+ },
168
+ soapVersion: "1.1",
169
+ soapActionBase: "",
170
+ usesEmptySoapAction: true
171
+ },
172
+ "padron-a13": {
173
+ namespace: "http://a13.soap.ws.server.puc.sr/",
174
+ endpoint: {
175
+ production: "https://aws.afip.gov.ar/sr-padron/webservices/personaServiceA13",
176
+ test: "https://awshomo.afip.gov.ar/sr-padron/webservices/personaServiceA13"
177
+ },
178
+ soapVersion: "1.1",
179
+ soapActionBase: "",
180
+ usesEmptySoapAction: true
181
+ }
182
+ };
183
+ function getArcaServiceConfig(service) {
184
+ const serviceConfig = ARCA_SERVICE_CONFIG[service];
185
+ if (!serviceConfig) {
186
+ throw new ArcaConfigurationError(
187
+ `Unsupported ARCA service configuration: ${service}`
188
+ );
189
+ }
190
+ return serviceConfig;
191
+ }
192
+ function normalizeArcaClientConfig(config) {
193
+ const normalizedEnvironment = normalizeEnvironmentValue(String(config.environment)) ?? config.environment;
194
+ const normalizedCache = normalizeWsaaCacheConfig(config.wsaa?.cache);
195
+ return {
196
+ taxId: config.taxId.trim(),
197
+ certificatePem: config.certificatePem.trim(),
198
+ privateKeyPem: config.privateKeyPem.trim(),
199
+ environment: normalizedEnvironment,
200
+ ...normalizedCache ? {
201
+ wsaa: {
202
+ cache: normalizedCache
203
+ }
204
+ } : {}
205
+ };
206
+ }
207
+ function normalizeWsaaCacheConfig(cache) {
208
+ if (!cache) {
209
+ return void 0;
210
+ }
211
+ const mode = normalizeCacheMode(cache.mode) ?? "memory";
212
+ if (mode !== "disk") {
213
+ return {
214
+ mode: "memory"
215
+ };
216
+ }
217
+ return {
218
+ mode,
219
+ directory: ("directory" in cache ? cache.directory : "").trim()
220
+ };
221
+ }
222
+ function normalizeEnvironmentValue(value) {
223
+ if (!value) {
224
+ return void 0;
225
+ }
226
+ const normalized = value.trim().toLowerCase();
227
+ if (ARCA_ENVIRONMENTS.includes(normalized)) {
228
+ return normalized;
229
+ }
230
+ return void 0;
231
+ }
232
+ function normalizeCacheMode(value) {
233
+ if (!value) {
234
+ return void 0;
235
+ }
236
+ const normalized = value.trim().toLowerCase();
237
+ if (normalized === "memory" || normalized === "disk") {
238
+ return normalized;
239
+ }
240
+ return void 0;
241
+ }
242
+ function readEnv(env, variableName) {
243
+ return env[variableName]?.trim() || void 0;
244
+ }
245
+
246
+ // src/services/padron.ts
247
+ function createPadronService(options) {
248
+ return {
249
+ async getTaxpayerDetails(taxId) {
250
+ const raw = await executePadronOperation(
251
+ options,
252
+ "padron-a5",
253
+ "getPersona_v2",
254
+ {
255
+ idPersona: Number.parseInt(String(taxId), 10)
256
+ }
257
+ );
258
+ if (!raw) {
259
+ return null;
260
+ }
261
+ const record = raw;
262
+ const datosGenerales = record.datosGenerales;
263
+ return {
264
+ taxId: String(record.idPersona ?? ""),
265
+ ...record.tipoPersona !== void 0 ? { personType: String(record.tipoPersona) } : {},
266
+ ...datosGenerales ? { name: extractPadronName(datosGenerales) } : {},
267
+ raw: record
268
+ };
269
+ },
270
+ async getTaxIdByDocument(documentNumber) {
271
+ const raw = await executePadronOperation(
272
+ options,
273
+ "padron-a13",
274
+ "getIdPersonaListByDocumento",
275
+ {
276
+ documento: String(documentNumber)
277
+ }
278
+ );
279
+ if (!raw) {
280
+ return null;
281
+ }
282
+ const record = raw;
283
+ const idPersona = record.idPersona;
284
+ const taxIds = Array.isArray(idPersona) ? idPersona.map(String) : idPersona !== void 0 ? [String(idPersona)] : [];
285
+ return {
286
+ taxIds,
287
+ raw: record
288
+ };
289
+ }
290
+ };
291
+ }
292
+ function extractPadronName(datosGenerales) {
293
+ if (typeof datosGenerales.razonSocial === "string") {
294
+ return datosGenerales.razonSocial;
295
+ }
296
+ const nombre = datosGenerales.nombre;
297
+ const apellido = datosGenerales.apellido;
298
+ if (typeof apellido === "string" && typeof nombre === "string") {
299
+ return `${apellido} ${nombre}`.trim();
300
+ }
301
+ if (typeof apellido === "string") {
302
+ return apellido;
303
+ }
304
+ if (typeof nombre === "string") {
305
+ return nombre;
306
+ }
307
+ return void 0;
308
+ }
309
+ async function executePadronOperation(options, service, operation, body) {
310
+ const auth = await options.auth.login(
311
+ service === "padron-a5" ? "ws_sr_constancia_inscripcion" : "ws_sr_padron_a13"
312
+ );
313
+ try {
314
+ const response = await options.soap.execute({
315
+ service,
316
+ operation,
317
+ bodyElementNamespaceMode: "prefix",
318
+ body: {
319
+ token: auth.token,
320
+ sign: auth.sign,
321
+ cuitRepresentada: Number.parseInt(options.config.taxId, 10),
322
+ ...body
323
+ }
324
+ });
325
+ const operationResponse = response.result;
326
+ if (operation === "getPersona_v2") {
327
+ return operationResponse.personaReturn ?? null;
328
+ }
329
+ if (operation === "getIdPersonaListByDocumento") {
330
+ return operationResponse.idPersonaListReturn ?? null;
331
+ }
332
+ return operationResponse.return ?? null;
333
+ } catch (error) {
334
+ if (error instanceof ArcaSoapFaultError && error.message.toLowerCase().includes("no existe")) {
335
+ return null;
336
+ }
337
+ throw error;
338
+ }
339
+ }
340
+
341
+ // src/services/wsfe.ts
342
+ function createWsfeService(options) {
343
+ async function getLastVoucher({
344
+ representedTaxId,
345
+ salesPoint,
346
+ voucherType,
347
+ forceAuthRefresh
348
+ }) {
349
+ const auth = await options.auth.login("wsfe", {
350
+ representedTaxId,
351
+ forceRefresh: forceAuthRefresh
352
+ });
353
+ const response = await options.soap.execute({
354
+ service: "wsfe",
355
+ operation: "FECompUltimoAutorizado",
356
+ body: {
357
+ Auth: createWsfeAuth(
358
+ representedTaxId ?? options.config.taxId,
359
+ auth.token,
360
+ auth.sign
361
+ ),
362
+ PtoVta: salesPoint,
363
+ CbteTipo: voucherType
364
+ }
365
+ });
366
+ const result = unwrapWsfeOperationResult(
367
+ "FECompUltimoAutorizado",
368
+ response.result
369
+ );
370
+ return Number(result.CbteNro ?? 0) + 1;
371
+ }
372
+ return {
373
+ async createNextVoucher({ representedTaxId, data }) {
374
+ const voucherNumber = await getLastVoucher({
375
+ representedTaxId,
376
+ salesPoint: data.salesPoint,
377
+ voucherType: data.voucherType
378
+ });
379
+ const requestData = mapWsfeVoucherInput(data, voucherNumber);
380
+ const auth = await options.auth.login("wsfe", { representedTaxId });
381
+ const response = await options.soap.execute({
382
+ service: "wsfe",
383
+ operation: "FECAESolicitar",
384
+ body: {
385
+ Auth: createWsfeAuth(
386
+ representedTaxId ?? options.config.taxId,
387
+ auth.token,
388
+ auth.sign
389
+ ),
390
+ FeCAEReq: {
391
+ FeCabReq: {
392
+ CantReg: 1,
393
+ PtoVta: data.salesPoint,
394
+ CbteTipo: data.voucherType
395
+ },
396
+ FeDetReq: {
397
+ FECAEDetRequest: requestData
398
+ }
399
+ }
400
+ }
401
+ });
402
+ const result = unwrapWsfeOperationResult(
403
+ "FECAESolicitar",
404
+ response.result
405
+ );
406
+ const detailResponse = normalizeWsfeDetailResponse(result);
407
+ const cae = detailResponse.CAE;
408
+ const caeExpiry = detailResponse.CAEFchVto;
409
+ if (typeof cae !== "string" || typeof caeExpiry !== "string") {
410
+ throw new ArcaServiceError(
411
+ "WSFE did not return CAE authorization data",
412
+ { detail: result }
413
+ );
414
+ }
415
+ return {
416
+ cae,
417
+ caeExpiry: String(caeExpiry),
418
+ voucherNumber,
419
+ raw: result
420
+ };
421
+ },
422
+ getLastVoucher,
423
+ async getSalesPoints({ representedTaxId, forceAuthRefresh }) {
424
+ const auth = await options.auth.login("wsfe", {
425
+ representedTaxId,
426
+ forceRefresh: forceAuthRefresh
427
+ });
428
+ const response = await options.soap.execute({
429
+ service: "wsfe",
430
+ operation: "FEParamGetPtosVenta",
431
+ body: {
432
+ Auth: createWsfeAuth(
433
+ representedTaxId ?? options.config.taxId,
434
+ auth.token,
435
+ auth.sign
436
+ )
437
+ }
438
+ });
439
+ const result = unwrapWsfeOperationResult(
440
+ "FEParamGetPtosVenta",
441
+ response.result
442
+ );
443
+ const resultGet = result.ResultGet;
444
+ const rawPoints = resultGet?.PtoVenta;
445
+ if (!rawPoints) {
446
+ return [];
447
+ }
448
+ const entries = Array.isArray(rawPoints) ? rawPoints : [rawPoints];
449
+ return entries.map(mapWsfeSalesPoint);
450
+ },
451
+ async getVoucherInfo({
452
+ representedTaxId,
453
+ number,
454
+ salesPoint,
455
+ voucherType
456
+ }) {
457
+ const auth = await options.auth.login("wsfe", { representedTaxId });
458
+ const response = await options.soap.execute({
459
+ service: "wsfe",
460
+ operation: "FECompConsultar",
461
+ body: {
462
+ Auth: createWsfeAuth(
463
+ representedTaxId ?? options.config.taxId,
464
+ auth.token,
465
+ auth.sign
466
+ ),
467
+ FeCompConsReq: {
468
+ CbteNro: number,
469
+ PtoVta: salesPoint,
470
+ CbteTipo: voucherType
471
+ }
472
+ }
473
+ });
474
+ const result = unwrapWsfeOperationResult(
475
+ "FECompConsultar",
476
+ response.result
477
+ );
478
+ const raw = result.ResultGet ?? null;
479
+ if (!raw) {
480
+ return null;
481
+ }
482
+ return mapWsfeVoucherInfo(raw);
483
+ }
484
+ };
485
+ }
486
+ function mapWsfeVoucherInput(input, voucherNumber) {
487
+ const data = {
488
+ Concepto: input.concept,
489
+ DocTipo: input.documentType,
490
+ DocNro: input.documentNumber,
491
+ CbteDesde: voucherNumber,
492
+ CbteHasta: voucherNumber,
493
+ CbteFch: input.voucherDate,
494
+ ImpTotal: input.totalAmount,
495
+ ImpTotConc: input.nonTaxableAmount,
496
+ ImpNeto: input.netAmount,
497
+ ImpOpEx: input.exemptAmount,
498
+ ImpTrib: input.taxAmount,
499
+ ImpIVA: input.vatAmount,
500
+ MonId: input.currencyId,
501
+ MonCotiz: input.exchangeRate,
502
+ PtoVta: input.salesPoint,
503
+ CbteTipo: input.voucherType
504
+ };
505
+ if (input.receiverVatConditionId !== void 0) {
506
+ data.CondicionIVAReceptorId = input.receiverVatConditionId;
507
+ }
508
+ if (input.serviceStartDate !== void 0) {
509
+ data.FchServDesde = input.serviceStartDate;
510
+ }
511
+ if (input.serviceEndDate !== void 0) {
512
+ data.FchServHasta = input.serviceEndDate;
513
+ }
514
+ if (input.paymentDueDate !== void 0) {
515
+ data.FchVtoPago = input.paymentDueDate;
516
+ }
517
+ if (input.associatedVouchers) {
518
+ data.CbtesAsoc = {
519
+ CbteAsoc: input.associatedVouchers.map((v) => ({
520
+ Tipo: v.type,
521
+ PtoVta: v.salesPoint,
522
+ Nro: v.number,
523
+ ...v.taxId !== void 0 ? { Cuit: v.taxId } : {},
524
+ ...v.voucherDate !== void 0 ? { CbteFch: v.voucherDate } : {}
525
+ }))
526
+ };
527
+ }
528
+ if (input.taxes) {
529
+ data.Tributos = {
530
+ Tributo: input.taxes.map((t) => ({
531
+ Id: t.id,
532
+ ...t.description !== void 0 ? { Desc: t.description } : {},
533
+ BaseImp: t.baseAmount,
534
+ Alic: t.rate,
535
+ Importe: t.amount
536
+ }))
537
+ };
538
+ }
539
+ if (input.vatRates) {
540
+ data.Iva = {
541
+ AlicIva: input.vatRates.map((v) => ({
542
+ Id: v.id,
543
+ BaseImp: v.baseAmount,
544
+ Importe: v.amount
545
+ }))
546
+ };
547
+ }
548
+ if (input.optionalFields) {
549
+ data.Opcionales = {
550
+ Opcional: input.optionalFields.map((o) => ({
551
+ Id: o.id,
552
+ Valor: o.value
553
+ }))
554
+ };
555
+ }
556
+ if (input.buyers) {
557
+ data.Compradores = {
558
+ Comprador: input.buyers.map((b) => ({
559
+ DocTipo: b.documentType,
560
+ DocNro: b.documentNumber,
561
+ Porcentaje: b.percentage
562
+ }))
563
+ };
564
+ }
565
+ return data;
566
+ }
567
+ function mapWsfeSalesPoint(raw) {
568
+ const record = raw;
569
+ return {
570
+ number: Number(record.Nro ?? 0),
571
+ ...record.EmisionTipo !== void 0 ? { emissionType: String(record.EmisionTipo) } : {},
572
+ ...record.Bloqueado !== void 0 ? { blocked: String(record.Bloqueado) } : {},
573
+ ...record.FchBaja !== void 0 ? { deletedSince: String(record.FchBaja) } : {}
574
+ };
575
+ }
576
+ function mapWsfeVoucherInfo(raw) {
577
+ return {
578
+ voucherNumber: Number(raw.CbteDesde ?? raw.CbteHasta ?? 0),
579
+ ...raw.CbteFch !== void 0 ? { voucherDate: String(raw.CbteFch) } : {},
580
+ ...raw.PtoVta !== void 0 ? { salesPoint: Number(raw.PtoVta) } : {},
581
+ ...raw.CbteTipo !== void 0 ? { voucherType: Number(raw.CbteTipo) } : {},
582
+ ...raw.ImpTotal !== void 0 ? { totalAmount: Number(raw.ImpTotal) } : {},
583
+ ...raw.Resultado !== void 0 ? { result: String(raw.Resultado) } : {},
584
+ ...raw.CAE !== void 0 ? { cae: String(raw.CAE) } : {},
585
+ ...raw.CAEFchVto !== void 0 ? { caeExpiry: String(raw.CAEFchVto) } : {},
586
+ raw
587
+ };
588
+ }
589
+ function createWsfeAuth(representedTaxId, token, sign) {
590
+ return {
591
+ Token: token,
592
+ Sign: sign,
593
+ Cuit: Number.parseInt(String(representedTaxId), 10)
594
+ };
595
+ }
596
+ function unwrapWsfeOperationResult(operation, response) {
597
+ const operationResponse = response[`${operation}Response`];
598
+ const result = operationResponse?.[`${operation}Result`] ?? response[`${operation}Result`] ?? response;
599
+ if (operation === "FECAESolicitar") {
600
+ const detailResponse = normalizeWsfeDetailResponse(result);
601
+ const resultCode = detailResponse.Resultado;
602
+ if (resultCode && resultCode !== "A") {
603
+ const observationsContainer = detailResponse.Observaciones;
604
+ const observations = normalizeWsfeErrors(observationsContainer?.Obs);
605
+ if (observations.length > 0) {
606
+ const firstObservation = observations[0];
607
+ if (!firstObservation) {
608
+ throw new ArcaServiceError(
609
+ "WSFE returned an empty observation list",
610
+ {
611
+ detail: result
612
+ }
613
+ );
614
+ }
615
+ throw new ArcaServiceError(firstObservation.message, {
616
+ serviceCode: firstObservation.code,
617
+ detail: result
618
+ });
619
+ }
620
+ }
621
+ }
622
+ const errorsContainer = result.Errors;
623
+ const errors = normalizeWsfeErrors(errorsContainer?.Err);
624
+ if (errors.length > 0) {
625
+ const firstError = errors[0];
626
+ if (!firstError) {
627
+ throw new ArcaServiceError("WSFE returned an empty error list", {
628
+ detail: result
629
+ });
630
+ }
631
+ throw new ArcaServiceError(firstError.message, {
632
+ serviceCode: firstError.code,
633
+ detail: result
634
+ });
635
+ }
636
+ return result;
637
+ }
638
+ function normalizeWsfeDetailResponse(result) {
639
+ const detailResponse = result.FeDetResp;
640
+ const rawDetail = detailResponse?.FECAEDetResponse;
641
+ if (Array.isArray(rawDetail)) {
642
+ return rawDetail[0] ?? {};
643
+ }
644
+ return rawDetail ?? {};
645
+ }
646
+ function normalizeWsfeErrors(rawErrors) {
647
+ const entries = Array.isArray(rawErrors) ? rawErrors : rawErrors ? [rawErrors] : [];
648
+ return entries.map((entry) => entry).map((entry) => {
649
+ const code = entry.Code ?? entry.code ?? "N/A";
650
+ const message = entry.Msg ?? entry.msg ?? "Unknown WSFE error";
651
+ return {
652
+ code: String(code),
653
+ message: `(${String(code)}) ${String(message)}`
654
+ };
655
+ });
656
+ }
657
+
658
+ // src/services/wsmtxca.ts
659
+ function createWsmtxcaService(options) {
660
+ return {
661
+ async authorizeVoucher({ representedTaxId, data }) {
662
+ const auth = await options.auth.login("wsmtxca", { representedTaxId });
663
+ const response = await options.soap.execute({
664
+ service: "wsmtxca",
665
+ operation: "autorizarComprobante",
666
+ bodyElementName: "autorizarComprobanteRequest",
667
+ bodyElementNamespaceMode: "prefix",
668
+ body: {
669
+ authRequest: createWsmtxcaAuth(
670
+ representedTaxId ?? options.config.taxId,
671
+ auth.token,
672
+ auth.sign
673
+ ),
674
+ ...data
675
+ }
676
+ });
677
+ const raw = unwrapWsmtxcaOperationResponse(
678
+ response.result,
679
+ "autorizarComprobante"
680
+ );
681
+ const authorizationPayload = extractWsmtxcaAuthorizationPayload(raw);
682
+ const messages = extractWsmtxcaMessages(raw);
683
+ const resultado = raw.resultado ?? authorizationPayload.resultado;
684
+ const caeValue = authorizationPayload.CAE ?? authorizationPayload.codigoAutorizacion ?? raw.codigoAutorizacion;
685
+ if (resultado === "R" || caeValue == null) {
686
+ throw new ArcaServiceError(
687
+ messages.join(" | ") || "WSMTXCA rejected the voucher authorization",
688
+ { detail: raw }
689
+ );
690
+ }
691
+ return {
692
+ cae: String(caeValue),
693
+ caeExpiry: normalizeWsmtxcaResponseDate(
694
+ authorizationPayload.fechaVencimientoCAE ?? authorizationPayload.fechaVencimiento ?? raw.fechaVencimiento
695
+ ),
696
+ voucherNumber: parseWsmtxcaVoucherNumber(
697
+ authorizationPayload.numeroComprobante ?? raw.numeroComprobante,
698
+ "WSMTXCA did not return the authorized voucher number",
699
+ raw
700
+ ),
701
+ messages,
702
+ raw
703
+ };
704
+ },
705
+ async getLastAuthorizedVoucher({
706
+ representedTaxId,
707
+ voucherType,
708
+ salesPoint
709
+ }) {
710
+ const auth = await options.auth.login("wsmtxca", { representedTaxId });
711
+ const response = await options.soap.execute({
712
+ service: "wsmtxca",
713
+ operation: "consultarUltimoComprobanteAutorizado",
714
+ bodyElementName: "consultarUltimoComprobanteAutorizadoRequest",
715
+ bodyElementNamespaceMode: "prefix",
716
+ body: {
717
+ authRequest: createWsmtxcaAuth(
718
+ representedTaxId ?? options.config.taxId,
719
+ auth.token,
720
+ auth.sign
721
+ ),
722
+ consultaUltimoComprobanteAutorizadoRequest: {
723
+ codigoTipoComprobante: voucherType,
724
+ numeroPuntoVenta: salesPoint
725
+ }
726
+ }
727
+ });
728
+ const raw = unwrapWsmtxcaOperationResponse(
729
+ response.result,
730
+ "consultarUltimoComprobanteAutorizado"
731
+ );
732
+ return {
733
+ voucherNumber: parseWsmtxcaVoucherNumber(
734
+ raw.numeroComprobante ?? raw.cbteNro ?? raw.nroComprobante,
735
+ extractWsmtxcaMessages(raw).join(" | ") || "WSMTXCA did not return the last authorized voucher number",
736
+ raw
737
+ ),
738
+ raw
739
+ };
740
+ },
741
+ async getVoucher({
742
+ representedTaxId,
743
+ voucherType,
744
+ salesPoint,
745
+ voucherNumber
746
+ }) {
747
+ const auth = await options.auth.login("wsmtxca", { representedTaxId });
748
+ const response = await options.soap.execute({
749
+ service: "wsmtxca",
750
+ operation: "consultarComprobante",
751
+ bodyElementName: "consultarComprobanteRequest",
752
+ bodyElementNamespaceMode: "prefix",
753
+ body: {
754
+ authRequest: createWsmtxcaAuth(
755
+ representedTaxId ?? options.config.taxId,
756
+ auth.token,
757
+ auth.sign
758
+ ),
759
+ consultaComprobanteRequest: {
760
+ codigoTipoComprobante: voucherType,
761
+ numeroPuntoVenta: salesPoint,
762
+ numeroComprobante: voucherNumber
763
+ }
764
+ }
765
+ });
766
+ const raw = unwrapWsmtxcaOperationResponse(
767
+ response.result,
768
+ "consultarComprobante"
769
+ );
770
+ const voucher = extractWsmtxcaVoucherPayload(raw);
771
+ const messages = extractWsmtxcaMessages(raw);
772
+ const invoiceDate = normalizeWsmtxcaResponseDate(
773
+ voucher.fechaEmision ?? voucher.fecha ?? voucher.CbteFch
774
+ );
775
+ if (!invoiceDate) {
776
+ throw new ArcaServiceError(
777
+ messages[0] ?? "WSMTXCA did not return the voucher issue date",
778
+ { detail: raw }
779
+ );
780
+ }
781
+ return {
782
+ invoiceDate,
783
+ voucher,
784
+ messages,
785
+ raw
786
+ };
787
+ }
788
+ };
789
+ }
790
+ function createWsmtxcaAuth(representedTaxId, token, sign) {
791
+ return {
792
+ token,
793
+ sign,
794
+ cuitRepresentada: Number.parseInt(String(representedTaxId), 10)
795
+ };
796
+ }
797
+ function toRecord(value) {
798
+ return value && typeof value === "object" ? value : void 0;
799
+ }
800
+ function unwrapWsmtxcaOperationResponse(response, operation) {
801
+ const responseRecord = toRecord(response) ?? {};
802
+ if (operation === "autorizarComprobante") {
803
+ return toRecord(responseRecord.autorizarComprobanteResponse) ?? toRecord(responseRecord.autorizarComprobanteResult) ?? toRecord(responseRecord.comprobanteCAEResponse) ?? toRecord(responseRecord.comprobanteCAEReponse) ?? responseRecord;
804
+ }
805
+ if (operation === "consultarComprobante") {
806
+ return toRecord(responseRecord.consultarComprobanteResponse) ?? toRecord(responseRecord.consultaComprobanteResponse) ?? toRecord(responseRecord.consultarComprobanteResult) ?? responseRecord;
807
+ }
808
+ return toRecord(responseRecord.consultarUltimoComprobanteAutorizadoResponse) ?? toRecord(responseRecord.consultaUltimoComprobanteAutorizadoResponse) ?? toRecord(responseRecord.consultarUltimoComprobanteAutorizadoResult) ?? responseRecord;
809
+ }
810
+ function extractWsmtxcaAuthorizationPayload(raw) {
811
+ return toRecord(raw.comprobanteResponse) ?? toRecord(raw.comprobanteCAEResponse) ?? toRecord(raw.comprobanteCAEReponse) ?? raw;
812
+ }
813
+ function extractWsmtxcaVoucherPayload(raw) {
814
+ return toRecord(raw.comprobanteResponse) ?? toRecord(raw.comprobante) ?? toRecord(raw.cmp) ?? raw;
815
+ }
816
+ function extractWsmtxcaMessages(raw) {
817
+ const rawErrors = raw.arrayErrores;
818
+ const rawObservations = raw.arrayObservaciones;
819
+ const toEntries = (value) => {
820
+ if (!value) {
821
+ return [];
822
+ }
823
+ if (Array.isArray(value)) {
824
+ return value;
825
+ }
826
+ if (typeof value === "object") {
827
+ return [value];
828
+ }
829
+ return [];
830
+ };
831
+ const errors = toEntries(rawErrors?.codigoDescripcion).map((entry) => {
832
+ const code = entry.codigo == null ? "N/A" : String(entry.codigo);
833
+ const description = entry.descripcion == null ? "Unknown WSMTXCA error" : String(entry.descripcion);
834
+ return `Error ${code}: ${description}`;
835
+ });
836
+ const observations = toEntries(rawObservations?.codigoDescripcion).map(
837
+ (entry) => {
838
+ const code = entry.codigo == null ? "N/A" : String(entry.codigo);
839
+ const description = entry.descripcion == null ? "" : String(entry.descripcion);
840
+ return `Obs ${code}: ${description}`.trim();
841
+ }
842
+ );
843
+ return [...errors, ...observations];
844
+ }
845
+ function parseWsmtxcaVoucherNumber(value, message, detail) {
846
+ const parsed = Number.parseInt(String(value ?? ""), 10);
847
+ if (!Number.isFinite(parsed) || parsed <= 0) {
848
+ throw new ArcaServiceError(message, { detail });
849
+ }
850
+ return parsed;
851
+ }
852
+ function normalizeWsmtxcaResponseDate(value) {
853
+ if (typeof value === "number" && Number.isInteger(value)) {
854
+ return formatCompactDateToIso(value);
855
+ }
856
+ if (typeof value !== "string") {
857
+ return void 0;
858
+ }
859
+ const trimmed = value.trim();
860
+ if (!trimmed) {
861
+ return void 0;
862
+ }
863
+ if (/^\d{8}$/.test(trimmed)) {
864
+ return formatCompactDateToIso(Number.parseInt(trimmed, 10));
865
+ }
866
+ if (/^\d{4}-\d{2}-\d{2}/.test(trimmed)) {
867
+ return trimmed.slice(0, 10);
868
+ }
869
+ return void 0;
870
+ }
871
+ function formatCompactDateToIso(dateValue) {
872
+ if (!dateValue) {
873
+ return void 0;
874
+ }
875
+ const raw = String(dateValue);
876
+ if (raw.length !== 8) {
877
+ return void 0;
878
+ }
879
+ return `${raw.slice(0, 4)}-${raw.slice(4, 6)}-${raw.slice(6, 8)}`;
880
+ }
881
+
882
+ // src/internal/http.ts
883
+ import https from "https";
884
+ var defaultAgent = new https.Agent({
885
+ keepAlive: true
886
+ });
887
+ var legacyTlsAgent = new https.Agent({
888
+ keepAlive: true,
889
+ ciphers: "DEFAULT@SECLEVEL=0"
890
+ });
891
+ var REQUEST_TIMEOUT_MS = 3e4;
892
+ async function postXml({
893
+ url,
894
+ body,
895
+ contentType,
896
+ soapAction,
897
+ useLegacyTlsSecurityLevel0 = false
898
+ }) {
899
+ const endpoint = new URL(url);
900
+ const requestBody = Buffer.from(body, "utf8");
901
+ return await new Promise((resolve, reject) => {
902
+ let settled = false;
903
+ const settleResolve = (responseBody) => {
904
+ if (settled) {
905
+ return;
906
+ }
907
+ settled = true;
908
+ resolve(responseBody);
909
+ };
910
+ const settleReject = (error) => {
911
+ if (settled) {
912
+ return;
913
+ }
914
+ settled = true;
915
+ reject(error);
916
+ };
917
+ const request = https.request(
918
+ {
919
+ protocol: endpoint.protocol,
920
+ hostname: endpoint.hostname,
921
+ port: endpoint.port || void 0,
922
+ path: `${endpoint.pathname}${endpoint.search}`,
923
+ method: "POST",
924
+ agent: useLegacyTlsSecurityLevel0 ? legacyTlsAgent : defaultAgent,
925
+ headers: {
926
+ Accept: "text/xml, application/soap+xml",
927
+ "Content-Length": requestBody.byteLength,
928
+ "Content-Type": contentType,
929
+ ...soapAction === void 0 ? {} : { SOAPAction: `"${soapAction}"` }
930
+ }
931
+ },
932
+ (response) => {
933
+ const chunks = [];
934
+ const getResponseBody = () => Buffer.concat(chunks).toString("utf8");
935
+ response.on("data", (chunk) => {
936
+ chunks.push(
937
+ typeof chunk === "string" ? Buffer.from(chunk, "utf8") : chunk
938
+ );
939
+ });
940
+ response.on("error", (error) => {
941
+ settleReject(
942
+ new ArcaTransportError(
943
+ `ARCA HTTP response stream failed: ${error.message}`,
944
+ {
945
+ cause: error,
946
+ statusCode: response.statusCode,
947
+ responseBody: getResponseBody()
948
+ }
949
+ )
950
+ );
951
+ });
952
+ response.on("aborted", () => {
953
+ settleReject(
954
+ new ArcaTransportError("ARCA HTTP response was aborted", {
955
+ statusCode: response.statusCode,
956
+ responseBody: getResponseBody()
957
+ })
958
+ );
959
+ });
960
+ response.on("end", () => {
961
+ const responseBody = getResponseBody();
962
+ const statusCode = response.statusCode ?? 500;
963
+ const responseContentType = Array.isArray(
964
+ response.headers["content-type"]
965
+ ) ? response.headers["content-type"].join("; ") : response.headers["content-type"];
966
+ if (statusCode >= 200 && statusCode < 300) {
967
+ settleResolve(responseBody);
968
+ return;
969
+ }
970
+ if (isXmlLikeResponse(responseBody, responseContentType)) {
971
+ settleResolve(responseBody);
972
+ return;
973
+ }
974
+ settleReject(
975
+ new ArcaTransportError(
976
+ `ARCA HTTP request failed with status ${statusCode}`,
977
+ {
978
+ statusCode,
979
+ responseBody
980
+ }
981
+ )
982
+ );
983
+ });
984
+ }
985
+ );
986
+ request.setTimeout(REQUEST_TIMEOUT_MS, () => {
987
+ request.destroy(
988
+ new Error(`ARCA HTTP request timed out after ${REQUEST_TIMEOUT_MS}ms`)
989
+ );
990
+ });
991
+ request.on("error", (error) => {
992
+ settleReject(
993
+ new ArcaTransportError(`ARCA HTTP request failed: ${error.message}`, {
994
+ cause: error
995
+ })
996
+ );
997
+ });
998
+ request.write(requestBody);
999
+ request.end();
1000
+ });
1001
+ }
1002
+ function isXmlLikeResponse(body, contentType) {
1003
+ const normalizedContentType = contentType?.toLowerCase() ?? "";
1004
+ if (normalizedContentType.includes("xml") || normalizedContentType.includes("soap")) {
1005
+ return true;
1006
+ }
1007
+ return body.trimStart().startsWith("<");
1008
+ }
1009
+
1010
+ // src/internal/xml.ts
1011
+ import { XMLBuilder, XMLParser } from "fast-xml-parser";
1012
+ var xmlBuilder = new XMLBuilder({
1013
+ attributeNamePrefix: "@_",
1014
+ format: false,
1015
+ ignoreAttributes: false,
1016
+ suppressBooleanAttributes: false,
1017
+ suppressEmptyNode: true
1018
+ });
1019
+ var xmlParser = new XMLParser({
1020
+ attributeNamePrefix: "@_",
1021
+ ignoreAttributes: false,
1022
+ parseAttributeValue: false,
1023
+ parseTagValue: false,
1024
+ removeNSPrefix: true,
1025
+ trimValues: true
1026
+ });
1027
+ function buildSoapEnvelope(soapVersion, operation, namespace, body, options) {
1028
+ const prefix = soapVersion === "1.2" ? "soap12" : "soap";
1029
+ const envelopeNamespace = soapVersion === "1.2" ? "http://www.w3.org/2003/05/soap-envelope" : "http://schemas.xmlsoap.org/soap/envelope/";
1030
+ const namespaceMode = options?.namespaceMode ?? "default";
1031
+ const operationElementName = namespaceMode === "prefix" ? `tns:${operation}` : operation;
1032
+ const operationNamespaceAttributes = namespaceMode === "prefix" ? { "@_xmlns:tns": namespace } : { "@_xmlns": namespace };
1033
+ const payload = {
1034
+ [`${prefix}:Envelope`]: {
1035
+ "@_xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
1036
+ "@_xmlns:xsd": "http://www.w3.org/2001/XMLSchema",
1037
+ [`@_xmlns:${prefix}`]: envelopeNamespace,
1038
+ [`${prefix}:Body`]: {
1039
+ [operationElementName]: {
1040
+ ...operationNamespaceAttributes,
1041
+ ...pruneUndefinedDeep(body)
1042
+ }
1043
+ }
1044
+ }
1045
+ };
1046
+ return `<?xml version="1.0" encoding="utf-8"?>${xmlBuilder.build(payload)}`;
1047
+ }
1048
+ function parseSoapBody(xml) {
1049
+ const parsed = xmlParser.parse(xml);
1050
+ const envelope = parsed.Envelope;
1051
+ const body = envelope?.Body;
1052
+ if (!body) {
1053
+ throw new ArcaSoapFaultError("Invalid SOAP response: missing body", {
1054
+ detail: parsed
1055
+ });
1056
+ }
1057
+ const fault = body.Fault;
1058
+ if (fault) {
1059
+ throw createSoapFaultError(fault);
1060
+ }
1061
+ return body;
1062
+ }
1063
+ function getSingleBodyEntry(body) {
1064
+ const entries = Object.entries(body).filter(([key]) => key !== "@_xmlns");
1065
+ if (entries.length !== 1) {
1066
+ throw new ArcaSoapFaultError(
1067
+ `Invalid SOAP response: expected a single body entry, got ${entries.length}`,
1068
+ {
1069
+ detail: body
1070
+ }
1071
+ );
1072
+ }
1073
+ return entries[0];
1074
+ }
1075
+ function parseXmlDocument(xml) {
1076
+ return xmlParser.parse(xml);
1077
+ }
1078
+ function pruneUndefinedDeep(value) {
1079
+ if (Array.isArray(value)) {
1080
+ return value.map((item) => pruneUndefinedDeep(item)).filter((item) => item !== void 0);
1081
+ }
1082
+ if (value && typeof value === "object") {
1083
+ const entries = Object.entries(value).filter(([, nestedValue]) => nestedValue !== void 0).map(([key, nestedValue]) => [key, pruneUndefinedDeep(nestedValue)]);
1084
+ return Object.fromEntries(entries);
1085
+ }
1086
+ return value;
1087
+ }
1088
+ function createSoapFaultError(fault) {
1089
+ const faultCode = typeof fault.faultcode === "string" ? fault.faultcode : getNestedString(fault, ["Code", "Value"]);
1090
+ const message = typeof fault.faultstring === "string" ? fault.faultstring : getNestedString(fault, ["Reason", "Text"]) ?? "ARCA SOAP fault response";
1091
+ return new ArcaSoapFaultError(message, {
1092
+ faultCode: faultCode ?? void 0,
1093
+ detail: fault
1094
+ });
1095
+ }
1096
+ function getNestedString(value, path2) {
1097
+ let current = value;
1098
+ for (const key of path2) {
1099
+ if (!current || typeof current !== "object") {
1100
+ return null;
1101
+ }
1102
+ current = current[key];
1103
+ }
1104
+ return typeof current === "string" ? current : null;
1105
+ }
1106
+
1107
+ // src/soap/index.ts
1108
+ function createSoapTransport(options) {
1109
+ return {
1110
+ async execute(request) {
1111
+ const serviceConfig = getArcaServiceConfig(request.service);
1112
+ const soapActionOperation = request.operation;
1113
+ const bodyElementName = request.bodyElementName ?? request.operation;
1114
+ const soapAction = serviceConfig.usesEmptySoapAction ? "" : `${serviceConfig.soapActionBase}${soapActionOperation}`;
1115
+ const contentType = serviceConfig.soapVersion === "1.2" ? `application/soap+xml; charset=utf-8; action="${soapAction}"` : 'text/xml; charset="utf-8"';
1116
+ const xml = buildSoapEnvelope(
1117
+ serviceConfig.soapVersion,
1118
+ bodyElementName,
1119
+ serviceConfig.namespace,
1120
+ request.body,
1121
+ {
1122
+ namespaceMode: request.bodyElementNamespaceMode
1123
+ }
1124
+ );
1125
+ const responseXml = await postXml({
1126
+ url: serviceConfig.endpoint[options.config.environment],
1127
+ body: xml,
1128
+ contentType,
1129
+ soapAction: serviceConfig.soapVersion === "1.1" ? soapAction : void 0,
1130
+ useLegacyTlsSecurityLevel0: options.config.environment === "production" && serviceConfig.useLegacyTlsSecurityLevel0 === true
1131
+ });
1132
+ const soapBody = parseSoapBody(responseXml);
1133
+ const [, result] = getSingleBodyEntry(soapBody);
1134
+ return {
1135
+ service: request.service,
1136
+ operation: request.operation,
1137
+ raw: responseXml,
1138
+ result
1139
+ };
1140
+ }
1141
+ };
1142
+ }
1143
+
1144
+ // src/wsaa/index.ts
1145
+ import { createHash } from "crypto";
1146
+ import { mkdir, readFile, writeFile } from "fs/promises";
1147
+ import path from "path";
1148
+ import forge from "node-forge";
1149
+ function createWsaaAuthModule(options) {
1150
+ const cache = /* @__PURE__ */ new Map();
1151
+ const inFlight = /* @__PURE__ */ new Map();
1152
+ const persistedCache = createPersistedCredentialStore(options.config.wsaa?.cache);
1153
+ return {
1154
+ async login(service, authOptions = {}) {
1155
+ const cacheKey = buildWsaaCacheKey(options.config, service);
1156
+ const running = inFlight.get(cacheKey);
1157
+ if (running) {
1158
+ return running;
1159
+ }
1160
+ const loginPromise = (async () => {
1161
+ if (!authOptions.forceRefresh) {
1162
+ const cached = await getCachedCredentials(
1163
+ cache,
1164
+ cacheKey,
1165
+ persistedCache
1166
+ );
1167
+ if (cached) {
1168
+ return cached;
1169
+ }
1170
+ }
1171
+ const credentials = await requestCredentials(options.config, service);
1172
+ cache.set(cacheKey, credentials);
1173
+ await persistedCache.write(cacheKey, credentials);
1174
+ return credentials;
1175
+ })();
1176
+ inFlight.set(cacheKey, loginPromise);
1177
+ try {
1178
+ return await loginPromise;
1179
+ } catch (error) {
1180
+ if (!authOptions.forceRefresh && error instanceof ArcaSoapFaultError && error.faultCode === "ns1:coe.alreadyAuthenticated") {
1181
+ const cached = await getCachedCredentials(
1182
+ cache,
1183
+ cacheKey,
1184
+ persistedCache
1185
+ );
1186
+ if (cached) {
1187
+ return cached;
1188
+ }
1189
+ }
1190
+ throw error;
1191
+ } finally {
1192
+ inFlight.delete(cacheKey);
1193
+ }
1194
+ }
1195
+ };
1196
+ }
1197
+ async function requestCredentials(config, service) {
1198
+ const loginTicketRequestXml = buildLoginTicketRequest(service);
1199
+ const signedCms = signLoginTicketRequest(loginTicketRequestXml, {
1200
+ certificatePem: config.certificatePem,
1201
+ privateKeyPem: config.privateKeyPem
1202
+ });
1203
+ const requestXml = buildSoapEnvelope(
1204
+ ARCA_WSAA_CONFIG.soapVersion,
1205
+ "loginCms",
1206
+ ARCA_WSAA_CONFIG.namespace,
1207
+ { in0: signedCms }
1208
+ );
1209
+ const responseXml = await postXml({
1210
+ url: ARCA_WSAA_CONFIG.endpoint[config.environment],
1211
+ body: requestXml,
1212
+ contentType: 'text/xml; charset="utf-8"',
1213
+ soapAction: ARCA_WSAA_CONFIG.soapActionBase
1214
+ });
1215
+ const soapBody = parseSoapBody(responseXml);
1216
+ const [, response] = getSingleBodyEntry(soapBody);
1217
+ const loginCmsReturn = response.loginCmsReturn;
1218
+ if (typeof loginCmsReturn !== "string" || loginCmsReturn.trim().length < 1) {
1219
+ throw new ArcaTransportError(
1220
+ "WSAA response did not include loginCmsReturn XML"
1221
+ );
1222
+ }
1223
+ return parseLoginTicketResponse(loginCmsReturn);
1224
+ }
1225
+ function buildWsaaCacheKey(config, service) {
1226
+ return [config.environment, service, getCertificateFingerprint(config)].join(
1227
+ ":"
1228
+ );
1229
+ }
1230
+ function getCertificateFingerprint(config) {
1231
+ return createHash("sha256").update(config.certificatePem).digest("hex");
1232
+ }
1233
+ function isCredentialValid(credentials) {
1234
+ return new Date(credentials.expiresAt).getTime() - Date.now() > 6e4;
1235
+ }
1236
+ async function getCachedCredentials(cache, cacheKey, persistedCache) {
1237
+ const localCached = cache.get(cacheKey);
1238
+ if (localCached && isCredentialValid(localCached)) {
1239
+ return localCached;
1240
+ }
1241
+ const persisted = await persistedCache.read(cacheKey);
1242
+ if (persisted) {
1243
+ cache.set(cacheKey, persisted);
1244
+ return persisted;
1245
+ }
1246
+ return null;
1247
+ }
1248
+ function createPersistedCredentialStore(cacheConfig) {
1249
+ if (!cacheConfig || cacheConfig.mode !== "disk") {
1250
+ return {
1251
+ async read() {
1252
+ return null;
1253
+ },
1254
+ async write() {
1255
+ }
1256
+ };
1257
+ }
1258
+ const cacheDir = path.resolve(cacheConfig.directory);
1259
+ return {
1260
+ async read(cacheKey) {
1261
+ try {
1262
+ const serialized = await readFile(
1263
+ getSharedCacheFilePath(cacheDir, cacheKey),
1264
+ "utf8"
1265
+ );
1266
+ const parsed = JSON.parse(serialized);
1267
+ if (typeof parsed.token !== "string" || typeof parsed.sign !== "string" || typeof parsed.expiresAt !== "string") {
1268
+ return null;
1269
+ }
1270
+ const credentials = {
1271
+ token: parsed.token,
1272
+ sign: parsed.sign,
1273
+ expiresAt: parsed.expiresAt
1274
+ };
1275
+ return isCredentialValid(credentials) ? credentials : null;
1276
+ } catch {
1277
+ return null;
1278
+ }
1279
+ },
1280
+ async write(cacheKey, credentials) {
1281
+ await mkdir(cacheDir, { recursive: true });
1282
+ await writeFile(
1283
+ getSharedCacheFilePath(cacheDir, cacheKey),
1284
+ JSON.stringify(credentials),
1285
+ "utf8"
1286
+ );
1287
+ }
1288
+ };
1289
+ }
1290
+ function getSharedCacheFilePath(cacheDir, cacheKey) {
1291
+ const fileName = `${createHash("sha256").update(cacheKey).digest("hex")}.json`;
1292
+ return path.join(cacheDir, fileName);
1293
+ }
1294
+ function buildLoginTicketRequest(service) {
1295
+ const uniqueId = Math.floor(Date.now() / 1e3);
1296
+ const generationTime = new Date(Date.now() - 5 * 6e4).toISOString().replace(".000Z", "Z");
1297
+ const expirationTime = new Date(Date.now() + 5 * 6e4).toISOString().replace(".000Z", "Z");
1298
+ return `<?xml version="1.0" encoding="UTF-8"?>
1299
+ <loginTicketRequest version="1.0">
1300
+ <header>
1301
+ <uniqueId>${uniqueId}</uniqueId>
1302
+ <generationTime>${generationTime}</generationTime>
1303
+ <expirationTime>${expirationTime}</expirationTime>
1304
+ </header>
1305
+ <service>${service}</service>
1306
+ </loginTicketRequest>`;
1307
+ }
1308
+ function signLoginTicketRequest(loginTicketRequestXml, options) {
1309
+ const certificate = forge.pki.certificateFromPem(options.certificatePem);
1310
+ const privateKey = forge.pki.privateKeyFromPem(options.privateKeyPem);
1311
+ const signedData = forge.pkcs7.createSignedData();
1312
+ signedData.content = forge.util.createBuffer(loginTicketRequestXml, "utf8");
1313
+ signedData.addCertificate(certificate);
1314
+ const authenticatedAttributes = [
1315
+ {
1316
+ type: String(forge.pki.oids.contentType),
1317
+ value: String(forge.pki.oids.data)
1318
+ },
1319
+ {
1320
+ type: String(forge.pki.oids.messageDigest)
1321
+ },
1322
+ {
1323
+ type: String(forge.pki.oids.signingTime),
1324
+ value: /* @__PURE__ */ new Date()
1325
+ }
1326
+ ];
1327
+ const signerOptions = {
1328
+ key: privateKey,
1329
+ certificate,
1330
+ digestAlgorithm: String(forge.pki.oids.sha1),
1331
+ authenticatedAttributes
1332
+ };
1333
+ signedData.addSigner(signerOptions);
1334
+ signedData.sign();
1335
+ const der = forge.asn1.toDer(signedData.toAsn1()).getBytes();
1336
+ return Buffer.from(der, "binary").toString("base64");
1337
+ }
1338
+ function parseLoginTicketResponse(xml) {
1339
+ const parsed = parseXmlDocument(xml);
1340
+ const response = parsed.loginTicketResponse ?? parsed;
1341
+ const header = response.header;
1342
+ const credentials = response.credentials;
1343
+ const token = credentials?.token;
1344
+ const sign = credentials?.sign;
1345
+ const expiresAt = header?.expirationTime;
1346
+ if (typeof token !== "string" || typeof sign !== "string" || typeof expiresAt !== "string") {
1347
+ throw new ArcaTransportError(
1348
+ "Invalid WSAA login ticket response structure"
1349
+ );
1350
+ }
1351
+ return {
1352
+ token,
1353
+ sign,
1354
+ expiresAt
1355
+ };
1356
+ }
1357
+
1358
+ // src/client.ts
1359
+ function createArcaClient(config) {
1360
+ assertArcaClientConfig(config);
1361
+ const normalizedConfig = normalizeArcaClientConfig(config);
1362
+ const auth = createWsaaAuthModule({ config: normalizedConfig });
1363
+ const soap = createSoapTransport({ config: normalizedConfig });
1364
+ return {
1365
+ config: normalizedConfig,
1366
+ wsfe: createWsfeService({ config: normalizedConfig, auth, soap }),
1367
+ wsmtxca: createWsmtxcaService({ config: normalizedConfig, auth, soap }),
1368
+ padron: createPadronService({ config: normalizedConfig, auth, soap })
1369
+ };
1370
+ }
1371
+ export {
1372
+ ARCA_ENVIRONMENTS,
1373
+ ARCA_ENV_VARIABLES,
1374
+ ArcaConfigurationError,
1375
+ ArcaError,
1376
+ ArcaServiceError,
1377
+ ArcaSoapFaultError,
1378
+ ArcaTransportError,
1379
+ assertArcaClientConfig,
1380
+ createArcaClient,
1381
+ createArcaClientConfigFromEnv,
1382
+ createPadronService,
1383
+ createWsfeService,
1384
+ createWsmtxcaService,
1385
+ resolveArcaEnvironment
1386
+ };
1387
+ //# sourceMappingURL=index.js.map