@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
 
@@ -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;IA2G9B;;OAEG;IACG,cAAc,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,aAAa,CAAC;IAgGzE,OAAO,CAAC,MAAM,CAAC,cAAc;CAS9B"}
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("JWT token has expired and refresh failed. Please re-authenticate.");
191
+ throw new Error("Refresh token has expired. Please re-authenticate.");
192
192
  }
193
193
  }
194
194
  else {
195
- throw new Error("JWT token has expired. Please refresh your authentication token.");
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("JWT token has expired and refresh failed. Please re-authenticate.");
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("JWT token has expired and refresh failed. Please re-authenticate.");
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
- throw new Error("JWT token has expired. Please refresh your authentication token.");
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}"`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-abap-adt/connection",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "ABAP connection layer for MCP ABAP ADT server",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",