@mcp-abap-adt/connection 0.1.14 → 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 +62 -234
- 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,23 +247,19 @@ 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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
});
|
|
412
|
-
}
|
|
259
|
+
this.logger?.debug("CSRF token validation failed, fetching new token and retrying request", {
|
|
260
|
+
url: requestUrl,
|
|
261
|
+
method: normalizedMethod
|
|
262
|
+
});
|
|
413
263
|
this.csrfToken = await this.fetchCsrfToken(requestUrl, 5, 2000);
|
|
414
264
|
if (this.csrfToken) {
|
|
415
265
|
requestHeaders["x-csrf-token"] = this.csrfToken;
|
|
@@ -419,8 +269,6 @@ class AbstractAbapConnection {
|
|
|
419
269
|
}
|
|
420
270
|
const retryResponse = await this.getAxiosInstance()(requestConfig);
|
|
421
271
|
this.updateCookiesFromResponse(retryResponse.headers);
|
|
422
|
-
// Save session state after retry
|
|
423
|
-
await this.saveSessionState();
|
|
424
272
|
return retryResponse;
|
|
425
273
|
}
|
|
426
274
|
// Retry logic for 401 errors on GET requests (authentication issue - need cookies)
|
|
@@ -432,29 +280,27 @@ class AbstractAbapConnection {
|
|
|
432
280
|
) {
|
|
433
281
|
// If we already have cookies from error response, retry immediately
|
|
434
282
|
if (this.cookies) {
|
|
435
|
-
this.logger
|
|
283
|
+
this.logger?.debug(`[DEBUG] BaseAbapConnection - 401 on GET request, retrying with cookies from error response`);
|
|
436
284
|
requestHeaders["Cookie"] = this.cookies;
|
|
437
285
|
const retryResponse = await this.getAxiosInstance()(requestConfig);
|
|
438
286
|
this.updateCookiesFromResponse(retryResponse.headers);
|
|
439
|
-
await this.saveSessionState();
|
|
440
287
|
return retryResponse;
|
|
441
288
|
}
|
|
442
289
|
// If no cookies, try to get them via CSRF token fetch
|
|
443
|
-
this.logger
|
|
290
|
+
this.logger?.debug(`[DEBUG] BaseAbapConnection - 401 on GET request, attempting to get cookies via CSRF token fetch`);
|
|
444
291
|
try {
|
|
445
292
|
// Try to get CSRF token (this will also get cookies)
|
|
446
293
|
this.csrfToken = await this.fetchCsrfToken(requestUrl, 3, 1000);
|
|
447
294
|
if (this.cookies) {
|
|
448
295
|
requestHeaders["Cookie"] = this.cookies;
|
|
449
|
-
this.logger
|
|
296
|
+
this.logger?.debug(`[DEBUG] BaseAbapConnection - Retrying GET request with cookies from CSRF fetch`);
|
|
450
297
|
const retryResponse = await this.getAxiosInstance()(requestConfig);
|
|
451
298
|
this.updateCookiesFromResponse(retryResponse.headers);
|
|
452
|
-
await this.saveSessionState();
|
|
453
299
|
return retryResponse;
|
|
454
300
|
}
|
|
455
301
|
}
|
|
456
302
|
catch (csrfError) {
|
|
457
|
-
this.logger
|
|
303
|
+
this.logger?.debug(`[DEBUG] BaseAbapConnection - Failed to get CSRF token for 401 retry: ${csrfError instanceof Error ? csrfError.message : String(csrfError)}`);
|
|
458
304
|
// Fall through to throw original error
|
|
459
305
|
}
|
|
460
306
|
}
|
|
@@ -478,13 +324,11 @@ class AbstractAbapConnection {
|
|
|
478
324
|
csrfUrl = `${base}${csrfConfig_js_1.CSRF_CONFIG.ENDPOINT}`;
|
|
479
325
|
}
|
|
480
326
|
// If URL already contains the endpoint, use it as is
|
|
481
|
-
|
|
482
|
-
this.logger.csrfToken("fetch", `Fetching CSRF token from: ${csrfUrl}`);
|
|
483
|
-
}
|
|
327
|
+
this.logger?.debug(`Fetching CSRF token from: ${csrfUrl}`);
|
|
484
328
|
for (let attempt = 0; attempt <= retryCount; attempt++) {
|
|
485
329
|
try {
|
|
486
|
-
if (attempt > 0
|
|
487
|
-
this.logger
|
|
330
|
+
if (attempt > 0) {
|
|
331
|
+
this.logger?.debug(`Retry attempt ${attempt}/${retryCount} for CSRF token`);
|
|
488
332
|
}
|
|
489
333
|
const authHeaders = await this.getAuthHeaders();
|
|
490
334
|
const headers = {
|
|
@@ -495,13 +339,13 @@ class AbstractAbapConnection {
|
|
|
495
339
|
// Even on first attempt, if we have cookies from previous session or error response, use them
|
|
496
340
|
if (this.cookies) {
|
|
497
341
|
headers["Cookie"] = this.cookies;
|
|
498
|
-
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)}...`);
|
|
499
343
|
}
|
|
500
344
|
else {
|
|
501
|
-
this.logger
|
|
345
|
+
this.logger?.debug(`[DEBUG] BaseAbapConnection - No cookies available for CSRF token request (will get fresh cookies from response)`);
|
|
502
346
|
}
|
|
503
347
|
// Log request details for debugging (only if debug logging is enabled)
|
|
504
|
-
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}`);
|
|
505
349
|
const response = await this.getAxiosInstance()({
|
|
506
350
|
method: "GET",
|
|
507
351
|
url: csrfUrl,
|
|
@@ -511,12 +355,10 @@ class AbstractAbapConnection {
|
|
|
511
355
|
this.updateCookiesFromResponse(response.headers);
|
|
512
356
|
const token = response.headers["x-csrf-token"];
|
|
513
357
|
if (!token) {
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
});
|
|
519
|
-
}
|
|
358
|
+
this.logger?.error("No CSRF token in response headers", {
|
|
359
|
+
headers: response.headers,
|
|
360
|
+
status: response.status
|
|
361
|
+
});
|
|
520
362
|
if (attempt < retryCount) {
|
|
521
363
|
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
522
364
|
continue;
|
|
@@ -526,19 +368,13 @@ class AbstractAbapConnection {
|
|
|
526
368
|
if (response.headers["set-cookie"]) {
|
|
527
369
|
this.updateCookiesFromResponse(response.headers);
|
|
528
370
|
if (this.cookies) {
|
|
529
|
-
this.logger
|
|
530
|
-
|
|
531
|
-
this.
|
|
532
|
-
|
|
533
|
-
});
|
|
534
|
-
}
|
|
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", {
|
|
373
|
+
cookieLength: this.cookies.length
|
|
374
|
+
});
|
|
535
375
|
}
|
|
536
376
|
}
|
|
537
|
-
|
|
538
|
-
await this.saveSessionState();
|
|
539
|
-
if (this.logger.csrfToken) {
|
|
540
|
-
this.logger.csrfToken("success", "CSRF token successfully obtained");
|
|
541
|
-
}
|
|
377
|
+
this.logger?.debug("CSRF token successfully obtained");
|
|
542
378
|
return token;
|
|
543
379
|
}
|
|
544
380
|
catch (error) {
|
|
@@ -548,24 +384,20 @@ class AbstractAbapConnection {
|
|
|
548
384
|
if (error.response?.headers) {
|
|
549
385
|
this.updateCookiesFromResponse(error.response.headers);
|
|
550
386
|
if (this.cookies) {
|
|
551
|
-
this.logger
|
|
387
|
+
this.logger?.debug("Cookies extracted from error response", {
|
|
552
388
|
status: error.response.status,
|
|
553
389
|
cookieLength: this.cookies.length
|
|
554
390
|
});
|
|
555
391
|
}
|
|
556
392
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
});
|
|
564
|
-
}
|
|
393
|
+
this.logger?.error(`CSRF token error: ${error.message}`, {
|
|
394
|
+
url: csrfUrl,
|
|
395
|
+
status: error.response?.status,
|
|
396
|
+
attempt: attempt + 1,
|
|
397
|
+
maxAttempts: retryCount + 1
|
|
398
|
+
});
|
|
565
399
|
if (error.response?.status === 405 && error.response?.headers["x-csrf-token"]) {
|
|
566
|
-
|
|
567
|
-
this.logger.csrfToken("retry", "CSRF: SAP returned 405 (Method Not Allowed) — not critical, token found in header");
|
|
568
|
-
}
|
|
400
|
+
this.logger?.debug("CSRF: SAP returned 405 (Method Not Allowed) — not critical, token found in header");
|
|
569
401
|
const token = error.response.headers["x-csrf-token"];
|
|
570
402
|
if (token) {
|
|
571
403
|
this.updateCookiesFromResponse(error.response.headers);
|
|
@@ -573,15 +405,13 @@ class AbstractAbapConnection {
|
|
|
573
405
|
}
|
|
574
406
|
}
|
|
575
407
|
if (error.response?.headers["x-csrf-token"]) {
|
|
576
|
-
|
|
577
|
-
this.logger.csrfToken("success", `Got CSRF token despite error (status: ${error.response?.status})`);
|
|
578
|
-
}
|
|
408
|
+
this.logger?.debug(`Got CSRF token despite error (status: ${error.response?.status})`);
|
|
579
409
|
const token = error.response.headers["x-csrf-token"];
|
|
580
410
|
this.updateCookiesFromResponse(error.response.headers);
|
|
581
411
|
return token;
|
|
582
412
|
}
|
|
583
|
-
if (error.response
|
|
584
|
-
this.logger
|
|
413
|
+
if (error.response) {
|
|
414
|
+
this.logger?.error("CSRF error details", {
|
|
585
415
|
status: error.response.status,
|
|
586
416
|
statusText: error.response.statusText,
|
|
587
417
|
headers: Object.keys(error.response.headers),
|
|
@@ -590,14 +420,14 @@ class AbstractAbapConnection {
|
|
|
590
420
|
: JSON.stringify(error.response.data).slice(0, 200)
|
|
591
421
|
});
|
|
592
422
|
}
|
|
593
|
-
else if (error.request
|
|
594
|
-
this.logger
|
|
423
|
+
else if (error.request) {
|
|
424
|
+
this.logger?.error("CSRF request error - no response received", {
|
|
595
425
|
request: error.request.path
|
|
596
426
|
});
|
|
597
427
|
}
|
|
598
428
|
}
|
|
599
|
-
else
|
|
600
|
-
this.logger
|
|
429
|
+
else {
|
|
430
|
+
this.logger?.error("CSRF non-axios error", {
|
|
601
431
|
error: error instanceof Error ? error.message : String(error)
|
|
602
432
|
});
|
|
603
433
|
}
|
|
@@ -671,16 +501,14 @@ class AbstractAbapConnection {
|
|
|
671
501
|
return;
|
|
672
502
|
}
|
|
673
503
|
this.cookies = combined;
|
|
674
|
-
this.logger
|
|
504
|
+
this.logger?.debug(`[DEBUG] BaseAbapConnection - Updated cookies from response (first 100 chars): ${this.cookies.substring(0, 100)}...`);
|
|
675
505
|
}
|
|
676
506
|
getAxiosInstance() {
|
|
677
507
|
if (!this.axiosInstance) {
|
|
678
508
|
const rejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED === "1" ||
|
|
679
509
|
(process.env.TLS_REJECT_UNAUTHORIZED === "1" &&
|
|
680
510
|
process.env.NODE_TLS_REJECT_UNAUTHORIZED !== "0");
|
|
681
|
-
|
|
682
|
-
this.logger.tlsConfig(rejectUnauthorized);
|
|
683
|
-
}
|
|
511
|
+
this.logger?.debug(`TLS configuration: rejectUnauthorized=${rejectUnauthorized}`);
|
|
684
512
|
this.axiosInstance = axios_1.default.create({
|
|
685
513
|
httpsAgent: new https_1.Agent({
|
|
686
514
|
rejectUnauthorized
|
|
@@ -693,20 +521,20 @@ class AbstractAbapConnection {
|
|
|
693
521
|
// If we already have a CSRF token, reuse it to keep the same SAP session
|
|
694
522
|
// SAP ties the lock handle to the HTTP session (SAP_SESSIONID cookie)
|
|
695
523
|
if (this.csrfToken) {
|
|
696
|
-
this.logger
|
|
524
|
+
this.logger?.debug(`[DEBUG] BaseAbapConnection - Reusing existing CSRF token to maintain session`);
|
|
697
525
|
return;
|
|
698
526
|
}
|
|
699
527
|
try {
|
|
700
|
-
this.logger
|
|
528
|
+
this.logger?.debug(`[DEBUG] BaseAbapConnection - Fetching NEW CSRF token (will create new SAP session)`);
|
|
701
529
|
this.csrfToken = await this.fetchCsrfToken(requestUrl);
|
|
702
530
|
}
|
|
703
531
|
catch (error) {
|
|
704
|
-
// fetchCsrfToken
|
|
532
|
+
// fetchCsrfToken handles auth errors
|
|
705
533
|
// Just re-throw the error with minimal logging to avoid duplicate error messages
|
|
706
534
|
const errorMsg = error instanceof Error ? error.message : csrfConfig_js_1.CSRF_ERROR_MESSAGES.REQUIRED_FOR_MUTATION;
|
|
707
535
|
// Only log at DEBUG level to avoid duplicate error messages
|
|
708
536
|
// (fetchCsrfToken already logged the error at ERROR level if auth failed)
|
|
709
|
-
this.logger
|
|
537
|
+
this.logger?.debug(`[DEBUG] BaseAbapConnection - ensureFreshCsrfToken failed: ${errorMsg}`);
|
|
710
538
|
throw error;
|
|
711
539
|
}
|
|
712
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"}
|