@mcp-abap-adt/proxy 0.1.4 → 0.1.8
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 +66 -1
- package/README.md +73 -63
- package/bin/mcp-abap-adt-proxy.js +15 -42
- package/dist/index.d.ts +3 -12
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +440 -273
- package/dist/lib/config.d.ts +13 -4
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +110 -92
- package/dist/lib/errorHandler.d.ts +3 -3
- package/dist/lib/errorHandler.d.ts.map +1 -1
- package/dist/lib/errorHandler.js +31 -28
- package/dist/lib/logger.d.ts.map +1 -1
- package/dist/lib/logger.js +15 -1
- package/dist/lib/stores.d.ts +7 -6
- package/dist/lib/stores.d.ts.map +1 -1
- package/dist/lib/stores.js +121 -8
- package/dist/lib/transportConfig.d.ts +1 -1
- package/dist/lib/transportConfig.d.ts.map +1 -1
- package/dist/lib/transportConfig.js +58 -38
- package/dist/proxy/btpProxy.d.ts +102 -0
- package/dist/proxy/btpProxy.d.ts.map +1 -0
- package/dist/proxy/btpProxy.js +833 -0
- package/dist/proxy/cloudLlmHubProxy.d.ts +1 -60
- package/dist/proxy/cloudLlmHubProxy.d.ts.map +1 -1
- package/dist/proxy/cloudLlmHubProxy.js +1 -378
- package/dist/router/headerAnalyzer.d.ts +14 -18
- package/dist/router/headerAnalyzer.d.ts.map +1 -1
- package/dist/router/headerAnalyzer.js +40 -67
- package/dist/router/requestInterceptor.d.ts +5 -11
- package/dist/router/requestInterceptor.d.ts.map +1 -1
- package/dist/router/requestInterceptor.js +33 -30
- package/package.json +33 -14
- package/dist/router/directCloudRouter.d.ts +0 -32
- package/dist/router/directCloudRouter.d.ts.map +0 -1
- package/dist/router/directCloudRouter.js +0 -179
- package/dist/router/localBasicRouter.d.ts +0 -23
- package/dist/router/localBasicRouter.d.ts.map +0 -1
- package/dist/router/localBasicRouter.js +0 -140
|
@@ -0,0 +1,833 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* BTP Proxy - Proxies requests to MCP servers with JWT authentication
|
|
4
|
+
*
|
|
5
|
+
* For requests with x-btp-destination, proxies to target MCP server
|
|
6
|
+
* with JWT token from auth-broker (XSUAA/BTP)
|
|
7
|
+
*/
|
|
8
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
9
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.BtpProxy = void 0;
|
|
13
|
+
exports.shouldWriteStderr = shouldWriteStderr;
|
|
14
|
+
exports.createBtpProxy = createBtpProxy;
|
|
15
|
+
const auth_broker_1 = require("@mcp-abap-adt/auth-broker");
|
|
16
|
+
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
|
+
const config_js_1 = require("../lib/config.js");
|
|
20
|
+
const logger_js_1 = require("../lib/logger.js");
|
|
21
|
+
const stores_js_1 = require("../lib/stores.js");
|
|
22
|
+
/**
|
|
23
|
+
* Adapter to convert proxy Logger to ILogger interface expected by AuthBroker
|
|
24
|
+
*/
|
|
25
|
+
const loggerAdapter = {
|
|
26
|
+
debug: (message, meta) => logger_js_1.logger?.debug(message, meta && typeof meta === 'object' && meta !== null
|
|
27
|
+
? meta
|
|
28
|
+
: undefined),
|
|
29
|
+
info: (message, meta) => logger_js_1.logger?.info(message, meta && typeof meta === 'object' && meta !== null
|
|
30
|
+
? meta
|
|
31
|
+
: undefined),
|
|
32
|
+
warn: (message, meta) => logger_js_1.logger?.warn(message, meta && typeof meta === 'object' && meta !== null
|
|
33
|
+
? meta
|
|
34
|
+
: undefined),
|
|
35
|
+
error: (message, meta) => logger_js_1.logger?.error(message, meta && typeof meta === 'object' && meta !== null
|
|
36
|
+
? meta
|
|
37
|
+
: undefined),
|
|
38
|
+
};
|
|
39
|
+
const errorHandler_js_1 = require("../lib/errorHandler.js");
|
|
40
|
+
/**
|
|
41
|
+
* Check if error messages should be written to stderr
|
|
42
|
+
* Only output in verbose mode and not in test environment
|
|
43
|
+
*/
|
|
44
|
+
function shouldWriteStderr() {
|
|
45
|
+
const verboseMode = process.env.MCP_PROXY_VERBOSE === 'true' ||
|
|
46
|
+
process.env.DEBUG === 'true' ||
|
|
47
|
+
process.env.DEBUG?.includes('mcp-proxy') === true;
|
|
48
|
+
const isTestEnv = process.env.NODE_ENV === 'test' ||
|
|
49
|
+
process.env.JEST_WORKER_ID !== undefined ||
|
|
50
|
+
typeof globalThis.jest !== 'undefined';
|
|
51
|
+
return verboseMode && !isTestEnv;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* BTP Proxy Client
|
|
55
|
+
*/
|
|
56
|
+
class BtpProxy {
|
|
57
|
+
axiosInstance;
|
|
58
|
+
defaultBtpAuthBroker;
|
|
59
|
+
btpAuthBrokers = new Map();
|
|
60
|
+
tokenCache = new Map();
|
|
61
|
+
TOKEN_CACHE_TTL = 30 * 60 * 1000; // 30 minutes
|
|
62
|
+
circuitBreaker;
|
|
63
|
+
config;
|
|
64
|
+
unsafe;
|
|
65
|
+
constructor(defaultBtpAuthBroker, config) {
|
|
66
|
+
this.defaultBtpAuthBroker = defaultBtpAuthBroker;
|
|
67
|
+
this.unsafe = config?.unsafe ?? false;
|
|
68
|
+
// Use provided config if available (from YAML or CLI/ENV, already loaded in index.ts)
|
|
69
|
+
// If no config provided, load from CLI/ENV as fallback
|
|
70
|
+
if (config) {
|
|
71
|
+
this.config = config;
|
|
72
|
+
logger_js_1.logger?.debug('Using provided config in BtpProxy constructor', {
|
|
73
|
+
type: 'CONFIG_PROVIDED',
|
|
74
|
+
btpDestination: this.config.btpDestination,
|
|
75
|
+
configKeys: Object.keys(this.config),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
// Fallback: load from CLI/ENV (should not happen if called from index.ts)
|
|
80
|
+
this.config = (0, config_js_1.loadConfig)();
|
|
81
|
+
logger_js_1.logger?.info('Using fallback config from CLI/ENV in BtpProxy constructor', {
|
|
82
|
+
type: 'CONFIG_FALLBACK',
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
// Initialize circuit breaker
|
|
86
|
+
this.circuitBreaker = new errorHandler_js_1.CircuitBreaker(this.config.circuitBreakerThreshold || 5, this.config.circuitBreakerTimeout || 60000);
|
|
87
|
+
// Create axios instance without baseURL - we'll use full URLs from BTP destination
|
|
88
|
+
const https = require('node:https');
|
|
89
|
+
this.axiosInstance = axios_1.default.create({
|
|
90
|
+
timeout: this.config.requestTimeout || 60000,
|
|
91
|
+
headers: {
|
|
92
|
+
[interfaces_1.HEADER_CONTENT_TYPE]: 'application/json',
|
|
93
|
+
},
|
|
94
|
+
httpsAgent: new https.Agent({
|
|
95
|
+
rejectUnauthorized: process.env.NODE_TLS_REJECT_UNAUTHORIZED !== '0',
|
|
96
|
+
keepAlive: true,
|
|
97
|
+
}),
|
|
98
|
+
});
|
|
99
|
+
// Add request interceptor for logging
|
|
100
|
+
this.axiosInstance.interceptors.request.use((config) => {
|
|
101
|
+
logger_js_1.logger?.debug('Proxying request to target service', {
|
|
102
|
+
type: 'PROXY_REQUEST',
|
|
103
|
+
method: config.method,
|
|
104
|
+
url: config.url,
|
|
105
|
+
baseURL: config.baseURL,
|
|
106
|
+
});
|
|
107
|
+
return config;
|
|
108
|
+
}, (error) => {
|
|
109
|
+
logger_js_1.logger?.error('Request interceptor error', {
|
|
110
|
+
type: 'MCP_PROXY_REQUEST_ERROR',
|
|
111
|
+
error: error instanceof Error ? error.message : String(error),
|
|
112
|
+
});
|
|
113
|
+
return Promise.reject(error);
|
|
114
|
+
});
|
|
115
|
+
// Add response interceptor for logging
|
|
116
|
+
this.axiosInstance.interceptors.response.use((response) => {
|
|
117
|
+
logger_js_1.logger?.debug('Received response from target service', {
|
|
118
|
+
type: 'PROXY_RESPONSE',
|
|
119
|
+
status: response.status,
|
|
120
|
+
url: response.config.url,
|
|
121
|
+
});
|
|
122
|
+
return response;
|
|
123
|
+
}, (error) => {
|
|
124
|
+
logger_js_1.logger?.error('Response interceptor error', {
|
|
125
|
+
type: 'MCP_PROXY_RESPONSE_ERROR',
|
|
126
|
+
error: error instanceof Error ? error.message : String(error),
|
|
127
|
+
status: error.response?.status,
|
|
128
|
+
url: error.config?.url,
|
|
129
|
+
});
|
|
130
|
+
return Promise.reject(error);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Initialize authentication for destination (eager auth)
|
|
135
|
+
* This triggers the browser authentication flow immediately if needed.
|
|
136
|
+
*/
|
|
137
|
+
async initialize(destination) {
|
|
138
|
+
logger_js_1.logger?.info('Initializing BTP proxy authentication', {
|
|
139
|
+
type: 'BTP_PROXY_INIT',
|
|
140
|
+
destination,
|
|
141
|
+
});
|
|
142
|
+
try {
|
|
143
|
+
// Just getting the token will trigger authentication
|
|
144
|
+
await this.getJwtToken(destination);
|
|
145
|
+
logger_js_1.logger?.info('BTP proxy authentication initialized successfully', {
|
|
146
|
+
type: 'BTP_PROXY_INIT_SUCCESS',
|
|
147
|
+
destination,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
logger_js_1.logger?.error('Failed to initialize BTP proxy authentication', {
|
|
152
|
+
type: 'BTP_PROXY_INIT_ERROR',
|
|
153
|
+
destination,
|
|
154
|
+
error: error instanceof Error ? error.message : String(error),
|
|
155
|
+
});
|
|
156
|
+
// We don't throw here to avoid crashing the server on startup,
|
|
157
|
+
// but the user will see the error in logs/stderr (via getJwtToken)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Get or create BTP auth broker for specific destination
|
|
162
|
+
* If destination is not provided, returns default broker
|
|
163
|
+
* If broker doesn't exist in map, creates new one and stores it
|
|
164
|
+
*/
|
|
165
|
+
async getOrCreateBtpAuthBroker(destination, targetUrl) {
|
|
166
|
+
// If no destination, use default broker
|
|
167
|
+
if (!destination) {
|
|
168
|
+
return this.defaultBtpAuthBroker;
|
|
169
|
+
}
|
|
170
|
+
// Check if broker exists in map
|
|
171
|
+
let broker = this.btpAuthBrokers.get(destination);
|
|
172
|
+
if (broker) {
|
|
173
|
+
return broker;
|
|
174
|
+
}
|
|
175
|
+
// Create new broker for this destination
|
|
176
|
+
logger_js_1.logger?.info('Creating new BTP auth broker for destination', {
|
|
177
|
+
type: 'BTP_BROKER_CREATE',
|
|
178
|
+
destination,
|
|
179
|
+
});
|
|
180
|
+
const { serviceKeyStore, sessionStore } = await (0, stores_js_1.getPlatformStores)(this.unsafe);
|
|
181
|
+
// We must manually load the credentials because AuthorizationCodeProvider validates them in constructor
|
|
182
|
+
let authConfig = null;
|
|
183
|
+
try {
|
|
184
|
+
// Try service key store first
|
|
185
|
+
if (serviceKeyStore) {
|
|
186
|
+
authConfig = await serviceKeyStore.getAuthorizationConfig(destination);
|
|
187
|
+
}
|
|
188
|
+
// If not found, try session store (though less likely for initial setup)
|
|
189
|
+
if (!authConfig) {
|
|
190
|
+
authConfig = await sessionStore.getAuthorizationConfig(destination);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
logger_js_1.logger?.warn('Failed to load auth config for provider initialization', {
|
|
195
|
+
error: error instanceof Error ? error.message : String(error),
|
|
196
|
+
destination,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
if (!authConfig) {
|
|
200
|
+
logger_js_1.logger?.error('Service key not found for destination', {
|
|
201
|
+
destination,
|
|
202
|
+
hint: 'Ensure service key file exists in ~/.config/mcp-abap-adt/service-keys/',
|
|
203
|
+
});
|
|
204
|
+
// We cannot proceed without config, but we'll let it fail with a clear message
|
|
205
|
+
// Or we could throw here.
|
|
206
|
+
// If we don't provide config, provider will throw "Missing required fields".
|
|
207
|
+
}
|
|
208
|
+
// Always use AuthorizationCodeProvider (enforced)
|
|
209
|
+
// Map IAuthorizationConfig (uaaClientId) to ProviderConfig (clientId)
|
|
210
|
+
// Default redirectPort to 3333 to avoid conflict with main proxy port (default 3001)
|
|
211
|
+
const providerConfig = {
|
|
212
|
+
browser: this.config.browser,
|
|
213
|
+
redirectPort: this.config.browserAuthPort || 3333,
|
|
214
|
+
...(authConfig
|
|
215
|
+
? {
|
|
216
|
+
uaaUrl: authConfig.uaaUrl,
|
|
217
|
+
clientId: authConfig.uaaClientId,
|
|
218
|
+
clientSecret: authConfig.uaaClientSecret,
|
|
219
|
+
}
|
|
220
|
+
: {}),
|
|
221
|
+
};
|
|
222
|
+
const tokenProvider = new auth_providers_1.AuthorizationCodeProvider(providerConfig);
|
|
223
|
+
broker = new auth_broker_1.AuthBroker({
|
|
224
|
+
serviceKeyStore,
|
|
225
|
+
sessionStore,
|
|
226
|
+
tokenProvider,
|
|
227
|
+
}, this.config.browser, // Pass configured browser (default: 'system')
|
|
228
|
+
logger_js_1.logger);
|
|
229
|
+
this.btpAuthBrokers.set(destination, broker);
|
|
230
|
+
return this.ensureSessionServiceUrl(broker, destination, targetUrl);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Helper to ensure valid serviceUrl in session if override provided
|
|
234
|
+
*/
|
|
235
|
+
async ensureSessionServiceUrl(broker, destination, targetUrl) {
|
|
236
|
+
const activeTargetUrl = targetUrl || this.config.targetUrl;
|
|
237
|
+
if (!activeTargetUrl) {
|
|
238
|
+
return broker;
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
// Check if current connection config exists
|
|
242
|
+
const currentConn = await broker.getConnectionConfig(destination);
|
|
243
|
+
// We need to ensure we have a valid session with BOTH serviceUrl AND auth config.
|
|
244
|
+
// Even if serviceUrl matches, the auth config might be missing from the session
|
|
245
|
+
// (which causes ClientCredentialsProvider to fail if initialized with empty config).
|
|
246
|
+
// Cast to any to access potentially private methods if interface restricted
|
|
247
|
+
// biome-ignore lint/suspicious/noExplicitAny: Accessing internal methods for safe injection
|
|
248
|
+
const brokerAny = broker;
|
|
249
|
+
let authConfig = await broker.getAuthorizationConfig(destination);
|
|
250
|
+
if (!authConfig) {
|
|
251
|
+
try {
|
|
252
|
+
if (typeof brokerAny.getAuthorizationConfigFromServiceKey === 'function') {
|
|
253
|
+
authConfig =
|
|
254
|
+
await brokerAny.getAuthorizationConfigFromServiceKey(destination);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch (e) {
|
|
258
|
+
logger_js_1.logger?.debug('Could not find auth config for session update', {
|
|
259
|
+
error: String(e),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (!authConfig) {
|
|
264
|
+
authConfig = {
|
|
265
|
+
uaaUrl: 'https://placeholder.authentication.sap.hana.ondemand.com',
|
|
266
|
+
uaaClientId: 'placeholder',
|
|
267
|
+
uaaClientSecret: 'placeholder',
|
|
268
|
+
};
|
|
269
|
+
logger_js_1.logger?.info('Using placeholder auth config for session injection', {
|
|
270
|
+
type: 'BTP_SESSION_PLACEHOLDER',
|
|
271
|
+
destination,
|
|
272
|
+
targetUrl: activeTargetUrl,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
if (authConfig) {
|
|
276
|
+
const newConn = {
|
|
277
|
+
...(currentConn || {}),
|
|
278
|
+
serviceUrl: activeTargetUrl,
|
|
279
|
+
authType: 'jwt',
|
|
280
|
+
// Map XSUAA keys to ClientCredentialsProvider keys
|
|
281
|
+
clientId: authConfig.uaaClientId,
|
|
282
|
+
clientSecret: authConfig.uaaClientSecret,
|
|
283
|
+
uaaUrl: authConfig.uaaUrl,
|
|
284
|
+
};
|
|
285
|
+
if (typeof brokerAny.saveTokenToSession === 'function') {
|
|
286
|
+
await brokerAny.saveTokenToSession(destination, newConn, authConfig);
|
|
287
|
+
logger_js_1.logger?.info('Injected targetUrl and auth config into BTP session', {
|
|
288
|
+
type: 'BTP_SESSION_INJECTION',
|
|
289
|
+
destination,
|
|
290
|
+
url: activeTargetUrl,
|
|
291
|
+
hasClientId: !!newConn.clientId,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
logger_js_1.logger?.error('saveTokenToSession is not a function on broker', {
|
|
296
|
+
type: 'BTP_SESSION_METHOD_MISSING',
|
|
297
|
+
keys: Object.keys(brokerAny),
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
logger_js_1.logger?.error('Failed to inject targetUrl into session', {
|
|
304
|
+
type: 'BTP_SESSION_INJECTION_ERROR',
|
|
305
|
+
destination,
|
|
306
|
+
error: error instanceof Error ? error.message : String(error),
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
return broker;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Get JWT token for BTP destination from auth-broker with retry and token refresh
|
|
313
|
+
* @param destination Destination name
|
|
314
|
+
* @param forceRefresh Force token refresh
|
|
315
|
+
*/
|
|
316
|
+
async getJwtToken(destination, forceRefresh = false) {
|
|
317
|
+
logger_js_1.logger?.info('Getting JWT token', {
|
|
318
|
+
type: 'JWT_TOKEN_GET_START',
|
|
319
|
+
destination,
|
|
320
|
+
});
|
|
321
|
+
const authBroker = await this.getOrCreateBtpAuthBroker(destination);
|
|
322
|
+
// Check cache first (unless force refresh)
|
|
323
|
+
if (!forceRefresh) {
|
|
324
|
+
const cached = this.tokenCache.get(destination);
|
|
325
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
326
|
+
logger_js_1.logger?.debug('Using cached JWT token', {
|
|
327
|
+
type: 'JWT_TOKEN_CACHE_HIT',
|
|
328
|
+
destination,
|
|
329
|
+
});
|
|
330
|
+
return cached.token;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
// Retry logic for token retrieval
|
|
334
|
+
const retryOptions = {
|
|
335
|
+
maxRetries: this.config.maxRetries || 3,
|
|
336
|
+
retryDelay: this.config.retryDelay || 1000,
|
|
337
|
+
retryableStatusCodes: [500, 502, 503, 504],
|
|
338
|
+
};
|
|
339
|
+
try {
|
|
340
|
+
const token = await (0, errorHandler_js_1.retryWithBackoff)(async () => {
|
|
341
|
+
// Clear cache if force refresh
|
|
342
|
+
if (forceRefresh) {
|
|
343
|
+
this.tokenCache.delete(destination);
|
|
344
|
+
}
|
|
345
|
+
logger_js_1.logger?.debug('Getting JWT token from auth-broker', {
|
|
346
|
+
type: 'JWT_TOKEN_REQUEST_START',
|
|
347
|
+
destination,
|
|
348
|
+
forceRefresh,
|
|
349
|
+
});
|
|
350
|
+
// Get token from auth-broker
|
|
351
|
+
const token = await authBroker.getToken(destination);
|
|
352
|
+
// Cache token (assume it's valid for 30 minutes)
|
|
353
|
+
this.tokenCache.set(destination, {
|
|
354
|
+
token,
|
|
355
|
+
expiresAt: Date.now() + this.TOKEN_CACHE_TTL,
|
|
356
|
+
});
|
|
357
|
+
logger_js_1.logger?.debug('Retrieved JWT token from auth-broker', {
|
|
358
|
+
type: 'JWT_TOKEN_RETRIEVED',
|
|
359
|
+
destination,
|
|
360
|
+
forceRefresh,
|
|
361
|
+
});
|
|
362
|
+
return token;
|
|
363
|
+
}, retryOptions);
|
|
364
|
+
return token;
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
let errorMessage = error instanceof Error ? error.message : String(error);
|
|
368
|
+
// Rewrite error message to remove .env file references (proxy only uses service keys)
|
|
369
|
+
if (errorMessage.includes('.env') || errorMessage.includes('mcp.env')) {
|
|
370
|
+
const searchedInMatch = errorMessage.match(/Searched in:\s*([\s\S]*?)(?:\n|$)/);
|
|
371
|
+
const searchedPaths = searchedInMatch
|
|
372
|
+
? searchedInMatch[1]
|
|
373
|
+
.trim()
|
|
374
|
+
.split('\n')
|
|
375
|
+
.map((p) => p.trim().replace(/^-\s*/, ''))
|
|
376
|
+
.filter((p) => p)
|
|
377
|
+
: [];
|
|
378
|
+
errorMessage =
|
|
379
|
+
`Service key file not found for destination "${destination}".\n` +
|
|
380
|
+
`Please create service key file: ${destination}.json\n`;
|
|
381
|
+
if (searchedPaths.length > 0) {
|
|
382
|
+
errorMessage += `Searched in:\n`;
|
|
383
|
+
searchedPaths.forEach((path) => {
|
|
384
|
+
errorMessage += ` - ${path}\n`;
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
const isWindows = process.platform === 'win32';
|
|
389
|
+
const homeDir = require('node:os').homedir();
|
|
390
|
+
const defaultPath = isWindows
|
|
391
|
+
? require('node:path').join(homeDir, 'Documents', 'mcp-abap-adt', 'service-keys')
|
|
392
|
+
: require('node:path').join(homeDir, '.config', 'mcp-abap-adt', 'service-keys');
|
|
393
|
+
errorMessage += `Searched in:\n - ${defaultPath}\n`;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
logger_js_1.logger?.error('Failed to get JWT token from auth-broker after retries', {
|
|
397
|
+
type: 'JWT_TOKEN_ERROR',
|
|
398
|
+
destination,
|
|
399
|
+
error: errorMessage,
|
|
400
|
+
});
|
|
401
|
+
if (shouldWriteStderr()) {
|
|
402
|
+
process.stderr.write(`[MCP Proxy] ✗ ${errorMessage}`);
|
|
403
|
+
}
|
|
404
|
+
throw new Error(errorMessage);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Helper function to extract string value from header (handles arrays)
|
|
409
|
+
*/
|
|
410
|
+
getHeaderValue(headerValue) {
|
|
411
|
+
if (!headerValue)
|
|
412
|
+
return undefined;
|
|
413
|
+
if (Array.isArray(headerValue)) {
|
|
414
|
+
return headerValue[0]?.trim();
|
|
415
|
+
}
|
|
416
|
+
return typeof headerValue === 'string' ? headerValue.trim() : undefined;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Build proxy request with JWT token (BTP authentication only)
|
|
420
|
+
*
|
|
421
|
+
* Process flow:
|
|
422
|
+
*
|
|
423
|
+
* 1. BTP Authentication (XSUAA):
|
|
424
|
+
* 1.1 If x-btp-destination header exists:
|
|
425
|
+
* - Check map for broker with key = destination, get or create, save to map
|
|
426
|
+
* - Get token from xsuaa broker
|
|
427
|
+
* - Add/replace Authorization: Bearer <token> header
|
|
428
|
+
* 1.2 If header doesn't exist but --btp parameter exists:
|
|
429
|
+
* - Use destination from parameter, get or create broker, save to map
|
|
430
|
+
* - Get token from xsuaa broker
|
|
431
|
+
* - Add/replace Authorization: Bearer <token> header
|
|
432
|
+
* 1.3 If neither header nor parameter:
|
|
433
|
+
* - Do nothing, pass request further
|
|
434
|
+
*/
|
|
435
|
+
async buildProxyRequest(originalRequest, routingDecision, originalHeaders, forceTokenRefresh = false) {
|
|
436
|
+
logger_js_1.logger?.info('=== BUILD PROXY REQUEST - ORIGINAL ===', {
|
|
437
|
+
type: 'BUILD_PROXY_REQUEST_ORIGINAL',
|
|
438
|
+
originalRequestId: originalRequest.id,
|
|
439
|
+
originalRequestIdType: typeof originalRequest.id,
|
|
440
|
+
originalRequestMethod: originalRequest.method,
|
|
441
|
+
fullOriginalRequest: JSON.stringify(originalRequest),
|
|
442
|
+
});
|
|
443
|
+
// Build headers for target MCP server
|
|
444
|
+
const proxyHeaders = {
|
|
445
|
+
[interfaces_1.HEADER_CONTENT_TYPE]: 'application/json',
|
|
446
|
+
[interfaces_1.HEADER_ACCEPT]: 'application/json, application/x-ndjson, text/event-stream',
|
|
447
|
+
};
|
|
448
|
+
// ============================================
|
|
449
|
+
// BTP Authentication (XSUAA)
|
|
450
|
+
// ============================================
|
|
451
|
+
const btpDestinationHeader = this.getHeaderValue(originalHeaders[interfaces_1.HEADER_BTP_DESTINATION]);
|
|
452
|
+
const btpDestinationFromParam = routingDecision.btpDestination && !btpDestinationHeader
|
|
453
|
+
? routingDecision.btpDestination
|
|
454
|
+
: undefined;
|
|
455
|
+
let btpDestination;
|
|
456
|
+
if (btpDestinationHeader) {
|
|
457
|
+
btpDestination = btpDestinationHeader;
|
|
458
|
+
logger_js_1.logger?.debug('Using x-btp-destination from header', {
|
|
459
|
+
type: 'BTP_DESTINATION_FROM_HEADER',
|
|
460
|
+
destination: btpDestination,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
else if (btpDestinationFromParam) {
|
|
464
|
+
btpDestination = btpDestinationFromParam;
|
|
465
|
+
logger_js_1.logger?.debug('Using x-btp-destination from parameter', {
|
|
466
|
+
type: 'BTP_DESTINATION_FROM_PARAM',
|
|
467
|
+
destination: btpDestination,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
if (btpDestination) {
|
|
471
|
+
const _btpBroker = await this.getOrCreateBtpAuthBroker(btpDestination);
|
|
472
|
+
const existingAuth = originalHeaders[interfaces_1.HEADER_AUTHORIZATION.toLowerCase()] ||
|
|
473
|
+
originalHeaders[interfaces_1.HEADER_AUTHORIZATION];
|
|
474
|
+
const hasExistingAuth = !!existingAuth;
|
|
475
|
+
const authToken = await this.getJwtToken(btpDestination, forceTokenRefresh);
|
|
476
|
+
proxyHeaders[interfaces_1.HEADER_AUTHORIZATION] = `Bearer ${authToken}`;
|
|
477
|
+
logger_js_1.logger?.debug(hasExistingAuth
|
|
478
|
+
? 'Replaced existing Authorization header with BTP token'
|
|
479
|
+
: 'Added BTP Cloud authorization token', {
|
|
480
|
+
type: hasExistingAuth
|
|
481
|
+
? 'BTP_AUTH_TOKEN_REPLACED'
|
|
482
|
+
: 'BTP_AUTH_TOKEN_ADDED',
|
|
483
|
+
destination: btpDestination,
|
|
484
|
+
hadExistingAuth: hasExistingAuth,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
logger_js_1.logger?.debug('No BTP destination - skipping authentication', {
|
|
489
|
+
type: 'BTP_AUTH_SKIPPED',
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
// Preserve other SAP headers if provided by client
|
|
493
|
+
const sapHeaders = [interfaces_1.HEADER_SAP_CLIENT, interfaces_1.HEADER_SAP_DESTINATION_SERVICE];
|
|
494
|
+
for (const headerName of sapHeaders) {
|
|
495
|
+
const value = originalHeaders[headerName];
|
|
496
|
+
if (value) {
|
|
497
|
+
proxyHeaders[headerName] = Array.isArray(value) ? value[0] : value;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
// Get MCP server URL from BTP destination service key
|
|
501
|
+
let baseUrl;
|
|
502
|
+
if (btpDestination) {
|
|
503
|
+
try {
|
|
504
|
+
const btpBroker = await this.getOrCreateBtpAuthBroker(btpDestination, routingDecision.targetUrl);
|
|
505
|
+
const connConfig = await btpBroker.getConnectionConfig(btpDestination);
|
|
506
|
+
// Use service key URL as base (if available)
|
|
507
|
+
if (connConfig?.serviceUrl) {
|
|
508
|
+
baseUrl = connConfig.serviceUrl;
|
|
509
|
+
logger_js_1.logger?.debug('Using MCP URL from BTP destination service key', {
|
|
510
|
+
type: 'MCP_URL_FROM_BTP_DESTINATION',
|
|
511
|
+
destination: btpDestination,
|
|
512
|
+
url: baseUrl,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
logger_js_1.logger?.warn('BTP destination service key does not contain service URL', {
|
|
517
|
+
type: 'BTP_DESTINATION_NO_URL',
|
|
518
|
+
destination: btpDestination,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
catch (error) {
|
|
523
|
+
logger_js_1.logger?.warn('Failed to get URL from BTP destination service key', {
|
|
524
|
+
type: 'BTP_DESTINATION_URL_ERROR',
|
|
525
|
+
destination: btpDestination,
|
|
526
|
+
error: error instanceof Error ? error.message : String(error),
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
// Apply Target URL override logic
|
|
531
|
+
if (routingDecision.targetUrl) {
|
|
532
|
+
if (baseUrl) {
|
|
533
|
+
logger_js_1.logger?.debug('Overriding service key URL with explicit target URL', {
|
|
534
|
+
type: 'TARGET_URL_OVERRIDE',
|
|
535
|
+
url: routingDecision.targetUrl,
|
|
536
|
+
serviceKeyUrl: baseUrl,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
logger_js_1.logger?.debug('Using explicit target URL from configuration', {
|
|
541
|
+
type: 'TARGET_URL_FROM_CONFIG',
|
|
542
|
+
url: routingDecision.targetUrl,
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
baseUrl = routingDecision.targetUrl;
|
|
546
|
+
}
|
|
547
|
+
if (!baseUrl) {
|
|
548
|
+
throw new Error('Cannot determine target URL: provide btpDestination with service key containing URL OR use --target-url');
|
|
549
|
+
}
|
|
550
|
+
// Construct full MCP endpoint URL
|
|
551
|
+
let fullUrl;
|
|
552
|
+
// Determine the path to append
|
|
553
|
+
// If original request has a URL/path, use it. Otherwise default to nothing/root.
|
|
554
|
+
const requestPath = originalRequest.url || '';
|
|
555
|
+
// Check if we should use URL as-is
|
|
556
|
+
// 1. If it already looks like an MCP URL
|
|
557
|
+
// 2. OR if it was explicitly provided via targetUrl (user knows best)
|
|
558
|
+
const isExplicitTarget = !!routingDecision.targetUrl;
|
|
559
|
+
const hasMcpPath = baseUrl.includes('/mcp/') ||
|
|
560
|
+
baseUrl.endsWith('/mcp') ||
|
|
561
|
+
baseUrl.includes('/mcp/stream/');
|
|
562
|
+
if (isExplicitTarget) {
|
|
563
|
+
// Explicit Target URL logic (as per User request):
|
|
564
|
+
// "whatever endpoint [proxy] received, it adds to the target URL"
|
|
565
|
+
// Use join logic to avoid double slashes and ensure correctness
|
|
566
|
+
// Remove trailing slash from base
|
|
567
|
+
const base = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
|
568
|
+
// Ensure path starts with slash if not empty
|
|
569
|
+
const path = requestPath.startsWith('/')
|
|
570
|
+
? requestPath
|
|
571
|
+
: `/${requestPath}`;
|
|
572
|
+
// If requestPath is empty or just '/', ensure we don't end up with empty path if base is root
|
|
573
|
+
// But typically we just append.
|
|
574
|
+
fullUrl = `${base}${path}`;
|
|
575
|
+
logger_js_1.logger?.debug('Using explicit target URL with forwarded path', {
|
|
576
|
+
type: 'TARGET_URL_PATH_FORWARDING',
|
|
577
|
+
base,
|
|
578
|
+
path,
|
|
579
|
+
final: fullUrl,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
else if (hasMcpPath) {
|
|
583
|
+
// Use as-is (strip trailing slash if present, though typically not needed if strictly as-is, but good practice for consistency)
|
|
584
|
+
fullUrl = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
|
|
585
|
+
logger_js_1.logger?.debug('Using target URL as-is', {
|
|
586
|
+
type: 'MCP_URL_AS_IS',
|
|
587
|
+
original: baseUrl,
|
|
588
|
+
final: fullUrl,
|
|
589
|
+
reason: 'path detection',
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
else {
|
|
593
|
+
// Default: append MCP path
|
|
594
|
+
const mcpPath = '/mcp/stream/http';
|
|
595
|
+
fullUrl = baseUrl.endsWith('/')
|
|
596
|
+
? `${baseUrl.slice(0, -1)}${mcpPath}`
|
|
597
|
+
: `${baseUrl}${mcpPath}`;
|
|
598
|
+
logger_js_1.logger?.debug('Appended MCP path to base URL', {
|
|
599
|
+
type: 'MCP_URL_APPENDED',
|
|
600
|
+
original: baseUrl,
|
|
601
|
+
final: fullUrl,
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
const builtHeaders = {};
|
|
605
|
+
for (const [key, value] of Object.entries(proxyHeaders)) {
|
|
606
|
+
const lowerKey = key.toLowerCase();
|
|
607
|
+
if (lowerKey.includes('token') ||
|
|
608
|
+
lowerKey.includes('authorization') ||
|
|
609
|
+
lowerKey.includes('password') ||
|
|
610
|
+
lowerKey.includes('secret')) {
|
|
611
|
+
builtHeaders[key] = value
|
|
612
|
+
? `${String(value).substring(0, 20)}...`
|
|
613
|
+
: '[REDACTED]';
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
builtHeaders[key] = String(value || '');
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
const sanitizedRequestParams = {};
|
|
620
|
+
const requestData = originalRequest.data;
|
|
621
|
+
if (requestData &&
|
|
622
|
+
requestData.params &&
|
|
623
|
+
typeof requestData.params === 'object' &&
|
|
624
|
+
requestData.params !== null) {
|
|
625
|
+
const params = requestData.params;
|
|
626
|
+
// ... (existing sanitization logic) ...
|
|
627
|
+
if (params.arguments &&
|
|
628
|
+
typeof params.arguments === 'object' &&
|
|
629
|
+
params.arguments !== null) {
|
|
630
|
+
sanitizedRequestParams.arguments = {};
|
|
631
|
+
const sanitizedArgs = sanitizedRequestParams.arguments;
|
|
632
|
+
for (const [key, value] of Object.entries(params.arguments)) {
|
|
633
|
+
const lowerKey = key.toLowerCase();
|
|
634
|
+
if (lowerKey.includes('password') ||
|
|
635
|
+
lowerKey.includes('token') ||
|
|
636
|
+
lowerKey.includes('secret')) {
|
|
637
|
+
sanitizedArgs[key] = '[REDACTED]';
|
|
638
|
+
}
|
|
639
|
+
else {
|
|
640
|
+
sanitizedArgs[key] = value;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
for (const [key, value] of Object.entries(params)) {
|
|
645
|
+
if (key === 'arguments')
|
|
646
|
+
continue;
|
|
647
|
+
sanitizedRequestParams[key] = value;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
logger_js_1.logger?.info('=== BUILDING PROXY REQUEST ===', {
|
|
651
|
+
type: 'PROXY_REQUEST_BUILT',
|
|
652
|
+
btpDestination: btpDestination,
|
|
653
|
+
url: fullUrl,
|
|
654
|
+
headers: builtHeaders,
|
|
655
|
+
baseUrl,
|
|
656
|
+
fullUrl,
|
|
657
|
+
hasAuthToken: !!proxyHeaders[interfaces_1.HEADER_AUTHORIZATION],
|
|
658
|
+
btpSource: btpDestinationHeader
|
|
659
|
+
? 'header'
|
|
660
|
+
: btpDestinationFromParam
|
|
661
|
+
? 'parameter'
|
|
662
|
+
: 'none',
|
|
663
|
+
requestMethod: originalRequest.method,
|
|
664
|
+
requestId: originalRequest.id,
|
|
665
|
+
requestParams: sanitizedRequestParams,
|
|
666
|
+
});
|
|
667
|
+
// Return axios config with full URL
|
|
668
|
+
// Use original request method
|
|
669
|
+
const axiosConfig = {
|
|
670
|
+
method: originalRequest.method,
|
|
671
|
+
url: fullUrl,
|
|
672
|
+
headers: proxyHeaders,
|
|
673
|
+
data: originalRequest.data, // Send the original data (body)
|
|
674
|
+
};
|
|
675
|
+
logger_js_1.logger?.debug('Axios request config', {
|
|
676
|
+
type: 'AXIOS_REQUEST_CONFIG',
|
|
677
|
+
method: axiosConfig.method,
|
|
678
|
+
url: axiosConfig.url,
|
|
679
|
+
headers: Object.keys(axiosConfig.headers),
|
|
680
|
+
dataKeys: originalRequest ? Object.keys(originalRequest) : [],
|
|
681
|
+
});
|
|
682
|
+
return axiosConfig;
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Proxy MCP request to target server with retry, circuit breaker, and error handling
|
|
686
|
+
*/
|
|
687
|
+
async proxyRequest(originalRequest, routingDecision, originalHeaders) {
|
|
688
|
+
// Check circuit breaker
|
|
689
|
+
if (!this.circuitBreaker.canProceed()) {
|
|
690
|
+
logger_js_1.logger?.warn('Circuit breaker is open, rejecting request', {
|
|
691
|
+
type: 'CIRCUIT_BREAKER_REJECTED',
|
|
692
|
+
btpDestination: routingDecision.btpDestination,
|
|
693
|
+
});
|
|
694
|
+
return (0, errorHandler_js_1.createErrorResponse)(originalRequest.id || null, -32001, 'Service temporarily unavailable (circuit breaker open)', { circuitBreakerState: this.circuitBreaker.getState() });
|
|
695
|
+
}
|
|
696
|
+
const retryOptions = {
|
|
697
|
+
maxRetries: this.config.maxRetries || 3,
|
|
698
|
+
retryDelay: this.config.retryDelay || 1000,
|
|
699
|
+
retryableStatusCodes: [500, 502, 503, 504],
|
|
700
|
+
};
|
|
701
|
+
try {
|
|
702
|
+
const response = await (0, errorHandler_js_1.retryWithBackoff)(async () => {
|
|
703
|
+
const forceTokenRefresh = false;
|
|
704
|
+
const proxyConfig = await this.buildProxyRequest(originalRequest, routingDecision, originalHeaders, forceTokenRefresh);
|
|
705
|
+
// Log outgoing request details
|
|
706
|
+
const outgoingHeaders = {};
|
|
707
|
+
for (const [key, value] of Object.entries(proxyConfig.headers || {})) {
|
|
708
|
+
const lowerKey = key.toLowerCase();
|
|
709
|
+
if (lowerKey.includes('token') ||
|
|
710
|
+
lowerKey.includes('authorization') ||
|
|
711
|
+
lowerKey.includes('password') ||
|
|
712
|
+
lowerKey.includes('secret')) {
|
|
713
|
+
outgoingHeaders[key] = value
|
|
714
|
+
? `${String(value).substring(0, 20)}...`
|
|
715
|
+
: '[REDACTED]';
|
|
716
|
+
}
|
|
717
|
+
else {
|
|
718
|
+
outgoingHeaders[key] = String(value || '');
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
const sanitizedOutgoingBody = {};
|
|
722
|
+
if (proxyConfig.data && typeof proxyConfig.data === 'object') {
|
|
723
|
+
if (proxyConfig.data.params &&
|
|
724
|
+
typeof proxyConfig.data.params === 'object' &&
|
|
725
|
+
proxyConfig.data.params !== null) {
|
|
726
|
+
const params = proxyConfig.data.params;
|
|
727
|
+
sanitizedOutgoingBody.params = {};
|
|
728
|
+
const sanitizedParams = sanitizedOutgoingBody.params;
|
|
729
|
+
if (params.arguments &&
|
|
730
|
+
typeof params.arguments === 'object' &&
|
|
731
|
+
params.arguments !== null) {
|
|
732
|
+
sanitizedParams.arguments = {};
|
|
733
|
+
const sanitizedArgs = sanitizedParams.arguments;
|
|
734
|
+
for (const [key, value] of Object.entries(params.arguments)) {
|
|
735
|
+
const lowerKey = key.toLowerCase();
|
|
736
|
+
if (lowerKey.includes('password') ||
|
|
737
|
+
lowerKey.includes('token') ||
|
|
738
|
+
lowerKey.includes('secret')) {
|
|
739
|
+
sanitizedArgs[key] = '[REDACTED]';
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
sanitizedArgs[key] = value;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
for (const [key, value] of Object.entries(params)) {
|
|
747
|
+
if (key === 'arguments')
|
|
748
|
+
continue;
|
|
749
|
+
sanitizedParams[key] = value;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
sanitizedOutgoingBody.method = proxyConfig.data.method;
|
|
753
|
+
sanitizedOutgoingBody.id = proxyConfig.data.id;
|
|
754
|
+
sanitizedOutgoingBody.jsonrpc = proxyConfig.data.jsonrpc;
|
|
755
|
+
}
|
|
756
|
+
logger_js_1.logger?.info('=== SENDING PROXY REQUEST ===', {
|
|
757
|
+
type: 'PROXY_REQUEST_SENDING',
|
|
758
|
+
url: proxyConfig.url,
|
|
759
|
+
headers: outgoingHeaders,
|
|
760
|
+
body: sanitizedOutgoingBody,
|
|
761
|
+
});
|
|
762
|
+
const response = await this.axiosInstance.request(proxyConfig);
|
|
763
|
+
this.circuitBreaker.recordSuccess();
|
|
764
|
+
return response;
|
|
765
|
+
}, retryOptions);
|
|
766
|
+
// Return response in JSON-RPC format
|
|
767
|
+
return {
|
|
768
|
+
jsonrpc: '2.0',
|
|
769
|
+
id: originalRequest.id || null,
|
|
770
|
+
result: response.data.result,
|
|
771
|
+
error: response.data.error,
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
catch (error) {
|
|
775
|
+
if ((0, errorHandler_js_1.isTokenExpirationError)(error)) {
|
|
776
|
+
logger_js_1.logger?.warn('Token expired usually handled by retry with refresh', {
|
|
777
|
+
type: 'TOKEN_EXPIRED_ERROR',
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
this.circuitBreaker.recordFailure();
|
|
781
|
+
let statusCode = 500;
|
|
782
|
+
let errorMessage = 'Internal Server Error';
|
|
783
|
+
let errorData;
|
|
784
|
+
if (axios_1.default.isAxiosError(error)) {
|
|
785
|
+
statusCode = error.response?.status || 500;
|
|
786
|
+
errorMessage = error.message;
|
|
787
|
+
errorData = error.response?.data;
|
|
788
|
+
logger_js_1.logger?.error('Axios error in proxy request', {
|
|
789
|
+
type: 'AXIOS_PROXY_ERROR',
|
|
790
|
+
status: statusCode,
|
|
791
|
+
message: errorMessage,
|
|
792
|
+
data: errorData,
|
|
793
|
+
url: error.config?.url,
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
else {
|
|
797
|
+
errorMessage = error instanceof Error ? error.message : String(error);
|
|
798
|
+
logger_js_1.logger?.error('Generic error in proxy request', {
|
|
799
|
+
type: 'GENERIC_PROXY_ERROR',
|
|
800
|
+
message: errorMessage,
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
return (0, errorHandler_js_1.createErrorResponse)(originalRequest.id || null, -32000, `Proxy error: ${errorMessage}`, errorData);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
/**
|
|
807
|
+
* Create a new BtpProxy instance
|
|
808
|
+
*/
|
|
809
|
+
static async create(config) {
|
|
810
|
+
const loadedConfig = config ? { ...(0, config_js_1.loadConfig)(), ...config } : (0, config_js_1.loadConfig)();
|
|
811
|
+
const unsafeIndex = loadedConfig.unsafe ?? false;
|
|
812
|
+
const { serviceKeyStore, sessionStore } = await (0, stores_js_1.getPlatformStores)(unsafeIndex);
|
|
813
|
+
const defaultBtpAuthBroker = new auth_broker_1.AuthBroker({
|
|
814
|
+
serviceKeyStore,
|
|
815
|
+
sessionStore,
|
|
816
|
+
tokenProvider: new auth_providers_1.AuthorizationCodeProvider({
|
|
817
|
+
uaaUrl: 'https://placeholder.authentication.sap.hana.ondemand.com',
|
|
818
|
+
clientId: 'placeholder',
|
|
819
|
+
clientSecret: 'placeholder',
|
|
820
|
+
browser: loadedConfig.browser,
|
|
821
|
+
redirectPort: loadedConfig.browserAuthPort,
|
|
822
|
+
}),
|
|
823
|
+
}, 'none', loggerAdapter);
|
|
824
|
+
return new BtpProxy(defaultBtpAuthBroker, loadedConfig);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
exports.BtpProxy = BtpProxy;
|
|
828
|
+
/**
|
|
829
|
+
* Factory function to create BtpProxy
|
|
830
|
+
*/
|
|
831
|
+
async function createBtpProxy(config) {
|
|
832
|
+
return BtpProxy.create(config);
|
|
833
|
+
}
|