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.js CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
 
20
30
  // src/index.ts
@@ -252,6 +262,15 @@ function debugError(message, cause) {
252
262
 
253
263
  // src/types/config.ts
254
264
  var import_zod = require("zod");
265
+ var samlFormSelectorsSchema = import_zod.z.object({
266
+ username: import_zod.z.string().min(1),
267
+ password: import_zod.z.string().min(1),
268
+ submit: import_zod.z.string().min(1)
269
+ });
270
+ var samlProviderConfigSchema = import_zod.z.object({
271
+ ignoreHttpsErrors: import_zod.z.boolean(),
272
+ formSelectors: samlFormSelectorsSchema
273
+ });
255
274
  var clientConfigSchema = import_zod.z.object({
256
275
  url: import_zod.z.string().url(),
257
276
  client: import_zod.z.string().min(1).max(3),
@@ -265,10 +284,14 @@ var clientConfigSchema = import_zod.z.object({
265
284
  type: import_zod.z.literal("saml"),
266
285
  username: import_zod.z.string().min(1),
267
286
  password: import_zod.z.string().min(1),
268
- provider: import_zod.z.string().optional()
287
+ providerConfig: samlProviderConfigSchema.optional()
269
288
  }),
270
289
  import_zod.z.object({
271
290
  type: import_zod.z.literal("sso"),
291
+ slsUrl: import_zod.z.string().url(),
292
+ profile: import_zod.z.string().optional(),
293
+ servicePrincipalName: import_zod.z.string().optional(),
294
+ forceEnroll: import_zod.z.boolean().optional(),
272
295
  certificate: import_zod.z.string().optional()
273
296
  })
274
297
  ]),
@@ -276,6 +299,16 @@ var clientConfigSchema = import_zod.z.object({
276
299
  insecure: import_zod.z.boolean().optional()
277
300
  });
278
301
 
302
+ // src/core/session/types.ts
303
+ var DEFAULT_SESSION_CONFIG = {
304
+ sessionTimeout: 10800,
305
+ // 3 hours (Basic/SSO)
306
+ samlSessionTimeout: 1800,
307
+ // 30 minutes (SAML)
308
+ cleanupInterval: 60
309
+ // 1 minute
310
+ };
311
+
279
312
  // src/core/session/login.ts
280
313
  async function fetchCsrfToken(state, request) {
281
314
  const endpoint = state.config.auth.type === "saml" ? "/sap/bc/adt/core/http/sessions" : "/sap/bc/adt/compatibility/graph";
@@ -308,22 +341,43 @@ async function fetchCsrfToken(state, request) {
308
341
  debug(`Stored CSRF token in state: ${state.csrfToken?.substring(0, 20)}...`);
309
342
  return ok(token);
310
343
  }
311
- async function login(state, request) {
312
- if (state.config.auth.type === "saml") {
313
- return err(new Error("SAML authentication not yet implemented"));
344
+ function getSessionTimeout(authType) {
345
+ switch (authType) {
346
+ case "saml":
347
+ return DEFAULT_SESSION_CONFIG.samlSessionTimeout * 1e3;
348
+ case "basic":
349
+ case "sso":
350
+ return DEFAULT_SESSION_CONFIG.sessionTimeout * 1e3;
351
+ default: {
352
+ const _exhaustive = authType;
353
+ return DEFAULT_SESSION_CONFIG.sessionTimeout * 1e3;
354
+ }
314
355
  }
315
- if (state.config.auth.type === "sso") {
316
- return err(new Error("SSO authentication not yet implemented"));
356
+ }
357
+ function extractUsername(auth) {
358
+ switch (auth.type) {
359
+ case "basic":
360
+ case "saml":
361
+ return auth.username;
362
+ case "sso":
363
+ return process.env["USERNAME"] ?? process.env["USER"] ?? "SSO_USER";
364
+ default: {
365
+ const _exhaustive = auth;
366
+ return "";
367
+ }
317
368
  }
369
+ }
370
+ async function login(state, request) {
318
371
  const [token, tokenErr] = await fetchCsrfToken(state, request);
319
372
  if (tokenErr) {
320
373
  return err(new Error(`Login failed: ${tokenErr.message}`));
321
374
  }
322
- const username = state.config.auth.type === "basic" ? state.config.auth.username : "";
375
+ const username = extractUsername(state.config.auth);
376
+ const timeout = getSessionTimeout(state.config.auth.type);
323
377
  const session = {
324
378
  sessionId: token,
325
379
  username,
326
- expiresAt: Date.now() + 8 * 60 * 60 * 1e3
380
+ expiresAt: Date.now() + timeout
327
381
  };
328
382
  state.session = session;
329
383
  return ok(session);
@@ -1424,7 +1478,590 @@ async function gitDiff(client, object) {
1424
1478
  }
1425
1479
 
1426
1480
  // src/core/client.ts
1481
+ var import_undici2 = require("undici");
1482
+
1483
+ // src/core/auth/basic/basic.ts
1484
+ var BasicAuth = class {
1485
+ type = "basic";
1486
+ authHeader;
1487
+ /**
1488
+ * Create a Basic Auth strategy
1489
+ * @param username - SAP username
1490
+ * @param password - SAP password
1491
+ */
1492
+ constructor(username, password) {
1493
+ if (!username || !password) {
1494
+ throw new Error("BasicAuth requires both username and password");
1495
+ }
1496
+ const credentials = `${username}:${password}`;
1497
+ const encoded = btoa(credentials);
1498
+ this.authHeader = `Basic ${encoded}`;
1499
+ }
1500
+ /**
1501
+ * Get Authorization header with Basic credentials
1502
+ * @returns Headers object with Authorization field
1503
+ */
1504
+ getAuthHeaders() {
1505
+ return {
1506
+ Authorization: this.authHeader
1507
+ };
1508
+ }
1509
+ };
1510
+
1511
+ // src/core/auth/sso/slsClient.ts
1427
1512
  var import_undici = require("undici");
1513
+
1514
+ // src/core/auth/sso/types.ts
1515
+ var SLS_DEFAULTS = {
1516
+ PROFILE: "SAPSSO_P",
1517
+ LOGIN_ENDPOINT: "/SecureLoginServer/slc3/doLogin",
1518
+ CERTIFICATE_ENDPOINT: "/SecureLoginServer/slc2/getCertificate",
1519
+ KEY_SIZE: 2048
1520
+ };
1521
+ var CERTIFICATE_STORAGE = {
1522
+ BASE_DIR: "./certificates/sso",
1523
+ FULL_CHAIN_SUFFIX: "_full_chain.pem",
1524
+ KEY_SUFFIX: "_key.pem"
1525
+ };
1526
+
1527
+ // src/core/auth/sso/kerberos.ts
1528
+ async function loadKerberosModule() {
1529
+ try {
1530
+ const kerberosModule = require("kerberos");
1531
+ return ok(kerberosModule);
1532
+ } catch {
1533
+ return err(new Error(
1534
+ "kerberos package is not installed. Install it with: npm install kerberos"
1535
+ ));
1536
+ }
1537
+ }
1538
+ async function getSpnegoToken(servicePrincipalName) {
1539
+ const [kerberos, loadErr] = await loadKerberosModule();
1540
+ if (loadErr) return err(loadErr);
1541
+ try {
1542
+ const client = await kerberos.initializeClient(servicePrincipalName);
1543
+ const token = await client.step("");
1544
+ if (!token) {
1545
+ return err(new Error("Failed to generate SPNEGO token: empty response"));
1546
+ }
1547
+ return ok(token);
1548
+ } catch (error) {
1549
+ const message = error instanceof Error ? error.message : String(error);
1550
+ return err(new Error(`Kerberos authentication failed: ${message}`));
1551
+ }
1552
+ }
1553
+ function extractSpnFromUrl(slsUrl) {
1554
+ const url = new URL(slsUrl);
1555
+ return `HTTP/${url.hostname}`;
1556
+ }
1557
+
1558
+ // src/core/auth/sso/certificate.ts
1559
+ var import_node_forge = __toESM(require("node-forge"));
1560
+ function generateKeypair(keySize = SLS_DEFAULTS.KEY_SIZE) {
1561
+ const keypair = import_node_forge.default.pki.rsa.generateKeyPair({ bits: keySize, e: 65537 });
1562
+ const privateKeyPem = import_node_forge.default.pki.privateKeyToPem(keypair.privateKey);
1563
+ return {
1564
+ privateKeyPem,
1565
+ privateKey: keypair.privateKey,
1566
+ publicKey: keypair.publicKey
1567
+ };
1568
+ }
1569
+ function createCsr(keypair, username) {
1570
+ const csr = import_node_forge.default.pki.createCertificationRequest();
1571
+ csr.publicKey = keypair.publicKey;
1572
+ csr.setSubject([{
1573
+ name: "commonName",
1574
+ value: username
1575
+ }]);
1576
+ csr.setAttributes([{
1577
+ name: "extensionRequest",
1578
+ extensions: [
1579
+ {
1580
+ name: "keyUsage",
1581
+ digitalSignature: true,
1582
+ keyEncipherment: true
1583
+ },
1584
+ {
1585
+ name: "extKeyUsage",
1586
+ clientAuth: true
1587
+ }
1588
+ ]
1589
+ }]);
1590
+ csr.sign(keypair.privateKey, import_node_forge.default.md.sha256.create());
1591
+ const csrAsn1 = import_node_forge.default.pki.certificationRequestToAsn1(csr);
1592
+ const csrDer = import_node_forge.default.asn1.toDer(csrAsn1);
1593
+ return Buffer.from(csrDer.getBytes(), "binary");
1594
+ }
1595
+ function getCurrentUsername() {
1596
+ return process.env["USERNAME"] ?? process.env["USER"] ?? "unknown";
1597
+ }
1598
+
1599
+ // src/core/auth/sso/pkcs7.ts
1600
+ var import_node_forge2 = __toESM(require("node-forge"));
1601
+ function parsePkcs7Certificates(data) {
1602
+ try {
1603
+ const dataString = data.toString("utf-8").replace(/\r?\n/g, "").trim();
1604
+ const derBytes = import_node_forge2.default.util.decode64(dataString);
1605
+ const p7Asn1 = import_node_forge2.default.asn1.fromDer(derBytes);
1606
+ const p7 = import_node_forge2.default.pkcs7.messageFromAsn1(p7Asn1);
1607
+ if (!("certificates" in p7) || !p7.certificates || p7.certificates.length === 0) {
1608
+ return err(new Error("No certificates found in PKCS#7 structure"));
1609
+ }
1610
+ const certificates = p7.certificates;
1611
+ const clientCert = certificates[0];
1612
+ const caCerts = certificates.slice(1);
1613
+ if (!clientCert) {
1614
+ return err(new Error("No client certificate found in PKCS#7 structure"));
1615
+ }
1616
+ const clientCertPem = import_node_forge2.default.pki.certificateToPem(clientCert);
1617
+ const caChainPem = caCerts.map((cert) => import_node_forge2.default.pki.certificateToPem(cert)).join("");
1618
+ return ok({
1619
+ clientCert: clientCertPem,
1620
+ caChain: caChainPem,
1621
+ fullChain: clientCertPem + caChainPem
1622
+ });
1623
+ } catch (error) {
1624
+ const message = error instanceof Error ? error.message : String(error);
1625
+ return err(new Error(`Failed to parse PKCS#7 certificates: ${message}`));
1626
+ }
1627
+ }
1628
+
1629
+ // src/core/auth/sso/slsClient.ts
1630
+ async function enrollCertificate(options) {
1631
+ const { config, insecure = false } = options;
1632
+ const profile = config.profile ?? SLS_DEFAULTS.PROFILE;
1633
+ const agent = insecure ? new import_undici.Agent({ connect: { rejectUnauthorized: false } }) : void 0;
1634
+ const [authResponse, authErr] = await authenticateToSls(config, profile, agent);
1635
+ if (authErr) return err(authErr);
1636
+ const keySize = authResponse.clientConfig.keySize ?? SLS_DEFAULTS.KEY_SIZE;
1637
+ const keypair = generateKeypair(keySize);
1638
+ const username = getCurrentUsername();
1639
+ const csrDer = createCsr(keypair, username);
1640
+ const [certData, certErr] = await requestCertificate(config, profile, csrDer, agent);
1641
+ if (certErr) return err(certErr);
1642
+ const [certs, parseErr] = parsePkcs7Certificates(certData);
1643
+ if (parseErr) return err(parseErr);
1644
+ return ok({
1645
+ fullChain: certs.fullChain,
1646
+ privateKey: keypair.privateKeyPem
1647
+ });
1648
+ }
1649
+ async function authenticateToSls(config, profile, agent) {
1650
+ const spn = config.servicePrincipalName ?? extractSpnFromUrl(config.slsUrl);
1651
+ const [token, tokenErr] = await getSpnegoToken(spn);
1652
+ if (tokenErr) return err(tokenErr);
1653
+ const authUrl = `${config.slsUrl}${SLS_DEFAULTS.LOGIN_ENDPOINT}?profile=${profile}`;
1654
+ try {
1655
+ const fetchOptions = {
1656
+ method: "POST",
1657
+ headers: {
1658
+ "Authorization": `Negotiate ${token}`,
1659
+ "Accept": "*/*"
1660
+ }
1661
+ };
1662
+ if (agent) {
1663
+ fetchOptions.dispatcher = agent;
1664
+ }
1665
+ const response = await (0, import_undici.fetch)(authUrl, fetchOptions);
1666
+ if (!response.ok) {
1667
+ const text = await response.text();
1668
+ return err(new Error(`SLS authentication failed: ${response.status} - ${text}`));
1669
+ }
1670
+ const authResponse = await response.json();
1671
+ return ok(authResponse);
1672
+ } catch (error) {
1673
+ const message = error instanceof Error ? error.message : String(error);
1674
+ return err(new Error(`SLS authentication request failed: ${message}`));
1675
+ }
1676
+ }
1677
+ async function requestCertificate(config, profile, csrDer, agent) {
1678
+ const certUrl = `${config.slsUrl}${SLS_DEFAULTS.CERTIFICATE_ENDPOINT}?profile=${profile}`;
1679
+ try {
1680
+ const fetchOptions = {
1681
+ method: "POST",
1682
+ headers: {
1683
+ "Content-Type": "application/pkcs10",
1684
+ "Content-Length": String(csrDer.length),
1685
+ "Accept": "*/*"
1686
+ },
1687
+ body: csrDer
1688
+ };
1689
+ if (agent) {
1690
+ fetchOptions.dispatcher = agent;
1691
+ }
1692
+ const response = await (0, import_undici.fetch)(certUrl, fetchOptions);
1693
+ if (!response.ok) {
1694
+ const text = await response.text();
1695
+ return err(new Error(`Certificate request failed: ${response.status} - ${text}`));
1696
+ }
1697
+ const buffer = await response.arrayBuffer();
1698
+ return ok(Buffer.from(buffer));
1699
+ } catch (error) {
1700
+ const message = error instanceof Error ? error.message : String(error);
1701
+ return err(new Error(`Certificate request failed: ${message}`));
1702
+ }
1703
+ }
1704
+
1705
+ // src/core/auth/sso/storage.ts
1706
+ var import_promises = require("fs/promises");
1707
+ var import_node_path = require("path");
1708
+ function getCertificatePaths(username) {
1709
+ const user = username ?? getCurrentUsername();
1710
+ return {
1711
+ fullChainPath: (0, import_node_path.join)(CERTIFICATE_STORAGE.BASE_DIR, `${user}${CERTIFICATE_STORAGE.FULL_CHAIN_SUFFIX}`),
1712
+ keyPath: (0, import_node_path.join)(CERTIFICATE_STORAGE.BASE_DIR, `${user}${CERTIFICATE_STORAGE.KEY_SUFFIX}`)
1713
+ };
1714
+ }
1715
+ async function saveCertificates(material, username) {
1716
+ const paths = getCertificatePaths(username);
1717
+ try {
1718
+ await (0, import_promises.mkdir)(CERTIFICATE_STORAGE.BASE_DIR, { recursive: true });
1719
+ await (0, import_promises.writeFile)(paths.fullChainPath, material.fullChain, "utf-8");
1720
+ await (0, import_promises.writeFile)(paths.keyPath, material.privateKey, { encoding: "utf-8", mode: 384 });
1721
+ return ok(paths);
1722
+ } catch (error) {
1723
+ const message = error instanceof Error ? error.message : String(error);
1724
+ return err(new Error(`Failed to save certificates: ${message}`));
1725
+ }
1726
+ }
1727
+ async function loadCertificates(username) {
1728
+ const paths = getCertificatePaths(username);
1729
+ try {
1730
+ const [fullChain, privateKey] = await Promise.all([
1731
+ (0, import_promises.readFile)(paths.fullChainPath, "utf-8"),
1732
+ (0, import_promises.readFile)(paths.keyPath, "utf-8")
1733
+ ]);
1734
+ return ok({ fullChain, privateKey });
1735
+ } catch (error) {
1736
+ const message = error instanceof Error ? error.message : String(error);
1737
+ return err(new Error(`Failed to load certificates: ${message}`));
1738
+ }
1739
+ }
1740
+ async function certificatesExist(username) {
1741
+ const paths = getCertificatePaths(username);
1742
+ try {
1743
+ await Promise.all([
1744
+ (0, import_promises.stat)(paths.fullChainPath),
1745
+ (0, import_promises.stat)(paths.keyPath)
1746
+ ]);
1747
+ return true;
1748
+ } catch {
1749
+ return false;
1750
+ }
1751
+ }
1752
+ function isCertificateExpired(certPem, bufferDays = 1) {
1753
+ try {
1754
+ const forge3 = require("node-forge");
1755
+ const cert = forge3.pki.certificateFromPem(certPem);
1756
+ const notAfter = cert.validity.notAfter;
1757
+ const bufferMs = bufferDays * 24 * 60 * 60 * 1e3;
1758
+ const expiryThreshold = new Date(Date.now() + bufferMs);
1759
+ return ok(notAfter <= expiryThreshold);
1760
+ } catch (error) {
1761
+ const message = error instanceof Error ? error.message : String(error);
1762
+ return err(new Error(`Failed to check certificate expiry: ${message}`));
1763
+ }
1764
+ }
1765
+
1766
+ // src/core/auth/sso/sso.ts
1767
+ var SsoAuth = class {
1768
+ type = "sso";
1769
+ config;
1770
+ certificates = null;
1771
+ /**
1772
+ * Create an SSO Auth strategy
1773
+ *
1774
+ * @param config - SSO authentication configuration
1775
+ */
1776
+ constructor(config) {
1777
+ if (!config.slsUrl) {
1778
+ throw new Error("SsoAuth requires slsUrl");
1779
+ }
1780
+ this.config = config;
1781
+ }
1782
+ /**
1783
+ * Get auth headers for SSO
1784
+ *
1785
+ * SSO uses mTLS for authentication, not headers.
1786
+ * Returns empty headers - the mTLS agent handles auth.
1787
+ */
1788
+ getAuthHeaders() {
1789
+ return {};
1790
+ }
1791
+ /**
1792
+ * Get mTLS certificates
1793
+ *
1794
+ * Returns the certificate material after successful login.
1795
+ * Used by ADT client to create an mTLS agent.
1796
+ *
1797
+ * @returns Certificate material or null if not enrolled
1798
+ */
1799
+ getCertificates() {
1800
+ return this.certificates;
1801
+ }
1802
+ /**
1803
+ * Perform SSO login via certificate enrollment
1804
+ *
1805
+ * Checks for existing valid certificates and enrolls new ones if needed.
1806
+ *
1807
+ * @param _fetchFn - Unused, kept for interface compatibility
1808
+ * @returns Success/error tuple
1809
+ */
1810
+ async performLogin(_fetchFn) {
1811
+ if (!this.config.forceEnroll) {
1812
+ const [loadResult, loadErr] = await this.tryLoadExistingCertificates();
1813
+ if (!loadErr && loadResult) {
1814
+ this.certificates = loadResult;
1815
+ return ok(void 0);
1816
+ }
1817
+ }
1818
+ const slsConfig = {
1819
+ slsUrl: this.config.slsUrl
1820
+ };
1821
+ if (this.config.profile) {
1822
+ slsConfig.profile = this.config.profile;
1823
+ }
1824
+ if (this.config.servicePrincipalName) {
1825
+ slsConfig.servicePrincipalName = this.config.servicePrincipalName;
1826
+ }
1827
+ const [material, enrollErr] = await enrollCertificate({
1828
+ config: slsConfig,
1829
+ insecure: this.config.insecure ?? false
1830
+ });
1831
+ if (enrollErr) {
1832
+ return err(enrollErr);
1833
+ }
1834
+ if (!this.config.returnContents) {
1835
+ const [, saveErr] = await saveCertificates(material);
1836
+ if (saveErr) {
1837
+ return err(saveErr);
1838
+ }
1839
+ }
1840
+ this.certificates = material;
1841
+ return ok(void 0);
1842
+ }
1843
+ /**
1844
+ * Try to load and validate existing certificates
1845
+ */
1846
+ async tryLoadExistingCertificates() {
1847
+ const exists = await certificatesExist();
1848
+ if (!exists) {
1849
+ return err(new Error("No existing certificates found"));
1850
+ }
1851
+ const [material, loadErr] = await loadCertificates();
1852
+ if (loadErr) {
1853
+ return err(loadErr);
1854
+ }
1855
+ const [isExpired, expiryErr] = isCertificateExpired(material.fullChain);
1856
+ if (expiryErr) {
1857
+ return err(expiryErr);
1858
+ }
1859
+ if (isExpired) {
1860
+ return err(new Error("Certificate is expired or expiring soon"));
1861
+ }
1862
+ return ok(material);
1863
+ }
1864
+ };
1865
+
1866
+ // src/core/auth/saml/types.ts
1867
+ var DEFAULT_FORM_SELECTORS = {
1868
+ username: "#j_username",
1869
+ password: "#j_password",
1870
+ submit: "#logOnFormSubmit"
1871
+ };
1872
+ var DEFAULT_PROVIDER_CONFIG = {
1873
+ ignoreHttpsErrors: false,
1874
+ formSelectors: DEFAULT_FORM_SELECTORS
1875
+ };
1876
+
1877
+ // src/core/auth/saml/browser.ts
1878
+ var TIMEOUTS = {
1879
+ PAGE_LOAD: 6e4,
1880
+ FORM_SELECTOR: 1e4
1881
+ };
1882
+ async function performBrowserLogin(options) {
1883
+ const { baseUrl, credentials, headless = true } = options;
1884
+ const config = options.providerConfig ?? DEFAULT_PROVIDER_CONFIG;
1885
+ let playwright;
1886
+ try {
1887
+ playwright = await import("playwright");
1888
+ } catch {
1889
+ return err(
1890
+ new Error(
1891
+ "Playwright is required for SAML authentication but is not installed. Install it with: npm install playwright"
1892
+ )
1893
+ );
1894
+ }
1895
+ const browserArgs = config.ignoreHttpsErrors ? ["--ignore-certificate-errors", "--disable-web-security"] : [];
1896
+ let browser;
1897
+ try {
1898
+ browser = await playwright.chromium.launch({
1899
+ headless,
1900
+ args: browserArgs
1901
+ });
1902
+ } catch (launchError) {
1903
+ return err(
1904
+ new Error(
1905
+ `Failed to launch browser: ${launchError instanceof Error ? launchError.message : String(launchError)}`
1906
+ )
1907
+ );
1908
+ }
1909
+ try {
1910
+ const context = await browser.newContext({
1911
+ ignoreHTTPSErrors: config.ignoreHttpsErrors
1912
+ });
1913
+ const page = await context.newPage();
1914
+ const loginUrl = `${baseUrl}/sap/bc/adt/compatibility/graph`;
1915
+ try {
1916
+ await page.goto(loginUrl, {
1917
+ timeout: TIMEOUTS.PAGE_LOAD,
1918
+ waitUntil: "domcontentloaded"
1919
+ });
1920
+ } catch {
1921
+ return err(new Error("Failed to load login page. Please check if the server is online."));
1922
+ }
1923
+ try {
1924
+ await page.waitForSelector(config.formSelectors.username, {
1925
+ timeout: TIMEOUTS.FORM_SELECTOR
1926
+ });
1927
+ } catch {
1928
+ return err(new Error("Login form not found. The page may have changed or loaded incorrectly."));
1929
+ }
1930
+ await page.fill(config.formSelectors.username, credentials.username);
1931
+ await page.fill(config.formSelectors.password, credentials.password);
1932
+ await page.click(config.formSelectors.submit);
1933
+ await page.waitForLoadState("networkidle");
1934
+ const cookies = await context.cookies();
1935
+ return ok(cookies);
1936
+ } finally {
1937
+ await browser.close();
1938
+ }
1939
+ }
1940
+
1941
+ // src/core/auth/saml/cookies.ts
1942
+ function toAuthCookies(playwrightCookies) {
1943
+ return playwrightCookies.map((cookie) => ({
1944
+ name: cookie.name,
1945
+ value: cookie.value,
1946
+ domain: cookie.domain,
1947
+ path: cookie.path
1948
+ }));
1949
+ }
1950
+ function formatCookieHeader(cookies) {
1951
+ return cookies.map((c) => `${c.name}=${c.value}`).join("; ");
1952
+ }
1953
+
1954
+ // src/core/auth/saml/saml.ts
1955
+ var SamlAuth = class {
1956
+ type = "saml";
1957
+ cookies = [];
1958
+ config;
1959
+ /**
1960
+ * Create a SAML Auth strategy
1961
+ *
1962
+ * @param config - SAML authentication configuration
1963
+ */
1964
+ constructor(config) {
1965
+ if (!config.username || !config.password) {
1966
+ throw new Error("SamlAuth requires both username and password");
1967
+ }
1968
+ if (!config.baseUrl) {
1969
+ throw new Error("SamlAuth requires baseUrl");
1970
+ }
1971
+ this.config = config;
1972
+ }
1973
+ /**
1974
+ * Get auth headers for SAML
1975
+ *
1976
+ * After successful login, includes Cookie header with session cookies.
1977
+ */
1978
+ getAuthHeaders() {
1979
+ if (this.cookies.length === 0) {
1980
+ return {};
1981
+ }
1982
+ return {
1983
+ Cookie: formatCookieHeader(this.cookies)
1984
+ };
1985
+ }
1986
+ /**
1987
+ * Get authentication cookies
1988
+ *
1989
+ * @returns Array of cookies obtained during SAML login
1990
+ */
1991
+ getCookies() {
1992
+ return this.cookies;
1993
+ }
1994
+ /**
1995
+ * Perform SAML login using headless browser automation
1996
+ *
1997
+ * Launches a Chromium browser, navigates to the SAP login page,
1998
+ * fills in credentials, and extracts session cookies.
1999
+ *
2000
+ * @param _fetchFn - Unused, kept for interface compatibility
2001
+ * @returns Success/error tuple
2002
+ */
2003
+ async performLogin(_fetchFn) {
2004
+ const [playwrightCookies, loginError] = await performBrowserLogin({
2005
+ baseUrl: this.config.baseUrl,
2006
+ credentials: {
2007
+ username: this.config.username,
2008
+ password: this.config.password
2009
+ },
2010
+ ...this.config.providerConfig && { providerConfig: this.config.providerConfig }
2011
+ });
2012
+ if (loginError) {
2013
+ return err(loginError);
2014
+ }
2015
+ this.cookies = toAuthCookies(playwrightCookies);
2016
+ if (this.cookies.length === 0) {
2017
+ return err(new Error("SAML login succeeded but no cookies were returned"));
2018
+ }
2019
+ return ok(void 0);
2020
+ }
2021
+ };
2022
+
2023
+ // src/core/auth/factory.ts
2024
+ function createAuthStrategy(options) {
2025
+ const { config, baseUrl, insecure } = options;
2026
+ switch (config.type) {
2027
+ case "basic":
2028
+ return new BasicAuth(config.username, config.password);
2029
+ case "saml":
2030
+ if (!baseUrl) {
2031
+ throw new Error("SAML authentication requires baseUrl");
2032
+ }
2033
+ return new SamlAuth({
2034
+ username: config.username,
2035
+ password: config.password,
2036
+ baseUrl,
2037
+ ...config.providerConfig && { providerConfig: config.providerConfig }
2038
+ });
2039
+ case "sso": {
2040
+ const ssoConfig = {
2041
+ slsUrl: config.slsUrl
2042
+ };
2043
+ if (config.profile) {
2044
+ ssoConfig.profile = config.profile;
2045
+ }
2046
+ if (config.servicePrincipalName) {
2047
+ ssoConfig.servicePrincipalName = config.servicePrincipalName;
2048
+ }
2049
+ if (config.forceEnroll) {
2050
+ ssoConfig.forceEnroll = config.forceEnroll;
2051
+ }
2052
+ if (insecure) {
2053
+ ssoConfig.insecure = insecure;
2054
+ }
2055
+ return new SsoAuth(ssoConfig);
2056
+ }
2057
+ default: {
2058
+ const _exhaustive = config;
2059
+ throw new Error(`Unknown auth type: ${_exhaustive.type}`);
2060
+ }
2061
+ }
2062
+ }
2063
+
2064
+ // src/core/client.ts
1428
2065
  function buildParams(baseParams, clientNum) {
1429
2066
  const params = new URLSearchParams();
1430
2067
  if (baseParams) {
@@ -1449,15 +2086,24 @@ var ADTClientImpl = class {
1449
2086
  requestor;
1450
2087
  agent;
1451
2088
  constructor(config) {
2089
+ const authOptions = {
2090
+ config: config.auth,
2091
+ baseUrl: config.url
2092
+ };
2093
+ if (config.insecure) {
2094
+ authOptions.insecure = config.insecure;
2095
+ }
2096
+ const authStrategy = createAuthStrategy(authOptions);
1452
2097
  this.state = {
1453
2098
  config,
1454
2099
  session: null,
1455
2100
  csrfToken: null,
1456
- cookies: /* @__PURE__ */ new Map()
2101
+ cookies: /* @__PURE__ */ new Map(),
2102
+ authStrategy
1457
2103
  };
1458
2104
  this.requestor = { request: this.request.bind(this) };
1459
2105
  if (config.insecure) {
1460
- this.agent = new import_undici.Agent({
2106
+ this.agent = new import_undici2.Agent({
1461
2107
  connect: {
1462
2108
  rejectUnauthorized: false,
1463
2109
  checkServerIdentity: () => void 0
@@ -1518,7 +2164,7 @@ var ADTClientImpl = class {
1518
2164
  try {
1519
2165
  debug(`Fetching URL: ${url}`);
1520
2166
  debug(`Insecure mode: ${!!this.agent}`);
1521
- const response = await (0, import_undici.fetch)(url, fetchOptions);
2167
+ const response = await (0, import_undici2.fetch)(url, fetchOptions);
1522
2168
  this.storeCookies(response);
1523
2169
  if (response.status === 403) {
1524
2170
  const text = await response.text();
@@ -1533,7 +2179,7 @@ var ADTClientImpl = class {
1533
2179
  headers["Cookie"] = retryCookieHeader;
1534
2180
  }
1535
2181
  debug(`Retrying with new CSRF token: ${newToken.substring(0, 20)}...`);
1536
- const retryResponse = await (0, import_undici.fetch)(url, { ...fetchOptions, headers });
2182
+ const retryResponse = await (0, import_undici2.fetch)(url, { ...fetchOptions, headers });
1537
2183
  this.storeCookies(retryResponse);
1538
2184
  return ok(retryResponse);
1539
2185
  }
@@ -1569,6 +2215,29 @@ var ADTClientImpl = class {
1569
2215
  }
1570
2216
  // --- Lifecycle ---
1571
2217
  async login() {
2218
+ const { authStrategy } = this.state;
2219
+ if (authStrategy.performLogin) {
2220
+ const [, loginErr] = await authStrategy.performLogin(fetch);
2221
+ if (loginErr) {
2222
+ return err(loginErr);
2223
+ }
2224
+ }
2225
+ if (authStrategy.type === "sso" && authStrategy.getCertificates) {
2226
+ const certs = authStrategy.getCertificates();
2227
+ if (certs) {
2228
+ this.agent = new import_undici2.Agent({
2229
+ connect: {
2230
+ cert: certs.fullChain,
2231
+ key: certs.privateKey,
2232
+ rejectUnauthorized: !this.state.config.insecure,
2233
+ ...this.state.config.insecure && {
2234
+ checkServerIdentity: () => void 0
2235
+ }
2236
+ }
2237
+ });
2238
+ debug("Created mTLS agent with SSO certificates");
2239
+ }
2240
+ }
1572
2241
  return login(this.state, this.request.bind(this));
1573
2242
  }
1574
2243
  async logout() {