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.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
|
-
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
-
|
|
288
|
-
|
|
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
|
|
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() +
|
|
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
|
|
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/
|
|
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
|
|
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 [,
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
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 (
|
|
1061
|
-
return err(
|
|
1062
|
-
}
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
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 [,
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
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 (
|
|
1119
|
-
return err(
|
|
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
|
|
1132
|
-
if (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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() {
|