@babamba2/mcp-abap-connection 1.5.3

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 (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +522 -0
  3. package/bin/sap-abap-auth.js +665 -0
  4. package/dist/config/sapConfig.d.ts +5 -0
  5. package/dist/config/sapConfig.d.ts.map +1 -0
  6. package/dist/config/sapConfig.js +26 -0
  7. package/dist/connection/AbapConnection.d.ts +4 -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 +89 -0
  11. package/dist/connection/AbstractAbapConnection.d.ts.map +1 -0
  12. package/dist/connection/AbstractAbapConnection.js +612 -0
  13. package/dist/connection/BaseAbapConnection.d.ts +19 -0
  14. package/dist/connection/BaseAbapConnection.d.ts.map +1 -0
  15. package/dist/connection/BaseAbapConnection.js +65 -0
  16. package/dist/connection/GenericWebSocketTransport.d.ts +46 -0
  17. package/dist/connection/GenericWebSocketTransport.d.ts.map +1 -0
  18. package/dist/connection/GenericWebSocketTransport.js +135 -0
  19. package/dist/connection/JwtAbapConnection.d.ts +37 -0
  20. package/dist/connection/JwtAbapConnection.d.ts.map +1 -0
  21. package/dist/connection/JwtAbapConnection.js +181 -0
  22. package/dist/connection/RfcAbapConnection.d.ts +49 -0
  23. package/dist/connection/RfcAbapConnection.d.ts.map +1 -0
  24. package/dist/connection/RfcAbapConnection.js +322 -0
  25. package/dist/connection/SamlAbapConnection.d.ts +21 -0
  26. package/dist/connection/SamlAbapConnection.d.ts.map +1 -0
  27. package/dist/connection/SamlAbapConnection.js +65 -0
  28. package/dist/connection/connectionFactory.d.ts +8 -0
  29. package/dist/connection/connectionFactory.d.ts.map +1 -0
  30. package/dist/connection/connectionFactory.js +23 -0
  31. package/dist/connection/csrfConfig.d.ts +46 -0
  32. package/dist/connection/csrfConfig.d.ts.map +1 -0
  33. package/dist/connection/csrfConfig.js +48 -0
  34. package/dist/index.d.ts +14 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +32 -0
  37. package/dist/logger.d.ts +3 -0
  38. package/dist/logger.d.ts.map +1 -0
  39. package/dist/logger.js +2 -0
  40. package/dist/utils/timeouts.d.ts +5 -0
  41. package/dist/utils/timeouts.d.ts.map +1 -0
  42. package/dist/utils/timeouts.js +21 -0
  43. package/dist/utils/tokenRefresh.d.ts +15 -0
  44. package/dist/utils/tokenRefresh.d.ts.map +1 -0
  45. package/dist/utils/tokenRefresh.js +55 -0
  46. package/package.json +70 -0
@@ -0,0 +1,612 @@
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 node_crypto_1 = require("node:crypto");
38
+ const node_https_1 = require("node:https");
39
+ const mcp_abap_adt_interfaces_1 = require("@babamba2/mcp-abap-adt-interfaces");
40
+ const axios_1 = __importStar(require("axios"));
41
+ const timeouts_js_1 = require("../utils/timeouts.js");
42
+ const csrfConfig_js_1 = require("./csrfConfig.js");
43
+ class AbstractAbapConnection {
44
+ config;
45
+ logger;
46
+ axiosInstance = null;
47
+ csrfToken = null;
48
+ cookies = null;
49
+ cookieStore = new Map();
50
+ baseUrl;
51
+ sessionId = null;
52
+ sessionMode = 'stateless';
53
+ skipSessionType;
54
+ constructor(config, logger, sessionId, options) {
55
+ this.config = config;
56
+ this.logger = logger;
57
+ this.skipSessionType = options?.skipSessionType ?? false;
58
+ // Generate sessionId (used for sap-adt-connection-id header)
59
+ this.sessionId = sessionId || (0, node_crypto_1.randomUUID)();
60
+ // Initialize baseUrl from config (required, will throw if invalid)
61
+ try {
62
+ const urlObj = new URL(config.url);
63
+ this.baseUrl = urlObj.origin;
64
+ }
65
+ catch (error) {
66
+ throw new Error(`Invalid URL in configuration: ${error instanceof Error ? error.message : error}`);
67
+ }
68
+ this.logger?.debug(`AbstractAbapConnection - Session ID: ${this.sessionId.substring(0, 8)}...`);
69
+ }
70
+ /**
71
+ * Set session type (stateful or stateless)
72
+ * Controls whether x-sap-adt-sessiontype: stateful header is added to requests
73
+ * - stateful: SAP maintains session state between requests (locks, transactions)
74
+ * - stateless: Each request is independent
75
+ *
76
+ * When skipSessionType is enabled (via constructor options), this is a no-op:
77
+ * the x-sap-adt-sessiontype header will never be sent. This is needed for
78
+ * older BASIS versions (e.g. 7.40) where the stateful header causes locks
79
+ * to be stored in ABAP session memory instead of the global enqueue table,
80
+ * resulting in HTTP 423 on subsequent PUT requests.
81
+ */
82
+ setSessionType(type) {
83
+ if (this.skipSessionType) {
84
+ return;
85
+ }
86
+ this.sessionMode = type;
87
+ this.logger?.debug(`Session type set to: ${type}`, {
88
+ sessionId: this.sessionId?.substring(0, 8),
89
+ });
90
+ }
91
+ /**
92
+ * Get current session mode
93
+ */
94
+ getSessionMode() {
95
+ return this.sessionMode;
96
+ }
97
+ /**
98
+ * Set session ID
99
+ * @deprecated Session ID is auto-generated, use setSessionType() to control session mode
100
+ */
101
+ setSessionId(sessionId) {
102
+ this.sessionId = sessionId;
103
+ this.logger?.debug(`Session ID set to: ${sessionId.substring(0, 8)}...`);
104
+ }
105
+ /**
106
+ * Get current session ID
107
+ */
108
+ getSessionId() {
109
+ return this.sessionId;
110
+ }
111
+ getConfig() {
112
+ return this.config;
113
+ }
114
+ reset() {
115
+ if (this.axiosInstance) {
116
+ this.axiosInstance.interceptors.request.clear();
117
+ this.axiosInstance.interceptors.response.clear();
118
+ this.axiosInstance = null;
119
+ }
120
+ this.csrfToken = null;
121
+ this.cookies = null;
122
+ this.cookieStore.clear();
123
+ // Note: baseUrl is not reset as it's derived from immutable config
124
+ }
125
+ async getBaseUrl() {
126
+ return this.baseUrl;
127
+ }
128
+ async getAuthHeaders() {
129
+ const headers = {};
130
+ if (this.config.client) {
131
+ headers['X-SAP-Client'] = this.config.client;
132
+ }
133
+ const authorization = this.buildAuthorizationHeader();
134
+ if (authorization) {
135
+ headers.Authorization = authorization;
136
+ }
137
+ return headers;
138
+ }
139
+ async makeAdtRequest(options) {
140
+ const { url: endpoint, method, timeout, data, params, headers: customHeaders, } = options;
141
+ const normalizedMethod = method.toUpperCase();
142
+ // Build full URL: baseUrl + endpoint
143
+ const requestUrl = `${this.baseUrl}${endpoint}`;
144
+ // Try to ensure CSRF token is available for POST/PUT/DELETE, but don't fail if it can't be fetched
145
+ // The retry logic will handle CSRF token errors automatically
146
+ if (normalizedMethod === 'POST' ||
147
+ normalizedMethod === 'PUT' ||
148
+ normalizedMethod === 'DELETE') {
149
+ if (!this.csrfToken) {
150
+ try {
151
+ await this.ensureFreshCsrfToken(requestUrl);
152
+ }
153
+ catch (error) {
154
+ // If CSRF token can't be fetched upfront, continue anyway
155
+ // The retry logic will handle CSRF token errors automatically
156
+ this.logger?.debug(`[DEBUG] BaseAbapConnection - Could not fetch CSRF token upfront, will retry on error: ${error instanceof Error ? error.message : String(error)}`);
157
+ }
158
+ }
159
+ }
160
+ // Start with default Accept header
161
+ const requestHeaders = {};
162
+ if (!customHeaders || !customHeaders.Accept) {
163
+ requestHeaders.Accept =
164
+ 'application/xml, application/json, text/plain, */*';
165
+ }
166
+ // Add custom headers (but they won't override auth/cookies)
167
+ if (customHeaders) {
168
+ Object.assign(requestHeaders, customHeaders);
169
+ }
170
+ // ALWAYS add sap-adt-connection-id header (connectionId is sent for ALL session types)
171
+ if (this.sessionId) {
172
+ requestHeaders['sap-adt-connection-id'] = this.sessionId;
173
+ }
174
+ // Add stateful session headers if stateful mode is enabled
175
+ if (this.sessionMode === 'stateful') {
176
+ requestHeaders['x-sap-adt-sessiontype'] = 'stateful';
177
+ requestHeaders['sap-adt-request-id'] = (0, node_crypto_1.randomUUID)().replace(/-/g, '');
178
+ requestHeaders['X-sap-adt-profiling'] = 'server-time';
179
+ }
180
+ // Add auth headers (these MUST NOT be overridden)
181
+ Object.assign(requestHeaders, await this.getAuthHeaders());
182
+ if ((normalizedMethod === 'POST' ||
183
+ normalizedMethod === 'PUT' ||
184
+ normalizedMethod === 'DELETE') &&
185
+ this.csrfToken) {
186
+ requestHeaders['x-csrf-token'] = this.csrfToken;
187
+ }
188
+ // Add cookies LAST (MUST NOT be overridden by custom headers)
189
+ if (this.cookies) {
190
+ requestHeaders.Cookie = this.cookies;
191
+ this.logger?.debug(`[DEBUG] BaseAbapConnection - Adding cookies to request (first 100 chars): ${this.cookies.substring(0, 100)}...`);
192
+ }
193
+ else {
194
+ this.logger?.debug(`[DEBUG] BaseAbapConnection - NO COOKIES available for this request to ${requestUrl}`);
195
+ }
196
+ if ((normalizedMethod === 'POST' || normalizedMethod === 'PUT') && data) {
197
+ if (typeof data === 'string' && !requestHeaders['Content-Type']) {
198
+ if (requestUrl.includes('/usageReferences') &&
199
+ data.includes('usageReferenceRequest')) {
200
+ requestHeaders['Content-Type'] =
201
+ 'application/vnd.sap.adt.repository.usagereferences.request.v1+xml';
202
+ requestHeaders.Accept =
203
+ 'application/vnd.sap.adt.repository.usagereferences.result.v1+xml';
204
+ }
205
+ else {
206
+ requestHeaders['Content-Type'] = 'text/plain; charset=utf-8';
207
+ }
208
+ }
209
+ }
210
+ const requestConfig = {
211
+ method: normalizedMethod,
212
+ url: requestUrl,
213
+ headers: requestHeaders,
214
+ timeout,
215
+ params,
216
+ };
217
+ if (data !== undefined) {
218
+ requestConfig.data = data;
219
+ }
220
+ this.logger?.debug(`Executing ${normalizedMethod} request to: ${requestUrl}`, {
221
+ type: 'REQUEST_INFO',
222
+ url: requestUrl,
223
+ method: normalizedMethod,
224
+ });
225
+ try {
226
+ const response = await this.getAxiosInstance()(requestConfig);
227
+ this.updateCookiesFromResponse(response.headers);
228
+ this.logger?.debug(`Request succeeded with status ${response.status}`, {
229
+ type: 'REQUEST_SUCCESS',
230
+ status: response.status,
231
+ url: requestUrl,
232
+ method: normalizedMethod,
233
+ });
234
+ return response;
235
+ }
236
+ catch (error) {
237
+ const errorDetails = {
238
+ type: 'REQUEST_ERROR',
239
+ message: error instanceof Error ? error.message : String(error),
240
+ url: requestUrl,
241
+ method: normalizedMethod,
242
+ status: error instanceof axios_1.AxiosError ? error.response?.status : undefined,
243
+ data: undefined,
244
+ };
245
+ if (error instanceof axios_1.AxiosError && error.response) {
246
+ errorDetails.data =
247
+ typeof error.response.data === 'string'
248
+ ? error.response.data.slice(0, 200)
249
+ : JSON.stringify(error.response.data).slice(0, 200);
250
+ this.updateCookiesFromResponse(error.response.headers);
251
+ }
252
+ // Check if this is a network error (connection refused, timeout, DNS, etc.)
253
+ // Don't retry for network errors - these indicate infrastructure/VPN issues
254
+ const networkError = (0, mcp_abap_adt_interfaces_1.isNetworkError)(error);
255
+ if (networkError) {
256
+ this.logger?.error(`Network error - cannot connect to SAP system: ${errorDetails.message}`, errorDetails);
257
+ throw error;
258
+ }
259
+ // Log 404 as debug (common for existence checks), other errors as error
260
+ if (errorDetails.status === 404) {
261
+ this.logger?.debug(errorDetails.message, errorDetails);
262
+ }
263
+ else {
264
+ this.logger?.error(errorDetails.message, errorDetails);
265
+ }
266
+ // Retry logic for CSRF token errors (403 with CSRF message)
267
+ if (this.shouldRetryCsrf(error)) {
268
+ this.logger?.debug('CSRF token validation failed, fetching new token and retrying request', {
269
+ url: requestUrl,
270
+ method: normalizedMethod,
271
+ });
272
+ this.csrfToken = await this.fetchCsrfToken(requestUrl, 5, 2000);
273
+ if (this.csrfToken) {
274
+ requestHeaders['x-csrf-token'] = this.csrfToken;
275
+ }
276
+ if (this.cookies) {
277
+ requestHeaders.Cookie = this.cookies;
278
+ }
279
+ const retryResponse = await this.getAxiosInstance()(requestConfig);
280
+ this.updateCookiesFromResponse(retryResponse.headers);
281
+ return retryResponse;
282
+ }
283
+ // Retry logic for 401 errors on GET requests (authentication issue - need cookies)
284
+ // Only for basic auth - JWT auth will be handled by refresh logic below
285
+ if (error instanceof axios_1.AxiosError &&
286
+ error.response?.status === 401 &&
287
+ normalizedMethod === 'GET' &&
288
+ this.config.authType === 'basic' // Only for basic auth
289
+ ) {
290
+ // If we already have cookies from error response, retry immediately
291
+ if (this.cookies) {
292
+ this.logger?.debug(`[DEBUG] BaseAbapConnection - 401 on GET request, retrying with cookies from error response`);
293
+ requestHeaders.Cookie = this.cookies;
294
+ const retryResponse = await this.getAxiosInstance()(requestConfig);
295
+ this.updateCookiesFromResponse(retryResponse.headers);
296
+ return retryResponse;
297
+ }
298
+ // If no cookies, try to get them via CSRF token fetch
299
+ this.logger?.debug(`[DEBUG] BaseAbapConnection - 401 on GET request, attempting to get cookies via CSRF token fetch`);
300
+ try {
301
+ // Try to get CSRF token (this will also get cookies)
302
+ this.csrfToken = await this.fetchCsrfToken(requestUrl, 3, 1000);
303
+ if (this.cookies) {
304
+ requestHeaders.Cookie = this.cookies;
305
+ this.logger?.debug(`[DEBUG] BaseAbapConnection - Retrying GET request with cookies from CSRF fetch`);
306
+ const retryResponse = await this.getAxiosInstance()(requestConfig);
307
+ this.updateCookiesFromResponse(retryResponse.headers);
308
+ return retryResponse;
309
+ }
310
+ }
311
+ catch (csrfError) {
312
+ this.logger?.debug(`[DEBUG] BaseAbapConnection - Failed to get CSRF token for 401 retry: ${csrfError instanceof Error ? csrfError.message : String(csrfError)}`);
313
+ // Fall through to throw original error
314
+ }
315
+ }
316
+ throw error;
317
+ }
318
+ }
319
+ /**
320
+ * Fetch CSRF token from SAP system
321
+ * Protected method for use by concrete implementations in their connect() method
322
+ */
323
+ async fetchCsrfToken(url, retryCount = csrfConfig_js_1.CSRF_CONFIG.RETRY_COUNT, retryDelay = csrfConfig_js_1.CSRF_CONFIG.RETRY_DELAY) {
324
+ // Try primary endpoint first, then fallback for older systems
325
+ const baseUrl = url.includes('/sap/bc/adt/')
326
+ ? url.split('/sap/bc/adt')[0]
327
+ : url.endsWith('/')
328
+ ? url.slice(0, -1)
329
+ : url;
330
+ let endpoints;
331
+ // If the URL already contains a specific endpoint, use only that
332
+ if (url.includes(csrfConfig_js_1.CSRF_CONFIG.ENDPOINT)) {
333
+ endpoints = [url];
334
+ }
335
+ else if (url.includes(csrfConfig_js_1.CSRF_CONFIG.FALLBACK_ENDPOINT)) {
336
+ endpoints = [url];
337
+ }
338
+ else {
339
+ endpoints = [
340
+ `${baseUrl}${csrfConfig_js_1.CSRF_CONFIG.ENDPOINT}`,
341
+ `${baseUrl}${csrfConfig_js_1.CSRF_CONFIG.FALLBACK_ENDPOINT}`,
342
+ ];
343
+ }
344
+ let lastError;
345
+ for (const csrfUrl of endpoints) {
346
+ try {
347
+ return await this.fetchCsrfTokenFromEndpoint(csrfUrl, retryCount, retryDelay);
348
+ }
349
+ catch (error) {
350
+ lastError = error instanceof Error ? error : new Error(String(error));
351
+ this.logger?.debug(`CSRF token not available from ${csrfUrl}, trying next endpoint...`);
352
+ }
353
+ }
354
+ // All endpoints exhausted
355
+ throw lastError ?? new Error('CSRF token fetch failed unexpectedly');
356
+ }
357
+ /**
358
+ * Fetch CSRF token from a specific endpoint with retries
359
+ */
360
+ async fetchCsrfTokenFromEndpoint(csrfUrl, retryCount, retryDelay) {
361
+ this.logger?.debug(`Fetching CSRF token from: ${csrfUrl}`);
362
+ for (let attempt = 0; attempt <= retryCount; attempt++) {
363
+ try {
364
+ if (attempt > 0) {
365
+ this.logger?.debug(`Retry attempt ${attempt}/${retryCount} for CSRF token`);
366
+ }
367
+ const authHeaders = await this.getAuthHeaders();
368
+ const headers = {
369
+ ...authHeaders,
370
+ ...csrfConfig_js_1.CSRF_CONFIG.REQUIRED_HEADERS,
371
+ };
372
+ // Always add cookies if available - they are needed for session continuity
373
+ // Even on first attempt, if we have cookies from previous session or error response, use them
374
+ if (this.cookies) {
375
+ headers.Cookie = this.cookies;
376
+ this.logger?.debug(`[DEBUG] BaseAbapConnection - Adding cookies to CSRF token request (attempt ${attempt + 1}, first 100 chars): ${this.cookies.substring(0, 100)}...`);
377
+ }
378
+ else {
379
+ this.logger?.debug(`[DEBUG] BaseAbapConnection - No cookies available for CSRF token request (will get fresh cookies from response)`);
380
+ }
381
+ // Log request details for debugging (only if debug logging is enabled)
382
+ 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}`);
383
+ const response = await this.getAxiosInstance()({
384
+ method: 'GET',
385
+ url: csrfUrl,
386
+ headers,
387
+ timeout: (0, timeouts_js_1.getTimeout)('csrf'),
388
+ });
389
+ this.updateCookiesFromResponse(response.headers);
390
+ const token = response.headers['x-csrf-token'];
391
+ if (!token) {
392
+ this.logger?.error('No CSRF token in response headers', {
393
+ headers: response.headers,
394
+ status: response.status,
395
+ });
396
+ if (attempt < retryCount) {
397
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
398
+ continue;
399
+ }
400
+ throw new Error(csrfConfig_js_1.CSRF_ERROR_MESSAGES.NOT_IN_HEADERS);
401
+ }
402
+ if (response.headers['set-cookie']) {
403
+ this.updateCookiesFromResponse(response.headers);
404
+ if (this.cookies) {
405
+ this.logger?.debug(`[DEBUG] BaseAbapConnection - Cookies received from CSRF response (first 100 chars): ${this.cookies.substring(0, 100)}...`);
406
+ this.logger?.debug('Cookies extracted from response', {
407
+ cookieLength: this.cookies.length,
408
+ });
409
+ }
410
+ }
411
+ this.logger?.debug('CSRF token successfully obtained');
412
+ return token;
413
+ }
414
+ catch (error) {
415
+ if (error instanceof axios_1.AxiosError) {
416
+ // Always try to extract cookies from error response, even on 401
417
+ // This ensures cookies are available for subsequent requests
418
+ if (error.response?.headers) {
419
+ this.updateCookiesFromResponse(error.response.headers);
420
+ if (this.cookies) {
421
+ this.logger?.debug('Cookies extracted from error response', {
422
+ status: error.response.status,
423
+ cookieLength: this.cookies.length,
424
+ });
425
+ }
426
+ }
427
+ this.logger?.error(`CSRF token error: ${error.message}`, {
428
+ url: csrfUrl,
429
+ status: error.response?.status,
430
+ attempt: attempt + 1,
431
+ maxAttempts: retryCount + 1,
432
+ });
433
+ if (error.response?.status === 405 &&
434
+ error.response?.headers['x-csrf-token']) {
435
+ this.logger?.debug('CSRF: SAP returned 405 (Method Not Allowed) — not critical, token found in header');
436
+ const token = error.response.headers['x-csrf-token'];
437
+ if (token) {
438
+ this.updateCookiesFromResponse(error.response.headers);
439
+ return token;
440
+ }
441
+ }
442
+ if (error.response?.headers['x-csrf-token']) {
443
+ this.logger?.debug(`Got CSRF token despite error (status: ${error.response?.status})`);
444
+ const token = error.response.headers['x-csrf-token'];
445
+ this.updateCookiesFromResponse(error.response.headers);
446
+ return token;
447
+ }
448
+ if (error.response) {
449
+ this.logger?.error('CSRF error details', {
450
+ status: error.response.status,
451
+ statusText: error.response.statusText,
452
+ headers: Object.keys(error.response.headers),
453
+ data: typeof error.response.data === 'string'
454
+ ? error.response.data.slice(0, 200)
455
+ : JSON.stringify(error.response.data).slice(0, 200),
456
+ });
457
+ }
458
+ else if (error.request) {
459
+ this.logger?.error('CSRF request error - no response received', {
460
+ request: error.request.path,
461
+ });
462
+ }
463
+ }
464
+ else {
465
+ this.logger?.error('CSRF non-axios error', {
466
+ error: error instanceof Error ? error.message : String(error),
467
+ });
468
+ }
469
+ if (attempt < retryCount) {
470
+ await new Promise((resolve) => setTimeout(resolve, retryDelay));
471
+ continue;
472
+ }
473
+ // Preserve original error information, especially AxiosError with response
474
+ if (error instanceof axios_1.AxiosError && error.response) {
475
+ // Re-throw the original AxiosError to preserve response information
476
+ throw error;
477
+ }
478
+ throw new Error(csrfConfig_js_1.CSRF_ERROR_MESSAGES.FETCH_FAILED(retryCount + 1, error instanceof Error ? error.message : String(error)));
479
+ }
480
+ }
481
+ throw new Error('CSRF token fetch failed unexpectedly');
482
+ }
483
+ /**
484
+ * Get CSRF token (protected for use by subclasses)
485
+ */
486
+ getCsrfToken() {
487
+ return this.csrfToken;
488
+ }
489
+ /**
490
+ * Set CSRF token (protected for use by subclasses)
491
+ */
492
+ setCsrfToken(token) {
493
+ this.csrfToken = token;
494
+ }
495
+ /**
496
+ * Get cookies (protected for use by subclasses)
497
+ */
498
+ getCookies() {
499
+ return this.cookies;
500
+ }
501
+ setInitialCookies(cookies) {
502
+ this.cookies = cookies;
503
+ }
504
+ updateCookiesFromResponse(headers) {
505
+ if (!headers) {
506
+ return;
507
+ }
508
+ const setCookie = headers['set-cookie'];
509
+ if (!setCookie) {
510
+ return;
511
+ }
512
+ const cookiesArray = Array.isArray(setCookie) ? setCookie : [setCookie];
513
+ for (const entry of cookiesArray) {
514
+ if (typeof entry !== 'string') {
515
+ continue;
516
+ }
517
+ const [nameValue] = entry.split(';');
518
+ if (!nameValue) {
519
+ continue;
520
+ }
521
+ const [name, ...rest] = nameValue.split('=');
522
+ if (!name) {
523
+ continue;
524
+ }
525
+ const trimmedName = name.trim();
526
+ const trimmedValue = rest.join('=').trim();
527
+ if (!trimmedName) {
528
+ continue;
529
+ }
530
+ this.cookieStore.set(trimmedName, trimmedValue);
531
+ }
532
+ // Enforce configured SAP client in sap-usercontext cookie.
533
+ // SAP may return sap-usercontext=sap-client=<default_client> based on system
534
+ // default rather than the X-SAP-Client header value, causing requests to be
535
+ // routed to the wrong client (e.g. a read-only client → 403 on write operations).
536
+ if (this.config.client) {
537
+ this.cookieStore.set('sap-usercontext', `sap-client=${this.config.client}`);
538
+ }
539
+ if (this.cookieStore.size === 0) {
540
+ return;
541
+ }
542
+ const combined = Array.from(this.cookieStore.entries())
543
+ .map(([name, value]) => (value ? `${name}=${value}` : name))
544
+ .join('; ');
545
+ if (!combined) {
546
+ return;
547
+ }
548
+ this.cookies = combined;
549
+ this.logger?.debug(`[DEBUG] BaseAbapConnection - Updated cookies from response (first 100 chars): ${this.cookies.substring(0, 100)}...`);
550
+ }
551
+ getAxiosInstance() {
552
+ if (!this.axiosInstance) {
553
+ const rejectUnauthorized = process.env.NODE_TLS_REJECT_UNAUTHORIZED === '1' ||
554
+ (process.env.TLS_REJECT_UNAUTHORIZED === '1' &&
555
+ process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0');
556
+ this.logger?.debug(`TLS configuration: rejectUnauthorized=${rejectUnauthorized}`);
557
+ this.axiosInstance = axios_1.default.create({
558
+ httpsAgent: new node_https_1.Agent({
559
+ rejectUnauthorized,
560
+ }),
561
+ });
562
+ }
563
+ return this.axiosInstance;
564
+ }
565
+ async ensureFreshCsrfToken(requestUrl) {
566
+ // If we already have a CSRF token, reuse it to keep the same SAP session
567
+ // SAP ties the lock handle to the HTTP session (SAP_SESSIONID cookie)
568
+ if (this.csrfToken) {
569
+ this.logger?.debug(`[DEBUG] BaseAbapConnection - Reusing existing CSRF token to maintain session`);
570
+ return;
571
+ }
572
+ try {
573
+ this.logger?.debug(`[DEBUG] BaseAbapConnection - Fetching NEW CSRF token (will create new SAP session)`);
574
+ this.csrfToken = await this.fetchCsrfToken(requestUrl);
575
+ }
576
+ catch (error) {
577
+ // fetchCsrfToken handles auth errors
578
+ // Just re-throw the error with minimal logging to avoid duplicate error messages
579
+ const errorMsg = error instanceof Error
580
+ ? error.message
581
+ : csrfConfig_js_1.CSRF_ERROR_MESSAGES.REQUIRED_FOR_MUTATION;
582
+ // Only log at DEBUG level to avoid duplicate error messages
583
+ // (fetchCsrfToken already logged the error at ERROR level if auth failed)
584
+ this.logger?.debug(`[DEBUG] BaseAbapConnection - ensureFreshCsrfToken failed: ${errorMsg}`);
585
+ throw error;
586
+ }
587
+ }
588
+ shouldRetryCsrf(error) {
589
+ if (!(error instanceof axios_1.AxiosError)) {
590
+ return false;
591
+ }
592
+ const responseData = error.response?.data;
593
+ const responseText = typeof responseData === 'string'
594
+ ? responseData
595
+ : JSON.stringify(responseData || '');
596
+ // Don't retry for JWT auth - refresh logic will handle it
597
+ if (this.config.authType === 'jwt') {
598
+ return false;
599
+ }
600
+ // Retry on 403 with CSRF message, or if response mentions CSRF token
601
+ // Also retry on 401 for POST/PUT/DELETE if we don't have CSRF token yet (might need to get cookies first)
602
+ const method = error.config?.method?.toUpperCase();
603
+ const isPostPutDelete = method && ['POST', 'PUT', 'DELETE'].includes(method);
604
+ const needsCsrfToken = !!isPostPutDelete && !this.csrfToken;
605
+ return ((!!error.response &&
606
+ error.response.status === 403 &&
607
+ responseText.includes('CSRF')) ||
608
+ responseText.includes('CSRF token') ||
609
+ (needsCsrfToken && error.response?.status === 401));
610
+ }
611
+ }
612
+ exports.AbstractAbapConnection = AbstractAbapConnection;
@@ -0,0 +1,19 @@
1
+ import type { SapConfig } from '../config/sapConfig.js';
2
+ import type { ILogger } from '../logger.js';
3
+ import { AbstractAbapConnection } from './AbstractAbapConnection.js';
4
+ /**
5
+ * Basic Authentication connection for on-premise SAP systems
6
+ */
7
+ export declare class BaseAbapConnection extends AbstractAbapConnection {
8
+ constructor(config: SapConfig, logger?: ILogger | null, sessionId?: string, options?: {
9
+ skipSessionType?: boolean;
10
+ });
11
+ /**
12
+ * Connect to SAP system with Basic Auth
13
+ * Fetches CSRF token which also establishes session cookies
14
+ */
15
+ connect(): Promise<void>;
16
+ protected buildAuthorizationHeader(): string;
17
+ private static validateConfig;
18
+ }
19
+ //# sourceMappingURL=BaseAbapConnection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BaseAbapConnection.d.ts","sourceRoot":"","sources":["../../src/connection/BaseAbapConnection.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAErE;;GAEG;AACH,qBAAa,kBAAmB,SAAQ,sBAAsB;gBAE1D,MAAM,EAAE,SAAS,EACjB,MAAM,CAAC,EAAE,OAAO,GAAG,IAAI,EACvB,SAAS,CAAC,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAE;IAMzC;;;OAGG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAsC9B,SAAS,CAAC,wBAAwB,IAAI,MAAM;IAU5C,OAAO,CAAC,MAAM,CAAC,cAAc;CAmB9B"}