@mcp-abap-adt/auth-stores 0.1.6 → 0.1.7

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,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.7] - 2025-12-08
11
+
12
+ ### Added
13
+ - **Broker Usage Tests**: Added comprehensive test suites for broker usage scenarios
14
+ - Tests verify stores work correctly when used as in `AuthBroker` (without `saveSession`)
15
+ - Tests cover `setConnectionConfig` and `setAuthorizationConfig` on empty stores
16
+ - Tests verify session creation and updates in broker flow scenarios
17
+ - Test files: `*SessionStore.broker.test.ts` for all store types
18
+
19
+ ### Changed
20
+ - **Session Store Initialization**: File-based session stores now automatically create directory in constructor
21
+ - `AbapSessionStore`, `BtpSessionStore`, `XsuaaSessionStore` create directory if it doesn't exist
22
+ - Stores are ready to use immediately after construction
23
+ - Directory creation is logged at debug level
24
+ - **Session Creation Logic**: Session stores now automatically create sessions when calling `setConnectionConfig` or `setAuthorizationConfig`
25
+ - No need to call `saveSession` first - stores handle session creation internally
26
+ - `setConnectionConfig` creates new session if none exists (requires `serviceUrl` for ABAP)
27
+ - `setAuthorizationConfig` creates new session if none exists (for BTP/XSUAA, `mcpUrl` is optional)
28
+ - For ABAP: `setAuthorizationConfig` requires existing `serviceUrl` (from `setConnectionConfig` or throws error)
29
+ - This matches how `AuthBroker` uses stores - stores are now fully ready after construction
30
+ - **Token Validation**: Updated validation to allow empty string for `jwtToken` in BTP/XSUAA stores
31
+ - Empty token is allowed (can be set later via `setConnectionConfig`)
32
+ - Only `undefined` or `null` tokens are rejected
33
+ - This enables creating sessions with authorization config first, then adding connection config
34
+
35
+ ### Fixed
36
+ - **getConnectionConfig**: Fixed to allow empty string tokens (not just non-empty strings)
37
+ - Returns `null` only if token is `undefined` or `null`
38
+ - Empty string tokens are valid (can be set later)
39
+ - **setConnectionConfig Updates**: Fixed to preserve existing token when updating connection config
40
+ - Only updates `jwtToken` if `authorizationToken` is provided in config
41
+ - Preserves existing token if `authorizationToken` is `undefined`
42
+ - **Safe Session Stores**: Fixed session creation in `setConnectionConfig` and `setAuthorizationConfig`
43
+ - Now saves directly to Map (internal format) instead of calling `saveSession` with wrong format
44
+ - This fixes issues where `mcpUrl`/`serviceUrl` was not being saved correctly
45
+ - **loadXsuaaEnvFile**: Fixed to allow empty string for `jwtToken` (can be set later)
46
+ - Only rejects `undefined` or `null` tokens
47
+ - Empty string tokens are valid and can be set later via `setConnectionConfig`
48
+ - **testLogger**: Fixed to not output by default in test environment
49
+ - Now requires explicit enable via `DEBUG_AUTH_STORES=true` or `DEBUG=true`
50
+ - No longer enables logging automatically when `NODE_ENV === 'test'`
51
+
10
52
  ## [0.1.6] - 2025-12-08
11
53
 
12
54
  ### Added
package/README.md CHANGED
@@ -145,11 +145,12 @@ All stores accept a single directory path in the constructor:
145
145
  // Single directory path
146
146
  const store = new BtpServiceKeyStore('/path/to/service-keys');
147
147
 
148
- // Directory will be created automatically if it doesn't exist when saving files
149
- const sessionStore = new AbapSessionStore('/path/to/sessions');
150
- await sessionStore.saveSession('TRIAL', config); // Creates directory if needed
148
+ // File-based session stores automatically create directory in constructor if it doesn't exist
149
+ const sessionStore = new AbapSessionStore('/path/to/sessions'); // Directory created automatically
151
150
  ```
152
151
 
152
+ **Note**: File-based session stores (`AbapSessionStore`, `BtpSessionStore`, `XsuaaSessionStore`) automatically create the directory in the constructor if it doesn't exist. Stores are ready to use immediately after construction.
153
+
153
154
  ### Service Key Format
154
155
 
155
156
  **ABAP Service Key** (with nested `uaa` object):
@@ -355,12 +356,23 @@ Integration tests will skip if `test-config.yaml` is not configured or contains
355
356
 
356
357
  - All stores implement `IServiceKeyStore` or `ISessionStore` interfaces from `@mcp-abap-adt/auth-broker`
357
358
  - Stores accept a single directory path in constructor
358
- - File-based stores automatically create directories when saving
359
+ - File-based session stores automatically create directories in constructor if they don't exist
360
+ - Session stores automatically create sessions when calling `setConnectionConfig` or `setAuthorizationConfig` (no need to call `saveSession` first)
359
361
  - In-memory stores (`Safe*SessionStore`) don't persist data to disk
360
362
 
363
+ ### Session Store Behavior
364
+
365
+ Session stores are designed to work seamlessly with `AuthBroker`:
366
+
367
+ - **Ready after construction**: File-based stores create directory automatically, stores are ready to use immediately
368
+ - **Automatic session creation**: Calling `setConnectionConfig` or `setAuthorizationConfig` on an empty store creates a new session
369
+ - **ABAP stores**: Require `serviceUrl` when creating new session via `setConnectionConfig` or `setAuthorizationConfig`
370
+ - **BTP/XSUAA stores**: `mcpUrl` is optional - can create session with authorization config first, then add connection config
371
+ - **Token updates**: `setConnectionConfig` updates token if provided, preserves existing token if not provided
372
+
361
373
  ## Dependencies
362
374
 
363
- - `@mcp-abap-adt/auth-broker` (^0.1.6) - Interface definitions
375
+ - `@mcp-abap-adt/interfaces` (^0.1.3) - Interface definitions (`IServiceKeyStore`, `ISessionStore`, `IConfig`, `IConnectionConfig`, `IAuthorizationConfig`, `ILogger`)
364
376
  - `dotenv` - Environment variable parsing
365
377
 
366
378
  ## License
@@ -65,8 +65,10 @@ async function loadXsuaaEnvFile(destination, directory, log) {
65
65
  log?.debug(`Parsed XSUAA env variables: ${Object.keys(parsed).filter(k => k.startsWith('XSUAA_')).join(', ')}`);
66
66
  // Extract required fields (XSUAA_* variables)
67
67
  const jwtToken = parsed[constants_1.XSUAA_CONNECTION_VARS.AUTHORIZATION_TOKEN];
68
- log?.debug(`Extracted fields: hasJwtToken(${!!jwtToken})`);
69
- if (!jwtToken) {
68
+ log?.debug(`Extracted fields: hasJwtToken(${jwtToken !== undefined && jwtToken !== null})`);
69
+ // Allow empty string for jwtToken (can be set later via setConnectionConfig)
70
+ // Only reject if jwtToken is undefined or null
71
+ if (jwtToken === undefined || jwtToken === null) {
70
72
  log?.warn(`XSUAA env file missing required field: jwtToken`);
71
73
  return null;
72
74
  }
@@ -83,6 +83,7 @@ export declare class AbapSessionStore implements ISessionStore {
83
83
  /**
84
84
  * Set authorization configuration
85
85
  * Updates values needed for obtaining and refreshing tokens
86
+ * Creates new session if it doesn't exist
86
87
  * @param destination Destination name
87
88
  * @param config IAuthorizationConfig with values to set
88
89
  */
@@ -90,6 +91,7 @@ export declare class AbapSessionStore implements ISessionStore {
90
91
  /**
91
92
  * Set connection configuration
92
93
  * Updates values needed for connecting to services
94
+ * Creates new session if it doesn't exist
93
95
  * @param destination Destination name
94
96
  * @param config IConnectionConfig with values to set
95
97
  */
@@ -66,6 +66,11 @@ class AbapSessionStore {
66
66
  constructor(directory, log) {
67
67
  this.directory = directory;
68
68
  this.log = log;
69
+ // Ensure directory exists - create if it doesn't
70
+ if (!fs.existsSync(directory)) {
71
+ fs.mkdirSync(directory, { recursive: true });
72
+ this.log?.debug(`Created session directory: ${directory}`);
73
+ }
69
74
  }
70
75
  /**
71
76
  * Load session from file
@@ -252,13 +257,31 @@ class AbapSessionStore {
252
257
  /**
253
258
  * Set authorization configuration
254
259
  * Updates values needed for obtaining and refreshing tokens
260
+ * Creates new session if it doesn't exist
255
261
  * @param destination Destination name
256
262
  * @param config IAuthorizationConfig with values to set
257
263
  */
258
264
  async setAuthorizationConfig(destination, config) {
259
265
  const current = await this.loadRawSession(destination);
260
266
  if (!current) {
261
- throw new Error(`No session found for destination "${destination}"`);
267
+ // Session doesn't exist - try to get serviceUrl from connection config
268
+ // For ABAP, we need sapUrl to create session
269
+ const connConfig = await this.getConnectionConfig(destination);
270
+ const sapUrl = connConfig?.serviceUrl;
271
+ if (!sapUrl) {
272
+ throw new Error(`Cannot set authorization config for destination "${destination}": session does not exist and serviceUrl is required for ABAP sessions. Call setConnectionConfig first.`);
273
+ }
274
+ this.log?.debug(`Creating new session for ${destination} via setAuthorizationConfig: sapUrl(${sapUrl.substring(0, 40)}...)`);
275
+ const newSession = {
276
+ sapUrl,
277
+ jwtToken: connConfig?.authorizationToken || '', // Use token from connection config if available
278
+ uaaUrl: config.uaaUrl,
279
+ uaaClientId: config.uaaClientId,
280
+ uaaClientSecret: config.uaaClientSecret,
281
+ refreshToken: config.refreshToken,
282
+ };
283
+ await this.saveSession(destination, newSession);
284
+ return;
262
285
  }
263
286
  // Update authorization fields
264
287
  const updated = {
@@ -273,13 +296,28 @@ class AbapSessionStore {
273
296
  /**
274
297
  * Set connection configuration
275
298
  * Updates values needed for connecting to services
299
+ * Creates new session if it doesn't exist
276
300
  * @param destination Destination name
277
301
  * @param config IConnectionConfig with values to set
278
302
  */
279
303
  async setConnectionConfig(destination, config) {
280
304
  const current = await this.loadRawSession(destination);
281
305
  if (!current) {
282
- throw new Error(`No session found for destination "${destination}"`);
306
+ // Session doesn't exist - create new one
307
+ // For ABAP, serviceUrl is required
308
+ if (!config.serviceUrl) {
309
+ throw new Error(`Cannot create session for destination "${destination}": serviceUrl is required for ABAP sessions`);
310
+ }
311
+ this.log?.debug(`Creating new session for ${destination} via setConnectionConfig: serviceUrl(${config.serviceUrl.substring(0, 40)}...), token(${config.authorizationToken?.length || 0} chars)`);
312
+ const newSession = {
313
+ sapUrl: config.serviceUrl,
314
+ jwtToken: config.authorizationToken || '',
315
+ sapClient: config.sapClient,
316
+ language: config.language,
317
+ };
318
+ await this.saveSession(destination, newSession);
319
+ this.log?.info(`Session created for ${destination}: serviceUrl(${config.serviceUrl.substring(0, 40)}...), token(${config.authorizationToken?.length || 0} chars)`);
320
+ return;
283
321
  }
284
322
  // Update connection fields
285
323
  const updated = {
@@ -22,7 +22,6 @@ export declare class SafeAbapSessionStore implements ISessionStore {
22
22
  private loadRawSession;
23
23
  private validateSessionConfig;
24
24
  private convertToInternalFormat;
25
- private isValidSessionConfig;
26
25
  loadSession(destination: string): Promise<IConfig | null>;
27
26
  saveSession(destination: string, config: IConfig): Promise<void>;
28
27
  deleteSession(destination: string): Promise<void>;
@@ -58,13 +58,6 @@ class SafeAbapSessionStore {
58
58
  };
59
59
  return internal;
60
60
  }
61
- isValidSessionConfig(config) {
62
- if (!config || typeof config !== 'object')
63
- return false;
64
- const obj = config;
65
- // Accept both IConfig format (serviceUrl, authorizationToken) and internal format (sapUrl, jwtToken)
66
- return (('serviceUrl' in obj || 'sapUrl' in obj) && ('authorizationToken' in obj || 'jwtToken' in obj));
67
- }
68
61
  async loadSession(destination) {
69
62
  this.log?.debug(`Loading session for destination: ${destination}`);
70
63
  const authConfig = await this.getAuthorizationConfig(destination);
@@ -96,7 +89,7 @@ class SafeAbapSessionStore {
96
89
  }
97
90
  async getConnectionConfig(destination) {
98
91
  const sessionConfig = this.loadRawSession(destination);
99
- if (!this.isValidSessionConfig(sessionConfig)) {
92
+ if (!sessionConfig) {
100
93
  return null;
101
94
  }
102
95
  if (!sessionConfig.jwtToken || !sessionConfig.sapUrl) {
@@ -111,8 +104,23 @@ class SafeAbapSessionStore {
111
104
  }
112
105
  async setConnectionConfig(destination, config) {
113
106
  const current = this.loadRawSession(destination);
114
- if (!this.isValidSessionConfig(current)) {
115
- throw new Error(`No ABAP session found for destination "${destination}"`);
107
+ if (!current) {
108
+ // Session doesn't exist - create new one
109
+ // For ABAP, serviceUrl is required
110
+ if (!config.serviceUrl) {
111
+ throw new Error(`Cannot create session for destination "${destination}": serviceUrl is required for ABAP sessions`);
112
+ }
113
+ this.log?.debug(`Creating new session for ${destination} via setConnectionConfig: serviceUrl(${config.serviceUrl.substring(0, 40)}...), token(${config.authorizationToken?.length || 0} chars)`);
114
+ const newSession = {
115
+ sapUrl: config.serviceUrl,
116
+ jwtToken: config.authorizationToken || '',
117
+ sapClient: config.sapClient,
118
+ language: config.language,
119
+ };
120
+ // Save directly to Map (internal format)
121
+ this.sessions.set(destination, newSession);
122
+ this.log?.info(`Session created for ${destination}: serviceUrl(${config.serviceUrl.substring(0, 40)}...), token(${config.authorizationToken?.length || 0} chars)`);
123
+ return;
116
124
  }
117
125
  const updated = {
118
126
  ...current,
@@ -121,11 +129,12 @@ class SafeAbapSessionStore {
121
129
  sapClient: config.sapClient !== undefined ? config.sapClient : current.sapClient,
122
130
  language: config.language !== undefined ? config.language : current.language,
123
131
  };
124
- await this.saveSession(destination, updated);
132
+ // Save directly to Map (internal format)
133
+ this.sessions.set(destination, updated);
125
134
  }
126
135
  async getAuthorizationConfig(destination) {
127
136
  const sessionConfig = this.loadRawSession(destination);
128
- if (!this.isValidSessionConfig(sessionConfig)) {
137
+ if (!sessionConfig) {
129
138
  return null;
130
139
  }
131
140
  if (!sessionConfig.uaaUrl || !sessionConfig.uaaClientId || !sessionConfig.uaaClientSecret) {
@@ -140,8 +149,26 @@ class SafeAbapSessionStore {
140
149
  }
141
150
  async setAuthorizationConfig(destination, config) {
142
151
  const current = this.loadRawSession(destination);
143
- if (!this.isValidSessionConfig(current)) {
144
- throw new Error(`No ABAP session found for destination "${destination}"`);
152
+ if (!current) {
153
+ // Session doesn't exist - try to get serviceUrl from connection config
154
+ // For ABAP, we need sapUrl to create session
155
+ const connConfig = await this.getConnectionConfig(destination);
156
+ const sapUrl = connConfig?.serviceUrl;
157
+ if (!sapUrl) {
158
+ throw new Error(`Cannot set authorization config for destination "${destination}": session does not exist and serviceUrl is required for ABAP sessions. Call setConnectionConfig first.`);
159
+ }
160
+ this.log?.debug(`Creating new session for ${destination} via setAuthorizationConfig: sapUrl(${sapUrl.substring(0, 40)}...)`);
161
+ const newSession = {
162
+ sapUrl,
163
+ jwtToken: connConfig?.authorizationToken || '', // Use token from connection config if available
164
+ uaaUrl: config.uaaUrl,
165
+ uaaClientId: config.uaaClientId,
166
+ uaaClientSecret: config.uaaClientSecret,
167
+ refreshToken: config.refreshToken,
168
+ };
169
+ // Save directly to Map (internal format)
170
+ this.sessions.set(destination, newSession);
171
+ return;
145
172
  }
146
173
  const updated = {
147
174
  ...current,
@@ -150,7 +177,8 @@ class SafeAbapSessionStore {
150
177
  uaaClientSecret: config.uaaClientSecret,
151
178
  refreshToken: config.refreshToken || current.refreshToken,
152
179
  };
153
- await this.saveSession(destination, updated);
180
+ // Save directly to Map (internal format)
181
+ this.sessions.set(destination, updated);
154
182
  }
155
183
  }
156
184
  exports.SafeAbapSessionStore = SafeAbapSessionStore;
@@ -64,6 +64,11 @@ class BtpSessionStore {
64
64
  constructor(directory, log) {
65
65
  this.directory = directory;
66
66
  this.log = log;
67
+ // Ensure directory exists - create if it doesn't
68
+ if (!fs.existsSync(directory)) {
69
+ fs.mkdirSync(directory, { recursive: true });
70
+ this.log?.debug(`Created session directory: ${directory}`);
71
+ }
67
72
  }
68
73
  /**
69
74
  * Load session from file
@@ -117,7 +122,8 @@ class BtpSessionStore {
117
122
  throw new Error('BtpSessionStore can only store base BTP sessions (without abapUrl)');
118
123
  }
119
124
  // Validate required fields
120
- if (!config.jwtToken) {
125
+ // Allow empty string for jwtToken (can be set later via setConnectionConfig)
126
+ if (config.jwtToken === undefined || config.jwtToken === null) {
121
127
  throw new Error('Base BTP session config missing required field: jwtToken');
122
128
  }
123
129
  // Extract destination from file path
@@ -242,7 +248,8 @@ class BtpSessionStore {
242
248
  this.log?.debug(`Connection config not found for ${destination}`);
243
249
  return null;
244
250
  }
245
- if (!sessionConfig.jwtToken) {
251
+ // Return null if jwtToken is undefined or null (but allow empty string)
252
+ if (sessionConfig.jwtToken === undefined || sessionConfig.jwtToken === null) {
246
253
  this.log?.warn(`Connection config for ${destination} missing required field: jwtToken`);
247
254
  return null;
248
255
  }
@@ -261,7 +268,19 @@ class BtpSessionStore {
261
268
  async setAuthorizationConfig(destination, config) {
262
269
  const current = await this.loadRawSession(destination);
263
270
  if (!current) {
264
- throw new Error(`No session found for destination "${destination}"`);
271
+ // Session doesn't exist - create new one
272
+ // For BTP, mcpUrl is optional, so we can create session without it
273
+ this.log?.debug(`Creating new session for ${destination} via setAuthorizationConfig`);
274
+ const newSession = {
275
+ mcpUrl: undefined, // Will be set when connection config is set
276
+ jwtToken: '', // Will be set when connection config is set
277
+ uaaUrl: config.uaaUrl,
278
+ uaaClientId: config.uaaClientId,
279
+ uaaClientSecret: config.uaaClientSecret,
280
+ refreshToken: config.refreshToken,
281
+ };
282
+ await this.saveSession(destination, newSession);
283
+ return;
265
284
  }
266
285
  // Update authorization fields
267
286
  const updated = {
@@ -282,13 +301,22 @@ class BtpSessionStore {
282
301
  async setConnectionConfig(destination, config) {
283
302
  const current = await this.loadRawSession(destination);
284
303
  if (!current) {
285
- throw new Error(`No session found for destination "${destination}"`);
304
+ // Session doesn't exist - create new one
305
+ // For BTP, mcpUrl is optional
306
+ this.log?.debug(`Creating new session for ${destination} via setConnectionConfig: mcpUrl(${config.serviceUrl ? config.serviceUrl.substring(0, 40) + '...' : 'none'}), token(${config.authorizationToken?.length || 0} chars)`);
307
+ const newSession = {
308
+ mcpUrl: config.serviceUrl,
309
+ jwtToken: config.authorizationToken || '',
310
+ };
311
+ await this.saveSession(destination, newSession);
312
+ this.log?.info(`Session created for ${destination}: mcpUrl(${config.serviceUrl ? config.serviceUrl.substring(0, 40) + '...' : 'none'}), token(${config.authorizationToken?.length || 0} chars)`);
313
+ return;
286
314
  }
287
315
  // Update connection fields
288
316
  const updated = {
289
317
  ...current,
290
318
  mcpUrl: config.serviceUrl !== undefined ? config.serviceUrl : current.mcpUrl,
291
- jwtToken: config.authorizationToken,
319
+ jwtToken: config.authorizationToken !== undefined ? config.authorizationToken : current.jwtToken,
292
320
  };
293
321
  await this.saveSession(destination, updated);
294
322
  }
@@ -22,7 +22,6 @@ export declare class SafeBtpSessionStore implements ISessionStore {
22
22
  private loadRawSession;
23
23
  private validateSessionConfig;
24
24
  private convertToInternalFormat;
25
- private isValidSessionConfig;
26
25
  loadSession(destination: string): Promise<IConfig | null>;
27
26
  saveSession(destination: string, config: IConfig): Promise<void>;
28
27
  deleteSession(destination: string): Promise<void>;
@@ -40,7 +40,10 @@ class SafeBtpSessionStore {
40
40
  throw new Error('SafeBtpSessionStore can only store base BTP sessions (without abapUrl)');
41
41
  }
42
42
  // Accept IConfig format (has authorizationToken) or internal format (has jwtToken)
43
- if (!obj.authorizationToken && !obj.jwtToken) {
43
+ // Allow empty string for token (can be set later via setConnectionConfig)
44
+ const hasToken = (obj.authorizationToken !== undefined && obj.authorizationToken !== null) ||
45
+ (obj.jwtToken !== undefined && obj.jwtToken !== null);
46
+ if (!hasToken) {
44
47
  throw new Error('Base BTP session config missing required field: authorizationToken or jwtToken');
45
48
  }
46
49
  }
@@ -60,14 +63,6 @@ class SafeBtpSessionStore {
60
63
  };
61
64
  return internal;
62
65
  }
63
- isValidSessionConfig(config) {
64
- if (!config || typeof config !== 'object')
65
- return false;
66
- const obj = config;
67
- // Accept both IConfig format (authorizationToken) and internal format (jwtToken)
68
- // Reject ABAP (sapUrl) and BTP with abapUrl
69
- return (('authorizationToken' in obj || 'jwtToken' in obj) && !('sapUrl' in obj) && !('abapUrl' in obj));
70
- }
71
66
  async loadSession(destination) {
72
67
  this.log?.debug(`Loading session for destination: ${destination}`);
73
68
  const authConfig = await this.getAuthorizationConfig(destination);
@@ -99,10 +94,11 @@ class SafeBtpSessionStore {
99
94
  }
100
95
  async getConnectionConfig(destination) {
101
96
  const sessionConfig = this.loadRawSession(destination);
102
- if (!this.isValidSessionConfig(sessionConfig)) {
97
+ if (!sessionConfig) {
103
98
  return null;
104
99
  }
105
- if (!sessionConfig.jwtToken) {
100
+ // Return null if jwtToken is undefined or null (but allow empty string)
101
+ if (sessionConfig.jwtToken === undefined || sessionConfig.jwtToken === null) {
106
102
  return null;
107
103
  }
108
104
  return {
@@ -112,19 +108,30 @@ class SafeBtpSessionStore {
112
108
  }
113
109
  async setConnectionConfig(destination, config) {
114
110
  const current = this.loadRawSession(destination);
115
- if (!this.isValidSessionConfig(current)) {
116
- throw new Error(`No base BTP session found for destination "${destination}"`);
111
+ if (!current) {
112
+ // Session doesn't exist - create new one
113
+ // For BTP, mcpUrl is optional
114
+ this.log?.debug(`Creating new session for ${destination} via setConnectionConfig: mcpUrl(${config.serviceUrl ? config.serviceUrl.substring(0, 40) + '...' : 'none'}), token(${config.authorizationToken?.length || 0} chars)`);
115
+ const newSession = {
116
+ mcpUrl: config.serviceUrl,
117
+ jwtToken: config.authorizationToken || '',
118
+ };
119
+ // Save directly to Map (internal format)
120
+ this.sessions.set(destination, newSession);
121
+ this.log?.info(`Session created for ${destination}: mcpUrl(${config.serviceUrl ? config.serviceUrl.substring(0, 40) + '...' : 'none'}), token(${config.authorizationToken?.length || 0} chars)`);
122
+ return;
117
123
  }
118
124
  const updated = {
119
125
  ...current,
120
126
  mcpUrl: config.serviceUrl !== undefined ? config.serviceUrl : current.mcpUrl,
121
- jwtToken: config.authorizationToken,
127
+ jwtToken: config.authorizationToken !== undefined ? config.authorizationToken : current.jwtToken,
122
128
  };
123
- await this.saveSession(destination, updated);
129
+ // Save directly to Map (internal format)
130
+ this.sessions.set(destination, updated);
124
131
  }
125
132
  async getAuthorizationConfig(destination) {
126
133
  const sessionConfig = this.loadRawSession(destination);
127
- if (!this.isValidSessionConfig(sessionConfig)) {
134
+ if (!sessionConfig) {
128
135
  return null;
129
136
  }
130
137
  if (!sessionConfig.uaaUrl || !sessionConfig.uaaClientId || !sessionConfig.uaaClientSecret) {
@@ -139,8 +146,21 @@ class SafeBtpSessionStore {
139
146
  }
140
147
  async setAuthorizationConfig(destination, config) {
141
148
  const current = this.loadRawSession(destination);
142
- if (!this.isValidSessionConfig(current)) {
143
- throw new Error(`No base BTP session found for destination "${destination}"`);
149
+ if (!current) {
150
+ // Session doesn't exist - create new one
151
+ // For BTP, mcpUrl is optional, so we can create session without it
152
+ this.log?.debug(`Creating new session for ${destination} via setAuthorizationConfig`);
153
+ const newSession = {
154
+ mcpUrl: undefined, // Will be set when connection config is set
155
+ jwtToken: '', // Will be set when connection config is set
156
+ uaaUrl: config.uaaUrl,
157
+ uaaClientId: config.uaaClientId,
158
+ uaaClientSecret: config.uaaClientSecret,
159
+ refreshToken: config.refreshToken,
160
+ };
161
+ // Save directly to Map (internal format)
162
+ this.sessions.set(destination, newSession);
163
+ return;
144
164
  }
145
165
  const updated = {
146
166
  ...current,
@@ -149,7 +169,8 @@ class SafeBtpSessionStore {
149
169
  uaaClientSecret: config.uaaClientSecret,
150
170
  refreshToken: config.refreshToken || current.refreshToken,
151
171
  };
152
- await this.saveSession(destination, updated);
172
+ // Save directly to Map (internal format)
173
+ this.sessions.set(destination, updated);
153
174
  }
154
175
  }
155
176
  exports.SafeBtpSessionStore = SafeBtpSessionStore;
@@ -22,7 +22,6 @@ export declare class SafeXsuaaSessionStore implements ISessionStore {
22
22
  private loadRawSession;
23
23
  private validateSessionConfig;
24
24
  private convertToInternalFormat;
25
- private isValidSessionConfig;
26
25
  loadSession(destination: string): Promise<IConfig | null>;
27
26
  saveSession(destination: string, config: IConfig): Promise<void>;
28
27
  deleteSession(destination: string): Promise<void>;
@@ -40,7 +40,10 @@ class SafeXsuaaSessionStore {
40
40
  throw new Error('SafeXsuaaSessionStore can only store XSUAA sessions');
41
41
  }
42
42
  // Accept IConfig format (has authorizationToken) or internal format (has jwtToken)
43
- if (!obj.authorizationToken && !obj.jwtToken) {
43
+ // Allow empty string for token (can be set later via setConnectionConfig)
44
+ const hasToken = (obj.authorizationToken !== undefined && obj.authorizationToken !== null) ||
45
+ (obj.jwtToken !== undefined && obj.jwtToken !== null);
46
+ if (!hasToken) {
44
47
  throw new Error('XSUAA session config missing required field: authorizationToken or jwtToken');
45
48
  }
46
49
  }
@@ -60,14 +63,6 @@ class SafeXsuaaSessionStore {
60
63
  };
61
64
  return internal;
62
65
  }
63
- isValidSessionConfig(config) {
64
- if (!config || typeof config !== 'object')
65
- return false;
66
- const obj = config;
67
- // Accept both IConfig format (authorizationToken) and internal format (jwtToken)
68
- // Reject ABAP (sapUrl) and BTP with abapUrl
69
- return (('authorizationToken' in obj || 'jwtToken' in obj) && !('sapUrl' in obj) && !('abapUrl' in obj));
70
- }
71
66
  async loadSession(destination) {
72
67
  this.log?.debug(`Loading session for destination: ${destination}`);
73
68
  const authConfig = await this.getAuthorizationConfig(destination);
@@ -99,10 +94,11 @@ class SafeXsuaaSessionStore {
99
94
  }
100
95
  async getConnectionConfig(destination) {
101
96
  const sessionConfig = this.loadRawSession(destination);
102
- if (!this.isValidSessionConfig(sessionConfig)) {
97
+ if (!sessionConfig) {
103
98
  return null;
104
99
  }
105
- if (!sessionConfig.jwtToken) {
100
+ // Return null if jwtToken is undefined or null (but allow empty string)
101
+ if (sessionConfig.jwtToken === undefined || sessionConfig.jwtToken === null) {
106
102
  return null;
107
103
  }
108
104
  return {
@@ -112,19 +108,30 @@ class SafeXsuaaSessionStore {
112
108
  }
113
109
  async setConnectionConfig(destination, config) {
114
110
  const current = this.loadRawSession(destination);
115
- if (!this.isValidSessionConfig(current)) {
116
- throw new Error(`No XSUAA session found for destination "${destination}"`);
111
+ if (!current) {
112
+ // Session doesn't exist - create new one
113
+ // For XSUAA, mcpUrl is optional
114
+ this.log?.debug(`Creating new session for ${destination} via setConnectionConfig: mcpUrl(${config.serviceUrl ? config.serviceUrl.substring(0, 40) + '...' : 'none'}), token(${config.authorizationToken?.length || 0} chars)`);
115
+ const newSession = {
116
+ mcpUrl: config.serviceUrl,
117
+ jwtToken: config.authorizationToken || '',
118
+ };
119
+ // Save directly to Map (internal format)
120
+ this.sessions.set(destination, newSession);
121
+ this.log?.info(`Session created for ${destination}: mcpUrl(${config.serviceUrl ? config.serviceUrl.substring(0, 40) + '...' : 'none'}), token(${config.authorizationToken?.length || 0} chars)`);
122
+ return;
117
123
  }
118
124
  const updated = {
119
125
  ...current,
120
126
  mcpUrl: config.serviceUrl !== undefined ? config.serviceUrl : current.mcpUrl,
121
- jwtToken: config.authorizationToken,
127
+ jwtToken: config.authorizationToken !== undefined ? config.authorizationToken : current.jwtToken,
122
128
  };
123
- await this.saveSession(destination, updated);
129
+ // Save directly to Map (internal format)
130
+ this.sessions.set(destination, updated);
124
131
  }
125
132
  async getAuthorizationConfig(destination) {
126
133
  const sessionConfig = this.loadRawSession(destination);
127
- if (!this.isValidSessionConfig(sessionConfig)) {
134
+ if (!sessionConfig) {
128
135
  return null;
129
136
  }
130
137
  if (!sessionConfig.uaaUrl || !sessionConfig.uaaClientId || !sessionConfig.uaaClientSecret) {
@@ -139,8 +146,21 @@ class SafeXsuaaSessionStore {
139
146
  }
140
147
  async setAuthorizationConfig(destination, config) {
141
148
  const current = this.loadRawSession(destination);
142
- if (!this.isValidSessionConfig(current)) {
143
- throw new Error(`No XSUAA session found for destination "${destination}"`);
149
+ if (!current) {
150
+ // Session doesn't exist - create new one
151
+ // For XSUAA, mcpUrl is optional, so we can create session without it
152
+ this.log?.debug(`Creating new session for ${destination} via setAuthorizationConfig`);
153
+ const newSession = {
154
+ mcpUrl: undefined, // Will be set when connection config is set
155
+ jwtToken: '', // Will be set when connection config is set
156
+ uaaUrl: config.uaaUrl,
157
+ uaaClientId: config.uaaClientId,
158
+ uaaClientSecret: config.uaaClientSecret,
159
+ refreshToken: config.refreshToken,
160
+ };
161
+ // Save directly to Map (internal format)
162
+ this.sessions.set(destination, newSession);
163
+ return;
144
164
  }
145
165
  const updated = {
146
166
  ...current,
@@ -149,7 +169,8 @@ class SafeXsuaaSessionStore {
149
169
  uaaClientSecret: config.uaaClientSecret,
150
170
  refreshToken: config.refreshToken || current.refreshToken,
151
171
  };
152
- await this.saveSession(destination, updated);
172
+ // Save directly to Map (internal format)
173
+ this.sessions.set(destination, updated);
153
174
  }
154
175
  }
155
176
  exports.SafeXsuaaSessionStore = SafeXsuaaSessionStore;
@@ -64,6 +64,11 @@ class XsuaaSessionStore {
64
64
  constructor(directory, log) {
65
65
  this.directory = directory;
66
66
  this.log = log;
67
+ // Ensure directory exists - create if it doesn't
68
+ if (!fs.existsSync(directory)) {
69
+ fs.mkdirSync(directory, { recursive: true });
70
+ this.log?.debug(`Created session directory: ${directory}`);
71
+ }
67
72
  }
68
73
  /**
69
74
  * Get file name for destination
@@ -125,7 +130,8 @@ class XsuaaSessionStore {
125
130
  throw new Error('XsuaaSessionStore can only store XSUAA sessions');
126
131
  }
127
132
  // Validate required fields
128
- if (!config.jwtToken) {
133
+ // Allow empty string for jwtToken (can be set later via setConnectionConfig)
134
+ if (config.jwtToken === undefined || config.jwtToken === null) {
129
135
  throw new Error('XSUAA session config missing required field: jwtToken');
130
136
  }
131
137
  // Extract destination from file path
@@ -219,7 +225,8 @@ class XsuaaSessionStore {
219
225
  this.log?.debug(`Connection config not found for ${destination}`);
220
226
  return null;
221
227
  }
222
- if (!sessionConfig.jwtToken) {
228
+ // Return null if jwtToken is undefined or null (but allow empty string)
229
+ if (sessionConfig.jwtToken === undefined || sessionConfig.jwtToken === null) {
223
230
  this.log?.warn(`Connection config for ${destination} missing required field: jwtToken`);
224
231
  return null;
225
232
  }
@@ -232,13 +239,22 @@ class XsuaaSessionStore {
232
239
  async setConnectionConfig(destination, config) {
233
240
  const current = await this.loadRawSession(destination);
234
241
  if (!current) {
235
- throw new Error(`No session found for destination "${destination}"`);
242
+ // Session doesn't exist - create new one
243
+ // For XSUAA, mcpUrl is optional
244
+ this.log?.debug(`Creating new session for ${destination} via setConnectionConfig: mcpUrl(${config.serviceUrl ? config.serviceUrl.substring(0, 40) + '...' : 'none'}), token(${config.authorizationToken?.length || 0} chars)`);
245
+ const newSession = {
246
+ mcpUrl: config.serviceUrl,
247
+ jwtToken: config.authorizationToken || '',
248
+ };
249
+ await this.saveSession(destination, newSession);
250
+ this.log?.info(`Session created for ${destination}: mcpUrl(${config.serviceUrl ? config.serviceUrl.substring(0, 40) + '...' : 'none'}), token(${config.authorizationToken?.length || 0} chars)`);
251
+ return;
236
252
  }
237
253
  // Update connection fields
238
254
  const updated = {
239
255
  ...current,
240
256
  mcpUrl: config.serviceUrl !== undefined ? config.serviceUrl : current.mcpUrl,
241
- jwtToken: config.authorizationToken,
257
+ jwtToken: config.authorizationToken !== undefined ? config.authorizationToken : current.jwtToken,
242
258
  };
243
259
  await this.saveSession(destination, updated);
244
260
  }
@@ -263,7 +279,19 @@ class XsuaaSessionStore {
263
279
  async setAuthorizationConfig(destination, config) {
264
280
  const current = await this.loadRawSession(destination);
265
281
  if (!current) {
266
- throw new Error(`No session found for destination "${destination}"`);
282
+ // Session doesn't exist - create new one
283
+ // For XSUAA, mcpUrl is optional, so we can create session without it
284
+ this.log?.debug(`Creating new session for ${destination} via setAuthorizationConfig`);
285
+ const newSession = {
286
+ mcpUrl: undefined, // Will be set when connection config is set
287
+ jwtToken: '', // Will be set when connection config is set
288
+ uaaUrl: config.uaaUrl,
289
+ uaaClientId: config.uaaClientId,
290
+ uaaClientSecret: config.uaaClientSecret,
291
+ refreshToken: config.refreshToken,
292
+ };
293
+ await this.saveSession(destination, newSession);
294
+ return;
267
295
  }
268
296
  // Update authorization fields
269
297
  const updated = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-abap-adt/auth-stores",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "Stores for MCP ABAP ADT auth-broker - BTP, ABAP, and XSUAA implementations",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",