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/README.md +64 -7
- package/dist/index.d.mts +35 -26
- package/dist/index.d.ts +35 -26
- package/dist/index.js +718 -260
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +715 -260
- 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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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
|
-
|
|
316
|
-
|
|
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
|
|
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() +
|
|
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
|
|
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/
|
|
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
|
|
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 [,
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
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 (
|
|
1089
|
-
return err(
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
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 [,
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
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 (
|
|
1147
|
-
return err(
|
|
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
|
|
1160
|
-
if (
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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() {
|