@mcp-abap-adt/connection 0.1.8 → 0.1.10
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
CHANGED
|
@@ -320,9 +320,15 @@ interface AbapConnection {
|
|
|
320
320
|
enableStatefulSession(sessionId: string, storage: ISessionStorage): Promise<void>;
|
|
321
321
|
disableStatefulSession(): void;
|
|
322
322
|
getSessionMode(): "stateless" | "stateful";
|
|
323
|
+
getSessionId(): string | undefined; // Get current session ID
|
|
324
|
+
setSessionType(type: "stateless" | "stateful"): void; // Switch session type
|
|
323
325
|
}
|
|
324
326
|
```
|
|
325
327
|
|
|
328
|
+
**New in 0.1.6+:**
|
|
329
|
+
- `getSessionId()`: Returns the current session ID if stateful session is enabled, otherwise `undefined`
|
|
330
|
+
- `setSessionType(type)`: Programmatically switch between stateful and stateless modes without recreating connection
|
|
331
|
+
|
|
326
332
|
#### `ILogger`
|
|
327
333
|
|
|
328
334
|
Logger interface for custom logging implementations.
|
|
@@ -370,10 +376,21 @@ function createAbapConnection(
|
|
|
370
376
|
- Node.js >= 18.0.0
|
|
371
377
|
- Access to SAP ABAP system (on-premise or BTP)
|
|
372
378
|
|
|
379
|
+
## Changelog
|
|
380
|
+
|
|
381
|
+
See [CHANGELOG.md](./CHANGELOG.md) for detailed version history and breaking changes.
|
|
382
|
+
|
|
383
|
+
**Latest version: 0.1.9**
|
|
384
|
+
- Comprehensive documentation updates
|
|
385
|
+
- Enhanced README with new API methods documentation
|
|
386
|
+
- Complete version history in CHANGELOG
|
|
387
|
+
- Fixed documentation structure and links
|
|
388
|
+
|
|
373
389
|
## Documentation
|
|
374
390
|
|
|
375
|
-
- [Custom Session Storage](./CUSTOM_SESSION_STORAGE.md) - How to implement custom session persistence (database, Redis, etc.)
|
|
391
|
+
- [Custom Session Storage](./docs/CUSTOM_SESSION_STORAGE.md) - How to implement custom session persistence (database, Redis, etc.)
|
|
376
392
|
- [Examples](./examples/README.md) - Working code examples
|
|
393
|
+
- [Changelog](./CHANGELOG.md) - Version history and release notes
|
|
377
394
|
|
|
378
395
|
## License
|
|
379
396
|
|
package/bin/sap-abap-auth.js
CHANGED
|
@@ -122,6 +122,48 @@ async function tryRefreshToken(refreshToken, uaaUrl, clientId, clientSecret) {
|
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Decodes JWT token and extracts expiration time
|
|
127
|
+
* @param {string} token JWT token string
|
|
128
|
+
* @returns {Object|null} Object with expiration date and timestamp, or null if decoding fails
|
|
129
|
+
*/
|
|
130
|
+
function getTokenExpiry(token) {
|
|
131
|
+
try {
|
|
132
|
+
if (!token) return null;
|
|
133
|
+
|
|
134
|
+
// JWT format: header.payload.signature
|
|
135
|
+
const parts = token.split('.');
|
|
136
|
+
if (parts.length !== 3) return null;
|
|
137
|
+
|
|
138
|
+
// Decode payload (base64url)
|
|
139
|
+
const payload = parts[1];
|
|
140
|
+
// Add padding if needed
|
|
141
|
+
const paddedPayload = payload + '='.repeat((4 - payload.length % 4) % 4);
|
|
142
|
+
const decodedPayload = Buffer.from(paddedPayload, 'base64').toString('utf8');
|
|
143
|
+
const payloadObj = JSON.parse(decodedPayload);
|
|
144
|
+
|
|
145
|
+
if (!payloadObj.exp) return null;
|
|
146
|
+
|
|
147
|
+
// exp is Unix timestamp in seconds
|
|
148
|
+
const expiryTimestamp = payloadObj.exp * 1000; // Convert to milliseconds
|
|
149
|
+
const expiryDate = new Date(expiryTimestamp);
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
timestamp: expiryTimestamp,
|
|
153
|
+
date: expiryDate,
|
|
154
|
+
dateString: expiryDate.toISOString(),
|
|
155
|
+
readableDate: expiryDate.toLocaleString('en-US', {
|
|
156
|
+
timeZone: 'UTC',
|
|
157
|
+
dateStyle: 'full',
|
|
158
|
+
timeStyle: 'long'
|
|
159
|
+
})
|
|
160
|
+
};
|
|
161
|
+
} catch (error) {
|
|
162
|
+
// Silently fail - token might not be a valid JWT or might be in different format
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
125
167
|
/**
|
|
126
168
|
* Updates the .env file with new values
|
|
127
169
|
* @param {Object} updates Object with updated values
|
|
@@ -134,6 +176,29 @@ function updateEnvFile(updates, envFilePath) {
|
|
|
134
176
|
fs.unlinkSync(envFilePath);
|
|
135
177
|
}
|
|
136
178
|
let lines = [];
|
|
179
|
+
|
|
180
|
+
// Get token expiry information
|
|
181
|
+
const jwtTokenExpiry = getTokenExpiry(updates.SAP_JWT_TOKEN);
|
|
182
|
+
const refreshTokenExpiry = getTokenExpiry(updates.SAP_REFRESH_TOKEN);
|
|
183
|
+
|
|
184
|
+
// Add token expiry comments at the beginning if JWT auth
|
|
185
|
+
if (updates.SAP_AUTH_TYPE === "jwt") {
|
|
186
|
+
lines.push("# Token Expiry Information (auto-generated)");
|
|
187
|
+
if (jwtTokenExpiry) {
|
|
188
|
+
lines.push(`# JWT Token expires: ${jwtTokenExpiry.readableDate} (UTC)`);
|
|
189
|
+
lines.push(`# JWT Token expires at: ${jwtTokenExpiry.dateString}`);
|
|
190
|
+
} else {
|
|
191
|
+
lines.push("# JWT Token expiry: Unable to determine (token may not be a standard JWT)");
|
|
192
|
+
}
|
|
193
|
+
if (refreshTokenExpiry) {
|
|
194
|
+
lines.push(`# Refresh Token expires: ${refreshTokenExpiry.readableDate} (UTC)`);
|
|
195
|
+
lines.push(`# Refresh Token expires at: ${refreshTokenExpiry.dateString}`);
|
|
196
|
+
} else if (updates.SAP_REFRESH_TOKEN) {
|
|
197
|
+
lines.push("# Refresh Token expiry: Unable to determine (token may not be a standard JWT)");
|
|
198
|
+
}
|
|
199
|
+
lines.push("");
|
|
200
|
+
}
|
|
201
|
+
|
|
137
202
|
if (updates.SAP_AUTH_TYPE === "jwt") {
|
|
138
203
|
// jwt: write only relevant params
|
|
139
204
|
const jwtAllowed = [
|
|
@@ -28,6 +28,11 @@ export declare class JwtAbapConnection extends AbstractAbapConnection {
|
|
|
28
28
|
* Override makeAdtRequest to handle JWT token refresh on 401/403
|
|
29
29
|
*/
|
|
30
30
|
makeAdtRequest(options: AbapRequestOptions): Promise<AxiosResponse>;
|
|
31
|
+
/**
|
|
32
|
+
* Override fetchCsrfToken to handle JWT token refresh on 401/403 errors
|
|
33
|
+
* This ensures that token refresh works even when CSRF token is fetched during makeAdtRequest
|
|
34
|
+
*/
|
|
35
|
+
protected fetchCsrfToken(url: string, retryCount?: number, retryDelay?: number): Promise<string>;
|
|
31
36
|
private static validateConfig;
|
|
32
37
|
}
|
|
33
38
|
//# sourceMappingURL=JwtAbapConnection.d.ts.map
|
|
@@ -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,eAAe,EAAgB,MAAM,cAAc,CAAC;AAEtE,OAAO,EAAc,aAAa,EAAE,MAAM,OAAO,CAAC;AAElD;;;GAGG;AACH,qBAAa,iBAAkB,SAAQ,sBAAsB;IAC3D,OAAO,CAAC,sBAAsB,CAAkB;gBAG9C,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,OAAO,EACf,cAAc,CAAC,EAAE,eAAe,EAChC,SAAS,CAAC,EAAE,MAAM;IAMpB,SAAS,CAAC,wBAAwB,IAAI,MAAM;IAS5C;;;OAGG;IACG,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IA6EnC;;OAEG;IACH,eAAe,IAAI,OAAO;IAU1B;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;
|
|
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,eAAe,EAAgB,MAAM,cAAc,CAAC;AAEtE,OAAO,EAAc,aAAa,EAAE,MAAM,OAAO,CAAC;AAElD;;;GAGG;AACH,qBAAa,iBAAkB,SAAQ,sBAAsB;IAC3D,OAAO,CAAC,sBAAsB,CAAkB;gBAG9C,MAAM,EAAE,SAAS,EACjB,MAAM,EAAE,OAAO,EACf,cAAc,CAAC,EAAE,eAAe,EAChC,SAAS,CAAC,EAAE,MAAM;IAMpB,SAAS,CAAC,wBAAwB,IAAI,MAAM;IAS5C;;;OAGG;IACG,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IA6EnC;;OAEG;IACH,eAAe,IAAI,OAAO;IAU1B;;OAEG;IACG,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IA4G9B;;OAEG;IACG,cAAc,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,aAAa,CAAC;IAuGzE;;;OAGG;cACa,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,SAAI,EAAE,UAAU,SAAO,GAAG,OAAO,CAAC,MAAM,CAAC;IA4C/F,OAAO,CAAC,MAAM,CAAC,cAAc;CAS9B"}
|
|
@@ -188,11 +188,12 @@ class JwtAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnecti
|
|
|
188
188
|
}
|
|
189
189
|
catch (refreshError) {
|
|
190
190
|
this.logger.error(`❌ Token refresh failed during connect: ${refreshError.message}`);
|
|
191
|
-
throw new Error("
|
|
191
|
+
throw new Error("Refresh token has expired. Please re-authenticate.");
|
|
192
192
|
}
|
|
193
193
|
}
|
|
194
194
|
else {
|
|
195
|
-
|
|
195
|
+
// No refresh token available - JWT token expired, need to re-authenticate
|
|
196
|
+
throw new Error("JWT token has expired. Please re-authenticate.");
|
|
196
197
|
}
|
|
197
198
|
}
|
|
198
199
|
// Re-throw other errors
|
|
@@ -243,7 +244,7 @@ class JwtAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnecti
|
|
|
243
244
|
catch (refreshError) {
|
|
244
245
|
// Only catch errors from refreshToken()
|
|
245
246
|
this.logger.error(`❌ Token refresh failed: ${refreshError.message}`);
|
|
246
|
-
throw new Error("
|
|
247
|
+
throw new Error("Refresh token has expired. Please re-authenticate.");
|
|
247
248
|
}
|
|
248
249
|
// Step 2: Reconnect to get new CSRF token and cookies
|
|
249
250
|
try {
|
|
@@ -262,10 +263,16 @@ class JwtAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnecti
|
|
|
262
263
|
}
|
|
263
264
|
}
|
|
264
265
|
catch (connectError) {
|
|
266
|
+
// If connect() fails after token refresh, it means the refresh token was invalid
|
|
267
|
+
// Check if it's an auth error (401/403)
|
|
268
|
+
if (connectError instanceof axios_1.AxiosError &&
|
|
269
|
+
(connectError.response?.status === 401 || connectError.response?.status === 403)) {
|
|
270
|
+
this.logger.error(`❌ Failed to reconnect after token refresh: ${connectError.message}`);
|
|
271
|
+
throw new Error("Refresh token has expired. Please re-authenticate.");
|
|
272
|
+
}
|
|
273
|
+
// For other errors, log but continue - ensureFreshCsrfToken will try to get CSRF token during request retry
|
|
265
274
|
this.logger.error(`❌ Failed to reconnect after token refresh: ${connectError.message}`);
|
|
266
275
|
this.logger.error(`❌ This means CSRF token will not be available for POST/PUT/DELETE requests`);
|
|
267
|
-
// Continue anyway - ensureFreshCsrfToken will try to get CSRF token during request retry
|
|
268
|
-
// But this is likely to fail if connect() failed
|
|
269
276
|
}
|
|
270
277
|
// Step 3: Retry the request with new token
|
|
271
278
|
// ensureFreshCsrfToken will be called automatically if CSRF token is missing
|
|
@@ -278,7 +285,7 @@ class JwtAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnecti
|
|
|
278
285
|
if (retryError instanceof axios_1.AxiosError &&
|
|
279
286
|
(retryError.response?.status === 401 || retryError.response?.status === 403)) {
|
|
280
287
|
this.logger.error(`❌ Token refresh didn't help - still getting ${retryError.response.status}`);
|
|
281
|
-
throw new Error("
|
|
288
|
+
throw new Error("Refresh token has expired. Please re-authenticate.");
|
|
282
289
|
}
|
|
283
290
|
// For other errors (400, 500, etc.), re-throw the original error
|
|
284
291
|
// These are not auth errors, so they should be handled by the caller
|
|
@@ -287,12 +294,58 @@ class JwtAbapConnection extends AbstractAbapConnection_js_1.AbstractAbapConnecti
|
|
|
287
294
|
}
|
|
288
295
|
}
|
|
289
296
|
else {
|
|
290
|
-
|
|
297
|
+
// No refresh token available - JWT token expired, need to re-authenticate
|
|
298
|
+
throw new Error("JWT token has expired. Please re-authenticate.");
|
|
291
299
|
}
|
|
292
300
|
}
|
|
293
301
|
throw error;
|
|
294
302
|
}
|
|
295
303
|
}
|
|
304
|
+
/**
|
|
305
|
+
* Override fetchCsrfToken to handle JWT token refresh on 401/403 errors
|
|
306
|
+
* This ensures that token refresh works even when CSRF token is fetched during makeAdtRequest
|
|
307
|
+
*/
|
|
308
|
+
async fetchCsrfToken(url, retryCount = 3, retryDelay = 1000) {
|
|
309
|
+
try {
|
|
310
|
+
// Try to fetch CSRF token using parent implementation
|
|
311
|
+
return await super.fetchCsrfToken(url, retryCount, retryDelay);
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
// Handle JWT auth errors (401/403) during CSRF token fetch
|
|
315
|
+
if (error instanceof axios_1.AxiosError &&
|
|
316
|
+
(error.response?.status === 401 || error.response?.status === 403)) {
|
|
317
|
+
// Check if this is really an auth error, not a permissions error
|
|
318
|
+
const responseData = error.response?.data;
|
|
319
|
+
const responseText = typeof responseData === "string" ? responseData : JSON.stringify(responseData || "");
|
|
320
|
+
// Don't retry on "No Access" errors
|
|
321
|
+
if (responseText.includes("ExceptionResourceNoAccess") ||
|
|
322
|
+
responseText.includes("No authorization") ||
|
|
323
|
+
responseText.includes("Missing authorization")) {
|
|
324
|
+
throw error;
|
|
325
|
+
}
|
|
326
|
+
// Try token refresh if possible
|
|
327
|
+
if (this.canRefreshToken()) {
|
|
328
|
+
try {
|
|
329
|
+
this.logger.debug(`Received ${error.response.status} during CSRF token fetch, attempting JWT token refresh...`);
|
|
330
|
+
await this.refreshToken();
|
|
331
|
+
this.logger.debug(`✓ Token refreshed successfully, retrying CSRF token fetch...`);
|
|
332
|
+
// Retry CSRF token fetch with new JWT token
|
|
333
|
+
return await super.fetchCsrfToken(url, retryCount, retryDelay);
|
|
334
|
+
}
|
|
335
|
+
catch (refreshError) {
|
|
336
|
+
this.logger.error(`❌ Token refresh failed during CSRF token fetch: ${refreshError.message}`);
|
|
337
|
+
throw new Error("Refresh token has expired. Please re-authenticate.");
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
// No refresh token available - JWT token expired, need to re-authenticate
|
|
342
|
+
throw new Error("JWT token has expired. Please re-authenticate.");
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// Re-throw other errors
|
|
346
|
+
throw error;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
296
349
|
static validateConfig(config) {
|
|
297
350
|
if (config.authType !== "jwt") {
|
|
298
351
|
throw new Error(`JWT connection expects authType "jwt", got "${config.authType}"`);
|