facturas 0.4.1 → 0.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.
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) {
@@ -439,7 +485,64 @@ function createWsfeService(options) {
439
485
  });
440
486
  return getWsfeResultEntries(result, resultKey).map(mapWsfeCatalogEntry);
441
487
  }
488
+ function authorizeVoucher({
489
+ representedTaxId,
490
+ data,
491
+ voucherNumber
492
+ }) {
493
+ const normalizedInput = normalizeWsfeVoucherInput(data);
494
+ return authorizeNormalizedVoucher({
495
+ representedTaxId,
496
+ data: normalizedInput,
497
+ voucherNumber
498
+ });
499
+ }
500
+ async function authorizeNormalizedVoucher({
501
+ representedTaxId,
502
+ data: normalizedInput,
503
+ voucherNumber
504
+ }) {
505
+ const requestData = mapWsfeVoucherInput(normalizedInput, voucherNumber);
506
+ const auth = await options.auth.login("wsfe", { representedTaxId });
507
+ const response = await options.soap.execute({
508
+ service: "wsfe",
509
+ operation: "FECAESolicitar",
510
+ body: {
511
+ Auth: createWsfeAuth(
512
+ representedTaxId ?? options.config.taxId,
513
+ auth.token,
514
+ auth.sign
515
+ ),
516
+ FeCAEReq: {
517
+ FeCabReq: {
518
+ CantReg: 1,
519
+ PtoVta: normalizedInput.salesPoint,
520
+ CbteTipo: normalizedInput.voucherType
521
+ },
522
+ FeDetReq: {
523
+ FECAEDetRequest: requestData
524
+ }
525
+ }
526
+ }
527
+ });
528
+ const result = unwrapWsfeOperationResult("FECAESolicitar", response.result);
529
+ const detailResponse = normalizeWsfeDetailResponse(result);
530
+ const cae = detailResponse.CAE;
531
+ const caeExpiry = detailResponse.CAEFchVto;
532
+ if (typeof cae !== "string" || typeof caeExpiry !== "string") {
533
+ throw new ArcaServiceError("WSFE did not return CAE authorization data", {
534
+ detail: result
535
+ });
536
+ }
537
+ return {
538
+ cae,
539
+ caeExpiry: String(caeExpiry),
540
+ voucherNumber,
541
+ raw: result
542
+ };
543
+ }
442
544
  return {
545
+ authorizeVoucher,
443
546
  async createNextVoucher({ representedTaxId, data }) {
444
547
  const normalizedInput = normalizeWsfeVoucherInput(data);
445
548
  const voucherNumber = await getNextVoucherNumber({
@@ -447,48 +550,11 @@ function createWsfeService(options) {
447
550
  salesPoint: normalizedInput.salesPoint,
448
551
  voucherType: normalizedInput.voucherType
449
552
  });
450
- const requestData = mapWsfeVoucherInput(normalizedInput, voucherNumber);
451
- const auth = await options.auth.login("wsfe", { representedTaxId });
452
- const response = await options.soap.execute({
453
- service: "wsfe",
454
- operation: "FECAESolicitar",
455
- body: {
456
- Auth: createWsfeAuth(
457
- representedTaxId ?? options.config.taxId,
458
- auth.token,
459
- auth.sign
460
- ),
461
- FeCAEReq: {
462
- FeCabReq: {
463
- CantReg: 1,
464
- PtoVta: normalizedInput.salesPoint,
465
- CbteTipo: normalizedInput.voucherType
466
- },
467
- FeDetReq: {
468
- FECAEDetRequest: requestData
469
- }
470
- }
471
- }
553
+ return authorizeNormalizedVoucher({
554
+ representedTaxId,
555
+ data: normalizedInput,
556
+ voucherNumber
472
557
  });
473
- const result = unwrapWsfeOperationResult(
474
- "FECAESolicitar",
475
- response.result
476
- );
477
- const detailResponse = normalizeWsfeDetailResponse(result);
478
- const cae = detailResponse.CAE;
479
- const caeExpiry = detailResponse.CAEFchVto;
480
- if (typeof cae !== "string" || typeof caeExpiry !== "string") {
481
- throw new ArcaServiceError(
482
- "WSFE did not return CAE authorization data",
483
- { detail: result }
484
- );
485
- }
486
- return {
487
- cae,
488
- caeExpiry: String(caeExpiry),
489
- voucherNumber,
490
- raw: result
491
- };
492
558
  },
493
559
  getNextVoucherNumber,
494
560
  getLastVoucher(input) {
@@ -1199,7 +1265,7 @@ var legacyTlsAgent = new https.Agent({
1199
1265
  keepAlive: true,
1200
1266
  ciphers: "DEFAULT@SECLEVEL=0"
1201
1267
  });
1202
- async function postXml({
1268
+ async function postXmlWithMetadata({
1203
1269
  url,
1204
1270
  body,
1205
1271
  contentType,
@@ -1267,12 +1333,12 @@ async function postXmlOnce({
1267
1333
  const requestBody = Buffer.from(body, "utf8");
1268
1334
  return await new Promise((resolve, reject) => {
1269
1335
  let settled = false;
1270
- const settleResolve = (responseBody) => {
1336
+ const settleResolve = (response) => {
1271
1337
  if (settled) {
1272
1338
  return;
1273
1339
  }
1274
1340
  settled = true;
1275
- resolve(responseBody);
1341
+ resolve(response);
1276
1342
  };
1277
1343
  const settleReject = (error) => {
1278
1344
  if (settled) {
@@ -1331,11 +1397,19 @@ async function postXmlOnce({
1331
1397
  response.headers["content-type"]
1332
1398
  ) ? response.headers["content-type"].join("; ") : response.headers["content-type"];
1333
1399
  if (statusCode >= 200 && statusCode < 300) {
1334
- settleResolve(responseBody);
1400
+ settleResolve({
1401
+ body: responseBody,
1402
+ statusCode,
1403
+ contentType: responseContentType
1404
+ });
1335
1405
  return;
1336
1406
  }
1337
1407
  if (isXmlLikeResponse(responseBody, responseContentType)) {
1338
- settleResolve(responseBody);
1408
+ settleResolve({
1409
+ body: responseBody,
1410
+ statusCode,
1411
+ contentType: responseContentType
1412
+ });
1339
1413
  return;
1340
1414
  }
1341
1415
  settleReject(
@@ -1343,6 +1417,7 @@ async function postXmlOnce({
1343
1417
  `ARCA HTTP request failed with status ${statusCode}`,
1344
1418
  {
1345
1419
  statusCode,
1420
+ contentType: responseContentType,
1346
1421
  responseBody
1347
1422
  }
1348
1423
  )
@@ -1417,14 +1492,26 @@ function buildSoapEnvelope(soapVersion, operation, namespace, body, options) {
1417
1492
  };
1418
1493
  return `<?xml version="1.0" encoding="utf-8"?>${xmlBuilder.build(payload)}`;
1419
1494
  }
1420
- function parseSoapBody(xml) {
1421
- const parsed = xmlParser.parse(xml);
1495
+ function parseSoapBody(xml, context = {}) {
1496
+ let parsed;
1497
+ try {
1498
+ parsed = xmlParser.parse(xml);
1499
+ } catch (error) {
1500
+ throw createInvalidSoapResponseError(
1501
+ "Invalid SOAP response: XML parse failed",
1502
+ context,
1503
+ void 0,
1504
+ error instanceof Error ? error : void 0
1505
+ );
1506
+ }
1422
1507
  const envelope = parsed.Envelope;
1423
1508
  const body = envelope?.Body;
1424
1509
  if (!body) {
1425
- throw new ArcaSoapFaultError("Invalid SOAP response: missing body", {
1426
- detail: parsed
1427
- });
1510
+ throw createInvalidSoapResponseError(
1511
+ "Invalid SOAP response: missing body",
1512
+ context,
1513
+ parsed
1514
+ );
1428
1515
  }
1429
1516
  const fault = body.Fault;
1430
1517
  if (fault) {
@@ -1432,18 +1519,40 @@ function parseSoapBody(xml) {
1432
1519
  }
1433
1520
  return body;
1434
1521
  }
1435
- function getSingleBodyEntry(body) {
1522
+ function getSingleBodyEntry(body, context = {}) {
1436
1523
  const entries = Object.entries(body).filter(([key]) => key !== "@_xmlns");
1437
1524
  if (entries.length !== 1) {
1438
- throw new ArcaSoapFaultError(
1525
+ throw createInvalidSoapResponseError(
1439
1526
  `Invalid SOAP response: expected a single body entry, got ${entries.length}`,
1440
- {
1441
- detail: body
1442
- }
1527
+ context,
1528
+ body
1443
1529
  );
1444
1530
  }
1445
1531
  return entries[0];
1446
1532
  }
1533
+ function createInvalidSoapResponseError(message, context, parsedDetail, cause) {
1534
+ const responseBody = context.responseBody ?? "";
1535
+ return new ArcaInvalidSoapResponseError(message, {
1536
+ cause,
1537
+ service: context.service,
1538
+ operation: context.operation,
1539
+ endpointUrl: context.endpointUrl,
1540
+ statusCode: context.statusCode,
1541
+ contentType: context.contentType,
1542
+ responseBodyLength: responseBody.length,
1543
+ responseBodyPreview: sanitizeSoapResponsePreview(
1544
+ responseBody,
1545
+ context.responseBodyPreviewLength
1546
+ ),
1547
+ parsedDetail
1548
+ });
1549
+ }
1550
+ function sanitizeSoapResponsePreview(responseBody, maxLength = 4096) {
1551
+ return responseBody.replace(
1552
+ /<((?:[A-Za-z_][\w.-]*:)?(?:Token|Sign))\b([^>]*)>[\s\S]*?<\/\1>/gi,
1553
+ (_match, tagName, attributes) => `<${tagName}${attributes}>[REDACTED]</${tagName}>`
1554
+ ).slice(0, maxLength);
1555
+ }
1447
1556
  function parseXmlDocument(xml) {
1448
1557
  return xmlParser.parse(xml);
1449
1558
  }
@@ -1502,7 +1611,7 @@ function createSoapTransport(options) {
1502
1611
  url
1503
1612
  });
1504
1613
  try {
1505
- const responseXml = await postXml({
1614
+ const response = await postXmlWithMetadata({
1506
1615
  url,
1507
1616
  body: xml,
1508
1617
  contentType,
@@ -1520,12 +1629,23 @@ function createSoapTransport(options) {
1520
1629
  operation: request.operation,
1521
1630
  durationMs: Date.now() - startedAt
1522
1631
  });
1523
- const soapBody = parseSoapBody(responseXml);
1524
- const [, result] = getSingleBodyEntry(soapBody);
1632
+ const parseContext = {
1633
+ service: request.service,
1634
+ operation: request.operation,
1635
+ endpointUrl: url,
1636
+ statusCode: response.statusCode,
1637
+ contentType: response.contentType,
1638
+ responseBody: response.body
1639
+ };
1640
+ const soapBody = parseSoapBody(response.body, parseContext);
1641
+ const [, result] = getSingleBodyEntry(
1642
+ soapBody,
1643
+ parseContext
1644
+ );
1525
1645
  return {
1526
1646
  service: request.service,
1527
1647
  operation: request.operation,
1528
- raw: responseXml,
1648
+ raw: response.body,
1529
1649
  result
1530
1650
  };
1531
1651
  } catch (error) {
@@ -1538,6 +1658,17 @@ function createSoapTransport(options) {
1538
1658
  error
1539
1659
  });
1540
1660
  }
1661
+ if (error instanceof ArcaInvalidSoapResponseError) {
1662
+ options.logger?.error("ARCA invalid SOAP response", {
1663
+ service: request.service,
1664
+ operation: request.operation,
1665
+ url,
1666
+ statusCode: error.statusCode,
1667
+ contentType: error.contentType,
1668
+ responseBodyLength: error.responseBodyLength,
1669
+ error
1670
+ });
1671
+ }
1541
1672
  throw error;
1542
1673
  }
1543
1674
  }
@@ -1547,48 +1678,117 @@ function createSoapTransport(options) {
1547
1678
  // src/wsaa/index.ts
1548
1679
  import { createHash } from "crypto";
1549
1680
  import forge from "node-forge";
1681
+
1682
+ // src/wsaa/session-store.ts
1683
+ var CREDENTIAL_EXPIRY_SAFETY_MARGIN_MS = 6e4;
1684
+ function createMemoryWsaaSessionStore() {
1685
+ const sessions = /* @__PURE__ */ new Map();
1686
+ const locks = /* @__PURE__ */ new Map();
1687
+ return {
1688
+ get(key) {
1689
+ const credentials = sessions.get(serializeWsaaSessionKey(key));
1690
+ if (!(credentials && isWsaaCredentialValid(credentials))) {
1691
+ return Promise.resolve(null);
1692
+ }
1693
+ return Promise.resolve({ ...credentials });
1694
+ },
1695
+ set(key, credentials) {
1696
+ sessions.set(serializeWsaaSessionKey(key), { ...credentials });
1697
+ return Promise.resolve();
1698
+ },
1699
+ delete(key) {
1700
+ sessions.delete(serializeWsaaSessionKey(key));
1701
+ return Promise.resolve();
1702
+ },
1703
+ async withLock(key, fn) {
1704
+ const lockKey = serializeWsaaSessionKey(key);
1705
+ const previous = locks.get(lockKey) ?? Promise.resolve();
1706
+ let release = () => void 0;
1707
+ const current = new Promise((resolve) => {
1708
+ release = resolve;
1709
+ });
1710
+ const queued = previous.catch(() => void 0).then(() => current);
1711
+ locks.set(lockKey, queued);
1712
+ await previous.catch(() => void 0);
1713
+ try {
1714
+ return await fn();
1715
+ } finally {
1716
+ release();
1717
+ if (locks.get(lockKey) === queued) {
1718
+ locks.delete(lockKey);
1719
+ }
1720
+ }
1721
+ }
1722
+ };
1723
+ }
1724
+ function serializeWsaaSessionKey(key) {
1725
+ return [key.environment, key.service, key.certificateFingerprint].join(":");
1726
+ }
1727
+ function isWsaaCredentialValid(credentials) {
1728
+ return new Date(credentials.expiresAt).getTime() - Date.now() > CREDENTIAL_EXPIRY_SAFETY_MARGIN_MS;
1729
+ }
1730
+
1731
+ // src/wsaa/index.ts
1550
1732
  function createWsaaAuthModule(options) {
1551
1733
  const cache = /* @__PURE__ */ new Map();
1552
1734
  const inFlight = /* @__PURE__ */ new Map();
1553
1735
  return {
1554
1736
  async login(service, authOptions = {}) {
1555
- const cacheKey = buildWsaaCacheKey(options.config, service);
1737
+ const sessionKey = buildWsaaSessionKey(options.config, service);
1738
+ const cacheKey = serializeWsaaSessionKey(sessionKey);
1556
1739
  const running = inFlight.get(cacheKey);
1557
1740
  if (running) {
1558
1741
  return running;
1559
1742
  }
1560
1743
  const loginPromise = (async () => {
1561
- if (!authOptions.forceRefresh) {
1562
- const cached = getCachedCredentials(cache, cacheKey);
1563
- if (cached) {
1564
- options.logger?.debug("Attempting WSAA login", {
1565
- service,
1566
- source: "cached"
1567
- });
1568
- return cached;
1569
- }
1570
- }
1571
- options.logger?.debug("Attempting WSAA login", {
1744
+ const reuse = await getReusableCredentials({
1745
+ config: options.config,
1746
+ cache,
1747
+ cacheKey,
1748
+ sessionKey,
1749
+ logger: options.logger,
1572
1750
  service,
1573
- source: "fresh"
1574
- });
1575
- const credentials = await requestCredentials(options.config, service, {
1576
- logger: options.logger
1751
+ allowStore: !authOptions.forceRefresh,
1752
+ allowCache: !authOptions.forceRefresh
1577
1753
  });
1578
- options.logger?.info("WSAA login succeeded", {
1754
+ if (reuse) {
1755
+ return reuse;
1756
+ }
1757
+ const refresh = () => refreshWsaaCredentials({
1758
+ config: options.config,
1759
+ cache,
1760
+ cacheKey,
1761
+ sessionKey,
1762
+ logger: options.logger,
1579
1763
  service,
1580
- expiresAt: credentials.expiresAt
1764
+ forceRefresh: authOptions.forceRefresh === true
1581
1765
  });
1582
- cache.set(cacheKey, credentials);
1583
- return credentials;
1766
+ if (options.config.wsaaSessionStore?.withLock) {
1767
+ return await withWsaaSessionStoreLock(
1768
+ options.config,
1769
+ sessionKey,
1770
+ service,
1771
+ refresh
1772
+ );
1773
+ }
1774
+ return await refresh();
1584
1775
  })();
1585
1776
  inFlight.set(cacheKey, loginPromise);
1586
1777
  try {
1587
1778
  return await loginPromise;
1588
1779
  } catch (error) {
1589
- if (!authOptions.forceRefresh && error instanceof ArcaSoapFaultError && error.faultCode === "ns1:coe.alreadyAuthenticated") {
1590
- const cached = getCachedCredentials(cache, cacheKey);
1591
- if (cached) {
1780
+ if (error instanceof ArcaSoapFaultError && error.faultCode === "ns1:coe.alreadyAuthenticated") {
1781
+ const recovered = await getReusableCredentials({
1782
+ config: options.config,
1783
+ cache,
1784
+ cacheKey,
1785
+ sessionKey,
1786
+ logger: options.logger,
1787
+ service,
1788
+ allowStore: true,
1789
+ allowCache: true
1790
+ });
1791
+ if (recovered) {
1592
1792
  options.logger?.warn(
1593
1793
  "Recovered WSAA coe.alreadyAuthenticated fault",
1594
1794
  {
@@ -1596,7 +1796,13 @@ function createWsaaAuthModule(options) {
1596
1796
  faultCode: error.faultCode
1597
1797
  }
1598
1798
  );
1599
- return cached;
1799
+ return recovered;
1800
+ }
1801
+ if (!options.config.wsaaSessionStore) {
1802
+ throw new ArcaConfigurationError(
1803
+ "WSAA login failed because another process likely owns a valid TA. Configure a durable wsaaSessionStore for multi-process or serverless deployments.",
1804
+ { cause: error }
1805
+ );
1600
1806
  }
1601
1807
  }
1602
1808
  if (error instanceof ArcaSoapFaultError) {
@@ -1627,7 +1833,8 @@ async function requestCredentials(config, service, options) {
1627
1833
  ARCA_WSAA_CONFIG.namespace,
1628
1834
  { in0: signedCms }
1629
1835
  );
1630
- const responseXml = await postXml({
1836
+ const url = ARCA_WSAA_CONFIG.endpoint[config.environment];
1837
+ const response = await postXmlWithMetadata({
1631
1838
  url: ARCA_WSAA_CONFIG.endpoint[config.environment],
1632
1839
  body: requestXml,
1633
1840
  contentType: 'text/xml; charset="utf-8"',
@@ -1639,9 +1846,20 @@ async function requestCredentials(config, service, options) {
1639
1846
  service: "wsaa",
1640
1847
  operation: "loginCms"
1641
1848
  });
1642
- const soapBody = parseSoapBody(responseXml);
1643
- const [, response] = getSingleBodyEntry(soapBody);
1644
- const loginCmsReturn = response.loginCmsReturn;
1849
+ const parseContext = {
1850
+ service: "wsaa",
1851
+ operation: "loginCms",
1852
+ endpointUrl: url,
1853
+ statusCode: response.statusCode,
1854
+ contentType: response.contentType,
1855
+ responseBody: response.body
1856
+ };
1857
+ const soapBody = parseSoapBody(response.body, parseContext);
1858
+ const [, responseBody] = getSingleBodyEntry(
1859
+ soapBody,
1860
+ parseContext
1861
+ );
1862
+ const loginCmsReturn = responseBody.loginCmsReturn;
1645
1863
  if (typeof loginCmsReturn !== "string" || loginCmsReturn.trim().length < 1) {
1646
1864
  throw new ArcaTransportError(
1647
1865
  "WSAA response did not include loginCmsReturn XML"
@@ -1649,24 +1867,141 @@ async function requestCredentials(config, service, options) {
1649
1867
  }
1650
1868
  return parseLoginTicketResponse(loginCmsReturn);
1651
1869
  }
1652
- function buildWsaaCacheKey(config, service) {
1653
- return [config.environment, service, getCertificateFingerprint(config)].join(
1654
- ":"
1655
- );
1870
+ function buildWsaaSessionKey(config, service) {
1871
+ return {
1872
+ environment: config.environment,
1873
+ service,
1874
+ certificateFingerprint: getCertificateFingerprint(config)
1875
+ };
1656
1876
  }
1657
1877
  function getCertificateFingerprint(config) {
1658
1878
  return createHash("sha256").update(config.certificatePem).digest("hex");
1659
1879
  }
1660
- function isCredentialValid(credentials) {
1661
- return new Date(credentials.expiresAt).getTime() - Date.now() > 6e4;
1662
- }
1663
1880
  function getCachedCredentials(cache, cacheKey) {
1664
1881
  const localCached = cache.get(cacheKey);
1665
- if (localCached && isCredentialValid(localCached)) {
1882
+ if (localCached && isWsaaCredentialValid(localCached)) {
1666
1883
  return localCached;
1667
1884
  }
1668
1885
  return null;
1669
1886
  }
1887
+ async function getReusableCredentials(options) {
1888
+ if (options.allowCache) {
1889
+ const cached = getCachedCredentials(options.cache, options.cacheKey);
1890
+ if (cached) {
1891
+ options.logger?.debug("Attempting WSAA login", {
1892
+ service: options.service,
1893
+ source: "cached"
1894
+ });
1895
+ return cached;
1896
+ }
1897
+ }
1898
+ if (!(options.allowStore && options.config.wsaaSessionStore)) {
1899
+ return null;
1900
+ }
1901
+ const stored = await getStoredCredentials(
1902
+ options.config,
1903
+ options.sessionKey,
1904
+ options.service
1905
+ );
1906
+ if (!stored) {
1907
+ return null;
1908
+ }
1909
+ options.cache.set(options.cacheKey, stored);
1910
+ options.logger?.debug("Attempting WSAA login", {
1911
+ service: options.service,
1912
+ source: "store"
1913
+ });
1914
+ return stored;
1915
+ }
1916
+ async function refreshWsaaCredentials(options) {
1917
+ if (!options.forceRefresh) {
1918
+ const reuse = await getReusableCredentials({
1919
+ config: options.config,
1920
+ cache: options.cache,
1921
+ cacheKey: options.cacheKey,
1922
+ sessionKey: options.sessionKey,
1923
+ logger: options.logger,
1924
+ service: options.service,
1925
+ allowStore: true,
1926
+ allowCache: true
1927
+ });
1928
+ if (reuse) {
1929
+ return reuse;
1930
+ }
1931
+ }
1932
+ options.logger?.debug("Attempting WSAA login", {
1933
+ service: options.service,
1934
+ source: "fresh"
1935
+ });
1936
+ const credentials = await requestCredentials(
1937
+ options.config,
1938
+ options.service,
1939
+ {
1940
+ logger: options.logger
1941
+ }
1942
+ );
1943
+ options.logger?.info("WSAA login succeeded", {
1944
+ service: options.service,
1945
+ expiresAt: credentials.expiresAt
1946
+ });
1947
+ options.cache.set(options.cacheKey, credentials);
1948
+ await setStoredCredentials(options.config, options.sessionKey, credentials);
1949
+ return credentials;
1950
+ }
1951
+ async function getStoredCredentials(config, key, service) {
1952
+ if (!config.wsaaSessionStore) {
1953
+ return null;
1954
+ }
1955
+ try {
1956
+ const credentials = await config.wsaaSessionStore.get(key);
1957
+ if (!(credentials && isWsaaCredentialValid(credentials))) {
1958
+ return null;
1959
+ }
1960
+ return credentials;
1961
+ } catch (error) {
1962
+ throw new ArcaConfigurationError(
1963
+ `WSAA session store get failed for service ${service}`,
1964
+ { cause: error instanceof Error ? error : void 0 }
1965
+ );
1966
+ }
1967
+ }
1968
+ async function setStoredCredentials(config, key, credentials) {
1969
+ if (!config.wsaaSessionStore) {
1970
+ return;
1971
+ }
1972
+ try {
1973
+ await config.wsaaSessionStore.set(key, credentials);
1974
+ } catch (error) {
1975
+ throw new ArcaConfigurationError(
1976
+ `WSAA session store set failed for service ${key.service}`,
1977
+ { cause: error instanceof Error ? error : void 0 }
1978
+ );
1979
+ }
1980
+ }
1981
+ async function withWsaaSessionStoreLock(config, key, service, fn) {
1982
+ const store = config.wsaaSessionStore;
1983
+ if (!store?.withLock) {
1984
+ return await fn();
1985
+ }
1986
+ let entered = false;
1987
+ try {
1988
+ return await store.withLock(key, async () => {
1989
+ entered = true;
1990
+ return await fn();
1991
+ });
1992
+ } catch (error) {
1993
+ if (entered) {
1994
+ throw error;
1995
+ }
1996
+ if (error instanceof ArcaConfigurationError) {
1997
+ throw error;
1998
+ }
1999
+ throw new ArcaConfigurationError(
2000
+ `WSAA session store lock failed for service ${service}`,
2001
+ { cause: error instanceof Error ? error : void 0 }
2002
+ );
2003
+ }
2004
+ }
1670
2005
  function buildLoginTicketRequest(service) {
1671
2006
  const uniqueId = Math.floor(Date.now() / 1e3);
1672
2007
  const generationTime = new Date(Date.now() - 5 * 6e4).toISOString().replace(".000Z", "Z");
@@ -1751,12 +2086,14 @@ export {
1751
2086
  ArcaConfigurationError,
1752
2087
  ArcaError,
1753
2088
  ArcaInputError,
2089
+ ArcaInvalidSoapResponseError,
1754
2090
  ArcaServiceError,
1755
2091
  ArcaSoapFaultError,
1756
2092
  ArcaTransportError,
1757
2093
  assertArcaClientConfig,
1758
2094
  createArcaClient,
1759
2095
  createArcaClientConfigFromEnv,
2096
+ createMemoryWsaaSessionStore,
1760
2097
  createPadronService,
1761
2098
  createWsfeService,
1762
2099
  createWsmtxcaService,