@trueburn/spring-config-client 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TrueBurn
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,306 @@
1
+ # @trueburn/spring-config-client
2
+
3
+ [![CI](https://github.com/TrueBurn/node-spring-config-client/actions/workflows/ci.yml/badge.svg)](https://github.com/TrueBurn/node-spring-config-client/actions/workflows/ci.yml)
4
+ [![npm version](https://img.shields.io/npm/v/@trueburn/spring-config-client)](https://www.npmjs.com/package/@trueburn/spring-config-client)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ Spring Cloud Config Server client for Node.js / Next.js.
8
+
9
+ Bootstraps remote configuration into `process.env` at startup -- so your non-JVM services consume config from the same Spring Cloud Config Server as your Spring Boot microservices.
10
+
11
+ ## Why
12
+
13
+ Your Spring Boot services already use Spring Cloud Config Server for centralised configuration. When a non-JVM service (Next.js, Express, Fastify) joins the platform, you don't want config sprawl across Helm values and GitOps repos. This package lets Node.js apps pull config from the same server, using the same YAML files, with a single env var to enable it.
14
+
15
+ ## Design Principles
16
+
17
+ - **Zero config files** -- configured entirely via env vars (no `bootstrap.yml`)
18
+ - **Existing env vars never overwritten** -- secrets from Vault/K8s always win
19
+ - **Dual key format** -- properties injected as both `app.database.host` and `APP_DATABASE_HOST`
20
+ - **Multiple profiles** -- comma-separated profiles supported (e.g. `production,eu-west`)
21
+ - **Custom headers** -- add arbitrary headers via env vars for auth tokens, tracing, etc.
22
+ - **No refresh** -- config is loaded once at startup; pod restart handles changes
23
+ - **No cipher support** -- secrets come from Vault/external secret operators, not the config server
24
+ - **Zero dependencies** -- uses only Node.js built-ins (`fetch`, `Buffer`, `http`)
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ npm install @trueburn/spring-config-client
30
+ ```
31
+
32
+ ## Quick Start
33
+
34
+ ### 1. Set env vars in your Helm chart / K8s deployment
35
+
36
+ ```yaml
37
+ env:
38
+ - name: SPRING_CLOUD_CONFIG_ENABLED
39
+ value: "true"
40
+ - name: SPRING_CLOUD_CONFIG_URI
41
+ value: "http://config-server:8888"
42
+ - name: SPRING_CLOUD_CONFIG_NAME
43
+ value: "my-nextjs-app"
44
+ - name: SPRING_CLOUD_CONFIG_PROFILE
45
+ value: "production"
46
+ ```
47
+
48
+ ### 2a. Next.js -- instrumentation.ts
49
+
50
+ ```typescript
51
+ // instrumentation.ts
52
+ export async function register() {
53
+ if (process.env.NEXT_RUNTIME === 'nodejs') {
54
+ const { bootstrap } = require('@trueburn/spring-config-client');
55
+ await bootstrap();
56
+ }
57
+ }
58
+ ```
59
+
60
+ Or use the auto-register shorthand:
61
+
62
+ ```typescript
63
+ // instrumentation.ts
64
+ export async function register() {
65
+ if (process.env.NEXT_RUNTIME === 'nodejs') {
66
+ await import('@trueburn/spring-config-client/register');
67
+ }
68
+ }
69
+ ```
70
+
71
+ ### 2b. Express / Fastify / any Node.js app
72
+
73
+ ```typescript
74
+ import { bootstrap } from '@trueburn/spring-config-client';
75
+
76
+ async function main() {
77
+ await bootstrap();
78
+
79
+ // process.env is now populated with remote config
80
+ const app = express();
81
+ app.listen(process.env.SERVER_PORT ?? 3000);
82
+ }
83
+
84
+ main();
85
+ ```
86
+
87
+ ### 3. Access config via process.env
88
+
89
+ Given this YAML in your config server's Git repo (`my-nextjs-app-production.yml`):
90
+
91
+ ```yaml
92
+ app:
93
+ database:
94
+ host: "prod-db.internal"
95
+ port: 5432
96
+ feature:
97
+ dark-mode: true
98
+ api:
99
+ base-url: "https://api.example.com"
100
+ ```
101
+
102
+ Your code can access values in either format:
103
+
104
+ ```typescript
105
+ // Dot notation (as-is from config server)
106
+ process.env["app.database.host"] // "prod-db.internal"
107
+ process.env["app.feature.dark-mode"] // "true"
108
+
109
+ // UPPER_SNAKE_CASE (standard env var convention)
110
+ process.env.APP_DATABASE_HOST // "prod-db.internal"
111
+ process.env.APP_FEATURE_DARK_MODE // "true"
112
+ process.env.APP_API_BASE_URL // "https://api.example.com"
113
+ ```
114
+
115
+ ## Multiple Profiles
116
+
117
+ Spring Cloud Config Server supports multiple active profiles. Pass comma-separated profiles to load config from all of them:
118
+
119
+ ```yaml
120
+ env:
121
+ - name: SPRING_CLOUD_CONFIG_PROFILE
122
+ value: "production,eu-west"
123
+ ```
124
+
125
+ This generates a request to `GET /{name}/production,eu-west/{label}`. The config server returns property sources for all specified profiles, with earlier profiles taking precedence.
126
+
127
+ This matches Spring Boot's `spring.profiles.active=production,eu-west` behaviour.
128
+
129
+ ## Custom Headers
130
+
131
+ Add custom HTTP headers to config server requests using the `SPRING_CLOUD_CONFIG_HEADERS_` prefix. This is useful for Bearer tokens, tracing headers, or any custom authentication mechanism.
132
+
133
+ **Naming convention:** Strip the prefix, replace underscores with hyphens, and title-case each segment.
134
+
135
+ ```yaml
136
+ env:
137
+ # Authorization: Bearer tok123
138
+ - name: SPRING_CLOUD_CONFIG_HEADERS_AUTHORIZATION
139
+ value: "Bearer tok123"
140
+
141
+ # X-Custom-Header: my-value
142
+ - name: SPRING_CLOUD_CONFIG_HEADERS_X_CUSTOM_HEADER
143
+ value: "my-value"
144
+
145
+ # X-Request-Id: trace-abc
146
+ - name: SPRING_CLOUD_CONFIG_HEADERS_X_REQUEST_ID
147
+ value: "trace-abc"
148
+ ```
149
+
150
+ You can also pass headers programmatically:
151
+
152
+ ```typescript
153
+ import { bootstrap } from '@trueburn/spring-config-client';
154
+
155
+ await bootstrap({
156
+ headers: {
157
+ Authorization: 'Bearer my-token',
158
+ 'X-Tenant-Id': 'acme',
159
+ },
160
+ });
161
+ ```
162
+
163
+ ## Configurable Timeout
164
+
165
+ By default, requests to the config server time out after 10 seconds. You can adjust this:
166
+
167
+ ```yaml
168
+ env:
169
+ - name: SPRING_CLOUD_CONFIG_REQUEST_TIMEOUT
170
+ value: "15000" # 15 seconds
171
+ ```
172
+
173
+ ## Configuration Reference
174
+
175
+ All configuration is via environment variables. No config files needed.
176
+
177
+ | Env Var | Default | Description |
178
+ |---------|---------|-------------|
179
+ | `SPRING_CLOUD_CONFIG_ENABLED` | `false` | Enable/disable the client. Must be `true` to fetch config. |
180
+ | `SPRING_CLOUD_CONFIG_URI` | `http://localhost:8888` | Config server base URL |
181
+ | `SPRING_CLOUD_CONFIG_NAME` | `application` | Application name (maps to config file name) |
182
+ | `SPRING_CLOUD_CONFIG_PROFILE` | `default` | Active profile(s), comma-separated (e.g. `production,eu-west`) |
183
+ | `SPRING_CLOUD_CONFIG_LABEL` | `main` | Git branch/label |
184
+ | `SPRING_CLOUD_CONFIG_FAIL_FAST` | `false` | Throw on fetch failure (blocks startup) |
185
+ | `SPRING_CLOUD_CONFIG_AUTH_USER` | -- | Basic auth username |
186
+ | `SPRING_CLOUD_CONFIG_AUTH_PASS` | -- | Basic auth password |
187
+ | `SPRING_CLOUD_CONFIG_RETRY_ENABLED` | `false` | Enable retry with exponential backoff |
188
+ | `SPRING_CLOUD_CONFIG_RETRY_MAX_ATTEMPTS` | `5` | Max retry attempts |
189
+ | `SPRING_CLOUD_CONFIG_RETRY_INTERVAL` | `1000` | Initial retry interval (ms) |
190
+ | `SPRING_CLOUD_CONFIG_RETRY_MULTIPLIER` | `1.5` | Backoff multiplier |
191
+ | `SPRING_CLOUD_CONFIG_RETRY_MAX_INTERVAL` | `30000` | Max retry interval (ms) |
192
+ | `SPRING_CLOUD_CONFIG_REQUEST_TIMEOUT` | `10000` | Request timeout (ms) |
193
+ | `SPRING_CLOUD_CONFIG_HEADERS_<NAME>` | -- | Custom header (e.g. `_AUTHORIZATION` becomes `Authorization`) |
194
+ | `SPRING_CLOUD_CONFIG_LOG_LEVEL` | `info` | Log level: debug, info, warn, error, silent |
195
+
196
+ ## Precedence
197
+
198
+ Properties are resolved in this order (highest to lowest):
199
+
200
+ 1. **Existing env vars** -- from Vault, K8s ConfigMaps, external secret operators, etc. These are **never** overwritten.
201
+ 2. **Most specific profile** -- e.g. `my-nextjs-app-production.yml`
202
+ 3. **Application defaults** -- e.g. `my-nextjs-app.yml`
203
+ 4. **Global defaults** -- e.g. `application.yml`
204
+
205
+ This matches Spring Boot's property resolution order.
206
+
207
+ ## Key Format
208
+
209
+ Each property from the config server is injected in two formats:
210
+
211
+ | Config Server Key | Dot notation (injected) | UPPER_SNAKE (injected) |
212
+ |---|---|---|
213
+ | `app.database.host` | `app.database.host` | `APP_DATABASE_HOST` |
214
+ | `app.feature.dark-mode` | `app.feature.dark-mode` | `APP_FEATURE_DARK_MODE` |
215
+ | `app.list[0].name` | `app.list[0].name` | `APP_LIST_0_NAME` |
216
+
217
+ ## Feature Parity with Spring Cloud Config Client
218
+
219
+ Comparison with the official [Spring Cloud Config Client](https://docs.spring.io/spring-cloud-config/reference/client.html):
220
+
221
+ | Feature | Status | Notes |
222
+ |---------|--------|-------|
223
+ | Bootstrap config from server | Supported | `GET /{name}/{profile}/{label}` |
224
+ | Multiple profiles | Supported | Comma-separated, e.g. `production,eu-west` |
225
+ | Basic authentication | Supported | Via `AUTH_USER` / `AUTH_PASS` env vars |
226
+ | Custom headers | Supported | Via `HEADERS_*` env vars or programmatic |
227
+ | Retry with exponential backoff | Supported | Configurable max attempts, interval, multiplier |
228
+ | Fail-fast mode | Supported | Blocks startup if config server is unreachable |
229
+ | Property source precedence | Supported | Most specific profile wins |
230
+ | Configurable timeout | Supported | Via `REQUEST_TIMEOUT` env var |
231
+ | Config-first bootstrap | Supported | Call `bootstrap()` before app starts |
232
+ | Discovery-first bootstrap | Not Planned | Use config server URL directly |
233
+ | Vault backend | Not Applicable | Secrets come from K8s/Vault directly |
234
+ | Config encryption/decryption | Not Planned | Use external secret operators |
235
+ | Dynamic refresh (`@RefreshScope`) | Not Planned | Pod restart handles config changes |
236
+ | Health indicator | Not Planned | Use K8s liveness/readiness probes |
237
+ | Spring Cloud Bus | Not Applicable | No JVM runtime |
238
+ | Composite environment repos | Supported | Server-side; client receives merged sources |
239
+
240
+ ## Alternatives
241
+
242
+ There are several Node.js clients for Spring Cloud Config Server on npm. Here's how they compare:
243
+
244
+ | | @trueburn/spring-config-client | cloud-config-client | Others |
245
+ |---|---|---|---|
246
+ | **Last updated** | 2026 | 2022 | 2018--2020 |
247
+ | **Weekly downloads** | New | ~7,400 | 0--850 |
248
+ | **Runtime deps** | 0 | 0 | 1--4 (axios, rxjs, lodash) |
249
+ | **TypeScript** | Native (source is TS) | Hand-written .d.ts | Mostly none |
250
+ | **ESM + CJS** | Both | CJS only | CJS only |
251
+ | **Node.js** | >= 18 | >= 10 | Varies |
252
+ | **Pattern** | Starter (injects into process.env) | Library (.get() API) | Library |
253
+ | **Config via env vars** | Yes | No (programmatic only) | Varies |
254
+ | **process.env injection** | Automatic | Manual | No |
255
+ | **Next.js integration** | Dedicated entry point | No | No |
256
+ | **Retry with backoff** | Yes | No | No |
257
+ | **Fail-fast mode** | Yes | No | No |
258
+ | **Request timeout** | Configurable | No | No |
259
+ | **Multiple profiles** | Yes | Yes | Varies |
260
+ | **Basic auth** | Yes | Yes | Yes |
261
+ | **Custom headers** | Yes | Yes (since 1.5.0) | No |
262
+ | **Proxy support** | Not yet | Yes | No |
263
+ | **Self-signed certs** | Not yet | Yes | No |
264
+ | **Context substitution** | No | Yes | No |
265
+
266
+ **Key difference:** `cloud-config-client` is a *library* -- you call `client.load()` and query a `Config` object. This package is a *starter* -- call `bootstrap()` (or import `register`) and your config is in `process.env`. If you need a config object API, use `cloud-config-client`. If you want Spring Boot-style bootstrap behaviour with retry, fail-fast, and env var configuration, use this package.
267
+
268
+ ## Programmatic API
269
+
270
+ If you need more control, the individual components are exported:
271
+
272
+ ```typescript
273
+ import {
274
+ bootstrap, // Main entry point
275
+ resolveConfig, // Read client config from env vars
276
+ fetchConfig, // Fetch property sources from config server
277
+ mergeAndInject, // Merge sources into process.env
278
+ toUpperSnake, // Convert dot.notation to UPPER_SNAKE
279
+ } from '@trueburn/spring-config-client';
280
+
281
+ // Example: fetch without injecting
282
+ const config = resolveConfig();
283
+ const sources = await fetchConfig(config);
284
+ const flat = Object.fromEntries(
285
+ sources.flatMap(s => Object.entries(s.source))
286
+ );
287
+ ```
288
+
289
+ ## Development
290
+
291
+ ```bash
292
+ npm install
293
+ npm test # Run tests
294
+ npm run build # Build for publishing
295
+ npm run typecheck # Type-check without emitting
296
+ npm run lint # Lint source files
297
+ npm run format # Format source files
298
+ ```
299
+
300
+ ## Contributing
301
+
302
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
303
+
304
+ ## License
305
+
306
+ MIT
@@ -0,0 +1,276 @@
1
+ // src/config.ts
2
+ function resolveConfig(env = process.env) {
3
+ const auth = env.SPRING_CLOUD_CONFIG_AUTH_USER ? {
4
+ user: env.SPRING_CLOUD_CONFIG_AUTH_USER,
5
+ pass: env.SPRING_CLOUD_CONFIG_AUTH_PASS ?? ""
6
+ } : void 0;
7
+ return {
8
+ enabled: envBool(env, "SPRING_CLOUD_CONFIG_ENABLED", false),
9
+ uri: env.SPRING_CLOUD_CONFIG_URI ?? "http://localhost:8888",
10
+ name: env.SPRING_CLOUD_CONFIG_NAME ?? "application",
11
+ profile: env.SPRING_CLOUD_CONFIG_PROFILE ?? "default",
12
+ label: env.SPRING_CLOUD_CONFIG_LABEL ?? "main",
13
+ failFast: envBool(env, "SPRING_CLOUD_CONFIG_FAIL_FAST", false),
14
+ auth,
15
+ retry: {
16
+ enabled: envBool(env, "SPRING_CLOUD_CONFIG_RETRY_ENABLED", false),
17
+ maxAttempts: envInt(env, "SPRING_CLOUD_CONFIG_RETRY_MAX_ATTEMPTS", 5),
18
+ interval: envInt(env, "SPRING_CLOUD_CONFIG_RETRY_INTERVAL", 1e3),
19
+ multiplier: envFloat(env, "SPRING_CLOUD_CONFIG_RETRY_MULTIPLIER", 1.5),
20
+ maxInterval: envInt(env, "SPRING_CLOUD_CONFIG_RETRY_MAX_INTERVAL", 3e4)
21
+ },
22
+ headers: parseHeaders(env),
23
+ requestTimeout: envInt(env, "SPRING_CLOUD_CONFIG_REQUEST_TIMEOUT", 1e4)
24
+ };
25
+ }
26
+ function parseHeaders(env) {
27
+ const prefix = "SPRING_CLOUD_CONFIG_HEADERS_";
28
+ const headers = {};
29
+ let hasHeaders = false;
30
+ for (const [key, value] of Object.entries(env)) {
31
+ if (key.startsWith(prefix) && value !== void 0 && value !== "") {
32
+ const headerName = key.substring(prefix.length).split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()).join("-");
33
+ headers[headerName] = value;
34
+ hasHeaders = true;
35
+ }
36
+ }
37
+ return hasHeaders ? headers : void 0;
38
+ }
39
+ function envBool(env, key, defaultValue) {
40
+ const val = env[key];
41
+ if (val === void 0 || val === "") return defaultValue;
42
+ return val.toLowerCase() === "true";
43
+ }
44
+ function envInt(env, key, defaultValue) {
45
+ const val = env[key];
46
+ if (val === void 0 || val === "") return defaultValue;
47
+ const parsed = parseInt(val, 10);
48
+ return isNaN(parsed) ? defaultValue : parsed;
49
+ }
50
+ function envFloat(env, key, defaultValue) {
51
+ const val = env[key];
52
+ if (val === void 0 || val === "") return defaultValue;
53
+ const parsed = parseFloat(val);
54
+ return isNaN(parsed) ? defaultValue : parsed;
55
+ }
56
+
57
+ // src/logger.ts
58
+ var LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3, silent: 4 };
59
+ function getLogLevel() {
60
+ const level = (process.env.SPRING_CLOUD_CONFIG_LOG_LEVEL ?? "info").toLowerCase();
61
+ return level in LOG_LEVELS ? level : "info";
62
+ }
63
+ function createLogger(scope) {
64
+ const prefix = `[@trueburn/spring-config-client:${scope}]`;
65
+ const shouldLog = (level) => {
66
+ return LOG_LEVELS[level] >= LOG_LEVELS[getLogLevel()];
67
+ };
68
+ return {
69
+ debug: (msg) => shouldLog("debug") && console.debug(`${prefix} ${msg}`),
70
+ info: (msg) => shouldLog("info") && console.info(`${prefix} ${msg}`),
71
+ warn: (msg) => shouldLog("warn") && console.warn(`${prefix} ${msg}`),
72
+ error: (msg) => shouldLog("error") && console.error(`${prefix} ${msg}`)
73
+ };
74
+ }
75
+
76
+ // src/retry.ts
77
+ var log = createLogger("retry");
78
+ async function withRetry(fn, config) {
79
+ if (!config.enabled) {
80
+ return fn();
81
+ }
82
+ let lastError;
83
+ let currentInterval = config.interval;
84
+ for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {
85
+ try {
86
+ return await fn();
87
+ } catch (err) {
88
+ lastError = err instanceof Error ? err : new Error(String(err));
89
+ if (attempt === config.maxAttempts) {
90
+ log.error(
91
+ `All ${config.maxAttempts} attempts failed. Last error: ${lastError.message}`
92
+ );
93
+ break;
94
+ }
95
+ log.warn(
96
+ `Attempt ${attempt}/${config.maxAttempts} failed: ${lastError.message}. Retrying in ${currentInterval}ms...`
97
+ );
98
+ await sleep(currentInterval);
99
+ currentInterval = Math.min(
100
+ currentInterval * config.multiplier,
101
+ config.maxInterval
102
+ );
103
+ }
104
+ }
105
+ throw lastError;
106
+ }
107
+ function sleep(ms) {
108
+ return new Promise((resolve) => setTimeout(resolve, ms));
109
+ }
110
+
111
+ // src/client.ts
112
+ var log2 = createLogger("client");
113
+ async function fetchConfig(config) {
114
+ const url = buildUrl(config);
115
+ log2.info(`Fetching config from ${url}`);
116
+ const doFetch = async () => {
117
+ const headers = {
118
+ Accept: "application/json"
119
+ };
120
+ if (config.auth) {
121
+ const credentials = Buffer.from(
122
+ `${config.auth.user}:${config.auth.pass}`
123
+ ).toString("base64");
124
+ headers["Authorization"] = `Basic ${credentials}`;
125
+ }
126
+ if (config.headers) {
127
+ Object.assign(headers, config.headers);
128
+ }
129
+ const response = await fetch(url, {
130
+ method: "GET",
131
+ headers,
132
+ signal: AbortSignal.timeout(config.requestTimeout)
133
+ });
134
+ if (!response.ok) {
135
+ throw new Error(
136
+ `Config server returned HTTP ${response.status}: ${response.statusText}`
137
+ );
138
+ }
139
+ const data = await response.json();
140
+ log2.info(
141
+ `Received ${data.propertySources.length} property source(s) for ${data.name} [${data.profiles.join(", ")}]` + (data.version ? ` @ ${data.version.substring(0, 8)}` : "")
142
+ );
143
+ if (log2.debug) {
144
+ for (const source of data.propertySources) {
145
+ const keyCount = Object.keys(source.source).length;
146
+ log2.debug(` -> ${source.name} (${keyCount} properties)`);
147
+ }
148
+ }
149
+ return data.propertySources;
150
+ };
151
+ try {
152
+ return await withRetry(doFetch, config.retry);
153
+ } catch (err) {
154
+ const error = err instanceof Error ? err : new Error(String(err));
155
+ if (config.failFast) {
156
+ log2.error(`Failed to fetch config (fail-fast enabled): ${error.message}`);
157
+ throw error;
158
+ }
159
+ log2.warn(
160
+ `Failed to fetch config (fail-fast disabled, continuing without remote config): ${error.message}`
161
+ );
162
+ return [];
163
+ }
164
+ }
165
+ function buildUrl(config) {
166
+ const base = config.uri.replace(/\/+$/, "");
167
+ const profileEncoded = config.profile.split(",").map((p) => encodeURIComponent(p.trim())).join(",");
168
+ return `${base}/${encodeURIComponent(config.name)}/${profileEncoded}/${encodeURIComponent(config.label)}`;
169
+ }
170
+
171
+ // src/merge.ts
172
+ var log3 = createLogger("merge");
173
+ function mergeAndInject(sources, env = process.env) {
174
+ const merged = /* @__PURE__ */ new Map();
175
+ for (const source of sources) {
176
+ for (const [key, value] of Object.entries(source.source)) {
177
+ if (!merged.has(key)) {
178
+ merged.set(key, String(value));
179
+ }
180
+ }
181
+ }
182
+ log3.info(`Merged ${merged.size} unique properties from ${sources.length} source(s)`);
183
+ let injected = 0;
184
+ for (const [dotKey, value] of merged) {
185
+ const snakeKey = toUpperSnake(dotKey);
186
+ if (injectIfAbsent(env, dotKey, value)) {
187
+ injected++;
188
+ }
189
+ if (snakeKey !== dotKey) {
190
+ if (injectIfAbsent(env, snakeKey, value)) {
191
+ injected++;
192
+ }
193
+ }
194
+ }
195
+ log3.info(`Injected ${injected} new entries into process.env`);
196
+ return injected;
197
+ }
198
+ function injectIfAbsent(env, key, value) {
199
+ if (env[key] !== void 0 && env[key] !== "") {
200
+ log3.debug(`Skipping "${key}" -- already set in environment`);
201
+ return false;
202
+ }
203
+ env[key] = value;
204
+ return true;
205
+ }
206
+ function toUpperSnake(key) {
207
+ return key.replace(/[.-]/g, "_").replace(/\[(\d+)\]/g, "_$1").replace(/_+/g, "_").replace(/^_|_$/g, "").toUpperCase();
208
+ }
209
+
210
+ // src/bootstrap.ts
211
+ var log4 = createLogger("bootstrap");
212
+ var _bootstrapped = false;
213
+ async function bootstrap(overrides) {
214
+ const config = { ...resolveConfig(), ...overrides };
215
+ if (!config.enabled) {
216
+ log4.info("Config client is disabled (SPRING_CLOUD_CONFIG_ENABLED != true). Skipping.");
217
+ return {
218
+ success: true,
219
+ propertiesInjected: 0,
220
+ sources: []
221
+ };
222
+ }
223
+ if (_bootstrapped) {
224
+ log4.warn("Bootstrap has already been called. Skipping duplicate invocation.");
225
+ return {
226
+ success: true,
227
+ propertiesInjected: 0,
228
+ sources: []
229
+ };
230
+ }
231
+ log4.info(
232
+ `Bootstrapping config for "${config.name}" [profile=${config.profile}, label=${config.label}] from ${config.uri}`
233
+ );
234
+ try {
235
+ const sources = await fetchConfig(config);
236
+ if (sources.length === 0) {
237
+ log4.warn("No property sources returned from config server.");
238
+ _bootstrapped = true;
239
+ return {
240
+ success: true,
241
+ propertiesInjected: 0,
242
+ sources: []
243
+ };
244
+ }
245
+ const propertiesInjected = mergeAndInject(sources);
246
+ _bootstrapped = true;
247
+ log4.info("Bootstrap complete.");
248
+ return {
249
+ success: true,
250
+ propertiesInjected,
251
+ sources: sources.map((s) => s.name)
252
+ };
253
+ } catch (err) {
254
+ const error = err instanceof Error ? err : new Error(String(err));
255
+ log4.error(`Bootstrap failed: ${error.message}`);
256
+ return {
257
+ success: false,
258
+ propertiesInjected: 0,
259
+ sources: [],
260
+ error
261
+ };
262
+ }
263
+ }
264
+ function _resetBootstrap() {
265
+ _bootstrapped = false;
266
+ }
267
+
268
+ export {
269
+ resolveConfig,
270
+ fetchConfig,
271
+ mergeAndInject,
272
+ toUpperSnake,
273
+ bootstrap,
274
+ _resetBootstrap
275
+ };
276
+ //# sourceMappingURL=chunk-E3W7NEIV.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config.ts","../src/logger.ts","../src/retry.ts","../src/client.ts","../src/merge.ts","../src/bootstrap.ts"],"sourcesContent":["import type { ClientConfig } from \"./types.js\";\n\n/**\n * Resolves the client configuration from environment variables.\n *\n * Uses the SPRING_CLOUD_CONFIG_* prefix to stay consistent with\n * Spring Boot conventions -- teams set the same env var names\n * regardless of whether the service is JVM or Node.\n */\nexport function resolveConfig(env: Record<string, string | undefined> = process.env): ClientConfig {\n const auth = env.SPRING_CLOUD_CONFIG_AUTH_USER\n ? {\n user: env.SPRING_CLOUD_CONFIG_AUTH_USER,\n pass: env.SPRING_CLOUD_CONFIG_AUTH_PASS ?? \"\",\n }\n : undefined;\n\n return {\n enabled: envBool(env, \"SPRING_CLOUD_CONFIG_ENABLED\", false),\n uri: env.SPRING_CLOUD_CONFIG_URI ?? \"http://localhost:8888\",\n name: env.SPRING_CLOUD_CONFIG_NAME ?? \"application\",\n profile: env.SPRING_CLOUD_CONFIG_PROFILE ?? \"default\",\n label: env.SPRING_CLOUD_CONFIG_LABEL ?? \"main\",\n failFast: envBool(env, \"SPRING_CLOUD_CONFIG_FAIL_FAST\", false),\n auth,\n retry: {\n enabled: envBool(env, \"SPRING_CLOUD_CONFIG_RETRY_ENABLED\", false),\n maxAttempts: envInt(env, \"SPRING_CLOUD_CONFIG_RETRY_MAX_ATTEMPTS\", 5),\n interval: envInt(env, \"SPRING_CLOUD_CONFIG_RETRY_INTERVAL\", 1000),\n multiplier: envFloat(env, \"SPRING_CLOUD_CONFIG_RETRY_MULTIPLIER\", 1.5),\n maxInterval: envInt(env, \"SPRING_CLOUD_CONFIG_RETRY_MAX_INTERVAL\", 30000),\n },\n headers: parseHeaders(env),\n requestTimeout: envInt(env, \"SPRING_CLOUD_CONFIG_REQUEST_TIMEOUT\", 10000),\n };\n}\n\n/**\n * Parses custom headers from SPRING_CLOUD_CONFIG_HEADERS_* env vars.\n *\n * Strips the prefix, replaces underscores with hyphens, and title-cases each segment.\n *\n * Examples:\n * SPRING_CLOUD_CONFIG_HEADERS_AUTHORIZATION=Bearer tok → { Authorization: \"Bearer tok\" }\n * SPRING_CLOUD_CONFIG_HEADERS_X_CUSTOM_HEADER=val → { \"X-Custom-Header\": \"val\" }\n */\nfunction parseHeaders(\n env: Record<string, string | undefined>\n): Record<string, string> | undefined {\n const prefix = \"SPRING_CLOUD_CONFIG_HEADERS_\";\n const headers: Record<string, string> = {};\n let hasHeaders = false;\n\n for (const [key, value] of Object.entries(env)) {\n if (key.startsWith(prefix) && value !== undefined && value !== \"\") {\n const headerName = key\n .substring(prefix.length)\n .split(\"_\")\n .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())\n .join(\"-\");\n headers[headerName] = value;\n hasHeaders = true;\n }\n }\n\n return hasHeaders ? headers : undefined;\n}\n\nfunction envBool(\n env: Record<string, string | undefined>,\n key: string,\n defaultValue: boolean\n): boolean {\n const val = env[key];\n if (val === undefined || val === \"\") return defaultValue;\n return val.toLowerCase() === \"true\";\n}\n\nfunction envInt(\n env: Record<string, string | undefined>,\n key: string,\n defaultValue: number\n): number {\n const val = env[key];\n if (val === undefined || val === \"\") return defaultValue;\n const parsed = parseInt(val, 10);\n return isNaN(parsed) ? defaultValue : parsed;\n}\n\nfunction envFloat(\n env: Record<string, string | undefined>,\n key: string,\n defaultValue: number\n): number {\n const val = env[key];\n if (val === undefined || val === \"\") return defaultValue;\n const parsed = parseFloat(val);\n return isNaN(parsed) ? defaultValue : parsed;\n}\n","/**\n * Minimal logger with a consistent prefix.\n * Keeps things simple -- no external logging dependency.\n * Respects SPRING_CLOUD_CONFIG_LOG_LEVEL env var (debug, info, warn, error).\n */\n\nconst LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3, silent: 4 } as const;\ntype LogLevel = keyof typeof LOG_LEVELS;\n\nfunction getLogLevel(): LogLevel {\n const level = (\n process.env.SPRING_CLOUD_CONFIG_LOG_LEVEL ?? \"info\"\n ).toLowerCase() as LogLevel;\n return level in LOG_LEVELS ? level : \"info\";\n}\n\nexport interface Logger {\n debug: (msg: string) => void;\n info: (msg: string) => void;\n warn: (msg: string) => void;\n error: (msg: string) => void;\n}\n\nexport function createLogger(scope: string): Logger {\n const prefix = `[@trueburn/spring-config-client:${scope}]`;\n\n const shouldLog = (level: LogLevel): boolean => {\n return LOG_LEVELS[level] >= LOG_LEVELS[getLogLevel()];\n };\n\n return {\n debug: (msg: string) => shouldLog(\"debug\") && console.debug(`${prefix} ${msg}`),\n info: (msg: string) => shouldLog(\"info\") && console.info(`${prefix} ${msg}`),\n warn: (msg: string) => shouldLog(\"warn\") && console.warn(`${prefix} ${msg}`),\n error: (msg: string) => shouldLog(\"error\") && console.error(`${prefix} ${msg}`),\n };\n}\n","import type { RetryConfig } from \"./types.js\";\nimport { createLogger } from \"./logger.js\";\n\nconst log = createLogger(\"retry\");\n\n/**\n * Executes an async function with exponential backoff retry.\n *\n * The interval doubles (x multiplier) after each attempt, capped at maxInterval.\n * If all attempts fail, the last error is thrown.\n */\nexport async function withRetry<T>(\n fn: () => Promise<T>,\n config: RetryConfig\n): Promise<T> {\n if (!config.enabled) {\n return fn();\n }\n\n let lastError: Error | undefined;\n let currentInterval = config.interval;\n\n for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {\n try {\n return await fn();\n } catch (err) {\n lastError = err instanceof Error ? err : new Error(String(err));\n\n if (attempt === config.maxAttempts) {\n log.error(\n `All ${config.maxAttempts} attempts failed. Last error: ${lastError.message}`\n );\n break;\n }\n\n log.warn(\n `Attempt ${attempt}/${config.maxAttempts} failed: ${lastError.message}. ` +\n `Retrying in ${currentInterval}ms...`\n );\n\n await sleep(currentInterval);\n currentInterval = Math.min(\n currentInterval * config.multiplier,\n config.maxInterval\n );\n }\n }\n\n throw lastError;\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","import type { ClientConfig, ConfigServerResponse, PropertySource } from \"./types.js\";\nimport { withRetry } from \"./retry.js\";\nimport { createLogger } from \"./logger.js\";\n\nconst log = createLogger(\"client\");\n\n/**\n * Fetches configuration from the Spring Cloud Config Server.\n *\n * Calls: GET {uri}/{name}/{profile}/{label}\n *\n * Returns the ordered list of property sources (most specific first).\n * If the server is unreachable and failFast is false, returns an empty array.\n */\nexport async function fetchConfig(config: ClientConfig): Promise<PropertySource[]> {\n const url = buildUrl(config);\n\n log.info(`Fetching config from ${url}`);\n\n const doFetch = async (): Promise<PropertySource[]> => {\n const headers: Record<string, string> = {\n Accept: \"application/json\",\n };\n\n if (config.auth) {\n const credentials = Buffer.from(\n `${config.auth.user}:${config.auth.pass}`\n ).toString(\"base64\");\n headers[\"Authorization\"] = `Basic ${credentials}`;\n }\n\n if (config.headers) {\n Object.assign(headers, config.headers);\n }\n\n const response = await fetch(url, {\n method: \"GET\",\n headers,\n signal: AbortSignal.timeout(config.requestTimeout),\n });\n\n if (!response.ok) {\n throw new Error(\n `Config server returned HTTP ${response.status}: ${response.statusText}`\n );\n }\n\n const data = (await response.json()) as ConfigServerResponse;\n\n log.info(\n `Received ${data.propertySources.length} property source(s) ` +\n `for ${data.name} [${data.profiles.join(\", \")}]` +\n (data.version ? ` @ ${data.version.substring(0, 8)}` : \"\")\n );\n\n if (log.debug) {\n for (const source of data.propertySources) {\n const keyCount = Object.keys(source.source).length;\n log.debug(` -> ${source.name} (${keyCount} properties)`);\n }\n }\n\n return data.propertySources;\n };\n\n try {\n return await withRetry(doFetch, config.retry);\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n\n if (config.failFast) {\n log.error(`Failed to fetch config (fail-fast enabled): ${error.message}`);\n throw error;\n }\n\n log.warn(\n `Failed to fetch config (fail-fast disabled, continuing without remote config): ${error.message}`\n );\n return [];\n }\n}\n\n/**\n * Builds the config server URL.\n *\n * For multi-profile support, each profile segment is encoded individually\n * while commas are preserved (Spring Cloud Config Server expects unencoded commas).\n */\nfunction buildUrl(config: ClientConfig): string {\n const base = config.uri.replace(/\\/+$/, \"\");\n const profileEncoded = config.profile\n .split(\",\")\n .map((p) => encodeURIComponent(p.trim()))\n .join(\",\");\n return `${base}/${encodeURIComponent(config.name)}/${profileEncoded}/${encodeURIComponent(config.label)}`;\n}\n","import type { PropertySource } from \"./types.js\";\nimport { createLogger } from \"./logger.js\";\n\nconst log = createLogger(\"merge\");\n\n/**\n * Merges property sources and injects them into process.env.\n *\n * Precedence (highest to lowest):\n * 1. Existing env vars (VSO secrets, K8s ConfigMaps, etc.) -- NEVER overwritten\n * 2. Most specific property source (first in the array from config server)\n * 3. Less specific property sources\n *\n * For each property, both formats are injected:\n * - Dot notation as-is: \"app.database.host\"\n * - UPPER_SNAKE_CASE: \"APP_DATABASE_HOST\"\n *\n * Returns the number of new properties injected.\n */\nexport function mergeAndInject(\n sources: PropertySource[],\n env: Record<string, string | undefined> = process.env\n): number {\n // Merge sources: most specific first -> iterate in order, first write wins\n const merged = new Map<string, string>();\n\n for (const source of sources) {\n for (const [key, value] of Object.entries(source.source)) {\n if (!merged.has(key)) {\n merged.set(key, String(value));\n }\n }\n }\n\n log.info(`Merged ${merged.size} unique properties from ${sources.length} source(s)`);\n\n let injected = 0;\n\n for (const [dotKey, value] of merged) {\n const snakeKey = toUpperSnake(dotKey);\n\n // Dot notation -- only if not already set\n if (injectIfAbsent(env, dotKey, value)) {\n injected++;\n }\n\n // UPPER_SNAKE -- only if different from dot key and not already set\n if (snakeKey !== dotKey) {\n if (injectIfAbsent(env, snakeKey, value)) {\n injected++;\n }\n }\n }\n\n log.info(`Injected ${injected} new entries into process.env`);\n\n return injected;\n}\n\n/**\n * Injects a value into the env object only if the key is not already set.\n * Returns true if the value was injected, false if it was skipped.\n */\nfunction injectIfAbsent(\n env: Record<string, string | undefined>,\n key: string,\n value: string\n): boolean {\n if (env[key] !== undefined && env[key] !== \"\") {\n log.debug(`Skipping \"${key}\" -- already set in environment`);\n return false;\n }\n env[key] = value;\n return true;\n}\n\n/**\n * Converts a dot-notation or kebab-case property key to UPPER_SNAKE_CASE.\n *\n * Examples:\n * \"app.database.host\" -> \"APP_DATABASE_HOST\"\n * \"app.feature.dark-mode\" -> \"APP_FEATURE_DARK_MODE\"\n * \"server.port\" -> \"SERVER_PORT\"\n * \"app.list[0].name\" -> \"APP_LIST_0_NAME\"\n */\nexport function toUpperSnake(key: string): string {\n return key\n .replace(/[.-]/g, \"_\") // dots and hyphens -> underscores\n .replace(/\\[(\\d+)\\]/g, \"_$1\") // array notation [0] -> _0\n .replace(/_+/g, \"_\") // collapse multiple underscores\n .replace(/^_|_$/g, \"\") // trim leading/trailing underscores\n .toUpperCase();\n}\n","import type { BootstrapResult, ClientConfig } from \"./types.js\";\nimport { resolveConfig } from \"./config.js\";\nimport { fetchConfig } from \"./client.js\";\nimport { mergeAndInject } from \"./merge.js\";\nimport { createLogger } from \"./logger.js\";\n\nconst log = createLogger(\"bootstrap\");\n\nlet _bootstrapped = false;\n\n/**\n * Bootstraps the application by fetching remote config from the\n * Spring Cloud Config Server and injecting it into process.env.\n *\n * This should be called as early as possible in the application lifecycle.\n * For Next.js, call it in `instrumentation.ts` inside the `register()` function.\n * For Express/Fastify, call it before starting the HTTP server.\n *\n * If SPRING_CLOUD_CONFIG_ENABLED is not \"true\", this is a no-op.\n *\n * Existing env vars (from VSO, K8s ConfigMaps, etc.) are never overwritten.\n *\n * @param overrides - Optional partial config to override env-based resolution (useful for testing)\n * @returns Result of the bootstrap process\n *\n * @example\n * ```typescript\n * // Next.js instrumentation.ts\n * export async function register() {\n * if (process.env.NEXT_RUNTIME === 'nodejs') {\n * const { bootstrap } = require('@trueburn/spring-config-client');\n * await bootstrap();\n * }\n * }\n * ```\n *\n * @example\n * ```typescript\n * // Express / Fastify\n * import { bootstrap } from '@trueburn/spring-config-client';\n *\n * async function main() {\n * await bootstrap();\n * // process.env now has remote config\n * const app = express();\n * app.listen(process.env.SERVER_PORT ?? 3000);\n * }\n * main();\n * ```\n */\nexport async function bootstrap(\n overrides?: Partial<ClientConfig>\n): Promise<BootstrapResult> {\n const config = { ...resolveConfig(), ...overrides };\n\n if (!config.enabled) {\n log.info(\"Config client is disabled (SPRING_CLOUD_CONFIG_ENABLED != true). Skipping.\");\n return {\n success: true,\n propertiesInjected: 0,\n sources: [],\n };\n }\n\n if (_bootstrapped) {\n log.warn(\"Bootstrap has already been called. Skipping duplicate invocation.\");\n return {\n success: true,\n propertiesInjected: 0,\n sources: [],\n };\n }\n\n log.info(\n `Bootstrapping config for \"${config.name}\" ` +\n `[profile=${config.profile}, label=${config.label}] ` +\n `from ${config.uri}`\n );\n\n try {\n const sources = await fetchConfig(config);\n\n if (sources.length === 0) {\n log.warn(\"No property sources returned from config server.\");\n _bootstrapped = true;\n return {\n success: true,\n propertiesInjected: 0,\n sources: [],\n };\n }\n\n const propertiesInjected = mergeAndInject(sources);\n _bootstrapped = true;\n\n log.info(\"Bootstrap complete.\");\n\n return {\n success: true,\n propertiesInjected,\n sources: sources.map((s) => s.name),\n };\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err));\n\n // If failFast is true, fetchConfig already threw -- this re-throws\n // If failFast is false, fetchConfig returned [] -- so we won't get here\n // But just in case of unexpected errors:\n log.error(`Bootstrap failed: ${error.message}`);\n\n return {\n success: false,\n propertiesInjected: 0,\n sources: [],\n error,\n };\n }\n}\n\n/**\n * Resets the bootstrap state. Only useful for testing.\n */\nexport function _resetBootstrap(): void {\n _bootstrapped = false;\n}\n"],"mappings":"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","names":["log","log","log"]}