@mcp-abap-adt/auth-providers 0.2.7 → 0.2.9

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/CHANGELOG.md CHANGED
@@ -7,6 +7,61 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.9] - 2025-12-25
11
+
12
+ ### Changed
13
+ - **Logging Improvements**: Enhanced logging for better debugging and readability
14
+ - **Date Formatting**: Expiration dates now displayed in readable format (`YYYY-MM-DD HH:MM:SS UTC`) instead of ISO format (`2025-12-25T11:08:15.000Z`)
15
+ - **Token Formatting**: Tokens are logged in truncated format (start...end) for security and readability
16
+ - **Browser Information**: Added logging of browser type and authorization URL before starting browser authentication
17
+ - **Token Lifecycle**: Improved logging of token acquisition, validation, and refresh operations with formatted dates
18
+
19
+ ### Added
20
+ - **Browser Auth Timeout**: Added 30-second timeout for browser-based authentication
21
+ - Prevents provider from blocking consumer indefinitely when user doesn't complete authentication
22
+ - Timeout error is thrown if authentication is not completed within 30 seconds
23
+ - Helps prevent hanging in automated tests and CI/CD environments
24
+
25
+ ### Fixed
26
+ - **Test Hanging Issues**: Fixed Jest tests hanging after completion
27
+ - Added `forceExit: true` to Jest configuration to force exit after tests complete
28
+ - Improved cleanup of HTTP server and timers to prevent open handles
29
+ - Added protection against double execution of server close handlers
30
+ - Proper cleanup of `finishTimeoutId` timer in all scenarios (success, error, timeout)
31
+ - Server now resolves promise only after fully closing to ensure Jest can exit cleanly
32
+
33
+ ## [0.2.8] - 2025-12-24
34
+
35
+ ### Changed
36
+ - **Integration Tests**: Replaced mock-based unit tests with comprehensive integration tests using real YAML configuration
37
+ - Tests now use `test-config.yaml` for loading real service keys and session files
38
+ - Simplified test configuration structure: only `destination` and optional `destination_dir` (removed `abap`/`xsuaa` sections)
39
+ - Default paths: `~/.config/mcp-abap-adt` (Unix) or `%USERPROFILE%\Documents\mcp-abap-adt` (Windows)
40
+ - **Logging**: Migrated from `console.log` to structured logging using `@mcp-abap-adt/logger`
41
+ - All providers and browser auth functions now use `ILogger` interface
42
+ - Consistent structured logging with log levels (debug, info, warn, error)
43
+ - Better debugging with token validation details and execution flow
44
+
45
+ ### Added
46
+ - **Test Scenarios**: Comprehensive integration test coverage for token lifecycle
47
+ - Scenario 1 & 2: Token lifecycle - login via browser and reuse token from previous scenario
48
+ - Scenario 3: Expired session + expired refresh token - provider should re-authenticate via browser
49
+ - Token validation: Explicit validation of token expiration in all scenarios
50
+ - **Test Configuration**: Simplified `test-config.yaml.template` with detailed comments
51
+ - Only requires `destination` name (service key and session files are auto-resolved)
52
+ - Optional `destination_dir` (commented out by default, can be uncommented for custom paths)
53
+ - Clear documentation of test scenarios and file formats
54
+
55
+ ### Fixed
56
+ - **Test Hanging Issues**: Fixed tests hanging due to browser and port conflicts
57
+ - Changed `browser: 'none'` to `browser: 'system'` for interactive authentication
58
+ - Each test scenario uses unique ports (3101, 3102, 3103) to avoid conflicts
59
+ - Improved server cleanup: `resolve(tokens)` called immediately after token exchange, server closes asynchronously
60
+ - Added delays after browser-based tests to allow ports to free up
61
+ - **Token Validation**: Added explicit token validation in all test scenarios
62
+ - Tests verify that returned tokens are valid and not expired
63
+ - Uses JWT `exp` claim validation with 60-second buffer (matching `BaseTokenProvider`)
64
+
10
65
  ## [0.2.7] - 2025-12-24
11
66
 
12
67
  ### Changed
package/README.md CHANGED
@@ -207,7 +207,9 @@ const result = await provider.getTokens();
207
207
  // result.refreshToken is undefined (client_credentials doesn't provide refresh tokens)
208
208
  ```
209
209
 
210
- **Note**: The `browserAuthPort` parameter (default: 3001) configures the OAuth callback server port. If the requested port is already in use, an error will be thrown. You must specify a different port or free the port before starting authentication. The server properly closes all connections and frees the port after authentication completes, ensuring no lingering port occupation.
210
+ **Note**: The `browserAuthPort` parameter (default: 3001) configures the OAuth callback server port. If the requested port is already in use, an error will be thrown. You must specify a different port or free the port before starting authentication. The server properly closes all connections and frees the port after authentication completes, ensuring no lingering port occupation.
211
+
212
+ **Timeout**: Browser authentication has a 30-second timeout to prevent blocking the consumer. If authentication is not completed within 30 seconds, the operation will fail with a timeout error. This prevents the provider from hanging indefinitely when the user doesn't complete authentication.
211
213
 
212
214
  **Process Termination Handling**: The OAuth callback server registers cleanup handlers for `SIGTERM`, `SIGINT`, `SIGHUP`, and `exit` signals. This ensures ports are properly freed even when MCP clients (like Cline) terminate the process before authentication completes. This is especially important for stdio servers where the client may kill the process at any time. On Windows, the `SIGBREAK` signal (Ctrl+Break) is also handled.
213
215
 
@@ -335,27 +337,33 @@ npm test
335
337
  Integration tests work with real files from `tests/test-config.yaml`:
336
338
 
337
339
  1. Copy `tests/test-config.yaml.template` to `tests/test-config.yaml`
338
- 2. Fill in real paths, destinations, and URLs
340
+ 2. Fill in real destination name
339
341
  3. Run tests - integration tests will use real services if configured
340
342
 
341
343
  ```yaml
342
- auth_broker:
343
- paths:
344
- service_keys_dir: ~/.config/mcp-abap-adt/service-keys/
345
- sessions_dir: ~/.config/mcp-abap-adt/sessions/
346
- abap:
347
- destination: "TRIAL" # For ABAP tests (uses AbapServiceKeyStore, AbapSessionStore)
348
- xsuaa:
349
- btp_destination: "mcp" # For BTP tests (uses BtpServiceKeyStore, BtpSessionStore)
350
- mcp_url: "https://..."
344
+ # Destination name (used for service key file: <destination>.json and session file: <destination>.env)
345
+ destination: "trial" # Example: "trial" -> looks for trial.json and trial.env
346
+
347
+ # Optional: Destination directory (base directory for service keys and sessions)
348
+ # If not specified, uses default platform paths:
349
+ # Unix: ~/.config/mcp-abap-adt
350
+ # Windows: %USERPROFILE%\Documents\mcp-abap-adt
351
+ # Uncomment and set if you need a custom path:
352
+ # destination_dir: ~/.config/mcp-abap-adt
351
353
  ```
352
354
 
353
355
  Integration tests will skip if `test-config.yaml` is not configured or contains placeholder values.
354
356
 
357
+ **Test Scenarios**:
358
+ - **Scenario 1 & 2**: Token lifecycle - login via browser and reuse token from previous scenario
359
+ - **Scenario 3**: Expired session + expired refresh token - provider should re-authenticate via browser
360
+ - **Token validation**: Explicit validation of token expiration in all scenarios
361
+
355
362
  **Note**:
356
- - BTP integration tests use `xsuaa.btp_destination` and require `BtpServiceKeyStore`/`BtpSessionStore` (without `sapUrl`)
357
- - ABAP integration tests use `abap.destination` and require `AbapServiceKeyStore`/`AbapSessionStore` (with `sapUrl`)
358
- - BTP/ABAP integration tests may open a browser for authentication if no refresh token is available. This is expected behavior.
363
+ - Integration tests use `AbapServiceKeyStore` and `AbapSessionStore` for loading service keys and sessions
364
+ - Tests may open a browser for authentication if no refresh token is available. This is expected behavior.
365
+ - Each test scenario uses a unique port (3101, 3102, 3103) to avoid port conflicts
366
+ - Tests use `browser: 'system'` for interactive authentication (not `'none'`)
359
367
 
360
368
  ### Debug Logging
361
369
 
@@ -363,26 +371,33 @@ To enable detailed logging during tests or runtime, set environment variables:
363
371
 
364
372
  ```bash
365
373
  # Enable logging for auth providers
366
- DEBUG_AUTH_PROVIDERS=true npm test
367
-
368
- # Or enable browser auth specific logging
369
- DEBUG_BROWSER_AUTH=true npm test
374
+ DEBUG_PROVIDER=true npm test
370
375
 
371
376
  # Set log level (debug, info, warn, error)
372
377
  LOG_LEVEL=debug npm test
373
378
  ```
374
379
 
375
- Logging shows:
380
+ Logging uses `@mcp-abap-adt/logger` package with structured logging:
376
381
  - Token exchange stages (what we send, what we receive)
377
- - Token information (lengths, previews)
382
+ - Token information (lengths, previews, expiration)
383
+ - Token validation checks (expiration, validity)
378
384
  - Errors with details
379
385
 
380
386
  Example output:
381
387
  ```
382
- [INTEGRATION] Exchanging code for token: https://.../oauth/token
383
- [INTEGRATION] Tokens received: accessToken(2263 chars), refreshToken(34 chars)
388
+ [INFO] ℹ️ [browserAuth] Exchanging code for token...
389
+ [INFO] ℹ️ Tokens received: accessToken(2263 chars), refreshToken(34 chars)
390
+ [DEBUG] 🐛 [BaseTokenProvider] Token validation check {"expiresAt":"2025-12-25 11:08:15 UTC","isValid":true}
391
+ [INFO] ℹ️ [browserAuth] Authorization URL: https://.../oauth/authorize?...
392
+ [INFO] ℹ️ [browserAuth] Browser: system
384
393
  ```
385
394
 
395
+ **Logging Features**:
396
+ - **Token Formatting**: Tokens are logged in truncated format (start...end) for security
397
+ - **Date Formatting**: Expiration dates are displayed in readable format (YYYY-MM-DD HH:MM:SS UTC) instead of ISO format
398
+ - **Browser Information**: Logs browser type and authorization URL for debugging
399
+ - **Token Lifecycle**: Detailed logging of token acquisition, validation, and refresh operations
400
+
386
401
  ## Dependencies
387
402
 
388
403
  - `@mcp-abap-adt/interfaces` (^0.2.2) - Interface definitions and error code constants
@@ -1,22 +1,12 @@
1
1
  /**
2
2
  * Configuration helpers for auth-providers tests
3
- * Loads test configuration from test-config.yaml (same format as auth-broker)
3
+ * Loads test configuration from test-config.yaml
4
4
  */
5
5
  export interface TestConfig {
6
- auth_broker?: {
7
- paths?: {
8
- service_keys_dir?: string;
9
- sessions_dir?: string;
10
- };
11
- abap?: {
12
- destination?: string;
13
- };
14
- xsuaa?: {
15
- btp_destination?: string;
16
- mcp_destination?: string;
17
- mcp_url?: string;
18
- };
19
- };
6
+ destination?: string;
7
+ destination_dir?: string;
8
+ service_key_path?: string;
9
+ session_path?: string;
20
10
  }
21
11
  /**
22
12
  * Load test configuration from YAML
@@ -26,26 +16,31 @@ export declare function loadTestConfig(): TestConfig;
26
16
  /**
27
17
  * Check if test config has real values (not placeholders)
28
18
  */
29
- export declare function hasRealConfig(config: TestConfig, section: 'abap' | 'xsuaa'): boolean;
19
+ export declare function hasRealConfigValue(config?: TestConfig): boolean;
30
20
  /**
31
- * Get ABAP destination from config
21
+ * Get destination from config
32
22
  */
33
- export declare function getAbapDestination(config?: TestConfig): string | null;
34
- /**
35
- * Get XSUAA destinations from config
36
- */
37
- export declare function getXsuaaDestinations(config?: TestConfig): {
38
- btp_destination: string | null;
39
- mcp_url: string | null;
40
- };
23
+ export declare function getDestination(config?: TestConfig): string | null;
41
24
  /**
42
25
  * Get service keys directory from config
43
- * Expands ~ to home directory
26
+ * Uses base_dir/service-keys or default platform path
44
27
  */
45
- export declare function getServiceKeysDir(config?: TestConfig): string | null;
28
+ export declare function getServiceKeysDir(config?: TestConfig): string;
46
29
  /**
47
30
  * Get sessions directory from config
48
- * Expands ~ to home directory
31
+ * Uses base_dir/sessions or default platform path
32
+ */
33
+ export declare function getSessionsDir(config?: TestConfig): string;
34
+ /**
35
+ * Get service key file path
36
+ * Returns full path to service key file
37
+ */
38
+ export declare function getServiceKeyPath(config?: TestConfig): string | null;
39
+ /**
40
+ * Get session file path
41
+ * Returns full path to session file
49
42
  */
50
- export declare function getSessionsDir(config?: TestConfig): string | null;
43
+ export declare function getSessionPath(config?: TestConfig): string | null;
44
+ export declare function getAbapDestination(config?: TestConfig): string | null;
45
+ export declare function hasRealConfig(config?: TestConfig, _section?: 'abap' | 'xsuaa'): boolean;
51
46
  //# sourceMappingURL=configHelpers.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"configHelpers.d.ts","sourceRoot":"","sources":["../../../src/__tests__/helpers/configHelpers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH,MAAM,WAAW,UAAU;IACzB,WAAW,CAAC,EAAE;QACZ,KAAK,CAAC,EAAE;YACN,gBAAgB,CAAC,EAAE,MAAM,CAAC;YAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;SACvB,CAAC;QACF,IAAI,CAAC,EAAE;YACL,WAAW,CAAC,EAAE,MAAM,CAAC;SACtB,CAAC;QACF,KAAK,CAAC,EAAE;YACN,eAAe,CAAC,EAAE,MAAM,CAAC;YACzB,eAAe,CAAC,EAAE,MAAM,CAAC;YACzB,OAAO,CAAC,EAAE,MAAM,CAAC;SAClB,CAAC;KACH,CAAC;CACH;AAkBD;;;GAGG;AACH,wBAAgB,cAAc,IAAI,UAAU,CAyD3C;AAED;;GAEG;AACH,wBAAgB,aAAa,CAC3B,MAAM,EAAE,UAAU,EAClB,OAAO,EAAE,MAAM,GAAG,OAAO,GACxB,OAAO,CAwBT;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAGrE;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG;IACzD,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB,CAOA;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAYpE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAYjE"}
1
+ {"version":3,"file":"configHelpers.d.ts","sourceRoot":"","sources":["../../../src/__tests__/helpers/configHelpers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAQH,MAAM,WAAW,UAAU;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAkBD;;;GAGG;AACH,wBAAgB,cAAc,IAAI,UAAU,CAyD3C;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,OAAO,CAO/D;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAGjE;AA+BD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,CAa7D;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,CAa1D;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAcpE;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAcjE;AAGD,wBAAgB,kBAAkB,CAAC,MAAM,CAAC,EAAE,UAAU,GAAG,MAAM,GAAG,IAAI,CAErE;AAED,wBAAgB,aAAa,CAC3B,MAAM,CAAC,EAAE,UAAU,EACnB,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,GAC1B,OAAO,CAET"}
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  /**
3
3
  * Configuration helpers for auth-providers tests
4
- * Loads test configuration from test-config.yaml (same format as auth-broker)
4
+ * Loads test configuration from test-config.yaml
5
5
  */
6
6
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
7
7
  if (k2 === undefined) k2 = k;
@@ -38,11 +38,14 @@ var __importStar = (this && this.__importStar) || (function () {
38
38
  })();
39
39
  Object.defineProperty(exports, "__esModule", { value: true });
40
40
  exports.loadTestConfig = loadTestConfig;
41
- exports.hasRealConfig = hasRealConfig;
42
- exports.getAbapDestination = getAbapDestination;
43
- exports.getXsuaaDestinations = getXsuaaDestinations;
41
+ exports.hasRealConfigValue = hasRealConfigValue;
42
+ exports.getDestination = getDestination;
44
43
  exports.getServiceKeysDir = getServiceKeysDir;
45
44
  exports.getSessionsDir = getSessionsDir;
45
+ exports.getServiceKeyPath = getServiceKeyPath;
46
+ exports.getSessionPath = getSessionPath;
47
+ exports.getAbapDestination = getAbapDestination;
48
+ exports.hasRealConfig = hasRealConfig;
46
49
  const fs = __importStar(require("node:fs"));
47
50
  const path = __importStar(require("node:path"));
48
51
  const yaml = __importStar(require("js-yaml"));
@@ -112,75 +115,118 @@ function loadTestConfig() {
112
115
  /**
113
116
  * Check if test config has real values (not placeholders)
114
117
  */
115
- function hasRealConfig(config, section) {
116
- if (!config.auth_broker) {
118
+ function hasRealConfigValue(config) {
119
+ const cfg = config || loadTestConfig();
120
+ if (!cfg.destination) {
117
121
  return false;
118
122
  }
119
- if (section === 'abap') {
120
- const abap = config.auth_broker.abap;
121
- if (!abap?.destination) {
122
- return false;
123
- }
124
- // Check if destination is not a placeholder
125
- return !abap.destination.includes('<') && !abap.destination.includes('>');
126
- }
127
- if (section === 'xsuaa') {
128
- const xsuaa = config.auth_broker.xsuaa;
129
- if (!xsuaa?.btp_destination) {
130
- return false;
131
- }
132
- // Check if values are not placeholders
133
- return !xsuaa.btp_destination.includes('<');
134
- }
135
- return false;
123
+ // Check if destination is not a placeholder
124
+ return !cfg.destination.includes('<') && !cfg.destination.includes('>');
136
125
  }
137
126
  /**
138
- * Get ABAP destination from config
127
+ * Get destination from config
139
128
  */
140
- function getAbapDestination(config) {
129
+ function getDestination(config) {
141
130
  const cfg = config || loadTestConfig();
142
- return cfg.auth_broker?.abap?.destination || null;
131
+ return cfg.destination || null;
132
+ }
133
+ /**
134
+ * Get default destination directory based on platform
135
+ */
136
+ function getDefaultDestinationDir() {
137
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
138
+ if (process.platform === 'win32') {
139
+ return path.join(homeDir, 'Documents', 'mcp-abap-adt');
140
+ }
141
+ return path.join(homeDir, '.config', 'mcp-abap-adt');
143
142
  }
144
143
  /**
145
- * Get XSUAA destinations from config
144
+ * Get destination directory from config or use default
146
145
  */
147
- function getXsuaaDestinations(config) {
146
+ function getDestinationDir(config) {
148
147
  const cfg = config || loadTestConfig();
149
- const xsuaa = cfg.auth_broker?.xsuaa;
150
- return {
151
- btp_destination: xsuaa?.btp_destination || null,
152
- mcp_url: xsuaa?.mcp_url || null,
153
- };
148
+ if (cfg.destination_dir) {
149
+ // Expand ~ to home directory
150
+ if (cfg.destination_dir.startsWith('~')) {
151
+ const homeDir = process.env.HOME || process.env.USERPROFILE || '';
152
+ return cfg.destination_dir.replace('~', homeDir);
153
+ }
154
+ return cfg.destination_dir;
155
+ }
156
+ return getDefaultDestinationDir();
154
157
  }
155
158
  /**
156
159
  * Get service keys directory from config
157
- * Expands ~ to home directory
160
+ * Uses base_dir/service-keys or default platform path
158
161
  */
159
162
  function getServiceKeysDir(config) {
160
163
  const cfg = config || loadTestConfig();
161
- const dir = cfg.auth_broker?.paths?.service_keys_dir;
162
- if (!dir)
163
- return null;
164
- // Expand ~ to home directory
165
- if (dir.startsWith('~')) {
166
- const homeDir = process.env.HOME || process.env.USERPROFILE || '';
167
- return dir.replace('~', homeDir);
164
+ // If service_key_path is specified, return its directory
165
+ if (cfg.service_key_path) {
166
+ const projectRoot = findProjectRoot();
167
+ const fullPath = path.resolve(projectRoot, cfg.service_key_path);
168
+ return path.dirname(fullPath);
168
169
  }
169
- return dir;
170
+ // Use destination_dir/service-keys
171
+ const destinationDir = getDestinationDir(cfg);
172
+ return path.join(destinationDir, 'service-keys');
170
173
  }
171
174
  /**
172
175
  * Get sessions directory from config
173
- * Expands ~ to home directory
176
+ * Uses base_dir/sessions or default platform path
174
177
  */
175
178
  function getSessionsDir(config) {
176
179
  const cfg = config || loadTestConfig();
177
- const dir = cfg.auth_broker?.paths?.sessions_dir;
178
- if (!dir)
180
+ // If session_path is specified, return its directory
181
+ if (cfg.session_path) {
182
+ const projectRoot = findProjectRoot();
183
+ const fullPath = path.resolve(projectRoot, cfg.session_path);
184
+ return path.dirname(fullPath);
185
+ }
186
+ // Use destination_dir/sessions
187
+ const destinationDir = getDestinationDir(cfg);
188
+ return path.join(destinationDir, 'sessions');
189
+ }
190
+ /**
191
+ * Get service key file path
192
+ * Returns full path to service key file
193
+ */
194
+ function getServiceKeyPath(config) {
195
+ const cfg = config || loadTestConfig();
196
+ const destination = cfg.destination;
197
+ if (!destination)
198
+ return null;
199
+ // If service_key_path is specified, use it
200
+ if (cfg.service_key_path) {
201
+ const projectRoot = findProjectRoot();
202
+ return path.resolve(projectRoot, cfg.service_key_path);
203
+ }
204
+ // Construct from directory + destination
205
+ const serviceKeysDir = getServiceKeysDir(cfg);
206
+ return path.join(serviceKeysDir, `${destination}.json`);
207
+ }
208
+ /**
209
+ * Get session file path
210
+ * Returns full path to session file
211
+ */
212
+ function getSessionPath(config) {
213
+ const cfg = config || loadTestConfig();
214
+ const destination = cfg.destination;
215
+ if (!destination)
179
216
  return null;
180
- // Expand ~ to home directory
181
- if (dir.startsWith('~')) {
182
- const homeDir = process.env.HOME || process.env.USERPROFILE || '';
183
- return dir.replace('~', homeDir);
217
+ // If session_path is specified, use it
218
+ if (cfg.session_path) {
219
+ const projectRoot = findProjectRoot();
220
+ return path.resolve(projectRoot, cfg.session_path);
184
221
  }
185
- return dir;
222
+ // Construct from directory + destination
223
+ const sessionsDir = getSessionsDir(cfg);
224
+ return path.join(sessionsDir, `${destination}.env`);
225
+ }
226
+ // Legacy functions for backward compatibility
227
+ function getAbapDestination(config) {
228
+ return getDestination(config);
229
+ }
230
+ function hasRealConfig(config, _section) {
231
+ return hasRealConfigValue(config);
186
232
  }
@@ -1 +1 @@
1
- {"version":3,"file":"browserAuth.d.ts","sourceRoot":"","sources":["../../src/auth/browserAuth.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAAE,oBAAoB,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAI9E,KAAK,iBAAiB,GAAG,oBAAoB,GAAG;IAC9C,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AA8BF;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,oBAAoB,EAChC,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,MAAa,EACnB,GAAG,CAAC,EAAE,OAAO,GAAG,IAAI,GACnB,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAgDzD;AA6BD;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,UAAU,EAAE,iBAAiB,EAC7B,OAAO,GAAE,MAAiB,EAC1B,MAAM,CAAC,EAAE,OAAO,EAChB,IAAI,GAAE,MAAa,GAClB,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA+fzD"}
1
+ {"version":3,"file":"browserAuth.d.ts","sourceRoot":"","sources":["../../src/auth/browserAuth.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAAE,oBAAoB,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAI9E,KAAK,iBAAiB,GAAG,oBAAoB,GAAG;IAC9C,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AA8BF;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,oBAAoB,EAChC,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,MAAa,EACnB,GAAG,CAAC,EAAE,OAAO,GAAG,IAAI,GACnB,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAgDzD;AA6BD;;;;;;;;GAQG;AACH,wBAAsB,gBAAgB,CACpC,UAAU,EAAE,iBAAiB,EAC7B,OAAO,GAAE,MAAiB,EAC1B,MAAM,CAAC,EAAE,OAAO,EAChB,IAAI,GAAE,MAAa,GAClB,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAqjBzD"}
@@ -146,7 +146,9 @@ async function startBrowserAuth(authConfig, browser = 'system', logger, port = 3
146
146
  }
147
147
  return new Promise((originalResolve, originalReject) => {
148
148
  let timeoutId = null;
149
+ let finishTimeoutId = null;
149
150
  let cleanupDone = false;
151
+ let resolved = false;
150
152
  const app = (0, express_1.default)();
151
153
  const server = http.createServer(app);
152
154
  // Disable keep-alive to ensure connections close immediately
@@ -164,6 +166,10 @@ async function startBrowserAuth(authConfig, browser = 'system', logger, port = 3
164
166
  clearTimeout(timeoutId);
165
167
  timeoutId = null;
166
168
  }
169
+ if (finishTimeoutId) {
170
+ clearTimeout(finishTimeoutId);
171
+ finishTimeoutId = null;
172
+ }
167
173
  if (server) {
168
174
  try {
169
175
  if (typeof server.closeAllConnections === 'function') {
@@ -189,8 +195,17 @@ async function startBrowserAuth(authConfig, browser = 'system', logger, port = 3
189
195
  }
190
196
  };
191
197
  const resolve = (value) => {
192
- if (timeoutId)
198
+ if (resolved)
199
+ return; // Prevent double resolution
200
+ resolved = true;
201
+ if (timeoutId) {
193
202
  clearTimeout(timeoutId);
203
+ timeoutId = null;
204
+ }
205
+ if (finishTimeoutId) {
206
+ clearTimeout(finishTimeoutId);
207
+ finishTimeoutId = null;
208
+ }
194
209
  removeCleanupListeners();
195
210
  originalResolve(value);
196
211
  };
@@ -212,10 +227,27 @@ async function startBrowserAuth(authConfig, browser = 'system', logger, port = 3
212
227
  }
213
228
  // Use provided authorization URL or build from authConfig
214
229
  const authorizationUrl = authConfig.authorizationUrl ?? getJwtAuthorizationUrl(authConfig, PORT);
230
+ log?.info(`[browserAuth] Authorization URL: ${authorizationUrl}`);
231
+ log?.info(`[browserAuth] Server listening on port: ${PORT}`);
232
+ // Verify port in redirect_uri matches server port
233
+ const redirectUriMatch = authorizationUrl.match(/redirect_uri=([^&]+)/);
234
+ if (redirectUriMatch) {
235
+ const redirectUri = decodeURIComponent(redirectUriMatch[1]);
236
+ const urlPortMatch = redirectUri.match(/localhost:(\d+)/);
237
+ if (urlPortMatch) {
238
+ const urlPort = parseInt(urlPortMatch[1], 10);
239
+ if (urlPort !== PORT) {
240
+ log?.warn(`[browserAuth] WARNING: Port mismatch! URL has port ${urlPort}, but server listens on ${PORT}`);
241
+ }
242
+ else {
243
+ log?.info(`[browserAuth] Port match: URL and server both use port ${PORT}`);
244
+ }
245
+ }
246
+ }
215
247
  // OAuth2 callback handler
216
248
  app.get('/callback', async (req, res) => {
217
249
  try {
218
- log?.info(`Callback received: ${req.url}`);
250
+ log?.info(`[browserAuth] Callback received: ${req.url}`);
219
251
  log?.debug(`Callback query: ${JSON.stringify(req.query)}`);
220
252
  // Check for OAuth2 error parameters
221
253
  const { error, error_description, error_uri } = req.query;
@@ -291,13 +323,14 @@ async function startBrowserAuth(authConfig, browser = 'system', logger, port = 3
291
323
  return reject(new Error(`OAuth2 authentication failed: ${errorMsg}${error_uri ? ` (${error_uri})` : ''}`));
292
324
  }
293
325
  const { code } = req.query;
326
+ log?.info(`[browserAuth] Callback code received: ${code ? 'yes' : 'no'}`);
294
327
  log?.debug(`Callback code received: ${code ? 'yes' : 'no'}`);
295
328
  if (!code || typeof code !== 'string') {
296
- log?.error(`Callback code missing`);
329
+ log?.error(`[browserAuth] Callback code missing`);
297
330
  res.status(400).send('Error: Authorization code missing');
298
331
  return reject(new Error('Authorization code missing'));
299
332
  }
300
- log?.debug(`Exchanging code for token`);
333
+ log?.info(`[browserAuth] Exchanging code for token...`);
301
334
  // Send success page
302
335
  const html = `<!DOCTYPE html>
303
336
  <html lang="en">
@@ -356,37 +389,45 @@ async function startBrowserAuth(authConfig, browser = 'system', logger, port = 3
356
389
  </div>
357
390
  </body>
358
391
  </html>`;
359
- // Send success page first and ensure response is finished
360
- res.send(html);
361
- // Wait for response to finish before closing server
362
- res.on('finish', () => {
363
- // Response finished, now we can safely close server
364
- });
365
- // Exchange code for tokens and close server
392
+ // Exchange code for tokens first
366
393
  try {
394
+ log?.info(`[browserAuth] Starting token exchange...`);
367
395
  const tokens = await exchangeCodeForToken(authConfig, code, PORT, log);
368
- log?.info(`Tokens received: accessToken(${tokens.accessToken?.length || 0} chars), refreshToken(${tokens.refreshToken?.length || 0} chars)`);
369
- // Close all connections first to ensure port is freed
370
- if (typeof server.closeAllConnections === 'function') {
371
- server.closeAllConnections();
372
- }
373
- // Close server after response is finished
374
- // This ensures the response connection is closed before server.close()
375
- const closeServer = () => {
396
+ log?.info(`[browserAuth] Tokens received: accessToken(${tokens.accessToken?.length || 0} chars), refreshToken(${tokens.refreshToken?.length || 0} chars)`);
397
+ // Send success page (non-blocking, doesn't affect promise)
398
+ res.send(html);
399
+ log?.info(`[browserAuth] Response sent, waiting for finish...`);
400
+ // Close all connections and server after response is sent
401
+ // Resolve promise AFTER server is closed to prevent Jest from hanging
402
+ let serverClosing = false;
403
+ const closeServerAndResolve = () => {
404
+ if (serverClosing)
405
+ return; // Prevent double execution
406
+ serverClosing = true;
407
+ if (finishTimeoutId) {
408
+ clearTimeout(finishTimeoutId);
409
+ finishTimeoutId = null;
410
+ }
411
+ log?.info(`[browserAuth] Response finished, closing server...`);
412
+ if (typeof server.closeAllConnections === 'function') {
413
+ server.closeAllConnections();
414
+ }
415
+ // Wait for server to close before resolving to prevent Jest from hanging
376
416
  server.close(() => {
377
417
  // Server closed - port should be freed
378
- log?.debug(`Server closed, port ${PORT} should be freed`);
418
+ log?.info(`[browserAuth] Server closed, port ${PORT} should be freed`);
419
+ // Resolve after server is fully closed
420
+ log?.info(`[browserAuth] Resolving promise with tokens...`);
421
+ resolve({
422
+ accessToken: tokens.accessToken,
423
+ refreshToken: tokens.refreshToken,
424
+ });
379
425
  });
380
426
  };
381
- if (res.finished) {
382
- // Response already finished, close immediately
383
- closeServer();
384
- }
385
- else {
386
- // Wait for response to finish
387
- res.once('finish', closeServer);
388
- }
389
- resolve(tokens);
427
+ // Wait for response to finish, but add timeout to prevent hanging
428
+ res.once('finish', closeServerAndResolve);
429
+ // Fallback: if finish event doesn't fire within 1 second, close anyway
430
+ finishTimeoutId = setTimeout(closeServerAndResolve, 1000);
390
431
  }
391
432
  catch (error) {
392
433
  if (typeof server.closeAllConnections === 'function') {
@@ -429,6 +470,7 @@ async function startBrowserAuth(authConfig, browser = 'system', logger, port = 3
429
470
  }
430
471
  });
431
472
  serverInstance = server.listen(PORT, async () => {
473
+ log?.info(`[browserAuth] Server started on port ${PORT}`);
432
474
  const browserApp = BROWSER_MAP[browser];
433
475
  // Handle 'none' and 'headless' modes - log URL and wait for callback
434
476
  // (for SSH/remote sessions or when browser should not be opened)
@@ -554,7 +596,7 @@ async function startBrowserAuth(authConfig, browser = 'system', logger, port = 3
554
596
  }
555
597
  }
556
598
  });
557
- // Timeout after 5 minutes
599
+ // Timeout after 30 seconds to prevent blocking consumer
558
600
  timeoutId = setTimeout(() => {
559
601
  if (serverInstance) {
560
602
  if (typeof server.closeAllConnections === 'function') {
@@ -567,8 +609,8 @@ async function startBrowserAuth(authConfig, browser = 'system', logger, port = 3
567
609
  log?.debug(`Server closed on timeout, port ${PORT} should be freed`);
568
610
  });
569
611
  }, 100);
570
- reject(new Error('Authentication timeout. Process aborted.'));
612
+ reject(new Error('Authentication timeout after 30 seconds. Please try again.'));
571
613
  }
572
- }, 5 * 60 * 1000);
614
+ }, 30 * 1000);
573
615
  });
574
616
  }
@@ -4,7 +4,7 @@
4
4
  * Uses authorization_code grant type with browser-based OAuth2 flow.
5
5
  * Supports pre-built authorization URLs and automatic refresh.
6
6
  */
7
- import type { ITokenResult, OAuth2GrantType } from '@mcp-abap-adt/interfaces';
7
+ import type { ILogger, ITokenResult, OAuth2GrantType } from '@mcp-abap-adt/interfaces';
8
8
  import { BaseTokenProvider } from './BaseTokenProvider';
9
9
  export interface AuthorizationCodeProviderConfig {
10
10
  uaaUrl: string;
@@ -15,6 +15,7 @@ export interface AuthorizationCodeProviderConfig {
15
15
  redirectPort?: number;
16
16
  accessToken?: string;
17
17
  refreshToken?: string;
18
+ logger?: ILogger;
18
19
  }
19
20
  /**
20
21
  * Authorization Code token provider
@@ -1 +1 @@
1
- {"version":3,"file":"AuthorizationCodeProvider.d.ts","sourceRoot":"","sources":["../../src/providers/AuthorizationCodeProvider.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAEV,YAAY,EACZ,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAIlC,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAExD,MAAM,WAAW,+BAA+B;IAE9C,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IAGrB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAG1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IAGtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;GAKG;AACH,qBAAa,yBAA0B,SAAQ,iBAAiB;IAC9D,OAAO,CAAC,MAAM,CAAkC;gBAEpC,MAAM,EAAE,+BAA+B;IAkC7C,SAAS,IAAI,OAAO,CAAC,YAAY,CAAC;IAIxC,SAAS,CAAC,WAAW,IAAI,eAAe;cAIxB,YAAY,IAAI,OAAO,CAAC,YAAY,CAAC;cAmCrC,cAAc,IAAI,OAAO,CAAC,YAAY,CAAC;CA2BxD"}
1
+ {"version":3,"file":"AuthorizationCodeProvider.d.ts","sourceRoot":"","sources":["../../src/providers/AuthorizationCodeProvider.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAEV,OAAO,EACP,YAAY,EACZ,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAIlC,OAAO,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAExD,MAAM,WAAW,+BAA+B;IAE9C,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IAGrB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAG1B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;IAGtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IAGtB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;;;;GAKG;AACH,qBAAa,yBAA0B,SAAQ,iBAAiB;IAC9D,OAAO,CAAC,MAAM,CAAkC;gBAEpC,MAAM,EAAE,+BAA+B;IA8D7C,SAAS,IAAI,OAAO,CAAC,YAAY,CAAC;IAIxC,SAAS,CAAC,WAAW,IAAI,eAAe;cAIxB,YAAY,IAAI,OAAO,CAAC,YAAY,CAAC;cA2ErC,cAAc,IAAI,OAAO,CAAC,YAAY,CAAC;CA0CxD"}
@@ -22,6 +22,17 @@ class AuthorizationCodeProvider extends BaseTokenProvider_1.BaseTokenProvider {
22
22
  constructor(config) {
23
23
  super();
24
24
  this.config = config;
25
+ this.logger = config.logger;
26
+ this.logger?.info('[AuthorizationCodeProvider] Provider created', {
27
+ uaaUrl: config.uaaUrl,
28
+ clientId: config.clientId,
29
+ hasAccessToken: !!config.accessToken,
30
+ hasRefreshToken: !!config.refreshToken,
31
+ accessToken: this.formatToken(config.accessToken),
32
+ refreshToken: this.formatToken(config.refreshToken),
33
+ browser: config.browser || 'none',
34
+ redirectPort: config.redirectPort || 3001,
35
+ });
25
36
  const missingFields = [];
26
37
  if (!config.uaaUrl) {
27
38
  missingFields.push('uaaUrl');
@@ -43,9 +54,19 @@ class AuthorizationCodeProvider extends BaseTokenProvider_1.BaseTokenProvider {
43
54
  this.authorizationToken = config.accessToken;
44
55
  // Parse expiration from JWT
45
56
  this.expiresAt = this.parseExpirationFromJWT(config.accessToken);
57
+ this.logger?.info('[AuthorizationCodeProvider] Initialized with access token', {
58
+ accessToken: this.formatToken(config.accessToken),
59
+ hasExpiresAt: !!this.expiresAt,
60
+ expiresAt: this.expiresAt
61
+ ? this.formatExpirationDate(this.expiresAt)
62
+ : undefined,
63
+ });
46
64
  }
47
65
  if (config.refreshToken) {
48
66
  this.refreshToken = config.refreshToken;
67
+ this.logger?.info('[AuthorizationCodeProvider] Initialized with refresh token', {
68
+ refreshToken: this.formatToken(config.refreshToken),
69
+ });
49
70
  }
50
71
  }
51
72
  async getTokens() {
@@ -68,8 +89,36 @@ class AuthorizationCodeProvider extends BaseTokenProvider_1.BaseTokenProvider {
68
89
  }
69
90
  // Use provided browser or default to 'none' (prints URL to console)
70
91
  const browser = this.config.browser || 'none';
71
- const result = await (0, browserAuth_1.startBrowserAuth)(authConfig, browser, undefined, // logger
72
- this.config.redirectPort || 3001);
92
+ const redirectPort = this.config.redirectPort || 3001;
93
+ // Build authorization URL for logging (same logic as in startBrowserAuth)
94
+ const authorizationUrl = authConfig.authorizationUrl ??
95
+ `${authConfig.uaaUrl}/oauth/authorize?client_id=${encodeURIComponent(authConfig.uaaClientId)}&redirect_uri=${encodeURIComponent(`http://localhost:${redirectPort}/callback`)}&response_type=code`;
96
+ this.logger?.info('[AuthorizationCodeProvider] Performing login via browser', {
97
+ browser,
98
+ redirectPort,
99
+ authorizationUrl,
100
+ uaaUrl: authConfig.uaaUrl,
101
+ clientId: authConfig.uaaClientId,
102
+ });
103
+ // Wrap startBrowserAuth with timeout
104
+ const timeoutMs = 30 * 1000; // 30 seconds
105
+ const timeoutPromise = new Promise((_, reject) => {
106
+ setTimeout(() => {
107
+ reject(new Error(`Authentication timeout after ${timeoutMs / 1000} seconds. Please try again.`));
108
+ }, timeoutMs);
109
+ });
110
+ const result = await Promise.race([
111
+ (0, browserAuth_1.startBrowserAuth)(authConfig, browser, this.logger || undefined, // Pass logger to browserAuth
112
+ redirectPort),
113
+ timeoutPromise,
114
+ ]);
115
+ this.logger?.info('[AuthorizationCodeProvider] Login completed', {
116
+ hasAccessToken: !!result.accessToken,
117
+ hasRefreshToken: !!result.refreshToken,
118
+ accessToken: this.formatToken(result.accessToken),
119
+ refreshToken: this.formatToken(result.refreshToken),
120
+ accessTokenLength: result.accessToken?.length || 0,
121
+ });
73
122
  // Parse expiration from JWT
74
123
  const expiresIn = this.calculateExpiresIn(result.accessToken);
75
124
  return {
@@ -83,9 +132,17 @@ class AuthorizationCodeProvider extends BaseTokenProvider_1.BaseTokenProvider {
83
132
  if (!this.refreshToken) {
84
133
  throw new Error('Refresh token is required for refresh');
85
134
  }
135
+ this.logger?.info('[AuthorizationCodeProvider] Refreshing token');
86
136
  // Try refresh first
87
137
  try {
88
138
  const result = await (0, tokenRefresher_1.refreshJwtToken)(this.refreshToken, this.config.uaaUrl, this.config.clientId, this.config.clientSecret);
139
+ this.logger?.info('[AuthorizationCodeProvider] Token refresh completed', {
140
+ hasAccessToken: !!result.accessToken,
141
+ hasRefreshToken: !!result.refreshToken,
142
+ newAccessToken: this.formatToken(result.accessToken),
143
+ newRefreshToken: this.formatToken(result.refreshToken),
144
+ oldRefreshToken: this.formatToken(this.refreshToken),
145
+ });
89
146
  const expiresIn = this.calculateExpiresIn(result.accessToken);
90
147
  return {
91
148
  authorizationToken: result.accessToken,
@@ -94,7 +151,10 @@ class AuthorizationCodeProvider extends BaseTokenProvider_1.BaseTokenProvider {
94
151
  expiresIn,
95
152
  };
96
153
  }
97
- catch (_error) {
154
+ catch (error) {
155
+ this.logger?.warn('[AuthorizationCodeProvider] Token refresh failed, falling back to login', {
156
+ error: error instanceof Error ? error.message : String(error),
157
+ });
98
158
  // Refresh failed - try login (will use uaaUrl + clientId to build URL)
99
159
  return await this.performLogin();
100
160
  }
@@ -7,7 +7,7 @@
7
7
  * - Expiration checking
8
8
  * - Automatic refresh/relogin
9
9
  */
10
- import type { ITokenProvider, ITokenResult, OAuth2GrantType } from '@mcp-abap-adt/interfaces';
10
+ import type { ILogger, ITokenProvider, ITokenResult, OAuth2GrantType } from '@mcp-abap-adt/interfaces';
11
11
  /**
12
12
  * Abstract base class for token providers
13
13
  *
@@ -21,6 +21,17 @@ export declare abstract class BaseTokenProvider implements ITokenProvider {
21
21
  protected authorizationToken?: string;
22
22
  protected refreshToken?: string;
23
23
  protected expiresAt?: number;
24
+ protected logger?: ILogger;
25
+ /**
26
+ * Format timestamp to readable date/time string
27
+ * @param timestamp Timestamp in milliseconds
28
+ * @returns Formatted date string (e.g., "2025-12-25 19:21:27 UTC")
29
+ */
30
+ protected formatExpirationDate(timestamp: number): string;
31
+ /**
32
+ * Format token for logging (start...end)
33
+ */
34
+ protected formatToken(token?: string): string | undefined;
24
35
  /**
25
36
  * Check if current token is valid (not expired)
26
37
  * @returns true if token exists and is not expired, false otherwise
@@ -1 +1 @@
1
- {"version":3,"file":"BaseTokenProvider.d.ts","sourceRoot":"","sources":["../../src/providers/BaseTokenProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EACV,cAAc,EACd,YAAY,EACZ,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAElC;;;;;;;;GAQG;AACH,8BAAsB,iBAAkB,YAAW,cAAc;IAC/D,SAAS,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACtC,SAAS,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAChC,SAAS,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAE7B;;;OAGG;IACH,SAAS,CAAC,YAAY,IAAI,OAAO;IASjC;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,YAAY,IAAI,OAAO,CAAC,YAAY,CAAC;IAExD;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,cAAc,IAAI,OAAO,CAAC,YAAY,CAAC;IAE1D;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,WAAW,IAAI,eAAe;IAEjD;;;;;;;;OAQG;IACG,SAAS,IAAI,OAAO,CAAC,YAAY,CAAC;IAqClC,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAS3E;;;OAGG;IACH,SAAS,CAAC,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI;IAWlD;;;;OAIG;IACH,SAAS,CAAC,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IA0BnE;;;;OAIG;IACH,SAAS,CAAC,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;CAShE"}
1
+ {"version":3,"file":"BaseTokenProvider.d.ts","sourceRoot":"","sources":["../../src/providers/BaseTokenProvider.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EACV,OAAO,EACP,cAAc,EACd,YAAY,EACZ,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAElC;;;;;;;;GAQG;AACH,8BAAsB,iBAAkB,YAAW,cAAc;IAC/D,SAAS,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACtC,SAAS,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAChC,SAAS,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC7B,SAAS,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAE3B;;;;OAIG;IACH,SAAS,CAAC,oBAAoB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;IAWzD;;OAEG;IACH,SAAS,CAAC,WAAW,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAMzD;;;OAGG;IACH,SAAS,CAAC,YAAY,IAAI,OAAO;IAyBjC;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,YAAY,IAAI,OAAO,CAAC,YAAY,CAAC;IAExD;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,cAAc,IAAI,OAAO,CAAC,YAAY,CAAC;IAE1D;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,WAAW,IAAI,eAAe;IAEjD;;;;;;;;OAQG;IACG,SAAS,IAAI,OAAO,CAAC,YAAY,CAAC;IAuElC,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAmB3E;;;OAGG;IACH,SAAS,CAAC,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI;IAoBlD;;;;OAIG;IACH,SAAS,CAAC,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IA0BnE;;;;OAIG;IACH,SAAS,CAAC,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;CAShE"}
@@ -23,17 +23,56 @@ class BaseTokenProvider {
23
23
  authorizationToken;
24
24
  refreshToken;
25
25
  expiresAt; // timestamp in milliseconds
26
+ logger;
27
+ /**
28
+ * Format timestamp to readable date/time string
29
+ * @param timestamp Timestamp in milliseconds
30
+ * @returns Formatted date string (e.g., "2025-12-25 19:21:27 UTC")
31
+ */
32
+ formatExpirationDate(timestamp) {
33
+ const date = new Date(timestamp);
34
+ const year = date.getUTCFullYear();
35
+ const month = String(date.getUTCMonth() + 1).padStart(2, '0');
36
+ const day = String(date.getUTCDate()).padStart(2, '0');
37
+ const hours = String(date.getUTCHours()).padStart(2, '0');
38
+ const minutes = String(date.getUTCMinutes()).padStart(2, '0');
39
+ const seconds = String(date.getUTCSeconds()).padStart(2, '0');
40
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds} UTC`;
41
+ }
42
+ /**
43
+ * Format token for logging (start...end)
44
+ */
45
+ formatToken(token) {
46
+ if (!token)
47
+ return undefined;
48
+ if (token.length <= 50)
49
+ return token;
50
+ return `${token.substring(0, 25)}...${token.substring(token.length - 25)}`;
51
+ }
26
52
  /**
27
53
  * Check if current token is valid (not expired)
28
54
  * @returns true if token exists and is not expired, false otherwise
29
55
  */
30
56
  isTokenValid() {
31
57
  if (!this.authorizationToken || !this.expiresAt) {
58
+ this.logger?.debug('[BaseTokenProvider] Token invalid: missing token or expiration', {
59
+ hasToken: !!this.authorizationToken,
60
+ hasExpiresAt: !!this.expiresAt,
61
+ });
32
62
  return false;
33
63
  }
34
64
  // Add 60 second buffer to account for clock skew and network latency
35
65
  const bufferMs = 60 * 1000;
36
- return Date.now() < this.expiresAt - bufferMs;
66
+ const now = Date.now();
67
+ const isValid = now < this.expiresAt - bufferMs;
68
+ this.logger?.debug('[BaseTokenProvider] Token validation check', {
69
+ now: this.formatExpirationDate(now),
70
+ expiresAt: this.formatExpirationDate(this.expiresAt),
71
+ expiresIn: Math.floor((this.expiresAt - now) / 1000),
72
+ isValid,
73
+ bufferMs,
74
+ });
75
+ return isValid;
37
76
  }
38
77
  /**
39
78
  * Main method - handles token lifecycle
@@ -45,12 +84,25 @@ class BaseTokenProvider {
45
84
  * @returns Promise that resolves to token result
46
85
  */
47
86
  async getTokens() {
87
+ this.logger?.debug('[BaseTokenProvider] getTokens called', {
88
+ hasToken: !!this.authorizationToken,
89
+ hasExpiresAt: !!this.expiresAt,
90
+ hasRefreshToken: !!this.refreshToken,
91
+ currentToken: this.formatToken(this.authorizationToken),
92
+ });
48
93
  // If token is valid, return cached
49
- if (this.isTokenValid()) {
94
+ const isValid = this.isTokenValid();
95
+ if (isValid) {
50
96
  const authorizationToken = this.authorizationToken;
51
97
  if (!authorizationToken) {
52
98
  throw new Error('Authorization token is missing.');
53
99
  }
100
+ this.logger?.info('[BaseTokenProvider] Returning cached valid token', {
101
+ token: this.formatToken(authorizationToken),
102
+ expiresIn: this.expiresAt
103
+ ? Math.floor((this.expiresAt - Date.now()) / 1000)
104
+ : undefined,
105
+ });
54
106
  return {
55
107
  authorizationToken,
56
108
  refreshToken: this.refreshToken,
@@ -62,12 +114,23 @@ class BaseTokenProvider {
62
114
  }
63
115
  // Try refresh if we have refresh token
64
116
  if (this.refreshToken) {
117
+ this.logger?.info('[BaseTokenProvider] Token invalid, attempting refresh', {
118
+ oldToken: this.formatToken(this.authorizationToken),
119
+ refreshToken: this.formatToken(this.refreshToken),
120
+ });
65
121
  try {
66
122
  const result = await this.performRefresh();
67
123
  this.updateTokens(result);
124
+ this.logger?.info('[BaseTokenProvider] Token refreshed successfully', {
125
+ newToken: this.formatToken(result.authorizationToken),
126
+ newRefreshToken: this.formatToken(result.refreshToken),
127
+ });
68
128
  return result;
69
129
  }
70
- catch (_error) {
130
+ catch (error) {
131
+ this.logger?.warn('[BaseTokenProvider] Refresh failed', {
132
+ error: error instanceof Error ? error.message : String(error),
133
+ });
71
134
  // Refresh failed - need to login
72
135
  // Clear refresh token as it's invalid
73
136
  this.refreshToken = undefined;
@@ -75,23 +138,37 @@ class BaseTokenProvider {
75
138
  }
76
139
  }
77
140
  // Perform login
141
+ this.logger?.info('[BaseTokenProvider] Token invalid and no refresh token, performing login');
78
142
  const result = await this.performLogin();
79
143
  this.updateTokens(result);
144
+ this.logger?.info('[BaseTokenProvider] Login completed', {
145
+ newToken: this.formatToken(result.authorizationToken),
146
+ newRefreshToken: this.formatToken(result.refreshToken),
147
+ });
80
148
  return result;
81
149
  }
82
150
  async validateToken(_token, _serviceUrl) {
151
+ this.logger?.debug('[BaseTokenProvider] Validating token');
83
152
  const expiresAt = this.parseExpirationFromJWT(_token);
84
153
  if (!expiresAt) {
154
+ this.logger?.warn('[BaseTokenProvider] Token validation failed: cannot parse expiration');
85
155
  return false;
86
156
  }
87
157
  const bufferMs = 60 * 1000;
88
- return Date.now() < expiresAt - bufferMs;
158
+ const isValid = Date.now() < expiresAt - bufferMs;
159
+ this.logger?.info('[BaseTokenProvider] Token validation result', {
160
+ isValid,
161
+ expiresAt: this.formatExpirationDate(expiresAt),
162
+ expiresIn: Math.floor((expiresAt - Date.now()) / 1000),
163
+ });
164
+ return isValid;
89
165
  }
90
166
  /**
91
167
  * Update internal token cache from result
92
168
  * @param result Token result to cache
93
169
  */
94
170
  updateTokens(result) {
171
+ const oldToken = this.formatToken(this.authorizationToken);
95
172
  this.authorizationToken = result.authorizationToken;
96
173
  this.refreshToken = result.refreshToken;
97
174
  if (result.expiresIn) {
@@ -101,6 +178,14 @@ class BaseTokenProvider {
101
178
  // Try to parse expiration from JWT if expiresIn not provided
102
179
  this.expiresAt = this.parseExpirationFromJWT(result.authorizationToken);
103
180
  }
181
+ this.logger?.info('[BaseTokenProvider] Tokens updated', {
182
+ oldToken,
183
+ newToken: this.formatToken(result.authorizationToken),
184
+ newRefreshToken: this.formatToken(result.refreshToken),
185
+ expiresAt: this.expiresAt
186
+ ? this.formatExpirationDate(this.expiresAt)
187
+ : undefined,
188
+ });
104
189
  }
105
190
  /**
106
191
  * Parse expiration time from JWT token
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-abap-adt/auth-providers",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Token providers for MCP ABAP ADT auth-broker",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",