@mcp-abap-adt/auth-providers 0.2.8 → 0.2.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/CHANGELOG.md CHANGED
@@ -7,6 +7,62 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.10] - 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
+ - **Structured Logging**: Replaced `console.log/info/warn/error` with `DefaultLogger` from `@mcp-abap-adt/logger` in test helpers
19
+ - Proper formatting with icons and level prefixes (ℹ️, 🐛, ⚠️, ❌)
20
+ - Respects `LOG_LEVEL` or `AUTH_LOG_LEVEL` environment variable
21
+ - Consistent logging format across all packages
22
+ - **Environment Variable Names**: Added short names for debug flags (backward compatible)
23
+ - `DEBUG_PROVIDER=true` (short) or `DEBUG_AUTH_PROVIDERS=true` (long)
24
+ - Both names are supported for backward compatibility
25
+
26
+ ### Added
27
+ - **Browser Auth Timeout**: Added 30-second timeout for browser-based authentication
28
+ - Prevents provider from blocking consumer indefinitely when user doesn't complete authentication
29
+ - Timeout error is thrown if authentication is not completed within 30 seconds
30
+ - Helps prevent hanging in automated tests and CI/CD environments
31
+ - **Logger Package Dependency**: Added `@mcp-abap-adt/logger` to devDependencies
32
+ - Required for `DefaultLogger` and `getLogLevel()` utilities in test helpers
33
+ - Added `pino` and `pino-pretty` to devDependencies to support PinoLogger initialization
34
+
35
+ ### Fixed
36
+ - **Test Hanging Issues**: Fixed Jest tests hanging after completion
37
+ - Added `forceExit: true` to Jest configuration to force exit after tests complete
38
+ - Improved cleanup of HTTP server and timers to prevent open handles
39
+ - Added protection against double execution of server close handlers
40
+ - Proper cleanup of `finishTimeoutId` timer in all scenarios (success, error, timeout)
41
+ - Server now resolves promise only after fully closing to ensure Jest can exit cleanly
42
+
43
+ ## [0.2.9] - 2025-12-25
44
+
45
+ ### Changed
46
+ - **Logging Improvements**: Enhanced logging for better debugging and readability
47
+ - **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`)
48
+ - **Token Formatting**: Tokens are logged in truncated format (start...end) for security and readability
49
+ - **Browser Information**: Added logging of browser type and authorization URL before starting browser authentication
50
+ - **Token Lifecycle**: Improved logging of token acquisition, validation, and refresh operations with formatted dates
51
+
52
+ ### Added
53
+ - **Browser Auth Timeout**: Added 30-second timeout for browser-based authentication
54
+ - Prevents provider from blocking consumer indefinitely when user doesn't complete authentication
55
+ - Timeout error is thrown if authentication is not completed within 30 seconds
56
+ - Helps prevent hanging in automated tests and CI/CD environments
57
+
58
+ ### Fixed
59
+ - **Test Hanging Issues**: Fixed Jest tests hanging after completion
60
+ - Added `forceExit: true` to Jest configuration to force exit after tests complete
61
+ - Improved cleanup of HTTP server and timers to prevent open handles
62
+ - Added protection against double execution of server close handlers
63
+ - Proper cleanup of `finishTimeoutId` timer in all scenarios (success, error, timeout)
64
+ - Server now resolves promise only after fully closing to ensure Jest can exit cleanly
65
+
10
66
  ## [0.2.8] - 2025-12-24
11
67
 
12
68
  ### 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
 
@@ -368,9 +370,20 @@ Integration tests will skip if `test-config.yaml` is not configured or contains
368
370
  To enable detailed logging during tests or runtime, set environment variables:
369
371
 
370
372
  ```bash
371
- # Enable logging for auth providers
373
+ # Enable logging for auth providers (short name)
372
374
  DEBUG_PROVIDER=true npm test
373
375
 
376
+ # Or use long name (backward compatibility)
377
+ DEBUG_AUTH_PROVIDERS=true npm test
378
+
379
+ # Or enable via general DEBUG variable
380
+ DEBUG=true npm test
381
+
382
+ # Or include in DEBUG list
383
+ DEBUG=provider npm test
384
+ # Or
385
+ DEBUG=auth-providers npm test
386
+
374
387
  # Set log level (debug, info, warn, error)
375
388
  LOG_LEVEL=debug npm test
376
389
  ```
@@ -385,9 +398,17 @@ Example output:
385
398
  ```
386
399
  [INFO] ℹ️ [browserAuth] Exchanging code for token...
387
400
  [INFO] ℹ️ Tokens received: accessToken(2263 chars), refreshToken(34 chars)
388
- [DEBUG] 🐛 [BaseTokenProvider] Token validation check {"expiresAt":"2025-12-25T11:08:15.000Z","isValid":true}
401
+ [DEBUG] 🐛 [BaseTokenProvider] Token validation check {"expiresAt":"2025-12-25 11:08:15 UTC","isValid":true}
402
+ [INFO] ℹ️ [browserAuth] Authorization URL: https://.../oauth/authorize?...
403
+ [INFO] ℹ️ [browserAuth] Browser: system
389
404
  ```
390
405
 
406
+ **Logging Features**:
407
+ - **Token Formatting**: Tokens are logged in truncated format (start...end) for security
408
+ - **Date Formatting**: Expiration dates are displayed in readable format (YYYY-MM-DD HH:MM:SS UTC) instead of ISO format
409
+ - **Browser Information**: Logs browser type and authorization URL for debugging
410
+ - **Token Lifecycle**: Detailed logging of token acquisition, validation, and refresh operations
411
+
391
412
  ## Dependencies
392
413
 
393
414
  - `@mcp-abap-adt/interfaces` (^0.2.2) - Interface definitions and error code constants
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Test logger with environment variable control
3
+ * Uses DefaultLogger from @mcp-abap-adt/logger for proper formatting
3
4
  */
4
5
  import type { ILogger } from '@mcp-abap-adt/interfaces';
5
6
  export declare function createTestLogger(prefix?: string): ILogger;
@@ -1 +1 @@
1
- {"version":3,"file":"testLogger.d.ts","sourceRoot":"","sources":["../../../src/__tests__/helpers/testLogger.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAgBxD,wBAAgB,gBAAgB,CAAC,MAAM,GAAE,MAAe,GAAG,OAAO,CA6BjE"}
1
+ {"version":3,"file":"testLogger.d.ts","sourceRoot":"","sources":["../../../src/__tests__/helpers/testLogger.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,0BAA0B,CAAC;AAGxD,wBAAgB,gBAAgB,CAAC,MAAM,GAAE,MAAe,GAAG,OAAO,CAwCjE"}
@@ -1,43 +1,44 @@
1
1
  "use strict";
2
2
  /**
3
3
  * Test logger with environment variable control
4
+ * Uses DefaultLogger from @mcp-abap-adt/logger for proper formatting
4
5
  */
5
6
  Object.defineProperty(exports, "__esModule", { value: true });
6
7
  exports.createTestLogger = createTestLogger;
7
- function getLogLevel() {
8
- const level = process.env.LOG_LEVEL?.toLowerCase() || 'info';
9
- const levels = ['debug', 'info', 'warn', 'error'];
10
- return levels.includes(level) ? level : 'info';
11
- }
12
- function shouldLog(level) {
13
- const currentLevel = getLogLevel();
14
- const levels = ['debug', 'info', 'warn', 'error'];
15
- return levels.indexOf(level) >= levels.indexOf(currentLevel);
16
- }
8
+ const logger_1 = require("@mcp-abap-adt/logger");
17
9
  function createTestLogger(prefix = 'TEST') {
18
- const level = getLogLevel();
19
- const enabled = process.env.DEBUG_AUTH_PROVIDERS === 'true' ||
20
- process.env.DEBUG_BROWSER_AUTH === 'true' ||
21
- process.env.DEBUG === 'true';
10
+ // Check if logging is enabled
11
+ const isEnabled = () => {
12
+ return (process.env.DEBUG_PROVIDER === 'true' ||
13
+ process.env.DEBUG_AUTH_PROVIDERS === 'true' ||
14
+ process.env.DEBUG_BROWSER_AUTH === 'true' ||
15
+ process.env.DEBUG === 'true' ||
16
+ process.env.DEBUG?.includes('provider') === true ||
17
+ process.env.DEBUG?.includes('auth-providers') === true);
18
+ };
19
+ // Create DefaultLogger with appropriate log level
20
+ // getLogLevel respects AUTH_LOG_LEVEL env var and defaults to INFO
21
+ const baseLogger = new logger_1.DefaultLogger((0, logger_1.getLogLevel)());
22
+ // Return wrapper that checks if logging is enabled
22
23
  return {
23
24
  debug: (message, meta) => {
24
- if (enabled && shouldLog('debug')) {
25
- console.debug(`[${prefix}] [DEBUG] ${message}`, meta || '');
25
+ if (isEnabled()) {
26
+ baseLogger.debug(`[${prefix}] ${message}`, meta);
26
27
  }
27
28
  },
28
29
  info: (message, meta) => {
29
- if (enabled && shouldLog('info')) {
30
- console.info(`[${prefix}] ${message}`, meta || '');
30
+ if (isEnabled()) {
31
+ baseLogger.info(`[${prefix}] ${message}`, meta);
31
32
  }
32
33
  },
33
34
  warn: (message, meta) => {
34
- if (enabled && shouldLog('warn')) {
35
- console.warn(`[${prefix}] [WARN] ${message}`, meta || '');
35
+ if (isEnabled()) {
36
+ baseLogger.warn(`[${prefix}] ${message}`, meta);
36
37
  }
37
38
  },
38
39
  error: (message, meta) => {
39
- if (enabled && shouldLog('error')) {
40
- console.error(`[${prefix}] [ERROR] ${message}`, meta || '');
40
+ if (isEnabled()) {
41
+ baseLogger.error(`[${prefix}] ${message}`, meta);
41
42
  }
42
43
  },
43
44
  };
@@ -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,CAihBzD"}
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
  };
@@ -379,24 +394,40 @@ async function startBrowserAuth(authConfig, browser = 'system', logger, port = 3
379
394
  log?.info(`[browserAuth] Starting token exchange...`);
380
395
  const tokens = await exchangeCodeForToken(authConfig, code, PORT, log);
381
396
  log?.info(`[browserAuth] Tokens received: accessToken(${tokens.accessToken?.length || 0} chars), refreshToken(${tokens.refreshToken?.length || 0} chars)`);
382
- // Resolve promise FIRST - this allows test to continue immediately
383
- log?.info(`[browserAuth] Resolving promise with tokens...`);
384
- resolve(tokens);
385
- log?.info(`[browserAuth] Promise resolved, sending response...`);
386
397
  // Send success page (non-blocking, doesn't affect promise)
387
398
  res.send(html);
388
399
  log?.info(`[browserAuth] Response sent, waiting for finish...`);
389
400
  // Close all connections and server after response is sent
390
- res.once('finish', () => {
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
+ }
391
411
  log?.info(`[browserAuth] Response finished, closing server...`);
392
412
  if (typeof server.closeAllConnections === 'function') {
393
413
  server.closeAllConnections();
394
414
  }
415
+ // Wait for server to close before resolving to prevent Jest from hanging
395
416
  server.close(() => {
396
417
  // Server closed - port should be freed
397
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
+ });
398
425
  });
399
- });
426
+ };
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);
400
431
  }
401
432
  catch (error) {
402
433
  if (typeof server.closeAllConnections === 'function') {
@@ -565,7 +596,7 @@ async function startBrowserAuth(authConfig, browser = 'system', logger, port = 3
565
596
  }
566
597
  }
567
598
  });
568
- // Timeout after 5 minutes
599
+ // Timeout after 30 seconds to prevent blocking consumer
569
600
  timeoutId = setTimeout(() => {
570
601
  if (serverInstance) {
571
602
  if (typeof server.closeAllConnections === 'function') {
@@ -578,8 +609,8 @@ async function startBrowserAuth(authConfig, browser = 'system', logger, port = 3
578
609
  log?.debug(`Server closed on timeout, port ${PORT} should be freed`);
579
610
  });
580
611
  }, 100);
581
- reject(new Error('Authentication timeout. Process aborted.'));
612
+ reject(new Error('Authentication timeout after 30 seconds. Please try again.'));
582
613
  }
583
- }, 5 * 60 * 1000);
614
+ }, 30 * 1000);
584
615
  });
585
616
  }
@@ -1 +1 @@
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;IAuD7C,SAAS,IAAI,OAAO,CAAC,YAAY,CAAC;IAIxC,SAAS,CAAC,WAAW,IAAI,eAAe;cAIxB,YAAY,IAAI,OAAO,CAAC,YAAY,CAAC;cA+CrC,cAAc,IAAI,OAAO,CAAC,YAAY,CAAC;CAsCxD"}
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"}
@@ -28,6 +28,8 @@ class AuthorizationCodeProvider extends BaseTokenProvider_1.BaseTokenProvider {
28
28
  clientId: config.clientId,
29
29
  hasAccessToken: !!config.accessToken,
30
30
  hasRefreshToken: !!config.refreshToken,
31
+ accessToken: this.formatToken(config.accessToken),
32
+ refreshToken: this.formatToken(config.refreshToken),
31
33
  browser: config.browser || 'none',
32
34
  redirectPort: config.redirectPort || 3001,
33
35
  });
@@ -53,15 +55,18 @@ class AuthorizationCodeProvider extends BaseTokenProvider_1.BaseTokenProvider {
53
55
  // Parse expiration from JWT
54
56
  this.expiresAt = this.parseExpirationFromJWT(config.accessToken);
55
57
  this.logger?.info('[AuthorizationCodeProvider] Initialized with access token', {
58
+ accessToken: this.formatToken(config.accessToken),
56
59
  hasExpiresAt: !!this.expiresAt,
57
60
  expiresAt: this.expiresAt
58
- ? new Date(this.expiresAt).toISOString()
61
+ ? this.formatExpirationDate(this.expiresAt)
59
62
  : undefined,
60
63
  });
61
64
  }
62
65
  if (config.refreshToken) {
63
66
  this.refreshToken = config.refreshToken;
64
- this.logger?.info('[AuthorizationCodeProvider] Initialized with refresh token');
67
+ this.logger?.info('[AuthorizationCodeProvider] Initialized with refresh token', {
68
+ refreshToken: this.formatToken(config.refreshToken),
69
+ });
65
70
  }
66
71
  }
67
72
  async getTokens() {
@@ -71,10 +76,6 @@ class AuthorizationCodeProvider extends BaseTokenProvider_1.BaseTokenProvider {
71
76
  return interfaces_1.AUTH_TYPE_AUTHORIZATION_CODE;
72
77
  }
73
78
  async performLogin() {
74
- this.logger?.info('[AuthorizationCodeProvider] Performing login via browser', {
75
- redirectPort: this.config.redirectPort || 3001,
76
- browser: this.config.browser || 'none',
77
- });
78
79
  // Build authorization config
79
80
  // If authorizationUrl is provided, use it; otherwise startBrowserAuth will build it from uaaUrl + clientId
80
81
  const authConfig = {
@@ -88,11 +89,34 @@ class AuthorizationCodeProvider extends BaseTokenProvider_1.BaseTokenProvider {
88
89
  }
89
90
  // Use provided browser or default to 'none' (prints URL to console)
90
91
  const browser = this.config.browser || 'none';
91
- const result = await (0, browserAuth_1.startBrowserAuth)(authConfig, browser, this.logger || undefined, // Pass logger to browserAuth
92
- 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
+ ]);
93
115
  this.logger?.info('[AuthorizationCodeProvider] Login completed', {
94
116
  hasAccessToken: !!result.accessToken,
95
117
  hasRefreshToken: !!result.refreshToken,
118
+ accessToken: this.formatToken(result.accessToken),
119
+ refreshToken: this.formatToken(result.refreshToken),
96
120
  accessTokenLength: result.accessToken?.length || 0,
97
121
  });
98
122
  // Parse expiration from JWT
@@ -115,6 +139,9 @@ class AuthorizationCodeProvider extends BaseTokenProvider_1.BaseTokenProvider {
115
139
  this.logger?.info('[AuthorizationCodeProvider] Token refresh completed', {
116
140
  hasAccessToken: !!result.accessToken,
117
141
  hasRefreshToken: !!result.refreshToken,
142
+ newAccessToken: this.formatToken(result.accessToken),
143
+ newRefreshToken: this.formatToken(result.refreshToken),
144
+ oldRefreshToken: this.formatToken(this.refreshToken),
118
145
  });
119
146
  const expiresIn = this.calculateExpiresIn(result.accessToken);
120
147
  return {
@@ -22,6 +22,16 @@ export declare abstract class BaseTokenProvider implements ITokenProvider {
22
22
  protected refreshToken?: string;
23
23
  protected expiresAt?: number;
24
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;
25
35
  /**
26
36
  * Check if current token is valid (not expired)
27
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,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;;;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;IA2DlC,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;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"}
@@ -24,6 +24,31 @@ class BaseTokenProvider {
24
24
  refreshToken;
25
25
  expiresAt; // timestamp in milliseconds
26
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
+ }
27
52
  /**
28
53
  * Check if current token is valid (not expired)
29
54
  * @returns true if token exists and is not expired, false otherwise
@@ -41,8 +66,8 @@ class BaseTokenProvider {
41
66
  const now = Date.now();
42
67
  const isValid = now < this.expiresAt - bufferMs;
43
68
  this.logger?.debug('[BaseTokenProvider] Token validation check', {
44
- now: new Date(now).toISOString(),
45
- expiresAt: new Date(this.expiresAt).toISOString(),
69
+ now: this.formatExpirationDate(now),
70
+ expiresAt: this.formatExpirationDate(this.expiresAt),
46
71
  expiresIn: Math.floor((this.expiresAt - now) / 1000),
47
72
  isValid,
48
73
  bufferMs,
@@ -63,6 +88,7 @@ class BaseTokenProvider {
63
88
  hasToken: !!this.authorizationToken,
64
89
  hasExpiresAt: !!this.expiresAt,
65
90
  hasRefreshToken: !!this.refreshToken,
91
+ currentToken: this.formatToken(this.authorizationToken),
66
92
  });
67
93
  // If token is valid, return cached
68
94
  const isValid = this.isTokenValid();
@@ -72,6 +98,7 @@ class BaseTokenProvider {
72
98
  throw new Error('Authorization token is missing.');
73
99
  }
74
100
  this.logger?.info('[BaseTokenProvider] Returning cached valid token', {
101
+ token: this.formatToken(authorizationToken),
75
102
  expiresIn: this.expiresAt
76
103
  ? Math.floor((this.expiresAt - Date.now()) / 1000)
77
104
  : undefined,
@@ -87,11 +114,17 @@ class BaseTokenProvider {
87
114
  }
88
115
  // Try refresh if we have refresh token
89
116
  if (this.refreshToken) {
90
- this.logger?.info('[BaseTokenProvider] Token invalid, attempting refresh');
117
+ this.logger?.info('[BaseTokenProvider] Token invalid, attempting refresh', {
118
+ oldToken: this.formatToken(this.authorizationToken),
119
+ refreshToken: this.formatToken(this.refreshToken),
120
+ });
91
121
  try {
92
122
  const result = await this.performRefresh();
93
123
  this.updateTokens(result);
94
- this.logger?.info('[BaseTokenProvider] Token refreshed successfully');
124
+ this.logger?.info('[BaseTokenProvider] Token refreshed successfully', {
125
+ newToken: this.formatToken(result.authorizationToken),
126
+ newRefreshToken: this.formatToken(result.refreshToken),
127
+ });
95
128
  return result;
96
129
  }
97
130
  catch (error) {
@@ -108,7 +141,10 @@ class BaseTokenProvider {
108
141
  this.logger?.info('[BaseTokenProvider] Token invalid and no refresh token, performing login');
109
142
  const result = await this.performLogin();
110
143
  this.updateTokens(result);
111
- this.logger?.info('[BaseTokenProvider] Login completed');
144
+ this.logger?.info('[BaseTokenProvider] Login completed', {
145
+ newToken: this.formatToken(result.authorizationToken),
146
+ newRefreshToken: this.formatToken(result.refreshToken),
147
+ });
112
148
  return result;
113
149
  }
114
150
  async validateToken(_token, _serviceUrl) {
@@ -122,7 +158,7 @@ class BaseTokenProvider {
122
158
  const isValid = Date.now() < expiresAt - bufferMs;
123
159
  this.logger?.info('[BaseTokenProvider] Token validation result', {
124
160
  isValid,
125
- expiresAt: new Date(expiresAt).toISOString(),
161
+ expiresAt: this.formatExpirationDate(expiresAt),
126
162
  expiresIn: Math.floor((expiresAt - Date.now()) / 1000),
127
163
  });
128
164
  return isValid;
@@ -132,6 +168,7 @@ class BaseTokenProvider {
132
168
  * @param result Token result to cache
133
169
  */
134
170
  updateTokens(result) {
171
+ const oldToken = this.formatToken(this.authorizationToken);
135
172
  this.authorizationToken = result.authorizationToken;
136
173
  this.refreshToken = result.refreshToken;
137
174
  if (result.expiresIn) {
@@ -141,6 +178,14 @@ class BaseTokenProvider {
141
178
  // Try to parse expiration from JWT if expiresIn not provided
142
179
  this.expiresAt = this.parseExpirationFromJWT(result.authorizationToken);
143
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
+ });
144
189
  }
145
190
  /**
146
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.8",
3
+ "version": "0.2.10",
4
4
  "description": "Token providers for MCP ABAP ADT auth-broker",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",