@mcp-abap-adt/connection 0.1.15 → 0.2.0
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 +34 -95
- package/dist/connection/AbapConnection.d.ts.map +1 -1
- package/dist/connection/AbstractAbapConnection.d.ts +6 -44
- package/dist/connection/AbstractAbapConnection.d.ts.map +1 -1
- package/dist/connection/AbstractAbapConnection.js +45 -199
- package/dist/connection/BaseAbapConnection.d.ts +2 -2
- package/dist/connection/BaseAbapConnection.d.ts.map +1 -1
- package/dist/connection/BaseAbapConnection.js +6 -9
- package/dist/connection/JwtAbapConnection.d.ts +8 -16
- package/dist/connection/JwtAbapConnection.d.ts.map +1 -1
- package/dist/connection/JwtAbapConnection.js +24 -248
- package/dist/connection/connectionFactory.d.ts +2 -2
- package/dist/connection/connectionFactory.d.ts.map +1 -1
- package/dist/connection/connectionFactory.js +3 -3
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -4
- package/dist/logger.d.ts +2 -3
- package/dist/logger.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/utils/FileSessionStorage.d.ts +0 -73
- package/dist/utils/FileSessionStorage.d.ts.map +0 -1
- package/dist/utils/FileSessionStorage.js +0 -191
|
@@ -48,16 +48,12 @@ class AbstractAbapConnection {
|
|
|
48
48
|
cookieStore = new Map();
|
|
49
49
|
baseUrl;
|
|
50
50
|
sessionId = null;
|
|
51
|
-
sessionStorage = null;
|
|
52
51
|
sessionMode = "stateless";
|
|
53
|
-
constructor(config, logger,
|
|
52
|
+
constructor(config, logger, sessionId) {
|
|
54
53
|
this.config = config;
|
|
55
54
|
this.logger = logger;
|
|
56
|
-
|
|
57
|
-
// Always generate sessionId (used for sap-adt-connection-id header in all session types)
|
|
55
|
+
// Generate sessionId (used for sap-adt-connection-id header)
|
|
58
56
|
this.sessionId = sessionId || (0, crypto_1.randomUUID)();
|
|
59
|
-
// Session mode depends only on storage availability (sessionId exists for both modes)
|
|
60
|
-
this.sessionMode = sessionStorage ? "stateful" : "stateless";
|
|
61
57
|
// Initialize baseUrl from config (required, will throw if invalid)
|
|
62
58
|
try {
|
|
63
59
|
const urlObj = new URL(config.url);
|
|
@@ -66,7 +62,7 @@ class AbstractAbapConnection {
|
|
|
66
62
|
catch (error) {
|
|
67
63
|
throw new Error(`Invalid URL in configuration: ${error instanceof Error ? error.message : error}`);
|
|
68
64
|
}
|
|
69
|
-
this.logger
|
|
65
|
+
this.logger?.debug(`AbstractAbapConnection - Session ID: ${this.sessionId.substring(0, 8)}...`);
|
|
70
66
|
}
|
|
71
67
|
/**
|
|
72
68
|
* Set session type (stateful or stateless)
|
|
@@ -76,15 +72,13 @@ class AbstractAbapConnection {
|
|
|
76
72
|
*/
|
|
77
73
|
setSessionType(type) {
|
|
78
74
|
this.sessionMode = type;
|
|
79
|
-
this.logger
|
|
80
|
-
sessionId: this.sessionId?.substring(0, 8)
|
|
81
|
-
hasStorage: !!this.sessionStorage
|
|
75
|
+
this.logger?.debug(`Session type set to: ${type}`, {
|
|
76
|
+
sessionId: this.sessionId?.substring(0, 8)
|
|
82
77
|
});
|
|
83
78
|
}
|
|
84
79
|
/**
|
|
85
80
|
* Enable stateful session mode (tells SAP to maintain stateful session)
|
|
86
81
|
* This controls whether x-sap-adt-sessiontype: stateful header is used
|
|
87
|
-
* Storage is controlled separately via setSessionStorage()
|
|
88
82
|
* @deprecated Use setSessionType("stateful") instead
|
|
89
83
|
*/
|
|
90
84
|
enableStatefulSession() {
|
|
@@ -92,20 +86,14 @@ class AbstractAbapConnection {
|
|
|
92
86
|
}
|
|
93
87
|
/**
|
|
94
88
|
* Disable stateful session mode (switch to stateless)
|
|
95
|
-
* Optionally saves current state before switching
|
|
96
89
|
* @deprecated Use setSessionType("stateless") instead
|
|
97
90
|
*/
|
|
98
|
-
|
|
91
|
+
disableStatefulSession() {
|
|
99
92
|
if (this.sessionMode === "stateless") {
|
|
100
93
|
return;
|
|
101
94
|
}
|
|
102
|
-
if (saveBeforeDisable && this.sessionId && this.sessionStorage) {
|
|
103
|
-
await this.saveSessionState();
|
|
104
|
-
}
|
|
105
95
|
this.sessionMode = "stateless";
|
|
106
|
-
this.logger
|
|
107
|
-
savedBeforeDisable: saveBeforeDisable
|
|
108
|
-
});
|
|
96
|
+
this.logger?.debug("Stateful session mode disabled");
|
|
109
97
|
}
|
|
110
98
|
/**
|
|
111
99
|
* Get current session mode
|
|
@@ -114,18 +102,12 @@ class AbstractAbapConnection {
|
|
|
114
102
|
return this.sessionMode;
|
|
115
103
|
}
|
|
116
104
|
/**
|
|
117
|
-
* Set session ID
|
|
118
|
-
*
|
|
119
|
-
* @deprecated Use enableStatefulSession() instead
|
|
105
|
+
* Set session ID
|
|
106
|
+
* @deprecated Session ID is auto-generated, use setSessionType() to control session mode
|
|
120
107
|
*/
|
|
121
108
|
setSessionId(sessionId) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
this.sessionMode = "stateful";
|
|
125
|
-
}
|
|
126
|
-
else {
|
|
127
|
-
this.logger.warn("Cannot set session ID without session storage. Use enableStatefulSession() instead.");
|
|
128
|
-
}
|
|
109
|
+
this.sessionId = sessionId;
|
|
110
|
+
this.logger?.debug(`Session ID set to: ${sessionId.substring(0, 8)}...`);
|
|
129
111
|
}
|
|
130
112
|
/**
|
|
131
113
|
* Get current session ID
|
|
@@ -133,132 +115,6 @@ class AbstractAbapConnection {
|
|
|
133
115
|
getSessionId() {
|
|
134
116
|
return this.sessionId;
|
|
135
117
|
}
|
|
136
|
-
/**
|
|
137
|
-
* Set session storage (can be changed at runtime)
|
|
138
|
-
* This controls whether session state (cookies, CSRF token) is persisted to disk/storage
|
|
139
|
-
*/
|
|
140
|
-
async setSessionStorage(storage) {
|
|
141
|
-
this.sessionStorage = storage;
|
|
142
|
-
// Load existing session state if storage is provided
|
|
143
|
-
if (storage && this.sessionId) {
|
|
144
|
-
await this.loadSessionState();
|
|
145
|
-
}
|
|
146
|
-
this.logger.debug("Session storage configured", {
|
|
147
|
-
hasStorage: !!storage,
|
|
148
|
-
sessionMode: this.sessionMode
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
/**
|
|
152
|
-
* Get current session storage
|
|
153
|
-
*/
|
|
154
|
-
getSessionStorage() {
|
|
155
|
-
return this.sessionStorage;
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* Load session state from storage
|
|
159
|
-
*/
|
|
160
|
-
async loadSessionState() {
|
|
161
|
-
if (!this.sessionId || !this.sessionStorage) {
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
try {
|
|
165
|
-
const state = await this.sessionStorage.load(this.sessionId);
|
|
166
|
-
if (state) {
|
|
167
|
-
this.csrfToken = state.csrfToken;
|
|
168
|
-
this.cookies = state.cookies;
|
|
169
|
-
this.cookieStore = new Map(Object.entries(state.cookieStore));
|
|
170
|
-
this.logger.debug("Session state loaded", {
|
|
171
|
-
sessionId: this.sessionId,
|
|
172
|
-
hasCookies: !!this.cookies,
|
|
173
|
-
hasCsrfToken: !!this.csrfToken
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
catch (error) {
|
|
178
|
-
this.logger.warn("Failed to load session state", {
|
|
179
|
-
sessionId: this.sessionId,
|
|
180
|
-
error: error instanceof Error ? error.message : String(error)
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* Save session state to storage
|
|
186
|
-
* Only saves if in stateful mode
|
|
187
|
-
*/
|
|
188
|
-
async saveSessionState() {
|
|
189
|
-
if (this.sessionMode !== "stateful" || !this.sessionId || !this.sessionStorage) {
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
try {
|
|
193
|
-
const state = {
|
|
194
|
-
cookies: this.cookies,
|
|
195
|
-
csrfToken: this.csrfToken,
|
|
196
|
-
cookieStore: Object.fromEntries(this.cookieStore)
|
|
197
|
-
};
|
|
198
|
-
await this.sessionStorage.save(this.sessionId, state);
|
|
199
|
-
this.logger.debug("Session state saved", {
|
|
200
|
-
sessionId: this.sessionId,
|
|
201
|
-
hasCookies: !!this.cookies,
|
|
202
|
-
hasCsrfToken: !!this.csrfToken
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
catch (error) {
|
|
206
|
-
this.logger.warn("Failed to save session state", {
|
|
207
|
-
sessionId: this.sessionId,
|
|
208
|
-
error: error instanceof Error ? error.message : String(error)
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
/**
|
|
213
|
-
* Get current session state
|
|
214
|
-
* Returns cookies, CSRF token, and cookie store for manual persistence
|
|
215
|
-
* @returns Current session state or null if no session data
|
|
216
|
-
*/
|
|
217
|
-
getSessionState() {
|
|
218
|
-
if (!this.cookies && !this.csrfToken) {
|
|
219
|
-
return null;
|
|
220
|
-
}
|
|
221
|
-
return {
|
|
222
|
-
cookies: this.cookies,
|
|
223
|
-
csrfToken: this.csrfToken,
|
|
224
|
-
cookieStore: Object.fromEntries(this.cookieStore)
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
/**
|
|
228
|
-
* Set session state manually
|
|
229
|
-
* Allows user to restore session from custom storage (e.g., database, Redis)
|
|
230
|
-
* @param state - Session state with cookies, CSRF token, and cookie store
|
|
231
|
-
*/
|
|
232
|
-
setSessionState(state) {
|
|
233
|
-
this.cookies = state.cookies || null;
|
|
234
|
-
this.csrfToken = state.csrfToken || null;
|
|
235
|
-
this.cookieStore = new Map(Object.entries(state.cookieStore || {}));
|
|
236
|
-
this.logger.debug("Session state set manually", {
|
|
237
|
-
hasCookies: !!this.cookies,
|
|
238
|
-
hasCsrfToken: !!this.csrfToken,
|
|
239
|
-
cookieCount: this.cookieStore.size
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
/**
|
|
243
|
-
* Clear session state from storage
|
|
244
|
-
*/
|
|
245
|
-
async clearSessionState() {
|
|
246
|
-
if (!this.sessionId || !this.sessionStorage) {
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
try {
|
|
250
|
-
await this.sessionStorage.delete(this.sessionId);
|
|
251
|
-
this.logger.debug("Session state cleared", {
|
|
252
|
-
sessionId: this.sessionId
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
catch (error) {
|
|
256
|
-
this.logger.warn("Failed to clear session state", {
|
|
257
|
-
sessionId: this.sessionId,
|
|
258
|
-
error: error instanceof Error ? error.message : String(error)
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
118
|
getConfig() {
|
|
263
119
|
return this.config;
|
|
264
120
|
}
|
|
@@ -302,7 +158,7 @@ class AbstractAbapConnection {
|
|
|
302
158
|
catch (error) {
|
|
303
159
|
// If CSRF token can't be fetched upfront, continue anyway
|
|
304
160
|
// The retry logic will handle CSRF token errors automatically
|
|
305
|
-
this.logger
|
|
161
|
+
this.logger?.debug(`[DEBUG] BaseAbapConnection - Could not fetch CSRF token upfront, will retry on error: ${error instanceof Error ? error.message : String(error)}`);
|
|
306
162
|
}
|
|
307
163
|
}
|
|
308
164
|
}
|
|
@@ -333,10 +189,10 @@ class AbstractAbapConnection {
|
|
|
333
189
|
// Add cookies LAST (MUST NOT be overridden by custom headers)
|
|
334
190
|
if (this.cookies) {
|
|
335
191
|
requestHeaders["Cookie"] = this.cookies;
|
|
336
|
-
this.logger
|
|
192
|
+
this.logger?.debug(`[DEBUG] BaseAbapConnection - Adding cookies to request (first 100 chars): ${this.cookies.substring(0, 100)}...`);
|
|
337
193
|
}
|
|
338
194
|
else {
|
|
339
|
-
this.logger
|
|
195
|
+
this.logger?.debug(`[DEBUG] BaseAbapConnection - NO COOKIES available for this request to ${requestUrl}`);
|
|
340
196
|
}
|
|
341
197
|
if ((normalizedMethod === "POST" || normalizedMethod === "PUT") && data) {
|
|
342
198
|
if (typeof data === "string" && !requestHeaders["Content-Type"]) {
|
|
@@ -359,7 +215,7 @@ class AbstractAbapConnection {
|
|
|
359
215
|
if (data !== undefined) {
|
|
360
216
|
requestConfig.data = data;
|
|
361
217
|
}
|
|
362
|
-
this.logger
|
|
218
|
+
this.logger?.debug(`Executing ${normalizedMethod} request to: ${requestUrl}`, {
|
|
363
219
|
type: "REQUEST_INFO",
|
|
364
220
|
url: requestUrl,
|
|
365
221
|
method: normalizedMethod
|
|
@@ -367,9 +223,7 @@ class AbstractAbapConnection {
|
|
|
367
223
|
try {
|
|
368
224
|
const response = await this.getAxiosInstance()(requestConfig);
|
|
369
225
|
this.updateCookiesFromResponse(response.headers);
|
|
370
|
-
|
|
371
|
-
await this.saveSessionState();
|
|
372
|
-
this.logger.debug(`Request succeeded with status ${response.status}`, {
|
|
226
|
+
this.logger?.debug(`Request succeeded with status ${response.status}`, {
|
|
373
227
|
type: "REQUEST_SUCCESS",
|
|
374
228
|
status: response.status,
|
|
375
229
|
url: requestUrl,
|
|
@@ -393,18 +247,16 @@ class AbstractAbapConnection {
|
|
|
393
247
|
: JSON.stringify(error.response.data).slice(0, 200);
|
|
394
248
|
this.updateCookiesFromResponse(error.response.headers);
|
|
395
249
|
}
|
|
396
|
-
// Save session state even on error (cookies might have been updated)
|
|
397
|
-
await this.saveSessionState();
|
|
398
250
|
// Log 404 as debug (common for existence checks), other errors as error
|
|
399
251
|
if (errorDetails.status === 404) {
|
|
400
|
-
this.logger
|
|
252
|
+
this.logger?.debug(errorDetails.message, errorDetails);
|
|
401
253
|
}
|
|
402
254
|
else {
|
|
403
|
-
this.logger
|
|
255
|
+
this.logger?.error(errorDetails.message, errorDetails);
|
|
404
256
|
}
|
|
405
257
|
// Retry logic for CSRF token errors (403 with CSRF message)
|
|
406
258
|
if (this.shouldRetryCsrf(error)) {
|
|
407
|
-
this.logger
|
|
259
|
+
this.logger?.debug("CSRF token validation failed, fetching new token and retrying request", {
|
|
408
260
|
url: requestUrl,
|
|
409
261
|
method: normalizedMethod
|
|
410
262
|
});
|
|
@@ -417,8 +269,6 @@ class AbstractAbapConnection {
|
|
|
417
269
|
}
|
|
418
270
|
const retryResponse = await this.getAxiosInstance()(requestConfig);
|
|
419
271
|
this.updateCookiesFromResponse(retryResponse.headers);
|
|
420
|
-
// Save session state after retry
|
|
421
|
-
await this.saveSessionState();
|
|
422
272
|
return retryResponse;
|
|
423
273
|
}
|
|
424
274
|
// Retry logic for 401 errors on GET requests (authentication issue - need cookies)
|
|
@@ -430,29 +280,27 @@ class AbstractAbapConnection {
|
|
|
430
280
|
) {
|
|
431
281
|
// If we already have cookies from error response, retry immediately
|
|
432
282
|
if (this.cookies) {
|
|
433
|
-
this.logger
|
|
283
|
+
this.logger?.debug(`[DEBUG] BaseAbapConnection - 401 on GET request, retrying with cookies from error response`);
|
|
434
284
|
requestHeaders["Cookie"] = this.cookies;
|
|
435
285
|
const retryResponse = await this.getAxiosInstance()(requestConfig);
|
|
436
286
|
this.updateCookiesFromResponse(retryResponse.headers);
|
|
437
|
-
await this.saveSessionState();
|
|
438
287
|
return retryResponse;
|
|
439
288
|
}
|
|
440
289
|
// If no cookies, try to get them via CSRF token fetch
|
|
441
|
-
this.logger
|
|
290
|
+
this.logger?.debug(`[DEBUG] BaseAbapConnection - 401 on GET request, attempting to get cookies via CSRF token fetch`);
|
|
442
291
|
try {
|
|
443
292
|
// Try to get CSRF token (this will also get cookies)
|
|
444
293
|
this.csrfToken = await this.fetchCsrfToken(requestUrl, 3, 1000);
|
|
445
294
|
if (this.cookies) {
|
|
446
295
|
requestHeaders["Cookie"] = this.cookies;
|
|
447
|
-
this.logger
|
|
296
|
+
this.logger?.debug(`[DEBUG] BaseAbapConnection - Retrying GET request with cookies from CSRF fetch`);
|
|
448
297
|
const retryResponse = await this.getAxiosInstance()(requestConfig);
|
|
449
298
|
this.updateCookiesFromResponse(retryResponse.headers);
|
|
450
|
-
await this.saveSessionState();
|
|
451
299
|
return retryResponse;
|
|
452
300
|
}
|
|
453
301
|
}
|
|
454
302
|
catch (csrfError) {
|
|
455
|
-
this.logger
|
|
303
|
+
this.logger?.debug(`[DEBUG] BaseAbapConnection - Failed to get CSRF token for 401 retry: ${csrfError instanceof Error ? csrfError.message : String(csrfError)}`);
|
|
456
304
|
// Fall through to throw original error
|
|
457
305
|
}
|
|
458
306
|
}
|
|
@@ -476,11 +324,11 @@ class AbstractAbapConnection {
|
|
|
476
324
|
csrfUrl = `${base}${csrfConfig_js_1.CSRF_CONFIG.ENDPOINT}`;
|
|
477
325
|
}
|
|
478
326
|
// If URL already contains the endpoint, use it as is
|
|
479
|
-
this.logger
|
|
327
|
+
this.logger?.debug(`Fetching CSRF token from: ${csrfUrl}`);
|
|
480
328
|
for (let attempt = 0; attempt <= retryCount; attempt++) {
|
|
481
329
|
try {
|
|
482
330
|
if (attempt > 0) {
|
|
483
|
-
this.logger
|
|
331
|
+
this.logger?.debug(`Retry attempt ${attempt}/${retryCount} for CSRF token`);
|
|
484
332
|
}
|
|
485
333
|
const authHeaders = await this.getAuthHeaders();
|
|
486
334
|
const headers = {
|
|
@@ -491,13 +339,13 @@ class AbstractAbapConnection {
|
|
|
491
339
|
// Even on first attempt, if we have cookies from previous session or error response, use them
|
|
492
340
|
if (this.cookies) {
|
|
493
341
|
headers["Cookie"] = this.cookies;
|
|
494
|
-
this.logger
|
|
342
|
+
this.logger?.debug(`[DEBUG] BaseAbapConnection - Adding cookies to CSRF token request (attempt ${attempt + 1}, first 100 chars): ${this.cookies.substring(0, 100)}...`);
|
|
495
343
|
}
|
|
496
344
|
else {
|
|
497
|
-
this.logger
|
|
345
|
+
this.logger?.debug(`[DEBUG] BaseAbapConnection - No cookies available for CSRF token request (will get fresh cookies from response)`);
|
|
498
346
|
}
|
|
499
347
|
// Log request details for debugging (only if debug logging is enabled)
|
|
500
|
-
this.logger
|
|
348
|
+
this.logger?.debug(`[DEBUG] CSRF Token Request: url=${csrfUrl}, method=GET, hasAuth=${!!authHeaders.Authorization}, hasClient=${!!authHeaders["X-SAP-Client"]}, hasCookies=${!!headers["Cookie"]}, attempt=${attempt + 1}`);
|
|
501
349
|
const response = await this.getAxiosInstance()({
|
|
502
350
|
method: "GET",
|
|
503
351
|
url: csrfUrl,
|
|
@@ -507,7 +355,7 @@ class AbstractAbapConnection {
|
|
|
507
355
|
this.updateCookiesFromResponse(response.headers);
|
|
508
356
|
const token = response.headers["x-csrf-token"];
|
|
509
357
|
if (!token) {
|
|
510
|
-
this.logger
|
|
358
|
+
this.logger?.error("No CSRF token in response headers", {
|
|
511
359
|
headers: response.headers,
|
|
512
360
|
status: response.status
|
|
513
361
|
});
|
|
@@ -520,15 +368,13 @@ class AbstractAbapConnection {
|
|
|
520
368
|
if (response.headers["set-cookie"]) {
|
|
521
369
|
this.updateCookiesFromResponse(response.headers);
|
|
522
370
|
if (this.cookies) {
|
|
523
|
-
this.logger
|
|
524
|
-
this.logger
|
|
371
|
+
this.logger?.debug(`[DEBUG] BaseAbapConnection - Cookies received from CSRF response (first 100 chars): ${this.cookies.substring(0, 100)}...`);
|
|
372
|
+
this.logger?.debug("Cookies extracted from response", {
|
|
525
373
|
cookieLength: this.cookies.length
|
|
526
374
|
});
|
|
527
375
|
}
|
|
528
376
|
}
|
|
529
|
-
|
|
530
|
-
await this.saveSessionState();
|
|
531
|
-
this.logger.debug("CSRF token successfully obtained");
|
|
377
|
+
this.logger?.debug("CSRF token successfully obtained");
|
|
532
378
|
return token;
|
|
533
379
|
}
|
|
534
380
|
catch (error) {
|
|
@@ -538,20 +384,20 @@ class AbstractAbapConnection {
|
|
|
538
384
|
if (error.response?.headers) {
|
|
539
385
|
this.updateCookiesFromResponse(error.response.headers);
|
|
540
386
|
if (this.cookies) {
|
|
541
|
-
this.logger
|
|
387
|
+
this.logger?.debug("Cookies extracted from error response", {
|
|
542
388
|
status: error.response.status,
|
|
543
389
|
cookieLength: this.cookies.length
|
|
544
390
|
});
|
|
545
391
|
}
|
|
546
392
|
}
|
|
547
|
-
this.logger
|
|
393
|
+
this.logger?.error(`CSRF token error: ${error.message}`, {
|
|
548
394
|
url: csrfUrl,
|
|
549
395
|
status: error.response?.status,
|
|
550
396
|
attempt: attempt + 1,
|
|
551
397
|
maxAttempts: retryCount + 1
|
|
552
398
|
});
|
|
553
399
|
if (error.response?.status === 405 && error.response?.headers["x-csrf-token"]) {
|
|
554
|
-
this.logger
|
|
400
|
+
this.logger?.debug("CSRF: SAP returned 405 (Method Not Allowed) — not critical, token found in header");
|
|
555
401
|
const token = error.response.headers["x-csrf-token"];
|
|
556
402
|
if (token) {
|
|
557
403
|
this.updateCookiesFromResponse(error.response.headers);
|
|
@@ -559,13 +405,13 @@ class AbstractAbapConnection {
|
|
|
559
405
|
}
|
|
560
406
|
}
|
|
561
407
|
if (error.response?.headers["x-csrf-token"]) {
|
|
562
|
-
this.logger
|
|
408
|
+
this.logger?.debug(`Got CSRF token despite error (status: ${error.response?.status})`);
|
|
563
409
|
const token = error.response.headers["x-csrf-token"];
|
|
564
410
|
this.updateCookiesFromResponse(error.response.headers);
|
|
565
411
|
return token;
|
|
566
412
|
}
|
|
567
413
|
if (error.response) {
|
|
568
|
-
this.logger
|
|
414
|
+
this.logger?.error("CSRF error details", {
|
|
569
415
|
status: error.response.status,
|
|
570
416
|
statusText: error.response.statusText,
|
|
571
417
|
headers: Object.keys(error.response.headers),
|
|
@@ -575,13 +421,13 @@ class AbstractAbapConnection {
|
|
|
575
421
|
});
|
|
576
422
|
}
|
|
577
423
|
else if (error.request) {
|
|
578
|
-
this.logger
|
|
424
|
+
this.logger?.error("CSRF request error - no response received", {
|
|
579
425
|
request: error.request.path
|
|
580
426
|
});
|
|
581
427
|
}
|
|
582
428
|
}
|
|
583
429
|
else {
|
|
584
|
-
this.logger
|
|
430
|
+
this.logger?.error("CSRF non-axios error", {
|
|
585
431
|
error: error instanceof Error ? error.message : String(error)
|
|
586
432
|
});
|
|
587
433
|
}
|
|
@@ -655,14 +501,14 @@ class AbstractAbapConnection {
|
|
|
655
501
|
return;
|
|
656
502
|
}
|
|
657
503
|
this.cookies = combined;
|
|
658
|
-
this.logger
|
|
504
|
+
this.logger?.debug(`[DEBUG] BaseAbapConnection - Updated cookies from response (first 100 chars): ${this.cookies.substring(0, 100)}...`);
|
|
659
505
|
}
|
|
660
506
|
getAxiosInstance() {
|
|
661
507
|
if (!this.axiosInstance) {
|
|
662
508
|
const rejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED === "1" ||
|
|
663
509
|
(process.env.TLS_REJECT_UNAUTHORIZED === "1" &&
|
|
664
510
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED !== "0");
|
|
665
|
-
this.logger
|
|
511
|
+
this.logger?.debug(`TLS configuration: rejectUnauthorized=${rejectUnauthorized}`);
|
|
666
512
|
this.axiosInstance = axios_1.default.create({
|
|
667
513
|
httpsAgent: new https_1.Agent({
|
|
668
514
|
rejectUnauthorized
|
|
@@ -675,20 +521,20 @@ class AbstractAbapConnection {
|
|
|
675
521
|
// If we already have a CSRF token, reuse it to keep the same SAP session
|
|
676
522
|
// SAP ties the lock handle to the HTTP session (SAP_SESSIONID cookie)
|
|
677
523
|
if (this.csrfToken) {
|
|
678
|
-
this.logger
|
|
524
|
+
this.logger?.debug(`[DEBUG] BaseAbapConnection - Reusing existing CSRF token to maintain session`);
|
|
679
525
|
return;
|
|
680
526
|
}
|
|
681
527
|
try {
|
|
682
|
-
this.logger
|
|
528
|
+
this.logger?.debug(`[DEBUG] BaseAbapConnection - Fetching NEW CSRF token (will create new SAP session)`);
|
|
683
529
|
this.csrfToken = await this.fetchCsrfToken(requestUrl);
|
|
684
530
|
}
|
|
685
531
|
catch (error) {
|
|
686
|
-
// fetchCsrfToken
|
|
532
|
+
// fetchCsrfToken handles auth errors
|
|
687
533
|
// Just re-throw the error with minimal logging to avoid duplicate error messages
|
|
688
534
|
const errorMsg = error instanceof Error ? error.message : csrfConfig_js_1.CSRF_ERROR_MESSAGES.REQUIRED_FOR_MUTATION;
|
|
689
535
|
// Only log at DEBUG level to avoid duplicate error messages
|
|
690
536
|
// (fetchCsrfToken already logged the error at ERROR level if auth failed)
|
|
691
|
-
this.logger
|
|
537
|
+
this.logger?.debug(`[DEBUG] BaseAbapConnection - ensureFreshCsrfToken failed: ${errorMsg}`);
|
|
692
538
|
throw error;
|
|
693
539
|
}
|
|
694
540
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { SapConfig } from "../config/sapConfig.js";
|
|
2
2
|
import { AbstractAbapConnection } from "./AbstractAbapConnection.js";
|
|
3
|
-
import { ILogger
|
|
3
|
+
import { ILogger } from "../logger.js";
|
|
4
4
|
/**
|
|
5
5
|
* Basic Authentication connection for on-premise SAP systems
|
|
6
6
|
*/
|
|
7
7
|
export declare class BaseAbapConnection extends AbstractAbapConnection {
|
|
8
|
-
constructor(config: SapConfig, logger
|
|
8
|
+
constructor(config: SapConfig, logger?: ILogger | null, sessionId?: string);
|
|
9
9
|
/**
|
|
10
10
|
* Connect to SAP system with Basic Auth
|
|
11
11
|
* Fetches CSRF token which also establishes session cookies
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BaseAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/BaseAbapConnection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"BaseAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/BaseAbapConnection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAGvC;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,sBAAsB;gBAE1D,MAAM,EAAE,SAAS,EACjB,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,EACvB,SAAS,CAAC,EAAE,MAAM;IAMpB;;;OAGG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAiC9B,SAAS,CAAC,wBAAwB,IAAI,MAAM;IAQ5C,OAAO,CAAC,MAAM,CAAC,cAAc;CAa9B"}
|
|
@@ -7,9 +7,9 @@ const axios_1 = require("axios");
|
|
|
7
7
|
* Basic Authentication connection for on-premise SAP systems
|
|
8
8
|
*/
|
|
9
9
|
class BaseAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnection {
|
|
10
|
-
constructor(config, logger,
|
|
10
|
+
constructor(config, logger, sessionId) {
|
|
11
11
|
BaseAbapConnection.validateConfig(config);
|
|
12
|
-
super(config, logger
|
|
12
|
+
super(config, logger || null, sessionId);
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* Connect to SAP system with Basic Auth
|
|
@@ -18,14 +18,12 @@ class BaseAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnect
|
|
|
18
18
|
async connect() {
|
|
19
19
|
const baseUrl = await this.getBaseUrl();
|
|
20
20
|
const discoveryUrl = `${baseUrl}/sap/bc/adt/discovery`;
|
|
21
|
-
this.logger
|
|
21
|
+
this.logger?.debug(`[DEBUG] BaseAbapConnection - Connecting to SAP system: ${discoveryUrl}`);
|
|
22
22
|
try {
|
|
23
23
|
// Try to get CSRF token (this will also get cookies)
|
|
24
24
|
const token = await this.fetchCsrfToken(discoveryUrl, 3, 1000);
|
|
25
25
|
this.setCsrfToken(token);
|
|
26
|
-
|
|
27
|
-
await this.saveSessionState();
|
|
28
|
-
this.logger.debug("Successfully connected to SAP system", {
|
|
26
|
+
this.logger?.debug("Successfully connected to SAP system", {
|
|
29
27
|
hasCsrfToken: !!this.getCsrfToken(),
|
|
30
28
|
hasCookies: !!this.getCookies(),
|
|
31
29
|
cookieLength: this.getCookies()?.length || 0
|
|
@@ -35,13 +33,12 @@ class BaseAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnect
|
|
|
35
33
|
// For Basic auth, log warning but don't fail
|
|
36
34
|
// The retry logic in makeAdtRequest will handle transient errors automatically
|
|
37
35
|
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
38
|
-
this.logger
|
|
36
|
+
this.logger?.warn(`[WARN] BaseAbapConnection - Could not connect to SAP system upfront: ${errorMsg}. Will retry on first request.`);
|
|
39
37
|
// Still try to extract cookies from error response if available
|
|
40
38
|
if (error instanceof axios_1.AxiosError && error.response?.headers) {
|
|
41
39
|
// updateCookiesFromResponse is private, but cookies are extracted in fetchCsrfToken
|
|
42
40
|
if (this.getCookies()) {
|
|
43
|
-
this.logger
|
|
44
|
-
await this.saveSessionState();
|
|
41
|
+
this.logger?.debug(`[DEBUG] BaseAbapConnection - Cookies extracted from error response during connect (first 100 chars): ${this.getCookies().substring(0, 100)}...`);
|
|
45
42
|
}
|
|
46
43
|
}
|
|
47
44
|
}
|
|
@@ -1,36 +1,28 @@
|
|
|
1
1
|
import { SapConfig } from "../config/sapConfig.js";
|
|
2
2
|
import { AbstractAbapConnection } from "./AbstractAbapConnection.js";
|
|
3
3
|
import { AbapRequestOptions } from "./AbapConnection.js";
|
|
4
|
-
import { ILogger
|
|
4
|
+
import { ILogger } from "../logger.js";
|
|
5
5
|
import { AxiosResponse } from "axios";
|
|
6
6
|
/**
|
|
7
7
|
* JWT Authentication connection for SAP BTP Cloud systems
|
|
8
|
-
*
|
|
8
|
+
* Note: Token refresh functionality is not supported in this package.
|
|
9
|
+
* Use @mcp-abap-adt/auth-broker for token refresh functionality.
|
|
9
10
|
*/
|
|
10
11
|
export declare class JwtAbapConnection extends AbstractAbapConnection {
|
|
11
|
-
|
|
12
|
-
constructor(config: SapConfig, logger: ILogger, sessionStorage?: ISessionStorage, sessionId?: string);
|
|
12
|
+
constructor(config: SapConfig, logger?: ILogger | null, sessionId?: string);
|
|
13
13
|
protected buildAuthorizationHeader(): string;
|
|
14
|
-
/**
|
|
15
|
-
* Refresh JWT token using refresh token
|
|
16
|
-
* @returns Promise that resolves when token is refreshed
|
|
17
|
-
*/
|
|
18
|
-
refreshToken(): Promise<void>;
|
|
19
|
-
/**
|
|
20
|
-
* Check if token refresh is possible
|
|
21
|
-
*/
|
|
22
|
-
canRefreshToken(): boolean;
|
|
23
14
|
/**
|
|
24
15
|
* Override connect to handle JWT token refresh on errors
|
|
25
16
|
*/
|
|
26
17
|
connect(): Promise<void>;
|
|
27
18
|
/**
|
|
28
|
-
* Override makeAdtRequest to handle JWT
|
|
19
|
+
* Override makeAdtRequest to handle JWT auth errors
|
|
20
|
+
* Note: Token refresh is not supported in connection package - use auth-broker instead
|
|
29
21
|
*/
|
|
30
22
|
makeAdtRequest(options: AbapRequestOptions): Promise<AxiosResponse>;
|
|
31
23
|
/**
|
|
32
|
-
* Override fetchCsrfToken to handle JWT
|
|
33
|
-
*
|
|
24
|
+
* Override fetchCsrfToken to handle JWT auth errors
|
|
25
|
+
* Note: Token refresh is not supported in connection package - use auth-broker instead
|
|
34
26
|
*/
|
|
35
27
|
protected fetchCsrfToken(url: string, retryCount?: number, retryDelay?: number): Promise<string>;
|
|
36
28
|
private static validateConfig;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"JwtAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/JwtAbapConnection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"JwtAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/JwtAbapConnection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAc,aAAa,EAAE,MAAM,OAAO,CAAC;AAElD;;;;GAIG;AACH,qBAAa,iBAAkB,SAAQ,sBAAsB;gBAGzD,MAAM,EAAE,SAAS,EACjB,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,EACvB,SAAS,CAAC,EAAE,MAAM;IAMpB,SAAS,CAAC,wBAAwB,IAAI,MAAM;IAW5C;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IA2C9B;;;OAGG;IACG,cAAc,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,aAAa,CAAC;IAkCzE;;;OAGG;cACa,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,SAAI,EAAE,UAAU,SAAO,GAAG,OAAO,CAAC,MAAM,CAAC;IA8B/F,OAAO,CAAC,MAAM,CAAC,cAAc;CAS9B"}
|