@mcp-abap-adt/proxy 1.6.4 → 4.0.0
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 +217 -0
- package/LICENSE +669 -17
- package/README.md +79 -8
- package/bin/mcp-abap-adt-proxy-mcp.js +113 -0
- package/dist/index.d.ts +10 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +54 -97
- package/dist/lib/stores.d.ts +23 -2
- package/dist/lib/stores.d.ts.map +1 -1
- package/dist/lib/stores.js +56 -1
- package/dist/mcp/cli.d.ts +2 -0
- package/dist/mcp/cli.d.ts.map +1 -0
- package/dist/mcp/cli.js +21 -0
- package/dist/mcp/configs.d.ts +33 -0
- package/dist/mcp/configs.d.ts.map +1 -0
- package/dist/mcp/configs.js +142 -0
- package/dist/mcp/ports.d.ts +15 -0
- package/dist/mcp/ports.d.ts.map +1 -0
- package/dist/mcp/ports.js +35 -0
- package/dist/mcp/registry.d.ts +57 -0
- package/dist/mcp/registry.d.ts.map +1 -0
- package/dist/mcp/registry.js +145 -0
- package/dist/mcp/server.d.ts +34 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +79 -0
- package/dist/mcp/shutdown.d.ts +37 -0
- package/dist/mcp/shutdown.d.ts.map +1 -0
- package/dist/mcp/shutdown.js +82 -0
- package/dist/mcp/supervisor.d.ts +125 -0
- package/dist/mcp/supervisor.d.ts.map +1 -0
- package/dist/mcp/supervisor.js +330 -0
- package/dist/mcp/tools.d.ts +28 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +152 -0
- package/dist/proxy/btpProxy.d.ts +24 -73
- package/dist/proxy/btpProxy.d.ts.map +1 -1
- package/dist/proxy/btpProxy.js +116 -631
- package/dist/proxy/credentials.d.ts +45 -0
- package/dist/proxy/credentials.d.ts.map +1 -0
- package/dist/proxy/credentials.js +41 -0
- package/dist/proxy/requestHandler.d.ts +38 -0
- package/dist/proxy/requestHandler.d.ts.map +1 -0
- package/dist/proxy/requestHandler.js +73 -0
- package/dist/proxy/reverseProxy.d.ts +11 -2
- package/dist/proxy/reverseProxy.d.ts.map +1 -1
- package/dist/proxy/reverseProxy.js +52 -9
- package/dist/router/headerAnalyzer.js +2 -2
- package/dist/router/requestInterceptor.js +9 -9
- package/docs/API.md +172 -0
- package/docs/ARCHITECTURE.md +322 -0
- package/docs/CLIENT_SETUP.md +413 -0
- package/docs/CONFIGURATION.md +258 -0
- package/docs/MIGRATION-4.0.md +125 -0
- package/docs/ROUTING_LOGIC.md +126 -0
- package/docs/TROUBLESHOOTING.md +488 -0
- package/docs/USAGE.md +422 -0
- package/docs/YAML_CONFIG.md +273 -0
- package/docs/mcp-proxy-config.example.yaml +62 -0
- package/package.json +17 -10
- package/dist/proxy/cloudLlmHubProxy.d.ts +0 -2
- package/dist/proxy/cloudLlmHubProxy.d.ts.map +0 -1
- package/dist/proxy/cloudLlmHubProxy.js +0 -3
package/dist/proxy/btpProxy.js
CHANGED
|
@@ -5,20 +5,32 @@
|
|
|
5
5
|
* For requests with x-sap-destination, proxies to target MCP server
|
|
6
6
|
* with JWT token from auth-broker (XSUAA/BTP)
|
|
7
7
|
*/
|
|
8
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
-
};
|
|
11
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
9
|
exports.BtpProxy = void 0;
|
|
13
10
|
exports.shouldWriteStderr = shouldWriteStderr;
|
|
14
11
|
exports.createBtpProxy = createBtpProxy;
|
|
15
12
|
const auth_broker_1 = require("@mcp-abap-adt/auth-broker");
|
|
16
13
|
const auth_providers_1 = require("@mcp-abap-adt/auth-providers");
|
|
17
|
-
const interfaces_1 = require("@mcp-abap-adt/interfaces");
|
|
18
|
-
const axios_1 = __importDefault(require("axios"));
|
|
19
14
|
const config_js_1 = require("../lib/config.js");
|
|
20
15
|
const logger_js_1 = require("../lib/logger.js");
|
|
21
16
|
const stores_js_1 = require("../lib/stores.js");
|
|
17
|
+
const credentials_js_1 = require("./credentials.js");
|
|
18
|
+
/**
|
|
19
|
+
* Default port for the local OAuth2 callback server, kept away from the
|
|
20
|
+
* proxy's own default HTTP port (3001). auth-providers@2.0.0's own default
|
|
21
|
+
* (61001) is deliberately not inherited here: this port is documented as
|
|
22
|
+
* `--browser-auth-port` and may be registered with the identity provider, so
|
|
23
|
+
* silently moving it would break every proxy already relying on 3333.
|
|
24
|
+
*/
|
|
25
|
+
const DEFAULT_BROWSER_AUTH_PORT = 3333;
|
|
26
|
+
/**
|
|
27
|
+
* auth-providers@2.0.0's `browserCallbackStrategy` defaults `timeoutMs` to 30
|
|
28
|
+
* seconds — sized for an unattended caller. Here the login is driven by a
|
|
29
|
+
* person: a browser tab has to open, and they may need to type credentials or
|
|
30
|
+
* complete MFA before the redirect lands. Five minutes gives them room to do
|
|
31
|
+
* that without the callback server tearing itself down mid-login.
|
|
32
|
+
*/
|
|
33
|
+
const INTERACTIVE_LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
22
34
|
/**
|
|
23
35
|
* Adapter to convert proxy Logger to ILogger interface expected by AuthBroker
|
|
24
36
|
*/
|
|
@@ -54,14 +66,9 @@ function shouldWriteStderr() {
|
|
|
54
66
|
* BTP Proxy Client
|
|
55
67
|
*/
|
|
56
68
|
class BtpProxy {
|
|
57
|
-
axiosInstance;
|
|
58
69
|
defaultBtpAuthBroker;
|
|
59
70
|
btpAuthBrokers = new Map();
|
|
60
|
-
|
|
61
|
-
refreshTimers = new Map();
|
|
62
|
-
TOKEN_CACHE_TTL = 30 * 60 * 1000; // fallback if JWT has no exp
|
|
63
|
-
REFRESH_LEAD_MS = 5 * 60 * 1000; // refresh 5 min before expiry
|
|
64
|
-
circuitBreaker;
|
|
71
|
+
credentials;
|
|
65
72
|
config;
|
|
66
73
|
unsafe;
|
|
67
74
|
constructor(defaultBtpAuthBroker, config) {
|
|
@@ -84,53 +91,15 @@ class BtpProxy {
|
|
|
84
91
|
type: 'CONFIG_FALLBACK',
|
|
85
92
|
});
|
|
86
93
|
}
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
httpsAgent: new https.Agent({
|
|
97
|
-
rejectUnauthorized: process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0',
|
|
98
|
-
keepAlive: true,
|
|
99
|
-
}),
|
|
100
|
-
});
|
|
101
|
-
// Add request interceptor for logging
|
|
102
|
-
this.axiosInstance.interceptors.request.use((config) => {
|
|
103
|
-
logger_js_1.logger?.debug('Proxying request to target service', {
|
|
104
|
-
type: 'PROXY_REQUEST',
|
|
105
|
-
method: config.method,
|
|
106
|
-
url: config.url,
|
|
107
|
-
baseURL: config.baseURL,
|
|
108
|
-
});
|
|
109
|
-
return config;
|
|
110
|
-
}, (error) => {
|
|
111
|
-
logger_js_1.logger?.error('Request interceptor error', {
|
|
112
|
-
type: 'MCP_PROXY_REQUEST_ERROR',
|
|
113
|
-
error: error instanceof Error ? error.message : String(error),
|
|
114
|
-
});
|
|
115
|
-
return Promise.reject(error);
|
|
116
|
-
});
|
|
117
|
-
// Add response interceptor for logging
|
|
118
|
-
this.axiosInstance.interceptors.response.use((response) => {
|
|
119
|
-
logger_js_1.logger?.debug('Received response from target service', {
|
|
120
|
-
type: 'PROXY_RESPONSE',
|
|
121
|
-
status: response.status,
|
|
122
|
-
url: response.config.url,
|
|
123
|
-
});
|
|
124
|
-
return response;
|
|
125
|
-
}, (error) => {
|
|
126
|
-
logger_js_1.logger?.error('Response interceptor error', {
|
|
127
|
-
type: 'MCP_PROXY_RESPONSE_ERROR',
|
|
128
|
-
error: error instanceof Error ? error.message : String(error),
|
|
129
|
-
status: error.response?.status,
|
|
130
|
-
url: error.config?.url,
|
|
131
|
-
});
|
|
132
|
-
return Promise.reject(error);
|
|
133
|
-
});
|
|
94
|
+
// One credential per destination, over the brokers this class already
|
|
95
|
+
// caches. The lookup is passed rather than the broker itself so a
|
|
96
|
+
// destination that has never been seen still gets one built.
|
|
97
|
+
this.credentials = new credentials_js_1.DestinationCredentials((destination) => this.getOrCreateBtpAuthBroker(destination));
|
|
98
|
+
// No circuit breaker. It only ever guarded the buffered axios forward that
|
|
99
|
+
// M2 deleted, and the streaming path has nowhere to put one without
|
|
100
|
+
// buffering the response again. `circuitBreakerThreshold` and
|
|
101
|
+
// `circuitBreakerTimeout` are still accepted so existing configs load, and
|
|
102
|
+
// are documented as inert.
|
|
134
103
|
}
|
|
135
104
|
/**
|
|
136
105
|
* Initialize authentication for destination (eager auth)
|
|
@@ -143,7 +112,7 @@ class BtpProxy {
|
|
|
143
112
|
});
|
|
144
113
|
try {
|
|
145
114
|
// Just getting the token will trigger authentication
|
|
146
|
-
await this.
|
|
115
|
+
await this.getAuthorizationHeader(destination);
|
|
147
116
|
logger_js_1.logger?.info('BTP proxy authentication initialized successfully', {
|
|
148
117
|
type: 'BTP_PROXY_INIT_SUCCESS',
|
|
149
118
|
destination,
|
|
@@ -211,21 +180,28 @@ class BtpProxy {
|
|
|
211
180
|
}
|
|
212
181
|
// Always use AuthorizationCodeProvider (enforced)
|
|
213
182
|
// Map IAuthorizationConfig (uaaClientId) to ProviderConfig (clientId)
|
|
214
|
-
//
|
|
183
|
+
// How the login is conducted is now a strategy the provider config
|
|
184
|
+
// carries under `authorization`; the callback port lives inside it.
|
|
185
|
+
//
|
|
186
|
+
// uaaUrl/clientId/clientSecret fall back to '' rather than being omitted
|
|
187
|
+
// when authConfig is missing: AuthorizationCodeProviderConfig declares
|
|
188
|
+
// them required, and the provider's own constructor already treats an
|
|
189
|
+
// empty string the same as absent, throwing its "Missing required
|
|
190
|
+
// fields" ValidationError — which is the clear failure this was always
|
|
191
|
+
// meant to produce.
|
|
215
192
|
const providerConfig = {
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
193
|
+
authorization: (0, auth_providers_1.browserCallbackStrategy)({
|
|
194
|
+
browser: this.config.browser,
|
|
195
|
+
port: this.config.browserAuthPort || DEFAULT_BROWSER_AUTH_PORT,
|
|
196
|
+
timeoutMs: INTERACTIVE_LOGIN_TIMEOUT_MS,
|
|
197
|
+
}),
|
|
198
|
+
// Pass a logger so the callback strategy can surface the "Open this
|
|
199
|
+
// URL" prompt in 'none'/'headless' mode. Without it, the callback
|
|
200
|
+
// server gets a null logger and silently drops the authorization URL.
|
|
221
201
|
logger: loggerAdapter,
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
clientId: authConfig.uaaClientId,
|
|
226
|
-
clientSecret: authConfig.uaaClientSecret,
|
|
227
|
-
}
|
|
228
|
-
: {}),
|
|
202
|
+
uaaUrl: authConfig?.uaaUrl ?? '',
|
|
203
|
+
clientId: authConfig?.uaaClientId ?? '',
|
|
204
|
+
clientSecret: authConfig?.uaaClientSecret ?? '',
|
|
229
205
|
};
|
|
230
206
|
const tokenProvider = new auth_providers_1.AuthorizationCodeProvider(providerConfig);
|
|
231
207
|
broker = new auth_broker_1.AuthBroker({
|
|
@@ -317,170 +293,81 @@ class BtpProxy {
|
|
|
317
293
|
return broker;
|
|
318
294
|
}
|
|
319
295
|
/**
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
296
|
+
* The `Authorization` header value for a destination, or `null` when this
|
|
297
|
+
* credential is not a header at all.
|
|
298
|
+
*
|
|
299
|
+
* A complete header value, not a token: the caller puts it on the request as
|
|
300
|
+
* it stands. The credential is asked every time because it renews behind this
|
|
301
|
+
* call — a cache here would serve the stale token and hide the renewal the
|
|
302
|
+
* broker exists to do, which is precisely what the token cache, the JWT `exp`
|
|
303
|
+
* decoder and the per-destination refresh timer that used to live here did.
|
|
304
|
+
*
|
|
305
|
+
* Retried, because a refusal here is not always an answer. In the standalone
|
|
306
|
+
* proxy an authentication failure is FATAL — it exits so something can start
|
|
307
|
+
* it again — so without this a single transient UAA 503 kills a proxy that
|
|
308
|
+
* used to ride it out. Only failures that can get better are retried;
|
|
309
|
+
* `isRetryableError` says which, and a missing service key is not one of
|
|
310
|
+
* them, so it fails on the first attempt instead of three times slower.
|
|
323
311
|
*/
|
|
324
|
-
async
|
|
325
|
-
logger_js_1.logger?.
|
|
326
|
-
type: '
|
|
312
|
+
async getAuthorizationHeader(destination) {
|
|
313
|
+
logger_js_1.logger?.debug('Asking the credential for a header', {
|
|
314
|
+
type: 'CREDENTIAL_HEADER_GET',
|
|
327
315
|
destination,
|
|
328
316
|
});
|
|
329
|
-
const authBroker = await this.getOrCreateBtpAuthBroker(destination);
|
|
330
|
-
// Check cache first (unless force refresh)
|
|
331
|
-
if (!forceRefresh) {
|
|
332
|
-
const cached = this.tokenCache.get(destination);
|
|
333
|
-
if (cached && cached.expiresAt > Date.now()) {
|
|
334
|
-
logger_js_1.logger?.debug('Using cached JWT token', {
|
|
335
|
-
type: 'JWT_TOKEN_CACHE_HIT',
|
|
336
|
-
destination,
|
|
337
|
-
});
|
|
338
|
-
return cached.token;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
// Retry logic for token retrieval
|
|
342
317
|
const retryOptions = {
|
|
343
318
|
maxRetries: this.config.maxRetries || 3,
|
|
344
319
|
retryDelay: this.config.retryDelay || 1000,
|
|
345
320
|
retryableStatusCodes: [500, 502, 503, 504],
|
|
346
321
|
};
|
|
347
322
|
try {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
this.tokenCache.delete(destination);
|
|
352
|
-
}
|
|
353
|
-
logger_js_1.logger?.debug('Getting JWT token from auth-broker', {
|
|
354
|
-
type: 'JWT_TOKEN_REQUEST_START',
|
|
355
|
-
destination,
|
|
356
|
-
forceRefresh,
|
|
357
|
-
});
|
|
358
|
-
// Get token from auth-broker
|
|
359
|
-
const token = await authBroker.getToken(destination);
|
|
360
|
-
this.cacheToken(destination, token);
|
|
361
|
-
logger_js_1.logger?.debug('Retrieved JWT token from auth-broker', {
|
|
362
|
-
type: 'JWT_TOKEN_RETRIEVED',
|
|
363
|
-
destination,
|
|
364
|
-
forceRefresh,
|
|
365
|
-
});
|
|
366
|
-
return token;
|
|
323
|
+
return await (0, errorHandler_js_1.retryWithBackoff)(async () => {
|
|
324
|
+
const { credential } = await this.credentials.get(destination);
|
|
325
|
+
return credential.authorizationHeader();
|
|
367
326
|
}, retryOptions);
|
|
368
|
-
return token;
|
|
369
327
|
}
|
|
370
328
|
catch (error) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const searchedInMatch = errorMessage.match(/Searched in:\s*([\s\S]*?)(?:\n|$)/);
|
|
375
|
-
const searchedPaths = searchedInMatch
|
|
376
|
-
? searchedInMatch[1]
|
|
377
|
-
.trim()
|
|
378
|
-
.split('\n')
|
|
379
|
-
.map((p) => p.trim().replace(/^-\s*/, ''))
|
|
380
|
-
.filter((p) => p)
|
|
381
|
-
: [];
|
|
382
|
-
errorMessage =
|
|
383
|
-
`Service key file not found for destination "${destination}".\n` +
|
|
384
|
-
`Please create service key file: ${destination}.json\n`;
|
|
385
|
-
if (searchedPaths.length > 0) {
|
|
386
|
-
errorMessage += `Searched in:\n`;
|
|
387
|
-
searchedPaths.forEach((path) => {
|
|
388
|
-
errorMessage += ` - ${path}\n`;
|
|
389
|
-
});
|
|
390
|
-
}
|
|
391
|
-
else {
|
|
392
|
-
const isWindows = process.platform === 'win32';
|
|
393
|
-
const homeDir = require('node:os').homedir();
|
|
394
|
-
const defaultPath = isWindows
|
|
395
|
-
? require('node:path').join(homeDir, 'Documents', 'mcp-abap-adt', 'service-keys')
|
|
396
|
-
: require('node:path').join(homeDir, '.config', 'mcp-abap-adt', 'service-keys');
|
|
397
|
-
errorMessage += `Searched in:\n - ${defaultPath}\n`;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
logger_js_1.logger?.error('Failed to get JWT token from auth-broker after retries', {
|
|
401
|
-
type: 'JWT_TOKEN_ERROR',
|
|
329
|
+
const message = this.explainAuthFailure(error, destination);
|
|
330
|
+
logger_js_1.logger?.error('Failed to get an authorization header', {
|
|
331
|
+
type: 'CREDENTIAL_HEADER_ERROR',
|
|
402
332
|
destination,
|
|
403
|
-
error:
|
|
333
|
+
error: message,
|
|
404
334
|
});
|
|
405
335
|
if (shouldWriteStderr()) {
|
|
406
|
-
process.stderr.write(`[MCP Proxy] ✗ ${
|
|
336
|
+
process.stderr.write(`[MCP Proxy] ✗ ${message}\n`);
|
|
407
337
|
}
|
|
408
|
-
throw new Error(
|
|
338
|
+
throw new Error(message);
|
|
409
339
|
}
|
|
410
340
|
}
|
|
411
341
|
/**
|
|
412
|
-
*
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
try {
|
|
419
|
-
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf8'));
|
|
420
|
-
return typeof payload.exp === 'number' ? payload.exp : null;
|
|
421
|
-
}
|
|
422
|
-
catch {
|
|
423
|
-
return null;
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
/**
|
|
427
|
-
* Cache token with expiry derived from JWT, and schedule proactive refresh
|
|
428
|
-
* 5 minutes before expiry.
|
|
429
|
-
*/
|
|
430
|
-
cacheToken(destination, token) {
|
|
431
|
-
const exp = this.decodeJwtExp(token);
|
|
432
|
-
const expiresAt = exp !== null ? exp * 1000 : Date.now() + this.TOKEN_CACHE_TTL;
|
|
433
|
-
this.tokenCache.set(destination, { token, expiresAt });
|
|
434
|
-
this.scheduleProactiveRefresh(destination, expiresAt);
|
|
435
|
-
}
|
|
436
|
-
/**
|
|
437
|
-
* Schedule background refresh REFRESH_LEAD_MS before token expiry.
|
|
438
|
-
*/
|
|
439
|
-
scheduleProactiveRefresh(destination, expiresAt) {
|
|
440
|
-
const existing = this.refreshTimers.get(destination);
|
|
441
|
-
if (existing)
|
|
442
|
-
clearTimeout(existing);
|
|
443
|
-
const delay = expiresAt - Date.now() - this.REFRESH_LEAD_MS;
|
|
444
|
-
if (delay <= 0) {
|
|
445
|
-
// Token already within lead window or expired; skip scheduling.
|
|
446
|
-
this.refreshTimers.delete(destination);
|
|
447
|
-
return;
|
|
448
|
-
}
|
|
449
|
-
const timer = setTimeout(() => {
|
|
450
|
-
this.refreshTimers.delete(destination);
|
|
451
|
-
this.proactiveRefresh(destination).catch((err) => {
|
|
452
|
-
logger_js_1.logger?.warn('Proactive token refresh failed, will retry reactively', {
|
|
453
|
-
type: 'JWT_TOKEN_PROACTIVE_REFRESH_ERROR',
|
|
454
|
-
destination,
|
|
455
|
-
error: err instanceof Error ? err.message : String(err),
|
|
456
|
-
});
|
|
457
|
-
});
|
|
458
|
-
}, delay);
|
|
459
|
-
// Don't keep the event loop alive just for refresh timers.
|
|
460
|
-
timer.unref?.();
|
|
461
|
-
this.refreshTimers.set(destination, timer);
|
|
462
|
-
}
|
|
463
|
-
/**
|
|
464
|
-
* Background refresh via auth-broker's refreshToken grant.
|
|
342
|
+
* Turn the broker's failure into one that names what this proxy needs.
|
|
343
|
+
*
|
|
344
|
+
* The broker speaks of `.env` and `mcp.env` because it can be fed either way.
|
|
345
|
+
* This proxy only ever reads service keys, so passing that message through
|
|
346
|
+
* told the most common failure — a missing key — to go and create a file that
|
|
347
|
+
* would be ignored.
|
|
465
348
|
*/
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
}
|
|
471
|
-
const
|
|
472
|
-
const
|
|
473
|
-
|
|
349
|
+
explainAuthFailure(error, destination) {
|
|
350
|
+
const original = error instanceof Error ? error.message : String(error);
|
|
351
|
+
if (!original.includes('.env') && !original.includes('mcp.env')) {
|
|
352
|
+
return original;
|
|
353
|
+
}
|
|
354
|
+
const searched = original.match(/Searched in:\s*([\s\S]*?)(?:\n\n|$)/);
|
|
355
|
+
const paths = searched
|
|
356
|
+
? searched[1]
|
|
357
|
+
.trim()
|
|
358
|
+
.split('\n')
|
|
359
|
+
.map((p) => p.trim().replace(/^-\s*/, ''))
|
|
360
|
+
.filter(Boolean)
|
|
361
|
+
: [(0, stores_js_1.storeDir)('service-keys')];
|
|
362
|
+
return [
|
|
363
|
+
`Service key file not found for destination "${destination}".`,
|
|
364
|
+
`Please create service key file: ${destination}.json`,
|
|
365
|
+
'Searched in:',
|
|
366
|
+
...paths.map((p) => ` - ${p}`),
|
|
367
|
+
].join('\n');
|
|
474
368
|
}
|
|
475
|
-
/**
|
|
476
|
-
* Cancel all pending refresh timers. Call on shutdown.
|
|
477
|
-
*/
|
|
478
369
|
dispose() {
|
|
479
|
-
|
|
480
|
-
clearTimeout(timer);
|
|
481
|
-
}
|
|
482
|
-
this.refreshTimers.clear();
|
|
483
|
-
this.tokenCache.clear();
|
|
370
|
+
this.credentials.clear();
|
|
484
371
|
this.btpAuthBrokers.clear();
|
|
485
372
|
}
|
|
486
373
|
/**
|
|
@@ -499,421 +386,6 @@ class BtpProxy {
|
|
|
499
386
|
throw new Error(`No target URL found for destination "${destination}". ` +
|
|
500
387
|
`Set targetUrl in config or ensure service key contains abap.url.`);
|
|
501
388
|
}
|
|
502
|
-
/**
|
|
503
|
-
* Helper function to extract string value from header (handles arrays)
|
|
504
|
-
*/
|
|
505
|
-
getHeaderValue(headerValue) {
|
|
506
|
-
if (!headerValue)
|
|
507
|
-
return undefined;
|
|
508
|
-
if (Array.isArray(headerValue)) {
|
|
509
|
-
return headerValue[0]?.trim();
|
|
510
|
-
}
|
|
511
|
-
return typeof headerValue === 'string' ? headerValue.trim() : undefined;
|
|
512
|
-
}
|
|
513
|
-
/**
|
|
514
|
-
* Build proxy request with JWT token (BTP authentication only)
|
|
515
|
-
*
|
|
516
|
-
* Process flow:
|
|
517
|
-
*
|
|
518
|
-
* 1. BTP Authentication (XSUAA):
|
|
519
|
-
* 1.1 If x-sap-destination header exists:
|
|
520
|
-
* - Check map for broker with key = destination, get or create, save to map
|
|
521
|
-
* - Get token from xsuaa broker
|
|
522
|
-
* - Add/replace Authorization: Bearer <token> header
|
|
523
|
-
* 1.2 If header doesn't exist but --btp parameter exists:
|
|
524
|
-
* - Use destination from parameter, get or create broker, save to map
|
|
525
|
-
* - Get token from xsuaa broker
|
|
526
|
-
* - Add/replace Authorization: Bearer <token> header
|
|
527
|
-
* 1.3 If neither header nor parameter:
|
|
528
|
-
* - Do nothing, pass request further
|
|
529
|
-
*/
|
|
530
|
-
async buildProxyRequest(originalRequest, routingDecision, originalHeaders, forceTokenRefresh = false) {
|
|
531
|
-
logger_js_1.logger?.info('=== BUILD PROXY REQUEST - ORIGINAL ===', {
|
|
532
|
-
type: 'BUILD_PROXY_REQUEST_ORIGINAL',
|
|
533
|
-
originalRequestId: originalRequest.id,
|
|
534
|
-
originalRequestIdType: typeof originalRequest.id,
|
|
535
|
-
originalRequestMethod: originalRequest.method,
|
|
536
|
-
fullOriginalRequest: JSON.stringify(originalRequest),
|
|
537
|
-
});
|
|
538
|
-
// Build headers for target MCP server
|
|
539
|
-
const proxyHeaders = {
|
|
540
|
-
[interfaces_1.HEADER_CONTENT_TYPE]: 'application/json',
|
|
541
|
-
[interfaces_1.HEADER_ACCEPT]: 'application/json, application/x-ndjson, text/event-stream',
|
|
542
|
-
};
|
|
543
|
-
// ============================================
|
|
544
|
-
// BTP Authentication (XSUAA)
|
|
545
|
-
// ============================================
|
|
546
|
-
const btpDestinationHeader = this.getHeaderValue(originalHeaders[interfaces_1.HEADER_SAP_DESTINATION]);
|
|
547
|
-
const btpDestinationFromParam = routingDecision.btpDestination && !btpDestinationHeader
|
|
548
|
-
? routingDecision.btpDestination
|
|
549
|
-
: undefined;
|
|
550
|
-
let btpDestination;
|
|
551
|
-
if (btpDestinationHeader) {
|
|
552
|
-
btpDestination = btpDestinationHeader;
|
|
553
|
-
logger_js_1.logger?.debug('Using x-sap-destination from header', {
|
|
554
|
-
type: 'BTP_DESTINATION_FROM_HEADER',
|
|
555
|
-
destination: btpDestination,
|
|
556
|
-
});
|
|
557
|
-
}
|
|
558
|
-
else if (btpDestinationFromParam) {
|
|
559
|
-
btpDestination = btpDestinationFromParam;
|
|
560
|
-
logger_js_1.logger?.debug('Using x-sap-destination from parameter', {
|
|
561
|
-
type: 'BTP_DESTINATION_FROM_PARAM',
|
|
562
|
-
destination: btpDestination,
|
|
563
|
-
});
|
|
564
|
-
}
|
|
565
|
-
if (btpDestination) {
|
|
566
|
-
const _btpBroker = await this.getOrCreateBtpAuthBroker(btpDestination);
|
|
567
|
-
const existingAuth = originalHeaders[interfaces_1.HEADER_AUTHORIZATION.toLowerCase()] ||
|
|
568
|
-
originalHeaders[interfaces_1.HEADER_AUTHORIZATION];
|
|
569
|
-
const hasExistingAuth = !!existingAuth;
|
|
570
|
-
const authToken = await this.getJwtToken(btpDestination, forceTokenRefresh);
|
|
571
|
-
proxyHeaders[interfaces_1.HEADER_AUTHORIZATION] = `Bearer ${authToken}`;
|
|
572
|
-
logger_js_1.logger?.debug(hasExistingAuth
|
|
573
|
-
? 'Replaced existing Authorization header with BTP token'
|
|
574
|
-
: 'Added BTP Cloud authorization token', {
|
|
575
|
-
type: hasExistingAuth
|
|
576
|
-
? 'BTP_AUTH_TOKEN_REPLACED'
|
|
577
|
-
: 'BTP_AUTH_TOKEN_ADDED',
|
|
578
|
-
destination: btpDestination,
|
|
579
|
-
hadExistingAuth: hasExistingAuth,
|
|
580
|
-
});
|
|
581
|
-
}
|
|
582
|
-
else {
|
|
583
|
-
logger_js_1.logger?.debug('No BTP destination - skipping authentication', {
|
|
584
|
-
type: 'BTP_AUTH_SKIPPED',
|
|
585
|
-
});
|
|
586
|
-
}
|
|
587
|
-
// Preserve other SAP headers if provided by client
|
|
588
|
-
const sapHeaders = [interfaces_1.HEADER_SAP_CLIENT, interfaces_1.HEADER_SAP_DESTINATION_SERVICE];
|
|
589
|
-
for (const headerName of sapHeaders) {
|
|
590
|
-
const value = originalHeaders[headerName];
|
|
591
|
-
if (value) {
|
|
592
|
-
proxyHeaders[headerName] = Array.isArray(value) ? value[0] : value;
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
// Get MCP server URL from BTP destination service key
|
|
596
|
-
let baseUrl;
|
|
597
|
-
if (btpDestination) {
|
|
598
|
-
try {
|
|
599
|
-
const btpBroker = await this.getOrCreateBtpAuthBroker(btpDestination, routingDecision.targetUrl);
|
|
600
|
-
const connConfig = await btpBroker.getConnectionConfig(btpDestination);
|
|
601
|
-
// Use service key URL as base (if available)
|
|
602
|
-
if (connConfig?.serviceUrl) {
|
|
603
|
-
baseUrl = connConfig.serviceUrl;
|
|
604
|
-
logger_js_1.logger?.debug('Using MCP URL from BTP destination service key', {
|
|
605
|
-
type: 'MCP_URL_FROM_BTP_DESTINATION',
|
|
606
|
-
destination: btpDestination,
|
|
607
|
-
url: baseUrl,
|
|
608
|
-
});
|
|
609
|
-
}
|
|
610
|
-
else {
|
|
611
|
-
logger_js_1.logger?.warn('BTP destination service key does not contain service URL', {
|
|
612
|
-
type: 'BTP_DESTINATION_NO_URL',
|
|
613
|
-
destination: btpDestination,
|
|
614
|
-
});
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
catch (error) {
|
|
618
|
-
logger_js_1.logger?.warn('Failed to get URL from BTP destination service key', {
|
|
619
|
-
type: 'BTP_DESTINATION_URL_ERROR',
|
|
620
|
-
destination: btpDestination,
|
|
621
|
-
error: error instanceof Error ? error.message : String(error),
|
|
622
|
-
});
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
// Apply Target URL override logic
|
|
626
|
-
if (routingDecision.targetUrl) {
|
|
627
|
-
if (baseUrl) {
|
|
628
|
-
logger_js_1.logger?.debug('Overriding service key URL with explicit target URL', {
|
|
629
|
-
type: 'TARGET_URL_OVERRIDE',
|
|
630
|
-
url: routingDecision.targetUrl,
|
|
631
|
-
serviceKeyUrl: baseUrl,
|
|
632
|
-
});
|
|
633
|
-
}
|
|
634
|
-
else {
|
|
635
|
-
logger_js_1.logger?.debug('Using explicit target URL from configuration', {
|
|
636
|
-
type: 'TARGET_URL_FROM_CONFIG',
|
|
637
|
-
url: routingDecision.targetUrl,
|
|
638
|
-
});
|
|
639
|
-
}
|
|
640
|
-
baseUrl = routingDecision.targetUrl;
|
|
641
|
-
}
|
|
642
|
-
if (!baseUrl) {
|
|
643
|
-
throw new Error('Cannot determine target URL: provide btpDestination with service key containing URL OR use --target-url');
|
|
644
|
-
}
|
|
645
|
-
// Construct full MCP endpoint URL
|
|
646
|
-
let fullUrl;
|
|
647
|
-
// Determine the path to append
|
|
648
|
-
// If original request has a URL/path, use it. Otherwise default to nothing/root.
|
|
649
|
-
const requestPath = originalRequest.url || '';
|
|
650
|
-
// Check if we should use URL as-is
|
|
651
|
-
// 1. If it already looks like an MCP URL
|
|
652
|
-
// 2. OR if it was explicitly provided via targetUrl (user knows best)
|
|
653
|
-
const isExplicitTarget = !!routingDecision.targetUrl;
|
|
654
|
-
const hasMcpPath = baseUrl.includes('/mcp/') ||
|
|
655
|
-
baseUrl.endsWith('/mcp') ||
|
|
656
|
-
baseUrl.includes('/mcp/stream/');
|
|
657
|
-
if (isExplicitTarget) {
|
|
658
|
-
// Explicit Target URL logic (as per User request):
|
|
659
|
-
// "whatever endpoint [proxy] received, it adds to the target URL"
|
|
660
|
-
// Use join logic to avoid double slashes and ensure correctness
|
|
661
|
-
// Remove trailing slash from base
|
|
662
|
-
const base = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
|
663
|
-
// Ensure path starts with slash if not empty
|
|
664
|
-
const path = requestPath.startsWith('/')
|
|
665
|
-
? requestPath
|
|
666
|
-
: `/${requestPath}`;
|
|
667
|
-
// If requestPath is empty or just '/', ensure we don't end up with empty path if base is root
|
|
668
|
-
// But typically we just append.
|
|
669
|
-
fullUrl = `${base}${path}`;
|
|
670
|
-
logger_js_1.logger?.debug('Using explicit target URL with forwarded path', {
|
|
671
|
-
type: 'TARGET_URL_PATH_FORWARDING',
|
|
672
|
-
base,
|
|
673
|
-
path,
|
|
674
|
-
final: fullUrl,
|
|
675
|
-
});
|
|
676
|
-
}
|
|
677
|
-
else if (hasMcpPath) {
|
|
678
|
-
// Use as-is (strip trailing slash if present, though typically not needed if strictly as-is, but good practice for consistency)
|
|
679
|
-
fullUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
|
680
|
-
logger_js_1.logger?.debug('Using target URL as-is', {
|
|
681
|
-
type: 'MCP_URL_AS_IS',
|
|
682
|
-
original: baseUrl,
|
|
683
|
-
final: fullUrl,
|
|
684
|
-
reason: 'path detection',
|
|
685
|
-
});
|
|
686
|
-
}
|
|
687
|
-
else {
|
|
688
|
-
// Default: append MCP path
|
|
689
|
-
const mcpPath = '/mcp/stream/http';
|
|
690
|
-
fullUrl = baseUrl.endsWith('/')
|
|
691
|
-
? `${baseUrl.slice(0, -1)}${mcpPath}`
|
|
692
|
-
: `${baseUrl}${mcpPath}`;
|
|
693
|
-
logger_js_1.logger?.debug('Appended MCP path to base URL', {
|
|
694
|
-
type: 'MCP_URL_APPENDED',
|
|
695
|
-
original: baseUrl,
|
|
696
|
-
final: fullUrl,
|
|
697
|
-
});
|
|
698
|
-
}
|
|
699
|
-
const builtHeaders = {};
|
|
700
|
-
for (const [key, value] of Object.entries(proxyHeaders)) {
|
|
701
|
-
const lowerKey = key.toLowerCase();
|
|
702
|
-
if (lowerKey.includes('token') ||
|
|
703
|
-
lowerKey.includes('authorization') ||
|
|
704
|
-
lowerKey.includes('password') ||
|
|
705
|
-
lowerKey.includes('secret')) {
|
|
706
|
-
builtHeaders[key] = value
|
|
707
|
-
? `${String(value).substring(0, 20)}...`
|
|
708
|
-
: '[REDACTED]';
|
|
709
|
-
}
|
|
710
|
-
else {
|
|
711
|
-
builtHeaders[key] = String(value || '');
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
const sanitizedRequestParams = {};
|
|
715
|
-
const requestData = originalRequest.data;
|
|
716
|
-
if (requestData &&
|
|
717
|
-
requestData.params &&
|
|
718
|
-
typeof requestData.params === 'object' &&
|
|
719
|
-
requestData.params !== null) {
|
|
720
|
-
const params = requestData.params;
|
|
721
|
-
// ... (existing sanitization logic) ...
|
|
722
|
-
if (params.arguments &&
|
|
723
|
-
typeof params.arguments === 'object' &&
|
|
724
|
-
params.arguments !== null) {
|
|
725
|
-
sanitizedRequestParams.arguments = {};
|
|
726
|
-
const sanitizedArgs = sanitizedRequestParams.arguments;
|
|
727
|
-
for (const [key, value] of Object.entries(params.arguments)) {
|
|
728
|
-
const lowerKey = key.toLowerCase();
|
|
729
|
-
if (lowerKey.includes('password') ||
|
|
730
|
-
lowerKey.includes('token') ||
|
|
731
|
-
lowerKey.includes('secret')) {
|
|
732
|
-
sanitizedArgs[key] = '[REDACTED]';
|
|
733
|
-
}
|
|
734
|
-
else {
|
|
735
|
-
sanitizedArgs[key] = value;
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
for (const [key, value] of Object.entries(params)) {
|
|
740
|
-
if (key === 'arguments')
|
|
741
|
-
continue;
|
|
742
|
-
sanitizedRequestParams[key] = value;
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
logger_js_1.logger?.info('=== BUILDING PROXY REQUEST ===', {
|
|
746
|
-
type: 'PROXY_REQUEST_BUILT',
|
|
747
|
-
btpDestination: btpDestination,
|
|
748
|
-
url: fullUrl,
|
|
749
|
-
headers: builtHeaders,
|
|
750
|
-
baseUrl,
|
|
751
|
-
fullUrl,
|
|
752
|
-
hasAuthToken: !!proxyHeaders[interfaces_1.HEADER_AUTHORIZATION],
|
|
753
|
-
btpSource: btpDestinationHeader
|
|
754
|
-
? 'header'
|
|
755
|
-
: btpDestinationFromParam
|
|
756
|
-
? 'parameter'
|
|
757
|
-
: 'none',
|
|
758
|
-
requestMethod: originalRequest.method,
|
|
759
|
-
requestId: originalRequest.id,
|
|
760
|
-
requestParams: sanitizedRequestParams,
|
|
761
|
-
});
|
|
762
|
-
// Return axios config with full URL
|
|
763
|
-
// Use original request method
|
|
764
|
-
const axiosConfig = {
|
|
765
|
-
method: originalRequest.method,
|
|
766
|
-
url: fullUrl,
|
|
767
|
-
headers: proxyHeaders,
|
|
768
|
-
data: originalRequest.data, // Send the original data (body)
|
|
769
|
-
};
|
|
770
|
-
logger_js_1.logger?.debug('Axios request config', {
|
|
771
|
-
type: 'AXIOS_REQUEST_CONFIG',
|
|
772
|
-
method: axiosConfig.method,
|
|
773
|
-
url: axiosConfig.url,
|
|
774
|
-
headers: Object.keys(axiosConfig.headers),
|
|
775
|
-
dataKeys: originalRequest ? Object.keys(originalRequest) : [],
|
|
776
|
-
});
|
|
777
|
-
return axiosConfig;
|
|
778
|
-
}
|
|
779
|
-
/**
|
|
780
|
-
* Proxy MCP request to target server with retry, circuit breaker, and error handling
|
|
781
|
-
*/
|
|
782
|
-
async proxyRequest(originalRequest, routingDecision, originalHeaders) {
|
|
783
|
-
// Check circuit breaker
|
|
784
|
-
if (!this.circuitBreaker.canProceed()) {
|
|
785
|
-
logger_js_1.logger?.warn('Circuit breaker is open, rejecting request', {
|
|
786
|
-
type: 'CIRCUIT_BREAKER_REJECTED',
|
|
787
|
-
btpDestination: routingDecision.btpDestination,
|
|
788
|
-
});
|
|
789
|
-
return (0, errorHandler_js_1.createErrorResponse)(originalRequest.id || null, -32001, 'Service temporarily unavailable (circuit breaker open)', { circuitBreakerState: this.circuitBreaker.getState() });
|
|
790
|
-
}
|
|
791
|
-
const retryOptions = {
|
|
792
|
-
maxRetries: this.config.maxRetries || 3,
|
|
793
|
-
retryDelay: this.config.retryDelay || 1000,
|
|
794
|
-
retryableStatusCodes: [500, 502, 503, 504],
|
|
795
|
-
};
|
|
796
|
-
try {
|
|
797
|
-
let forceTokenRefresh = false;
|
|
798
|
-
const executeRequest = async () => {
|
|
799
|
-
const proxyConfig = await this.buildProxyRequest(originalRequest, routingDecision, originalHeaders, forceTokenRefresh);
|
|
800
|
-
// Log outgoing request details
|
|
801
|
-
const outgoingHeaders = {};
|
|
802
|
-
for (const [key, value] of Object.entries(proxyConfig.headers || {})) {
|
|
803
|
-
const lowerKey = key.toLowerCase();
|
|
804
|
-
if (lowerKey.includes('token') ||
|
|
805
|
-
lowerKey.includes('authorization') ||
|
|
806
|
-
lowerKey.includes('password') ||
|
|
807
|
-
lowerKey.includes('secret')) {
|
|
808
|
-
outgoingHeaders[key] = value
|
|
809
|
-
? `${String(value).substring(0, 20)}...`
|
|
810
|
-
: '[REDACTED]';
|
|
811
|
-
}
|
|
812
|
-
else {
|
|
813
|
-
outgoingHeaders[key] = String(value || '');
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
const sanitizedOutgoingBody = {};
|
|
817
|
-
if (proxyConfig.data && typeof proxyConfig.data === 'object') {
|
|
818
|
-
if (proxyConfig.data.params &&
|
|
819
|
-
typeof proxyConfig.data.params === 'object' &&
|
|
820
|
-
proxyConfig.data.params !== null) {
|
|
821
|
-
const params = proxyConfig.data.params;
|
|
822
|
-
sanitizedOutgoingBody.params = {};
|
|
823
|
-
const sanitizedParams = sanitizedOutgoingBody.params;
|
|
824
|
-
if (params.arguments &&
|
|
825
|
-
typeof params.arguments === 'object' &&
|
|
826
|
-
params.arguments !== null) {
|
|
827
|
-
sanitizedParams.arguments = {};
|
|
828
|
-
const sanitizedArgs = sanitizedParams.arguments;
|
|
829
|
-
for (const [key, value] of Object.entries(params.arguments)) {
|
|
830
|
-
const lowerKey = key.toLowerCase();
|
|
831
|
-
if (lowerKey.includes('password') ||
|
|
832
|
-
lowerKey.includes('token') ||
|
|
833
|
-
lowerKey.includes('secret')) {
|
|
834
|
-
sanitizedArgs[key] = '[REDACTED]';
|
|
835
|
-
}
|
|
836
|
-
else {
|
|
837
|
-
sanitizedArgs[key] = value;
|
|
838
|
-
}
|
|
839
|
-
}
|
|
840
|
-
}
|
|
841
|
-
for (const [key, value] of Object.entries(params)) {
|
|
842
|
-
if (key === 'arguments')
|
|
843
|
-
continue;
|
|
844
|
-
sanitizedParams[key] = value;
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
sanitizedOutgoingBody.method = proxyConfig.data.method;
|
|
848
|
-
sanitizedOutgoingBody.id = proxyConfig.data.id;
|
|
849
|
-
sanitizedOutgoingBody.jsonrpc = proxyConfig.data.jsonrpc;
|
|
850
|
-
}
|
|
851
|
-
logger_js_1.logger?.info('=== SENDING PROXY REQUEST ===', {
|
|
852
|
-
type: 'PROXY_REQUEST_SENDING',
|
|
853
|
-
url: proxyConfig.url,
|
|
854
|
-
headers: outgoingHeaders,
|
|
855
|
-
body: sanitizedOutgoingBody,
|
|
856
|
-
});
|
|
857
|
-
const response = await this.axiosInstance.request(proxyConfig);
|
|
858
|
-
this.circuitBreaker.recordSuccess();
|
|
859
|
-
return response;
|
|
860
|
-
};
|
|
861
|
-
const response = await (0, errorHandler_js_1.retryWithBackoff)(async () => {
|
|
862
|
-
try {
|
|
863
|
-
return await executeRequest();
|
|
864
|
-
}
|
|
865
|
-
catch (error) {
|
|
866
|
-
if (!forceTokenRefresh && (0, errorHandler_js_1.isTokenExpirationError)(error)) {
|
|
867
|
-
const destination = routingDecision.btpDestination ||
|
|
868
|
-
this.getHeaderValue(originalHeaders[interfaces_1.HEADER_SAP_DESTINATION]);
|
|
869
|
-
logger_js_1.logger?.warn('Token expired, retrying with refreshed token', {
|
|
870
|
-
type: 'TOKEN_EXPIRED_RETRY',
|
|
871
|
-
destination,
|
|
872
|
-
});
|
|
873
|
-
if (destination) {
|
|
874
|
-
this.tokenCache.delete(destination);
|
|
875
|
-
}
|
|
876
|
-
forceTokenRefresh = true;
|
|
877
|
-
return await executeRequest();
|
|
878
|
-
}
|
|
879
|
-
throw error;
|
|
880
|
-
}
|
|
881
|
-
}, retryOptions);
|
|
882
|
-
// Return response in JSON-RPC format
|
|
883
|
-
return {
|
|
884
|
-
jsonrpc: '2.0',
|
|
885
|
-
id: originalRequest.id || null,
|
|
886
|
-
result: response.data.result,
|
|
887
|
-
error: response.data.error,
|
|
888
|
-
};
|
|
889
|
-
}
|
|
890
|
-
catch (error) {
|
|
891
|
-
this.circuitBreaker.recordFailure();
|
|
892
|
-
let statusCode = 500;
|
|
893
|
-
let errorMessage = 'Internal Server Error';
|
|
894
|
-
let errorData;
|
|
895
|
-
if (axios_1.default.isAxiosError(error)) {
|
|
896
|
-
statusCode = error.response?.status || 500;
|
|
897
|
-
errorMessage = error.message;
|
|
898
|
-
errorData = error.response?.data;
|
|
899
|
-
logger_js_1.logger?.error('Axios error in proxy request', {
|
|
900
|
-
type: 'AXIOS_PROXY_ERROR',
|
|
901
|
-
status: statusCode,
|
|
902
|
-
message: errorMessage,
|
|
903
|
-
data: errorData,
|
|
904
|
-
url: error.config?.url,
|
|
905
|
-
});
|
|
906
|
-
}
|
|
907
|
-
else {
|
|
908
|
-
errorMessage = error instanceof Error ? error.message : String(error);
|
|
909
|
-
logger_js_1.logger?.error('Generic error in proxy request', {
|
|
910
|
-
type: 'GENERIC_PROXY_ERROR',
|
|
911
|
-
message: errorMessage,
|
|
912
|
-
});
|
|
913
|
-
}
|
|
914
|
-
return (0, errorHandler_js_1.createErrorResponse)(originalRequest.id || null, -32000, `Proxy error: ${errorMessage}`, errorData);
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
389
|
/**
|
|
918
390
|
* Create a new BtpProxy instance
|
|
919
391
|
*/
|
|
@@ -928,8 +400,21 @@ class BtpProxy {
|
|
|
928
400
|
uaaUrl: 'https://placeholder.authentication.sap.hana.ondemand.com',
|
|
929
401
|
clientId: 'placeholder',
|
|
930
402
|
clientSecret: 'placeholder',
|
|
931
|
-
|
|
932
|
-
|
|
403
|
+
authorization: (0, auth_providers_1.browserCallbackStrategy)({
|
|
404
|
+
browser: loadedConfig.browser,
|
|
405
|
+
// This call site never had its own fallback: it passed
|
|
406
|
+
// `redirectPort` straight through and relied on
|
|
407
|
+
// auth-providers@1.2.0 defaulting an omitted value to 3001
|
|
408
|
+
// internally — which is this proxy's own default `httpPort`
|
|
409
|
+
// (src/lib/config.ts). An omitted `browserAuthPort` therefore
|
|
410
|
+
// told the callback server to bind the port the proxy already
|
|
411
|
+
// listens on, failing every such login with "Port 3001 is
|
|
412
|
+
// already in use". Both call sites now share the one documented
|
|
413
|
+
// default instead of leaving this one to an invisible, colliding
|
|
414
|
+
// number.
|
|
415
|
+
port: loadedConfig.browserAuthPort || DEFAULT_BROWSER_AUTH_PORT,
|
|
416
|
+
timeoutMs: INTERACTIVE_LOGIN_TIMEOUT_MS,
|
|
417
|
+
}),
|
|
933
418
|
logger: loggerAdapter,
|
|
934
419
|
}),
|
|
935
420
|
}, 'none', loggerAdapter);
|