@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.
- package/LICENSE +21 -0
- package/README.md +80 -0
- package/bin/sap-abap-auth.js +600 -0
- package/dist/config/sapConfig.d.ts +43 -0
- package/dist/config/sapConfig.d.ts.map +1 -0
- package/dist/config/sapConfig.js +202 -0
- package/dist/connection/AbapConnection.d.ts +22 -0
- package/dist/connection/AbapConnection.d.ts.map +1 -0
- package/dist/connection/AbapConnection.js +2 -0
- package/dist/connection/AbstractAbapConnection.d.ts +115 -0
- package/dist/connection/AbstractAbapConnection.d.ts.map +1 -0
- package/dist/connection/AbstractAbapConnection.js +716 -0
- package/dist/connection/BaseAbapConnection.d.ts +17 -0
- package/dist/connection/BaseAbapConnection.d.ts.map +1 -0
- package/dist/connection/BaseAbapConnection.js +68 -0
- package/dist/connection/JwtAbapConnection.d.ts +33 -0
- package/dist/connection/JwtAbapConnection.d.ts.map +1 -0
- package/dist/connection/JwtAbapConnection.js +305 -0
- package/dist/connection/connectionFactory.d.ts +5 -0
- package/dist/connection/connectionFactory.d.ts.map +1 -0
- package/dist/connection/connectionFactory.js +15 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/logger.d.ts +67 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +2 -0
- package/dist/utils/FileSessionStorage.d.ts +73 -0
- package/dist/utils/FileSessionStorage.d.ts.map +1 -0
- package/dist/utils/FileSessionStorage.js +191 -0
- package/dist/utils/timeouts.d.ts +8 -0
- package/dist/utils/timeouts.d.ts.map +1 -0
- package/dist/utils/timeouts.js +21 -0
- package/dist/utils/tokenRefresh.d.ts +17 -0
- package/dist/utils/tokenRefresh.d.ts.map +1 -0
- package/dist/utils/tokenRefresh.js +53 -0
- 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;
|