catalyst-relay 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,3 +1,10 @@
1
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
2
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
3
+ }) : x)(function(x) {
4
+ if (typeof require !== "undefined") return require.apply(this, arguments);
5
+ throw Error('Dynamic require of "' + x + '" is not supported');
6
+ });
7
+
1
8
  // src/types/result.ts
2
9
  function ok(value) {
3
10
  return [value, null];
@@ -87,93 +94,6 @@ function dictToAbapXml(data, root = "DATA") {
87
94
  </asx:abap>`;
88
95
  }
89
96
 
90
- // src/core/utils/sql.ts
91
- var SqlValidationError = class extends Error {
92
- constructor(message) {
93
- super(message);
94
- this.name = "SqlValidationError";
95
- }
96
- };
97
- function validateSqlInput(input, maxLength = 1e4) {
98
- if (typeof input !== "string") {
99
- return err(new SqlValidationError("Input must be a string"));
100
- }
101
- if (input.length > maxLength) {
102
- return err(new SqlValidationError(`Input exceeds maximum length of ${maxLength}`));
103
- }
104
- const dangerousPatterns = [
105
- {
106
- pattern: /\b(DROP|DELETE|INSERT|UPDATE|ALTER|CREATE|TRUNCATE)\s+/i,
107
- description: "DDL/DML keywords (DROP, DELETE, INSERT, etc.)"
108
- },
109
- {
110
- pattern: /;[\s]*\w/,
111
- description: "Statement termination followed by another statement"
112
- },
113
- {
114
- pattern: /--[\s]*\w/,
115
- description: "SQL comments with content"
116
- },
117
- {
118
- pattern: /\/\*.*?\*\//,
119
- description: "Block comments"
120
- },
121
- {
122
- pattern: /\bEXEC(UTE)?\s*\(/i,
123
- description: "Procedure execution"
124
- },
125
- {
126
- pattern: /\bSP_\w+/i,
127
- description: "Stored procedures"
128
- },
129
- {
130
- pattern: /\bXP_\w+/i,
131
- description: "Extended stored procedures"
132
- },
133
- {
134
- pattern: /\bUNION\s+(ALL\s+)?SELECT/i,
135
- description: "Union-based injection"
136
- },
137
- {
138
- pattern: /@@\w+/,
139
- description: "System variables"
140
- },
141
- {
142
- pattern: /\bDECLARE\s+@/i,
143
- description: "Variable declarations"
144
- },
145
- {
146
- pattern: /\bCAST\s*\(/i,
147
- description: "Type casting"
148
- },
149
- {
150
- pattern: /\bCONVERT\s*\(/i,
151
- description: "Type conversion"
152
- }
153
- ];
154
- for (const { pattern, description } of dangerousPatterns) {
155
- if (pattern.test(input)) {
156
- return err(new SqlValidationError(
157
- `Input contains potentially dangerous SQL pattern: ${description}`
158
- ));
159
- }
160
- }
161
- const specialCharMatches = input.match(/[;'"\\]/g);
162
- const specialCharCount = specialCharMatches ? specialCharMatches.length : 0;
163
- if (specialCharCount > 5) {
164
- return err(new SqlValidationError("Input contains excessive special characters"));
165
- }
166
- const singleQuoteCount = (input.match(/'/g) || []).length;
167
- if (singleQuoteCount % 2 !== 0) {
168
- return err(new SqlValidationError("Unbalanced single quotes detected"));
169
- }
170
- const doubleQuoteCount = (input.match(/"/g) || []).length;
171
- if (doubleQuoteCount % 2 !== 0) {
172
- return err(new SqlValidationError("Unbalanced double quotes detected"));
173
- }
174
- return ok(true);
175
- }
176
-
177
97
  // src/core/utils/csrf.ts
178
98
  var FETCH_CSRF_TOKEN = "fetch";
179
99
  var CSRF_TOKEN_HEADER = "x-csrf-token";
@@ -224,6 +144,15 @@ function debugError(message, cause) {
224
144
 
225
145
  // src/types/config.ts
226
146
  import { z } from "zod";
147
+ var samlFormSelectorsSchema = z.object({
148
+ username: z.string().min(1),
149
+ password: z.string().min(1),
150
+ submit: z.string().min(1)
151
+ });
152
+ var samlProviderConfigSchema = z.object({
153
+ ignoreHttpsErrors: z.boolean(),
154
+ formSelectors: samlFormSelectorsSchema
155
+ });
227
156
  var clientConfigSchema = z.object({
228
157
  url: z.string().url(),
229
158
  client: z.string().min(1).max(3),
@@ -237,10 +166,14 @@ var clientConfigSchema = z.object({
237
166
  type: z.literal("saml"),
238
167
  username: z.string().min(1),
239
168
  password: z.string().min(1),
240
- provider: z.string().optional()
169
+ providerConfig: samlProviderConfigSchema.optional()
241
170
  }),
242
171
  z.object({
243
172
  type: z.literal("sso"),
173
+ slsUrl: z.string().url(),
174
+ profile: z.string().optional(),
175
+ servicePrincipalName: z.string().optional(),
176
+ forceEnroll: z.boolean().optional(),
244
177
  certificate: z.string().optional()
245
178
  })
246
179
  ]),
@@ -248,6 +181,16 @@ var clientConfigSchema = z.object({
248
181
  insecure: z.boolean().optional()
249
182
  });
250
183
 
184
+ // src/core/session/types.ts
185
+ var DEFAULT_SESSION_CONFIG = {
186
+ sessionTimeout: 10800,
187
+ // 3 hours (Basic/SSO)
188
+ samlSessionTimeout: 1800,
189
+ // 30 minutes (SAML)
190
+ cleanupInterval: 60
191
+ // 1 minute
192
+ };
193
+
251
194
  // src/core/session/login.ts
252
195
  async function fetchCsrfToken(state, request) {
253
196
  const endpoint = state.config.auth.type === "saml" ? "/sap/bc/adt/core/http/sessions" : "/sap/bc/adt/compatibility/graph";
@@ -280,22 +223,43 @@ async function fetchCsrfToken(state, request) {
280
223
  debug(`Stored CSRF token in state: ${state.csrfToken?.substring(0, 20)}...`);
281
224
  return ok(token);
282
225
  }
283
- async function login(state, request) {
284
- if (state.config.auth.type === "saml") {
285
- return err(new Error("SAML authentication not yet implemented"));
226
+ function getSessionTimeout(authType) {
227
+ switch (authType) {
228
+ case "saml":
229
+ return DEFAULT_SESSION_CONFIG.samlSessionTimeout * 1e3;
230
+ case "basic":
231
+ case "sso":
232
+ return DEFAULT_SESSION_CONFIG.sessionTimeout * 1e3;
233
+ default: {
234
+ const _exhaustive = authType;
235
+ return DEFAULT_SESSION_CONFIG.sessionTimeout * 1e3;
236
+ }
286
237
  }
287
- if (state.config.auth.type === "sso") {
288
- return err(new Error("SSO authentication not yet implemented"));
238
+ }
239
+ function extractUsername(auth) {
240
+ switch (auth.type) {
241
+ case "basic":
242
+ case "saml":
243
+ return auth.username;
244
+ case "sso":
245
+ return process.env["USERNAME"] ?? process.env["USER"] ?? "SSO_USER";
246
+ default: {
247
+ const _exhaustive = auth;
248
+ return "";
249
+ }
289
250
  }
251
+ }
252
+ async function login(state, request) {
290
253
  const [token, tokenErr] = await fetchCsrfToken(state, request);
291
254
  if (tokenErr) {
292
255
  return err(new Error(`Login failed: ${tokenErr.message}`));
293
256
  }
294
- const username = state.config.auth.type === "basic" ? state.config.auth.username : "";
257
+ const username = extractUsername(state.config.auth);
258
+ const timeout = getSessionTimeout(state.config.auth.type);
295
259
  const session = {
296
260
  sessionId: token,
297
261
  username,
298
- expiresAt: Date.now() + 8 * 60 * 60 * 1e3
262
+ expiresAt: Date.now() + timeout
299
263
  };
300
264
  state.session = session;
301
265
  return ok(session);
@@ -876,63 +840,6 @@ function extractTransports(xml) {
876
840
  return ok(transports);
877
841
  }
878
842
 
879
- // src/core/adt/queryBuilder.ts
880
- function buildWhereClauses(filters) {
881
- if (!filters || filters.length === 0) {
882
- return "";
883
- }
884
- const clauses = filters.map((filter) => {
885
- const { column, operator, value } = filter;
886
- switch (operator) {
887
- case "eq":
888
- return `${column} = ${formatValue(value)}`;
889
- case "ne":
890
- return `${column} != ${formatValue(value)}`;
891
- case "gt":
892
- return `${column} > ${formatValue(value)}`;
893
- case "ge":
894
- return `${column} >= ${formatValue(value)}`;
895
- case "lt":
896
- return `${column} < ${formatValue(value)}`;
897
- case "le":
898
- return `${column} <= ${formatValue(value)}`;
899
- case "like":
900
- return `${column} LIKE ${formatValue(value)}`;
901
- case "in":
902
- if (Array.isArray(value)) {
903
- const values = value.map((v) => formatValue(v)).join(", ");
904
- return `${column} IN (${values})`;
905
- }
906
- return `${column} IN (${formatValue(value)})`;
907
- default:
908
- return "";
909
- }
910
- }).filter((c) => c);
911
- if (clauses.length === 0) {
912
- return "";
913
- }
914
- return ` WHERE ${clauses.join(" AND ")}`;
915
- }
916
- function buildOrderByClauses(orderBy) {
917
- if (!orderBy || orderBy.length === 0) {
918
- return "";
919
- }
920
- const clauses = orderBy.map((o) => `${o.column} ${o.direction.toUpperCase()}`);
921
- return ` ORDER BY ${clauses.join(", ")}`;
922
- }
923
- function formatValue(value) {
924
- if (value === null) {
925
- return "NULL";
926
- }
927
- if (typeof value === "string") {
928
- return `'${value.replace(/'/g, "''")}'`;
929
- }
930
- if (typeof value === "boolean") {
931
- return value ? "1" : "0";
932
- }
933
- return String(value);
934
- }
935
-
936
843
  // src/core/adt/previewParser.ts
937
844
  function parseDataPreview(xml, maxRows, isTable) {
938
845
  const [doc, parseErr] = safeParseXml(xml);
@@ -951,10 +858,18 @@ function parseDataPreview(xml, maxRows, isTable) {
951
858
  if (!name || !dataType) continue;
952
859
  columns.push({ name, dataType });
953
860
  }
861
+ const dataSetElements = doc.getElementsByTagNameNS(namespace, "dataSet");
862
+ if (columns.length === 0 && dataSetElements.length > 0) {
863
+ for (let i = 0; i < dataSetElements.length; i++) {
864
+ const dataSet = dataSetElements[i];
865
+ if (!dataSet) continue;
866
+ const name = dataSet.getAttributeNS(namespace, "columnName") || dataSet.getAttribute("columnName") || `column${i}`;
867
+ columns.push({ name, dataType: "unknown" });
868
+ }
869
+ }
954
870
  if (columns.length === 0) {
955
- return err(new Error("No columns found in preview response"));
871
+ return ok({ columns: [], rows: [], totalRows: 0 });
956
872
  }
957
- const dataSetElements = doc.getElementsByTagNameNS(namespace, "dataSet");
958
873
  const columnData = Array.from({ length: columns.length }, () => []);
959
874
  for (let i = 0; i < dataSetElements.length; i++) {
960
875
  const dataSet = dataSetElements[i];
@@ -984,23 +899,16 @@ function parseDataPreview(xml, maxRows, isTable) {
984
899
  return ok(dataFrame);
985
900
  }
986
901
 
987
- // src/core/adt/data.ts
902
+ // src/core/adt/dataPreview.ts
988
903
  async function previewData(client, query) {
989
904
  const extension = query.objectType === "table" ? "astabldt" : "asddls";
990
905
  const config = getConfigByExtension(extension);
991
- if (!config || !config.dpEndpoint || !config.dpParam) {
906
+ if (!config?.dpEndpoint || !config?.dpParam) {
992
907
  return err(new Error(`Data preview not supported for object type: ${query.objectType}`));
993
908
  }
994
909
  const limit = query.limit ?? 100;
995
- const whereClauses = buildWhereClauses(query.filters);
996
- const orderByClauses = buildOrderByClauses(query.orderBy);
997
- const sqlQuery = `select * from ${query.objectName}${whereClauses}${orderByClauses}`;
998
- const [, validationErr] = validateSqlInput(sqlQuery);
999
- if (validationErr) {
1000
- return err(new Error(`SQL validation failed: ${validationErr.message}`));
1001
- }
1002
910
  debug(`Data preview: endpoint=${config.dpEndpoint}, param=${config.dpParam}=${query.objectName}`);
1003
- debug(`SQL: ${sqlQuery}`);
911
+ debug(`SQL: ${query.sqlQuery}`);
1004
912
  const [response, requestErr] = await client.request({
1005
913
  method: "POST",
1006
914
  path: `/sap/bc/adt/datapreview/${config.dpEndpoint}`,
@@ -1012,7 +920,7 @@ async function previewData(client, query) {
1012
920
  "Accept": "application/vnd.sap.adt.datapreview.table.v1+xml",
1013
921
  "Content-Type": "text/plain"
1014
922
  },
1015
- body: sqlQuery
923
+ body: query.sqlQuery
1016
924
  });
1017
925
  if (requestErr) {
1018
926
  return err(requestErr);
@@ -1034,109 +942,41 @@ async function previewData(client, query) {
1034
942
  // src/core/adt/distinct.ts
1035
943
  var MAX_ROW_COUNT = 5e4;
1036
944
  async function getDistinctValues(client, objectName, column, objectType = "view") {
1037
- const extension = objectType === "table" ? "astabldt" : "asddls";
1038
- const config = getConfigByExtension(extension);
1039
- if (!config || !config.dpEndpoint || !config.dpParam) {
1040
- return err(new Error(`Data preview not supported for object type: ${objectType}`));
1041
- }
1042
945
  const columnName = column.toUpperCase();
1043
946
  const sqlQuery = `SELECT ${columnName} AS value, COUNT(*) AS count FROM ${objectName} GROUP BY ${columnName}`;
1044
- const [, validationErr] = validateSqlInput(sqlQuery);
1045
- if (validationErr) {
1046
- return err(new Error(`SQL validation failed: ${validationErr.message}`));
1047
- }
1048
- const [response, requestErr] = await client.request({
1049
- method: "POST",
1050
- path: `/sap/bc/adt/datapreview/${config.dpEndpoint}`,
1051
- params: {
1052
- "rowNumber": MAX_ROW_COUNT,
1053
- [config.dpParam]: objectName
1054
- },
1055
- headers: {
1056
- "Accept": "application/vnd.sap.adt.datapreview.table.v1+xml"
1057
- },
1058
- body: sqlQuery
947
+ const [dataFrame, error] = await previewData(client, {
948
+ objectName,
949
+ objectType,
950
+ sqlQuery,
951
+ limit: MAX_ROW_COUNT
1059
952
  });
1060
- if (requestErr) {
1061
- return err(requestErr);
1062
- }
1063
- if (!response.ok) {
1064
- const text2 = await response.text();
1065
- const errorMsg = extractError(text2);
1066
- return err(new Error(`Distinct values query failed: ${errorMsg}`));
1067
- }
1068
- const text = await response.text();
1069
- const [doc, parseErr] = safeParseXml(text);
1070
- if (parseErr) {
1071
- return err(parseErr);
1072
- }
1073
- const dataSets = doc.getElementsByTagNameNS("http://www.sap.com/adt/dataPreview", "dataSet");
1074
- const values = [];
1075
- for (let i = 0; i < dataSets.length; i++) {
1076
- const dataSet = dataSets[i];
1077
- if (!dataSet) continue;
1078
- const dataElements = dataSet.getElementsByTagNameNS("http://www.sap.com/adt/dataPreview", "data");
1079
- if (dataElements.length < 2) continue;
1080
- const value = dataElements[0]?.textContent ?? "";
1081
- const countText = dataElements[1]?.textContent?.trim() ?? "0";
1082
- values.push({
1083
- value,
1084
- count: parseInt(countText, 10)
1085
- });
1086
- }
1087
- const result = {
1088
- column,
1089
- values
1090
- };
1091
- return ok(result);
953
+ if (error) {
954
+ return err(new Error(`Distinct values query failed: ${error.message}`));
955
+ }
956
+ const values = dataFrame.rows.map((row) => ({
957
+ value: row[0],
958
+ count: parseInt(String(row[1]), 10)
959
+ }));
960
+ return ok({ column, values });
1092
961
  }
1093
962
 
1094
963
  // src/core/adt/count.ts
1095
964
  async function countRows(client, objectName, objectType) {
1096
- const extension = objectType === "table" ? "astabldt" : "asddls";
1097
- const config = getConfigByExtension(extension);
1098
- if (!config || !config.dpEndpoint || !config.dpParam) {
1099
- return err(new Error(`Data preview not supported for object type: ${objectType}`));
1100
- }
1101
965
  const sqlQuery = `SELECT COUNT(*) AS count FROM ${objectName}`;
1102
- const [, validationErr] = validateSqlInput(sqlQuery);
1103
- if (validationErr) {
1104
- return err(new Error(`SQL validation failed: ${validationErr.message}`));
1105
- }
1106
- const [response, requestErr] = await client.request({
1107
- method: "POST",
1108
- path: `/sap/bc/adt/datapreview/${config.dpEndpoint}`,
1109
- params: {
1110
- "rowNumber": 1,
1111
- [config.dpParam]: objectName
1112
- },
1113
- headers: {
1114
- "Accept": "application/vnd.sap.adt.datapreview.table.v1+xml"
1115
- },
1116
- body: sqlQuery
966
+ const [dataFrame, error] = await previewData(client, {
967
+ objectName,
968
+ objectType,
969
+ sqlQuery,
970
+ limit: 1
1117
971
  });
1118
- if (requestErr) {
1119
- return err(requestErr);
1120
- }
1121
- if (!response.ok) {
1122
- const text2 = await response.text();
1123
- const errorMsg = extractError(text2);
1124
- return err(new Error(`Row count query failed: ${errorMsg}`));
1125
- }
1126
- const text = await response.text();
1127
- const [doc, parseErr] = safeParseXml(text);
1128
- if (parseErr) {
1129
- return err(parseErr);
972
+ if (error) {
973
+ return err(new Error(`Row count query failed: ${error.message}`));
1130
974
  }
1131
- const dataElements = doc.getElementsByTagNameNS("http://www.sap.com/adt/dataPreview", "data");
1132
- if (dataElements.length === 0) {
975
+ const countValue = dataFrame.rows[0]?.[0];
976
+ if (countValue === void 0) {
1133
977
  return err(new Error("No count value returned"));
1134
978
  }
1135
- const countText = dataElements[0]?.textContent?.trim();
1136
- if (!countText) {
1137
- return err(new Error("Empty count value returned"));
1138
- }
1139
- const count = parseInt(countText, 10);
979
+ const count = parseInt(String(countValue), 10);
1140
980
  if (isNaN(count)) {
1141
981
  return err(new Error("Invalid count value returned"));
1142
982
  }
@@ -1396,7 +1236,590 @@ async function gitDiff(client, object) {
1396
1236
  }
1397
1237
 
1398
1238
  // src/core/client.ts
1239
+ import { Agent as Agent2, fetch as undiciFetch2 } from "undici";
1240
+
1241
+ // src/core/auth/basic/basic.ts
1242
+ var BasicAuth = class {
1243
+ type = "basic";
1244
+ authHeader;
1245
+ /**
1246
+ * Create a Basic Auth strategy
1247
+ * @param username - SAP username
1248
+ * @param password - SAP password
1249
+ */
1250
+ constructor(username, password) {
1251
+ if (!username || !password) {
1252
+ throw new Error("BasicAuth requires both username and password");
1253
+ }
1254
+ const credentials = `${username}:${password}`;
1255
+ const encoded = btoa(credentials);
1256
+ this.authHeader = `Basic ${encoded}`;
1257
+ }
1258
+ /**
1259
+ * Get Authorization header with Basic credentials
1260
+ * @returns Headers object with Authorization field
1261
+ */
1262
+ getAuthHeaders() {
1263
+ return {
1264
+ Authorization: this.authHeader
1265
+ };
1266
+ }
1267
+ };
1268
+
1269
+ // src/core/auth/sso/slsClient.ts
1399
1270
  import { Agent, fetch as undiciFetch } from "undici";
1271
+
1272
+ // src/core/auth/sso/types.ts
1273
+ var SLS_DEFAULTS = {
1274
+ PROFILE: "SAPSSO_P",
1275
+ LOGIN_ENDPOINT: "/SecureLoginServer/slc3/doLogin",
1276
+ CERTIFICATE_ENDPOINT: "/SecureLoginServer/slc2/getCertificate",
1277
+ KEY_SIZE: 2048
1278
+ };
1279
+ var CERTIFICATE_STORAGE = {
1280
+ BASE_DIR: "./certificates/sso",
1281
+ FULL_CHAIN_SUFFIX: "_full_chain.pem",
1282
+ KEY_SUFFIX: "_key.pem"
1283
+ };
1284
+
1285
+ // src/core/auth/sso/kerberos.ts
1286
+ async function loadKerberosModule() {
1287
+ try {
1288
+ const kerberosModule = __require("kerberos");
1289
+ return ok(kerberosModule);
1290
+ } catch {
1291
+ return err(new Error(
1292
+ "kerberos package is not installed. Install it with: npm install kerberos"
1293
+ ));
1294
+ }
1295
+ }
1296
+ async function getSpnegoToken(servicePrincipalName) {
1297
+ const [kerberos, loadErr] = await loadKerberosModule();
1298
+ if (loadErr) return err(loadErr);
1299
+ try {
1300
+ const client = await kerberos.initializeClient(servicePrincipalName);
1301
+ const token = await client.step("");
1302
+ if (!token) {
1303
+ return err(new Error("Failed to generate SPNEGO token: empty response"));
1304
+ }
1305
+ return ok(token);
1306
+ } catch (error) {
1307
+ const message = error instanceof Error ? error.message : String(error);
1308
+ return err(new Error(`Kerberos authentication failed: ${message}`));
1309
+ }
1310
+ }
1311
+ function extractSpnFromUrl(slsUrl) {
1312
+ const url = new URL(slsUrl);
1313
+ return `HTTP/${url.hostname}`;
1314
+ }
1315
+
1316
+ // src/core/auth/sso/certificate.ts
1317
+ import forge from "node-forge";
1318
+ function generateKeypair(keySize = SLS_DEFAULTS.KEY_SIZE) {
1319
+ const keypair = forge.pki.rsa.generateKeyPair({ bits: keySize, e: 65537 });
1320
+ const privateKeyPem = forge.pki.privateKeyToPem(keypair.privateKey);
1321
+ return {
1322
+ privateKeyPem,
1323
+ privateKey: keypair.privateKey,
1324
+ publicKey: keypair.publicKey
1325
+ };
1326
+ }
1327
+ function createCsr(keypair, username) {
1328
+ const csr = forge.pki.createCertificationRequest();
1329
+ csr.publicKey = keypair.publicKey;
1330
+ csr.setSubject([{
1331
+ name: "commonName",
1332
+ value: username
1333
+ }]);
1334
+ csr.setAttributes([{
1335
+ name: "extensionRequest",
1336
+ extensions: [
1337
+ {
1338
+ name: "keyUsage",
1339
+ digitalSignature: true,
1340
+ keyEncipherment: true
1341
+ },
1342
+ {
1343
+ name: "extKeyUsage",
1344
+ clientAuth: true
1345
+ }
1346
+ ]
1347
+ }]);
1348
+ csr.sign(keypair.privateKey, forge.md.sha256.create());
1349
+ const csrAsn1 = forge.pki.certificationRequestToAsn1(csr);
1350
+ const csrDer = forge.asn1.toDer(csrAsn1);
1351
+ return Buffer.from(csrDer.getBytes(), "binary");
1352
+ }
1353
+ function getCurrentUsername() {
1354
+ return process.env["USERNAME"] ?? process.env["USER"] ?? "unknown";
1355
+ }
1356
+
1357
+ // src/core/auth/sso/pkcs7.ts
1358
+ import forge2 from "node-forge";
1359
+ function parsePkcs7Certificates(data) {
1360
+ try {
1361
+ const dataString = data.toString("utf-8").replace(/\r?\n/g, "").trim();
1362
+ const derBytes = forge2.util.decode64(dataString);
1363
+ const p7Asn1 = forge2.asn1.fromDer(derBytes);
1364
+ const p7 = forge2.pkcs7.messageFromAsn1(p7Asn1);
1365
+ if (!("certificates" in p7) || !p7.certificates || p7.certificates.length === 0) {
1366
+ return err(new Error("No certificates found in PKCS#7 structure"));
1367
+ }
1368
+ const certificates = p7.certificates;
1369
+ const clientCert = certificates[0];
1370
+ const caCerts = certificates.slice(1);
1371
+ if (!clientCert) {
1372
+ return err(new Error("No client certificate found in PKCS#7 structure"));
1373
+ }
1374
+ const clientCertPem = forge2.pki.certificateToPem(clientCert);
1375
+ const caChainPem = caCerts.map((cert) => forge2.pki.certificateToPem(cert)).join("");
1376
+ return ok({
1377
+ clientCert: clientCertPem,
1378
+ caChain: caChainPem,
1379
+ fullChain: clientCertPem + caChainPem
1380
+ });
1381
+ } catch (error) {
1382
+ const message = error instanceof Error ? error.message : String(error);
1383
+ return err(new Error(`Failed to parse PKCS#7 certificates: ${message}`));
1384
+ }
1385
+ }
1386
+
1387
+ // src/core/auth/sso/slsClient.ts
1388
+ async function enrollCertificate(options) {
1389
+ const { config, insecure = false } = options;
1390
+ const profile = config.profile ?? SLS_DEFAULTS.PROFILE;
1391
+ const agent = insecure ? new Agent({ connect: { rejectUnauthorized: false } }) : void 0;
1392
+ const [authResponse, authErr] = await authenticateToSls(config, profile, agent);
1393
+ if (authErr) return err(authErr);
1394
+ const keySize = authResponse.clientConfig.keySize ?? SLS_DEFAULTS.KEY_SIZE;
1395
+ const keypair = generateKeypair(keySize);
1396
+ const username = getCurrentUsername();
1397
+ const csrDer = createCsr(keypair, username);
1398
+ const [certData, certErr] = await requestCertificate(config, profile, csrDer, agent);
1399
+ if (certErr) return err(certErr);
1400
+ const [certs, parseErr] = parsePkcs7Certificates(certData);
1401
+ if (parseErr) return err(parseErr);
1402
+ return ok({
1403
+ fullChain: certs.fullChain,
1404
+ privateKey: keypair.privateKeyPem
1405
+ });
1406
+ }
1407
+ async function authenticateToSls(config, profile, agent) {
1408
+ const spn = config.servicePrincipalName ?? extractSpnFromUrl(config.slsUrl);
1409
+ const [token, tokenErr] = await getSpnegoToken(spn);
1410
+ if (tokenErr) return err(tokenErr);
1411
+ const authUrl = `${config.slsUrl}${SLS_DEFAULTS.LOGIN_ENDPOINT}?profile=${profile}`;
1412
+ try {
1413
+ const fetchOptions = {
1414
+ method: "POST",
1415
+ headers: {
1416
+ "Authorization": `Negotiate ${token}`,
1417
+ "Accept": "*/*"
1418
+ }
1419
+ };
1420
+ if (agent) {
1421
+ fetchOptions.dispatcher = agent;
1422
+ }
1423
+ const response = await undiciFetch(authUrl, fetchOptions);
1424
+ if (!response.ok) {
1425
+ const text = await response.text();
1426
+ return err(new Error(`SLS authentication failed: ${response.status} - ${text}`));
1427
+ }
1428
+ const authResponse = await response.json();
1429
+ return ok(authResponse);
1430
+ } catch (error) {
1431
+ const message = error instanceof Error ? error.message : String(error);
1432
+ return err(new Error(`SLS authentication request failed: ${message}`));
1433
+ }
1434
+ }
1435
+ async function requestCertificate(config, profile, csrDer, agent) {
1436
+ const certUrl = `${config.slsUrl}${SLS_DEFAULTS.CERTIFICATE_ENDPOINT}?profile=${profile}`;
1437
+ try {
1438
+ const fetchOptions = {
1439
+ method: "POST",
1440
+ headers: {
1441
+ "Content-Type": "application/pkcs10",
1442
+ "Content-Length": String(csrDer.length),
1443
+ "Accept": "*/*"
1444
+ },
1445
+ body: csrDer
1446
+ };
1447
+ if (agent) {
1448
+ fetchOptions.dispatcher = agent;
1449
+ }
1450
+ const response = await undiciFetch(certUrl, fetchOptions);
1451
+ if (!response.ok) {
1452
+ const text = await response.text();
1453
+ return err(new Error(`Certificate request failed: ${response.status} - ${text}`));
1454
+ }
1455
+ const buffer = await response.arrayBuffer();
1456
+ return ok(Buffer.from(buffer));
1457
+ } catch (error) {
1458
+ const message = error instanceof Error ? error.message : String(error);
1459
+ return err(new Error(`Certificate request failed: ${message}`));
1460
+ }
1461
+ }
1462
+
1463
+ // src/core/auth/sso/storage.ts
1464
+ import { readFile, writeFile, mkdir, stat } from "fs/promises";
1465
+ import { join } from "path";
1466
+ function getCertificatePaths(username) {
1467
+ const user = username ?? getCurrentUsername();
1468
+ return {
1469
+ fullChainPath: join(CERTIFICATE_STORAGE.BASE_DIR, `${user}${CERTIFICATE_STORAGE.FULL_CHAIN_SUFFIX}`),
1470
+ keyPath: join(CERTIFICATE_STORAGE.BASE_DIR, `${user}${CERTIFICATE_STORAGE.KEY_SUFFIX}`)
1471
+ };
1472
+ }
1473
+ async function saveCertificates(material, username) {
1474
+ const paths = getCertificatePaths(username);
1475
+ try {
1476
+ await mkdir(CERTIFICATE_STORAGE.BASE_DIR, { recursive: true });
1477
+ await writeFile(paths.fullChainPath, material.fullChain, "utf-8");
1478
+ await writeFile(paths.keyPath, material.privateKey, { encoding: "utf-8", mode: 384 });
1479
+ return ok(paths);
1480
+ } catch (error) {
1481
+ const message = error instanceof Error ? error.message : String(error);
1482
+ return err(new Error(`Failed to save certificates: ${message}`));
1483
+ }
1484
+ }
1485
+ async function loadCertificates(username) {
1486
+ const paths = getCertificatePaths(username);
1487
+ try {
1488
+ const [fullChain, privateKey] = await Promise.all([
1489
+ readFile(paths.fullChainPath, "utf-8"),
1490
+ readFile(paths.keyPath, "utf-8")
1491
+ ]);
1492
+ return ok({ fullChain, privateKey });
1493
+ } catch (error) {
1494
+ const message = error instanceof Error ? error.message : String(error);
1495
+ return err(new Error(`Failed to load certificates: ${message}`));
1496
+ }
1497
+ }
1498
+ async function certificatesExist(username) {
1499
+ const paths = getCertificatePaths(username);
1500
+ try {
1501
+ await Promise.all([
1502
+ stat(paths.fullChainPath),
1503
+ stat(paths.keyPath)
1504
+ ]);
1505
+ return true;
1506
+ } catch {
1507
+ return false;
1508
+ }
1509
+ }
1510
+ function isCertificateExpired(certPem, bufferDays = 1) {
1511
+ try {
1512
+ const forge3 = __require("node-forge");
1513
+ const cert = forge3.pki.certificateFromPem(certPem);
1514
+ const notAfter = cert.validity.notAfter;
1515
+ const bufferMs = bufferDays * 24 * 60 * 60 * 1e3;
1516
+ const expiryThreshold = new Date(Date.now() + bufferMs);
1517
+ return ok(notAfter <= expiryThreshold);
1518
+ } catch (error) {
1519
+ const message = error instanceof Error ? error.message : String(error);
1520
+ return err(new Error(`Failed to check certificate expiry: ${message}`));
1521
+ }
1522
+ }
1523
+
1524
+ // src/core/auth/sso/sso.ts
1525
+ var SsoAuth = class {
1526
+ type = "sso";
1527
+ config;
1528
+ certificates = null;
1529
+ /**
1530
+ * Create an SSO Auth strategy
1531
+ *
1532
+ * @param config - SSO authentication configuration
1533
+ */
1534
+ constructor(config) {
1535
+ if (!config.slsUrl) {
1536
+ throw new Error("SsoAuth requires slsUrl");
1537
+ }
1538
+ this.config = config;
1539
+ }
1540
+ /**
1541
+ * Get auth headers for SSO
1542
+ *
1543
+ * SSO uses mTLS for authentication, not headers.
1544
+ * Returns empty headers - the mTLS agent handles auth.
1545
+ */
1546
+ getAuthHeaders() {
1547
+ return {};
1548
+ }
1549
+ /**
1550
+ * Get mTLS certificates
1551
+ *
1552
+ * Returns the certificate material after successful login.
1553
+ * Used by ADT client to create an mTLS agent.
1554
+ *
1555
+ * @returns Certificate material or null if not enrolled
1556
+ */
1557
+ getCertificates() {
1558
+ return this.certificates;
1559
+ }
1560
+ /**
1561
+ * Perform SSO login via certificate enrollment
1562
+ *
1563
+ * Checks for existing valid certificates and enrolls new ones if needed.
1564
+ *
1565
+ * @param _fetchFn - Unused, kept for interface compatibility
1566
+ * @returns Success/error tuple
1567
+ */
1568
+ async performLogin(_fetchFn) {
1569
+ if (!this.config.forceEnroll) {
1570
+ const [loadResult, loadErr] = await this.tryLoadExistingCertificates();
1571
+ if (!loadErr && loadResult) {
1572
+ this.certificates = loadResult;
1573
+ return ok(void 0);
1574
+ }
1575
+ }
1576
+ const slsConfig = {
1577
+ slsUrl: this.config.slsUrl
1578
+ };
1579
+ if (this.config.profile) {
1580
+ slsConfig.profile = this.config.profile;
1581
+ }
1582
+ if (this.config.servicePrincipalName) {
1583
+ slsConfig.servicePrincipalName = this.config.servicePrincipalName;
1584
+ }
1585
+ const [material, enrollErr] = await enrollCertificate({
1586
+ config: slsConfig,
1587
+ insecure: this.config.insecure ?? false
1588
+ });
1589
+ if (enrollErr) {
1590
+ return err(enrollErr);
1591
+ }
1592
+ if (!this.config.returnContents) {
1593
+ const [, saveErr] = await saveCertificates(material);
1594
+ if (saveErr) {
1595
+ return err(saveErr);
1596
+ }
1597
+ }
1598
+ this.certificates = material;
1599
+ return ok(void 0);
1600
+ }
1601
+ /**
1602
+ * Try to load and validate existing certificates
1603
+ */
1604
+ async tryLoadExistingCertificates() {
1605
+ const exists = await certificatesExist();
1606
+ if (!exists) {
1607
+ return err(new Error("No existing certificates found"));
1608
+ }
1609
+ const [material, loadErr] = await loadCertificates();
1610
+ if (loadErr) {
1611
+ return err(loadErr);
1612
+ }
1613
+ const [isExpired, expiryErr] = isCertificateExpired(material.fullChain);
1614
+ if (expiryErr) {
1615
+ return err(expiryErr);
1616
+ }
1617
+ if (isExpired) {
1618
+ return err(new Error("Certificate is expired or expiring soon"));
1619
+ }
1620
+ return ok(material);
1621
+ }
1622
+ };
1623
+
1624
+ // src/core/auth/saml/types.ts
1625
+ var DEFAULT_FORM_SELECTORS = {
1626
+ username: "#j_username",
1627
+ password: "#j_password",
1628
+ submit: "#logOnFormSubmit"
1629
+ };
1630
+ var DEFAULT_PROVIDER_CONFIG = {
1631
+ ignoreHttpsErrors: false,
1632
+ formSelectors: DEFAULT_FORM_SELECTORS
1633
+ };
1634
+
1635
+ // src/core/auth/saml/browser.ts
1636
+ var TIMEOUTS = {
1637
+ PAGE_LOAD: 6e4,
1638
+ FORM_SELECTOR: 1e4
1639
+ };
1640
+ async function performBrowserLogin(options) {
1641
+ const { baseUrl, credentials, headless = true } = options;
1642
+ const config = options.providerConfig ?? DEFAULT_PROVIDER_CONFIG;
1643
+ let playwright;
1644
+ try {
1645
+ playwright = await import("playwright");
1646
+ } catch {
1647
+ return err(
1648
+ new Error(
1649
+ "Playwright is required for SAML authentication but is not installed. Install it with: npm install playwright"
1650
+ )
1651
+ );
1652
+ }
1653
+ const browserArgs = config.ignoreHttpsErrors ? ["--ignore-certificate-errors", "--disable-web-security"] : [];
1654
+ let browser;
1655
+ try {
1656
+ browser = await playwright.chromium.launch({
1657
+ headless,
1658
+ args: browserArgs
1659
+ });
1660
+ } catch (launchError) {
1661
+ return err(
1662
+ new Error(
1663
+ `Failed to launch browser: ${launchError instanceof Error ? launchError.message : String(launchError)}`
1664
+ )
1665
+ );
1666
+ }
1667
+ try {
1668
+ const context = await browser.newContext({
1669
+ ignoreHTTPSErrors: config.ignoreHttpsErrors
1670
+ });
1671
+ const page = await context.newPage();
1672
+ const loginUrl = `${baseUrl}/sap/bc/adt/compatibility/graph`;
1673
+ try {
1674
+ await page.goto(loginUrl, {
1675
+ timeout: TIMEOUTS.PAGE_LOAD,
1676
+ waitUntil: "domcontentloaded"
1677
+ });
1678
+ } catch {
1679
+ return err(new Error("Failed to load login page. Please check if the server is online."));
1680
+ }
1681
+ try {
1682
+ await page.waitForSelector(config.formSelectors.username, {
1683
+ timeout: TIMEOUTS.FORM_SELECTOR
1684
+ });
1685
+ } catch {
1686
+ return err(new Error("Login form not found. The page may have changed or loaded incorrectly."));
1687
+ }
1688
+ await page.fill(config.formSelectors.username, credentials.username);
1689
+ await page.fill(config.formSelectors.password, credentials.password);
1690
+ await page.click(config.formSelectors.submit);
1691
+ await page.waitForLoadState("networkidle");
1692
+ const cookies = await context.cookies();
1693
+ return ok(cookies);
1694
+ } finally {
1695
+ await browser.close();
1696
+ }
1697
+ }
1698
+
1699
+ // src/core/auth/saml/cookies.ts
1700
+ function toAuthCookies(playwrightCookies) {
1701
+ return playwrightCookies.map((cookie) => ({
1702
+ name: cookie.name,
1703
+ value: cookie.value,
1704
+ domain: cookie.domain,
1705
+ path: cookie.path
1706
+ }));
1707
+ }
1708
+ function formatCookieHeader(cookies) {
1709
+ return cookies.map((c) => `${c.name}=${c.value}`).join("; ");
1710
+ }
1711
+
1712
+ // src/core/auth/saml/saml.ts
1713
+ var SamlAuth = class {
1714
+ type = "saml";
1715
+ cookies = [];
1716
+ config;
1717
+ /**
1718
+ * Create a SAML Auth strategy
1719
+ *
1720
+ * @param config - SAML authentication configuration
1721
+ */
1722
+ constructor(config) {
1723
+ if (!config.username || !config.password) {
1724
+ throw new Error("SamlAuth requires both username and password");
1725
+ }
1726
+ if (!config.baseUrl) {
1727
+ throw new Error("SamlAuth requires baseUrl");
1728
+ }
1729
+ this.config = config;
1730
+ }
1731
+ /**
1732
+ * Get auth headers for SAML
1733
+ *
1734
+ * After successful login, includes Cookie header with session cookies.
1735
+ */
1736
+ getAuthHeaders() {
1737
+ if (this.cookies.length === 0) {
1738
+ return {};
1739
+ }
1740
+ return {
1741
+ Cookie: formatCookieHeader(this.cookies)
1742
+ };
1743
+ }
1744
+ /**
1745
+ * Get authentication cookies
1746
+ *
1747
+ * @returns Array of cookies obtained during SAML login
1748
+ */
1749
+ getCookies() {
1750
+ return this.cookies;
1751
+ }
1752
+ /**
1753
+ * Perform SAML login using headless browser automation
1754
+ *
1755
+ * Launches a Chromium browser, navigates to the SAP login page,
1756
+ * fills in credentials, and extracts session cookies.
1757
+ *
1758
+ * @param _fetchFn - Unused, kept for interface compatibility
1759
+ * @returns Success/error tuple
1760
+ */
1761
+ async performLogin(_fetchFn) {
1762
+ const [playwrightCookies, loginError] = await performBrowserLogin({
1763
+ baseUrl: this.config.baseUrl,
1764
+ credentials: {
1765
+ username: this.config.username,
1766
+ password: this.config.password
1767
+ },
1768
+ ...this.config.providerConfig && { providerConfig: this.config.providerConfig }
1769
+ });
1770
+ if (loginError) {
1771
+ return err(loginError);
1772
+ }
1773
+ this.cookies = toAuthCookies(playwrightCookies);
1774
+ if (this.cookies.length === 0) {
1775
+ return err(new Error("SAML login succeeded but no cookies were returned"));
1776
+ }
1777
+ return ok(void 0);
1778
+ }
1779
+ };
1780
+
1781
+ // src/core/auth/factory.ts
1782
+ function createAuthStrategy(options) {
1783
+ const { config, baseUrl, insecure } = options;
1784
+ switch (config.type) {
1785
+ case "basic":
1786
+ return new BasicAuth(config.username, config.password);
1787
+ case "saml":
1788
+ if (!baseUrl) {
1789
+ throw new Error("SAML authentication requires baseUrl");
1790
+ }
1791
+ return new SamlAuth({
1792
+ username: config.username,
1793
+ password: config.password,
1794
+ baseUrl,
1795
+ ...config.providerConfig && { providerConfig: config.providerConfig }
1796
+ });
1797
+ case "sso": {
1798
+ const ssoConfig = {
1799
+ slsUrl: config.slsUrl
1800
+ };
1801
+ if (config.profile) {
1802
+ ssoConfig.profile = config.profile;
1803
+ }
1804
+ if (config.servicePrincipalName) {
1805
+ ssoConfig.servicePrincipalName = config.servicePrincipalName;
1806
+ }
1807
+ if (config.forceEnroll) {
1808
+ ssoConfig.forceEnroll = config.forceEnroll;
1809
+ }
1810
+ if (insecure) {
1811
+ ssoConfig.insecure = insecure;
1812
+ }
1813
+ return new SsoAuth(ssoConfig);
1814
+ }
1815
+ default: {
1816
+ const _exhaustive = config;
1817
+ throw new Error(`Unknown auth type: ${_exhaustive.type}`);
1818
+ }
1819
+ }
1820
+ }
1821
+
1822
+ // src/core/client.ts
1400
1823
  function buildParams(baseParams, clientNum) {
1401
1824
  const params = new URLSearchParams();
1402
1825
  if (baseParams) {
@@ -1421,15 +1844,24 @@ var ADTClientImpl = class {
1421
1844
  requestor;
1422
1845
  agent;
1423
1846
  constructor(config) {
1847
+ const authOptions = {
1848
+ config: config.auth,
1849
+ baseUrl: config.url
1850
+ };
1851
+ if (config.insecure) {
1852
+ authOptions.insecure = config.insecure;
1853
+ }
1854
+ const authStrategy = createAuthStrategy(authOptions);
1424
1855
  this.state = {
1425
1856
  config,
1426
1857
  session: null,
1427
1858
  csrfToken: null,
1428
- cookies: /* @__PURE__ */ new Map()
1859
+ cookies: /* @__PURE__ */ new Map(),
1860
+ authStrategy
1429
1861
  };
1430
1862
  this.requestor = { request: this.request.bind(this) };
1431
1863
  if (config.insecure) {
1432
- this.agent = new Agent({
1864
+ this.agent = new Agent2({
1433
1865
  connect: {
1434
1866
  rejectUnauthorized: false,
1435
1867
  checkServerIdentity: () => void 0
@@ -1490,7 +1922,7 @@ var ADTClientImpl = class {
1490
1922
  try {
1491
1923
  debug(`Fetching URL: ${url}`);
1492
1924
  debug(`Insecure mode: ${!!this.agent}`);
1493
- const response = await undiciFetch(url, fetchOptions);
1925
+ const response = await undiciFetch2(url, fetchOptions);
1494
1926
  this.storeCookies(response);
1495
1927
  if (response.status === 403) {
1496
1928
  const text = await response.text();
@@ -1505,7 +1937,7 @@ var ADTClientImpl = class {
1505
1937
  headers["Cookie"] = retryCookieHeader;
1506
1938
  }
1507
1939
  debug(`Retrying with new CSRF token: ${newToken.substring(0, 20)}...`);
1508
- const retryResponse = await undiciFetch(url, { ...fetchOptions, headers });
1940
+ const retryResponse = await undiciFetch2(url, { ...fetchOptions, headers });
1509
1941
  this.storeCookies(retryResponse);
1510
1942
  return ok(retryResponse);
1511
1943
  }
@@ -1541,6 +1973,29 @@ var ADTClientImpl = class {
1541
1973
  }
1542
1974
  // --- Lifecycle ---
1543
1975
  async login() {
1976
+ const { authStrategy } = this.state;
1977
+ if (authStrategy.performLogin) {
1978
+ const [, loginErr] = await authStrategy.performLogin(fetch);
1979
+ if (loginErr) {
1980
+ return err(loginErr);
1981
+ }
1982
+ }
1983
+ if (authStrategy.type === "sso" && authStrategy.getCertificates) {
1984
+ const certs = authStrategy.getCertificates();
1985
+ if (certs) {
1986
+ this.agent = new Agent2({
1987
+ connect: {
1988
+ cert: certs.fullChain,
1989
+ key: certs.privateKey,
1990
+ rejectUnauthorized: !this.state.config.insecure,
1991
+ ...this.state.config.insecure && {
1992
+ checkServerIdentity: () => void 0
1993
+ }
1994
+ }
1995
+ });
1996
+ debug("Created mTLS agent with SSO certificates");
1997
+ }
1998
+ }
1544
1999
  return login(this.state, this.request.bind(this));
1545
2000
  }
1546
2001
  async logout() {