@mcp-abap-adt/connection 0.1.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.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -0
  3. package/bin/sap-abap-auth.js +600 -0
  4. package/dist/config/sapConfig.d.ts +43 -0
  5. package/dist/config/sapConfig.d.ts.map +1 -0
  6. package/dist/config/sapConfig.js +202 -0
  7. package/dist/connection/AbapConnection.d.ts +22 -0
  8. package/dist/connection/AbapConnection.d.ts.map +1 -0
  9. package/dist/connection/AbapConnection.js +2 -0
  10. package/dist/connection/AbstractAbapConnection.d.ts +115 -0
  11. package/dist/connection/AbstractAbapConnection.d.ts.map +1 -0
  12. package/dist/connection/AbstractAbapConnection.js +716 -0
  13. package/dist/connection/BaseAbapConnection.d.ts +17 -0
  14. package/dist/connection/BaseAbapConnection.d.ts.map +1 -0
  15. package/dist/connection/BaseAbapConnection.js +68 -0
  16. package/dist/connection/JwtAbapConnection.d.ts +33 -0
  17. package/dist/connection/JwtAbapConnection.d.ts.map +1 -0
  18. package/dist/connection/JwtAbapConnection.js +305 -0
  19. package/dist/connection/connectionFactory.d.ts +5 -0
  20. package/dist/connection/connectionFactory.d.ts.map +1 -0
  21. package/dist/connection/connectionFactory.js +15 -0
  22. package/dist/index.d.ts +13 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +29 -0
  25. package/dist/logger.d.ts +67 -0
  26. package/dist/logger.d.ts.map +1 -0
  27. package/dist/logger.js +2 -0
  28. package/dist/utils/FileSessionStorage.d.ts +73 -0
  29. package/dist/utils/FileSessionStorage.d.ts.map +1 -0
  30. package/dist/utils/FileSessionStorage.js +191 -0
  31. package/dist/utils/timeouts.d.ts +8 -0
  32. package/dist/utils/timeouts.d.ts.map +1 -0
  33. package/dist/utils/timeouts.js +21 -0
  34. package/dist/utils/tokenRefresh.d.ts +17 -0
  35. package/dist/utils/tokenRefresh.d.ts.map +1 -0
  36. package/dist/utils/tokenRefresh.js +53 -0
  37. package/package.json +63 -0
@@ -0,0 +1,716 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.AbstractAbapConnection = void 0;
37
+ const axios_1 = __importStar(require("axios"));
38
+ const https_1 = require("https");
39
+ const timeouts_js_1 = require("../utils/timeouts.js");
40
+ class AbstractAbapConnection {
41
+ config;
42
+ logger;
43
+ axiosInstance = null;
44
+ csrfToken = null;
45
+ cookies = null;
46
+ cookieStore = new Map();
47
+ cachedBaseUrl = null;
48
+ sessionId = null;
49
+ sessionStorage = null;
50
+ sessionMode = "stateless";
51
+ constructor(config, logger, sessionStorage, sessionId) {
52
+ this.config = config;
53
+ this.logger = logger;
54
+ this.sessionStorage = sessionStorage || null;
55
+ this.sessionId = sessionId || null;
56
+ this.sessionMode = sessionId && sessionStorage ? "stateful" : "stateless";
57
+ }
58
+ /**
59
+ * Enable stateful session mode with storage
60
+ * @param sessionId - Unique session identifier
61
+ * @param storage - Storage implementation for persisting session state
62
+ */
63
+ async enableStatefulSession(sessionId, storage) {
64
+ this.sessionId = sessionId;
65
+ this.sessionStorage = storage;
66
+ this.sessionMode = "stateful";
67
+ // Try to load existing session state
68
+ await this.loadSessionState();
69
+ this.logger.debug("Stateful session enabled", {
70
+ sessionId,
71
+ hasExistingState: !!this.csrfToken || !!this.cookies
72
+ });
73
+ }
74
+ /**
75
+ * Disable stateful session mode (switch to stateless)
76
+ * Optionally saves current state before switching
77
+ */
78
+ async disableStatefulSession(saveBeforeDisable = false) {
79
+ if (this.sessionMode === "stateless") {
80
+ return;
81
+ }
82
+ if (saveBeforeDisable && this.sessionId && this.sessionStorage) {
83
+ await this.saveSessionState();
84
+ }
85
+ this.sessionMode = "stateless";
86
+ this.sessionId = null;
87
+ this.sessionStorage = null;
88
+ this.logger.debug("Stateful session disabled", {
89
+ savedBeforeDisable: saveBeforeDisable
90
+ });
91
+ }
92
+ /**
93
+ * Get current session mode
94
+ */
95
+ getSessionMode() {
96
+ return this.sessionMode;
97
+ }
98
+ /**
99
+ * Set session ID for stateful operations
100
+ * When session ID is set, session state (cookies, CSRF token) will be persisted
101
+ * @deprecated Use enableStatefulSession() instead
102
+ */
103
+ setSessionId(sessionId) {
104
+ if (this.sessionStorage) {
105
+ this.sessionId = sessionId;
106
+ this.sessionMode = "stateful";
107
+ }
108
+ else {
109
+ this.logger.warn("Cannot set session ID without session storage. Use enableStatefulSession() instead.");
110
+ }
111
+ }
112
+ /**
113
+ * Get current session ID
114
+ */
115
+ getSessionId() {
116
+ return this.sessionId;
117
+ }
118
+ /**
119
+ * Set session storage (can be changed at runtime)
120
+ */
121
+ setSessionStorage(storage) {
122
+ this.sessionStorage = storage;
123
+ if (storage && this.sessionId) {
124
+ this.sessionMode = "stateful";
125
+ }
126
+ else if (!storage) {
127
+ this.sessionMode = "stateless";
128
+ }
129
+ }
130
+ /**
131
+ * Get current session storage
132
+ */
133
+ getSessionStorage() {
134
+ return this.sessionStorage;
135
+ }
136
+ /**
137
+ * Load session state from storage
138
+ */
139
+ async loadSessionState() {
140
+ if (!this.sessionId || !this.sessionStorage) {
141
+ return;
142
+ }
143
+ try {
144
+ const state = await this.sessionStorage.load(this.sessionId);
145
+ if (state) {
146
+ this.csrfToken = state.csrfToken;
147
+ this.cookies = state.cookies;
148
+ this.cookieStore = new Map(Object.entries(state.cookieStore));
149
+ this.logger.debug("Session state loaded", {
150
+ sessionId: this.sessionId,
151
+ hasCookies: !!this.cookies,
152
+ hasCsrfToken: !!this.csrfToken
153
+ });
154
+ }
155
+ }
156
+ catch (error) {
157
+ this.logger.warn("Failed to load session state", {
158
+ sessionId: this.sessionId,
159
+ error: error instanceof Error ? error.message : String(error)
160
+ });
161
+ }
162
+ }
163
+ /**
164
+ * Save session state to storage
165
+ * Only saves if in stateful mode
166
+ */
167
+ async saveSessionState() {
168
+ if (this.sessionMode !== "stateful" || !this.sessionId || !this.sessionStorage) {
169
+ return;
170
+ }
171
+ try {
172
+ const state = {
173
+ cookies: this.cookies,
174
+ csrfToken: this.csrfToken,
175
+ cookieStore: Object.fromEntries(this.cookieStore)
176
+ };
177
+ await this.sessionStorage.save(this.sessionId, state);
178
+ this.logger.debug("Session state saved", {
179
+ sessionId: this.sessionId,
180
+ hasCookies: !!this.cookies,
181
+ hasCsrfToken: !!this.csrfToken
182
+ });
183
+ }
184
+ catch (error) {
185
+ this.logger.warn("Failed to save session state", {
186
+ sessionId: this.sessionId,
187
+ error: error instanceof Error ? error.message : String(error)
188
+ });
189
+ }
190
+ }
191
+ /**
192
+ * Get current session state
193
+ * Returns cookies, CSRF token, and cookie store for manual persistence
194
+ * @returns Current session state or null if no session data
195
+ */
196
+ getSessionState() {
197
+ if (!this.cookies && !this.csrfToken) {
198
+ return null;
199
+ }
200
+ return {
201
+ cookies: this.cookies,
202
+ csrfToken: this.csrfToken,
203
+ cookieStore: Object.fromEntries(this.cookieStore)
204
+ };
205
+ }
206
+ /**
207
+ * Set session state manually
208
+ * Allows user to restore session from custom storage (e.g., database, Redis)
209
+ * @param state - Session state with cookies, CSRF token, and cookie store
210
+ */
211
+ setSessionState(state) {
212
+ this.cookies = state.cookies || null;
213
+ this.csrfToken = state.csrfToken || null;
214
+ this.cookieStore = new Map(Object.entries(state.cookieStore || {}));
215
+ this.logger.debug("Session state set manually", {
216
+ hasCookies: !!this.cookies,
217
+ hasCsrfToken: !!this.csrfToken,
218
+ cookieCount: this.cookieStore.size
219
+ });
220
+ }
221
+ /**
222
+ * Clear session state from storage
223
+ */
224
+ async clearSessionState() {
225
+ if (!this.sessionId || !this.sessionStorage) {
226
+ return;
227
+ }
228
+ try {
229
+ await this.sessionStorage.delete(this.sessionId);
230
+ this.logger.debug("Session state cleared", {
231
+ sessionId: this.sessionId
232
+ });
233
+ }
234
+ catch (error) {
235
+ this.logger.warn("Failed to clear session state", {
236
+ sessionId: this.sessionId,
237
+ error: error instanceof Error ? error.message : String(error)
238
+ });
239
+ }
240
+ }
241
+ getConfig() {
242
+ return this.config;
243
+ }
244
+ reset() {
245
+ if (this.axiosInstance) {
246
+ this.axiosInstance.interceptors.request.clear();
247
+ this.axiosInstance.interceptors.response.clear();
248
+ this.axiosInstance = null;
249
+ }
250
+ this.csrfToken = null;
251
+ this.cookies = null;
252
+ this.cookieStore.clear();
253
+ this.cachedBaseUrl = null;
254
+ }
255
+ async getBaseUrl() {
256
+ if (this.cachedBaseUrl) {
257
+ return this.cachedBaseUrl;
258
+ }
259
+ const { url } = this.config;
260
+ try {
261
+ const urlObj = new URL(url);
262
+ this.cachedBaseUrl = urlObj.origin;
263
+ return this.cachedBaseUrl;
264
+ }
265
+ catch (error) {
266
+ const errorMessage = `Invalid URL in configuration: ${error instanceof Error ? error.message : error}`;
267
+ throw new Error(errorMessage);
268
+ }
269
+ }
270
+ async getAuthHeaders() {
271
+ const headers = {};
272
+ if (this.config.client) {
273
+ headers["X-SAP-Client"] = this.config.client;
274
+ }
275
+ const authorization = this.buildAuthorizationHeader();
276
+ if (authorization) {
277
+ headers["Authorization"] = authorization;
278
+ }
279
+ return headers;
280
+ }
281
+ async makeAdtRequest(options) {
282
+ const { url, method, timeout, data, params, headers: customHeaders } = options;
283
+ const normalizedMethod = method.toUpperCase();
284
+ const requestUrl = this.normalizeRequestUrl(url);
285
+ // Try to ensure CSRF token is available for POST/PUT/DELETE, but don't fail if it can't be fetched
286
+ // The retry logic will handle CSRF token errors automatically
287
+ if (normalizedMethod === "POST" || normalizedMethod === "PUT" || normalizedMethod === "DELETE") {
288
+ if (!this.csrfToken) {
289
+ try {
290
+ await this.ensureFreshCsrfToken(requestUrl);
291
+ }
292
+ catch (error) {
293
+ // If CSRF token can't be fetched upfront, continue anyway
294
+ // The retry logic will handle CSRF token errors automatically
295
+ this.logger.debug(`[DEBUG] BaseAbapConnection - Could not fetch CSRF token upfront, will retry on error: ${error instanceof Error ? error.message : String(error)}`);
296
+ }
297
+ }
298
+ }
299
+ // Start with default Accept header
300
+ const requestHeaders = {};
301
+ if (!customHeaders || !customHeaders["Accept"]) {
302
+ requestHeaders["Accept"] = "application/xml, application/json, text/plain, */*";
303
+ }
304
+ // Add custom headers (but they won't override auth/cookies)
305
+ if (customHeaders) {
306
+ Object.assign(requestHeaders, customHeaders);
307
+ }
308
+ // Add auth headers (these MUST NOT be overridden)
309
+ Object.assign(requestHeaders, await this.getAuthHeaders());
310
+ if ((normalizedMethod === "POST" || normalizedMethod === "PUT" || normalizedMethod === "DELETE") && this.csrfToken) {
311
+ requestHeaders["x-csrf-token"] = this.csrfToken;
312
+ }
313
+ // Add cookies LAST (MUST NOT be overridden by custom headers)
314
+ if (this.cookies) {
315
+ requestHeaders["Cookie"] = this.cookies;
316
+ this.logger.debug(`[DEBUG] BaseAbapConnection - Adding cookies to request (first 100 chars): ${this.cookies.substring(0, 100)}...`);
317
+ }
318
+ else {
319
+ this.logger.debug(`[DEBUG] BaseAbapConnection - NO COOKIES available for this request to ${requestUrl}`);
320
+ }
321
+ if ((normalizedMethod === "POST" || normalizedMethod === "PUT") && data) {
322
+ if (typeof data === "string" && !requestHeaders["Content-Type"]) {
323
+ if (requestUrl.includes("/usageReferences") && data.includes("usageReferenceRequest")) {
324
+ requestHeaders["Content-Type"] = "application/vnd.sap.adt.repository.usagereferences.request.v1+xml";
325
+ requestHeaders["Accept"] = "application/vnd.sap.adt.repository.usagereferences.result.v1+xml";
326
+ }
327
+ else {
328
+ requestHeaders["Content-Type"] = "text/plain; charset=utf-8";
329
+ }
330
+ }
331
+ }
332
+ const requestConfig = {
333
+ method: normalizedMethod,
334
+ url: requestUrl,
335
+ headers: requestHeaders,
336
+ timeout,
337
+ params
338
+ };
339
+ if (data !== undefined) {
340
+ requestConfig.data = data;
341
+ }
342
+ this.logger.debug(`Executing ${normalizedMethod} request to: ${requestUrl}`, {
343
+ type: "REQUEST_INFO",
344
+ url: requestUrl,
345
+ method: normalizedMethod
346
+ });
347
+ try {
348
+ const response = await this.getAxiosInstance()(requestConfig);
349
+ this.updateCookiesFromResponse(response.headers);
350
+ // Save session state after successful request (if session storage is configured)
351
+ await this.saveSessionState();
352
+ this.logger.debug(`Request succeeded with status ${response.status}`, {
353
+ type: "REQUEST_SUCCESS",
354
+ status: response.status,
355
+ url: requestUrl,
356
+ method: normalizedMethod
357
+ });
358
+ return response;
359
+ }
360
+ catch (error) {
361
+ const errorDetails = {
362
+ type: "REQUEST_ERROR",
363
+ message: error instanceof Error ? error.message : String(error),
364
+ url: requestUrl,
365
+ method: normalizedMethod,
366
+ status: error instanceof axios_1.AxiosError ? error.response?.status : undefined,
367
+ data: undefined
368
+ };
369
+ if (error instanceof axios_1.AxiosError && error.response) {
370
+ errorDetails.data =
371
+ typeof error.response.data === "string"
372
+ ? error.response.data.slice(0, 200)
373
+ : JSON.stringify(error.response.data).slice(0, 200);
374
+ this.updateCookiesFromResponse(error.response.headers);
375
+ }
376
+ // Save session state even on error (cookies might have been updated)
377
+ await this.saveSessionState();
378
+ // Log 404 as debug (common for existence checks), other errors as error
379
+ if (errorDetails.status === 404) {
380
+ this.logger.debug(errorDetails.message, errorDetails);
381
+ }
382
+ else {
383
+ this.logger.error(errorDetails.message, errorDetails);
384
+ }
385
+ // Retry logic for CSRF token errors (403 with CSRF message)
386
+ if (this.shouldRetryCsrf(error)) {
387
+ if (this.logger.csrfToken) {
388
+ this.logger.csrfToken("retry", "CSRF token validation failed, fetching new token and retrying request", {
389
+ url: requestUrl,
390
+ method: normalizedMethod
391
+ });
392
+ }
393
+ this.csrfToken = await this.fetchCsrfToken(requestUrl, 5, 2000);
394
+ if (this.csrfToken) {
395
+ requestHeaders["x-csrf-token"] = this.csrfToken;
396
+ }
397
+ if (this.cookies) {
398
+ requestHeaders["Cookie"] = this.cookies;
399
+ }
400
+ const retryResponse = await this.getAxiosInstance()(requestConfig);
401
+ this.updateCookiesFromResponse(retryResponse.headers);
402
+ // Save session state after retry
403
+ await this.saveSessionState();
404
+ return retryResponse;
405
+ }
406
+ // Retry logic for 401 errors on GET requests (authentication issue - need cookies)
407
+ // Only for basic auth - JWT auth will be handled by refresh logic below
408
+ if (error instanceof axios_1.AxiosError &&
409
+ error.response?.status === 401 &&
410
+ normalizedMethod === "GET" &&
411
+ this.config.authType === 'basic' // Only for basic auth
412
+ ) {
413
+ // If we already have cookies from error response, retry immediately
414
+ if (this.cookies) {
415
+ this.logger.debug(`[DEBUG] BaseAbapConnection - 401 on GET request, retrying with cookies from error response`);
416
+ requestHeaders["Cookie"] = this.cookies;
417
+ const retryResponse = await this.getAxiosInstance()(requestConfig);
418
+ this.updateCookiesFromResponse(retryResponse.headers);
419
+ await this.saveSessionState();
420
+ return retryResponse;
421
+ }
422
+ // If no cookies, try to get them via CSRF token fetch
423
+ this.logger.debug(`[DEBUG] BaseAbapConnection - 401 on GET request, attempting to get cookies via CSRF token fetch`);
424
+ try {
425
+ // Try to get CSRF token (this will also get cookies)
426
+ this.csrfToken = await this.fetchCsrfToken(requestUrl, 3, 1000);
427
+ if (this.cookies) {
428
+ requestHeaders["Cookie"] = this.cookies;
429
+ this.logger.debug(`[DEBUG] BaseAbapConnection - Retrying GET request with cookies from CSRF fetch`);
430
+ const retryResponse = await this.getAxiosInstance()(requestConfig);
431
+ this.updateCookiesFromResponse(retryResponse.headers);
432
+ await this.saveSessionState();
433
+ return retryResponse;
434
+ }
435
+ }
436
+ catch (csrfError) {
437
+ this.logger.debug(`[DEBUG] BaseAbapConnection - Failed to get CSRF token for 401 retry: ${csrfError instanceof Error ? csrfError.message : String(csrfError)}`);
438
+ // Fall through to throw original error
439
+ }
440
+ }
441
+ throw error;
442
+ }
443
+ }
444
+ /**
445
+ * Fetch CSRF token from SAP system
446
+ * Protected method for use by concrete implementations in their connect() method
447
+ */
448
+ async fetchCsrfToken(url, retryCount = 3, retryDelay = 1000) {
449
+ let csrfUrl = url;
450
+ if (!url.includes("/sap/bc/adt/")) {
451
+ csrfUrl = url.endsWith("/") ? `${url}sap/bc/adt/discovery` : `${url}/sap/bc/adt/discovery`;
452
+ }
453
+ else if (!url.includes("/sap/bc/adt/discovery")) {
454
+ const base = url.split("/sap/bc/adt")[0];
455
+ csrfUrl = `${base}/sap/bc/adt/discovery`;
456
+ }
457
+ if (this.logger.csrfToken) {
458
+ this.logger.csrfToken("fetch", `Fetching CSRF token from: ${csrfUrl}`);
459
+ }
460
+ for (let attempt = 0; attempt <= retryCount; attempt++) {
461
+ try {
462
+ if (attempt > 0 && this.logger.csrfToken) {
463
+ this.logger.csrfToken("retry", `Retry attempt ${attempt}/${retryCount} for CSRF token`);
464
+ }
465
+ const authHeaders = await this.getAuthHeaders();
466
+ const headers = {
467
+ ...authHeaders,
468
+ "x-csrf-token": "fetch",
469
+ Accept: "application/atomsvc+xml"
470
+ };
471
+ // Always add cookies if available - they are needed for session continuity
472
+ // Even on first attempt, if we have cookies from previous session or error response, use them
473
+ if (this.cookies) {
474
+ headers["Cookie"] = this.cookies;
475
+ this.logger.debug(`[DEBUG] BaseAbapConnection - Adding cookies to CSRF token request (attempt ${attempt + 1}, first 100 chars): ${this.cookies.substring(0, 100)}...`);
476
+ }
477
+ else {
478
+ this.logger.debug(`[DEBUG] BaseAbapConnection - No cookies available for CSRF token request (will get fresh cookies from response)`);
479
+ }
480
+ // Log request details for debugging (only if debug logging is enabled)
481
+ 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}`);
482
+ const response = await this.getAxiosInstance()({
483
+ method: "GET",
484
+ url: csrfUrl,
485
+ headers,
486
+ timeout: (0, timeouts_js_1.getTimeout)("csrf")
487
+ });
488
+ this.updateCookiesFromResponse(response.headers);
489
+ const token = response.headers["x-csrf-token"];
490
+ if (!token) {
491
+ if (this.logger.csrfToken) {
492
+ this.logger.csrfToken("error", "No CSRF token in response headers", {
493
+ headers: response.headers,
494
+ status: response.status
495
+ });
496
+ }
497
+ if (attempt < retryCount) {
498
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
499
+ continue;
500
+ }
501
+ throw new Error("No CSRF token in response headers");
502
+ }
503
+ if (response.headers["set-cookie"]) {
504
+ this.updateCookiesFromResponse(response.headers);
505
+ if (this.cookies) {
506
+ this.logger.debug(`[DEBUG] BaseAbapConnection - Cookies received from CSRF response (first 100 chars): ${this.cookies.substring(0, 100)}...`);
507
+ if (this.logger.csrfToken) {
508
+ this.logger.csrfToken("success", "Cookies extracted from response", {
509
+ cookieLength: this.cookies.length
510
+ });
511
+ }
512
+ }
513
+ }
514
+ // Save session state after CSRF token fetch (cookies and token are now available)
515
+ await this.saveSessionState();
516
+ if (this.logger.csrfToken) {
517
+ this.logger.csrfToken("success", "CSRF token successfully obtained");
518
+ }
519
+ return token;
520
+ }
521
+ catch (error) {
522
+ if (error instanceof axios_1.AxiosError) {
523
+ // Always try to extract cookies from error response, even on 401
524
+ // This ensures cookies are available for subsequent requests
525
+ if (error.response?.headers) {
526
+ this.updateCookiesFromResponse(error.response.headers);
527
+ if (this.cookies) {
528
+ this.logger.debug("Cookies extracted from error response", {
529
+ status: error.response.status,
530
+ cookieLength: this.cookies.length
531
+ });
532
+ }
533
+ }
534
+ if (this.logger.csrfToken) {
535
+ this.logger.csrfToken("error", `CSRF token error: ${error.message}`, {
536
+ url: csrfUrl,
537
+ status: error.response?.status,
538
+ attempt: attempt + 1,
539
+ maxAttempts: retryCount + 1
540
+ });
541
+ }
542
+ if (error.response?.status === 405 && error.response?.headers["x-csrf-token"]) {
543
+ if (this.logger.csrfToken) {
544
+ this.logger.csrfToken("retry", "CSRF: SAP returned 405 (Method Not Allowed) — not critical, token found in header");
545
+ }
546
+ const token = error.response.headers["x-csrf-token"];
547
+ if (token) {
548
+ this.updateCookiesFromResponse(error.response.headers);
549
+ return token;
550
+ }
551
+ }
552
+ if (error.response?.headers["x-csrf-token"]) {
553
+ if (this.logger.csrfToken) {
554
+ this.logger.csrfToken("success", `Got CSRF token despite error (status: ${error.response?.status})`);
555
+ }
556
+ const token = error.response.headers["x-csrf-token"];
557
+ this.updateCookiesFromResponse(error.response.headers);
558
+ return token;
559
+ }
560
+ if (error.response && this.logger.csrfToken) {
561
+ this.logger.csrfToken("error", "CSRF error details", {
562
+ status: error.response.status,
563
+ statusText: error.response.statusText,
564
+ headers: Object.keys(error.response.headers),
565
+ data: typeof error.response.data === "string"
566
+ ? error.response.data.slice(0, 200)
567
+ : JSON.stringify(error.response.data).slice(0, 200)
568
+ });
569
+ }
570
+ else if (error.request && this.logger.csrfToken) {
571
+ this.logger.csrfToken("error", "CSRF request error - no response received", {
572
+ request: error.request.path
573
+ });
574
+ }
575
+ }
576
+ else if (this.logger.csrfToken) {
577
+ this.logger.csrfToken("error", "CSRF non-axios error", {
578
+ error: error instanceof Error ? error.message : String(error)
579
+ });
580
+ }
581
+ if (attempt < retryCount) {
582
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
583
+ continue;
584
+ }
585
+ // Preserve original error information, especially AxiosError with response
586
+ if (error instanceof axios_1.AxiosError && error.response) {
587
+ // Re-throw the original AxiosError to preserve response information
588
+ throw error;
589
+ }
590
+ throw new Error(`Failed to fetch CSRF token after ${retryCount + 1} attempts: ${error instanceof Error ? error.message : String(error)}`);
591
+ }
592
+ }
593
+ throw new Error("CSRF token fetch failed unexpectedly");
594
+ }
595
+ /**
596
+ * Get CSRF token (protected for use by subclasses)
597
+ */
598
+ getCsrfToken() {
599
+ return this.csrfToken;
600
+ }
601
+ /**
602
+ * Set CSRF token (protected for use by subclasses)
603
+ */
604
+ setCsrfToken(token) {
605
+ this.csrfToken = token;
606
+ }
607
+ /**
608
+ * Get cookies (protected for use by subclasses)
609
+ */
610
+ getCookies() {
611
+ return this.cookies;
612
+ }
613
+ updateCookiesFromResponse(headers) {
614
+ if (!headers) {
615
+ return;
616
+ }
617
+ const setCookie = headers["set-cookie"];
618
+ if (!setCookie) {
619
+ return;
620
+ }
621
+ const cookiesArray = Array.isArray(setCookie) ? setCookie : [setCookie];
622
+ for (const entry of cookiesArray) {
623
+ if (typeof entry !== "string") {
624
+ continue;
625
+ }
626
+ const [nameValue] = entry.split(";");
627
+ if (!nameValue) {
628
+ continue;
629
+ }
630
+ const [name, ...rest] = nameValue.split("=");
631
+ if (!name) {
632
+ continue;
633
+ }
634
+ const trimmedName = name.trim();
635
+ const trimmedValue = rest.join("=").trim();
636
+ if (!trimmedName) {
637
+ continue;
638
+ }
639
+ this.cookieStore.set(trimmedName, trimmedValue);
640
+ }
641
+ if (this.cookieStore.size === 0) {
642
+ return;
643
+ }
644
+ const combined = Array.from(this.cookieStore.entries())
645
+ .map(([name, value]) => (value ? `${name}=${value}` : name))
646
+ .join("; ");
647
+ if (!combined) {
648
+ return;
649
+ }
650
+ this.cookies = combined;
651
+ this.logger.debug(`[DEBUG] BaseAbapConnection - Updated cookies from response (first 100 chars): ${this.cookies.substring(0, 100)}...`);
652
+ }
653
+ getAxiosInstance() {
654
+ if (!this.axiosInstance) {
655
+ const rejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED === "1" ||
656
+ (process.env.TLS_REJECT_UNAUTHORIZED === "1" &&
657
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED !== "0");
658
+ if (this.logger.tlsConfig) {
659
+ this.logger.tlsConfig(rejectUnauthorized);
660
+ }
661
+ this.axiosInstance = axios_1.default.create({
662
+ httpsAgent: new https_1.Agent({
663
+ rejectUnauthorized
664
+ })
665
+ });
666
+ }
667
+ return this.axiosInstance;
668
+ }
669
+ normalizeRequestUrl(url) {
670
+ if (!url.includes("/sap/bc/adt/") && !url.endsWith("/sap/bc/adt")) {
671
+ return url.endsWith("/") ? `${url}sap/bc/adt` : `${url}/sap/bc/adt`;
672
+ }
673
+ return url;
674
+ }
675
+ async ensureFreshCsrfToken(requestUrl) {
676
+ // If we already have a CSRF token, reuse it to keep the same SAP session
677
+ // SAP ties the lock handle to the HTTP session (SAP_SESSIONID cookie)
678
+ if (this.csrfToken) {
679
+ this.logger.debug(`[DEBUG] BaseAbapConnection - Reusing existing CSRF token to maintain session`);
680
+ return;
681
+ }
682
+ try {
683
+ this.logger.debug(`[DEBUG] BaseAbapConnection - Fetching NEW CSRF token (will create new SAP session)`);
684
+ this.csrfToken = await this.fetchCsrfToken(requestUrl);
685
+ }
686
+ catch (error) {
687
+ // fetchCsrfToken already handles auth errors and auto-refresh
688
+ // Just re-throw the error with minimal logging to avoid duplicate error messages
689
+ const errorMsg = error instanceof Error ? error.message : "CSRF token is required for POST/PUT requests but could not be fetched";
690
+ // Only log at DEBUG level to avoid duplicate error messages
691
+ // (fetchCsrfToken already logged the error at ERROR level if auth failed)
692
+ this.logger.debug(`[DEBUG] BaseAbapConnection - ensureFreshCsrfToken failed: ${errorMsg}`);
693
+ throw error;
694
+ }
695
+ }
696
+ shouldRetryCsrf(error) {
697
+ if (!(error instanceof axios_1.AxiosError)) {
698
+ return false;
699
+ }
700
+ const responseData = error.response?.data;
701
+ const responseText = typeof responseData === "string" ? responseData : JSON.stringify(responseData || "");
702
+ // Don't retry for JWT auth - refresh logic will handle it
703
+ if (this.config.authType === 'jwt') {
704
+ return false;
705
+ }
706
+ // Retry on 403 with CSRF message, or if response mentions CSRF token
707
+ // Also retry on 401 for POST/PUT/DELETE if we don't have CSRF token yet (might need to get cookies first)
708
+ const method = error.config?.method?.toUpperCase();
709
+ const isPostPutDelete = method && ["POST", "PUT", "DELETE"].includes(method);
710
+ const needsCsrfToken = !!isPostPutDelete && !this.csrfToken;
711
+ return ((!!error.response && error.response.status === 403 && responseText.includes("CSRF")) ||
712
+ responseText.includes("CSRF token") ||
713
+ (needsCsrfToken && error.response?.status === 401));
714
+ }
715
+ }
716
+ exports.AbstractAbapConnection = AbstractAbapConnection;