@morojs/moro 1.5.4 → 1.5.6

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.
Files changed (70) hide show
  1. package/dist/core/config/config-manager.d.ts +44 -0
  2. package/dist/core/config/config-manager.js +114 -0
  3. package/dist/core/config/config-manager.js.map +1 -0
  4. package/dist/core/config/config-sources.d.ts +21 -0
  5. package/dist/core/config/config-sources.js +314 -0
  6. package/dist/core/config/config-sources.js.map +1 -0
  7. package/dist/core/config/config-validator.d.ts +21 -0
  8. package/dist/core/config/config-validator.js +737 -0
  9. package/dist/core/config/config-validator.js.map +1 -0
  10. package/dist/core/config/file-loader.d.ts +0 -5
  11. package/dist/core/config/file-loader.js +0 -171
  12. package/dist/core/config/file-loader.js.map +1 -1
  13. package/dist/core/config/index.d.ts +39 -10
  14. package/dist/core/config/index.js +66 -29
  15. package/dist/core/config/index.js.map +1 -1
  16. package/dist/core/config/schema.js +22 -31
  17. package/dist/core/config/schema.js.map +1 -1
  18. package/dist/core/config/utils.d.ts +9 -2
  19. package/dist/core/config/utils.js +19 -32
  20. package/dist/core/config/utils.js.map +1 -1
  21. package/dist/core/framework.d.ts +2 -7
  22. package/dist/core/framework.js +12 -5
  23. package/dist/core/framework.js.map +1 -1
  24. package/dist/core/http/http-server.d.ts +12 -0
  25. package/dist/core/http/http-server.js +56 -0
  26. package/dist/core/http/http-server.js.map +1 -1
  27. package/dist/core/http/router.d.ts +12 -0
  28. package/dist/core/http/router.js +114 -36
  29. package/dist/core/http/router.js.map +1 -1
  30. package/dist/core/logger/index.d.ts +1 -1
  31. package/dist/core/logger/index.js +2 -1
  32. package/dist/core/logger/index.js.map +1 -1
  33. package/dist/core/logger/logger.d.ts +10 -1
  34. package/dist/core/logger/logger.js +99 -37
  35. package/dist/core/logger/logger.js.map +1 -1
  36. package/dist/core/routing/index.d.ts +20 -0
  37. package/dist/core/routing/index.js +109 -11
  38. package/dist/core/routing/index.js.map +1 -1
  39. package/dist/moro.d.ts +22 -0
  40. package/dist/moro.js +134 -98
  41. package/dist/moro.js.map +1 -1
  42. package/dist/types/config.d.ts +39 -2
  43. package/dist/types/core.d.ts +22 -39
  44. package/dist/types/logger.d.ts +4 -0
  45. package/package.json +1 -1
  46. package/src/core/config/config-manager.ts +133 -0
  47. package/src/core/config/config-sources.ts +384 -0
  48. package/src/core/config/config-validator.ts +1035 -0
  49. package/src/core/config/file-loader.ts +0 -233
  50. package/src/core/config/index.ts +77 -32
  51. package/src/core/config/schema.ts +22 -31
  52. package/src/core/config/utils.ts +22 -29
  53. package/src/core/framework.ts +18 -11
  54. package/src/core/http/http-server.ts +66 -0
  55. package/src/core/http/router.ts +127 -38
  56. package/src/core/logger/index.ts +1 -0
  57. package/src/core/logger/logger.ts +109 -36
  58. package/src/core/routing/index.ts +116 -12
  59. package/src/moro.ts +159 -107
  60. package/src/types/config.ts +40 -2
  61. package/src/types/core.ts +32 -43
  62. package/src/types/logger.ts +6 -0
  63. package/dist/core/config/loader.d.ts +0 -7
  64. package/dist/core/config/loader.js +0 -269
  65. package/dist/core/config/loader.js.map +0 -1
  66. package/dist/core/config/validation.d.ts +0 -17
  67. package/dist/core/config/validation.js +0 -131
  68. package/dist/core/config/validation.js.map +0 -1
  69. package/src/core/config/loader.ts +0 -633
  70. package/src/core/config/validation.ts +0 -140
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morojs/moro",
3
- "version": "1.5.4",
3
+ "version": "1.5.6",
4
4
  "description": "High-performance Node.js framework with intelligent routing, automatic middleware ordering, enterprise authentication (Auth.js), type-safe Zod validation, and functional architecture",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Configuration Manager - Immutable Single Source of Truth
3
+ *
4
+ * This module provides centralized, immutable configuration state management.
5
+ * Configuration is locked at createApp() initialization and cannot be changed afterward.
6
+ *
7
+ * Precedence: Environment Variables > createApp Options > Config File > Defaults
8
+ */
9
+
10
+ import { AppConfig } from '../../types/config';
11
+ import { createFrameworkLogger } from '../logger';
12
+
13
+ const logger = createFrameworkLogger('ConfigManager');
14
+
15
+ /**
16
+ * Global configuration state - immutable after initialization
17
+ */
18
+ let globalConfig: Readonly<AppConfig> | null = null;
19
+ let isLocked = false;
20
+
21
+ /**
22
+ * Initialize and lock global configuration state
23
+ * This should only be called once during createApp() initialization
24
+ */
25
+ export function initializeAndLockConfig(config: AppConfig): void {
26
+ if (isLocked) {
27
+ throw new Error(
28
+ 'Configuration is already locked and cannot be changed. ' +
29
+ 'Configuration can only be set once during createApp() initialization.'
30
+ );
31
+ }
32
+
33
+ // Deep freeze the configuration to make it truly immutable
34
+ globalConfig = deepFreeze(config);
35
+ isLocked = true;
36
+
37
+ logger.info(
38
+ `Configuration locked and initialized: ${process.env.NODE_ENV || 'development'}:${config.server.port}`
39
+ );
40
+ }
41
+
42
+ /**
43
+ * Get the current global configuration
44
+ * Throws if configuration hasn't been initialized
45
+ */
46
+ export function getGlobalConfig(): Readonly<AppConfig> {
47
+ if (!globalConfig || !isLocked) {
48
+ throw new Error(
49
+ 'Configuration not initialized. Call createApp() to initialize the configuration system.'
50
+ );
51
+ }
52
+ return globalConfig;
53
+ }
54
+
55
+ /**
56
+ * Check if configuration has been initialized and locked
57
+ */
58
+ export function isConfigLocked(): boolean {
59
+ return isLocked && globalConfig !== null;
60
+ }
61
+
62
+ /**
63
+ * Reset configuration state (for testing only)
64
+ * @internal - This should only be used in tests
65
+ */
66
+ export function resetConfigForTesting(): void {
67
+ if (
68
+ process.env.NODE_ENV !== 'test' &&
69
+ !process.env.MORO_ALLOW_CONFIG_RESET &&
70
+ !process.env.JEST_WORKER_ID
71
+ ) {
72
+ throw new Error(
73
+ 'Configuration reset is only allowed in test environments. ' +
74
+ 'Set MORO_ALLOW_CONFIG_RESET=true to override this check.'
75
+ );
76
+ }
77
+
78
+ globalConfig = null;
79
+ isLocked = false;
80
+ logger.debug('Configuration state reset for testing');
81
+ }
82
+
83
+ /**
84
+ * Deep freeze an object to make it truly immutable
85
+ * This prevents any accidental mutations to the configuration
86
+ */
87
+ function deepFreeze<T>(obj: T): Readonly<T> {
88
+ // Get property names
89
+ const propNames = Object.getOwnPropertyNames(obj);
90
+
91
+ // Freeze properties before freezing self
92
+ for (const name of propNames) {
93
+ const value = (obj as any)[name];
94
+
95
+ if (value && typeof value === 'object') {
96
+ deepFreeze(value);
97
+ }
98
+ }
99
+
100
+ return Object.freeze(obj);
101
+ }
102
+
103
+ /**
104
+ * Get a specific configuration value using dot notation
105
+ * This provides a safe way to access nested config values
106
+ *
107
+ * @example
108
+ * getConfigValue('server.port') // Returns the server port
109
+ * getConfigValue('database.redis.url') // Returns the Redis URL
110
+ */
111
+ export function getConfigValue<T = any>(path: string): T | undefined {
112
+ const config = getGlobalConfig();
113
+
114
+ return path.split('.').reduce((obj: any, key: string) => {
115
+ return obj && obj[key] !== undefined ? obj[key] : undefined;
116
+ }, config);
117
+ }
118
+
119
+ /**
120
+ * Utility functions for common environment checks
121
+ * These now read NODE_ENV directly for consistency with Node.js ecosystem
122
+ */
123
+ export function isDevelopment(): boolean {
124
+ return process.env.NODE_ENV === 'development';
125
+ }
126
+
127
+ export function isProduction(): boolean {
128
+ return process.env.NODE_ENV === 'production';
129
+ }
130
+
131
+ export function isStaging(): boolean {
132
+ return process.env.NODE_ENV === 'staging';
133
+ }
@@ -0,0 +1,384 @@
1
+ /**
2
+ * Configuration Sources - Load from Environment, Files, and Options
3
+ *
4
+ * This module handles loading configuration from different sources with clear precedence:
5
+ * Environment Variables > createApp Options > Config File > Schema Defaults
6
+ */
7
+
8
+ import { AppConfig } from '../../types/config';
9
+ import { MoroOptions } from '../../types/core';
10
+ import { DEFAULT_CONFIG } from './schema';
11
+ import { loadConfigFileSync } from './file-loader';
12
+ import { createFrameworkLogger } from '../logger';
13
+ import { validateConfig } from './config-validator';
14
+
15
+ const logger = createFrameworkLogger('ConfigSources');
16
+
17
+ /**
18
+ * Configuration source metadata for debugging
19
+ */
20
+ export interface ConfigSourceInfo {
21
+ source: 'environment' | 'createApp' | 'configFile' | 'default';
22
+ path: string;
23
+ value: any;
24
+ }
25
+
26
+ /**
27
+ * Load configuration from all sources with proper precedence
28
+ * Returns a validated, complete configuration object
29
+ */
30
+ export function loadConfigFromAllSources(createAppOptions?: MoroOptions): AppConfig {
31
+ logger.debug('Loading configuration from all sources');
32
+
33
+ // 1. Start with schema defaults
34
+ let config = JSON.parse(JSON.stringify(DEFAULT_CONFIG)) as AppConfig;
35
+ const sourceMap = new Map<string, ConfigSourceInfo>();
36
+
37
+ // Track default values
38
+ trackConfigSource(config, sourceMap, 'default', 'schema');
39
+
40
+ // 2. Load and merge config file (if exists)
41
+ try {
42
+ const fileConfig = loadConfigFileSync();
43
+ if (fileConfig) {
44
+ config = deepMerge(config, fileConfig);
45
+ trackConfigSource(fileConfig, sourceMap, 'configFile', 'moro.config.js/ts');
46
+ logger.debug('Config file loaded and merged');
47
+ }
48
+ } catch (error) {
49
+ logger.warn('Config file loading failed, continuing without it:', String(error));
50
+ }
51
+
52
+ // 3. Load and merge environment variables
53
+ const envConfig = loadEnvironmentConfig();
54
+ config = deepMerge(config, envConfig);
55
+ trackConfigSource(envConfig, sourceMap, 'environment', 'process.env');
56
+
57
+ // 4. Load and merge createApp options (highest precedence)
58
+ if (createAppOptions) {
59
+ const normalizedOptions = normalizeCreateAppOptions(createAppOptions);
60
+ config = deepMerge(config, normalizedOptions);
61
+ trackConfigSource(normalizedOptions, sourceMap, 'createApp', 'createApp()');
62
+ logger.debug('createApp options merged');
63
+ }
64
+
65
+ // 5. Validate the final configuration
66
+ const validatedConfig = validateConfig(config);
67
+
68
+ // Log configuration sources for debugging
69
+ logConfigurationSources(sourceMap);
70
+
71
+ return validatedConfig;
72
+ }
73
+
74
+ /**
75
+ * Load configuration from environment variables
76
+ * Handles both standard and MORO_ prefixed variables
77
+ */
78
+ function loadEnvironmentConfig(): Partial<AppConfig> {
79
+ const config: Partial<AppConfig> = {};
80
+
81
+ // Server configuration
82
+ if (process.env.PORT || process.env.MORO_PORT) {
83
+ if (!config.server) config.server = {} as any;
84
+ config.server!.port = parseInt(process.env.PORT || process.env.MORO_PORT || '3001', 10);
85
+ }
86
+
87
+ if (process.env.HOST || process.env.MORO_HOST) {
88
+ if (!config.server) config.server = {} as any;
89
+ config.server!.host = process.env.HOST || process.env.MORO_HOST || 'localhost';
90
+ }
91
+
92
+ if (process.env.MAX_CONNECTIONS || process.env.MORO_MAX_CONNECTIONS) {
93
+ if (!config.server) config.server = {} as any;
94
+ config.server!.maxConnections = parseInt(
95
+ process.env.MAX_CONNECTIONS || process.env.MORO_MAX_CONNECTIONS || '1000',
96
+ 10
97
+ );
98
+ }
99
+
100
+ if (process.env.REQUEST_TIMEOUT || process.env.MORO_TIMEOUT) {
101
+ if (!config.server) config.server = {} as any;
102
+ config.server!.timeout = parseInt(
103
+ process.env.REQUEST_TIMEOUT || process.env.MORO_TIMEOUT || '30000',
104
+ 10
105
+ );
106
+ }
107
+
108
+ // Database configuration
109
+ if (process.env.DATABASE_URL || process.env.MORO_DATABASE_URL) {
110
+ if (!config.database) config.database = {} as any;
111
+ config.database!.url = process.env.DATABASE_URL || process.env.MORO_DATABASE_URL;
112
+ }
113
+
114
+ // Redis configuration
115
+ if (process.env.REDIS_URL || process.env.MORO_REDIS_URL) {
116
+ if (!config.database) config.database = {} as any;
117
+ if (!config.database!.redis) config.database!.redis = {} as any;
118
+ config.database!.redis!.url =
119
+ process.env.REDIS_URL || process.env.MORO_REDIS_URL || 'redis://localhost:6379';
120
+ config.database!.redis!.maxRetries = parseInt(
121
+ process.env.REDIS_MAX_RETRIES || process.env.MORO_REDIS_MAX_RETRIES || '3',
122
+ 10
123
+ );
124
+ config.database!.redis!.retryDelay = parseInt(
125
+ process.env.REDIS_RETRY_DELAY || process.env.MORO_REDIS_RETRY_DELAY || '1000',
126
+ 10
127
+ );
128
+ config.database!.redis!.keyPrefix =
129
+ process.env.REDIS_KEY_PREFIX || process.env.MORO_REDIS_KEY_PREFIX || 'moro:';
130
+ }
131
+
132
+ // MySQL configuration - only include if MYSQL_HOST is set
133
+ if (process.env.MYSQL_HOST || process.env.MORO_MYSQL_HOST) {
134
+ if (!config.database) config.database = {} as any;
135
+ config.database!.mysql = {
136
+ host: process.env.MYSQL_HOST || process.env.MORO_MYSQL_HOST || 'localhost',
137
+ port: parseInt(process.env.MYSQL_PORT || process.env.MORO_MYSQL_PORT || '3306', 10),
138
+ database: process.env.MYSQL_DATABASE || process.env.MORO_MYSQL_DB,
139
+ username: process.env.MYSQL_USERNAME || process.env.MORO_MYSQL_USER,
140
+ password: process.env.MYSQL_PASSWORD || process.env.MORO_MYSQL_PASS,
141
+ connectionLimit: parseInt(
142
+ process.env.MYSQL_CONNECTION_LIMIT || process.env.MORO_MYSQL_CONNECTIONS || '10',
143
+ 10
144
+ ),
145
+ acquireTimeout: parseInt(
146
+ process.env.MYSQL_ACQUIRE_TIMEOUT || process.env.MORO_MYSQL_ACQUIRE || '60000',
147
+ 10
148
+ ),
149
+ timeout: parseInt(process.env.MYSQL_TIMEOUT || process.env.MORO_MYSQL_TIMEOUT || '60000', 10),
150
+ } as any;
151
+ }
152
+
153
+ // Logging configuration
154
+ if (process.env.LOG_LEVEL || process.env.MORO_LOG_LEVEL) {
155
+ const level = process.env.LOG_LEVEL || process.env.MORO_LOG_LEVEL;
156
+ if (
157
+ level === 'debug' ||
158
+ level === 'info' ||
159
+ level === 'warn' ||
160
+ level === 'error' ||
161
+ level === 'fatal'
162
+ ) {
163
+ if (!config.logging) config.logging = {} as any;
164
+ config.logging!.level = level;
165
+ }
166
+ }
167
+
168
+ // External services - only include if configured
169
+ const externalConfig: Partial<AppConfig['external']> = {};
170
+
171
+ // Stripe
172
+ if (process.env.STRIPE_SECRET_KEY || process.env.MORO_STRIPE_SECRET) {
173
+ externalConfig.stripe = {
174
+ secretKey: process.env.STRIPE_SECRET_KEY || process.env.MORO_STRIPE_SECRET,
175
+ publishableKey: process.env.STRIPE_PUBLISHABLE_KEY || process.env.MORO_STRIPE_PUBLIC,
176
+ webhookSecret: process.env.STRIPE_WEBHOOK_SECRET || process.env.MORO_STRIPE_WEBHOOK,
177
+ apiVersion: process.env.STRIPE_API_VERSION || process.env.MORO_STRIPE_VERSION || '2023-10-16',
178
+ };
179
+ }
180
+
181
+ // PayPal
182
+ if (process.env.PAYPAL_CLIENT_ID || process.env.MORO_PAYPAL_CLIENT) {
183
+ externalConfig.paypal = {
184
+ clientId: process.env.PAYPAL_CLIENT_ID || process.env.MORO_PAYPAL_CLIENT,
185
+ clientSecret: process.env.PAYPAL_CLIENT_SECRET || process.env.MORO_PAYPAL_SECRET,
186
+ webhookId: process.env.PAYPAL_WEBHOOK_ID || process.env.MORO_PAYPAL_WEBHOOK,
187
+ environment:
188
+ (process.env.PAYPAL_ENVIRONMENT || process.env.MORO_PAYPAL_ENV) === 'production'
189
+ ? 'production'
190
+ : 'sandbox',
191
+ };
192
+ }
193
+
194
+ // SMTP
195
+ if (process.env.SMTP_HOST || process.env.MORO_SMTP_HOST) {
196
+ externalConfig.smtp = {
197
+ host: process.env.SMTP_HOST || process.env.MORO_SMTP_HOST,
198
+ port: parseInt(process.env.SMTP_PORT || process.env.MORO_SMTP_PORT || '587', 10),
199
+ secure: (process.env.SMTP_SECURE || 'false').toLowerCase() === 'true',
200
+ username: process.env.SMTP_USERNAME || process.env.MORO_SMTP_USER,
201
+ password: process.env.SMTP_PASSWORD || process.env.MORO_SMTP_PASS,
202
+ };
203
+ }
204
+
205
+ if (Object.keys(externalConfig).length > 0) {
206
+ config.external = externalConfig;
207
+ }
208
+
209
+ return config;
210
+ }
211
+
212
+ /**
213
+ * Normalize createApp options to match AppConfig structure
214
+ * This handles the flexible createApp API while converting to structured config
215
+ */
216
+ function normalizeCreateAppOptions(options: MoroOptions): Partial<AppConfig> {
217
+ const config: Partial<AppConfig> = {};
218
+
219
+ // Direct config section overrides - merge with existing config
220
+ if (options.server) {
221
+ config.server = { ...config.server, ...options.server } as any;
222
+ }
223
+ if (options.database) {
224
+ config.database = { ...config.database, ...options.database } as any;
225
+ }
226
+ if (options.modules) {
227
+ config.modules = { ...config.modules, ...options.modules } as any;
228
+ }
229
+ if (options.logging) {
230
+ config.logging = { ...config.logging, ...options.logging } as any;
231
+ }
232
+ if (options.security) {
233
+ config.security = { ...config.security, ...options.security } as any;
234
+ }
235
+ if (options.external) {
236
+ config.external = { ...config.external, ...options.external } as any;
237
+ }
238
+ if (options.performance) {
239
+ config.performance = { ...config.performance, ...options.performance } as any;
240
+ }
241
+
242
+ // Handle shorthand boolean/object options
243
+ if (options.cors !== undefined) {
244
+ config.security = {
245
+ ...config.security,
246
+ cors:
247
+ typeof options.cors === 'boolean'
248
+ ? { ...DEFAULT_CONFIG.security.cors, enabled: options.cors }
249
+ : { ...DEFAULT_CONFIG.security.cors, ...options.cors },
250
+ } as any;
251
+ }
252
+
253
+ if (options.compression !== undefined) {
254
+ config.performance = {
255
+ ...config.performance,
256
+ compression:
257
+ typeof options.compression === 'boolean'
258
+ ? { ...DEFAULT_CONFIG.performance.compression, enabled: options.compression }
259
+ : { ...DEFAULT_CONFIG.performance.compression, ...options.compression },
260
+ } as any;
261
+ }
262
+
263
+ if (options.helmet !== undefined) {
264
+ config.security = {
265
+ ...config.security,
266
+ helmet:
267
+ typeof options.helmet === 'boolean'
268
+ ? { ...DEFAULT_CONFIG.security.helmet, enabled: options.helmet }
269
+ : { ...DEFAULT_CONFIG.security.helmet, ...options.helmet },
270
+ } as any;
271
+ }
272
+
273
+ return config;
274
+ }
275
+
276
+ /**
277
+ * Check if a config field contains sensitive information
278
+ */
279
+ function isSensitiveField(path: string): boolean {
280
+ const sensitivePatterns = [
281
+ 'password',
282
+ 'secret',
283
+ 'key',
284
+ 'token',
285
+ 'auth',
286
+ 'stripe',
287
+ 'paypal',
288
+ 'smtp.password',
289
+ 'smtp.username',
290
+ 'database.url',
291
+ 'redis.url',
292
+ 'mysql.password',
293
+ ];
294
+
295
+ return sensitivePatterns.some(pattern => path.toLowerCase().includes(pattern.toLowerCase()));
296
+ }
297
+
298
+ /**
299
+ * Deep merge two configuration objects
300
+ * Later object properties override earlier ones
301
+ */
302
+ function deepMerge<T>(target: T, source: Partial<T>): T {
303
+ const result = { ...target };
304
+
305
+ for (const key in source) {
306
+ const sourceValue = source[key];
307
+ const targetValue = result[key];
308
+
309
+ if (
310
+ sourceValue &&
311
+ typeof sourceValue === 'object' &&
312
+ !Array.isArray(sourceValue) &&
313
+ targetValue &&
314
+ typeof targetValue === 'object' &&
315
+ !Array.isArray(targetValue)
316
+ ) {
317
+ (result as any)[key] = deepMerge(targetValue, sourceValue);
318
+ } else if (sourceValue !== undefined) {
319
+ (result as any)[key] = sourceValue;
320
+ }
321
+ }
322
+
323
+ return result;
324
+ }
325
+
326
+ /**
327
+ * Track configuration sources for debugging
328
+ */
329
+ function trackConfigSource(
330
+ config: any,
331
+ sourceMap: Map<string, ConfigSourceInfo>,
332
+ source: ConfigSourceInfo['source'],
333
+ path: string
334
+ ): void {
335
+ function traverse(obj: any, currentPath: string): void {
336
+ for (const key in obj) {
337
+ const value = obj[key];
338
+ const fullPath = currentPath ? `${currentPath}.${key}` : key;
339
+
340
+ if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
341
+ traverse(value, fullPath);
342
+ } else {
343
+ sourceMap.set(fullPath, { source, path, value });
344
+ }
345
+ }
346
+ }
347
+
348
+ traverse(config, '');
349
+ }
350
+
351
+ /**
352
+ * Log configuration sources for debugging
353
+ */
354
+ function logConfigurationSources(sourceMap: Map<string, ConfigSourceInfo>): void {
355
+ const allSources = Array.from(sourceMap.entries()).sort(([a], [b]) => a.localeCompare(b));
356
+ const nonDefaultSources = allSources.filter(([_, info]) => info.source !== 'default');
357
+
358
+ if (process.env.NODE_ENV === 'production') {
359
+ // In production, only show non-default values with sensitive data obfuscated
360
+ if (nonDefaultSources.length > 0) {
361
+ logger.debug(`Configuration overrides loaded (${nonDefaultSources.length} total)`);
362
+
363
+ nonDefaultSources.forEach(([path, info]) => {
364
+ const valueStr = isSensitiveField(path)
365
+ ? '***'
366
+ : typeof info.value === 'object'
367
+ ? JSON.stringify(info.value)
368
+ : String(info.value);
369
+ logger.debug(` ${path}: ${valueStr} (from ${info.source})`);
370
+ });
371
+ } else {
372
+ logger.debug('Using default configuration (no overrides)');
373
+ }
374
+ } else {
375
+ // In development, show all sources for debugging
376
+ logger.debug(`Configuration sources loaded (${allSources.length} total)`);
377
+
378
+ allSources.forEach(([path, info]) => {
379
+ const valueStr =
380
+ typeof info.value === 'object' ? JSON.stringify(info.value) : String(info.value);
381
+ logger.debug(` ${path}: ${valueStr} (from ${info.source})`);
382
+ });
383
+ }
384
+ }