facturas 0.4.2 → 0.5.1

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 CHANGED
@@ -24,10 +24,12 @@ var ArcaInputError = class extends ArcaError {
24
24
  var ArcaTransportError = class extends ArcaError {
25
25
  name = "ArcaTransportError";
26
26
  statusCode;
27
+ contentType;
27
28
  responseBody;
28
29
  constructor(message, options) {
29
30
  super(message, "ARCA_TRANSPORT_ERROR", options);
30
31
  this.statusCode = options?.statusCode;
32
+ this.contentType = options?.contentType;
31
33
  this.responseBody = options?.responseBody;
32
34
  }
33
35
  };
@@ -41,6 +43,28 @@ var ArcaSoapFaultError = class extends ArcaError {
41
43
  this.detail = options?.detail;
42
44
  }
43
45
  };
46
+ var ArcaInvalidSoapResponseError = class extends ArcaError {
47
+ name = "ArcaInvalidSoapResponseError";
48
+ service;
49
+ operation;
50
+ endpointUrl;
51
+ statusCode;
52
+ contentType;
53
+ responseBodyLength;
54
+ responseBodyPreview;
55
+ parsedDetail;
56
+ constructor(message, options) {
57
+ super(message, "ARCA_INVALID_SOAP_RESPONSE", options);
58
+ this.service = options?.service;
59
+ this.operation = options?.operation;
60
+ this.endpointUrl = options?.endpointUrl;
61
+ this.statusCode = options?.statusCode;
62
+ this.contentType = options?.contentType;
63
+ this.responseBodyLength = options?.responseBodyLength;
64
+ this.responseBodyPreview = options?.responseBodyPreview;
65
+ this.parsedDetail = options?.parsedDetail;
66
+ }
67
+ };
44
68
  var ArcaServiceError = class extends ArcaError {
45
69
  name = "ArcaServiceError";
46
70
  serviceCode;
@@ -125,12 +149,33 @@ function assertArcaClientConfig(config) {
125
149
  if (normalized.logger?.log !== void 0 && typeof normalized.logger.log !== "function") {
126
150
  invalidFields.push("logger.log");
127
151
  }
152
+ invalidFields.push(...getInvalidWsaaSessionStoreFields(normalized));
128
153
  if (invalidFields.length > 0) {
129
154
  throw new ArcaConfigurationError(
130
155
  `Missing or invalid ARCA client config fields: ${invalidFields.join(", ")}`
131
156
  );
132
157
  }
133
158
  }
159
+ function getInvalidWsaaSessionStoreFields(config) {
160
+ const store = config.wsaaSessionStore;
161
+ if (store === void 0) {
162
+ return [];
163
+ }
164
+ const invalidFields = [];
165
+ if (typeof store.get !== "function") {
166
+ invalidFields.push("wsaaSessionStore.get");
167
+ }
168
+ if (typeof store.set !== "function") {
169
+ invalidFields.push("wsaaSessionStore.set");
170
+ }
171
+ if (store.delete !== void 0 && typeof store.delete !== "function") {
172
+ invalidFields.push("wsaaSessionStore.delete");
173
+ }
174
+ if (store.withLock !== void 0 && typeof store.withLock !== "function") {
175
+ invalidFields.push("wsaaSessionStore.withLock");
176
+ }
177
+ return invalidFields;
178
+ }
134
179
  var ARCA_WSAA_CONFIG = {
135
180
  namespace: "http://wsaa.view.sua.dvadac.desein.afip.gov",
136
181
  endpoint: {
@@ -208,7 +253,8 @@ function normalizeArcaClientConfig(config) {
208
253
  ...config.logger,
209
254
  ...normalizedLoggerLevel === void 0 ? {} : { level: normalizedLoggerLevel }
210
255
  }
211
- }
256
+ },
257
+ ...config.wsaaSessionStore === void 0 ? {} : { wsaaSessionStore: config.wsaaSessionStore }
212
258
  };
213
259
  }
214
260
  function normalizeEnvironmentValue(value) {
@@ -389,7 +435,7 @@ function createWsfeService(options) {
389
435
  async function executeWsfeAuthenticatedOperation(operation, input, body = {}) {
390
436
  const auth = await options.auth.login("wsfe", {
391
437
  representedTaxId: input.representedTaxId,
392
- forceRefresh: input.forceAuthRefresh
438
+ forceRefresh: input.forceRefresh
393
439
  });
394
440
  const response = await options.soap.execute({
395
441
  service: "wsfe",
@@ -417,13 +463,13 @@ function createWsfeService(options) {
417
463
  representedTaxId,
418
464
  salesPoint,
419
465
  voucherType,
420
- forceAuthRefresh
466
+ forceRefresh
421
467
  }) {
422
468
  const result = await executeWsfeAuthenticatedOperation(
423
469
  "FECompUltimoAutorizado",
424
470
  {
425
471
  representedTaxId,
426
- forceAuthRefresh
472
+ forceRefresh
427
473
  },
428
474
  {
429
475
  PtoVta: salesPoint,
@@ -435,29 +481,35 @@ function createWsfeService(options) {
435
481
  async function getWsfeCatalog(operation, resultKey, input) {
436
482
  const result = await executeWsfeAuthenticatedOperation(operation, {
437
483
  representedTaxId: input.representedTaxId,
438
- forceAuthRefresh: input.forceAuthRefresh
484
+ forceRefresh: input.forceRefresh
439
485
  });
440
486
  return getWsfeResultEntries(result, resultKey).map(mapWsfeCatalogEntry);
441
487
  }
442
488
  function authorizeVoucher({
443
489
  representedTaxId,
444
490
  data,
445
- voucherNumber
491
+ voucherNumber,
492
+ forceRefresh
446
493
  }) {
447
494
  const normalizedInput = normalizeWsfeVoucherInput(data);
448
495
  return authorizeNormalizedVoucher({
449
496
  representedTaxId,
450
497
  data: normalizedInput,
451
- voucherNumber
498
+ voucherNumber,
499
+ forceRefresh
452
500
  });
453
501
  }
454
502
  async function authorizeNormalizedVoucher({
455
503
  representedTaxId,
456
504
  data: normalizedInput,
457
- voucherNumber
505
+ voucherNumber,
506
+ forceRefresh
458
507
  }) {
459
508
  const requestData = mapWsfeVoucherInput(normalizedInput, voucherNumber);
460
- const auth = await options.auth.login("wsfe", { representedTaxId });
509
+ const auth = await options.auth.login("wsfe", {
510
+ representedTaxId,
511
+ forceRefresh
512
+ });
461
513
  const response = await options.soap.execute({
462
514
  service: "wsfe",
463
515
  operation: "FECAESolicitar",
@@ -497,12 +549,13 @@ function createWsfeService(options) {
497
549
  }
498
550
  return {
499
551
  authorizeVoucher,
500
- async createNextVoucher({ representedTaxId, data }) {
552
+ async createNextVoucher({ representedTaxId, data, forceRefresh }) {
501
553
  const normalizedInput = normalizeWsfeVoucherInput(data);
502
554
  const voucherNumber = await getNextVoucherNumber({
503
555
  representedTaxId,
504
556
  salesPoint: normalizedInput.salesPoint,
505
- voucherType: normalizedInput.voucherType
557
+ voucherType: normalizedInput.voucherType,
558
+ forceRefresh
506
559
  });
507
560
  return authorizeNormalizedVoucher({
508
561
  representedTaxId,
@@ -514,12 +567,12 @@ function createWsfeService(options) {
514
567
  getLastVoucher(input) {
515
568
  return getNextVoucherNumber(input);
516
569
  },
517
- async getSalesPoints({ representedTaxId, forceAuthRefresh }) {
570
+ async getSalesPoints({ representedTaxId, forceRefresh }) {
518
571
  const result = await executeWsfeAuthenticatedOperation(
519
572
  "FEParamGetPtosVenta",
520
573
  {
521
574
  representedTaxId,
522
- forceAuthRefresh
575
+ forceRefresh
523
576
  }
524
577
  );
525
578
  const rawPoints = result.ResultGet?.PtoVenta;
@@ -538,12 +591,12 @@ function createWsfeService(options) {
538
591
  getConceptTypes(input) {
539
592
  return getWsfeCatalog("FEParamGetTiposConcepto", "ConceptoTipo", input);
540
593
  },
541
- async getCurrencyTypes({ representedTaxId, forceAuthRefresh }) {
594
+ async getCurrencyTypes({ representedTaxId, forceRefresh }) {
542
595
  const result = await executeWsfeAuthenticatedOperation(
543
596
  "FEParamGetTiposMonedas",
544
597
  {
545
598
  representedTaxId,
546
- forceAuthRefresh
599
+ forceRefresh
547
600
  }
548
601
  );
549
602
  return getWsfeResultEntries(result, "Moneda").map(mapWsfeCurrencyType);
@@ -557,12 +610,12 @@ function createWsfeService(options) {
557
610
  getOptionalTypes(input) {
558
611
  return getWsfeCatalog("FEParamGetTiposOpcional", "OpcionalTipo", input);
559
612
  },
560
- async getActivities({ representedTaxId, forceAuthRefresh }) {
613
+ async getActivities({ representedTaxId, forceRefresh }) {
561
614
  const result = await executeWsfeAuthenticatedOperation(
562
615
  "FEParamGetActividades",
563
616
  {
564
617
  representedTaxId,
565
- forceAuthRefresh
618
+ forceRefresh
566
619
  }
567
620
  );
568
621
  return getWsfeResultEntries(result, "ActividadesTipo").map(
@@ -572,13 +625,13 @@ function createWsfeService(options) {
572
625
  async getReceiverVatConditions({
573
626
  representedTaxId,
574
627
  voucherClass,
575
- forceAuthRefresh
628
+ forceRefresh
576
629
  }) {
577
630
  const result = await executeWsfeAuthenticatedOperation(
578
631
  "FEParamGetCondicionIvaReceptor",
579
632
  {
580
633
  representedTaxId,
581
- forceAuthRefresh
634
+ forceRefresh
582
635
  },
583
636
  {
584
637
  ...voucherClass === void 0 ? {} : { ClaseCmp: voucherClass }
@@ -592,12 +645,12 @@ function createWsfeService(options) {
592
645
  const result = await executeWsfeOperation("FEDummy");
593
646
  return mapWsfeServerStatus(result);
594
647
  },
595
- async getQuotation({ currencyId, representedTaxId, forceAuthRefresh }) {
648
+ async getQuotation({ currencyId, representedTaxId, forceRefresh }) {
596
649
  const result = await executeWsfeAuthenticatedOperation(
597
650
  "FEParamGetCotizacion",
598
651
  {
599
652
  representedTaxId,
600
- forceAuthRefresh
653
+ forceRefresh
601
654
  },
602
655
  {
603
656
  MonId: currencyId
@@ -610,12 +663,14 @@ function createWsfeService(options) {
610
663
  representedTaxId,
611
664
  number,
612
665
  salesPoint,
613
- voucherType
666
+ voucherType,
667
+ forceRefresh
614
668
  }) {
615
669
  const result = await executeWsfeAuthenticatedOperation(
616
670
  "FECompConsultar",
617
671
  {
618
- representedTaxId
672
+ representedTaxId,
673
+ forceRefresh
619
674
  },
620
675
  {
621
676
  FeCompConsReq: {
@@ -989,8 +1044,11 @@ function getWsfeResultEntries(result, key) {
989
1044
  // src/services/wsmtxca.ts
990
1045
  function createWsmtxcaService(options) {
991
1046
  return {
992
- async authorizeVoucher({ representedTaxId, data }) {
993
- const auth = await options.auth.login("wsmtxca", { representedTaxId });
1047
+ async authorizeVoucher({ representedTaxId, data, forceRefresh }) {
1048
+ const auth = await options.auth.login("wsmtxca", {
1049
+ representedTaxId,
1050
+ forceRefresh
1051
+ });
994
1052
  const response = await options.soap.execute({
995
1053
  service: "wsmtxca",
996
1054
  operation: "autorizarComprobante",
@@ -1036,9 +1094,13 @@ function createWsmtxcaService(options) {
1036
1094
  async getLastAuthorizedVoucher({
1037
1095
  representedTaxId,
1038
1096
  voucherType,
1039
- salesPoint
1097
+ salesPoint,
1098
+ forceRefresh
1040
1099
  }) {
1041
- const auth = await options.auth.login("wsmtxca", { representedTaxId });
1100
+ const auth = await options.auth.login("wsmtxca", {
1101
+ representedTaxId,
1102
+ forceRefresh
1103
+ });
1042
1104
  const response = await options.soap.execute({
1043
1105
  service: "wsmtxca",
1044
1106
  operation: "consultarUltimoComprobanteAutorizado",
@@ -1073,9 +1135,13 @@ function createWsmtxcaService(options) {
1073
1135
  representedTaxId,
1074
1136
  voucherType,
1075
1137
  salesPoint,
1076
- voucherNumber
1138
+ voucherNumber,
1139
+ forceRefresh
1077
1140
  }) {
1078
- const auth = await options.auth.login("wsmtxca", { representedTaxId });
1141
+ const auth = await options.auth.login("wsmtxca", {
1142
+ representedTaxId,
1143
+ forceRefresh
1144
+ });
1079
1145
  const response = await options.soap.execute({
1080
1146
  service: "wsmtxca",
1081
1147
  operation: "consultarComprobante",
@@ -1219,7 +1285,7 @@ var legacyTlsAgent = new https.Agent({
1219
1285
  keepAlive: true,
1220
1286
  ciphers: "DEFAULT@SECLEVEL=0"
1221
1287
  });
1222
- async function postXml({
1288
+ async function postXmlWithMetadata({
1223
1289
  url,
1224
1290
  body,
1225
1291
  contentType,
@@ -1287,12 +1353,12 @@ async function postXmlOnce({
1287
1353
  const requestBody = Buffer.from(body, "utf8");
1288
1354
  return await new Promise((resolve, reject) => {
1289
1355
  let settled = false;
1290
- const settleResolve = (responseBody) => {
1356
+ const settleResolve = (response) => {
1291
1357
  if (settled) {
1292
1358
  return;
1293
1359
  }
1294
1360
  settled = true;
1295
- resolve(responseBody);
1361
+ resolve(response);
1296
1362
  };
1297
1363
  const settleReject = (error) => {
1298
1364
  if (settled) {
@@ -1351,11 +1417,19 @@ async function postXmlOnce({
1351
1417
  response.headers["content-type"]
1352
1418
  ) ? response.headers["content-type"].join("; ") : response.headers["content-type"];
1353
1419
  if (statusCode >= 200 && statusCode < 300) {
1354
- settleResolve(responseBody);
1420
+ settleResolve({
1421
+ body: responseBody,
1422
+ statusCode,
1423
+ contentType: responseContentType
1424
+ });
1355
1425
  return;
1356
1426
  }
1357
1427
  if (isXmlLikeResponse(responseBody, responseContentType)) {
1358
- settleResolve(responseBody);
1428
+ settleResolve({
1429
+ body: responseBody,
1430
+ statusCode,
1431
+ contentType: responseContentType
1432
+ });
1359
1433
  return;
1360
1434
  }
1361
1435
  settleReject(
@@ -1363,6 +1437,7 @@ async function postXmlOnce({
1363
1437
  `ARCA HTTP request failed with status ${statusCode}`,
1364
1438
  {
1365
1439
  statusCode,
1440
+ contentType: responseContentType,
1366
1441
  responseBody
1367
1442
  }
1368
1443
  )
@@ -1437,14 +1512,26 @@ function buildSoapEnvelope(soapVersion, operation, namespace, body, options) {
1437
1512
  };
1438
1513
  return `<?xml version="1.0" encoding="utf-8"?>${xmlBuilder.build(payload)}`;
1439
1514
  }
1440
- function parseSoapBody(xml) {
1441
- const parsed = xmlParser.parse(xml);
1515
+ function parseSoapBody(xml, context = {}) {
1516
+ let parsed;
1517
+ try {
1518
+ parsed = xmlParser.parse(xml);
1519
+ } catch (error) {
1520
+ throw createInvalidSoapResponseError(
1521
+ "Invalid SOAP response: XML parse failed",
1522
+ context,
1523
+ void 0,
1524
+ error instanceof Error ? error : void 0
1525
+ );
1526
+ }
1442
1527
  const envelope = parsed.Envelope;
1443
1528
  const body = envelope?.Body;
1444
1529
  if (!body) {
1445
- throw new ArcaSoapFaultError("Invalid SOAP response: missing body", {
1446
- detail: parsed
1447
- });
1530
+ throw createInvalidSoapResponseError(
1531
+ "Invalid SOAP response: missing body",
1532
+ context,
1533
+ parsed
1534
+ );
1448
1535
  }
1449
1536
  const fault = body.Fault;
1450
1537
  if (fault) {
@@ -1452,18 +1539,40 @@ function parseSoapBody(xml) {
1452
1539
  }
1453
1540
  return body;
1454
1541
  }
1455
- function getSingleBodyEntry(body) {
1542
+ function getSingleBodyEntry(body, context = {}) {
1456
1543
  const entries = Object.entries(body).filter(([key]) => key !== "@_xmlns");
1457
1544
  if (entries.length !== 1) {
1458
- throw new ArcaSoapFaultError(
1545
+ throw createInvalidSoapResponseError(
1459
1546
  `Invalid SOAP response: expected a single body entry, got ${entries.length}`,
1460
- {
1461
- detail: body
1462
- }
1547
+ context,
1548
+ body
1463
1549
  );
1464
1550
  }
1465
1551
  return entries[0];
1466
1552
  }
1553
+ function createInvalidSoapResponseError(message, context, parsedDetail, cause) {
1554
+ const responseBody = context.responseBody ?? "";
1555
+ return new ArcaInvalidSoapResponseError(message, {
1556
+ cause,
1557
+ service: context.service,
1558
+ operation: context.operation,
1559
+ endpointUrl: context.endpointUrl,
1560
+ statusCode: context.statusCode,
1561
+ contentType: context.contentType,
1562
+ responseBodyLength: responseBody.length,
1563
+ responseBodyPreview: sanitizeSoapResponsePreview(
1564
+ responseBody,
1565
+ context.responseBodyPreviewLength
1566
+ ),
1567
+ parsedDetail
1568
+ });
1569
+ }
1570
+ function sanitizeSoapResponsePreview(responseBody, maxLength = 4096) {
1571
+ return responseBody.replace(
1572
+ /<((?:[A-Za-z_][\w.-]*:)?(?:Token|Sign))\b([^>]*)>[\s\S]*?<\/\1>/gi,
1573
+ (_match, tagName, attributes) => `<${tagName}${attributes}>[REDACTED]</${tagName}>`
1574
+ ).slice(0, maxLength);
1575
+ }
1467
1576
  function parseXmlDocument(xml) {
1468
1577
  return xmlParser.parse(xml);
1469
1578
  }
@@ -1522,7 +1631,7 @@ function createSoapTransport(options) {
1522
1631
  url
1523
1632
  });
1524
1633
  try {
1525
- const responseXml = await postXml({
1634
+ const response = await postXmlWithMetadata({
1526
1635
  url,
1527
1636
  body: xml,
1528
1637
  contentType,
@@ -1540,12 +1649,23 @@ function createSoapTransport(options) {
1540
1649
  operation: request.operation,
1541
1650
  durationMs: Date.now() - startedAt
1542
1651
  });
1543
- const soapBody = parseSoapBody(responseXml);
1544
- const [, result] = getSingleBodyEntry(soapBody);
1652
+ const parseContext = {
1653
+ service: request.service,
1654
+ operation: request.operation,
1655
+ endpointUrl: url,
1656
+ statusCode: response.statusCode,
1657
+ contentType: response.contentType,
1658
+ responseBody: response.body
1659
+ };
1660
+ const soapBody = parseSoapBody(response.body, parseContext);
1661
+ const [, result] = getSingleBodyEntry(
1662
+ soapBody,
1663
+ parseContext
1664
+ );
1545
1665
  return {
1546
1666
  service: request.service,
1547
1667
  operation: request.operation,
1548
- raw: responseXml,
1668
+ raw: response.body,
1549
1669
  result
1550
1670
  };
1551
1671
  } catch (error) {
@@ -1558,6 +1678,17 @@ function createSoapTransport(options) {
1558
1678
  error
1559
1679
  });
1560
1680
  }
1681
+ if (error instanceof ArcaInvalidSoapResponseError) {
1682
+ options.logger?.error("ARCA invalid SOAP response", {
1683
+ service: request.service,
1684
+ operation: request.operation,
1685
+ url,
1686
+ statusCode: error.statusCode,
1687
+ contentType: error.contentType,
1688
+ responseBodyLength: error.responseBodyLength,
1689
+ error
1690
+ });
1691
+ }
1561
1692
  throw error;
1562
1693
  }
1563
1694
  }
@@ -1567,48 +1698,117 @@ function createSoapTransport(options) {
1567
1698
  // src/wsaa/index.ts
1568
1699
  import { createHash } from "crypto";
1569
1700
  import forge from "node-forge";
1701
+
1702
+ // src/wsaa/session-store.ts
1703
+ var CREDENTIAL_EXPIRY_SAFETY_MARGIN_MS = 6e4;
1704
+ function createMemoryWsaaSessionStore() {
1705
+ const sessions = /* @__PURE__ */ new Map();
1706
+ const locks = /* @__PURE__ */ new Map();
1707
+ return {
1708
+ get(key) {
1709
+ const credentials = sessions.get(serializeWsaaSessionKey(key));
1710
+ if (!(credentials && isWsaaCredentialValid(credentials))) {
1711
+ return Promise.resolve(null);
1712
+ }
1713
+ return Promise.resolve({ ...credentials });
1714
+ },
1715
+ set(key, credentials) {
1716
+ sessions.set(serializeWsaaSessionKey(key), { ...credentials });
1717
+ return Promise.resolve();
1718
+ },
1719
+ delete(key) {
1720
+ sessions.delete(serializeWsaaSessionKey(key));
1721
+ return Promise.resolve();
1722
+ },
1723
+ async withLock(key, fn) {
1724
+ const lockKey = serializeWsaaSessionKey(key);
1725
+ const previous = locks.get(lockKey) ?? Promise.resolve();
1726
+ let release = () => void 0;
1727
+ const current = new Promise((resolve) => {
1728
+ release = resolve;
1729
+ });
1730
+ const queued = previous.catch(() => void 0).then(() => current);
1731
+ locks.set(lockKey, queued);
1732
+ await previous.catch(() => void 0);
1733
+ try {
1734
+ return await fn();
1735
+ } finally {
1736
+ release();
1737
+ if (locks.get(lockKey) === queued) {
1738
+ locks.delete(lockKey);
1739
+ }
1740
+ }
1741
+ }
1742
+ };
1743
+ }
1744
+ function serializeWsaaSessionKey(key) {
1745
+ return [key.environment, key.service, key.certificateFingerprint].join(":");
1746
+ }
1747
+ function isWsaaCredentialValid(credentials) {
1748
+ return new Date(credentials.expiresAt).getTime() - Date.now() > CREDENTIAL_EXPIRY_SAFETY_MARGIN_MS;
1749
+ }
1750
+
1751
+ // src/wsaa/index.ts
1570
1752
  function createWsaaAuthModule(options) {
1571
1753
  const cache = /* @__PURE__ */ new Map();
1572
1754
  const inFlight = /* @__PURE__ */ new Map();
1573
1755
  return {
1574
1756
  async login(service, authOptions = {}) {
1575
- const cacheKey = buildWsaaCacheKey(options.config, service);
1757
+ const sessionKey = buildWsaaSessionKey(options.config, service);
1758
+ const cacheKey = serializeWsaaSessionKey(sessionKey);
1576
1759
  const running = inFlight.get(cacheKey);
1577
1760
  if (running) {
1578
1761
  return running;
1579
1762
  }
1580
1763
  const loginPromise = (async () => {
1581
- if (!authOptions.forceRefresh) {
1582
- const cached = getCachedCredentials(cache, cacheKey);
1583
- if (cached) {
1584
- options.logger?.debug("Attempting WSAA login", {
1585
- service,
1586
- source: "cached"
1587
- });
1588
- return cached;
1589
- }
1590
- }
1591
- options.logger?.debug("Attempting WSAA login", {
1764
+ const reuse = await getReusableCredentials({
1765
+ config: options.config,
1766
+ cache,
1767
+ cacheKey,
1768
+ sessionKey,
1769
+ logger: options.logger,
1592
1770
  service,
1593
- source: "fresh"
1594
- });
1595
- const credentials = await requestCredentials(options.config, service, {
1596
- logger: options.logger
1771
+ allowStore: !authOptions.forceRefresh,
1772
+ allowCache: !authOptions.forceRefresh
1597
1773
  });
1598
- options.logger?.info("WSAA login succeeded", {
1774
+ if (reuse) {
1775
+ return reuse;
1776
+ }
1777
+ const refresh = () => refreshWsaaCredentials({
1778
+ config: options.config,
1779
+ cache,
1780
+ cacheKey,
1781
+ sessionKey,
1782
+ logger: options.logger,
1599
1783
  service,
1600
- expiresAt: credentials.expiresAt
1784
+ forceRefresh: authOptions.forceRefresh === true
1601
1785
  });
1602
- cache.set(cacheKey, credentials);
1603
- return credentials;
1786
+ if (options.config.wsaaSessionStore?.withLock) {
1787
+ return await withWsaaSessionStoreLock(
1788
+ options.config,
1789
+ sessionKey,
1790
+ service,
1791
+ refresh
1792
+ );
1793
+ }
1794
+ return await refresh();
1604
1795
  })();
1605
1796
  inFlight.set(cacheKey, loginPromise);
1606
1797
  try {
1607
1798
  return await loginPromise;
1608
1799
  } catch (error) {
1609
- if (!authOptions.forceRefresh && error instanceof ArcaSoapFaultError && error.faultCode === "ns1:coe.alreadyAuthenticated") {
1610
- const cached = getCachedCredentials(cache, cacheKey);
1611
- if (cached) {
1800
+ if (error instanceof ArcaSoapFaultError && error.faultCode === "ns1:coe.alreadyAuthenticated") {
1801
+ const recovered = await getReusableCredentials({
1802
+ config: options.config,
1803
+ cache,
1804
+ cacheKey,
1805
+ sessionKey,
1806
+ logger: options.logger,
1807
+ service,
1808
+ allowStore: true,
1809
+ allowCache: true
1810
+ });
1811
+ if (recovered) {
1612
1812
  options.logger?.warn(
1613
1813
  "Recovered WSAA coe.alreadyAuthenticated fault",
1614
1814
  {
@@ -1616,7 +1816,13 @@ function createWsaaAuthModule(options) {
1616
1816
  faultCode: error.faultCode
1617
1817
  }
1618
1818
  );
1619
- return cached;
1819
+ return recovered;
1820
+ }
1821
+ if (!options.config.wsaaSessionStore) {
1822
+ throw new ArcaConfigurationError(
1823
+ "WSAA login failed because another process likely owns a valid TA. Configure a durable wsaaSessionStore for multi-process or serverless deployments.",
1824
+ { cause: error }
1825
+ );
1620
1826
  }
1621
1827
  }
1622
1828
  if (error instanceof ArcaSoapFaultError) {
@@ -1647,7 +1853,8 @@ async function requestCredentials(config, service, options) {
1647
1853
  ARCA_WSAA_CONFIG.namespace,
1648
1854
  { in0: signedCms }
1649
1855
  );
1650
- const responseXml = await postXml({
1856
+ const url = ARCA_WSAA_CONFIG.endpoint[config.environment];
1857
+ const response = await postXmlWithMetadata({
1651
1858
  url: ARCA_WSAA_CONFIG.endpoint[config.environment],
1652
1859
  body: requestXml,
1653
1860
  contentType: 'text/xml; charset="utf-8"',
@@ -1659,9 +1866,20 @@ async function requestCredentials(config, service, options) {
1659
1866
  service: "wsaa",
1660
1867
  operation: "loginCms"
1661
1868
  });
1662
- const soapBody = parseSoapBody(responseXml);
1663
- const [, response] = getSingleBodyEntry(soapBody);
1664
- const loginCmsReturn = response.loginCmsReturn;
1869
+ const parseContext = {
1870
+ service: "wsaa",
1871
+ operation: "loginCms",
1872
+ endpointUrl: url,
1873
+ statusCode: response.statusCode,
1874
+ contentType: response.contentType,
1875
+ responseBody: response.body
1876
+ };
1877
+ const soapBody = parseSoapBody(response.body, parseContext);
1878
+ const [, responseBody] = getSingleBodyEntry(
1879
+ soapBody,
1880
+ parseContext
1881
+ );
1882
+ const loginCmsReturn = responseBody.loginCmsReturn;
1665
1883
  if (typeof loginCmsReturn !== "string" || loginCmsReturn.trim().length < 1) {
1666
1884
  throw new ArcaTransportError(
1667
1885
  "WSAA response did not include loginCmsReturn XML"
@@ -1669,24 +1887,141 @@ async function requestCredentials(config, service, options) {
1669
1887
  }
1670
1888
  return parseLoginTicketResponse(loginCmsReturn);
1671
1889
  }
1672
- function buildWsaaCacheKey(config, service) {
1673
- return [config.environment, service, getCertificateFingerprint(config)].join(
1674
- ":"
1675
- );
1890
+ function buildWsaaSessionKey(config, service) {
1891
+ return {
1892
+ environment: config.environment,
1893
+ service,
1894
+ certificateFingerprint: getCertificateFingerprint(config)
1895
+ };
1676
1896
  }
1677
1897
  function getCertificateFingerprint(config) {
1678
1898
  return createHash("sha256").update(config.certificatePem).digest("hex");
1679
1899
  }
1680
- function isCredentialValid(credentials) {
1681
- return new Date(credentials.expiresAt).getTime() - Date.now() > 6e4;
1682
- }
1683
1900
  function getCachedCredentials(cache, cacheKey) {
1684
1901
  const localCached = cache.get(cacheKey);
1685
- if (localCached && isCredentialValid(localCached)) {
1902
+ if (localCached && isWsaaCredentialValid(localCached)) {
1686
1903
  return localCached;
1687
1904
  }
1688
1905
  return null;
1689
1906
  }
1907
+ async function getReusableCredentials(options) {
1908
+ if (options.allowCache) {
1909
+ const cached = getCachedCredentials(options.cache, options.cacheKey);
1910
+ if (cached) {
1911
+ options.logger?.debug("Attempting WSAA login", {
1912
+ service: options.service,
1913
+ source: "cached"
1914
+ });
1915
+ return cached;
1916
+ }
1917
+ }
1918
+ if (!(options.allowStore && options.config.wsaaSessionStore)) {
1919
+ return null;
1920
+ }
1921
+ const stored = await getStoredCredentials(
1922
+ options.config,
1923
+ options.sessionKey,
1924
+ options.service
1925
+ );
1926
+ if (!stored) {
1927
+ return null;
1928
+ }
1929
+ options.cache.set(options.cacheKey, stored);
1930
+ options.logger?.debug("Attempting WSAA login", {
1931
+ service: options.service,
1932
+ source: "store"
1933
+ });
1934
+ return stored;
1935
+ }
1936
+ async function refreshWsaaCredentials(options) {
1937
+ if (!options.forceRefresh) {
1938
+ const reuse = await getReusableCredentials({
1939
+ config: options.config,
1940
+ cache: options.cache,
1941
+ cacheKey: options.cacheKey,
1942
+ sessionKey: options.sessionKey,
1943
+ logger: options.logger,
1944
+ service: options.service,
1945
+ allowStore: true,
1946
+ allowCache: true
1947
+ });
1948
+ if (reuse) {
1949
+ return reuse;
1950
+ }
1951
+ }
1952
+ options.logger?.debug("Attempting WSAA login", {
1953
+ service: options.service,
1954
+ source: "fresh"
1955
+ });
1956
+ const credentials = await requestCredentials(
1957
+ options.config,
1958
+ options.service,
1959
+ {
1960
+ logger: options.logger
1961
+ }
1962
+ );
1963
+ options.logger?.info("WSAA login succeeded", {
1964
+ service: options.service,
1965
+ expiresAt: credentials.expiresAt
1966
+ });
1967
+ options.cache.set(options.cacheKey, credentials);
1968
+ await setStoredCredentials(options.config, options.sessionKey, credentials);
1969
+ return credentials;
1970
+ }
1971
+ async function getStoredCredentials(config, key, service) {
1972
+ if (!config.wsaaSessionStore) {
1973
+ return null;
1974
+ }
1975
+ try {
1976
+ const credentials = await config.wsaaSessionStore.get(key);
1977
+ if (!(credentials && isWsaaCredentialValid(credentials))) {
1978
+ return null;
1979
+ }
1980
+ return credentials;
1981
+ } catch (error) {
1982
+ throw new ArcaConfigurationError(
1983
+ `WSAA session store get failed for service ${service}`,
1984
+ { cause: error instanceof Error ? error : void 0 }
1985
+ );
1986
+ }
1987
+ }
1988
+ async function setStoredCredentials(config, key, credentials) {
1989
+ if (!config.wsaaSessionStore) {
1990
+ return;
1991
+ }
1992
+ try {
1993
+ await config.wsaaSessionStore.set(key, credentials);
1994
+ } catch (error) {
1995
+ throw new ArcaConfigurationError(
1996
+ `WSAA session store set failed for service ${key.service}`,
1997
+ { cause: error instanceof Error ? error : void 0 }
1998
+ );
1999
+ }
2000
+ }
2001
+ async function withWsaaSessionStoreLock(config, key, service, fn) {
2002
+ const store = config.wsaaSessionStore;
2003
+ if (!store?.withLock) {
2004
+ return await fn();
2005
+ }
2006
+ let entered = false;
2007
+ try {
2008
+ return await store.withLock(key, async () => {
2009
+ entered = true;
2010
+ return await fn();
2011
+ });
2012
+ } catch (error) {
2013
+ if (entered) {
2014
+ throw error;
2015
+ }
2016
+ if (error instanceof ArcaConfigurationError) {
2017
+ throw error;
2018
+ }
2019
+ throw new ArcaConfigurationError(
2020
+ `WSAA session store lock failed for service ${service}`,
2021
+ { cause: error instanceof Error ? error : void 0 }
2022
+ );
2023
+ }
2024
+ }
1690
2025
  function buildLoginTicketRequest(service) {
1691
2026
  const uniqueId = Math.floor(Date.now() / 1e3);
1692
2027
  const generationTime = new Date(Date.now() - 5 * 6e4).toISOString().replace(".000Z", "Z");
@@ -1771,12 +2106,14 @@ export {
1771
2106
  ArcaConfigurationError,
1772
2107
  ArcaError,
1773
2108
  ArcaInputError,
2109
+ ArcaInvalidSoapResponseError,
1774
2110
  ArcaServiceError,
1775
2111
  ArcaSoapFaultError,
1776
2112
  ArcaTransportError,
1777
2113
  assertArcaClientConfig,
1778
2114
  createArcaClient,
1779
2115
  createArcaClientConfigFromEnv,
2116
+ createMemoryWsaaSessionStore,
1780
2117
  createPadronService,
1781
2118
  createWsfeService,
1782
2119
  createWsmtxcaService,