catalyst-relay 0.2.0 → 0.2.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.mjs CHANGED
@@ -1,3 +1,10 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
1
8
  // src/types/result.ts
2
9
  function ok(value) {
3
10
  return [value, null];
@@ -224,6 +231,15 @@ function debugError(message, cause) {
224
231
 
225
232
  // src/types/config.ts
226
233
  import { z } from "zod";
234
+ var samlFormSelectorsSchema = z.object({
235
+ username: z.string().min(1),
236
+ password: z.string().min(1),
237
+ submit: z.string().min(1)
238
+ });
239
+ var samlProviderConfigSchema = z.object({
240
+ ignoreHttpsErrors: z.boolean(),
241
+ formSelectors: samlFormSelectorsSchema
242
+ });
227
243
  var clientConfigSchema = z.object({
228
244
  url: z.string().url(),
229
245
  client: z.string().min(1).max(3),
@@ -237,10 +253,14 @@ var clientConfigSchema = z.object({
237
253
  type: z.literal("saml"),
238
254
  username: z.string().min(1),
239
255
  password: z.string().min(1),
240
- provider: z.string().optional()
256
+ providerConfig: samlProviderConfigSchema.optional()
241
257
  }),
242
258
  z.object({
243
259
  type: z.literal("sso"),
260
+ slsUrl: z.string().url(),
261
+ profile: z.string().optional(),
262
+ servicePrincipalName: z.string().optional(),
263
+ forceEnroll: z.boolean().optional(),
244
264
  certificate: z.string().optional()
245
265
  })
246
266
  ]),
@@ -248,6 +268,16 @@ var clientConfigSchema = z.object({
248
268
  insecure: z.boolean().optional()
249
269
  });
250
270
 
271
+ // src/core/session/types.ts
272
+ var DEFAULT_SESSION_CONFIG = {
273
+ sessionTimeout: 10800,
274
+ // 3 hours (Basic/SSO)
275
+ samlSessionTimeout: 1800,
276
+ // 30 minutes (SAML)
277
+ cleanupInterval: 60
278
+ // 1 minute
279
+ };
280
+
251
281
  // src/core/session/login.ts
252
282
  async function fetchCsrfToken(state, request) {
253
283
  const endpoint = state.config.auth.type === "saml" ? "/sap/bc/adt/core/http/sessions" : "/sap/bc/adt/compatibility/graph";
@@ -280,22 +310,43 @@ async function fetchCsrfToken(state, request) {
280
310
  debug(`Stored CSRF token in state: ${state.csrfToken?.substring(0, 20)}...`);
281
311
  return ok(token);
282
312
  }
283
- async function login(state, request) {
284
- if (state.config.auth.type === "saml") {
285
- return err(new Error("SAML authentication not yet implemented"));
313
+ function getSessionTimeout(authType) {
314
+ switch (authType) {
315
+ case "saml":
316
+ return DEFAULT_SESSION_CONFIG.samlSessionTimeout * 1e3;
317
+ case "basic":
318
+ case "sso":
319
+ return DEFAULT_SESSION_CONFIG.sessionTimeout * 1e3;
320
+ default: {
321
+ const _exhaustive = authType;
322
+ return DEFAULT_SESSION_CONFIG.sessionTimeout * 1e3;
323
+ }
286
324
  }
287
- if (state.config.auth.type === "sso") {
288
- return err(new Error("SSO authentication not yet implemented"));
325
+ }
326
+ function extractUsername(auth) {
327
+ switch (auth.type) {
328
+ case "basic":
329
+ case "saml":
330
+ return auth.username;
331
+ case "sso":
332
+ return process.env["USERNAME"] ?? process.env["USER"] ?? "SSO_USER";
333
+ default: {
334
+ const _exhaustive = auth;
335
+ return "";
336
+ }
289
337
  }
338
+ }
339
+ async function login(state, request) {
290
340
  const [token, tokenErr] = await fetchCsrfToken(state, request);
291
341
  if (tokenErr) {
292
342
  return err(new Error(`Login failed: ${tokenErr.message}`));
293
343
  }
294
- const username = state.config.auth.type === "basic" ? state.config.auth.username : "";
344
+ const username = extractUsername(state.config.auth);
345
+ const timeout = getSessionTimeout(state.config.auth.type);
295
346
  const session = {
296
347
  sessionId: token,
297
348
  username,
298
- expiresAt: Date.now() + 8 * 60 * 60 * 1e3
349
+ expiresAt: Date.now() + timeout
299
350
  };
300
351
  state.session = session;
301
352
  return ok(session);
@@ -1396,7 +1447,590 @@ async function gitDiff(client, object) {
1396
1447
  }
1397
1448
 
1398
1449
  // src/core/client.ts
1450
+ import { Agent as Agent2, fetch as undiciFetch2 } from "undici";
1451
+
1452
+ // src/core/auth/basic/basic.ts
1453
+ var BasicAuth = class {
1454
+ type = "basic";
1455
+ authHeader;
1456
+ /**
1457
+ * Create a Basic Auth strategy
1458
+ * @param username - SAP username
1459
+ * @param password - SAP password
1460
+ */
1461
+ constructor(username, password) {
1462
+ if (!username || !password) {
1463
+ throw new Error("BasicAuth requires both username and password");
1464
+ }
1465
+ const credentials = `${username}:${password}`;
1466
+ const encoded = btoa(credentials);
1467
+ this.authHeader = `Basic ${encoded}`;
1468
+ }
1469
+ /**
1470
+ * Get Authorization header with Basic credentials
1471
+ * @returns Headers object with Authorization field
1472
+ */
1473
+ getAuthHeaders() {
1474
+ return {
1475
+ Authorization: this.authHeader
1476
+ };
1477
+ }
1478
+ };
1479
+
1480
+ // src/core/auth/sso/slsClient.ts
1399
1481
  import { Agent, fetch as undiciFetch } from "undici";
1482
+
1483
+ // src/core/auth/sso/types.ts
1484
+ var SLS_DEFAULTS = {
1485
+ PROFILE: "SAPSSO_P",
1486
+ LOGIN_ENDPOINT: "/SecureLoginServer/slc3/doLogin",
1487
+ CERTIFICATE_ENDPOINT: "/SecureLoginServer/slc2/getCertificate",
1488
+ KEY_SIZE: 2048
1489
+ };
1490
+ var CERTIFICATE_STORAGE = {
1491
+ BASE_DIR: "./certificates/sso",
1492
+ FULL_CHAIN_SUFFIX: "_full_chain.pem",
1493
+ KEY_SUFFIX: "_key.pem"
1494
+ };
1495
+
1496
+ // src/core/auth/sso/kerberos.ts
1497
+ async function loadKerberosModule() {
1498
+ try {
1499
+ const kerberosModule = __require("kerberos");
1500
+ return ok(kerberosModule);
1501
+ } catch {
1502
+ return err(new Error(
1503
+ "kerberos package is not installed. Install it with: npm install kerberos"
1504
+ ));
1505
+ }
1506
+ }
1507
+ async function getSpnegoToken(servicePrincipalName) {
1508
+ const [kerberos, loadErr] = await loadKerberosModule();
1509
+ if (loadErr) return err(loadErr);
1510
+ try {
1511
+ const client = await kerberos.initializeClient(servicePrincipalName);
1512
+ const token = await client.step("");
1513
+ if (!token) {
1514
+ return err(new Error("Failed to generate SPNEGO token: empty response"));
1515
+ }
1516
+ return ok(token);
1517
+ } catch (error) {
1518
+ const message = error instanceof Error ? error.message : String(error);
1519
+ return err(new Error(`Kerberos authentication failed: ${message}`));
1520
+ }
1521
+ }
1522
+ function extractSpnFromUrl(slsUrl) {
1523
+ const url = new URL(slsUrl);
1524
+ return `HTTP/${url.hostname}`;
1525
+ }
1526
+
1527
+ // src/core/auth/sso/certificate.ts
1528
+ import forge from "node-forge";
1529
+ function generateKeypair(keySize = SLS_DEFAULTS.KEY_SIZE) {
1530
+ const keypair = forge.pki.rsa.generateKeyPair({ bits: keySize, e: 65537 });
1531
+ const privateKeyPem = forge.pki.privateKeyToPem(keypair.privateKey);
1532
+ return {
1533
+ privateKeyPem,
1534
+ privateKey: keypair.privateKey,
1535
+ publicKey: keypair.publicKey
1536
+ };
1537
+ }
1538
+ function createCsr(keypair, username) {
1539
+ const csr = forge.pki.createCertificationRequest();
1540
+ csr.publicKey = keypair.publicKey;
1541
+ csr.setSubject([{
1542
+ name: "commonName",
1543
+ value: username
1544
+ }]);
1545
+ csr.setAttributes([{
1546
+ name: "extensionRequest",
1547
+ extensions: [
1548
+ {
1549
+ name: "keyUsage",
1550
+ digitalSignature: true,
1551
+ keyEncipherment: true
1552
+ },
1553
+ {
1554
+ name: "extKeyUsage",
1555
+ clientAuth: true
1556
+ }
1557
+ ]
1558
+ }]);
1559
+ csr.sign(keypair.privateKey, forge.md.sha256.create());
1560
+ const csrAsn1 = forge.pki.certificationRequestToAsn1(csr);
1561
+ const csrDer = forge.asn1.toDer(csrAsn1);
1562
+ return Buffer.from(csrDer.getBytes(), "binary");
1563
+ }
1564
+ function getCurrentUsername() {
1565
+ return process.env["USERNAME"] ?? process.env["USER"] ?? "unknown";
1566
+ }
1567
+
1568
+ // src/core/auth/sso/pkcs7.ts
1569
+ import forge2 from "node-forge";
1570
+ function parsePkcs7Certificates(data) {
1571
+ try {
1572
+ const dataString = data.toString("utf-8").replace(/\r?\n/g, "").trim();
1573
+ const derBytes = forge2.util.decode64(dataString);
1574
+ const p7Asn1 = forge2.asn1.fromDer(derBytes);
1575
+ const p7 = forge2.pkcs7.messageFromAsn1(p7Asn1);
1576
+ if (!("certificates" in p7) || !p7.certificates || p7.certificates.length === 0) {
1577
+ return err(new Error("No certificates found in PKCS#7 structure"));
1578
+ }
1579
+ const certificates = p7.certificates;
1580
+ const clientCert = certificates[0];
1581
+ const caCerts = certificates.slice(1);
1582
+ if (!clientCert) {
1583
+ return err(new Error("No client certificate found in PKCS#7 structure"));
1584
+ }
1585
+ const clientCertPem = forge2.pki.certificateToPem(clientCert);
1586
+ const caChainPem = caCerts.map((cert) => forge2.pki.certificateToPem(cert)).join("");
1587
+ return ok({
1588
+ clientCert: clientCertPem,
1589
+ caChain: caChainPem,
1590
+ fullChain: clientCertPem + caChainPem
1591
+ });
1592
+ } catch (error) {
1593
+ const message = error instanceof Error ? error.message : String(error);
1594
+ return err(new Error(`Failed to parse PKCS#7 certificates: ${message}`));
1595
+ }
1596
+ }
1597
+
1598
+ // src/core/auth/sso/slsClient.ts
1599
+ async function enrollCertificate(options) {
1600
+ const { config, insecure = false } = options;
1601
+ const profile = config.profile ?? SLS_DEFAULTS.PROFILE;
1602
+ const agent = insecure ? new Agent({ connect: { rejectUnauthorized: false } }) : void 0;
1603
+ const [authResponse, authErr] = await authenticateToSls(config, profile, agent);
1604
+ if (authErr) return err(authErr);
1605
+ const keySize = authResponse.clientConfig.keySize ?? SLS_DEFAULTS.KEY_SIZE;
1606
+ const keypair = generateKeypair(keySize);
1607
+ const username = getCurrentUsername();
1608
+ const csrDer = createCsr(keypair, username);
1609
+ const [certData, certErr] = await requestCertificate(config, profile, csrDer, agent);
1610
+ if (certErr) return err(certErr);
1611
+ const [certs, parseErr] = parsePkcs7Certificates(certData);
1612
+ if (parseErr) return err(parseErr);
1613
+ return ok({
1614
+ fullChain: certs.fullChain,
1615
+ privateKey: keypair.privateKeyPem
1616
+ });
1617
+ }
1618
+ async function authenticateToSls(config, profile, agent) {
1619
+ const spn = config.servicePrincipalName ?? extractSpnFromUrl(config.slsUrl);
1620
+ const [token, tokenErr] = await getSpnegoToken(spn);
1621
+ if (tokenErr) return err(tokenErr);
1622
+ const authUrl = `${config.slsUrl}${SLS_DEFAULTS.LOGIN_ENDPOINT}?profile=${profile}`;
1623
+ try {
1624
+ const fetchOptions = {
1625
+ method: "POST",
1626
+ headers: {
1627
+ "Authorization": `Negotiate ${token}`,
1628
+ "Accept": "*/*"
1629
+ }
1630
+ };
1631
+ if (agent) {
1632
+ fetchOptions.dispatcher = agent;
1633
+ }
1634
+ const response = await undiciFetch(authUrl, fetchOptions);
1635
+ if (!response.ok) {
1636
+ const text = await response.text();
1637
+ return err(new Error(`SLS authentication failed: ${response.status} - ${text}`));
1638
+ }
1639
+ const authResponse = await response.json();
1640
+ return ok(authResponse);
1641
+ } catch (error) {
1642
+ const message = error instanceof Error ? error.message : String(error);
1643
+ return err(new Error(`SLS authentication request failed: ${message}`));
1644
+ }
1645
+ }
1646
+ async function requestCertificate(config, profile, csrDer, agent) {
1647
+ const certUrl = `${config.slsUrl}${SLS_DEFAULTS.CERTIFICATE_ENDPOINT}?profile=${profile}`;
1648
+ try {
1649
+ const fetchOptions = {
1650
+ method: "POST",
1651
+ headers: {
1652
+ "Content-Type": "application/pkcs10",
1653
+ "Content-Length": String(csrDer.length),
1654
+ "Accept": "*/*"
1655
+ },
1656
+ body: csrDer
1657
+ };
1658
+ if (agent) {
1659
+ fetchOptions.dispatcher = agent;
1660
+ }
1661
+ const response = await undiciFetch(certUrl, fetchOptions);
1662
+ if (!response.ok) {
1663
+ const text = await response.text();
1664
+ return err(new Error(`Certificate request failed: ${response.status} - ${text}`));
1665
+ }
1666
+ const buffer = await response.arrayBuffer();
1667
+ return ok(Buffer.from(buffer));
1668
+ } catch (error) {
1669
+ const message = error instanceof Error ? error.message : String(error);
1670
+ return err(new Error(`Certificate request failed: ${message}`));
1671
+ }
1672
+ }
1673
+
1674
+ // src/core/auth/sso/storage.ts
1675
+ import { readFile, writeFile, mkdir, stat } from "fs/promises";
1676
+ import { join } from "path";
1677
+ function getCertificatePaths(username) {
1678
+ const user = username ?? getCurrentUsername();
1679
+ return {
1680
+ fullChainPath: join(CERTIFICATE_STORAGE.BASE_DIR, `${user}${CERTIFICATE_STORAGE.FULL_CHAIN_SUFFIX}`),
1681
+ keyPath: join(CERTIFICATE_STORAGE.BASE_DIR, `${user}${CERTIFICATE_STORAGE.KEY_SUFFIX}`)
1682
+ };
1683
+ }
1684
+ async function saveCertificates(material, username) {
1685
+ const paths = getCertificatePaths(username);
1686
+ try {
1687
+ await mkdir(CERTIFICATE_STORAGE.BASE_DIR, { recursive: true });
1688
+ await writeFile(paths.fullChainPath, material.fullChain, "utf-8");
1689
+ await writeFile(paths.keyPath, material.privateKey, { encoding: "utf-8", mode: 384 });
1690
+ return ok(paths);
1691
+ } catch (error) {
1692
+ const message = error instanceof Error ? error.message : String(error);
1693
+ return err(new Error(`Failed to save certificates: ${message}`));
1694
+ }
1695
+ }
1696
+ async function loadCertificates(username) {
1697
+ const paths = getCertificatePaths(username);
1698
+ try {
1699
+ const [fullChain, privateKey] = await Promise.all([
1700
+ readFile(paths.fullChainPath, "utf-8"),
1701
+ readFile(paths.keyPath, "utf-8")
1702
+ ]);
1703
+ return ok({ fullChain, privateKey });
1704
+ } catch (error) {
1705
+ const message = error instanceof Error ? error.message : String(error);
1706
+ return err(new Error(`Failed to load certificates: ${message}`));
1707
+ }
1708
+ }
1709
+ async function certificatesExist(username) {
1710
+ const paths = getCertificatePaths(username);
1711
+ try {
1712
+ await Promise.all([
1713
+ stat(paths.fullChainPath),
1714
+ stat(paths.keyPath)
1715
+ ]);
1716
+ return true;
1717
+ } catch {
1718
+ return false;
1719
+ }
1720
+ }
1721
+ function isCertificateExpired(certPem, bufferDays = 1) {
1722
+ try {
1723
+ const forge3 = __require("node-forge");
1724
+ const cert = forge3.pki.certificateFromPem(certPem);
1725
+ const notAfter = cert.validity.notAfter;
1726
+ const bufferMs = bufferDays * 24 * 60 * 60 * 1e3;
1727
+ const expiryThreshold = new Date(Date.now() + bufferMs);
1728
+ return ok(notAfter <= expiryThreshold);
1729
+ } catch (error) {
1730
+ const message = error instanceof Error ? error.message : String(error);
1731
+ return err(new Error(`Failed to check certificate expiry: ${message}`));
1732
+ }
1733
+ }
1734
+
1735
+ // src/core/auth/sso/sso.ts
1736
+ var SsoAuth = class {
1737
+ type = "sso";
1738
+ config;
1739
+ certificates = null;
1740
+ /**
1741
+ * Create an SSO Auth strategy
1742
+ *
1743
+ * @param config - SSO authentication configuration
1744
+ */
1745
+ constructor(config) {
1746
+ if (!config.slsUrl) {
1747
+ throw new Error("SsoAuth requires slsUrl");
1748
+ }
1749
+ this.config = config;
1750
+ }
1751
+ /**
1752
+ * Get auth headers for SSO
1753
+ *
1754
+ * SSO uses mTLS for authentication, not headers.
1755
+ * Returns empty headers - the mTLS agent handles auth.
1756
+ */
1757
+ getAuthHeaders() {
1758
+ return {};
1759
+ }
1760
+ /**
1761
+ * Get mTLS certificates
1762
+ *
1763
+ * Returns the certificate material after successful login.
1764
+ * Used by ADT client to create an mTLS agent.
1765
+ *
1766
+ * @returns Certificate material or null if not enrolled
1767
+ */
1768
+ getCertificates() {
1769
+ return this.certificates;
1770
+ }
1771
+ /**
1772
+ * Perform SSO login via certificate enrollment
1773
+ *
1774
+ * Checks for existing valid certificates and enrolls new ones if needed.
1775
+ *
1776
+ * @param _fetchFn - Unused, kept for interface compatibility
1777
+ * @returns Success/error tuple
1778
+ */
1779
+ async performLogin(_fetchFn) {
1780
+ if (!this.config.forceEnroll) {
1781
+ const [loadResult, loadErr] = await this.tryLoadExistingCertificates();
1782
+ if (!loadErr && loadResult) {
1783
+ this.certificates = loadResult;
1784
+ return ok(void 0);
1785
+ }
1786
+ }
1787
+ const slsConfig = {
1788
+ slsUrl: this.config.slsUrl
1789
+ };
1790
+ if (this.config.profile) {
1791
+ slsConfig.profile = this.config.profile;
1792
+ }
1793
+ if (this.config.servicePrincipalName) {
1794
+ slsConfig.servicePrincipalName = this.config.servicePrincipalName;
1795
+ }
1796
+ const [material, enrollErr] = await enrollCertificate({
1797
+ config: slsConfig,
1798
+ insecure: this.config.insecure ?? false
1799
+ });
1800
+ if (enrollErr) {
1801
+ return err(enrollErr);
1802
+ }
1803
+ if (!this.config.returnContents) {
1804
+ const [, saveErr] = await saveCertificates(material);
1805
+ if (saveErr) {
1806
+ return err(saveErr);
1807
+ }
1808
+ }
1809
+ this.certificates = material;
1810
+ return ok(void 0);
1811
+ }
1812
+ /**
1813
+ * Try to load and validate existing certificates
1814
+ */
1815
+ async tryLoadExistingCertificates() {
1816
+ const exists = await certificatesExist();
1817
+ if (!exists) {
1818
+ return err(new Error("No existing certificates found"));
1819
+ }
1820
+ const [material, loadErr] = await loadCertificates();
1821
+ if (loadErr) {
1822
+ return err(loadErr);
1823
+ }
1824
+ const [isExpired, expiryErr] = isCertificateExpired(material.fullChain);
1825
+ if (expiryErr) {
1826
+ return err(expiryErr);
1827
+ }
1828
+ if (isExpired) {
1829
+ return err(new Error("Certificate is expired or expiring soon"));
1830
+ }
1831
+ return ok(material);
1832
+ }
1833
+ };
1834
+
1835
+ // src/core/auth/saml/types.ts
1836
+ var DEFAULT_FORM_SELECTORS = {
1837
+ username: "#j_username",
1838
+ password: "#j_password",
1839
+ submit: "#logOnFormSubmit"
1840
+ };
1841
+ var DEFAULT_PROVIDER_CONFIG = {
1842
+ ignoreHttpsErrors: false,
1843
+ formSelectors: DEFAULT_FORM_SELECTORS
1844
+ };
1845
+
1846
+ // src/core/auth/saml/browser.ts
1847
+ var TIMEOUTS = {
1848
+ PAGE_LOAD: 6e4,
1849
+ FORM_SELECTOR: 1e4
1850
+ };
1851
+ async function performBrowserLogin(options) {
1852
+ const { baseUrl, credentials, headless = true } = options;
1853
+ const config = options.providerConfig ?? DEFAULT_PROVIDER_CONFIG;
1854
+ let playwright;
1855
+ try {
1856
+ playwright = await import("playwright");
1857
+ } catch {
1858
+ return err(
1859
+ new Error(
1860
+ "Playwright is required for SAML authentication but is not installed. Install it with: npm install playwright"
1861
+ )
1862
+ );
1863
+ }
1864
+ const browserArgs = config.ignoreHttpsErrors ? ["--ignore-certificate-errors", "--disable-web-security"] : [];
1865
+ let browser;
1866
+ try {
1867
+ browser = await playwright.chromium.launch({
1868
+ headless,
1869
+ args: browserArgs
1870
+ });
1871
+ } catch (launchError) {
1872
+ return err(
1873
+ new Error(
1874
+ `Failed to launch browser: ${launchError instanceof Error ? launchError.message : String(launchError)}`
1875
+ )
1876
+ );
1877
+ }
1878
+ try {
1879
+ const context = await browser.newContext({
1880
+ ignoreHTTPSErrors: config.ignoreHttpsErrors
1881
+ });
1882
+ const page = await context.newPage();
1883
+ const loginUrl = `${baseUrl}/sap/bc/adt/compatibility/graph`;
1884
+ try {
1885
+ await page.goto(loginUrl, {
1886
+ timeout: TIMEOUTS.PAGE_LOAD,
1887
+ waitUntil: "domcontentloaded"
1888
+ });
1889
+ } catch {
1890
+ return err(new Error("Failed to load login page. Please check if the server is online."));
1891
+ }
1892
+ try {
1893
+ await page.waitForSelector(config.formSelectors.username, {
1894
+ timeout: TIMEOUTS.FORM_SELECTOR
1895
+ });
1896
+ } catch {
1897
+ return err(new Error("Login form not found. The page may have changed or loaded incorrectly."));
1898
+ }
1899
+ await page.fill(config.formSelectors.username, credentials.username);
1900
+ await page.fill(config.formSelectors.password, credentials.password);
1901
+ await page.click(config.formSelectors.submit);
1902
+ await page.waitForLoadState("networkidle");
1903
+ const cookies = await context.cookies();
1904
+ return ok(cookies);
1905
+ } finally {
1906
+ await browser.close();
1907
+ }
1908
+ }
1909
+
1910
+ // src/core/auth/saml/cookies.ts
1911
+ function toAuthCookies(playwrightCookies) {
1912
+ return playwrightCookies.map((cookie) => ({
1913
+ name: cookie.name,
1914
+ value: cookie.value,
1915
+ domain: cookie.domain,
1916
+ path: cookie.path
1917
+ }));
1918
+ }
1919
+ function formatCookieHeader(cookies) {
1920
+ return cookies.map((c) => `${c.name}=${c.value}`).join("; ");
1921
+ }
1922
+
1923
+ // src/core/auth/saml/saml.ts
1924
+ var SamlAuth = class {
1925
+ type = "saml";
1926
+ cookies = [];
1927
+ config;
1928
+ /**
1929
+ * Create a SAML Auth strategy
1930
+ *
1931
+ * @param config - SAML authentication configuration
1932
+ */
1933
+ constructor(config) {
1934
+ if (!config.username || !config.password) {
1935
+ throw new Error("SamlAuth requires both username and password");
1936
+ }
1937
+ if (!config.baseUrl) {
1938
+ throw new Error("SamlAuth requires baseUrl");
1939
+ }
1940
+ this.config = config;
1941
+ }
1942
+ /**
1943
+ * Get auth headers for SAML
1944
+ *
1945
+ * After successful login, includes Cookie header with session cookies.
1946
+ */
1947
+ getAuthHeaders() {
1948
+ if (this.cookies.length === 0) {
1949
+ return {};
1950
+ }
1951
+ return {
1952
+ Cookie: formatCookieHeader(this.cookies)
1953
+ };
1954
+ }
1955
+ /**
1956
+ * Get authentication cookies
1957
+ *
1958
+ * @returns Array of cookies obtained during SAML login
1959
+ */
1960
+ getCookies() {
1961
+ return this.cookies;
1962
+ }
1963
+ /**
1964
+ * Perform SAML login using headless browser automation
1965
+ *
1966
+ * Launches a Chromium browser, navigates to the SAP login page,
1967
+ * fills in credentials, and extracts session cookies.
1968
+ *
1969
+ * @param _fetchFn - Unused, kept for interface compatibility
1970
+ * @returns Success/error tuple
1971
+ */
1972
+ async performLogin(_fetchFn) {
1973
+ const [playwrightCookies, loginError] = await performBrowserLogin({
1974
+ baseUrl: this.config.baseUrl,
1975
+ credentials: {
1976
+ username: this.config.username,
1977
+ password: this.config.password
1978
+ },
1979
+ ...this.config.providerConfig && { providerConfig: this.config.providerConfig }
1980
+ });
1981
+ if (loginError) {
1982
+ return err(loginError);
1983
+ }
1984
+ this.cookies = toAuthCookies(playwrightCookies);
1985
+ if (this.cookies.length === 0) {
1986
+ return err(new Error("SAML login succeeded but no cookies were returned"));
1987
+ }
1988
+ return ok(void 0);
1989
+ }
1990
+ };
1991
+
1992
+ // src/core/auth/factory.ts
1993
+ function createAuthStrategy(options) {
1994
+ const { config, baseUrl, insecure } = options;
1995
+ switch (config.type) {
1996
+ case "basic":
1997
+ return new BasicAuth(config.username, config.password);
1998
+ case "saml":
1999
+ if (!baseUrl) {
2000
+ throw new Error("SAML authentication requires baseUrl");
2001
+ }
2002
+ return new SamlAuth({
2003
+ username: config.username,
2004
+ password: config.password,
2005
+ baseUrl,
2006
+ ...config.providerConfig && { providerConfig: config.providerConfig }
2007
+ });
2008
+ case "sso": {
2009
+ const ssoConfig = {
2010
+ slsUrl: config.slsUrl
2011
+ };
2012
+ if (config.profile) {
2013
+ ssoConfig.profile = config.profile;
2014
+ }
2015
+ if (config.servicePrincipalName) {
2016
+ ssoConfig.servicePrincipalName = config.servicePrincipalName;
2017
+ }
2018
+ if (config.forceEnroll) {
2019
+ ssoConfig.forceEnroll = config.forceEnroll;
2020
+ }
2021
+ if (insecure) {
2022
+ ssoConfig.insecure = insecure;
2023
+ }
2024
+ return new SsoAuth(ssoConfig);
2025
+ }
2026
+ default: {
2027
+ const _exhaustive = config;
2028
+ throw new Error(`Unknown auth type: ${_exhaustive.type}`);
2029
+ }
2030
+ }
2031
+ }
2032
+
2033
+ // src/core/client.ts
1400
2034
  function buildParams(baseParams, clientNum) {
1401
2035
  const params = new URLSearchParams();
1402
2036
  if (baseParams) {
@@ -1421,15 +2055,24 @@ var ADTClientImpl = class {
1421
2055
  requestor;
1422
2056
  agent;
1423
2057
  constructor(config) {
2058
+ const authOptions = {
2059
+ config: config.auth,
2060
+ baseUrl: config.url
2061
+ };
2062
+ if (config.insecure) {
2063
+ authOptions.insecure = config.insecure;
2064
+ }
2065
+ const authStrategy = createAuthStrategy(authOptions);
1424
2066
  this.state = {
1425
2067
  config,
1426
2068
  session: null,
1427
2069
  csrfToken: null,
1428
- cookies: /* @__PURE__ */ new Map()
2070
+ cookies: /* @__PURE__ */ new Map(),
2071
+ authStrategy
1429
2072
  };
1430
2073
  this.requestor = { request: this.request.bind(this) };
1431
2074
  if (config.insecure) {
1432
- this.agent = new Agent({
2075
+ this.agent = new Agent2({
1433
2076
  connect: {
1434
2077
  rejectUnauthorized: false,
1435
2078
  checkServerIdentity: () => void 0
@@ -1490,7 +2133,7 @@ var ADTClientImpl = class {
1490
2133
  try {
1491
2134
  debug(`Fetching URL: ${url}`);
1492
2135
  debug(`Insecure mode: ${!!this.agent}`);
1493
- const response = await undiciFetch(url, fetchOptions);
2136
+ const response = await undiciFetch2(url, fetchOptions);
1494
2137
  this.storeCookies(response);
1495
2138
  if (response.status === 403) {
1496
2139
  const text = await response.text();
@@ -1505,7 +2148,7 @@ var ADTClientImpl = class {
1505
2148
  headers["Cookie"] = retryCookieHeader;
1506
2149
  }
1507
2150
  debug(`Retrying with new CSRF token: ${newToken.substring(0, 20)}...`);
1508
- const retryResponse = await undiciFetch(url, { ...fetchOptions, headers });
2151
+ const retryResponse = await undiciFetch2(url, { ...fetchOptions, headers });
1509
2152
  this.storeCookies(retryResponse);
1510
2153
  return ok(retryResponse);
1511
2154
  }
@@ -1541,6 +2184,29 @@ var ADTClientImpl = class {
1541
2184
  }
1542
2185
  // --- Lifecycle ---
1543
2186
  async login() {
2187
+ const { authStrategy } = this.state;
2188
+ if (authStrategy.performLogin) {
2189
+ const [, loginErr] = await authStrategy.performLogin(fetch);
2190
+ if (loginErr) {
2191
+ return err(loginErr);
2192
+ }
2193
+ }
2194
+ if (authStrategy.type === "sso" && authStrategy.getCertificates) {
2195
+ const certs = authStrategy.getCertificates();
2196
+ if (certs) {
2197
+ this.agent = new Agent2({
2198
+ connect: {
2199
+ cert: certs.fullChain,
2200
+ key: certs.privateKey,
2201
+ rejectUnauthorized: !this.state.config.insecure,
2202
+ ...this.state.config.insecure && {
2203
+ checkServerIdentity: () => void 0
2204
+ }
2205
+ }
2206
+ });
2207
+ debug("Created mTLS agent with SSO certificates");
2208
+ }
2209
+ }
1544
2210
  return login(this.state, this.request.bind(this));
1545
2211
  }
1546
2212
  async logout() {