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.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
@@ -115,93 +125,6 @@ function dictToAbapXml(data, root = "DATA") {
115
125
  </asx:abap>`;
116
126
  }
117
127
 
118
- // src/core/utils/sql.ts
119
- var SqlValidationError = class extends Error {
120
- constructor(message) {
121
- super(message);
122
- this.name = "SqlValidationError";
123
- }
124
- };
125
- function validateSqlInput(input, maxLength = 1e4) {
126
- if (typeof input !== "string") {
127
- return err(new SqlValidationError("Input must be a string"));
128
- }
129
- if (input.length > maxLength) {
130
- return err(new SqlValidationError(`Input exceeds maximum length of ${maxLength}`));
131
- }
132
- const dangerousPatterns = [
133
- {
134
- pattern: /\b(DROP|DELETE|INSERT|UPDATE|ALTER|CREATE|TRUNCATE)\s+/i,
135
- description: "DDL/DML keywords (DROP, DELETE, INSERT, etc.)"
136
- },
137
- {
138
- pattern: /;[\s]*\w/,
139
- description: "Statement termination followed by another statement"
140
- },
141
- {
142
- pattern: /--[\s]*\w/,
143
- description: "SQL comments with content"
144
- },
145
- {
146
- pattern: /\/\*.*?\*\//,
147
- description: "Block comments"
148
- },
149
- {
150
- pattern: /\bEXEC(UTE)?\s*\(/i,
151
- description: "Procedure execution"
152
- },
153
- {
154
- pattern: /\bSP_\w+/i,
155
- description: "Stored procedures"
156
- },
157
- {
158
- pattern: /\bXP_\w+/i,
159
- description: "Extended stored procedures"
160
- },
161
- {
162
- pattern: /\bUNION\s+(ALL\s+)?SELECT/i,
163
- description: "Union-based injection"
164
- },
165
- {
166
- pattern: /@@\w+/,
167
- description: "System variables"
168
- },
169
- {
170
- pattern: /\bDECLARE\s+@/i,
171
- description: "Variable declarations"
172
- },
173
- {
174
- pattern: /\bCAST\s*\(/i,
175
- description: "Type casting"
176
- },
177
- {
178
- pattern: /\bCONVERT\s*\(/i,
179
- description: "Type conversion"
180
- }
181
- ];
182
- for (const { pattern, description } of dangerousPatterns) {
183
- if (pattern.test(input)) {
184
- return err(new SqlValidationError(
185
- `Input contains potentially dangerous SQL pattern: ${description}`
186
- ));
187
- }
188
- }
189
- const specialCharMatches = input.match(/[;'"\\]/g);
190
- const specialCharCount = specialCharMatches ? specialCharMatches.length : 0;
191
- if (specialCharCount > 5) {
192
- return err(new SqlValidationError("Input contains excessive special characters"));
193
- }
194
- const singleQuoteCount = (input.match(/'/g) || []).length;
195
- if (singleQuoteCount % 2 !== 0) {
196
- return err(new SqlValidationError("Unbalanced single quotes detected"));
197
- }
198
- const doubleQuoteCount = (input.match(/"/g) || []).length;
199
- if (doubleQuoteCount % 2 !== 0) {
200
- return err(new SqlValidationError("Unbalanced double quotes detected"));
201
- }
202
- return ok(true);
203
- }
204
-
205
128
  // src/core/utils/csrf.ts
206
129
  var FETCH_CSRF_TOKEN = "fetch";
207
130
  var CSRF_TOKEN_HEADER = "x-csrf-token";
@@ -252,6 +175,15 @@ function debugError(message, cause) {
252
175
 
253
176
  // src/types/config.ts
254
177
  var import_zod = require("zod");
178
+ var samlFormSelectorsSchema = import_zod.z.object({
179
+ username: import_zod.z.string().min(1),
180
+ password: import_zod.z.string().min(1),
181
+ submit: import_zod.z.string().min(1)
182
+ });
183
+ var samlProviderConfigSchema = import_zod.z.object({
184
+ ignoreHttpsErrors: import_zod.z.boolean(),
185
+ formSelectors: samlFormSelectorsSchema
186
+ });
255
187
  var clientConfigSchema = import_zod.z.object({
256
188
  url: import_zod.z.string().url(),
257
189
  client: import_zod.z.string().min(1).max(3),
@@ -265,10 +197,14 @@ var clientConfigSchema = import_zod.z.object({
265
197
  type: import_zod.z.literal("saml"),
266
198
  username: import_zod.z.string().min(1),
267
199
  password: import_zod.z.string().min(1),
268
- provider: import_zod.z.string().optional()
200
+ providerConfig: samlProviderConfigSchema.optional()
269
201
  }),
270
202
  import_zod.z.object({
271
203
  type: import_zod.z.literal("sso"),
204
+ slsUrl: import_zod.z.string().url(),
205
+ profile: import_zod.z.string().optional(),
206
+ servicePrincipalName: import_zod.z.string().optional(),
207
+ forceEnroll: import_zod.z.boolean().optional(),
272
208
  certificate: import_zod.z.string().optional()
273
209
  })
274
210
  ]),
@@ -276,6 +212,16 @@ var clientConfigSchema = import_zod.z.object({
276
212
  insecure: import_zod.z.boolean().optional()
277
213
  });
278
214
 
215
+ // src/core/session/types.ts
216
+ var DEFAULT_SESSION_CONFIG = {
217
+ sessionTimeout: 10800,
218
+ // 3 hours (Basic/SSO)
219
+ samlSessionTimeout: 1800,
220
+ // 30 minutes (SAML)
221
+ cleanupInterval: 60
222
+ // 1 minute
223
+ };
224
+
279
225
  // src/core/session/login.ts
280
226
  async function fetchCsrfToken(state, request) {
281
227
  const endpoint = state.config.auth.type === "saml" ? "/sap/bc/adt/core/http/sessions" : "/sap/bc/adt/compatibility/graph";
@@ -308,22 +254,43 @@ async function fetchCsrfToken(state, request) {
308
254
  debug(`Stored CSRF token in state: ${state.csrfToken?.substring(0, 20)}...`);
309
255
  return ok(token);
310
256
  }
311
- async function login(state, request) {
312
- if (state.config.auth.type === "saml") {
313
- return err(new Error("SAML authentication not yet implemented"));
257
+ function getSessionTimeout(authType) {
258
+ switch (authType) {
259
+ case "saml":
260
+ return DEFAULT_SESSION_CONFIG.samlSessionTimeout * 1e3;
261
+ case "basic":
262
+ case "sso":
263
+ return DEFAULT_SESSION_CONFIG.sessionTimeout * 1e3;
264
+ default: {
265
+ const _exhaustive = authType;
266
+ return DEFAULT_SESSION_CONFIG.sessionTimeout * 1e3;
267
+ }
314
268
  }
315
- if (state.config.auth.type === "sso") {
316
- return err(new Error("SSO authentication not yet implemented"));
269
+ }
270
+ function extractUsername(auth) {
271
+ switch (auth.type) {
272
+ case "basic":
273
+ case "saml":
274
+ return auth.username;
275
+ case "sso":
276
+ return process.env["USERNAME"] ?? process.env["USER"] ?? "SSO_USER";
277
+ default: {
278
+ const _exhaustive = auth;
279
+ return "";
280
+ }
317
281
  }
282
+ }
283
+ async function login(state, request) {
318
284
  const [token, tokenErr] = await fetchCsrfToken(state, request);
319
285
  if (tokenErr) {
320
286
  return err(new Error(`Login failed: ${tokenErr.message}`));
321
287
  }
322
- const username = state.config.auth.type === "basic" ? state.config.auth.username : "";
288
+ const username = extractUsername(state.config.auth);
289
+ const timeout = getSessionTimeout(state.config.auth.type);
323
290
  const session = {
324
291
  sessionId: token,
325
292
  username,
326
- expiresAt: Date.now() + 8 * 60 * 60 * 1e3
293
+ expiresAt: Date.now() + timeout
327
294
  };
328
295
  state.session = session;
329
296
  return ok(session);
@@ -904,63 +871,6 @@ function extractTransports(xml) {
904
871
  return ok(transports);
905
872
  }
906
873
 
907
- // src/core/adt/queryBuilder.ts
908
- function buildWhereClauses(filters) {
909
- if (!filters || filters.length === 0) {
910
- return "";
911
- }
912
- const clauses = filters.map((filter) => {
913
- const { column, operator, value } = filter;
914
- switch (operator) {
915
- case "eq":
916
- return `${column} = ${formatValue(value)}`;
917
- case "ne":
918
- return `${column} != ${formatValue(value)}`;
919
- case "gt":
920
- return `${column} > ${formatValue(value)}`;
921
- case "ge":
922
- return `${column} >= ${formatValue(value)}`;
923
- case "lt":
924
- return `${column} < ${formatValue(value)}`;
925
- case "le":
926
- return `${column} <= ${formatValue(value)}`;
927
- case "like":
928
- return `${column} LIKE ${formatValue(value)}`;
929
- case "in":
930
- if (Array.isArray(value)) {
931
- const values = value.map((v) => formatValue(v)).join(", ");
932
- return `${column} IN (${values})`;
933
- }
934
- return `${column} IN (${formatValue(value)})`;
935
- default:
936
- return "";
937
- }
938
- }).filter((c) => c);
939
- if (clauses.length === 0) {
940
- return "";
941
- }
942
- return ` WHERE ${clauses.join(" AND ")}`;
943
- }
944
- function buildOrderByClauses(orderBy) {
945
- if (!orderBy || orderBy.length === 0) {
946
- return "";
947
- }
948
- const clauses = orderBy.map((o) => `${o.column} ${o.direction.toUpperCase()}`);
949
- return ` ORDER BY ${clauses.join(", ")}`;
950
- }
951
- function formatValue(value) {
952
- if (value === null) {
953
- return "NULL";
954
- }
955
- if (typeof value === "string") {
956
- return `'${value.replace(/'/g, "''")}'`;
957
- }
958
- if (typeof value === "boolean") {
959
- return value ? "1" : "0";
960
- }
961
- return String(value);
962
- }
963
-
964
874
  // src/core/adt/previewParser.ts
965
875
  function parseDataPreview(xml, maxRows, isTable) {
966
876
  const [doc, parseErr] = safeParseXml(xml);
@@ -979,10 +889,18 @@ function parseDataPreview(xml, maxRows, isTable) {
979
889
  if (!name || !dataType) continue;
980
890
  columns.push({ name, dataType });
981
891
  }
892
+ const dataSetElements = doc.getElementsByTagNameNS(namespace, "dataSet");
893
+ if (columns.length === 0 && dataSetElements.length > 0) {
894
+ for (let i = 0; i < dataSetElements.length; i++) {
895
+ const dataSet = dataSetElements[i];
896
+ if (!dataSet) continue;
897
+ const name = dataSet.getAttributeNS(namespace, "columnName") || dataSet.getAttribute("columnName") || `column${i}`;
898
+ columns.push({ name, dataType: "unknown" });
899
+ }
900
+ }
982
901
  if (columns.length === 0) {
983
- return err(new Error("No columns found in preview response"));
902
+ return ok({ columns: [], rows: [], totalRows: 0 });
984
903
  }
985
- const dataSetElements = doc.getElementsByTagNameNS(namespace, "dataSet");
986
904
  const columnData = Array.from({ length: columns.length }, () => []);
987
905
  for (let i = 0; i < dataSetElements.length; i++) {
988
906
  const dataSet = dataSetElements[i];
@@ -1012,23 +930,16 @@ function parseDataPreview(xml, maxRows, isTable) {
1012
930
  return ok(dataFrame);
1013
931
  }
1014
932
 
1015
- // src/core/adt/data.ts
933
+ // src/core/adt/dataPreview.ts
1016
934
  async function previewData(client, query) {
1017
935
  const extension = query.objectType === "table" ? "astabldt" : "asddls";
1018
936
  const config = getConfigByExtension(extension);
1019
- if (!config || !config.dpEndpoint || !config.dpParam) {
937
+ if (!config?.dpEndpoint || !config?.dpParam) {
1020
938
  return err(new Error(`Data preview not supported for object type: ${query.objectType}`));
1021
939
  }
1022
940
  const limit = query.limit ?? 100;
1023
- const whereClauses = buildWhereClauses(query.filters);
1024
- const orderByClauses = buildOrderByClauses(query.orderBy);
1025
- const sqlQuery = `select * from ${query.objectName}${whereClauses}${orderByClauses}`;
1026
- const [, validationErr] = validateSqlInput(sqlQuery);
1027
- if (validationErr) {
1028
- return err(new Error(`SQL validation failed: ${validationErr.message}`));
1029
- }
1030
941
  debug(`Data preview: endpoint=${config.dpEndpoint}, param=${config.dpParam}=${query.objectName}`);
1031
- debug(`SQL: ${sqlQuery}`);
942
+ debug(`SQL: ${query.sqlQuery}`);
1032
943
  const [response, requestErr] = await client.request({
1033
944
  method: "POST",
1034
945
  path: `/sap/bc/adt/datapreview/${config.dpEndpoint}`,
@@ -1040,7 +951,7 @@ async function previewData(client, query) {
1040
951
  "Accept": "application/vnd.sap.adt.datapreview.table.v1+xml",
1041
952
  "Content-Type": "text/plain"
1042
953
  },
1043
- body: sqlQuery
954
+ body: query.sqlQuery
1044
955
  });
1045
956
  if (requestErr) {
1046
957
  return err(requestErr);
@@ -1062,109 +973,41 @@ async function previewData(client, query) {
1062
973
  // src/core/adt/distinct.ts
1063
974
  var MAX_ROW_COUNT = 5e4;
1064
975
  async function getDistinctValues(client, objectName, column, objectType = "view") {
1065
- const extension = objectType === "table" ? "astabldt" : "asddls";
1066
- const config = getConfigByExtension(extension);
1067
- if (!config || !config.dpEndpoint || !config.dpParam) {
1068
- return err(new Error(`Data preview not supported for object type: ${objectType}`));
1069
- }
1070
976
  const columnName = column.toUpperCase();
1071
977
  const sqlQuery = `SELECT ${columnName} AS value, COUNT(*) AS count FROM ${objectName} GROUP BY ${columnName}`;
1072
- const [, validationErr] = validateSqlInput(sqlQuery);
1073
- if (validationErr) {
1074
- return err(new Error(`SQL validation failed: ${validationErr.message}`));
1075
- }
1076
- const [response, requestErr] = await client.request({
1077
- method: "POST",
1078
- path: `/sap/bc/adt/datapreview/${config.dpEndpoint}`,
1079
- params: {
1080
- "rowNumber": MAX_ROW_COUNT,
1081
- [config.dpParam]: objectName
1082
- },
1083
- headers: {
1084
- "Accept": "application/vnd.sap.adt.datapreview.table.v1+xml"
1085
- },
1086
- body: sqlQuery
978
+ const [dataFrame, error] = await previewData(client, {
979
+ objectName,
980
+ objectType,
981
+ sqlQuery,
982
+ limit: MAX_ROW_COUNT
1087
983
  });
1088
- if (requestErr) {
1089
- return err(requestErr);
1090
- }
1091
- if (!response.ok) {
1092
- const text2 = await response.text();
1093
- const errorMsg = extractError(text2);
1094
- return err(new Error(`Distinct values query failed: ${errorMsg}`));
1095
- }
1096
- const text = await response.text();
1097
- const [doc, parseErr] = safeParseXml(text);
1098
- if (parseErr) {
1099
- return err(parseErr);
1100
- }
1101
- const dataSets = doc.getElementsByTagNameNS("http://www.sap.com/adt/dataPreview", "dataSet");
1102
- const values = [];
1103
- for (let i = 0; i < dataSets.length; i++) {
1104
- const dataSet = dataSets[i];
1105
- if (!dataSet) continue;
1106
- const dataElements = dataSet.getElementsByTagNameNS("http://www.sap.com/adt/dataPreview", "data");
1107
- if (dataElements.length < 2) continue;
1108
- const value = dataElements[0]?.textContent ?? "";
1109
- const countText = dataElements[1]?.textContent?.trim() ?? "0";
1110
- values.push({
1111
- value,
1112
- count: parseInt(countText, 10)
1113
- });
1114
- }
1115
- const result = {
1116
- column,
1117
- values
1118
- };
1119
- return ok(result);
984
+ if (error) {
985
+ return err(new Error(`Distinct values query failed: ${error.message}`));
986
+ }
987
+ const values = dataFrame.rows.map((row) => ({
988
+ value: row[0],
989
+ count: parseInt(String(row[1]), 10)
990
+ }));
991
+ return ok({ column, values });
1120
992
  }
1121
993
 
1122
994
  // src/core/adt/count.ts
1123
995
  async function countRows(client, objectName, objectType) {
1124
- const extension = objectType === "table" ? "astabldt" : "asddls";
1125
- const config = getConfigByExtension(extension);
1126
- if (!config || !config.dpEndpoint || !config.dpParam) {
1127
- return err(new Error(`Data preview not supported for object type: ${objectType}`));
1128
- }
1129
996
  const sqlQuery = `SELECT COUNT(*) AS count FROM ${objectName}`;
1130
- const [, validationErr] = validateSqlInput(sqlQuery);
1131
- if (validationErr) {
1132
- return err(new Error(`SQL validation failed: ${validationErr.message}`));
1133
- }
1134
- const [response, requestErr] = await client.request({
1135
- method: "POST",
1136
- path: `/sap/bc/adt/datapreview/${config.dpEndpoint}`,
1137
- params: {
1138
- "rowNumber": 1,
1139
- [config.dpParam]: objectName
1140
- },
1141
- headers: {
1142
- "Accept": "application/vnd.sap.adt.datapreview.table.v1+xml"
1143
- },
1144
- body: sqlQuery
997
+ const [dataFrame, error] = await previewData(client, {
998
+ objectName,
999
+ objectType,
1000
+ sqlQuery,
1001
+ limit: 1
1145
1002
  });
1146
- if (requestErr) {
1147
- return err(requestErr);
1148
- }
1149
- if (!response.ok) {
1150
- const text2 = await response.text();
1151
- const errorMsg = extractError(text2);
1152
- return err(new Error(`Row count query failed: ${errorMsg}`));
1153
- }
1154
- const text = await response.text();
1155
- const [doc, parseErr] = safeParseXml(text);
1156
- if (parseErr) {
1157
- return err(parseErr);
1003
+ if (error) {
1004
+ return err(new Error(`Row count query failed: ${error.message}`));
1158
1005
  }
1159
- const dataElements = doc.getElementsByTagNameNS("http://www.sap.com/adt/dataPreview", "data");
1160
- if (dataElements.length === 0) {
1006
+ const countValue = dataFrame.rows[0]?.[0];
1007
+ if (countValue === void 0) {
1161
1008
  return err(new Error("No count value returned"));
1162
1009
  }
1163
- const countText = dataElements[0]?.textContent?.trim();
1164
- if (!countText) {
1165
- return err(new Error("Empty count value returned"));
1166
- }
1167
- const count = parseInt(countText, 10);
1010
+ const count = parseInt(String(countValue), 10);
1168
1011
  if (isNaN(count)) {
1169
1012
  return err(new Error("Invalid count value returned"));
1170
1013
  }
@@ -1424,7 +1267,590 @@ async function gitDiff(client, object) {
1424
1267
  }
1425
1268
 
1426
1269
  // src/core/client.ts
1270
+ var import_undici2 = require("undici");
1271
+
1272
+ // src/core/auth/basic/basic.ts
1273
+ var BasicAuth = class {
1274
+ type = "basic";
1275
+ authHeader;
1276
+ /**
1277
+ * Create a Basic Auth strategy
1278
+ * @param username - SAP username
1279
+ * @param password - SAP password
1280
+ */
1281
+ constructor(username, password) {
1282
+ if (!username || !password) {
1283
+ throw new Error("BasicAuth requires both username and password");
1284
+ }
1285
+ const credentials = `${username}:${password}`;
1286
+ const encoded = btoa(credentials);
1287
+ this.authHeader = `Basic ${encoded}`;
1288
+ }
1289
+ /**
1290
+ * Get Authorization header with Basic credentials
1291
+ * @returns Headers object with Authorization field
1292
+ */
1293
+ getAuthHeaders() {
1294
+ return {
1295
+ Authorization: this.authHeader
1296
+ };
1297
+ }
1298
+ };
1299
+
1300
+ // src/core/auth/sso/slsClient.ts
1427
1301
  var import_undici = require("undici");
1302
+
1303
+ // src/core/auth/sso/types.ts
1304
+ var SLS_DEFAULTS = {
1305
+ PROFILE: "SAPSSO_P",
1306
+ LOGIN_ENDPOINT: "/SecureLoginServer/slc3/doLogin",
1307
+ CERTIFICATE_ENDPOINT: "/SecureLoginServer/slc2/getCertificate",
1308
+ KEY_SIZE: 2048
1309
+ };
1310
+ var CERTIFICATE_STORAGE = {
1311
+ BASE_DIR: "./certificates/sso",
1312
+ FULL_CHAIN_SUFFIX: "_full_chain.pem",
1313
+ KEY_SUFFIX: "_key.pem"
1314
+ };
1315
+
1316
+ // src/core/auth/sso/kerberos.ts
1317
+ async function loadKerberosModule() {
1318
+ try {
1319
+ const kerberosModule = require("kerberos");
1320
+ return ok(kerberosModule);
1321
+ } catch {
1322
+ return err(new Error(
1323
+ "kerberos package is not installed. Install it with: npm install kerberos"
1324
+ ));
1325
+ }
1326
+ }
1327
+ async function getSpnegoToken(servicePrincipalName) {
1328
+ const [kerberos, loadErr] = await loadKerberosModule();
1329
+ if (loadErr) return err(loadErr);
1330
+ try {
1331
+ const client = await kerberos.initializeClient(servicePrincipalName);
1332
+ const token = await client.step("");
1333
+ if (!token) {
1334
+ return err(new Error("Failed to generate SPNEGO token: empty response"));
1335
+ }
1336
+ return ok(token);
1337
+ } catch (error) {
1338
+ const message = error instanceof Error ? error.message : String(error);
1339
+ return err(new Error(`Kerberos authentication failed: ${message}`));
1340
+ }
1341
+ }
1342
+ function extractSpnFromUrl(slsUrl) {
1343
+ const url = new URL(slsUrl);
1344
+ return `HTTP/${url.hostname}`;
1345
+ }
1346
+
1347
+ // src/core/auth/sso/certificate.ts
1348
+ var import_node_forge = __toESM(require("node-forge"));
1349
+ function generateKeypair(keySize = SLS_DEFAULTS.KEY_SIZE) {
1350
+ const keypair = import_node_forge.default.pki.rsa.generateKeyPair({ bits: keySize, e: 65537 });
1351
+ const privateKeyPem = import_node_forge.default.pki.privateKeyToPem(keypair.privateKey);
1352
+ return {
1353
+ privateKeyPem,
1354
+ privateKey: keypair.privateKey,
1355
+ publicKey: keypair.publicKey
1356
+ };
1357
+ }
1358
+ function createCsr(keypair, username) {
1359
+ const csr = import_node_forge.default.pki.createCertificationRequest();
1360
+ csr.publicKey = keypair.publicKey;
1361
+ csr.setSubject([{
1362
+ name: "commonName",
1363
+ value: username
1364
+ }]);
1365
+ csr.setAttributes([{
1366
+ name: "extensionRequest",
1367
+ extensions: [
1368
+ {
1369
+ name: "keyUsage",
1370
+ digitalSignature: true,
1371
+ keyEncipherment: true
1372
+ },
1373
+ {
1374
+ name: "extKeyUsage",
1375
+ clientAuth: true
1376
+ }
1377
+ ]
1378
+ }]);
1379
+ csr.sign(keypair.privateKey, import_node_forge.default.md.sha256.create());
1380
+ const csrAsn1 = import_node_forge.default.pki.certificationRequestToAsn1(csr);
1381
+ const csrDer = import_node_forge.default.asn1.toDer(csrAsn1);
1382
+ return Buffer.from(csrDer.getBytes(), "binary");
1383
+ }
1384
+ function getCurrentUsername() {
1385
+ return process.env["USERNAME"] ?? process.env["USER"] ?? "unknown";
1386
+ }
1387
+
1388
+ // src/core/auth/sso/pkcs7.ts
1389
+ var import_node_forge2 = __toESM(require("node-forge"));
1390
+ function parsePkcs7Certificates(data) {
1391
+ try {
1392
+ const dataString = data.toString("utf-8").replace(/\r?\n/g, "").trim();
1393
+ const derBytes = import_node_forge2.default.util.decode64(dataString);
1394
+ const p7Asn1 = import_node_forge2.default.asn1.fromDer(derBytes);
1395
+ const p7 = import_node_forge2.default.pkcs7.messageFromAsn1(p7Asn1);
1396
+ if (!("certificates" in p7) || !p7.certificates || p7.certificates.length === 0) {
1397
+ return err(new Error("No certificates found in PKCS#7 structure"));
1398
+ }
1399
+ const certificates = p7.certificates;
1400
+ const clientCert = certificates[0];
1401
+ const caCerts = certificates.slice(1);
1402
+ if (!clientCert) {
1403
+ return err(new Error("No client certificate found in PKCS#7 structure"));
1404
+ }
1405
+ const clientCertPem = import_node_forge2.default.pki.certificateToPem(clientCert);
1406
+ const caChainPem = caCerts.map((cert) => import_node_forge2.default.pki.certificateToPem(cert)).join("");
1407
+ return ok({
1408
+ clientCert: clientCertPem,
1409
+ caChain: caChainPem,
1410
+ fullChain: clientCertPem + caChainPem
1411
+ });
1412
+ } catch (error) {
1413
+ const message = error instanceof Error ? error.message : String(error);
1414
+ return err(new Error(`Failed to parse PKCS#7 certificates: ${message}`));
1415
+ }
1416
+ }
1417
+
1418
+ // src/core/auth/sso/slsClient.ts
1419
+ async function enrollCertificate(options) {
1420
+ const { config, insecure = false } = options;
1421
+ const profile = config.profile ?? SLS_DEFAULTS.PROFILE;
1422
+ const agent = insecure ? new import_undici.Agent({ connect: { rejectUnauthorized: false } }) : void 0;
1423
+ const [authResponse, authErr] = await authenticateToSls(config, profile, agent);
1424
+ if (authErr) return err(authErr);
1425
+ const keySize = authResponse.clientConfig.keySize ?? SLS_DEFAULTS.KEY_SIZE;
1426
+ const keypair = generateKeypair(keySize);
1427
+ const username = getCurrentUsername();
1428
+ const csrDer = createCsr(keypair, username);
1429
+ const [certData, certErr] = await requestCertificate(config, profile, csrDer, agent);
1430
+ if (certErr) return err(certErr);
1431
+ const [certs, parseErr] = parsePkcs7Certificates(certData);
1432
+ if (parseErr) return err(parseErr);
1433
+ return ok({
1434
+ fullChain: certs.fullChain,
1435
+ privateKey: keypair.privateKeyPem
1436
+ });
1437
+ }
1438
+ async function authenticateToSls(config, profile, agent) {
1439
+ const spn = config.servicePrincipalName ?? extractSpnFromUrl(config.slsUrl);
1440
+ const [token, tokenErr] = await getSpnegoToken(spn);
1441
+ if (tokenErr) return err(tokenErr);
1442
+ const authUrl = `${config.slsUrl}${SLS_DEFAULTS.LOGIN_ENDPOINT}?profile=${profile}`;
1443
+ try {
1444
+ const fetchOptions = {
1445
+ method: "POST",
1446
+ headers: {
1447
+ "Authorization": `Negotiate ${token}`,
1448
+ "Accept": "*/*"
1449
+ }
1450
+ };
1451
+ if (agent) {
1452
+ fetchOptions.dispatcher = agent;
1453
+ }
1454
+ const response = await (0, import_undici.fetch)(authUrl, fetchOptions);
1455
+ if (!response.ok) {
1456
+ const text = await response.text();
1457
+ return err(new Error(`SLS authentication failed: ${response.status} - ${text}`));
1458
+ }
1459
+ const authResponse = await response.json();
1460
+ return ok(authResponse);
1461
+ } catch (error) {
1462
+ const message = error instanceof Error ? error.message : String(error);
1463
+ return err(new Error(`SLS authentication request failed: ${message}`));
1464
+ }
1465
+ }
1466
+ async function requestCertificate(config, profile, csrDer, agent) {
1467
+ const certUrl = `${config.slsUrl}${SLS_DEFAULTS.CERTIFICATE_ENDPOINT}?profile=${profile}`;
1468
+ try {
1469
+ const fetchOptions = {
1470
+ method: "POST",
1471
+ headers: {
1472
+ "Content-Type": "application/pkcs10",
1473
+ "Content-Length": String(csrDer.length),
1474
+ "Accept": "*/*"
1475
+ },
1476
+ body: csrDer
1477
+ };
1478
+ if (agent) {
1479
+ fetchOptions.dispatcher = agent;
1480
+ }
1481
+ const response = await (0, import_undici.fetch)(certUrl, fetchOptions);
1482
+ if (!response.ok) {
1483
+ const text = await response.text();
1484
+ return err(new Error(`Certificate request failed: ${response.status} - ${text}`));
1485
+ }
1486
+ const buffer = await response.arrayBuffer();
1487
+ return ok(Buffer.from(buffer));
1488
+ } catch (error) {
1489
+ const message = error instanceof Error ? error.message : String(error);
1490
+ return err(new Error(`Certificate request failed: ${message}`));
1491
+ }
1492
+ }
1493
+
1494
+ // src/core/auth/sso/storage.ts
1495
+ var import_promises = require("fs/promises");
1496
+ var import_node_path = require("path");
1497
+ function getCertificatePaths(username) {
1498
+ const user = username ?? getCurrentUsername();
1499
+ return {
1500
+ fullChainPath: (0, import_node_path.join)(CERTIFICATE_STORAGE.BASE_DIR, `${user}${CERTIFICATE_STORAGE.FULL_CHAIN_SUFFIX}`),
1501
+ keyPath: (0, import_node_path.join)(CERTIFICATE_STORAGE.BASE_DIR, `${user}${CERTIFICATE_STORAGE.KEY_SUFFIX}`)
1502
+ };
1503
+ }
1504
+ async function saveCertificates(material, username) {
1505
+ const paths = getCertificatePaths(username);
1506
+ try {
1507
+ await (0, import_promises.mkdir)(CERTIFICATE_STORAGE.BASE_DIR, { recursive: true });
1508
+ await (0, import_promises.writeFile)(paths.fullChainPath, material.fullChain, "utf-8");
1509
+ await (0, import_promises.writeFile)(paths.keyPath, material.privateKey, { encoding: "utf-8", mode: 384 });
1510
+ return ok(paths);
1511
+ } catch (error) {
1512
+ const message = error instanceof Error ? error.message : String(error);
1513
+ return err(new Error(`Failed to save certificates: ${message}`));
1514
+ }
1515
+ }
1516
+ async function loadCertificates(username) {
1517
+ const paths = getCertificatePaths(username);
1518
+ try {
1519
+ const [fullChain, privateKey] = await Promise.all([
1520
+ (0, import_promises.readFile)(paths.fullChainPath, "utf-8"),
1521
+ (0, import_promises.readFile)(paths.keyPath, "utf-8")
1522
+ ]);
1523
+ return ok({ fullChain, privateKey });
1524
+ } catch (error) {
1525
+ const message = error instanceof Error ? error.message : String(error);
1526
+ return err(new Error(`Failed to load certificates: ${message}`));
1527
+ }
1528
+ }
1529
+ async function certificatesExist(username) {
1530
+ const paths = getCertificatePaths(username);
1531
+ try {
1532
+ await Promise.all([
1533
+ (0, import_promises.stat)(paths.fullChainPath),
1534
+ (0, import_promises.stat)(paths.keyPath)
1535
+ ]);
1536
+ return true;
1537
+ } catch {
1538
+ return false;
1539
+ }
1540
+ }
1541
+ function isCertificateExpired(certPem, bufferDays = 1) {
1542
+ try {
1543
+ const forge3 = require("node-forge");
1544
+ const cert = forge3.pki.certificateFromPem(certPem);
1545
+ const notAfter = cert.validity.notAfter;
1546
+ const bufferMs = bufferDays * 24 * 60 * 60 * 1e3;
1547
+ const expiryThreshold = new Date(Date.now() + bufferMs);
1548
+ return ok(notAfter <= expiryThreshold);
1549
+ } catch (error) {
1550
+ const message = error instanceof Error ? error.message : String(error);
1551
+ return err(new Error(`Failed to check certificate expiry: ${message}`));
1552
+ }
1553
+ }
1554
+
1555
+ // src/core/auth/sso/sso.ts
1556
+ var SsoAuth = class {
1557
+ type = "sso";
1558
+ config;
1559
+ certificates = null;
1560
+ /**
1561
+ * Create an SSO Auth strategy
1562
+ *
1563
+ * @param config - SSO authentication configuration
1564
+ */
1565
+ constructor(config) {
1566
+ if (!config.slsUrl) {
1567
+ throw new Error("SsoAuth requires slsUrl");
1568
+ }
1569
+ this.config = config;
1570
+ }
1571
+ /**
1572
+ * Get auth headers for SSO
1573
+ *
1574
+ * SSO uses mTLS for authentication, not headers.
1575
+ * Returns empty headers - the mTLS agent handles auth.
1576
+ */
1577
+ getAuthHeaders() {
1578
+ return {};
1579
+ }
1580
+ /**
1581
+ * Get mTLS certificates
1582
+ *
1583
+ * Returns the certificate material after successful login.
1584
+ * Used by ADT client to create an mTLS agent.
1585
+ *
1586
+ * @returns Certificate material or null if not enrolled
1587
+ */
1588
+ getCertificates() {
1589
+ return this.certificates;
1590
+ }
1591
+ /**
1592
+ * Perform SSO login via certificate enrollment
1593
+ *
1594
+ * Checks for existing valid certificates and enrolls new ones if needed.
1595
+ *
1596
+ * @param _fetchFn - Unused, kept for interface compatibility
1597
+ * @returns Success/error tuple
1598
+ */
1599
+ async performLogin(_fetchFn) {
1600
+ if (!this.config.forceEnroll) {
1601
+ const [loadResult, loadErr] = await this.tryLoadExistingCertificates();
1602
+ if (!loadErr && loadResult) {
1603
+ this.certificates = loadResult;
1604
+ return ok(void 0);
1605
+ }
1606
+ }
1607
+ const slsConfig = {
1608
+ slsUrl: this.config.slsUrl
1609
+ };
1610
+ if (this.config.profile) {
1611
+ slsConfig.profile = this.config.profile;
1612
+ }
1613
+ if (this.config.servicePrincipalName) {
1614
+ slsConfig.servicePrincipalName = this.config.servicePrincipalName;
1615
+ }
1616
+ const [material, enrollErr] = await enrollCertificate({
1617
+ config: slsConfig,
1618
+ insecure: this.config.insecure ?? false
1619
+ });
1620
+ if (enrollErr) {
1621
+ return err(enrollErr);
1622
+ }
1623
+ if (!this.config.returnContents) {
1624
+ const [, saveErr] = await saveCertificates(material);
1625
+ if (saveErr) {
1626
+ return err(saveErr);
1627
+ }
1628
+ }
1629
+ this.certificates = material;
1630
+ return ok(void 0);
1631
+ }
1632
+ /**
1633
+ * Try to load and validate existing certificates
1634
+ */
1635
+ async tryLoadExistingCertificates() {
1636
+ const exists = await certificatesExist();
1637
+ if (!exists) {
1638
+ return err(new Error("No existing certificates found"));
1639
+ }
1640
+ const [material, loadErr] = await loadCertificates();
1641
+ if (loadErr) {
1642
+ return err(loadErr);
1643
+ }
1644
+ const [isExpired, expiryErr] = isCertificateExpired(material.fullChain);
1645
+ if (expiryErr) {
1646
+ return err(expiryErr);
1647
+ }
1648
+ if (isExpired) {
1649
+ return err(new Error("Certificate is expired or expiring soon"));
1650
+ }
1651
+ return ok(material);
1652
+ }
1653
+ };
1654
+
1655
+ // src/core/auth/saml/types.ts
1656
+ var DEFAULT_FORM_SELECTORS = {
1657
+ username: "#j_username",
1658
+ password: "#j_password",
1659
+ submit: "#logOnFormSubmit"
1660
+ };
1661
+ var DEFAULT_PROVIDER_CONFIG = {
1662
+ ignoreHttpsErrors: false,
1663
+ formSelectors: DEFAULT_FORM_SELECTORS
1664
+ };
1665
+
1666
+ // src/core/auth/saml/browser.ts
1667
+ var TIMEOUTS = {
1668
+ PAGE_LOAD: 6e4,
1669
+ FORM_SELECTOR: 1e4
1670
+ };
1671
+ async function performBrowserLogin(options) {
1672
+ const { baseUrl, credentials, headless = true } = options;
1673
+ const config = options.providerConfig ?? DEFAULT_PROVIDER_CONFIG;
1674
+ let playwright;
1675
+ try {
1676
+ playwright = await import("playwright");
1677
+ } catch {
1678
+ return err(
1679
+ new Error(
1680
+ "Playwright is required for SAML authentication but is not installed. Install it with: npm install playwright"
1681
+ )
1682
+ );
1683
+ }
1684
+ const browserArgs = config.ignoreHttpsErrors ? ["--ignore-certificate-errors", "--disable-web-security"] : [];
1685
+ let browser;
1686
+ try {
1687
+ browser = await playwright.chromium.launch({
1688
+ headless,
1689
+ args: browserArgs
1690
+ });
1691
+ } catch (launchError) {
1692
+ return err(
1693
+ new Error(
1694
+ `Failed to launch browser: ${launchError instanceof Error ? launchError.message : String(launchError)}`
1695
+ )
1696
+ );
1697
+ }
1698
+ try {
1699
+ const context = await browser.newContext({
1700
+ ignoreHTTPSErrors: config.ignoreHttpsErrors
1701
+ });
1702
+ const page = await context.newPage();
1703
+ const loginUrl = `${baseUrl}/sap/bc/adt/compatibility/graph`;
1704
+ try {
1705
+ await page.goto(loginUrl, {
1706
+ timeout: TIMEOUTS.PAGE_LOAD,
1707
+ waitUntil: "domcontentloaded"
1708
+ });
1709
+ } catch {
1710
+ return err(new Error("Failed to load login page. Please check if the server is online."));
1711
+ }
1712
+ try {
1713
+ await page.waitForSelector(config.formSelectors.username, {
1714
+ timeout: TIMEOUTS.FORM_SELECTOR
1715
+ });
1716
+ } catch {
1717
+ return err(new Error("Login form not found. The page may have changed or loaded incorrectly."));
1718
+ }
1719
+ await page.fill(config.formSelectors.username, credentials.username);
1720
+ await page.fill(config.formSelectors.password, credentials.password);
1721
+ await page.click(config.formSelectors.submit);
1722
+ await page.waitForLoadState("networkidle");
1723
+ const cookies = await context.cookies();
1724
+ return ok(cookies);
1725
+ } finally {
1726
+ await browser.close();
1727
+ }
1728
+ }
1729
+
1730
+ // src/core/auth/saml/cookies.ts
1731
+ function toAuthCookies(playwrightCookies) {
1732
+ return playwrightCookies.map((cookie) => ({
1733
+ name: cookie.name,
1734
+ value: cookie.value,
1735
+ domain: cookie.domain,
1736
+ path: cookie.path
1737
+ }));
1738
+ }
1739
+ function formatCookieHeader(cookies) {
1740
+ return cookies.map((c) => `${c.name}=${c.value}`).join("; ");
1741
+ }
1742
+
1743
+ // src/core/auth/saml/saml.ts
1744
+ var SamlAuth = class {
1745
+ type = "saml";
1746
+ cookies = [];
1747
+ config;
1748
+ /**
1749
+ * Create a SAML Auth strategy
1750
+ *
1751
+ * @param config - SAML authentication configuration
1752
+ */
1753
+ constructor(config) {
1754
+ if (!config.username || !config.password) {
1755
+ throw new Error("SamlAuth requires both username and password");
1756
+ }
1757
+ if (!config.baseUrl) {
1758
+ throw new Error("SamlAuth requires baseUrl");
1759
+ }
1760
+ this.config = config;
1761
+ }
1762
+ /**
1763
+ * Get auth headers for SAML
1764
+ *
1765
+ * After successful login, includes Cookie header with session cookies.
1766
+ */
1767
+ getAuthHeaders() {
1768
+ if (this.cookies.length === 0) {
1769
+ return {};
1770
+ }
1771
+ return {
1772
+ Cookie: formatCookieHeader(this.cookies)
1773
+ };
1774
+ }
1775
+ /**
1776
+ * Get authentication cookies
1777
+ *
1778
+ * @returns Array of cookies obtained during SAML login
1779
+ */
1780
+ getCookies() {
1781
+ return this.cookies;
1782
+ }
1783
+ /**
1784
+ * Perform SAML login using headless browser automation
1785
+ *
1786
+ * Launches a Chromium browser, navigates to the SAP login page,
1787
+ * fills in credentials, and extracts session cookies.
1788
+ *
1789
+ * @param _fetchFn - Unused, kept for interface compatibility
1790
+ * @returns Success/error tuple
1791
+ */
1792
+ async performLogin(_fetchFn) {
1793
+ const [playwrightCookies, loginError] = await performBrowserLogin({
1794
+ baseUrl: this.config.baseUrl,
1795
+ credentials: {
1796
+ username: this.config.username,
1797
+ password: this.config.password
1798
+ },
1799
+ ...this.config.providerConfig && { providerConfig: this.config.providerConfig }
1800
+ });
1801
+ if (loginError) {
1802
+ return err(loginError);
1803
+ }
1804
+ this.cookies = toAuthCookies(playwrightCookies);
1805
+ if (this.cookies.length === 0) {
1806
+ return err(new Error("SAML login succeeded but no cookies were returned"));
1807
+ }
1808
+ return ok(void 0);
1809
+ }
1810
+ };
1811
+
1812
+ // src/core/auth/factory.ts
1813
+ function createAuthStrategy(options) {
1814
+ const { config, baseUrl, insecure } = options;
1815
+ switch (config.type) {
1816
+ case "basic":
1817
+ return new BasicAuth(config.username, config.password);
1818
+ case "saml":
1819
+ if (!baseUrl) {
1820
+ throw new Error("SAML authentication requires baseUrl");
1821
+ }
1822
+ return new SamlAuth({
1823
+ username: config.username,
1824
+ password: config.password,
1825
+ baseUrl,
1826
+ ...config.providerConfig && { providerConfig: config.providerConfig }
1827
+ });
1828
+ case "sso": {
1829
+ const ssoConfig = {
1830
+ slsUrl: config.slsUrl
1831
+ };
1832
+ if (config.profile) {
1833
+ ssoConfig.profile = config.profile;
1834
+ }
1835
+ if (config.servicePrincipalName) {
1836
+ ssoConfig.servicePrincipalName = config.servicePrincipalName;
1837
+ }
1838
+ if (config.forceEnroll) {
1839
+ ssoConfig.forceEnroll = config.forceEnroll;
1840
+ }
1841
+ if (insecure) {
1842
+ ssoConfig.insecure = insecure;
1843
+ }
1844
+ return new SsoAuth(ssoConfig);
1845
+ }
1846
+ default: {
1847
+ const _exhaustive = config;
1848
+ throw new Error(`Unknown auth type: ${_exhaustive.type}`);
1849
+ }
1850
+ }
1851
+ }
1852
+
1853
+ // src/core/client.ts
1428
1854
  function buildParams(baseParams, clientNum) {
1429
1855
  const params = new URLSearchParams();
1430
1856
  if (baseParams) {
@@ -1449,15 +1875,24 @@ var ADTClientImpl = class {
1449
1875
  requestor;
1450
1876
  agent;
1451
1877
  constructor(config) {
1878
+ const authOptions = {
1879
+ config: config.auth,
1880
+ baseUrl: config.url
1881
+ };
1882
+ if (config.insecure) {
1883
+ authOptions.insecure = config.insecure;
1884
+ }
1885
+ const authStrategy = createAuthStrategy(authOptions);
1452
1886
  this.state = {
1453
1887
  config,
1454
1888
  session: null,
1455
1889
  csrfToken: null,
1456
- cookies: /* @__PURE__ */ new Map()
1890
+ cookies: /* @__PURE__ */ new Map(),
1891
+ authStrategy
1457
1892
  };
1458
1893
  this.requestor = { request: this.request.bind(this) };
1459
1894
  if (config.insecure) {
1460
- this.agent = new import_undici.Agent({
1895
+ this.agent = new import_undici2.Agent({
1461
1896
  connect: {
1462
1897
  rejectUnauthorized: false,
1463
1898
  checkServerIdentity: () => void 0
@@ -1518,7 +1953,7 @@ var ADTClientImpl = class {
1518
1953
  try {
1519
1954
  debug(`Fetching URL: ${url}`);
1520
1955
  debug(`Insecure mode: ${!!this.agent}`);
1521
- const response = await (0, import_undici.fetch)(url, fetchOptions);
1956
+ const response = await (0, import_undici2.fetch)(url, fetchOptions);
1522
1957
  this.storeCookies(response);
1523
1958
  if (response.status === 403) {
1524
1959
  const text = await response.text();
@@ -1533,7 +1968,7 @@ var ADTClientImpl = class {
1533
1968
  headers["Cookie"] = retryCookieHeader;
1534
1969
  }
1535
1970
  debug(`Retrying with new CSRF token: ${newToken.substring(0, 20)}...`);
1536
- const retryResponse = await (0, import_undici.fetch)(url, { ...fetchOptions, headers });
1971
+ const retryResponse = await (0, import_undici2.fetch)(url, { ...fetchOptions, headers });
1537
1972
  this.storeCookies(retryResponse);
1538
1973
  return ok(retryResponse);
1539
1974
  }
@@ -1569,6 +2004,29 @@ var ADTClientImpl = class {
1569
2004
  }
1570
2005
  // --- Lifecycle ---
1571
2006
  async login() {
2007
+ const { authStrategy } = this.state;
2008
+ if (authStrategy.performLogin) {
2009
+ const [, loginErr] = await authStrategy.performLogin(fetch);
2010
+ if (loginErr) {
2011
+ return err(loginErr);
2012
+ }
2013
+ }
2014
+ if (authStrategy.type === "sso" && authStrategy.getCertificates) {
2015
+ const certs = authStrategy.getCertificates();
2016
+ if (certs) {
2017
+ this.agent = new import_undici2.Agent({
2018
+ connect: {
2019
+ cert: certs.fullChain,
2020
+ key: certs.privateKey,
2021
+ rejectUnauthorized: !this.state.config.insecure,
2022
+ ...this.state.config.insecure && {
2023
+ checkServerIdentity: () => void 0
2024
+ }
2025
+ }
2026
+ });
2027
+ debug("Created mTLS agent with SSO certificates");
2028
+ }
2029
+ }
1572
2030
  return login(this.state, this.request.bind(this));
1573
2031
  }
1574
2032
  async logout() {