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.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
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
316
|
-
|
|
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
|
|
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() +
|
|
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
|
|
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,
|
|
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,
|
|
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() {
|