@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 +21 -0
- package/README.md +306 -0
- package/dist/chunk-E3W7NEIV.mjs +276 -0
- package/dist/chunk-E3W7NEIV.mjs.map +1 -0
- package/dist/index.d.mts +95 -0
- package/dist/index.d.ts +95 -0
- package/dist/index.js +307 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +17 -0
- package/dist/index.mjs.map +1 -0
- package/dist/register.d.mts +24 -0
- package/dist/register.d.ts +24 -0
- package/dist/register.js +299 -0
- package/dist/register.js.map +1 -0
- package/dist/register.mjs +16 -0
- package/dist/register.mjs.map +1 -0
- package/dist/types-Cv_lvWI3.d.mts +99 -0
- package/dist/types-Cv_lvWI3.d.ts +99 -0
- package/package.json +66 -0
package/dist/register.js
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/register.ts
|
|
21
|
+
var register_exports = {};
|
|
22
|
+
__export(register_exports, {
|
|
23
|
+
default: () => register_default
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(register_exports);
|
|
26
|
+
|
|
27
|
+
// src/config.ts
|
|
28
|
+
function resolveConfig(env = process.env) {
|
|
29
|
+
const auth = env.SPRING_CLOUD_CONFIG_AUTH_USER ? {
|
|
30
|
+
user: env.SPRING_CLOUD_CONFIG_AUTH_USER,
|
|
31
|
+
pass: env.SPRING_CLOUD_CONFIG_AUTH_PASS ?? ""
|
|
32
|
+
} : void 0;
|
|
33
|
+
return {
|
|
34
|
+
enabled: envBool(env, "SPRING_CLOUD_CONFIG_ENABLED", false),
|
|
35
|
+
uri: env.SPRING_CLOUD_CONFIG_URI ?? "http://localhost:8888",
|
|
36
|
+
name: env.SPRING_CLOUD_CONFIG_NAME ?? "application",
|
|
37
|
+
profile: env.SPRING_CLOUD_CONFIG_PROFILE ?? "default",
|
|
38
|
+
label: env.SPRING_CLOUD_CONFIG_LABEL ?? "main",
|
|
39
|
+
failFast: envBool(env, "SPRING_CLOUD_CONFIG_FAIL_FAST", false),
|
|
40
|
+
auth,
|
|
41
|
+
retry: {
|
|
42
|
+
enabled: envBool(env, "SPRING_CLOUD_CONFIG_RETRY_ENABLED", false),
|
|
43
|
+
maxAttempts: envInt(env, "SPRING_CLOUD_CONFIG_RETRY_MAX_ATTEMPTS", 5),
|
|
44
|
+
interval: envInt(env, "SPRING_CLOUD_CONFIG_RETRY_INTERVAL", 1e3),
|
|
45
|
+
multiplier: envFloat(env, "SPRING_CLOUD_CONFIG_RETRY_MULTIPLIER", 1.5),
|
|
46
|
+
maxInterval: envInt(env, "SPRING_CLOUD_CONFIG_RETRY_MAX_INTERVAL", 3e4)
|
|
47
|
+
},
|
|
48
|
+
headers: parseHeaders(env),
|
|
49
|
+
requestTimeout: envInt(env, "SPRING_CLOUD_CONFIG_REQUEST_TIMEOUT", 1e4)
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function parseHeaders(env) {
|
|
53
|
+
const prefix = "SPRING_CLOUD_CONFIG_HEADERS_";
|
|
54
|
+
const headers = {};
|
|
55
|
+
let hasHeaders = false;
|
|
56
|
+
for (const [key, value] of Object.entries(env)) {
|
|
57
|
+
if (key.startsWith(prefix) && value !== void 0 && value !== "") {
|
|
58
|
+
const headerName = key.substring(prefix.length).split("_").map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase()).join("-");
|
|
59
|
+
headers[headerName] = value;
|
|
60
|
+
hasHeaders = true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return hasHeaders ? headers : void 0;
|
|
64
|
+
}
|
|
65
|
+
function envBool(env, key, defaultValue) {
|
|
66
|
+
const val = env[key];
|
|
67
|
+
if (val === void 0 || val === "") return defaultValue;
|
|
68
|
+
return val.toLowerCase() === "true";
|
|
69
|
+
}
|
|
70
|
+
function envInt(env, key, defaultValue) {
|
|
71
|
+
const val = env[key];
|
|
72
|
+
if (val === void 0 || val === "") return defaultValue;
|
|
73
|
+
const parsed = parseInt(val, 10);
|
|
74
|
+
return isNaN(parsed) ? defaultValue : parsed;
|
|
75
|
+
}
|
|
76
|
+
function envFloat(env, key, defaultValue) {
|
|
77
|
+
const val = env[key];
|
|
78
|
+
if (val === void 0 || val === "") return defaultValue;
|
|
79
|
+
const parsed = parseFloat(val);
|
|
80
|
+
return isNaN(parsed) ? defaultValue : parsed;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/logger.ts
|
|
84
|
+
var LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3, silent: 4 };
|
|
85
|
+
function getLogLevel() {
|
|
86
|
+
const level = (process.env.SPRING_CLOUD_CONFIG_LOG_LEVEL ?? "info").toLowerCase();
|
|
87
|
+
return level in LOG_LEVELS ? level : "info";
|
|
88
|
+
}
|
|
89
|
+
function createLogger(scope) {
|
|
90
|
+
const prefix = `[@trueburn/spring-config-client:${scope}]`;
|
|
91
|
+
const shouldLog = (level) => {
|
|
92
|
+
return LOG_LEVELS[level] >= LOG_LEVELS[getLogLevel()];
|
|
93
|
+
};
|
|
94
|
+
return {
|
|
95
|
+
debug: (msg) => shouldLog("debug") && console.debug(`${prefix} ${msg}`),
|
|
96
|
+
info: (msg) => shouldLog("info") && console.info(`${prefix} ${msg}`),
|
|
97
|
+
warn: (msg) => shouldLog("warn") && console.warn(`${prefix} ${msg}`),
|
|
98
|
+
error: (msg) => shouldLog("error") && console.error(`${prefix} ${msg}`)
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/retry.ts
|
|
103
|
+
var log = createLogger("retry");
|
|
104
|
+
async function withRetry(fn, config) {
|
|
105
|
+
if (!config.enabled) {
|
|
106
|
+
return fn();
|
|
107
|
+
}
|
|
108
|
+
let lastError;
|
|
109
|
+
let currentInterval = config.interval;
|
|
110
|
+
for (let attempt = 1; attempt <= config.maxAttempts; attempt++) {
|
|
111
|
+
try {
|
|
112
|
+
return await fn();
|
|
113
|
+
} catch (err) {
|
|
114
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
115
|
+
if (attempt === config.maxAttempts) {
|
|
116
|
+
log.error(
|
|
117
|
+
`All ${config.maxAttempts} attempts failed. Last error: ${lastError.message}`
|
|
118
|
+
);
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
log.warn(
|
|
122
|
+
`Attempt ${attempt}/${config.maxAttempts} failed: ${lastError.message}. Retrying in ${currentInterval}ms...`
|
|
123
|
+
);
|
|
124
|
+
await sleep(currentInterval);
|
|
125
|
+
currentInterval = Math.min(
|
|
126
|
+
currentInterval * config.multiplier,
|
|
127
|
+
config.maxInterval
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
throw lastError;
|
|
132
|
+
}
|
|
133
|
+
function sleep(ms) {
|
|
134
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/client.ts
|
|
138
|
+
var log2 = createLogger("client");
|
|
139
|
+
async function fetchConfig(config) {
|
|
140
|
+
const url = buildUrl(config);
|
|
141
|
+
log2.info(`Fetching config from ${url}`);
|
|
142
|
+
const doFetch = async () => {
|
|
143
|
+
const headers = {
|
|
144
|
+
Accept: "application/json"
|
|
145
|
+
};
|
|
146
|
+
if (config.auth) {
|
|
147
|
+
const credentials = Buffer.from(
|
|
148
|
+
`${config.auth.user}:${config.auth.pass}`
|
|
149
|
+
).toString("base64");
|
|
150
|
+
headers["Authorization"] = `Basic ${credentials}`;
|
|
151
|
+
}
|
|
152
|
+
if (config.headers) {
|
|
153
|
+
Object.assign(headers, config.headers);
|
|
154
|
+
}
|
|
155
|
+
const response = await fetch(url, {
|
|
156
|
+
method: "GET",
|
|
157
|
+
headers,
|
|
158
|
+
signal: AbortSignal.timeout(config.requestTimeout)
|
|
159
|
+
});
|
|
160
|
+
if (!response.ok) {
|
|
161
|
+
throw new Error(
|
|
162
|
+
`Config server returned HTTP ${response.status}: ${response.statusText}`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
const data = await response.json();
|
|
166
|
+
log2.info(
|
|
167
|
+
`Received ${data.propertySources.length} property source(s) for ${data.name} [${data.profiles.join(", ")}]` + (data.version ? ` @ ${data.version.substring(0, 8)}` : "")
|
|
168
|
+
);
|
|
169
|
+
if (log2.debug) {
|
|
170
|
+
for (const source of data.propertySources) {
|
|
171
|
+
const keyCount = Object.keys(source.source).length;
|
|
172
|
+
log2.debug(` -> ${source.name} (${keyCount} properties)`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return data.propertySources;
|
|
176
|
+
};
|
|
177
|
+
try {
|
|
178
|
+
return await withRetry(doFetch, config.retry);
|
|
179
|
+
} catch (err) {
|
|
180
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
181
|
+
if (config.failFast) {
|
|
182
|
+
log2.error(`Failed to fetch config (fail-fast enabled): ${error.message}`);
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
log2.warn(
|
|
186
|
+
`Failed to fetch config (fail-fast disabled, continuing without remote config): ${error.message}`
|
|
187
|
+
);
|
|
188
|
+
return [];
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
function buildUrl(config) {
|
|
192
|
+
const base = config.uri.replace(/\/+$/, "");
|
|
193
|
+
const profileEncoded = config.profile.split(",").map((p) => encodeURIComponent(p.trim())).join(",");
|
|
194
|
+
return `${base}/${encodeURIComponent(config.name)}/${profileEncoded}/${encodeURIComponent(config.label)}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// src/merge.ts
|
|
198
|
+
var log3 = createLogger("merge");
|
|
199
|
+
function mergeAndInject(sources, env = process.env) {
|
|
200
|
+
const merged = /* @__PURE__ */ new Map();
|
|
201
|
+
for (const source of sources) {
|
|
202
|
+
for (const [key, value] of Object.entries(source.source)) {
|
|
203
|
+
if (!merged.has(key)) {
|
|
204
|
+
merged.set(key, String(value));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
log3.info(`Merged ${merged.size} unique properties from ${sources.length} source(s)`);
|
|
209
|
+
let injected = 0;
|
|
210
|
+
for (const [dotKey, value] of merged) {
|
|
211
|
+
const snakeKey = toUpperSnake(dotKey);
|
|
212
|
+
if (injectIfAbsent(env, dotKey, value)) {
|
|
213
|
+
injected++;
|
|
214
|
+
}
|
|
215
|
+
if (snakeKey !== dotKey) {
|
|
216
|
+
if (injectIfAbsent(env, snakeKey, value)) {
|
|
217
|
+
injected++;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
log3.info(`Injected ${injected} new entries into process.env`);
|
|
222
|
+
return injected;
|
|
223
|
+
}
|
|
224
|
+
function injectIfAbsent(env, key, value) {
|
|
225
|
+
if (env[key] !== void 0 && env[key] !== "") {
|
|
226
|
+
log3.debug(`Skipping "${key}" -- already set in environment`);
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
env[key] = value;
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
function toUpperSnake(key) {
|
|
233
|
+
return key.replace(/[.-]/g, "_").replace(/\[(\d+)\]/g, "_$1").replace(/_+/g, "_").replace(/^_|_$/g, "").toUpperCase();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// src/bootstrap.ts
|
|
237
|
+
var log4 = createLogger("bootstrap");
|
|
238
|
+
var _bootstrapped = false;
|
|
239
|
+
async function bootstrap(overrides) {
|
|
240
|
+
const config = { ...resolveConfig(), ...overrides };
|
|
241
|
+
if (!config.enabled) {
|
|
242
|
+
log4.info("Config client is disabled (SPRING_CLOUD_CONFIG_ENABLED != true). Skipping.");
|
|
243
|
+
return {
|
|
244
|
+
success: true,
|
|
245
|
+
propertiesInjected: 0,
|
|
246
|
+
sources: []
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
if (_bootstrapped) {
|
|
250
|
+
log4.warn("Bootstrap has already been called. Skipping duplicate invocation.");
|
|
251
|
+
return {
|
|
252
|
+
success: true,
|
|
253
|
+
propertiesInjected: 0,
|
|
254
|
+
sources: []
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
log4.info(
|
|
258
|
+
`Bootstrapping config for "${config.name}" [profile=${config.profile}, label=${config.label}] from ${config.uri}`
|
|
259
|
+
);
|
|
260
|
+
try {
|
|
261
|
+
const sources = await fetchConfig(config);
|
|
262
|
+
if (sources.length === 0) {
|
|
263
|
+
log4.warn("No property sources returned from config server.");
|
|
264
|
+
_bootstrapped = true;
|
|
265
|
+
return {
|
|
266
|
+
success: true,
|
|
267
|
+
propertiesInjected: 0,
|
|
268
|
+
sources: []
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
const propertiesInjected = mergeAndInject(sources);
|
|
272
|
+
_bootstrapped = true;
|
|
273
|
+
log4.info("Bootstrap complete.");
|
|
274
|
+
return {
|
|
275
|
+
success: true,
|
|
276
|
+
propertiesInjected,
|
|
277
|
+
sources: sources.map((s) => s.name)
|
|
278
|
+
};
|
|
279
|
+
} catch (err) {
|
|
280
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
281
|
+
log4.error(`Bootstrap failed: ${error.message}`);
|
|
282
|
+
return {
|
|
283
|
+
success: false,
|
|
284
|
+
propertiesInjected: 0,
|
|
285
|
+
sources: [],
|
|
286
|
+
error
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/register.ts
|
|
292
|
+
var promise = bootstrap().then((result) => {
|
|
293
|
+
if (!result.success && result.error) {
|
|
294
|
+
throw result.error;
|
|
295
|
+
}
|
|
296
|
+
return result;
|
|
297
|
+
});
|
|
298
|
+
var register_default = promise;
|
|
299
|
+
//# sourceMappingURL=register.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/register.ts","../src/config.ts","../src/logger.ts","../src/retry.ts","../src/client.ts","../src/merge.ts","../src/bootstrap.ts"],"sourcesContent":["/**\n * Auto-bootstrap entry point for Next.js instrumentation.\n *\n * Usage in your Next.js project's instrumentation.ts:\n *\n * ```typescript\n * export async function register() {\n * if (process.env.NEXT_RUNTIME === 'nodejs') {\n * await import('@trueburn/spring-config-client/register');\n * }\n * }\n * ```\n *\n * This module calls bootstrap() on import, so simply importing it\n * is enough to load remote config into process.env.\n *\n * The default export is a promise that resolves when bootstrap completes,\n * which allows `await import(...)` to wait for the config to be loaded.\n */\n\nimport { bootstrap } from \"./bootstrap.js\";\n\nconst promise = bootstrap().then((result) => {\n if (!result.success && result.error) {\n throw result.error;\n }\n return result;\n});\n\nexport default promise;\n","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"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
bootstrap
|
|
3
|
+
} from "./chunk-E3W7NEIV.mjs";
|
|
4
|
+
|
|
5
|
+
// src/register.ts
|
|
6
|
+
var promise = bootstrap().then((result) => {
|
|
7
|
+
if (!result.success && result.error) {
|
|
8
|
+
throw result.error;
|
|
9
|
+
}
|
|
10
|
+
return result;
|
|
11
|
+
});
|
|
12
|
+
var register_default = promise;
|
|
13
|
+
export {
|
|
14
|
+
register_default as default
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=register.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/register.ts"],"sourcesContent":["/**\n * Auto-bootstrap entry point for Next.js instrumentation.\n *\n * Usage in your Next.js project's instrumentation.ts:\n *\n * ```typescript\n * export async function register() {\n * if (process.env.NEXT_RUNTIME === 'nodejs') {\n * await import('@trueburn/spring-config-client/register');\n * }\n * }\n * ```\n *\n * This module calls bootstrap() on import, so simply importing it\n * is enough to load remote config into process.env.\n *\n * The default export is a promise that resolves when bootstrap completes,\n * which allows `await import(...)` to wait for the config to be loaded.\n */\n\nimport { bootstrap } from \"./bootstrap.js\";\n\nconst promise = bootstrap().then((result) => {\n if (!result.success && result.error) {\n throw result.error;\n }\n return result;\n});\n\nexport default promise;\n"],"mappings":";;;;;AAsBA,IAAM,UAAU,UAAU,EAAE,KAAK,CAAC,WAAW;AAC3C,MAAI,CAAC,OAAO,WAAW,OAAO,OAAO;AACnC,UAAM,OAAO;AAAA,EACf;AACA,SAAO;AACT,CAAC;AAED,IAAO,mBAAQ;","names":[]}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration options for the config client, resolved from environment variables.
|
|
3
|
+
*
|
|
4
|
+
* Environment variable mapping:
|
|
5
|
+
* SPRING_CLOUD_CONFIG_ENABLED -- Enable/disable the client (default: false)
|
|
6
|
+
* SPRING_CLOUD_CONFIG_URI -- Config server base URL (default: http://localhost:8888)
|
|
7
|
+
* SPRING_CLOUD_CONFIG_NAME -- Application name (default: application)
|
|
8
|
+
* SPRING_CLOUD_CONFIG_PROFILE -- Active profile(s), comma-separated (default: default)
|
|
9
|
+
* SPRING_CLOUD_CONFIG_LABEL -- Git branch/label (default: main)
|
|
10
|
+
* SPRING_CLOUD_CONFIG_FAIL_FAST -- Throw on fetch failure (default: false)
|
|
11
|
+
* SPRING_CLOUD_CONFIG_AUTH_USER -- Basic auth username
|
|
12
|
+
* SPRING_CLOUD_CONFIG_AUTH_PASS -- Basic auth password
|
|
13
|
+
* SPRING_CLOUD_CONFIG_RETRY_ENABLED -- Enable retry with backoff (default: false)
|
|
14
|
+
* SPRING_CLOUD_CONFIG_RETRY_MAX_ATTEMPTS -- Max retry attempts (default: 5)
|
|
15
|
+
* SPRING_CLOUD_CONFIG_RETRY_INTERVAL -- Initial retry interval in ms (default: 1000)
|
|
16
|
+
* SPRING_CLOUD_CONFIG_RETRY_MULTIPLIER -- Backoff multiplier (default: 1.5)
|
|
17
|
+
* SPRING_CLOUD_CONFIG_RETRY_MAX_INTERVAL -- Max retry interval in ms (default: 30000)
|
|
18
|
+
* SPRING_CLOUD_CONFIG_REQUEST_TIMEOUT -- Request timeout in ms (default: 10000)
|
|
19
|
+
* SPRING_CLOUD_CONFIG_HEADERS_<NAME> -- Custom headers (e.g. SPRING_CLOUD_CONFIG_HEADERS_AUTHORIZATION)
|
|
20
|
+
*/
|
|
21
|
+
interface ClientConfig {
|
|
22
|
+
/** Whether the config client is enabled */
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
/** Config server base URL */
|
|
25
|
+
uri: string;
|
|
26
|
+
/** Application name used to resolve config on the server */
|
|
27
|
+
name: string;
|
|
28
|
+
/** Active profile(s), comma-separated (e.g. "production" or "production,eu-west") */
|
|
29
|
+
profile: string;
|
|
30
|
+
/** Git branch or label */
|
|
31
|
+
label: string;
|
|
32
|
+
/** If true, throw an error when the config server is unreachable */
|
|
33
|
+
failFast: boolean;
|
|
34
|
+
/** Basic auth credentials */
|
|
35
|
+
auth?: {
|
|
36
|
+
user: string;
|
|
37
|
+
pass: string;
|
|
38
|
+
};
|
|
39
|
+
/** Retry configuration */
|
|
40
|
+
retry: RetryConfig;
|
|
41
|
+
/** Custom headers to send with the config server request */
|
|
42
|
+
headers?: Record<string, string>;
|
|
43
|
+
/** Request timeout in milliseconds (default: 10000) */
|
|
44
|
+
requestTimeout: number;
|
|
45
|
+
}
|
|
46
|
+
interface RetryConfig {
|
|
47
|
+
/** Whether retry is enabled */
|
|
48
|
+
enabled: boolean;
|
|
49
|
+
/** Maximum number of retry attempts */
|
|
50
|
+
maxAttempts: number;
|
|
51
|
+
/** Initial interval between retries in ms */
|
|
52
|
+
interval: number;
|
|
53
|
+
/** Multiplier applied to interval after each attempt */
|
|
54
|
+
multiplier: number;
|
|
55
|
+
/** Maximum interval between retries in ms */
|
|
56
|
+
maxInterval: number;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A single property source returned by the Spring Cloud Config Server.
|
|
60
|
+
* Each source represents a config file (e.g. my-app-production.yml).
|
|
61
|
+
*/
|
|
62
|
+
interface PropertySource {
|
|
63
|
+
/** Source identifier (usually the file path in the git repo) */
|
|
64
|
+
name: string;
|
|
65
|
+
/** Flat key-value map of resolved properties (dot-notation keys) */
|
|
66
|
+
source: Record<string, string | number | boolean>;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The full response from the Config Server's /{name}/{profile}/{label} endpoint.
|
|
70
|
+
*/
|
|
71
|
+
interface ConfigServerResponse {
|
|
72
|
+
/** Application name */
|
|
73
|
+
name: string;
|
|
74
|
+
/** Active profiles */
|
|
75
|
+
profiles: string[];
|
|
76
|
+
/** Git label */
|
|
77
|
+
label: string;
|
|
78
|
+
/** Git version/commit hash */
|
|
79
|
+
version?: string;
|
|
80
|
+
/** State identifier */
|
|
81
|
+
state?: string;
|
|
82
|
+
/** Ordered property sources (most specific first) */
|
|
83
|
+
propertySources: PropertySource[];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Result of the bootstrap process.
|
|
87
|
+
*/
|
|
88
|
+
interface BootstrapResult {
|
|
89
|
+
/** Whether config was fetched successfully */
|
|
90
|
+
success: boolean;
|
|
91
|
+
/** Number of properties injected into process.env */
|
|
92
|
+
propertiesInjected: number;
|
|
93
|
+
/** Property sources that were loaded (names only) */
|
|
94
|
+
sources: string[];
|
|
95
|
+
/** Error if the bootstrap failed and failFast is false */
|
|
96
|
+
error?: Error;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type { BootstrapResult as B, ClientConfig as C, PropertySource as P, RetryConfig as R, ConfigServerResponse as a };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration options for the config client, resolved from environment variables.
|
|
3
|
+
*
|
|
4
|
+
* Environment variable mapping:
|
|
5
|
+
* SPRING_CLOUD_CONFIG_ENABLED -- Enable/disable the client (default: false)
|
|
6
|
+
* SPRING_CLOUD_CONFIG_URI -- Config server base URL (default: http://localhost:8888)
|
|
7
|
+
* SPRING_CLOUD_CONFIG_NAME -- Application name (default: application)
|
|
8
|
+
* SPRING_CLOUD_CONFIG_PROFILE -- Active profile(s), comma-separated (default: default)
|
|
9
|
+
* SPRING_CLOUD_CONFIG_LABEL -- Git branch/label (default: main)
|
|
10
|
+
* SPRING_CLOUD_CONFIG_FAIL_FAST -- Throw on fetch failure (default: false)
|
|
11
|
+
* SPRING_CLOUD_CONFIG_AUTH_USER -- Basic auth username
|
|
12
|
+
* SPRING_CLOUD_CONFIG_AUTH_PASS -- Basic auth password
|
|
13
|
+
* SPRING_CLOUD_CONFIG_RETRY_ENABLED -- Enable retry with backoff (default: false)
|
|
14
|
+
* SPRING_CLOUD_CONFIG_RETRY_MAX_ATTEMPTS -- Max retry attempts (default: 5)
|
|
15
|
+
* SPRING_CLOUD_CONFIG_RETRY_INTERVAL -- Initial retry interval in ms (default: 1000)
|
|
16
|
+
* SPRING_CLOUD_CONFIG_RETRY_MULTIPLIER -- Backoff multiplier (default: 1.5)
|
|
17
|
+
* SPRING_CLOUD_CONFIG_RETRY_MAX_INTERVAL -- Max retry interval in ms (default: 30000)
|
|
18
|
+
* SPRING_CLOUD_CONFIG_REQUEST_TIMEOUT -- Request timeout in ms (default: 10000)
|
|
19
|
+
* SPRING_CLOUD_CONFIG_HEADERS_<NAME> -- Custom headers (e.g. SPRING_CLOUD_CONFIG_HEADERS_AUTHORIZATION)
|
|
20
|
+
*/
|
|
21
|
+
interface ClientConfig {
|
|
22
|
+
/** Whether the config client is enabled */
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
/** Config server base URL */
|
|
25
|
+
uri: string;
|
|
26
|
+
/** Application name used to resolve config on the server */
|
|
27
|
+
name: string;
|
|
28
|
+
/** Active profile(s), comma-separated (e.g. "production" or "production,eu-west") */
|
|
29
|
+
profile: string;
|
|
30
|
+
/** Git branch or label */
|
|
31
|
+
label: string;
|
|
32
|
+
/** If true, throw an error when the config server is unreachable */
|
|
33
|
+
failFast: boolean;
|
|
34
|
+
/** Basic auth credentials */
|
|
35
|
+
auth?: {
|
|
36
|
+
user: string;
|
|
37
|
+
pass: string;
|
|
38
|
+
};
|
|
39
|
+
/** Retry configuration */
|
|
40
|
+
retry: RetryConfig;
|
|
41
|
+
/** Custom headers to send with the config server request */
|
|
42
|
+
headers?: Record<string, string>;
|
|
43
|
+
/** Request timeout in milliseconds (default: 10000) */
|
|
44
|
+
requestTimeout: number;
|
|
45
|
+
}
|
|
46
|
+
interface RetryConfig {
|
|
47
|
+
/** Whether retry is enabled */
|
|
48
|
+
enabled: boolean;
|
|
49
|
+
/** Maximum number of retry attempts */
|
|
50
|
+
maxAttempts: number;
|
|
51
|
+
/** Initial interval between retries in ms */
|
|
52
|
+
interval: number;
|
|
53
|
+
/** Multiplier applied to interval after each attempt */
|
|
54
|
+
multiplier: number;
|
|
55
|
+
/** Maximum interval between retries in ms */
|
|
56
|
+
maxInterval: number;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A single property source returned by the Spring Cloud Config Server.
|
|
60
|
+
* Each source represents a config file (e.g. my-app-production.yml).
|
|
61
|
+
*/
|
|
62
|
+
interface PropertySource {
|
|
63
|
+
/** Source identifier (usually the file path in the git repo) */
|
|
64
|
+
name: string;
|
|
65
|
+
/** Flat key-value map of resolved properties (dot-notation keys) */
|
|
66
|
+
source: Record<string, string | number | boolean>;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The full response from the Config Server's /{name}/{profile}/{label} endpoint.
|
|
70
|
+
*/
|
|
71
|
+
interface ConfigServerResponse {
|
|
72
|
+
/** Application name */
|
|
73
|
+
name: string;
|
|
74
|
+
/** Active profiles */
|
|
75
|
+
profiles: string[];
|
|
76
|
+
/** Git label */
|
|
77
|
+
label: string;
|
|
78
|
+
/** Git version/commit hash */
|
|
79
|
+
version?: string;
|
|
80
|
+
/** State identifier */
|
|
81
|
+
state?: string;
|
|
82
|
+
/** Ordered property sources (most specific first) */
|
|
83
|
+
propertySources: PropertySource[];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Result of the bootstrap process.
|
|
87
|
+
*/
|
|
88
|
+
interface BootstrapResult {
|
|
89
|
+
/** Whether config was fetched successfully */
|
|
90
|
+
success: boolean;
|
|
91
|
+
/** Number of properties injected into process.env */
|
|
92
|
+
propertiesInjected: number;
|
|
93
|
+
/** Property sources that were loaded (names only) */
|
|
94
|
+
sources: string[];
|
|
95
|
+
/** Error if the bootstrap failed and failFast is false */
|
|
96
|
+
error?: Error;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export type { BootstrapResult as B, ClientConfig as C, PropertySource as P, RetryConfig as R, ConfigServerResponse as a };
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@trueburn/spring-config-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Spring Cloud Config Server client for Node.js/Next.js -- bootstraps remote configuration into process.env at startup",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/TrueBurn/node-spring-config-client.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/TrueBurn/node-spring-config-client/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/TrueBurn/node-spring-config-client#readme",
|
|
16
|
+
"author": "trueburn",
|
|
17
|
+
"main": "dist/index.js",
|
|
18
|
+
"types": "dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.mjs",
|
|
23
|
+
"require": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./register": {
|
|
26
|
+
"types": "./dist/register.d.ts",
|
|
27
|
+
"import": "./dist/register.mjs",
|
|
28
|
+
"require": "./dist/register.js"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"dist"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsup",
|
|
36
|
+
"test": "vitest run",
|
|
37
|
+
"test:watch": "vitest",
|
|
38
|
+
"lint": "eslint src/",
|
|
39
|
+
"format": "prettier --write 'src/**/*.ts'",
|
|
40
|
+
"typecheck": "tsc --noEmit",
|
|
41
|
+
"prepublishOnly": "npm run build"
|
|
42
|
+
},
|
|
43
|
+
"keywords": [
|
|
44
|
+
"spring-cloud-config",
|
|
45
|
+
"config-server",
|
|
46
|
+
"nextjs",
|
|
47
|
+
"nodejs",
|
|
48
|
+
"configuration",
|
|
49
|
+
"bootstrap",
|
|
50
|
+
"env"
|
|
51
|
+
],
|
|
52
|
+
"license": "MIT",
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/node": "^20.11.0",
|
|
55
|
+
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
|
56
|
+
"@typescript-eslint/parser": "^7.0.0",
|
|
57
|
+
"eslint": "^8.56.0",
|
|
58
|
+
"prettier": "^3.2.0",
|
|
59
|
+
"tsup": "^8.0.0",
|
|
60
|
+
"typescript": "^5.3.0",
|
|
61
|
+
"vitest": "^1.2.0"
|
|
62
|
+
},
|
|
63
|
+
"engines": {
|
|
64
|
+
"node": ">=18.0.0"
|
|
65
|
+
}
|
|
66
|
+
}
|