@mcp-abap-adt/auth-broker 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.
@@ -14,6 +14,23 @@ const noOpLogger = {
14
14
  warn: () => { },
15
15
  debug: () => { },
16
16
  };
17
+ /**
18
+ * Helper function to check if error has a code property
19
+ */
20
+ // biome-ignore lint/suspicious/noExplicitAny: Helper function needs to accept any error type
21
+ function hasErrorCode(error) {
22
+ return (error !== null &&
23
+ typeof error === 'object' &&
24
+ 'code' in error &&
25
+ typeof error.code === 'string');
26
+ }
27
+ /**
28
+ * Helper function to get error message safely
29
+ */
30
+ // biome-ignore lint/suspicious/noExplicitAny: Helper function needs to accept any error type
31
+ function getErrorMessage(error) {
32
+ return error instanceof Error ? error.message : String(error);
33
+ }
17
34
  /**
18
35
  * AuthBroker manages JWT authentication tokens for destinations
19
36
  */
@@ -94,82 +111,55 @@ class AuthBroker {
94
111
  this.logger?.debug(`AuthBroker initialized: sessionStore(ok), serviceKeyStore(${hasServiceKeyStore ? 'ok' : 'none'}), tokenProvider(ok)`);
95
112
  }
96
113
  /**
97
- * Get authentication token for destination.
98
- * Uses tokenProvider for all authentication operations (browser-based authorization).
99
- *
100
- * **Flow:**
101
- * **Step 0: Initialize Session with Token (if needed)**
102
- * - Check if session has `authorizationToken` AND UAA credentials
103
- * - If both are empty AND serviceKeyStore is available:
104
- * - Get UAA credentials from service key
105
- * - Use tokenProvider for browser-based authentication
106
- * - Save token and refresh token to session
107
- *
108
- * **Step 1: Token Validation**
109
- * - If token exists in session, validate it (if provider supports validation)
110
- * - If valid → return token
111
- * - If invalid or no token → continue to refresh
112
- *
113
- * **Step 2: Refresh Token Flow**
114
- * - Check if refresh token exists in session
115
- * - If refresh token exists:
116
- * - Use tokenProvider to refresh token (browser-based or refresh grant)
117
- * - Save new token to session
118
- * - Return new token
119
- * - Otherwise → proceed to Step 3
120
- *
121
- * **Step 3: New Token Flow**
122
- * - Get UAA credentials from session or service key
123
- * - Use tokenProvider for browser-based authentication
124
- * - Save new token to session
125
- * - Return new token
126
- *
127
- * **Important Notes:**
128
- * - All authentication is handled by tokenProvider (e.g., XSUAA provider)
129
- * - Provider uses browser-based authorization to ensure proper role assignment
130
- * - Direct UAA HTTP requests are not used to avoid role assignment issues
131
- *
132
- * @param destination Destination name (e.g., "TRIAL")
133
- * @returns Promise that resolves to JWT token string
134
- * @throws Error if session initialization fails or authentication failed
114
+ * Load session data (connection and authorization configs)
135
115
  */
136
- async getToken(destination) {
137
- this.logger?.debug(`Getting token for destination: ${destination}`);
138
- // Step 0: Initialize Session with Token (if needed)
116
+ async loadSessionData(destination) {
139
117
  let connConfig = null;
140
118
  let authConfig = null;
141
119
  try {
142
120
  connConfig = await this.sessionStore.getConnectionConfig(destination);
143
121
  }
144
122
  catch (error) {
145
- // Handle typed store errors from session store
146
- if (error.code === interfaces_1.STORE_ERROR_CODES.FILE_NOT_FOUND) {
147
- this.logger?.debug(`Session file not found for ${destination}: ${error.filePath || 'unknown path'}`);
148
- }
149
- else if (error.code === interfaces_1.STORE_ERROR_CODES.PARSE_ERROR) {
150
- this.logger?.warn(`Failed to parse session file for ${destination}: ${error.filePath || 'unknown path'} - ${error.message}`);
123
+ if (hasErrorCode(error)) {
124
+ if (error.code === interfaces_1.STORE_ERROR_CODES.FILE_NOT_FOUND) {
125
+ this.logger?.debug(`Session file not found for ${destination}: ${error.filePath || 'unknown path'}`);
126
+ }
127
+ else if (error.code === interfaces_1.STORE_ERROR_CODES.PARSE_ERROR) {
128
+ this.logger?.warn(`Failed to parse session file for ${destination}: ${error.filePath || 'unknown path'} - ${getErrorMessage(error)}`);
129
+ }
130
+ else {
131
+ this.logger?.warn(`Failed to get connection config from session store for ${destination}: ${getErrorMessage(error)}`);
132
+ }
151
133
  }
152
134
  else {
153
- this.logger?.warn(`Failed to get connection config from session store for ${destination}: ${error.message}`);
135
+ this.logger?.warn(`Failed to get connection config from session store for ${destination}: ${getErrorMessage(error)}`);
154
136
  }
155
137
  }
156
138
  try {
157
139
  authConfig = await this.sessionStore.getAuthorizationConfig(destination);
158
140
  }
159
141
  catch (error) {
160
- // Handle typed store errors from session store
161
- if (error.code === interfaces_1.STORE_ERROR_CODES.FILE_NOT_FOUND) {
162
- this.logger?.debug(`Session file not found for ${destination}: ${error.filePath || 'unknown path'}`);
163
- }
164
- else if (error.code === interfaces_1.STORE_ERROR_CODES.PARSE_ERROR) {
165
- this.logger?.warn(`Failed to parse session file for ${destination}: ${error.filePath || 'unknown path'} - ${error.message}`);
142
+ if (hasErrorCode(error)) {
143
+ if (error.code === interfaces_1.STORE_ERROR_CODES.FILE_NOT_FOUND) {
144
+ this.logger?.debug(`Session file not found for ${destination}: ${error.filePath || 'unknown path'}`);
145
+ }
146
+ else if (error.code === interfaces_1.STORE_ERROR_CODES.PARSE_ERROR) {
147
+ this.logger?.warn(`Failed to parse session file for ${destination}: ${error.filePath || 'unknown path'} - ${getErrorMessage(error)}`);
148
+ }
149
+ else {
150
+ this.logger?.warn(`Failed to get authorization config from session store for ${destination}: ${getErrorMessage(error)}`);
151
+ }
166
152
  }
167
153
  else {
168
- this.logger?.warn(`Failed to get authorization config from session store for ${destination}: ${error.message}`);
154
+ this.logger?.warn(`Failed to get authorization config from session store for ${destination}: ${getErrorMessage(error)}`);
169
155
  }
170
156
  }
171
- // Check if session has serviceUrl (required)
172
- // If not in session, try to get it from serviceKeyStore
157
+ return { connConfig, authConfig };
158
+ }
159
+ /**
160
+ * Get serviceUrl from session or service key store
161
+ */
162
+ async getServiceUrl(destination, connConfig) {
173
163
  let serviceUrl = connConfig?.serviceUrl;
174
164
  if (!serviceUrl && this.serviceKeyStore) {
175
165
  try {
@@ -180,15 +170,19 @@ class AuthBroker {
180
170
  }
181
171
  }
182
172
  catch (error) {
183
- // Handle typed store errors
184
- if (error.code === interfaces_1.STORE_ERROR_CODES.FILE_NOT_FOUND) {
185
- this.logger?.debug(`Service key file not found for ${destination}: ${error.filePath || 'unknown path'}`);
186
- }
187
- else if (error.code === interfaces_1.STORE_ERROR_CODES.PARSE_ERROR) {
188
- this.logger?.warn(`Failed to parse service key for ${destination}: ${error.filePath || 'unknown path'} - ${error.message}`);
173
+ if (hasErrorCode(error)) {
174
+ if (error.code === interfaces_1.STORE_ERROR_CODES.FILE_NOT_FOUND) {
175
+ this.logger?.debug(`Service key file not found for ${destination}: ${error.filePath || 'unknown path'}`);
176
+ }
177
+ else if (error.code === interfaces_1.STORE_ERROR_CODES.PARSE_ERROR) {
178
+ this.logger?.warn(`Failed to parse service key for ${destination}: ${error.filePath || 'unknown path'} - ${getErrorMessage(error)}`);
179
+ }
180
+ else {
181
+ this.logger?.warn(`Failed to get serviceUrl from service key store for ${destination}: ${getErrorMessage(error)}`);
182
+ }
189
183
  }
190
184
  else {
191
- this.logger?.warn(`Failed to get serviceUrl from service key store for ${destination}: ${error.message}`);
185
+ this.logger?.warn(`Failed to get serviceUrl from service key store for ${destination}: ${getErrorMessage(error)}`);
192
186
  }
193
187
  }
194
188
  }
@@ -197,307 +191,388 @@ class AuthBroker {
197
191
  throw new Error(`Session for destination "${destination}" is missing required field 'serviceUrl'. ` +
198
192
  `SessionStore must contain initial session with serviceUrl${this.serviceKeyStore ? ' or serviceKeyStore must contain serviceUrl' : ''}.`);
199
193
  }
200
- // Check if we have token or UAA credentials
201
- const hasToken = !!connConfig?.authorizationToken;
202
- const hasUaaCredentials = !!(authConfig?.uaaUrl && authConfig?.uaaClientId && authConfig?.uaaClientSecret);
203
- this.logger?.debug(`Step 0: Session check for ${destination}: hasToken(${hasToken}), hasUaaCredentials(${hasUaaCredentials}), serviceUrl(${serviceUrl ? 'yes' : 'no'})`);
204
- // If token is empty AND UAA fields are empty, try to initialize from service key
205
- if (!hasToken && !hasUaaCredentials) {
206
- this.logger?.debug(`Step 0: Token and UAA credentials are empty for ${destination}, attempting initialization from service key`);
207
- if (!this.serviceKeyStore) {
208
- this.logger?.error(`Step 0: Cannot initialize session for ${destination}: authorizationToken is empty, UAA credentials are empty, and serviceKeyStore is not available`);
209
- throw new Error(`Cannot initialize session for destination "${destination}": authorizationToken is empty, UAA credentials are empty, and serviceKeyStore is not available. ` +
210
- `Provide serviceKeyStore to initialize from service key.`);
211
- }
212
- try {
213
- // Get UAA credentials from service key
214
- const serviceKeyAuthConfig = await this.serviceKeyStore.getAuthorizationConfig(destination);
215
- if (!serviceKeyAuthConfig || !serviceKeyAuthConfig.uaaUrl || !serviceKeyAuthConfig.uaaClientId || !serviceKeyAuthConfig.uaaClientSecret) {
216
- this.logger?.error(`Step 0: Service key for ${destination} missing UAA credentials`);
217
- throw new Error(`Service key for destination "${destination}" does not contain UAA credentials`);
218
- }
219
- // Check if browser auth is allowed
220
- if (!this.allowBrowserAuth) {
221
- const error = new Error(`Browser authentication required for destination "${destination}" but allowBrowserAuth is disabled. ` +
222
- `Either enable browser auth or provide a valid session with token.`);
223
- error.code = 'BROWSER_AUTH_REQUIRED';
224
- error.destination = destination;
225
- this.logger?.error(`Step 0: Browser auth required but disabled for ${destination}`);
226
- throw error;
227
- }
228
- // Use tokenProvider for browser-based authentication
229
- this.logger?.debug(`Step 0: Authenticating via provider (browser) for ${destination} using service key UAA credentials`);
230
- let tokenResult;
231
- try {
232
- tokenResult = await this.tokenProvider.getConnectionConfig(serviceKeyAuthConfig, {
233
- browser: this.browser,
234
- logger: this.logger,
235
- });
236
- }
237
- catch (error) {
238
- // Handle provider errors (network, auth, validation)
239
- if (error.code === 'VALIDATION_ERROR') {
240
- this.logger?.error(`Step 0: Provider validation error for ${destination}: missing ${error.missingFields?.join(', ') || 'required fields'}`);
241
- throw new Error(`Cannot initialize session for destination "${destination}": provider validation failed - missing ${error.missingFields?.join(', ') || 'required fields'}`);
242
- }
243
- else if (error.code === 'BROWSER_AUTH_ERROR') {
244
- this.logger?.error(`Step 0: Browser authentication failed for ${destination}: ${error.message}`);
245
- throw new Error(`Cannot initialize session for destination "${destination}": browser authentication failed - ${error.message}`);
246
- }
247
- else if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT' || error.code === 'ENOTFOUND') {
248
- this.logger?.error(`Step 0: Network error for ${destination}: ${error.code}`);
249
- throw new Error(`Cannot initialize session for destination "${destination}": network error - cannot reach authentication server (${error.code})`);
250
- }
251
- this.logger?.error(`Step 0: Provider error for ${destination}: ${error.message}`);
252
- throw new Error(`Cannot initialize session for destination "${destination}": provider error - ${error.message}`);
253
- }
254
- const tokenLength = tokenResult.connectionConfig.authorizationToken?.length || 0;
255
- this.logger?.info(`Step 0: Token initialized for ${destination}: token(${tokenLength} chars), hasRefreshToken(${!!tokenResult.refreshToken})`);
256
- // Get serviceUrl from service key store if not in connectionConfig
257
- const serviceKeyConnConfig = await this.serviceKeyStore.getConnectionConfig(destination);
258
- const connectionConfigWithServiceUrl = {
259
- ...tokenResult.connectionConfig,
260
- serviceUrl: tokenResult.connectionConfig.serviceUrl || serviceKeyConnConfig?.serviceUrl || serviceUrl,
261
- };
262
- // Save token and UAA credentials to session
263
- try {
264
- await this.sessionStore.setConnectionConfig(destination, connectionConfigWithServiceUrl);
265
- }
266
- catch (error) {
267
- this.logger?.error(`Step 0: Failed to save connection config to session for ${destination}: ${error.message}`);
268
- throw new Error(`Failed to save connection config for destination "${destination}": ${error.message}`);
269
- }
270
- try {
271
- await this.sessionStore.setAuthorizationConfig(destination, {
272
- ...serviceKeyAuthConfig,
273
- refreshToken: tokenResult.refreshToken || serviceKeyAuthConfig.refreshToken,
274
- });
275
- }
276
- catch (error) {
277
- this.logger?.error(`Step 0: Failed to save authorization config to session for ${destination}: ${error.message}`);
278
- throw new Error(`Failed to save authorization config for destination "${destination}": ${error.message}`);
279
- }
280
- return tokenResult.connectionConfig.authorizationToken;
281
- }
282
- catch (error) {
283
- // Re-throw BROWSER_AUTH_REQUIRED error without wrapping
284
- if (error.code === 'BROWSER_AUTH_REQUIRED') {
285
- throw error;
286
- }
287
- // Handle typed store errors
288
- if (error.code === interfaces_1.STORE_ERROR_CODES.FILE_NOT_FOUND) {
289
- this.logger?.error(`Step 0: Service key file not found for ${destination}: ${error.filePath || 'unknown path'}`);
290
- throw new Error(`Cannot initialize session for destination "${destination}": service key file not found`);
291
- }
292
- else if (error.code === interfaces_1.STORE_ERROR_CODES.PARSE_ERROR) {
293
- this.logger?.error(`Step 0: Failed to parse service key for ${destination}: ${error.filePath || 'unknown path'} - ${error.message}`);
294
- throw new Error(`Cannot initialize session for destination "${destination}": service key parsing failed - ${error.message}`);
295
- }
296
- else if (error.code === interfaces_1.STORE_ERROR_CODES.INVALID_CONFIG) {
297
- this.logger?.error(`Step 0: Invalid service key config for ${destination}: missing fields ${error.missingFields?.join(', ') || 'unknown'}`);
298
- throw new Error(`Cannot initialize session for destination "${destination}": invalid service key - missing ${error.missingFields?.join(', ') || 'required fields'}`);
299
- }
300
- this.logger?.error(`Step 0: Failed to initialize session for ${destination}: ${error.message}`);
301
- throw new Error(`Cannot initialize session for destination "${destination}": ${error.message}`);
302
- }
194
+ return serviceUrl;
195
+ }
196
+ /**
197
+ * Get UAA credentials from session or service key
198
+ */
199
+ async getUaaCredentials(destination, authConfig) {
200
+ if (authConfig?.uaaUrl &&
201
+ authConfig?.uaaClientId &&
202
+ authConfig?.uaaClientSecret) {
203
+ return authConfig;
303
204
  }
304
- // If we have a token, validate it first
305
- if (hasToken && connConfig?.authorizationToken) {
306
- this.logger?.debug(`Step 0: Token found for ${destination}, validating`);
307
- // Validate token if provider supports validation and we have service URL
308
- if (this.tokenProvider?.validateToken && serviceUrl) {
309
- try {
310
- const isValid = await this.tokenProvider.validateToken(connConfig.authorizationToken, serviceUrl);
311
- if (isValid) {
312
- this.logger?.info(`Step 0: Token valid for ${destination}: token(${connConfig.authorizationToken.length} chars)`);
313
- return connConfig.authorizationToken;
314
- }
315
- this.logger?.debug(`Step 0: Token invalid for ${destination}, continuing to refresh`);
316
- }
317
- catch (error) {
318
- // Validation failed due to network/server error - log and continue to refresh
319
- this.logger?.warn(`Step 0: Token validation failed for ${destination} (network error): ${error.message}. Continuing to refresh.`);
320
- // Don't throw - continue to refresh flow
321
- }
322
- }
323
- else {
324
- // No service URL or provider doesn't support validation - just return token
325
- this.logger?.info(`Step 0: Token found for ${destination} (no validation): token(${connConfig.authorizationToken.length} chars)`);
326
- return connConfig.authorizationToken;
327
- }
205
+ if (!this.serviceKeyStore) {
206
+ throw new Error(`UAA credentials not found for ${destination}. Session has no UAA credentials and serviceKeyStore is not available.`);
328
207
  }
329
- // Step 2: Refresh Token Flow
330
- this.logger?.debug(`Step 2: Attempting token refresh for ${destination}`);
331
- // Get UAA credentials from session or service key
332
208
  let serviceKeyAuthConfig = null;
333
- if (!authConfig && this.serviceKeyStore) {
334
- try {
335
- serviceKeyAuthConfig = await this.serviceKeyStore.getAuthorizationConfig(destination);
336
- }
337
- catch (error) {
338
- // Handle typed store errors
209
+ try {
210
+ serviceKeyAuthConfig =
211
+ await this.serviceKeyStore.getAuthorizationConfig(destination);
212
+ }
213
+ catch (error) {
214
+ if (hasErrorCode(error)) {
339
215
  if (error.code === interfaces_1.STORE_ERROR_CODES.FILE_NOT_FOUND) {
340
216
  this.logger?.debug(`Service key file not found for ${destination}: ${error.filePath || 'unknown path'}`);
341
217
  }
342
218
  else if (error.code === interfaces_1.STORE_ERROR_CODES.PARSE_ERROR) {
343
- this.logger?.warn(`Failed to parse service key for ${destination}: ${error.filePath || 'unknown path'} - ${error.message}`);
219
+ this.logger?.warn(`Failed to parse service key for ${destination}: ${error.filePath || 'unknown path'} - ${getErrorMessage(error)}`);
344
220
  }
345
221
  else {
346
- this.logger?.warn(`Failed to get UAA credentials from service key store for ${destination}: ${error.message}`);
222
+ this.logger?.warn(`Failed to get UAA credentials from service key store for ${destination}: ${getErrorMessage(error)}`);
347
223
  }
348
224
  }
225
+ else {
226
+ this.logger?.warn(`Failed to get UAA credentials from service key store for ${destination}: ${getErrorMessage(error)}`);
227
+ }
349
228
  }
350
229
  const uaaCredentials = authConfig || serviceKeyAuthConfig;
351
- if (!uaaCredentials || !uaaCredentials.uaaUrl || !uaaCredentials.uaaClientId || !uaaCredentials.uaaClientSecret) {
352
- const errorMessage = `Step 2: UAA credentials not found for ${destination}. ` +
353
- `Session has no UAA credentials${this.serviceKeyStore ? ' and serviceKeyStore has no UAA credentials' : ' and serviceKeyStore is not available'}.`;
354
- this.logger?.error(errorMessage);
355
- throw new Error(errorMessage);
230
+ if (!uaaCredentials ||
231
+ !uaaCredentials.uaaUrl ||
232
+ !uaaCredentials.uaaClientId ||
233
+ !uaaCredentials.uaaClientSecret) {
234
+ throw new Error(`UAA credentials not found for ${destination}. Session has no UAA credentials${this.serviceKeyStore ? ' and serviceKeyStore has no UAA credentials' : ' and serviceKeyStore is not available'}.`);
356
235
  }
357
- // Try refresh from session first (if refresh token exists)
358
- const refreshToken = authConfig?.refreshToken;
359
- if (refreshToken) {
360
- try {
361
- this.logger?.debug(`Step 2a: Trying refreshTokenFromSession for ${destination}`);
362
- const authConfigWithRefresh = { ...uaaCredentials, refreshToken };
363
- let tokenResult;
364
- try {
365
- tokenResult = await this.tokenProvider.refreshTokenFromSession(authConfigWithRefresh, {
366
- browser: this.browser,
367
- logger: this.logger,
368
- });
369
- }
370
- catch (providerError) {
371
- // Handle provider network/auth errors
372
- if (providerError.code === 'ECONNREFUSED' || providerError.code === 'ETIMEDOUT' || providerError.code === 'ENOTFOUND') {
373
- this.logger?.debug(`Step 2a: Network error during refreshTokenFromSession for ${destination}: ${providerError.code}. Trying refreshTokenFromServiceKey`);
374
- throw providerError; // Re-throw to trigger fallback to Step 2b
375
- }
376
- throw providerError; // Re-throw other errors
377
- }
378
- const tokenLength = tokenResult.connectionConfig.authorizationToken?.length || 0;
379
- this.logger?.info(`Step 2a: Token refreshed from session for ${destination}: token(${tokenLength} chars), hasRefreshToken(${!!tokenResult.refreshToken})`);
380
- // Get serviceUrl from session or service key
381
- let serviceKeyServiceUrl;
382
- if (this.serviceKeyStore) {
383
- try {
384
- const serviceKeyConn = await this.serviceKeyStore.getConnectionConfig(destination);
385
- serviceKeyServiceUrl = serviceKeyConn?.serviceUrl;
386
- }
387
- catch (error) {
388
- this.logger?.debug(`Could not get serviceUrl from service key store: ${error.message}`);
389
- }
390
- }
391
- const finalServiceUrl = tokenResult.connectionConfig.serviceUrl || serviceUrl || serviceKeyServiceUrl;
392
- const connectionConfigWithServiceUrl = {
393
- ...tokenResult.connectionConfig,
394
- serviceUrl: finalServiceUrl,
395
- };
396
- // Update session with new token
397
- try {
398
- await this.sessionStore.setConnectionConfig(destination, connectionConfigWithServiceUrl);
399
- }
400
- catch (error) {
401
- this.logger?.error(`Step 2a: Failed to save connection config to session for ${destination}: ${error.message}`);
402
- throw new Error(`Failed to save connection config for destination "${destination}": ${error.message}`);
403
- }
404
- if (tokenResult.refreshToken) {
405
- try {
406
- await this.sessionStore.setAuthorizationConfig(destination, {
407
- ...uaaCredentials,
408
- refreshToken: tokenResult.refreshToken,
409
- });
410
- }
411
- catch (error) {
412
- this.logger?.error(`Step 2a: Failed to save authorization config to session for ${destination}: ${error.message}`);
413
- throw new Error(`Failed to save authorization config for destination "${destination}": ${error.message}`);
414
- }
415
- }
416
- return tokenResult.connectionConfig.authorizationToken;
236
+ return uaaCredentials;
237
+ }
238
+ /**
239
+ * Save token and config to session
240
+ */
241
+ async saveTokenToSession(destination, connectionConfig, authorizationConfig) {
242
+ try {
243
+ await this.sessionStore.setConnectionConfig(destination, connectionConfig);
244
+ }
245
+ catch (error) {
246
+ this.logger?.error(`Failed to save connection config to session for ${destination}: ${getErrorMessage(error)}`);
247
+ throw new Error(`Failed to save connection config for destination "${destination}": ${getErrorMessage(error)}`);
248
+ }
249
+ try {
250
+ await this.sessionStore.setAuthorizationConfig(destination, authorizationConfig);
251
+ }
252
+ catch (error) {
253
+ this.logger?.error(`Failed to save authorization config to session for ${destination}: ${getErrorMessage(error)}`);
254
+ throw new Error(`Failed to save authorization config for destination "${destination}": ${getErrorMessage(error)}`);
255
+ }
256
+ }
257
+ /**
258
+ * Initialize session from service key (Step 0)
259
+ */
260
+ async initializeSessionFromServiceKey(destination, serviceUrl) {
261
+ if (!this.serviceKeyStore) {
262
+ throw new Error(`Cannot initialize session for destination "${destination}": authorizationToken is empty, UAA credentials are empty, and serviceKeyStore is not available. Provide serviceKeyStore to initialize from service key.`);
263
+ }
264
+ const serviceKeyAuthConfig = await this.serviceKeyStore.getAuthorizationConfig(destination);
265
+ if (!serviceKeyAuthConfig ||
266
+ !serviceKeyAuthConfig.uaaUrl ||
267
+ !serviceKeyAuthConfig.uaaClientId ||
268
+ !serviceKeyAuthConfig.uaaClientSecret) {
269
+ throw new Error(`Service key for destination "${destination}" does not contain UAA credentials`);
270
+ }
271
+ if (!this.allowBrowserAuth) {
272
+ const error = new Error(`Browser authentication required for destination "${destination}" but allowBrowserAuth is disabled. Either enable browser auth or provide a valid session with token.`);
273
+ error.code = 'BROWSER_AUTH_REQUIRED';
274
+ error.destination = destination;
275
+ this.logger?.error(`Step 0: Browser auth required but disabled for ${destination}`);
276
+ throw error;
277
+ }
278
+ this.logger?.debug(`Step 0: Authenticating via provider (browser) for ${destination} using service key UAA credentials`);
279
+ let tokenResult;
280
+ try {
281
+ tokenResult = await this.tokenProvider.getConnectionConfig(serviceKeyAuthConfig, {
282
+ browser: this.browser,
283
+ logger: this.logger,
284
+ });
285
+ }
286
+ catch (error) {
287
+ if (hasErrorCode(error)) {
288
+ if (error.code === 'VALIDATION_ERROR') {
289
+ throw new Error(`Cannot initialize session for destination "${destination}": provider validation failed - missing ${error.missingFields?.join(', ') || 'required fields'}`);
290
+ }
291
+ else if (error.code === 'BROWSER_AUTH_ERROR') {
292
+ throw new Error(`Cannot initialize session for destination "${destination}": browser authentication failed - ${getErrorMessage(error)}`);
293
+ }
294
+ else if (error.code === 'ECONNREFUSED' ||
295
+ error.code === 'ETIMEDOUT' ||
296
+ error.code === 'ENOTFOUND') {
297
+ throw new Error(`Cannot initialize session for destination "${destination}": network error - cannot reach authentication server (${error.code})`);
298
+ }
299
+ }
300
+ throw new Error(`Cannot initialize session for destination "${destination}": provider error - ${getErrorMessage(error)}`);
301
+ }
302
+ const token = tokenResult.connectionConfig.authorizationToken;
303
+ if (!token) {
304
+ throw new Error(`Token provider did not return authorization token for destination "${destination}"`);
305
+ }
306
+ const tokenLength = token.length;
307
+ this.logger?.info(`Step 0: Token initialized for ${destination}: token(${tokenLength} chars), hasRefreshToken(${!!tokenResult.refreshToken})`);
308
+ // Get serviceUrl from service key store if not in connectionConfig
309
+ const serviceKeyConnConfig = await this.serviceKeyStore.getConnectionConfig(destination);
310
+ const connectionConfigWithServiceUrl = {
311
+ ...tokenResult.connectionConfig,
312
+ serviceUrl: tokenResult.connectionConfig.serviceUrl ||
313
+ serviceKeyConnConfig?.serviceUrl ||
314
+ serviceUrl,
315
+ };
316
+ await this.saveTokenToSession(destination, connectionConfigWithServiceUrl, {
317
+ ...serviceKeyAuthConfig,
318
+ refreshToken: tokenResult.refreshToken || serviceKeyAuthConfig.refreshToken,
319
+ });
320
+ return token;
321
+ }
322
+ /**
323
+ * Validate existing token (Step 1)
324
+ */
325
+ async validateExistingToken(destination, token, serviceUrl) {
326
+ if (!this.tokenProvider?.validateToken) {
327
+ return false;
328
+ }
329
+ try {
330
+ const isValid = await this.tokenProvider.validateToken(token, serviceUrl);
331
+ if (isValid) {
332
+ this.logger?.info(`Step 1: Token valid for ${destination}: token(${token.length} chars)`);
333
+ return true;
417
334
  }
418
- catch (error) {
419
- this.logger?.debug(`Step 2a: refreshTokenFromSession failed for ${destination}: ${error.message}, trying refreshTokenFromServiceKey`);
420
- // Continue to try service key refresh
335
+ this.logger?.debug(`Step 1: Token invalid for ${destination}, continuing to refresh`);
336
+ return false;
337
+ }
338
+ catch (error) {
339
+ // Validation failed due to network/server error - log and continue to refresh
340
+ this.logger?.warn(`Step 1: Token validation failed for ${destination} (network error): ${getErrorMessage(error)}. Continuing to refresh.`);
341
+ return false;
342
+ }
343
+ }
344
+ /**
345
+ * Refresh token from session (Step 2a)
346
+ */
347
+ async refreshTokenFromSession(destination, uaaCredentials, refreshToken, serviceUrl) {
348
+ this.logger?.debug(`Step 2a: Trying refreshTokenFromSession for ${destination}`);
349
+ const authConfigWithRefresh = { ...uaaCredentials, refreshToken };
350
+ let tokenResult;
351
+ try {
352
+ tokenResult = await this.tokenProvider.refreshTokenFromSession(authConfigWithRefresh, {
353
+ browser: this.browser,
354
+ logger: this.logger,
355
+ });
356
+ }
357
+ catch (error) {
358
+ if (hasErrorCode(error)) {
359
+ if (error.code === 'ECONNREFUSED' ||
360
+ error.code === 'ETIMEDOUT' ||
361
+ error.code === 'ENOTFOUND') {
362
+ this.logger?.debug(`Step 2a: Network error during refreshTokenFromSession for ${destination}: ${error.code}. Trying refreshTokenFromServiceKey`);
363
+ throw error; // Re-throw to trigger fallback to Step 2b
364
+ }
421
365
  }
366
+ throw error; // Re-throw other errors
422
367
  }
423
- else {
424
- this.logger?.debug(`Step 2a: No refresh token in session for ${destination}, skipping to service key refresh`);
368
+ const token = tokenResult.connectionConfig.authorizationToken;
369
+ if (!token) {
370
+ throw new Error(`Token provider did not return authorization token for destination "${destination}"`);
425
371
  }
426
- // Try refresh from service key (browser authentication)
427
- // Check if browser auth is allowed
372
+ const tokenLength = token.length;
373
+ this.logger?.info(`Step 2a: Token refreshed from session for ${destination}: token(${tokenLength} chars), hasRefreshToken(${!!tokenResult.refreshToken})`);
374
+ // Get serviceUrl from session or service key
375
+ let serviceKeyServiceUrl;
376
+ if (this.serviceKeyStore) {
377
+ try {
378
+ const serviceKeyConn = await this.serviceKeyStore.getConnectionConfig(destination);
379
+ serviceKeyServiceUrl = serviceKeyConn?.serviceUrl;
380
+ }
381
+ catch (error) {
382
+ this.logger?.debug(`Could not get serviceUrl from service key store: ${getErrorMessage(error)}`);
383
+ }
384
+ }
385
+ const finalServiceUrl = tokenResult.connectionConfig.serviceUrl ||
386
+ serviceUrl ||
387
+ serviceKeyServiceUrl;
388
+ const connectionConfigWithServiceUrl = {
389
+ ...tokenResult.connectionConfig,
390
+ serviceUrl: finalServiceUrl,
391
+ };
392
+ const authorizationConfig = {
393
+ ...uaaCredentials,
394
+ refreshToken: tokenResult.refreshToken || refreshToken,
395
+ };
396
+ await this.saveTokenToSession(destination, connectionConfigWithServiceUrl, authorizationConfig);
397
+ return token;
398
+ }
399
+ /**
400
+ * Refresh token from service key (Step 2b)
401
+ */
402
+ async refreshTokenFromServiceKey(destination, uaaCredentials, serviceUrl) {
428
403
  if (!this.allowBrowserAuth) {
429
- const error = new Error(`Browser authentication required for destination "${destination}" but allowBrowserAuth is disabled. ` +
430
- `Token refresh via session failed and browser auth is not allowed. ` +
431
- `Either enable browser auth or ensure a valid refresh token exists in session.`);
404
+ const error = new Error(`Browser authentication required for destination "${destination}" but allowBrowserAuth is disabled. Token refresh via session failed and browser auth is not allowed. Either enable browser auth or ensure a valid refresh token exists in session.`);
432
405
  error.code = 'BROWSER_AUTH_REQUIRED';
433
406
  error.destination = destination;
434
407
  this.logger?.error(`Step 2b: Browser auth required but disabled for ${destination}`);
435
408
  throw error;
436
409
  }
410
+ this.logger?.debug(`Step 2b: Trying refreshTokenFromServiceKey for ${destination}`);
411
+ let tokenResult;
437
412
  try {
438
- this.logger?.debug(`Step 2b: Trying refreshTokenFromServiceKey for ${destination}`);
439
- const tokenResult = await this.tokenProvider.refreshTokenFromServiceKey(uaaCredentials, {
413
+ tokenResult = await this.tokenProvider.refreshTokenFromServiceKey(uaaCredentials, {
440
414
  browser: this.browser,
441
415
  logger: this.logger,
442
416
  });
443
- const tokenLength = tokenResult.connectionConfig.authorizationToken?.length || 0;
444
- this.logger?.info(`Step 2b: Token refreshed from service key for ${destination}: token(${tokenLength} chars), hasRefreshToken(${!!tokenResult.refreshToken})`);
445
- // Get serviceUrl from session or service key
446
- let serviceKeyServiceUrl;
447
- if (this.serviceKeyStore) {
448
- try {
449
- const serviceKeyConn = await this.serviceKeyStore.getConnectionConfig(destination);
450
- serviceKeyServiceUrl = serviceKeyConn?.serviceUrl;
417
+ }
418
+ catch (error) {
419
+ if (hasErrorCode(error)) {
420
+ if (error.code === 'VALIDATION_ERROR') {
421
+ throw new Error(`Token refresh failed: Missing required fields in authConfig - ${error.missingFields?.join(', ')}`);
422
+ }
423
+ else if (error.code === 'BROWSER_AUTH_ERROR') {
424
+ throw new Error(`Token refresh failed: Browser authentication failed or was cancelled - ${getErrorMessage(error)}`);
451
425
  }
452
- catch (error) {
453
- this.logger?.debug(`Could not get serviceUrl from service key store: ${error.message}`);
426
+ else if (error.code === 'ECONNREFUSED' ||
427
+ error.code === 'ETIMEDOUT' ||
428
+ error.code === 'ENOTFOUND') {
429
+ throw new Error(`Token refresh failed: Network error - ${error.code}: Cannot reach authentication server`);
430
+ }
431
+ else if (error.code === 'SERVICE_KEY_ERROR') {
432
+ throw new Error(`Token refresh failed: Service key not found or invalid for ${destination}`);
454
433
  }
455
434
  }
456
- const finalServiceUrl = tokenResult.connectionConfig.serviceUrl || serviceUrl || serviceKeyServiceUrl;
457
- const connectionConfigWithServiceUrl = {
458
- ...tokenResult.connectionConfig,
459
- serviceUrl: finalServiceUrl,
460
- };
461
- // Update session with new token
435
+ throw new Error(`Token refresh failed for ${destination}: ${getErrorMessage(error)}`);
436
+ }
437
+ const token = tokenResult.connectionConfig.authorizationToken;
438
+ if (!token) {
439
+ throw new Error(`Token provider did not return authorization token for destination "${destination}"`);
440
+ }
441
+ const tokenLength = token.length;
442
+ this.logger?.info(`Step 2b: Token refreshed from service key for ${destination}: token(${tokenLength} chars), hasRefreshToken(${!!tokenResult.refreshToken})`);
443
+ // Get serviceUrl from session or service key
444
+ let serviceKeyServiceUrl;
445
+ if (this.serviceKeyStore) {
462
446
  try {
463
- await this.sessionStore.setConnectionConfig(destination, connectionConfigWithServiceUrl);
447
+ const serviceKeyConn = await this.serviceKeyStore.getConnectionConfig(destination);
448
+ serviceKeyServiceUrl = serviceKeyConn?.serviceUrl;
464
449
  }
465
450
  catch (error) {
466
- this.logger?.error(`Step 2b: Failed to save connection config to session for ${destination}: ${error.message}`);
467
- throw new Error(`Failed to save connection config for destination "${destination}": ${error.message}`);
451
+ this.logger?.debug(`Could not get serviceUrl from service key store: ${getErrorMessage(error)}`);
452
+ }
453
+ }
454
+ const finalServiceUrl = tokenResult.connectionConfig.serviceUrl ||
455
+ serviceUrl ||
456
+ serviceKeyServiceUrl;
457
+ const connectionConfigWithServiceUrl = {
458
+ ...tokenResult.connectionConfig,
459
+ serviceUrl: finalServiceUrl,
460
+ };
461
+ const authorizationConfig = {
462
+ ...uaaCredentials,
463
+ refreshToken: tokenResult.refreshToken || uaaCredentials.refreshToken,
464
+ };
465
+ await this.saveTokenToSession(destination, connectionConfigWithServiceUrl, authorizationConfig);
466
+ return token;
467
+ }
468
+ /**
469
+ * Get authentication token for destination.
470
+ * Uses tokenProvider for all authentication operations (browser-based authorization).
471
+ *
472
+ * **Flow:**
473
+ * **Step 0: Initialize Session with Token (if needed)**
474
+ * - Check if session has `authorizationToken` AND UAA credentials
475
+ * - If both are empty AND serviceKeyStore is available:
476
+ * - Get UAA credentials from service key
477
+ * - Use tokenProvider for browser-based authentication
478
+ * - Save token and refresh token to session
479
+ *
480
+ * **Step 1: Token Validation**
481
+ * - If token exists in session, validate it (if provider supports validation)
482
+ * - If valid → return token
483
+ * - If invalid or no token → continue to refresh
484
+ *
485
+ * **Step 2: Refresh Token Flow**
486
+ * - Check if refresh token exists in session
487
+ * - If refresh token exists:
488
+ * - Use tokenProvider to refresh token (browser-based or refresh grant)
489
+ * - Save new token to session
490
+ * - Return new token
491
+ * - Otherwise → proceed to Step 3
492
+ *
493
+ * **Step 3: New Token Flow**
494
+ * - Get UAA credentials from session or service key
495
+ * - Use tokenProvider for browser-based authentication
496
+ * - Save new token to session
497
+ * - Return new token
498
+ *
499
+ * **Important Notes:**
500
+ * - All authentication is handled by tokenProvider (e.g., XSUAA provider)
501
+ * - Provider uses browser-based authorization to ensure proper role assignment
502
+ * - Direct UAA HTTP requests are not used to avoid role assignment issues
503
+ *
504
+ * @param destination Destination name (e.g., "TRIAL")
505
+ * @returns Promise that resolves to JWT token string
506
+ * @throws Error if session initialization fails or authentication failed
507
+ */
508
+ async getToken(destination) {
509
+ this.logger?.debug(`Getting token for destination: ${destination}`);
510
+ // Load session data
511
+ const { connConfig, authConfig } = await this.loadSessionData(destination);
512
+ // Get serviceUrl (required)
513
+ const serviceUrl = await this.getServiceUrl(destination, connConfig);
514
+ // Check if we have token or UAA credentials
515
+ const hasToken = !!connConfig?.authorizationToken;
516
+ const hasUaaCredentials = !!(authConfig?.uaaUrl &&
517
+ authConfig?.uaaClientId &&
518
+ authConfig?.uaaClientSecret);
519
+ this.logger?.debug(`Session check for ${destination}: hasToken(${hasToken}), hasUaaCredentials(${hasUaaCredentials}), serviceUrl(${serviceUrl ? 'yes' : 'no'})`);
520
+ // Step 0: Initialize Session with Token (if needed)
521
+ if (!hasToken && !hasUaaCredentials) {
522
+ try {
523
+ return await this.initializeSessionFromServiceKey(destination, serviceUrl);
468
524
  }
469
- if (tokenResult.refreshToken) {
470
- try {
471
- await this.sessionStore.setAuthorizationConfig(destination, {
472
- ...uaaCredentials,
473
- refreshToken: tokenResult.refreshToken,
474
- });
525
+ catch (error) {
526
+ // Re-throw BROWSER_AUTH_REQUIRED error without wrapping
527
+ if (hasErrorCode(error) && error.code === 'BROWSER_AUTH_REQUIRED') {
528
+ throw error;
475
529
  }
476
- catch (error) {
477
- this.logger?.error(`Step 2b: Failed to save authorization config to session for ${destination}: ${error.message}`);
478
- throw new Error(`Failed to save authorization config for destination "${destination}": ${error.message}`);
530
+ // Handle typed store errors
531
+ if (hasErrorCode(error)) {
532
+ if (error.code === interfaces_1.STORE_ERROR_CODES.FILE_NOT_FOUND) {
533
+ throw new Error(`Cannot initialize session for destination "${destination}": service key file not found`);
534
+ }
535
+ else if (error.code === interfaces_1.STORE_ERROR_CODES.PARSE_ERROR) {
536
+ throw new Error(`Cannot initialize session for destination "${destination}": service key parsing failed - ${getErrorMessage(error)}`);
537
+ }
538
+ else if (error.code === interfaces_1.STORE_ERROR_CODES.INVALID_CONFIG) {
539
+ throw new Error(`Cannot initialize session for destination "${destination}": invalid service key - missing ${error.missingFields?.join(', ') || 'required fields'}`);
540
+ }
479
541
  }
542
+ throw error;
480
543
  }
481
- return tokenResult.connectionConfig.authorizationToken;
482
544
  }
483
- catch (error) {
484
- this.logger?.error(`Step 2b: refreshTokenFromServiceKey failed for ${destination}: ${error.message}`);
485
- // Determine error cause and throw meaningful error
486
- if (error.code === 'VALIDATION_ERROR') {
487
- throw new Error(`Token refresh failed: Missing required fields in authConfig - ${error.missingFields?.join(', ')}`);
545
+ // Step 1: Validate existing token
546
+ if (hasToken && connConfig?.authorizationToken) {
547
+ const isValid = await this.validateExistingToken(destination, connConfig.authorizationToken, serviceUrl);
548
+ if (isValid) {
549
+ return connConfig.authorizationToken;
488
550
  }
489
- else if (error.code === 'BROWSER_AUTH_ERROR') {
490
- throw new Error(`Token refresh failed: Browser authentication failed or was cancelled - ${error.message}`);
551
+ // If no validation or validation failed, continue to refresh
552
+ if (!this.tokenProvider?.validateToken) {
553
+ this.logger?.info(`Token found for ${destination} (no validation): token(${connConfig.authorizationToken.length} chars)`);
554
+ return connConfig.authorizationToken;
491
555
  }
492
- else if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT' || error.code === 'ENOTFOUND') {
493
- throw new Error(`Token refresh failed: Network error - ${error.code}: Cannot reach authentication server`);
556
+ }
557
+ // Step 2: Refresh Token Flow
558
+ this.logger?.debug(`Step 2: Attempting token refresh for ${destination}`);
559
+ const uaaCredentials = await this.getUaaCredentials(destination, authConfig);
560
+ // Step 2a: Try refresh from session (if refresh token exists)
561
+ const refreshToken = authConfig?.refreshToken;
562
+ if (refreshToken) {
563
+ try {
564
+ return await this.refreshTokenFromSession(destination, uaaCredentials, refreshToken, serviceUrl);
494
565
  }
495
- else if (error.code === 'SERVICE_KEY_ERROR') {
496
- throw new Error(`Token refresh failed: Service key not found or invalid for ${destination}`);
566
+ catch (error) {
567
+ this.logger?.debug(`Step 2a: refreshTokenFromSession failed for ${destination}: ${getErrorMessage(error)}, trying refreshTokenFromServiceKey`);
568
+ // Continue to try service key refresh
497
569
  }
498
- // Generic error
499
- throw new Error(`Token refresh failed for ${destination}: ${error.message}`);
500
570
  }
571
+ else {
572
+ this.logger?.debug(`Step 2a: No refresh token in session for ${destination}, skipping to service key refresh`);
573
+ }
574
+ // Step 2b: Try refresh from service key (browser authentication)
575
+ return await this.refreshTokenFromServiceKey(destination, uaaCredentials, serviceUrl);
501
576
  }
502
577
  /**
503
578
  * Force refresh token for destination.
@@ -521,10 +596,11 @@ class AuthBroker {
521
596
  this.logger?.debug(`Checking session store for authorization config: ${destination}`);
522
597
  let sessionAuthConfig = null;
523
598
  try {
524
- sessionAuthConfig = await this.sessionStore.getAuthorizationConfig(destination);
599
+ sessionAuthConfig =
600
+ await this.sessionStore.getAuthorizationConfig(destination);
525
601
  }
526
602
  catch (error) {
527
- this.logger?.warn(`Failed to get authorization config from session store for ${destination}: ${error.message}`);
603
+ this.logger?.warn(`Failed to get authorization config from session store for ${destination}: ${getErrorMessage(error)}`);
528
604
  }
529
605
  if (sessionAuthConfig) {
530
606
  this.logger?.debug(`Authorization config from session for ${destination}: hasUaaUrl(${!!sessionAuthConfig.uaaUrl}), hasRefreshToken(${!!sessionAuthConfig.refreshToken})`);
@@ -535,18 +611,24 @@ class AuthBroker {
535
611
  this.logger?.debug(`Checking service key store for authorization config: ${destination}`);
536
612
  let serviceKeyAuthConfig = null;
537
613
  try {
538
- serviceKeyAuthConfig = await this.serviceKeyStore.getAuthorizationConfig(destination);
614
+ serviceKeyAuthConfig =
615
+ await this.serviceKeyStore.getAuthorizationConfig(destination);
539
616
  }
540
617
  catch (error) {
541
618
  // Handle typed store errors
542
- if (error.code === interfaces_1.STORE_ERROR_CODES.FILE_NOT_FOUND) {
543
- this.logger?.debug(`Service key file not found for ${destination}: ${error.filePath || 'unknown path'}`);
544
- }
545
- else if (error.code === interfaces_1.STORE_ERROR_CODES.PARSE_ERROR) {
546
- this.logger?.warn(`Failed to parse service key for ${destination}: ${error.filePath || 'unknown path'} - ${error.message}`);
619
+ if (hasErrorCode(error)) {
620
+ if (error.code === interfaces_1.STORE_ERROR_CODES.FILE_NOT_FOUND) {
621
+ this.logger?.debug(`Service key file not found for ${destination}: ${error.filePath || 'unknown path'}`);
622
+ }
623
+ else if (error.code === interfaces_1.STORE_ERROR_CODES.PARSE_ERROR) {
624
+ this.logger?.warn(`Failed to parse service key for ${destination}: ${error.filePath || 'unknown path'} - ${getErrorMessage(error)}`);
625
+ }
626
+ else {
627
+ this.logger?.warn(`Failed to get authorization config from service key store for ${destination}: ${getErrorMessage(error)}`);
628
+ }
547
629
  }
548
630
  else {
549
- this.logger?.warn(`Failed to get authorization config from service key store for ${destination}: ${error.message}`);
631
+ this.logger?.warn(`Failed to get authorization config from service key store for ${destination}: ${getErrorMessage(error)}`);
550
632
  }
551
633
  }
552
634
  if (serviceKeyAuthConfig) {
@@ -570,10 +652,11 @@ class AuthBroker {
570
652
  // Try session store first (has tokens and URLs)
571
653
  let sessionConnConfig = null;
572
654
  try {
573
- sessionConnConfig = await this.sessionStore.getConnectionConfig(destination);
655
+ sessionConnConfig =
656
+ await this.sessionStore.getConnectionConfig(destination);
574
657
  }
575
658
  catch (error) {
576
- this.logger?.warn(`Failed to get connection config from session store for ${destination}: ${error.message}`);
659
+ this.logger?.warn(`Failed to get connection config from session store for ${destination}: ${getErrorMessage(error)}`);
577
660
  }
578
661
  if (sessionConnConfig) {
579
662
  this.logger?.debug(`Connection config from session for ${destination}: token(${sessionConnConfig.authorizationToken?.length || 0} chars), serviceUrl(${sessionConnConfig.serviceUrl ? 'yes' : 'no'})`);
@@ -583,18 +666,24 @@ class AuthBroker {
583
666
  if (this.serviceKeyStore) {
584
667
  let serviceKeyConnConfig = null;
585
668
  try {
586
- serviceKeyConnConfig = await this.serviceKeyStore.getConnectionConfig(destination);
669
+ serviceKeyConnConfig =
670
+ await this.serviceKeyStore.getConnectionConfig(destination);
587
671
  }
588
672
  catch (error) {
589
673
  // Handle typed store errors
590
- if (error.code === interfaces_1.STORE_ERROR_CODES.FILE_NOT_FOUND) {
591
- this.logger?.debug(`Service key file not found for ${destination}: ${error.filePath || 'unknown path'}`);
592
- }
593
- else if (error.code === interfaces_1.STORE_ERROR_CODES.PARSE_ERROR) {
594
- this.logger?.warn(`Failed to parse service key for ${destination}: ${error.filePath || 'unknown path'} - ${error.message}`);
674
+ if (hasErrorCode(error)) {
675
+ if (error.code === interfaces_1.STORE_ERROR_CODES.FILE_NOT_FOUND) {
676
+ this.logger?.debug(`Service key file not found for ${destination}: ${error.filePath || 'unknown path'}`);
677
+ }
678
+ else if (error.code === interfaces_1.STORE_ERROR_CODES.PARSE_ERROR) {
679
+ this.logger?.warn(`Failed to parse service key for ${destination}: ${error.filePath || 'unknown path'} - ${getErrorMessage(error)}`);
680
+ }
681
+ else {
682
+ this.logger?.warn(`Failed to get connection config from service key store for ${destination}: ${getErrorMessage(error)}`);
683
+ }
595
684
  }
596
685
  else {
597
- this.logger?.warn(`Failed to get connection config from service key store for ${destination}: ${error.message}`);
686
+ this.logger?.warn(`Failed to get connection config from service key store for ${destination}: ${getErrorMessage(error)}`);
598
687
  }
599
688
  }
600
689
  if (serviceKeyConnConfig) {
@@ -608,5 +697,41 @@ class AuthBroker {
608
697
  this.logger?.debug(`No connection config found for ${destination}`);
609
698
  return null;
610
699
  }
700
+ /**
701
+ * Create a token refresher for a specific destination.
702
+ *
703
+ * The token refresher is designed to be injected into JwtAbapConnection via DI,
704
+ * allowing the connection to handle token refresh transparently without knowing
705
+ * about authentication internals.
706
+ *
707
+ * **Usage:**
708
+ * ```typescript
709
+ * const broker = new AuthBroker(config);
710
+ * const tokenRefresher = broker.createTokenRefresher('TRIAL');
711
+ * const connection = new JwtAbapConnection(config, tokenRefresher);
712
+ * ```
713
+ *
714
+ * @param destination Destination name (e.g., "TRIAL")
715
+ * @returns ITokenRefresher implementation for the given destination
716
+ */
717
+ createTokenRefresher(destination) {
718
+ const broker = this;
719
+ return {
720
+ /**
721
+ * Get current valid token.
722
+ * Returns cached token if valid, otherwise refreshes and returns new token.
723
+ */
724
+ async getToken() {
725
+ return broker.getToken(destination);
726
+ },
727
+ /**
728
+ * Force refresh token and save to session store.
729
+ * Always performs refresh, ignoring cached token validity.
730
+ */
731
+ async refreshToken() {
732
+ return broker.refreshToken(destination);
733
+ },
734
+ };
735
+ }
611
736
  }
612
737
  exports.AuthBroker = AuthBroker;