@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 +42 -0
- package/README.md +17 -5
- package/dist/storage/xsuaa/xsuaaEnvLoader.js +4 -2
- package/dist/stores/abap/AbapSessionStore.d.ts +2 -0
- package/dist/stores/abap/AbapSessionStore.js +40 -2
- package/dist/stores/abap/SafeAbapSessionStore.d.ts +0 -1
- package/dist/stores/abap/SafeAbapSessionStore.js +43 -15
- package/dist/stores/btp/BtpSessionStore.js +33 -5
- package/dist/stores/btp/SafeBtpSessionStore.d.ts +0 -1
- package/dist/stores/btp/SafeBtpSessionStore.js +40 -19
- package/dist/stores/xsuaa/SafeXsuaaSessionStore.d.ts +0 -1
- package/dist/stores/xsuaa/SafeXsuaaSessionStore.js +40 -19
- package/dist/stores/xsuaa/XsuaaSessionStore.js +33 -5
- package/package.json +1 -1
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
|
-
//
|
|
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
|
|
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/
|
|
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(${
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
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 (!
|
|
115
|
-
|
|
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
|
-
|
|
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 (!
|
|
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 (!
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
97
|
+
if (!sessionConfig) {
|
|
103
98
|
return null;
|
|
104
99
|
}
|
|
105
|
-
if (
|
|
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 (!
|
|
116
|
-
|
|
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
|
-
|
|
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 (!
|
|
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 (!
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (!
|
|
97
|
+
if (!sessionConfig) {
|
|
103
98
|
return null;
|
|
104
99
|
}
|
|
105
|
-
if (
|
|
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 (!
|
|
116
|
-
|
|
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
|
-
|
|
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 (!
|
|
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 (!
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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 = {
|