@mcp-abap-adt/proxy 4.0.0 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [4.0.1] - 2026-09-24
11
+
12
+ **A variable from an env file could be silently lost, and the failure blamed the
13
+ variable instead of naming the file.** Both hit Windows hardest, with the same
14
+ config and the same `.env` that work on Linux.
15
+
16
+ ### Fixed
17
+
18
+ - **An empty environment variable no longer shadows the env file.** The lookup
19
+ was `process.env[key] ?? envFileMap[key]`, and `??` skips only `undefined` — so
20
+ a `SAP_LOGIN=` sitting in the environment beat the file the user had pointed at
21
+ with `--env-file`, and the header went out empty with nothing reported. Windows
22
+ carries variables nobody set deliberately, which is why the same files behaved
23
+ differently there. A real value in the environment still wins.
24
+
25
+ - **The interpolation failure now names where it looked.** It said only
26
+ `Config references undefined env variable: SAP_LOGIN`, so three different
27
+ causes produced one sentence and the cause had to be guessed:
28
+
29
+ ```
30
+ ... looked in process.env only — no env file was given (--env-file or envFile:)
31
+ ... looked in process.env, then /path/e19.env, which yielded NO variables
32
+ (0 bytes) — check its encoding, since a UTF-16 file reads as nothing here
33
+ ... looked in process.env, then /path/e19.env (1: SAP_PASSWORD)
34
+ ```
35
+
36
+ An env file that yields nothing is still not an error — an empty `.env` is
37
+ legitimate, and a config may take every value from the environment. It is
38
+ reported by whoever then needs a variable, where it can be said usefully.
39
+
40
+ ### Documentation
41
+
42
+ - `docs/YAML_CONFIG.md`: a Windows path in **double** quotes is a YAML escape
43
+ context. `envFile: "C:\Users\..."` fails to parse (`\U` is a Unicode escape)
44
+ and `"C:\temp\e19.env"` parses *silently wrong* (`\e` becomes 0x1B). Use
45
+ single quotes, no quotes, or forward slashes.
46
+
10
47
  ## [4.0.0] - 2026-09-24
11
48
 
12
49
  **The proxy stops re-implementing the broker, there is one forwarding path
@@ -1 +1 @@
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"}
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,CA8C3D;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"}
@@ -68,7 +68,15 @@ function loadConfig(configPath) {
68
68
  const envFilePath = resolveEnvFilePath(fileConfig, finalConfigPath);
69
69
  const envFileMap = envFilePath ? (0, envInterpolation_js_1.loadEnvFile)(envFilePath) : {};
70
70
  const lookup = (0, envInterpolation_js_1.buildLookup)(envFileMap);
71
- const interpolated = (0, envInterpolation_js_1.interpolateConfig)(fileConfig, lookup);
71
+ const keys = Object.keys(envFileMap);
72
+ const sources = !envFilePath
73
+ ? 'process.env only — no env file was given (--env-file or envFile:)'
74
+ : keys.length === 0
75
+ ? `process.env, then ${envFilePath}, which yielded NO variables ` +
76
+ `(${fs.statSync(envFilePath).size} bytes) — check its encoding, ` +
77
+ `since a UTF-16 file reads as nothing here, and that lines look like KEY=value`
78
+ : `process.env, then ${envFilePath} (${keys.length}: ${keys.join(', ')})`;
79
+ const interpolated = (0, envInterpolation_js_1.interpolateConfig)(fileConfig, lookup, '', sources);
72
80
  delete interpolated.envFile;
73
81
  const base = applyDefaults(interpolated);
74
82
  const cli = readCliOverrides();
@@ -8,14 +8,16 @@
8
8
  * default when one is given. A ${VAR} without a default that resolves to
9
9
  * undefined throws, naming the variable and the field it came from.
10
10
  */
11
- export declare function interpolateString(input: string, lookup: (key: string) => string | undefined, fieldPath: string): string;
11
+ export declare function interpolateString(input: string, lookup: (key: string) => string | undefined, fieldPath: string,
12
+ /** Where values were looked for, named in the failure. */
13
+ sources?: string): string;
12
14
  /**
13
15
  * Recursively interpolate all string values in a parsed config object.
14
16
  * Objects/arrays are walked; non-string scalars are returned unchanged.
15
17
  * The field path (e.g. `defaultHeaders.x-sap-password`) is threaded through for
16
18
  * error messages.
17
19
  */
18
- export declare function interpolateConfig(value: unknown, lookup: (key: string) => string | undefined, path?: string): unknown;
20
+ export declare function interpolateConfig(value: unknown, lookup: (key: string) => string | undefined, path?: string, sources?: string): unknown;
19
21
  /**
20
22
  * Parse a .env file into a flat map. Throws if the path is given but missing —
21
23
  * a specified-yet-absent secret source is a configuration error, not a no-op.
@@ -23,6 +25,16 @@ export declare function interpolateConfig(value: unknown, lookup: (key: string)
23
25
  export declare function loadEnvFile(envFilePath: string): Record<string, string>;
24
26
  /**
25
27
  * Build a lookup over process.env (highest priority) then the parsed .env map.
28
+ *
29
+ * An EMPTY environment variable does not count as a value. It used to: `??`
30
+ * skips only `undefined`, so a `SAP_LOGIN=` sitting in the environment won
31
+ * against the env file the user had explicitly pointed at with `--env-file`,
32
+ * and the header went out empty with nothing reported. Windows carries
33
+ * variables nobody set deliberately, which is why the same config and the same
34
+ * `.env` behaved differently there.
35
+ *
36
+ * A real value in the environment still wins — that is the documented
37
+ * precedence, and it is how a one-off override is meant to work.
26
38
  */
27
39
  export declare function buildLookup(envFileMap: Record<string, string>): (key: string) => string | undefined;
28
40
  //# sourceMappingURL=envInterpolation.d.ts.map
@@ -1 +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"}
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;AACjB,0DAA0D;AAC1D,OAAO,SAAgB,GACtB,MAAM,CAsBR;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,EAC3C,IAAI,SAAK,EACT,OAAO,SAAgB,GACtB,OAAO,CAsBT;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CASvE;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CACzB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GACjC,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,GAAG,SAAS,CAQrC"}
@@ -50,7 +50,9 @@ const PLACEHOLDER = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g;
50
50
  * default when one is given. A ${VAR} without a default that resolves to
51
51
  * undefined throws, naming the variable and the field it came from.
52
52
  */
53
- function interpolateString(input, lookup, fieldPath) {
53
+ function interpolateString(input, lookup, fieldPath,
54
+ /** Where values were looked for, named in the failure. */
55
+ sources = 'process.env') {
54
56
  return input.replace(PLACEHOLDER, (_match, name, defaultVal) => {
55
57
  const value = lookup(name);
56
58
  const isEmpty = value === undefined || value === '';
@@ -58,7 +60,12 @@ function interpolateString(input, lookup, fieldPath) {
58
60
  return isEmpty ? defaultVal : value;
59
61
  }
60
62
  if (value === undefined) {
61
- throw new Error(`Config references undefined env variable: ${name} (referenced in ${fieldPath})`);
63
+ // Naming where it looked, because the old message named only the
64
+ // variable — so a file never read, a file read empty, and a value
65
+ // shadowed by the environment all produced the same sentence and the
66
+ // cause had to be guessed.
67
+ throw new Error(`Config references undefined env variable: ${name} ` +
68
+ `(referenced in ${fieldPath}; looked in ${sources})`);
62
69
  }
63
70
  return value;
64
71
  });
@@ -69,17 +76,17 @@ function interpolateString(input, lookup, fieldPath) {
69
76
  * The field path (e.g. `defaultHeaders.x-sap-password`) is threaded through for
70
77
  * error messages.
71
78
  */
72
- function interpolateConfig(value, lookup, path = '') {
79
+ function interpolateConfig(value, lookup, path = '', sources = 'process.env') {
73
80
  if (typeof value === 'string') {
74
- return interpolateString(value, lookup, path || '(root)');
81
+ return interpolateString(value, lookup, path || '(root)', sources);
75
82
  }
76
83
  if (Array.isArray(value)) {
77
- return value.map((item, i) => interpolateConfig(item, lookup, `${path}[${i}]`));
84
+ return value.map((item, i) => interpolateConfig(item, lookup, `${path}[${i}]`, sources));
78
85
  }
79
86
  if (value !== null && typeof value === 'object') {
80
87
  const out = {};
81
88
  for (const [key, val] of Object.entries(value)) {
82
- out[key] = interpolateConfig(val, lookup, path ? `${path}.${key}` : key);
89
+ out[key] = interpolateConfig(val, lookup, path ? `${path}.${key}` : key, sources);
83
90
  }
84
91
  return out;
85
92
  }
@@ -93,11 +100,31 @@ function loadEnvFile(envFilePath) {
93
100
  if (!fs.existsSync(envFilePath)) {
94
101
  throw new Error(`env file not found: ${envFilePath}`);
95
102
  }
103
+ // Not an error when it yields nothing: an empty `.env` is legitimate, and a
104
+ // config may take every value from the environment. But the emptiness is
105
+ // reported by whoever needs a variable — see the `sources` note threaded into
106
+ // the interpolation failure, which names the file, its size and its key count.
96
107
  return dotenv.parse(fs.readFileSync(envFilePath, 'utf-8'));
97
108
  }
98
109
  /**
99
110
  * Build a lookup over process.env (highest priority) then the parsed .env map.
111
+ *
112
+ * An EMPTY environment variable does not count as a value. It used to: `??`
113
+ * skips only `undefined`, so a `SAP_LOGIN=` sitting in the environment won
114
+ * against the env file the user had explicitly pointed at with `--env-file`,
115
+ * and the header went out empty with nothing reported. Windows carries
116
+ * variables nobody set deliberately, which is why the same config and the same
117
+ * `.env` behaved differently there.
118
+ *
119
+ * A real value in the environment still wins — that is the documented
120
+ * precedence, and it is how a one-off override is meant to work.
100
121
  */
101
122
  function buildLookup(envFileMap) {
102
- return (key) => process.env[key] ?? envFileMap[key];
123
+ return (key) => {
124
+ const fromEnvironment = process.env[key];
125
+ if (fromEnvironment !== undefined && fromEnvironment !== '') {
126
+ return fromEnvironment;
127
+ }
128
+ return envFileMap[key];
129
+ };
103
130
  }
@@ -206,6 +206,23 @@ requestTimeout: 120000
206
206
  | `maxRetries` | `number` | `3` | Maximum number of retry attempts |
207
207
  | `retryDelay` | `number` | `1000` | Delay between retries (milliseconds) |
208
208
  | `requestTimeout` | `number` | `60000` | Request timeout (milliseconds) |
209
+ ### Windows paths must not go in double quotes
210
+
211
+ In YAML, a double-quoted string is an **escape context**, and a Windows path is
212
+ full of backslashes. Measured with the parser this package uses:
213
+
214
+ | written as | result |
215
+ |---|---|
216
+ | `envFile: "C:\Users\me\e19.env"` | **fails** — `expected hexadecimal character`, because `\U` starts a Unicode escape |
217
+ | `envFile: "C:\temp\e19.env"` | **silently wrong** — `\e` becomes the escape character `0x1B`, so the path points nowhere |
218
+ | `envFile: 'C:\Users\me\e19.env'` | correct |
219
+ | `envFile: C:\Users\me\e19.env` | correct |
220
+ | `envFile: "C:/Users/me/e19.env"` | correct |
221
+
222
+ The second row is the dangerous one: it does not fail, it resolves to a path that
223
+ does not exist. Forward slashes are the safest form — Node accepts them on
224
+ Windows, and they carry no meaning inside quotes.
225
+
209
226
  | ~~`circuitBreakerThreshold`~~ | `number` | — | **No effect since 4.0.0.** Still accepted so existing files load; the circuit breaker guarded the buffered forward that release removed |
210
227
  | ~~`circuitBreakerTimeout`~~ | `number` | — | **No effect since 4.0.0.** As above |
211
228
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-abap-adt/proxy",
3
- "version": "4.0.0",
3
+ "version": "4.0.1",
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",