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/README.md +59 -3
- package/dist/index.d.mts +32 -3
- package/dist/index.d.ts +32 -3
- package/dist/index.js +681 -12
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +678 -12
- package/dist/index.mjs.map +1 -1
- package/package.json +17 -1
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
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
288
|
-
|
|
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
|
|
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() +
|
|
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
|
|
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
|
|
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
|
|
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() {
|