@mcp-abap-adt/proxy 1.3.0 → 1.6.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 +29 -0
- package/README.md +23 -1
- package/bin/mcp-abap-adt-proxy.js +4 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +108 -10
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +28 -4
- package/dist/lib/envInterpolation.d.ts +28 -0
- package/dist/lib/envInterpolation.d.ts.map +1 -0
- package/dist/lib/envInterpolation.js +103 -0
- package/dist/proxy/btpProxy.d.ts.map +1 -1
- package/dist/proxy/btpProxy.js +10 -3
- package/package.json +4 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.6.0] - 2026-06-04
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Environment-variable interpolation in config + `.env` support.** Config string values (and `--header` values) may now contain `${VAR}` / `${VAR:-default}` placeholders, resolved from `process.env` and an explicitly-pointed `.env` file. This makes `defaultHeaders` the per-user auth-injection point for ABAP credentials (`x-sap-login` / `x-sap-password`) on shared servers like `cloud-llm-hub` — **without** storing secrets as plaintext in the YAML. (#4, #5)
|
|
14
|
+
- `.env` source is explicit only: `envFile: <path>` in the config (resolved relative to the config file's directory) or `--env-file <path>` on the command line (overrides `envFile`). There is no `cwd` auto-discovery.
|
|
15
|
+
- Resolution priority: `process.env` → `.env` → `${VAR:-default}`. `process.env` wins over the `.env` file.
|
|
16
|
+
- **Fail-fast:** a `${VAR}` without a default that cannot be resolved aborts startup with an error naming the variable and the field it came from, so a request never silently goes out with an empty credential.
|
|
17
|
+
- `Authorization` remains proxy-managed (destination JWT) and cannot be set via `defaultHeaders`.
|
|
18
|
+
|
|
19
|
+
## [1.5.0] - 2026-06-02
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- **Exit on incomplete authentication.** When authentication is requested but not completed — at startup (eager auth) or on a request that needs a token — the proxy now logs a categorised reason (timeout / invalid credentials / service key / network) and exits with code 1, so it can be restarted to retry (mirroring the one-shot `mcp-auth` command) instead of running in a permanently unauthenticated state. A downstream/forward error (after auth succeeds) is still returned to the client without exiting.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- Bump `@mcp-abap-adt/auth-broker` `^1.0.5` → `^1.0.6`: the broker no longer starts a redundant second browser login after an interactive session-login failure, so the real cause (e.g. login timeout) is reported instead of a misleading `Port <n> is already in use`.
|
|
26
|
+
|
|
27
|
+
## [1.4.0] - 2026-06-02
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
- **Manual paste login for `--browser none` / `headless`** (via `@mcp-abap-adt/auth-providers@1.1.0`). When the browser runs on a different machine than the proxy, login can be completed by opening the paste form at `http://<host>:<browser-auth-port>/` and pasting the `code` (or the whole redirected URL), or by pasting the code in the terminal — in addition to the existing automatic localhost callback.
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- **`--browser none` now prints the authorization URL.** The per-destination `AuthorizationCodeProvider` was created without a logger, so the "Open this URL" prompt from `browserAuth` was silently dropped and login could never be completed in headless mode. The proxy now passes its logger to the provider; `auth-providers@1.1.0` additionally falls back to `stderr` when no logger is supplied.
|
|
34
|
+
- **Platform-correct service-key path in `--help` and errors.** The "service key not found" hint and `--help` hard-coded the Unix `~/.config/...` path, which is wrong on Windows (`%USERPROFILE%\Documents\mcp-abap-adt\service-keys`). Both now show the real resolved directory and mention the `AUTH_BROKER_PATH` override.
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
- Bump `@mcp-abap-adt/auth-providers` `^1.0.5` → `^1.1.0`.
|
|
38
|
+
|
|
10
39
|
## [1.3.0] - 2026-05-28
|
|
11
40
|
|
|
12
41
|
### Added
|
package/README.md
CHANGED
|
@@ -98,7 +98,7 @@ For detailed setup instructions for Cline and GitHub Copilot, see the **[Client
|
|
|
98
98
|
|
|
99
99
|
MCP clients like Cline and Claude Code cannot set arbitrary request headers. Use default headers to inject SAP-specific headers (e.g. `x-sap-destination`, `x-sap-client`) that the target MCP server requires.
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
Precedence: client-supplied request headers always win over `defaultHeaders`. `Authorization` is the exception — it is always managed by the proxy (replaced with the destination JWT) and **cannot** be set via `defaultHeaders`.
|
|
102
102
|
|
|
103
103
|
Via YAML config (`defaultHeaders` map):
|
|
104
104
|
```yaml
|
|
@@ -109,6 +109,28 @@ defaultHeaders:
|
|
|
109
109
|
x-sap-client: "100"
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
+
**Per-user ABAP credentials.** `defaultHeaders` is the supported place to supply your own SAP login/password for on-premise / `NoAuthentication` destinations. The upstream `cloud-llm-hub` is a **shared** server with no default service user, so it expects each caller's own `x-sap-login` / `x-sap-password` on every request. Because the proxy runs **locally on each user's machine**, it carries *your* identity.
|
|
113
|
+
|
|
114
|
+
Do not hardcode secrets. Reference environment variables with `${VAR}` (or `${VAR:-default}`); values are resolved from `process.env` or an explicitly-pointed `.env` file:
|
|
115
|
+
|
|
116
|
+
```yaml
|
|
117
|
+
btpDestination: mcp
|
|
118
|
+
envFile: secrets.env # resolved relative to this config file's dir
|
|
119
|
+
defaultHeaders:
|
|
120
|
+
x-sap-destination: S4HANA_E19
|
|
121
|
+
x-sap-login: ${SAP_USER}
|
|
122
|
+
x-sap-password: ${SAP_PASSWORD}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`secrets.env` (user-local, `chmod 600`, never committed):
|
|
126
|
+
|
|
127
|
+
```dotenv
|
|
128
|
+
SAP_USER=MY_SAP_USER
|
|
129
|
+
SAP_PASSWORD=my-sap-password
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Resolution order is `process.env` → `.env` → `${VAR:-default}`. `process.env` wins; an unresolved `${VAR}` without a default fails the proxy at startup. Override the `.env` path at launch with `--env-file <path>`. This closes both auth layers from one local config: the **service layer** (`Authorization: Bearer <JWT>`, from the destination service key) and the **ABAP layer** (`x-sap-login` / `x-sap-password`).
|
|
133
|
+
|
|
112
134
|
Via CLI (`--header`, repeatable):
|
|
113
135
|
```bash
|
|
114
136
|
mcp-abap-adt-proxy --btp=mcp --url=https://example.com \
|
|
@@ -56,9 +56,13 @@ Options:
|
|
|
56
56
|
Useful for direct OData service testing or non-standard MCP URLs
|
|
57
57
|
--config=<file>, -c Load configuration from YAML or JSON file
|
|
58
58
|
Alternative to command-line parameters
|
|
59
|
+
--env-file=<path> Load a .env file for ${VAR} interpolation in config/headers
|
|
60
|
+
Overrides the config's "envFile" field
|
|
59
61
|
--unsafe Enable file-based session storage (persists tokens to disk)
|
|
60
62
|
--header key=value Add default header to every forwarded request (repeatable)
|
|
63
|
+
Values support ${VAR} / ${VAR:-default} interpolation
|
|
61
64
|
Example: --header x-sap-destination=S4HANA
|
|
65
|
+
Example: --header x-sap-password=${SAP_PASSWORD}
|
|
62
66
|
--help, -h Show this help message
|
|
63
67
|
--version, -v Show version number
|
|
64
68
|
|
package/dist/index.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export declare class McpAbapAdtProxyServer {
|
|
|
17
17
|
private config;
|
|
18
18
|
private httpServer?;
|
|
19
19
|
private btpProxy?;
|
|
20
|
+
private authExiting;
|
|
20
21
|
constructor(transportConfig?: TransportConfig, configPath?: string);
|
|
21
22
|
/**
|
|
22
23
|
* Setup MCP handlers
|
|
@@ -38,6 +39,13 @@ export declare class McpAbapAdtProxyServer {
|
|
|
38
39
|
* Start SSE server
|
|
39
40
|
*/
|
|
40
41
|
private startSseServer;
|
|
42
|
+
/**
|
|
43
|
+
* Authentication was requested but could not be completed. Report the reason
|
|
44
|
+
* clearly, release resources, and exit(1) so the proxy is restarted (and the
|
|
45
|
+
* user gets a fresh login prompt) instead of running unauthenticated. Mirrors
|
|
46
|
+
* the one-shot `mcp-auth` command, which exits on a failed/incomplete login.
|
|
47
|
+
*/
|
|
48
|
+
private fatalAuthFailure;
|
|
41
49
|
/**
|
|
42
50
|
* Shutdown server
|
|
43
51
|
*/
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAwBH,OAAO,EAEL,KAAK,eAAe,EAErB,MAAM,0BAA0B,CAAC;AAgElC;;GAEG;AACH,qBAAa,qBAAqB;IAChC,OAAO,CAAC,MAAM,CAAY;IAC1B,OAAO,CAAC,eAAe,CAAkB;IACzC,OAAO,CAAC,MAAM,CAAgC;IAC9C,OAAO,CAAC,UAAU,CAAC,CAAa;IAChC,OAAO,CAAC,QAAQ,CAAC,CAAW;IAC5B,OAAO,CAAC,WAAW,CAAS;gBAEhB,eAAe,CAAC,EAAE,eAAe,EAAE,UAAU,CAAC,EAAE,MAAM;IAoDlE;;OAEG;IACH,OAAO,CAAC,aAAa;IAMrB;;OAEG;IACG,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC;IA2E1B;;OAEG;YACW,eAAe;IAgG7B;;OAEG;YACW,kBAAkB;IAgEhC;;OAEG;YACW,cAAc;IA6T5B;;;;;OAKG;YACW,gBAAgB;IAiC9B;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAoBhC;AAGD,eAAe,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
|
18
18
|
const reverseProxy_js_1 = require("./proxy/reverseProxy.js");
|
|
19
19
|
const config_js_1 = require("./lib/config.js");
|
|
20
20
|
const logger_js_1 = require("./lib/logger.js");
|
|
21
|
+
const stores_js_1 = require("./lib/stores.js");
|
|
21
22
|
const transportConfig_js_1 = require("./lib/transportConfig.js");
|
|
22
23
|
const btpProxy_js_1 = require("./proxy/btpProxy.js");
|
|
23
24
|
const headerAnalyzer_js_1 = require("./router/headerAnalyzer.js");
|
|
@@ -31,6 +32,38 @@ function getBodyId(body) {
|
|
|
31
32
|
}
|
|
32
33
|
return null;
|
|
33
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Classify an authentication failure into a human-readable reason so the proxy
|
|
37
|
+
* can report *why* it is exiting (timeout vs bad credentials vs network ...).
|
|
38
|
+
*/
|
|
39
|
+
function classifyAuthFailure(error) {
|
|
40
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
41
|
+
if (/timeout/i.test(msg)) {
|
|
42
|
+
return {
|
|
43
|
+
category: 'timeout',
|
|
44
|
+
reason: 'the login URL was not completed in time',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (/service key|missing required fields/i.test(msg)) {
|
|
48
|
+
return {
|
|
49
|
+
category: 'service-key',
|
|
50
|
+
reason: 'service key missing or incomplete',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (/invalid_client|unauthorized|\b401\b|invalid credentials/i.test(msg)) {
|
|
54
|
+
return {
|
|
55
|
+
category: 'credentials',
|
|
56
|
+
reason: 'invalid credentials — UAA rejected the client',
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
if (/ENOTFOUND|ECONNREFUSED|ETIMEDOUT|network|ENETUNREACH/i.test(msg)) {
|
|
60
|
+
return {
|
|
61
|
+
category: 'network',
|
|
62
|
+
reason: 'network error reaching the authentication server',
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return { category: 'other', reason: msg };
|
|
66
|
+
}
|
|
34
67
|
/**
|
|
35
68
|
* MCP ABAP ADT Proxy Server
|
|
36
69
|
*/
|
|
@@ -40,6 +73,7 @@ class McpAbapAdtProxyServer {
|
|
|
40
73
|
config;
|
|
41
74
|
httpServer;
|
|
42
75
|
btpProxy;
|
|
76
|
+
authExiting = false;
|
|
43
77
|
constructor(transportConfig, configPath) {
|
|
44
78
|
// Load config first to extract transport overrides from YAML/JSON file
|
|
45
79
|
const resolvedConfigPath = configPath || (0, config_js_1.getConfigPath)();
|
|
@@ -125,20 +159,24 @@ Authorization source: BTP destination "${this.config.btpDestination}"`;
|
|
|
125
159
|
});
|
|
126
160
|
return;
|
|
127
161
|
}
|
|
128
|
-
// Eager initialization of BTP authentication
|
|
162
|
+
// Eager initialization of BTP authentication. Kept in the background so the
|
|
163
|
+
// server starts listening immediately (and, for --browser none, the auth URL
|
|
164
|
+
// is printed right away). If auth is requested but never completed, exit(1)
|
|
165
|
+
// so the proxy is restarted and the user gets a fresh login prompt.
|
|
129
166
|
if (this.config.btpDestination) {
|
|
167
|
+
const dest = this.config.btpDestination;
|
|
130
168
|
try {
|
|
131
169
|
if (!this.btpProxy) {
|
|
132
170
|
this.btpProxy = await (0, btpProxy_js_1.createBtpProxy)(this.config);
|
|
133
171
|
}
|
|
134
|
-
// Trigger initialization (background) - this will open the browser
|
|
135
|
-
this.btpProxy.initialize(this.config.btpDestination);
|
|
136
172
|
}
|
|
137
173
|
catch (error) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
});
|
|
174
|
+
await this.fatalAuthFailure(error, dest, 'startup');
|
|
175
|
+
return;
|
|
141
176
|
}
|
|
177
|
+
this.btpProxy
|
|
178
|
+
.initialize(dest)
|
|
179
|
+
.catch((error) => this.fatalAuthFailure(error, dest, 'startup'));
|
|
142
180
|
}
|
|
143
181
|
if (this.transportConfig.type === 'streamable-http') {
|
|
144
182
|
await this.startHttpServer();
|
|
@@ -246,9 +284,28 @@ Authorization source: ${authSourceObj}`;
|
|
|
246
284
|
if (!this.btpProxy) {
|
|
247
285
|
this.btpProxy = await (0, btpProxy_js_1.createBtpProxy)(this.config);
|
|
248
286
|
}
|
|
287
|
+
// Authentication is separated from forwarding: an auth failure is fatal
|
|
288
|
+
// (the proxy exits so it can be restarted), while a downstream/forward error
|
|
289
|
+
// is returned to the client without killing the proxy.
|
|
290
|
+
let jwtToken;
|
|
249
291
|
try {
|
|
250
292
|
// Get JWT token (cached, auto-refresh)
|
|
251
|
-
|
|
293
|
+
jwtToken = await this.btpProxy.getJwtToken(destination);
|
|
294
|
+
}
|
|
295
|
+
catch (authError) {
|
|
296
|
+
logger_js_1.logger?.error('Proxy request failed: authentication error', {
|
|
297
|
+
type: 'PROXY_REQUEST_AUTH_ERROR',
|
|
298
|
+
destination,
|
|
299
|
+
error: authError instanceof Error ? authError.message : String(authError),
|
|
300
|
+
});
|
|
301
|
+
if (!res.headersSent) {
|
|
302
|
+
res.writeHead(502, { 'Content-Type': 'application/json' });
|
|
303
|
+
res.end(JSON.stringify({ error: 'Authentication failed' }));
|
|
304
|
+
}
|
|
305
|
+
await this.fatalAuthFailure(authError, destination, 'request');
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
252
309
|
// Get target URL
|
|
253
310
|
const targetUrl = intercepted.routingDecision.targetUrl ||
|
|
254
311
|
(await this.btpProxy.getTargetUrl(destination));
|
|
@@ -540,6 +597,37 @@ Authorization source: ${authSourceObj}`;
|
|
|
540
597
|
});
|
|
541
598
|
});
|
|
542
599
|
}
|
|
600
|
+
/**
|
|
601
|
+
* Authentication was requested but could not be completed. Report the reason
|
|
602
|
+
* clearly, release resources, and exit(1) so the proxy is restarted (and the
|
|
603
|
+
* user gets a fresh login prompt) instead of running unauthenticated. Mirrors
|
|
604
|
+
* the one-shot `mcp-auth` command, which exits on a failed/incomplete login.
|
|
605
|
+
*/
|
|
606
|
+
async fatalAuthFailure(error, destination, phase) {
|
|
607
|
+
if (this.authExiting)
|
|
608
|
+
return;
|
|
609
|
+
this.authExiting = true;
|
|
610
|
+
const { category, reason } = classifyAuthFailure(error);
|
|
611
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
612
|
+
logger_js_1.logger?.error('Authentication not completed — exiting', {
|
|
613
|
+
type: 'AUTH_FATAL_EXIT',
|
|
614
|
+
destination,
|
|
615
|
+
phase,
|
|
616
|
+
category,
|
|
617
|
+
reason,
|
|
618
|
+
error: detail,
|
|
619
|
+
});
|
|
620
|
+
console.error(`[MCP Proxy] ✗ Authentication failed for destination "${destination}" (${phase}): ${reason}.\n` +
|
|
621
|
+
`[MCP Proxy] ${detail}\n` +
|
|
622
|
+
`[MCP Proxy] Exiting (code 1) so the proxy can be restarted to retry authentication.`);
|
|
623
|
+
try {
|
|
624
|
+
await this.shutdown();
|
|
625
|
+
}
|
|
626
|
+
catch {
|
|
627
|
+
// best-effort cleanup; exit regardless
|
|
628
|
+
}
|
|
629
|
+
process.exit(1);
|
|
630
|
+
}
|
|
543
631
|
/**
|
|
544
632
|
* Shutdown server
|
|
545
633
|
*/
|
|
@@ -571,6 +659,9 @@ exports.default = McpAbapAdtProxyServer;
|
|
|
571
659
|
* Print CLI usage and exit.
|
|
572
660
|
*/
|
|
573
661
|
function printHelp() {
|
|
662
|
+
// Show the real, platform-specific directory where service keys are read from
|
|
663
|
+
// (Windows: %USERPROFILE%\Documents\mcp-abap-adt\service-keys; Unix: ~/.config/...)
|
|
664
|
+
const serviceKeyDir = (0, stores_js_1.getPlatformPaths)('service-keys')[0];
|
|
574
665
|
const help = `Usage: mcp-abap-adt-proxy [options]
|
|
575
666
|
|
|
576
667
|
Authorization proxy for MCP clients (Cline, Claude Code, etc.) that cannot
|
|
@@ -582,7 +673,8 @@ CONFIGURATION
|
|
|
582
673
|
|
|
583
674
|
AUTHENTICATION (BTP / XSUAA)
|
|
584
675
|
--btp <destination> BTP destination name. Service key is read from
|
|
585
|
-
|
|
676
|
+
${serviceKeyDir}${require('node:path').sep}<dest>.json
|
|
677
|
+
(override the base dir with AUTH_BROKER_PATH)
|
|
586
678
|
--target-url <url> Override target service URL (default: from service key)
|
|
587
679
|
--url <url> Alias for --target-url
|
|
588
680
|
--unsafe Persist session/tokens to disk (default: in-memory)
|
|
@@ -595,12 +687,18 @@ BROWSER AUTHORIZATION
|
|
|
595
687
|
chrome - Google Chrome
|
|
596
688
|
edge - Microsoft Edge
|
|
597
689
|
firefox - Firefox
|
|
598
|
-
none - Print URL to console
|
|
599
|
-
callback
|
|
690
|
+
none - Print URL to console; complete login
|
|
691
|
+
via callback OR by pasting the code
|
|
692
|
+
(for SSH/headless/remote sessions)
|
|
600
693
|
headless - Alias for 'none'
|
|
601
694
|
--browser-auth-port <port> Local OAuth callback port. Must match the
|
|
602
695
|
redirect_uri registered on XSUAA (default: 3333).
|
|
603
696
|
|
|
697
|
+
Run without auto-opening a browser (prints the URL, then completes via the
|
|
698
|
+
localhost callback, the paste form at http://<host>:<port>/, or by pasting
|
|
699
|
+
the code in the terminal):
|
|
700
|
+
mcp-abap-adt-proxy --btp <dest> --browser none --browser-auth-port 3333
|
|
701
|
+
|
|
604
702
|
TRANSPORT
|
|
605
703
|
--transport <type> stdio | streamable-http | http | sse
|
|
606
704
|
(auto-detected from stdin TTY if not set)
|
package/dist/lib/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAAA;;GAEG;
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/lib/config.ts"],"names":[],"mappings":"AAAA;;GAEG;AAYH,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IAEjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAGxC,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAG/B,OAAO,CAAC,EAAE,QAAQ,GAAG,UAAU,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;IACzE,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,WAAW,CAoC3D;AAwED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CACpC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC3B,mBAAmB,CAQrB;AAkCD;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,MAAM,GACf,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAYhC;AAmJD;;GAEG;AACH,wBAAgB,aAAa,IAAI,MAAM,GAAG,SAAS,CAElD;AAUD;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG;IACnD,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CAoDA"}
|
package/dist/lib/config.js
CHANGED
|
@@ -44,6 +44,7 @@ exports.validateConfig = validateConfig;
|
|
|
44
44
|
const fs = __importStar(require("node:fs"));
|
|
45
45
|
const path = __importStar(require("node:path"));
|
|
46
46
|
const yaml = __importStar(require("js-yaml"));
|
|
47
|
+
const envInterpolation_js_1 = require("./envInterpolation.js");
|
|
47
48
|
/**
|
|
48
49
|
* Load configuration from file if --config is provided, otherwise from environment variables and command line
|
|
49
50
|
* Supports both JSON and YAML formats
|
|
@@ -64,7 +65,12 @@ function loadConfig(configPath) {
|
|
|
64
65
|
throw new Error(`Config file not found: ${finalConfigPath}`);
|
|
65
66
|
}
|
|
66
67
|
const fileConfig = loadConfigFile(finalConfigPath);
|
|
67
|
-
const
|
|
68
|
+
const envFilePath = resolveEnvFilePath(fileConfig, finalConfigPath);
|
|
69
|
+
const envFileMap = envFilePath ? (0, envInterpolation_js_1.loadEnvFile)(envFilePath) : {};
|
|
70
|
+
const lookup = (0, envInterpolation_js_1.buildLookup)(envFileMap);
|
|
71
|
+
const interpolated = (0, envInterpolation_js_1.interpolateConfig)(fileConfig, lookup);
|
|
72
|
+
delete interpolated.envFile;
|
|
73
|
+
const base = applyDefaults(interpolated);
|
|
68
74
|
const cli = readCliOverrides();
|
|
69
75
|
return mergeCliOverrides(base, cli, finalConfigPath);
|
|
70
76
|
}
|
|
@@ -155,6 +161,21 @@ function loadConfigFile(filePath) {
|
|
|
155
161
|
return JSON.parse(configContent);
|
|
156
162
|
}
|
|
157
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Resolve the .env path for the file-config path: --env-file wins (relative to
|
|
166
|
+
* cwd); otherwise the YAML `envFile` field, resolved relative to the config
|
|
167
|
+
* file's directory. Returns undefined when neither is set.
|
|
168
|
+
*/
|
|
169
|
+
function resolveEnvFilePath(rawConfig, configPath) {
|
|
170
|
+
const cliEnvFile = getArgValue('--env-file');
|
|
171
|
+
if (cliEnvFile !== undefined)
|
|
172
|
+
return path.resolve(cliEnvFile);
|
|
173
|
+
const yamlEnvFile = rawConfig.envFile;
|
|
174
|
+
if (typeof yamlEnvFile === 'string') {
|
|
175
|
+
return path.resolve(path.dirname(configPath), yamlEnvFile);
|
|
176
|
+
}
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
158
179
|
/**
|
|
159
180
|
* Load raw config from file for transport field extraction
|
|
160
181
|
*/
|
|
@@ -210,17 +231,20 @@ function loadFromEnv() {
|
|
|
210
231
|
const browserAuthPort = browserAuthPortStr
|
|
211
232
|
? parseInt(browserAuthPortStr, 10)
|
|
212
233
|
: undefined;
|
|
213
|
-
// Parse repeatable --header key=value arguments
|
|
234
|
+
// Parse repeatable --header key=value arguments, interpolating ${VAR}
|
|
214
235
|
const headerArgs = getAllArgValues('--header');
|
|
215
236
|
let defaultHeaders;
|
|
216
237
|
if (headerArgs.length > 0) {
|
|
238
|
+
const cliEnvFile = getArgValue('--env-file');
|
|
239
|
+
const envFileMap = cliEnvFile ? (0, envInterpolation_js_1.loadEnvFile)(path.resolve(cliEnvFile)) : {};
|
|
240
|
+
const lookup = (0, envInterpolation_js_1.buildLookup)(envFileMap);
|
|
217
241
|
defaultHeaders = {};
|
|
218
242
|
for (const arg of headerArgs) {
|
|
219
243
|
const eqIndex = arg.indexOf('=');
|
|
220
244
|
if (eqIndex > 0) {
|
|
221
245
|
const key = arg.substring(0, eqIndex).toLowerCase();
|
|
222
|
-
const
|
|
223
|
-
defaultHeaders[key] =
|
|
246
|
+
const rawValue = arg.substring(eqIndex + 1);
|
|
247
|
+
defaultHeaders[key] = (0, envInterpolation_js_1.interpolateString)(rawValue, lookup, `--header ${key}`);
|
|
224
248
|
}
|
|
225
249
|
}
|
|
226
250
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment-variable interpolation for proxy configuration.
|
|
3
|
+
* Supports ${VAR} and ${VAR:-default} in string values.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Resolve ${VAR} / ${VAR:-default} placeholders in a single string.
|
|
7
|
+
* Empty-string follows bash `:-` semantics: an empty value falls back to the
|
|
8
|
+
* default when one is given. A ${VAR} without a default that resolves to
|
|
9
|
+
* undefined throws, naming the variable and the field it came from.
|
|
10
|
+
*/
|
|
11
|
+
export declare function interpolateString(input: string, lookup: (key: string) => string | undefined, fieldPath: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Recursively interpolate all string values in a parsed config object.
|
|
14
|
+
* Objects/arrays are walked; non-string scalars are returned unchanged.
|
|
15
|
+
* The field path (e.g. `defaultHeaders.x-sap-password`) is threaded through for
|
|
16
|
+
* error messages.
|
|
17
|
+
*/
|
|
18
|
+
export declare function interpolateConfig(value: unknown, lookup: (key: string) => string | undefined, path?: string): unknown;
|
|
19
|
+
/**
|
|
20
|
+
* Parse a .env file into a flat map. Throws if the path is given but missing —
|
|
21
|
+
* a specified-yet-absent secret source is a configuration error, not a no-op.
|
|
22
|
+
*/
|
|
23
|
+
export declare function loadEnvFile(envFilePath: string): Record<string, string>;
|
|
24
|
+
/**
|
|
25
|
+
* Build a lookup over process.env (highest priority) then the parsed .env map.
|
|
26
|
+
*/
|
|
27
|
+
export declare function buildLookup(envFileMap: Record<string, string>): (key: string) => string | undefined;
|
|
28
|
+
//# sourceMappingURL=envInterpolation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"envInterpolation.d.ts","sourceRoot":"","sources":["../../src/lib/envInterpolation.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAOH;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,EAC3C,SAAS,EAAE,MAAM,GAChB,MAAM,CAiBR;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,EAC3C,IAAI,SAAK,GACR,OAAO,CAiBT;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAKvE;AAED;;GAEG;AACH,wBAAgB,WAAW,CACzB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACjC,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAGrC"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Environment-variable interpolation for proxy configuration.
|
|
4
|
+
* Supports ${VAR} and ${VAR:-default} in string values.
|
|
5
|
+
*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
+
}) : function(o, v) {
|
|
20
|
+
o["default"] = v;
|
|
21
|
+
});
|
|
22
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
+
var ownKeys = function(o) {
|
|
24
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
+
var ar = [];
|
|
26
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
+
return ar;
|
|
28
|
+
};
|
|
29
|
+
return ownKeys(o);
|
|
30
|
+
};
|
|
31
|
+
return function (mod) {
|
|
32
|
+
if (mod && mod.__esModule) return mod;
|
|
33
|
+
var result = {};
|
|
34
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
+
__setModuleDefault(result, mod);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
exports.interpolateString = interpolateString;
|
|
41
|
+
exports.interpolateConfig = interpolateConfig;
|
|
42
|
+
exports.loadEnvFile = loadEnvFile;
|
|
43
|
+
exports.buildLookup = buildLookup;
|
|
44
|
+
const fs = __importStar(require("node:fs"));
|
|
45
|
+
const dotenv = __importStar(require("dotenv"));
|
|
46
|
+
const PLACEHOLDER = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
|
|
47
|
+
/**
|
|
48
|
+
* Resolve ${VAR} / ${VAR:-default} placeholders in a single string.
|
|
49
|
+
* Empty-string follows bash `:-` semantics: an empty value falls back to the
|
|
50
|
+
* default when one is given. A ${VAR} without a default that resolves to
|
|
51
|
+
* undefined throws, naming the variable and the field it came from.
|
|
52
|
+
*/
|
|
53
|
+
function interpolateString(input, lookup, fieldPath) {
|
|
54
|
+
return input.replace(PLACEHOLDER, (_match, name, defaultVal) => {
|
|
55
|
+
const value = lookup(name);
|
|
56
|
+
const isEmpty = value === undefined || value === '';
|
|
57
|
+
if (defaultVal !== undefined) {
|
|
58
|
+
return isEmpty ? defaultVal : value;
|
|
59
|
+
}
|
|
60
|
+
if (value === undefined) {
|
|
61
|
+
throw new Error(`Config references undefined env variable: ${name} (referenced in ${fieldPath})`);
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Recursively interpolate all string values in a parsed config object.
|
|
68
|
+
* Objects/arrays are walked; non-string scalars are returned unchanged.
|
|
69
|
+
* The field path (e.g. `defaultHeaders.x-sap-password`) is threaded through for
|
|
70
|
+
* error messages.
|
|
71
|
+
*/
|
|
72
|
+
function interpolateConfig(value, lookup, path = '') {
|
|
73
|
+
if (typeof value === 'string') {
|
|
74
|
+
return interpolateString(value, lookup, path || '(root)');
|
|
75
|
+
}
|
|
76
|
+
if (Array.isArray(value)) {
|
|
77
|
+
return value.map((item, i) => interpolateConfig(item, lookup, `${path}[${i}]`));
|
|
78
|
+
}
|
|
79
|
+
if (value !== null && typeof value === 'object') {
|
|
80
|
+
const out = {};
|
|
81
|
+
for (const [key, val] of Object.entries(value)) {
|
|
82
|
+
out[key] = interpolateConfig(val, lookup, path ? `${path}.${key}` : key);
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Parse a .env file into a flat map. Throws if the path is given but missing —
|
|
90
|
+
* a specified-yet-absent secret source is a configuration error, not a no-op.
|
|
91
|
+
*/
|
|
92
|
+
function loadEnvFile(envFilePath) {
|
|
93
|
+
if (!fs.existsSync(envFilePath)) {
|
|
94
|
+
throw new Error(`env file not found: ${envFilePath}`);
|
|
95
|
+
}
|
|
96
|
+
return dotenv.parse(fs.readFileSync(envFilePath, 'utf-8'));
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Build a lookup over process.env (highest priority) then the parsed .env map.
|
|
100
|
+
*/
|
|
101
|
+
function buildLookup(envFileMap) {
|
|
102
|
+
return (key) => process.env[key] ?? envFileMap[key];
|
|
103
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"btpProxy.d.ts","sourceRoot":"","sources":["../../src/proxy/btpProxy.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,UAAU,EAAgB,MAAM,2BAA2B,CAAC;AAYrE,OAAO,EAAc,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGhE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AA4CnE;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAU3C;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAGD,MAAM,MAAM,YAAY,GAAG,mBAAmB,CAAC;AAE/C,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE;QACN,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,OAAO,CAAC;KAChB,CAAC;CACH;AAED;;GAEG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,oBAAoB,CAAa;IACzC,OAAO,CAAC,cAAc,CAAsC;IAC5D,OAAO,CAAC,UAAU,CACN;IACZ,OAAO,CAAC,aAAa,CAA0C;IAC/D,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkB;IAClD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAiB;IACjD,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,MAAM,CAAU;gBAEZ,oBAAoB,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC;IAqF3E;;;OAGG;IACU,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"btpProxy.d.ts","sourceRoot":"","sources":["../../src/proxy/btpProxy.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,UAAU,EAAgB,MAAM,2BAA2B,CAAC;AAYrE,OAAO,EAAc,KAAK,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAGhE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AA4CnE;;;GAGG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAU3C;AAED,MAAM,WAAW,mBAAmB;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5B,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAGD,MAAM,MAAM,YAAY,GAAG,mBAAmB,CAAC;AAE/C,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC5B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE;QACN,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,OAAO,CAAC;KAChB,CAAC;CACH;AAED;;GAEG;AACH,qBAAa,QAAQ;IACnB,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,oBAAoB,CAAa;IACzC,OAAO,CAAC,cAAc,CAAsC;IAC5D,OAAO,CAAC,UAAU,CACN;IACZ,OAAO,CAAC,aAAa,CAA0C;IAC/D,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAkB;IAClD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAiB;IACjD,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,MAAM,CAAU;gBAEZ,oBAAoB,EAAE,UAAU,EAAE,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC;IAqF3E;;;OAGG;IACU,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAwB3D;;;;OAIG;YACW,wBAAwB;IA0FtC;;OAEG;YACW,uBAAuB;IAyFrC;;;;OAIG;IACG,WAAW,CACf,WAAW,EAAE,MAAM,EACnB,YAAY,GAAE,OAAe,GAC5B,OAAO,CAAC,MAAM,CAAC;IAiHlB;;OAEG;IACH,OAAO,CAAC,YAAY;IAapB;;;OAGG;IACH,OAAO,CAAC,UAAU;IASlB;;OAEG;IACH,OAAO,CAAC,wBAAwB;IA6BhC;;OAEG;YACW,gBAAgB;IAU9B;;OAEG;IACI,OAAO,IAAI,IAAI;IAStB;;;OAGG;IACG,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAexD;;OAEG;IACH,OAAO,CAAC,cAAc;IAUtB;;;;;;;;;;;;;;;;OAgBG;YACW,iBAAiB;IAmT/B;;OAEG;IACG,YAAY,CAChB,eAAe,EAAE,YAAY,EAC7B,eAAe,EAAE,eAAe,EAChC,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,GAC7D,OAAO,CAAC,aAAa,CAAC;IAiLzB;;OAEG;WACiB,MAAM,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAAG,OAAO,CAAC,QAAQ,CAAC;CA0B7E;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,MAAM,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,GAC5B,OAAO,CAAC,QAAQ,CAAC,CAEnB"}
|
package/dist/proxy/btpProxy.js
CHANGED
|
@@ -155,8 +155,9 @@ class BtpProxy {
|
|
|
155
155
|
destination,
|
|
156
156
|
error: error instanceof Error ? error.message : String(error),
|
|
157
157
|
});
|
|
158
|
-
//
|
|
159
|
-
//
|
|
158
|
+
// Re-throw so the server can decide to exit (auth requested but not
|
|
159
|
+
// completed) instead of running in a permanently unauthenticated state.
|
|
160
|
+
throw error;
|
|
160
161
|
}
|
|
161
162
|
}
|
|
162
163
|
/**
|
|
@@ -199,9 +200,10 @@ class BtpProxy {
|
|
|
199
200
|
});
|
|
200
201
|
}
|
|
201
202
|
if (!authConfig) {
|
|
203
|
+
const serviceKeyDir = (0, stores_js_1.getPlatformPaths)('service-keys')[0];
|
|
202
204
|
logger_js_1.logger?.error('Service key not found for destination', {
|
|
203
205
|
destination,
|
|
204
|
-
hint:
|
|
206
|
+
hint: `Ensure service key file "${destination}.json" exists in ${serviceKeyDir} (override the base dir with AUTH_BROKER_PATH)`,
|
|
205
207
|
});
|
|
206
208
|
// We cannot proceed without config, but we'll let it fail with a clear message
|
|
207
209
|
// Or we could throw here.
|
|
@@ -213,6 +215,10 @@ class BtpProxy {
|
|
|
213
215
|
const providerConfig = {
|
|
214
216
|
browser: this.config.browser,
|
|
215
217
|
redirectPort: this.config.browserAuthPort || 3333,
|
|
218
|
+
// Pass a logger so browserAuth can surface the "Open this URL" prompt in
|
|
219
|
+
// 'none'/'headless' mode. Without it, startBrowserAuth gets a null logger
|
|
220
|
+
// and silently drops the authorization URL.
|
|
221
|
+
logger: loggerAdapter,
|
|
216
222
|
...(authConfig
|
|
217
223
|
? {
|
|
218
224
|
uaaUrl: authConfig.uaaUrl,
|
|
@@ -924,6 +930,7 @@ class BtpProxy {
|
|
|
924
930
|
clientSecret: 'placeholder',
|
|
925
931
|
browser: loadedConfig.browser,
|
|
926
932
|
redirectPort: loadedConfig.browserAuthPort,
|
|
933
|
+
logger: loggerAdapter,
|
|
927
934
|
}),
|
|
928
935
|
}, 'none', loggerAdapter);
|
|
929
936
|
return new BtpProxy(defaultBtpAuthBroker, loadedConfig);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcp-abap-adt/proxy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "MCP proxy server for SAP ABAP ADT - proxies local requests to cloud-llm-hub with JWT authentication",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -63,14 +63,15 @@
|
|
|
63
63
|
"typescript": "^6.0.3"
|
|
64
64
|
},
|
|
65
65
|
"dependencies": {
|
|
66
|
-
"@mcp-abap-adt/auth-broker": "^1.0.
|
|
67
|
-
"@mcp-abap-adt/auth-providers": "^1.0
|
|
66
|
+
"@mcp-abap-adt/auth-broker": "^1.0.6",
|
|
67
|
+
"@mcp-abap-adt/auth-providers": "^1.1.0",
|
|
68
68
|
"@mcp-abap-adt/auth-stores": "^1.0.4",
|
|
69
69
|
"@mcp-abap-adt/header-validator": "^0.1.8",
|
|
70
70
|
"@mcp-abap-adt/interfaces": "^7.0.0",
|
|
71
71
|
"@mcp-abap-adt/logger": "^0.1.4",
|
|
72
72
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
73
73
|
"axios": "^1.15.1",
|
|
74
|
+
"dotenv": "^17.4.2",
|
|
74
75
|
"js-yaml": "^4.1.1",
|
|
75
76
|
"zod": "^4.3.6"
|
|
76
77
|
},
|