@morojs/moro 1.3.0 → 1.5.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.
Files changed (86) hide show
  1. package/README.md +61 -7
  2. package/dist/core/config/index.d.ts +0 -1
  3. package/dist/core/config/index.js +0 -4
  4. package/dist/core/config/index.js.map +1 -1
  5. package/dist/core/config/loader.js +219 -226
  6. package/dist/core/config/loader.js.map +1 -1
  7. package/dist/core/config/schema.d.ts +30 -335
  8. package/dist/core/config/schema.js +133 -224
  9. package/dist/core/config/schema.js.map +1 -1
  10. package/dist/core/config/utils.d.ts +3 -2
  11. package/dist/core/config/utils.js.map +1 -1
  12. package/dist/core/config/validation.d.ts +17 -0
  13. package/dist/core/config/validation.js +129 -0
  14. package/dist/core/config/validation.js.map +1 -0
  15. package/dist/core/docs/index.js +1 -1
  16. package/dist/core/docs/index.js.map +1 -1
  17. package/dist/core/docs/openapi-generator.js +6 -6
  18. package/dist/core/docs/openapi-generator.js.map +1 -1
  19. package/dist/core/docs/schema-to-openapi.d.ts +7 -0
  20. package/dist/core/docs/schema-to-openapi.js +124 -0
  21. package/dist/core/docs/schema-to-openapi.js.map +1 -0
  22. package/dist/core/docs/simple-docs.js +5 -5
  23. package/dist/core/docs/zod-to-openapi.d.ts +4 -3
  24. package/dist/core/docs/zod-to-openapi.js +28 -0
  25. package/dist/core/docs/zod-to-openapi.js.map +1 -1
  26. package/dist/core/framework.d.ts +29 -6
  27. package/dist/core/framework.js +117 -18
  28. package/dist/core/framework.js.map +1 -1
  29. package/dist/core/networking/adapters/index.d.ts +3 -0
  30. package/dist/core/networking/adapters/index.js +10 -0
  31. package/dist/core/networking/adapters/index.js.map +1 -0
  32. package/dist/core/networking/adapters/socketio-adapter.d.ts +16 -0
  33. package/dist/core/networking/adapters/socketio-adapter.js +244 -0
  34. package/dist/core/networking/adapters/socketio-adapter.js.map +1 -0
  35. package/dist/core/networking/adapters/ws-adapter.d.ts +54 -0
  36. package/dist/core/networking/adapters/ws-adapter.js +383 -0
  37. package/dist/core/networking/adapters/ws-adapter.js.map +1 -0
  38. package/dist/core/networking/websocket-adapter.d.ts +171 -0
  39. package/dist/core/networking/websocket-adapter.js +5 -0
  40. package/dist/core/networking/websocket-adapter.js.map +1 -0
  41. package/dist/core/networking/websocket-manager.d.ts +53 -17
  42. package/dist/core/networking/websocket-manager.js +184 -126
  43. package/dist/core/networking/websocket-manager.js.map +1 -1
  44. package/dist/core/routing/index.d.ts +13 -13
  45. package/dist/core/routing/index.js.map +1 -1
  46. package/dist/core/validation/adapters.d.ts +51 -0
  47. package/dist/core/validation/adapters.js +135 -0
  48. package/dist/core/validation/adapters.js.map +1 -0
  49. package/dist/core/validation/index.d.ts +12 -11
  50. package/dist/core/validation/index.js +32 -26
  51. package/dist/core/validation/index.js.map +1 -1
  52. package/dist/core/validation/schema-interface.d.ts +36 -0
  53. package/dist/core/validation/schema-interface.js +68 -0
  54. package/dist/core/validation/schema-interface.js.map +1 -0
  55. package/dist/index.d.ts +9 -2
  56. package/dist/index.js +23 -4
  57. package/dist/index.js.map +1 -1
  58. package/dist/moro.js +24 -17
  59. package/dist/moro.js.map +1 -1
  60. package/dist/types/config.d.ts +146 -0
  61. package/dist/types/config.js +4 -0
  62. package/dist/types/config.js.map +1 -0
  63. package/package.json +30 -7
  64. package/src/core/config/index.ts +0 -3
  65. package/src/core/config/loader.ts +571 -247
  66. package/src/core/config/schema.ts +146 -279
  67. package/src/core/config/utils.ts +1 -2
  68. package/src/core/config/validation.ts +140 -0
  69. package/src/core/docs/index.ts +1 -1
  70. package/src/core/docs/openapi-generator.ts +7 -6
  71. package/src/core/docs/schema-to-openapi.ts +148 -0
  72. package/src/core/docs/simple-docs.ts +5 -5
  73. package/src/core/docs/zod-to-openapi.ts +52 -20
  74. package/src/core/framework.ts +121 -28
  75. package/src/core/networking/adapters/index.ts +16 -0
  76. package/src/core/networking/adapters/socketio-adapter.ts +252 -0
  77. package/src/core/networking/adapters/ws-adapter.ts +425 -0
  78. package/src/core/networking/websocket-adapter.ts +217 -0
  79. package/src/core/networking/websocket-manager.ts +201 -143
  80. package/src/core/routing/index.ts +13 -13
  81. package/src/core/validation/adapters.ts +147 -0
  82. package/src/core/validation/index.ts +54 -38
  83. package/src/core/validation/schema-interface.ts +100 -0
  84. package/src/index.ts +36 -3
  85. package/src/moro.ts +27 -17
  86. package/src/types/config.ts +157 -0
@@ -1,6 +1,17 @@
1
1
  // Configuration Loader - Environment Variable Mapping and Validation
2
- import { ZodError } from 'zod';
3
- import { ConfigSchema, AppConfig } from './schema';
2
+ import { ConfigSchema, AppConfig, DEFAULT_CONFIG } from './schema';
3
+ import {
4
+ validatePort,
5
+ validateBoolean,
6
+ validateNumber,
7
+ validateString,
8
+ validateUrl,
9
+ validateEnum,
10
+ validateStringArray,
11
+ validateOptional,
12
+ coerceEnvValue,
13
+ ConfigValidationError,
14
+ } from './validation';
4
15
  import { createFrameworkLogger } from '../logger';
5
16
  import { loadConfigFileSync, applyConfigAsEnvironmentVariables } from './file-loader';
6
17
 
@@ -12,9 +23,9 @@ const logger = createFrameworkLogger('Config');
12
23
  * @returns Validated and typed application configuration
13
24
  */
14
25
  export function loadConfig(): AppConfig {
15
- logger.debug('Loading configuration from config files and environment variables');
26
+ logger.debug('Loading configuration with TypeScript validation');
16
27
 
17
- // First, try to load from config file and apply as environment variables (synchronously)
28
+ // First, try to load from config file and apply as environment variables
18
29
  try {
19
30
  const fileConfig = loadConfigFileSync();
20
31
  if (fileConfig) {
@@ -28,283 +39,596 @@ export function loadConfig(): AppConfig {
28
39
  );
29
40
  }
30
41
 
31
- // Map environment variables to configuration structure
32
- const envConfig = {
33
- server: {
34
- port: process.env.PORT || process.env.MORO_PORT,
35
- host: process.env.HOST || process.env.MORO_HOST,
36
- environment: process.env.NODE_ENV || process.env.MORO_ENV,
37
- maxConnections: process.env.MAX_CONNECTIONS || process.env.MORO_MAX_CONNECTIONS,
38
- timeout: process.env.REQUEST_TIMEOUT || process.env.MORO_TIMEOUT,
39
- },
40
-
41
- serviceDiscovery: {
42
- enabled: process.env.SERVICE_DISCOVERY_ENABLED || process.env.MORO_SERVICE_DISCOVERY,
43
- type: process.env.DISCOVERY_TYPE || process.env.MORO_DISCOVERY_TYPE,
44
- consulUrl: process.env.CONSUL_URL || process.env.MORO_CONSUL_URL,
45
- kubernetesNamespace: process.env.K8S_NAMESPACE || process.env.MORO_K8S_NAMESPACE,
46
- healthCheckInterval: process.env.HEALTH_CHECK_INTERVAL || process.env.MORO_HEALTH_INTERVAL,
47
- retryAttempts: process.env.DISCOVERY_RETRY_ATTEMPTS || process.env.MORO_DISCOVERY_RETRIES,
48
- },
49
-
50
- database: {
51
- url: process.env.DATABASE_URL || process.env.MORO_DATABASE_URL,
52
- redis: {
53
- url: process.env.REDIS_URL || process.env.MORO_REDIS_URL,
54
- maxRetries: process.env.REDIS_MAX_RETRIES || process.env.MORO_REDIS_RETRIES,
55
- retryDelay: process.env.REDIS_RETRY_DELAY || process.env.MORO_REDIS_DELAY,
56
- keyPrefix: process.env.REDIS_KEY_PREFIX || process.env.MORO_REDIS_PREFIX,
42
+ try {
43
+ // Build configuration using robust validation functions
44
+ const config: AppConfig = {
45
+ server: {
46
+ port: validatePort(
47
+ coerceEnvValue(process.env.PORT || process.env.MORO_PORT || '') ||
48
+ DEFAULT_CONFIG.server.port,
49
+ 'server.port'
50
+ ),
51
+ host: validateString(
52
+ process.env.HOST || process.env.MORO_HOST || DEFAULT_CONFIG.server.host,
53
+ 'server.host'
54
+ ),
55
+ environment: validateEnum(
56
+ process.env.NODE_ENV || process.env.MORO_ENV || DEFAULT_CONFIG.server.environment,
57
+ ['development', 'staging', 'production'] as const,
58
+ 'server.environment'
59
+ ),
60
+ maxConnections: validateNumber(
61
+ coerceEnvValue(process.env.MAX_CONNECTIONS || process.env.MORO_MAX_CONNECTIONS || '') ||
62
+ DEFAULT_CONFIG.server.maxConnections,
63
+ 'server.maxConnections',
64
+ { min: 1 }
65
+ ),
66
+ timeout: validateNumber(
67
+ coerceEnvValue(process.env.REQUEST_TIMEOUT || process.env.MORO_TIMEOUT || '') ||
68
+ DEFAULT_CONFIG.server.timeout,
69
+ 'server.timeout',
70
+ { min: 1000 }
71
+ ),
57
72
  },
58
- mysql: process.env.MYSQL_HOST
59
- ? {
60
- host: process.env.MYSQL_HOST || process.env.MORO_MYSQL_HOST,
61
- port: process.env.MYSQL_PORT || process.env.MORO_MYSQL_PORT,
62
- database: process.env.MYSQL_DATABASE || process.env.MORO_MYSQL_DB,
63
- username: process.env.MYSQL_USERNAME || process.env.MORO_MYSQL_USER,
64
- password: process.env.MYSQL_PASSWORD || process.env.MORO_MYSQL_PASS,
65
- connectionLimit:
66
- process.env.MYSQL_CONNECTION_LIMIT || process.env.MORO_MYSQL_CONNECTIONS,
67
- acquireTimeout:
68
- process.env.MYSQL_ACQUIRE_TIMEOUT || process.env.MORO_MYSQL_ACQUIRE_TIMEOUT,
69
- timeout: process.env.MYSQL_TIMEOUT || process.env.MORO_MYSQL_TIMEOUT,
70
- }
71
- : undefined,
72
- },
73
73
 
74
- modules: {
75
- cache: {
76
- enabled: process.env.CACHE_ENABLED || process.env.MORO_CACHE_ENABLED,
77
- defaultTtl: process.env.DEFAULT_CACHE_TTL || process.env.MORO_CACHE_TTL,
78
- maxSize: process.env.CACHE_MAX_SIZE || process.env.MORO_CACHE_SIZE,
79
- strategy: process.env.CACHE_STRATEGY || process.env.MORO_CACHE_STRATEGY,
80
- },
81
- rateLimit: {
82
- enabled: process.env.RATE_LIMIT_ENABLED || process.env.MORO_RATE_LIMIT_ENABLED,
83
- defaultRequests:
84
- process.env.DEFAULT_RATE_LIMIT_REQUESTS || process.env.MORO_RATE_LIMIT_REQUESTS,
85
- defaultWindow: process.env.DEFAULT_RATE_LIMIT_WINDOW || process.env.MORO_RATE_LIMIT_WINDOW,
86
- skipSuccessfulRequests:
87
- process.env.RATE_LIMIT_SKIP_SUCCESS || process.env.MORO_RATE_LIMIT_SKIP_SUCCESS,
88
- skipFailedRequests:
89
- process.env.RATE_LIMIT_SKIP_FAILED || process.env.MORO_RATE_LIMIT_SKIP_FAILED,
74
+ serviceDiscovery: {
75
+ enabled: validateBoolean(
76
+ coerceEnvValue(
77
+ process.env.SERVICE_DISCOVERY_ENABLED || process.env.MORO_SERVICE_DISCOVERY || ''
78
+ ) ?? DEFAULT_CONFIG.serviceDiscovery.enabled,
79
+ 'serviceDiscovery.enabled'
80
+ ),
81
+ type: validateEnum(
82
+ process.env.DISCOVERY_TYPE ||
83
+ process.env.MORO_DISCOVERY_TYPE ||
84
+ DEFAULT_CONFIG.serviceDiscovery.type,
85
+ ['memory', 'consul', 'kubernetes'] as const,
86
+ 'serviceDiscovery.type'
87
+ ),
88
+ consulUrl: validateUrl(
89
+ process.env.CONSUL_URL ||
90
+ process.env.MORO_CONSUL_URL ||
91
+ DEFAULT_CONFIG.serviceDiscovery.consulUrl,
92
+ 'serviceDiscovery.consulUrl'
93
+ ),
94
+ kubernetesNamespace: validateString(
95
+ process.env.KUBERNETES_NAMESPACE ||
96
+ process.env.MORO_KUBERNETES_NAMESPACE ||
97
+ DEFAULT_CONFIG.serviceDiscovery.kubernetesNamespace,
98
+ 'serviceDiscovery.kubernetesNamespace'
99
+ ),
100
+ healthCheckInterval: validateNumber(
101
+ coerceEnvValue(
102
+ process.env.HEALTH_CHECK_INTERVAL || process.env.MORO_HEALTH_CHECK_INTERVAL || ''
103
+ ) || DEFAULT_CONFIG.serviceDiscovery.healthCheckInterval,
104
+ 'serviceDiscovery.healthCheckInterval',
105
+ { min: 1000 }
106
+ ),
107
+ retryAttempts: validateNumber(
108
+ coerceEnvValue(process.env.RETRY_ATTEMPTS || process.env.MORO_RETRY_ATTEMPTS || '') ||
109
+ DEFAULT_CONFIG.serviceDiscovery.retryAttempts,
110
+ 'serviceDiscovery.retryAttempts',
111
+ { min: 0 }
112
+ ),
90
113
  },
91
- validation: {
92
- enabled: process.env.VALIDATION_ENABLED || process.env.MORO_VALIDATION_ENABLED,
93
- stripUnknown: process.env.VALIDATION_STRIP_UNKNOWN || process.env.MORO_VALIDATION_STRIP,
94
- abortEarly: process.env.VALIDATION_ABORT_EARLY || process.env.MORO_VALIDATION_ABORT,
114
+
115
+ database: {
116
+ url: validateOptional(process.env.DATABASE_URL, validateString, 'database.url'),
117
+ redis: {
118
+ url: validateString(
119
+ process.env.REDIS_URL ||
120
+ process.env.MORO_REDIS_URL ||
121
+ DEFAULT_CONFIG.database.redis.url,
122
+ 'database.redis.url'
123
+ ),
124
+ maxRetries: validateNumber(
125
+ coerceEnvValue(process.env.REDIS_MAX_RETRIES || process.env.MORO_REDIS_RETRIES || '') ||
126
+ DEFAULT_CONFIG.database.redis.maxRetries,
127
+ 'database.redis.maxRetries',
128
+ { min: 0 }
129
+ ),
130
+ retryDelay: validateNumber(
131
+ coerceEnvValue(process.env.REDIS_RETRY_DELAY || process.env.MORO_REDIS_DELAY || '') ||
132
+ DEFAULT_CONFIG.database.redis.retryDelay,
133
+ 'database.redis.retryDelay',
134
+ { min: 100 }
135
+ ),
136
+ keyPrefix: validateString(
137
+ process.env.REDIS_KEY_PREFIX ||
138
+ process.env.MORO_REDIS_PREFIX ||
139
+ DEFAULT_CONFIG.database.redis.keyPrefix,
140
+ 'database.redis.keyPrefix'
141
+ ),
142
+ },
143
+ mysql: process.env.MYSQL_HOST
144
+ ? {
145
+ host: validateString(process.env.MYSQL_HOST, 'database.mysql.host'),
146
+ port: validatePort(
147
+ process.env.MYSQL_PORT || process.env.MORO_MYSQL_PORT || '3306',
148
+ 'database.mysql.port'
149
+ ),
150
+ database: validateOptional(
151
+ process.env.MYSQL_DATABASE || process.env.MORO_MYSQL_DB,
152
+ validateString,
153
+ 'database.mysql.database'
154
+ ),
155
+ username: validateOptional(
156
+ process.env.MYSQL_USERNAME || process.env.MORO_MYSQL_USER,
157
+ validateString,
158
+ 'database.mysql.username'
159
+ ),
160
+ password: validateOptional(
161
+ process.env.MYSQL_PASSWORD || process.env.MORO_MYSQL_PASS,
162
+ validateString,
163
+ 'database.mysql.password'
164
+ ),
165
+ connectionLimit: validateNumber(
166
+ coerceEnvValue(
167
+ process.env.MYSQL_CONNECTION_LIMIT || process.env.MORO_MYSQL_CONNECTIONS || ''
168
+ ) || 10,
169
+ 'database.mysql.connectionLimit',
170
+ { min: 1 }
171
+ ),
172
+ acquireTimeout: validateNumber(
173
+ coerceEnvValue(
174
+ process.env.MYSQL_ACQUIRE_TIMEOUT || process.env.MORO_MYSQL_ACQUIRE_TIMEOUT || ''
175
+ ) || 60000,
176
+ 'database.mysql.acquireTimeout',
177
+ { min: 1000 }
178
+ ),
179
+ timeout: validateNumber(
180
+ coerceEnvValue(process.env.MYSQL_TIMEOUT || process.env.MORO_MYSQL_TIMEOUT || '') ||
181
+ 60000,
182
+ 'database.mysql.timeout',
183
+ { min: 1000 }
184
+ ),
185
+ }
186
+ : undefined,
95
187
  },
96
- },
97
188
 
98
- logging: {
99
- level: process.env.LOG_LEVEL || process.env.MORO_LOG_LEVEL,
100
- format: process.env.LOG_FORMAT || process.env.MORO_LOG_FORMAT,
101
- enableColors: process.env.NO_COLOR ? false : process.env.LOG_COLORS !== 'false',
102
- enableTimestamp: process.env.LOG_TIMESTAMP !== 'false',
103
- enableContext: process.env.LOG_CONTEXT !== 'false',
104
- outputs: {
105
- console: process.env.LOG_CONSOLE !== 'false',
106
- file: {
107
- enabled: process.env.LOG_FILE_ENABLED === 'true' || process.env.MORO_LOG_FILE === 'true',
108
- path: process.env.LOG_FILE_PATH || process.env.MORO_LOG_PATH,
109
- maxSize: process.env.LOG_FILE_MAX_SIZE || process.env.MORO_LOG_MAX_SIZE,
110
- maxFiles: process.env.LOG_FILE_MAX_FILES || process.env.MORO_LOG_MAX_FILES,
189
+ modules: {
190
+ cache: {
191
+ enabled: validateBoolean(
192
+ coerceEnvValue(process.env.CACHE_ENABLED || process.env.MORO_CACHE_ENABLED || '') ??
193
+ DEFAULT_CONFIG.modules.cache.enabled,
194
+ 'modules.cache.enabled'
195
+ ),
196
+ defaultTtl: validateNumber(
197
+ coerceEnvValue(process.env.CACHE_TTL || process.env.MORO_CACHE_TTL || '') ||
198
+ DEFAULT_CONFIG.modules.cache.defaultTtl,
199
+ 'modules.cache.defaultTtl',
200
+ { min: 0 }
201
+ ),
202
+ maxSize: validateNumber(
203
+ coerceEnvValue(process.env.CACHE_MAX_SIZE || process.env.MORO_CACHE_MAX_SIZE || '') ||
204
+ DEFAULT_CONFIG.modules.cache.maxSize,
205
+ 'modules.cache.maxSize',
206
+ { min: 1 }
207
+ ),
208
+ strategy: validateEnum(
209
+ process.env.CACHE_STRATEGY ||
210
+ process.env.MORO_CACHE_STRATEGY ||
211
+ DEFAULT_CONFIG.modules.cache.strategy,
212
+ ['lru', 'lfu', 'fifo'] as const,
213
+ 'modules.cache.strategy'
214
+ ),
215
+ },
216
+ rateLimit: {
217
+ enabled: validateBoolean(
218
+ coerceEnvValue(
219
+ process.env.RATE_LIMIT_ENABLED || process.env.MORO_RATE_LIMIT_ENABLED || ''
220
+ ) ?? DEFAULT_CONFIG.modules.rateLimit.enabled,
221
+ 'modules.rateLimit.enabled'
222
+ ),
223
+ defaultRequests: validateNumber(
224
+ coerceEnvValue(
225
+ process.env.RATE_LIMIT_REQUESTS || process.env.MORO_RATE_LIMIT_REQUESTS || ''
226
+ ) || DEFAULT_CONFIG.modules.rateLimit.defaultRequests,
227
+ 'modules.rateLimit.defaultRequests',
228
+ { min: 1 }
229
+ ),
230
+ defaultWindow: validateNumber(
231
+ coerceEnvValue(
232
+ process.env.RATE_LIMIT_WINDOW || process.env.MORO_RATE_LIMIT_WINDOW || ''
233
+ ) || DEFAULT_CONFIG.modules.rateLimit.defaultWindow,
234
+ 'modules.rateLimit.defaultWindow',
235
+ { min: 1000 }
236
+ ),
237
+ skipSuccessfulRequests: validateBoolean(
238
+ coerceEnvValue(
239
+ process.env.RATE_LIMIT_SKIP_SUCCESS || process.env.MORO_RATE_LIMIT_SKIP_SUCCESS || ''
240
+ ) ?? DEFAULT_CONFIG.modules.rateLimit.skipSuccessfulRequests,
241
+ 'modules.rateLimit.skipSuccessfulRequests'
242
+ ),
243
+ skipFailedRequests: validateBoolean(
244
+ coerceEnvValue(
245
+ process.env.RATE_LIMIT_SKIP_FAILED || process.env.MORO_RATE_LIMIT_SKIP_FAILED || ''
246
+ ) ?? DEFAULT_CONFIG.modules.rateLimit.skipFailedRequests,
247
+ 'modules.rateLimit.skipFailedRequests'
248
+ ),
111
249
  },
112
- webhook: {
113
- enabled:
114
- process.env.LOG_WEBHOOK_ENABLED === 'true' || process.env.MORO_LOG_WEBHOOK === 'true',
115
- url: process.env.LOG_WEBHOOK_URL || process.env.MORO_LOG_WEBHOOK_URL,
116
- headers: parseJsonEnv(
117
- process.env.LOG_WEBHOOK_HEADERS || process.env.MORO_LOG_WEBHOOK_HEADERS,
118
- {}
250
+ validation: {
251
+ enabled: validateBoolean(
252
+ coerceEnvValue(
253
+ process.env.VALIDATION_ENABLED || process.env.MORO_VALIDATION_ENABLED || ''
254
+ ) ?? DEFAULT_CONFIG.modules.validation.enabled,
255
+ 'modules.validation.enabled'
256
+ ),
257
+ stripUnknown: validateBoolean(
258
+ coerceEnvValue(
259
+ process.env.VALIDATION_STRIP_UNKNOWN ||
260
+ process.env.MORO_VALIDATION_STRIP_UNKNOWN ||
261
+ ''
262
+ ) ?? DEFAULT_CONFIG.modules.validation.stripUnknown,
263
+ 'modules.validation.stripUnknown'
264
+ ),
265
+ abortEarly: validateBoolean(
266
+ coerceEnvValue(
267
+ process.env.VALIDATION_ABORT_EARLY || process.env.MORO_VALIDATION_ABORT_EARLY || ''
268
+ ) ?? DEFAULT_CONFIG.modules.validation.abortEarly,
269
+ 'modules.validation.abortEarly'
119
270
  ),
120
271
  },
121
272
  },
122
- },
123
273
 
124
- security: {
125
- cors: {
126
- enabled: process.env.CORS_ENABLED !== 'false',
127
- origin: parseArrayOrString(process.env.CORS_ORIGIN || process.env.MORO_CORS_ORIGIN),
128
- methods: parseArrayEnv(process.env.CORS_METHODS || process.env.MORO_CORS_METHODS),
129
- allowedHeaders: parseArrayEnv(process.env.CORS_HEADERS || process.env.MORO_CORS_HEADERS),
130
- credentials: process.env.CORS_CREDENTIALS === 'true',
131
- },
132
- helmet: {
133
- enabled: process.env.HELMET_ENABLED !== 'false',
134
- contentSecurityPolicy: process.env.HELMET_CSP !== 'false',
135
- hsts: process.env.HELMET_HSTS !== 'false',
136
- noSniff: process.env.HELMET_NO_SNIFF !== 'false',
137
- frameguard: process.env.HELMET_FRAMEGUARD !== 'false',
138
- },
139
- rateLimit: {
140
- global: {
141
- enabled: process.env.GLOBAL_RATE_LIMIT_ENABLED === 'true',
142
- requests: process.env.GLOBAL_RATE_LIMIT_REQUESTS || process.env.MORO_GLOBAL_RATE_REQUESTS,
143
- window: process.env.GLOBAL_RATE_LIMIT_WINDOW || process.env.MORO_GLOBAL_RATE_WINDOW,
274
+ logging: {
275
+ level: validateEnum(
276
+ process.env.LOG_LEVEL || process.env.MORO_LOG_LEVEL || DEFAULT_CONFIG.logging.level,
277
+ ['debug', 'info', 'warn', 'error', 'fatal'] as const,
278
+ 'logging.level'
279
+ ),
280
+ format: validateEnum(
281
+ process.env.LOG_FORMAT || process.env.MORO_LOG_FORMAT || DEFAULT_CONFIG.logging.format,
282
+ ['pretty', 'json', 'compact'] as const,
283
+ 'logging.format'
284
+ ),
285
+ enableColors: validateBoolean(
286
+ coerceEnvValue(process.env.LOG_COLORS || process.env.MORO_LOG_COLORS || '') ??
287
+ DEFAULT_CONFIG.logging.enableColors,
288
+ 'logging.enableColors'
289
+ ),
290
+ enableTimestamp: validateBoolean(
291
+ coerceEnvValue(process.env.LOG_TIMESTAMP || process.env.MORO_LOG_TIMESTAMP || '') ??
292
+ DEFAULT_CONFIG.logging.enableTimestamp,
293
+ 'logging.enableTimestamp'
294
+ ),
295
+ enableContext: validateBoolean(
296
+ coerceEnvValue(process.env.LOG_CONTEXT || process.env.MORO_LOG_CONTEXT || '') ??
297
+ DEFAULT_CONFIG.logging.enableContext,
298
+ 'logging.enableContext'
299
+ ),
300
+ outputs: {
301
+ console: validateBoolean(
302
+ coerceEnvValue(process.env.LOG_CONSOLE || process.env.MORO_LOG_CONSOLE || '') ??
303
+ DEFAULT_CONFIG.logging.outputs.console,
304
+ 'logging.outputs.console'
305
+ ),
306
+ file: {
307
+ enabled: validateBoolean(
308
+ coerceEnvValue(
309
+ process.env.LOG_FILE_ENABLED || process.env.MORO_LOG_FILE_ENABLED || ''
310
+ ) ?? DEFAULT_CONFIG.logging.outputs.file.enabled,
311
+ 'logging.outputs.file.enabled'
312
+ ),
313
+ path: validateString(
314
+ process.env.LOG_FILE_PATH ||
315
+ process.env.MORO_LOG_FILE_PATH ||
316
+ DEFAULT_CONFIG.logging.outputs.file.path,
317
+ 'logging.outputs.file.path'
318
+ ),
319
+ maxSize: validateString(
320
+ process.env.LOG_FILE_MAX_SIZE ||
321
+ process.env.MORO_LOG_FILE_MAX_SIZE ||
322
+ DEFAULT_CONFIG.logging.outputs.file.maxSize,
323
+ 'logging.outputs.file.maxSize'
324
+ ),
325
+ maxFiles: validateNumber(
326
+ coerceEnvValue(
327
+ process.env.LOG_FILE_MAX_FILES || process.env.MORO_LOG_FILE_MAX_FILES || ''
328
+ ) || DEFAULT_CONFIG.logging.outputs.file.maxFiles,
329
+ 'logging.outputs.file.maxFiles',
330
+ { min: 1 }
331
+ ),
332
+ },
333
+ webhook: {
334
+ enabled: validateBoolean(
335
+ coerceEnvValue(
336
+ process.env.LOG_WEBHOOK_ENABLED || process.env.MORO_LOG_WEBHOOK_ENABLED || ''
337
+ ) ?? DEFAULT_CONFIG.logging.outputs.webhook.enabled,
338
+ 'logging.outputs.webhook.enabled'
339
+ ),
340
+ url: validateOptional(
341
+ process.env.LOG_WEBHOOK_URL || process.env.MORO_LOG_WEBHOOK_URL,
342
+ validateUrl,
343
+ 'logging.outputs.webhook.url'
344
+ ),
345
+ headers: DEFAULT_CONFIG.logging.outputs.webhook.headers,
346
+ },
144
347
  },
145
348
  },
146
- },
147
349
 
148
- external: {
149
- stripe:
150
- process.env.STRIPE_SECRET_KEY || process.env.MORO_STRIPE_SECRET
151
- ? {
152
- secretKey: process.env.STRIPE_SECRET_KEY || process.env.MORO_STRIPE_SECRET,
153
- publishableKey: process.env.STRIPE_PUBLISHABLE_KEY || process.env.MORO_STRIPE_PUBLIC,
154
- webhookSecret: process.env.STRIPE_WEBHOOK_SECRET || process.env.MORO_STRIPE_WEBHOOK,
155
- apiVersion: process.env.STRIPE_API_VERSION || process.env.MORO_STRIPE_VERSION,
156
- }
157
- : undefined,
158
-
159
- paypal:
160
- process.env.PAYPAL_CLIENT_ID || process.env.MORO_PAYPAL_CLIENT
161
- ? {
162
- clientId: process.env.PAYPAL_CLIENT_ID || process.env.MORO_PAYPAL_CLIENT,
163
- clientSecret: process.env.PAYPAL_CLIENT_SECRET || process.env.MORO_PAYPAL_SECRET,
164
- webhookId: process.env.PAYPAL_WEBHOOK_ID || process.env.MORO_PAYPAL_WEBHOOK,
165
- environment: process.env.PAYPAL_ENVIRONMENT || process.env.MORO_PAYPAL_ENV,
166
- }
167
- : undefined,
168
-
169
- smtp:
170
- process.env.SMTP_HOST || process.env.MORO_SMTP_HOST
171
- ? {
172
- host: process.env.SMTP_HOST || process.env.MORO_SMTP_HOST,
173
- port: process.env.SMTP_PORT || process.env.MORO_SMTP_PORT,
174
- secure: process.env.SMTP_SECURE === 'true',
175
- username: process.env.SMTP_USERNAME || process.env.MORO_SMTP_USER,
176
- password: process.env.SMTP_PASSWORD || process.env.MORO_SMTP_PASS,
177
- }
178
- : undefined,
179
- },
180
-
181
- performance: {
182
- compression: {
183
- enabled: process.env.COMPRESSION_ENABLED !== 'false',
184
- level: process.env.COMPRESSION_LEVEL || process.env.MORO_COMPRESSION_LEVEL,
185
- threshold: process.env.COMPRESSION_THRESHOLD || process.env.MORO_COMPRESSION_THRESHOLD,
186
- },
187
- circuitBreaker: {
188
- enabled: process.env.CIRCUIT_BREAKER_ENABLED !== 'false',
189
- failureThreshold: process.env.CIRCUIT_BREAKER_THRESHOLD || process.env.MORO_CB_THRESHOLD,
190
- resetTimeout: process.env.CIRCUIT_BREAKER_RESET || process.env.MORO_CB_RESET,
191
- monitoringPeriod: process.env.CIRCUIT_BREAKER_MONITOR || process.env.MORO_CB_MONITOR,
350
+ security: {
351
+ cors: {
352
+ enabled: validateBoolean(
353
+ coerceEnvValue(process.env.CORS_ENABLED || process.env.MORO_CORS_ENABLED || '') ??
354
+ DEFAULT_CONFIG.security.cors.enabled,
355
+ 'security.cors.enabled'
356
+ ),
357
+ origin:
358
+ process.env.CORS_ORIGIN ||
359
+ process.env.MORO_CORS_ORIGIN ||
360
+ DEFAULT_CONFIG.security.cors.origin,
361
+ methods: validateStringArray(
362
+ process.env.CORS_METHODS ||
363
+ process.env.MORO_CORS_METHODS ||
364
+ DEFAULT_CONFIG.security.cors.methods,
365
+ 'security.cors.methods'
366
+ ),
367
+ allowedHeaders: validateStringArray(
368
+ process.env.CORS_HEADERS ||
369
+ process.env.MORO_CORS_HEADERS ||
370
+ DEFAULT_CONFIG.security.cors.allowedHeaders,
371
+ 'security.cors.allowedHeaders'
372
+ ),
373
+ credentials: validateBoolean(
374
+ coerceEnvValue(
375
+ process.env.CORS_CREDENTIALS || process.env.MORO_CORS_CREDENTIALS || ''
376
+ ) ?? DEFAULT_CONFIG.security.cors.credentials,
377
+ 'security.cors.credentials'
378
+ ),
379
+ },
380
+ helmet: {
381
+ enabled: validateBoolean(
382
+ coerceEnvValue(process.env.HELMET_ENABLED || process.env.MORO_HELMET_ENABLED || '') ??
383
+ DEFAULT_CONFIG.security.helmet.enabled,
384
+ 'security.helmet.enabled'
385
+ ),
386
+ contentSecurityPolicy: validateBoolean(
387
+ coerceEnvValue(process.env.HELMET_CSP || process.env.MORO_HELMET_CSP || '') ??
388
+ DEFAULT_CONFIG.security.helmet.contentSecurityPolicy,
389
+ 'security.helmet.contentSecurityPolicy'
390
+ ),
391
+ hsts: validateBoolean(
392
+ coerceEnvValue(process.env.HELMET_HSTS || process.env.MORO_HELMET_HSTS || '') ??
393
+ DEFAULT_CONFIG.security.helmet.hsts,
394
+ 'security.helmet.hsts'
395
+ ),
396
+ noSniff: validateBoolean(
397
+ coerceEnvValue(process.env.HELMET_NO_SNIFF || process.env.MORO_HELMET_NO_SNIFF || '') ??
398
+ DEFAULT_CONFIG.security.helmet.noSniff,
399
+ 'security.helmet.noSniff'
400
+ ),
401
+ frameguard: validateBoolean(
402
+ coerceEnvValue(
403
+ process.env.HELMET_FRAMEGUARD || process.env.MORO_HELMET_FRAMEGUARD || ''
404
+ ) ?? DEFAULT_CONFIG.security.helmet.frameguard,
405
+ 'security.helmet.frameguard'
406
+ ),
407
+ },
408
+ rateLimit: {
409
+ global: {
410
+ enabled: validateBoolean(
411
+ coerceEnvValue(
412
+ process.env.GLOBAL_RATE_LIMIT_ENABLED ||
413
+ process.env.MORO_GLOBAL_RATE_LIMIT_ENABLED ||
414
+ ''
415
+ ) ?? DEFAULT_CONFIG.security.rateLimit.global.enabled,
416
+ 'security.rateLimit.global.enabled'
417
+ ),
418
+ requests: validateNumber(
419
+ coerceEnvValue(
420
+ process.env.GLOBAL_RATE_LIMIT_REQUESTS ||
421
+ process.env.MORO_GLOBAL_RATE_LIMIT_REQUESTS ||
422
+ ''
423
+ ) || DEFAULT_CONFIG.security.rateLimit.global.requests,
424
+ 'security.rateLimit.global.requests',
425
+ { min: 1 }
426
+ ),
427
+ window: validateNumber(
428
+ coerceEnvValue(
429
+ process.env.GLOBAL_RATE_LIMIT_WINDOW ||
430
+ process.env.MORO_GLOBAL_RATE_LIMIT_WINDOW ||
431
+ ''
432
+ ) || DEFAULT_CONFIG.security.rateLimit.global.window,
433
+ 'security.rateLimit.global.window',
434
+ { min: 1000 }
435
+ ),
436
+ },
437
+ },
192
438
  },
193
- clustering: {
194
- enabled: process.env.CLUSTERING_ENABLED === 'true',
195
- workers: process.env.CLUSTER_WORKERS || process.env.MORO_WORKERS,
439
+
440
+ external: {
441
+ stripe: {
442
+ secretKey: validateOptional(
443
+ process.env.STRIPE_SECRET_KEY,
444
+ validateString,
445
+ 'external.stripe.secretKey'
446
+ ),
447
+ publishableKey: validateOptional(
448
+ process.env.STRIPE_PUBLISHABLE_KEY,
449
+ validateString,
450
+ 'external.stripe.publishableKey'
451
+ ),
452
+ webhookSecret: validateOptional(
453
+ process.env.STRIPE_WEBHOOK_SECRET,
454
+ validateString,
455
+ 'external.stripe.webhookSecret'
456
+ ),
457
+ apiVersion: validateString(
458
+ process.env.STRIPE_API_VERSION ||
459
+ DEFAULT_CONFIG.external.stripe?.apiVersion ||
460
+ '2023-10-16',
461
+ 'external.stripe.apiVersion'
462
+ ),
463
+ },
464
+ paypal: {
465
+ clientId: validateOptional(
466
+ process.env.PAYPAL_CLIENT_ID,
467
+ validateString,
468
+ 'external.paypal.clientId'
469
+ ),
470
+ clientSecret: validateOptional(
471
+ process.env.PAYPAL_CLIENT_SECRET,
472
+ validateString,
473
+ 'external.paypal.clientSecret'
474
+ ),
475
+ webhookId: validateOptional(
476
+ process.env.PAYPAL_WEBHOOK_ID,
477
+ validateString,
478
+ 'external.paypal.webhookId'
479
+ ),
480
+ environment: validateEnum(
481
+ process.env.PAYPAL_ENVIRONMENT ||
482
+ DEFAULT_CONFIG.external.paypal?.environment ||
483
+ 'sandbox',
484
+ ['sandbox', 'production'] as const,
485
+ 'external.paypal.environment'
486
+ ),
487
+ },
488
+ smtp: {
489
+ host: validateOptional(process.env.SMTP_HOST, validateString, 'external.smtp.host'),
490
+ port: validatePort(
491
+ process.env.SMTP_PORT || (DEFAULT_CONFIG.external.smtp?.port || 587).toString(),
492
+ 'external.smtp.port'
493
+ ),
494
+ secure: validateBoolean(
495
+ coerceEnvValue(process.env.SMTP_SECURE || '') ??
496
+ (DEFAULT_CONFIG.external.smtp?.secure || false),
497
+ 'external.smtp.secure'
498
+ ),
499
+ username: validateOptional(
500
+ process.env.SMTP_USERNAME,
501
+ validateString,
502
+ 'external.smtp.username'
503
+ ),
504
+ password: validateOptional(
505
+ process.env.SMTP_PASSWORD,
506
+ validateString,
507
+ 'external.smtp.password'
508
+ ),
509
+ },
196
510
  },
197
- },
198
- };
199
511
 
200
- // Validate and transform configuration using Zod
201
- try {
202
- const validatedConfig = ConfigSchema.parse(envConfig);
512
+ performance: {
513
+ compression: {
514
+ enabled: validateBoolean(
515
+ coerceEnvValue(
516
+ process.env.COMPRESSION_ENABLED || process.env.MORO_COMPRESSION_ENABLED || ''
517
+ ) ?? DEFAULT_CONFIG.performance.compression.enabled,
518
+ 'performance.compression.enabled'
519
+ ),
520
+ level: validateNumber(
521
+ coerceEnvValue(
522
+ process.env.COMPRESSION_LEVEL || process.env.MORO_COMPRESSION_LEVEL || ''
523
+ ) || DEFAULT_CONFIG.performance.compression.level,
524
+ 'performance.compression.level',
525
+ { min: 1, max: 9 }
526
+ ),
527
+ threshold: validateNumber(
528
+ coerceEnvValue(
529
+ process.env.COMPRESSION_THRESHOLD || process.env.MORO_COMPRESSION_THRESHOLD || ''
530
+ ) || DEFAULT_CONFIG.performance.compression.threshold,
531
+ 'performance.compression.threshold',
532
+ { min: 0 }
533
+ ),
534
+ },
535
+ circuitBreaker: {
536
+ enabled: validateBoolean(
537
+ coerceEnvValue(
538
+ process.env.CIRCUIT_BREAKER_ENABLED || process.env.MORO_CIRCUIT_BREAKER_ENABLED || ''
539
+ ) ?? DEFAULT_CONFIG.performance.circuitBreaker.enabled,
540
+ 'performance.circuitBreaker.enabled'
541
+ ),
542
+ failureThreshold: validateNumber(
543
+ coerceEnvValue(
544
+ process.env.CIRCUIT_BREAKER_THRESHOLD ||
545
+ process.env.MORO_CIRCUIT_BREAKER_THRESHOLD ||
546
+ ''
547
+ ) || DEFAULT_CONFIG.performance.circuitBreaker.failureThreshold,
548
+ 'performance.circuitBreaker.failureThreshold',
549
+ { min: 1 }
550
+ ),
551
+ resetTimeout: validateNumber(
552
+ coerceEnvValue(
553
+ process.env.CIRCUIT_BREAKER_RESET || process.env.MORO_CIRCUIT_BREAKER_RESET || ''
554
+ ) || DEFAULT_CONFIG.performance.circuitBreaker.resetTimeout,
555
+ 'performance.circuitBreaker.resetTimeout',
556
+ { min: 1000 }
557
+ ),
558
+ monitoringPeriod: validateNumber(
559
+ coerceEnvValue(
560
+ process.env.CIRCUIT_BREAKER_MONITOR || process.env.MORO_CIRCUIT_BREAKER_MONITOR || ''
561
+ ) || DEFAULT_CONFIG.performance.circuitBreaker.monitoringPeriod,
562
+ 'performance.circuitBreaker.monitoringPeriod',
563
+ { min: 1000 }
564
+ ),
565
+ },
566
+ clustering: {
567
+ enabled: validateBoolean(
568
+ coerceEnvValue(
569
+ process.env.CLUSTERING_ENABLED || process.env.MORO_CLUSTERING_ENABLED || ''
570
+ ) ?? DEFAULT_CONFIG.performance.clustering.enabled,
571
+ 'performance.clustering.enabled'
572
+ ),
573
+ workers:
574
+ process.env.CLUSTERING_WORKERS === 'auto' ||
575
+ process.env.MORO_CLUSTERING_WORKERS === 'auto'
576
+ ? 'auto'
577
+ : validateNumber(
578
+ coerceEnvValue(
579
+ process.env.CLUSTERING_WORKERS || process.env.MORO_CLUSTERING_WORKERS || ''
580
+ ) || DEFAULT_CONFIG.performance.clustering.workers,
581
+ 'performance.clustering.workers',
582
+ { min: 1 }
583
+ ),
584
+ },
585
+ },
586
+ };
203
587
 
204
- logger.info('Configuration loaded and validated successfully');
588
+ logger.info('Configuration loaded and validated successfully with TypeScript');
205
589
  logger.debug(
206
- 'Configuration details:',
590
+ 'Configuration summary:',
207
591
  JSON.stringify({
208
- server: {
209
- port: validatedConfig.server.port,
210
- environment: validatedConfig.server.environment,
211
- },
592
+ server: { port: config.server.port, environment: config.server.environment },
212
593
  serviceDiscovery: {
213
- enabled: validatedConfig.serviceDiscovery.enabled,
214
- type: validatedConfig.serviceDiscovery.type,
594
+ enabled: config.serviceDiscovery.enabled,
595
+ type: config.serviceDiscovery.type,
215
596
  },
216
597
  modules: {
217
- cacheEnabled: validatedConfig.modules.cache.enabled,
218
- rateLimitEnabled: validatedConfig.modules.rateLimit.enabled,
598
+ cache: config.modules.cache.enabled,
599
+ rateLimit: config.modules.rateLimit.enabled,
600
+ validation: config.modules.validation.enabled,
219
601
  },
220
602
  })
221
603
  );
222
604
 
223
- return validatedConfig;
605
+ return config;
224
606
  } catch (error) {
225
607
  logger.error('❌ Configuration validation failed');
226
608
 
227
- if (error instanceof ZodError) {
228
- logger.error('Configuration errors:');
229
- error.issues.forEach((err: any) => {
230
- const path = err.path.join('.');
231
- logger.error(` - ${path}: ${err.message}`);
609
+ if (error instanceof ConfigValidationError) {
610
+ logger.error(`Configuration error in '${error.field}': ${error.message}`);
611
+ logger.error(` Value: ${JSON.stringify(error.value)}`);
232
612
 
233
- // Provide helpful hints for common errors
234
- if (path.includes('port') && err.code === 'invalid_type') {
235
- logger.error(` Hint: PORT must be a number between 1 and 65535`);
236
- }
237
- if (path.includes('url') && err.code === 'invalid_string') {
238
- logger.error(` Hint: URLs must include protocol (http:// or https://)`);
239
- }
240
- if (path.includes('environment') && err.code === 'invalid_enum_value') {
241
- logger.error(` Hint: NODE_ENV must be one of: development, staging, production`);
242
- }
243
- });
244
-
245
- logger.error('\nConfiguration Help:');
246
- logger.error(' - Use MORO_* prefixed environment variables for framework-specific config');
247
- logger.error(' - Check .env.example for available configuration options');
248
- logger.error(' - See documentation for detailed configuration guide');
613
+ // Provide helpful hints
614
+ if (error.field.includes('port')) {
615
+ logger.error(' Hint: Ports must be numbers between 1 and 65535');
616
+ }
617
+ if (error.field.includes('url')) {
618
+ logger.error(' Hint: URLs must include protocol (http:// or https://)');
619
+ }
620
+ if (error.field.includes('environment')) {
621
+ logger.error(' Hint: NODE_ENV must be one of: development, staging, production');
622
+ }
249
623
  } else {
250
624
  logger.error('Unexpected configuration error:', String(error));
251
625
  }
252
626
 
253
- process.exit(1);
254
- }
255
- }
256
-
257
- /**
258
- * Parse JSON environment variable safely
259
- */
260
- function parseJsonEnv(value: string | undefined, defaultValue: any): any {
261
- if (!value) return defaultValue;
262
-
263
- try {
264
- return JSON.parse(value);
265
- } catch {
266
- logger.warn(`Invalid JSON in environment variable, using default:`, value);
267
- return defaultValue;
268
- }
269
- }
627
+ logger.error('\nConfiguration Help:');
628
+ logger.error(' - Use MORO_* prefixed environment variables for framework-specific config');
629
+ logger.error(' - Check .env.example for available configuration options');
630
+ logger.error(' - See documentation for detailed configuration guide');
270
631
 
271
- /**
272
- * Parse comma-separated array environment variable
273
- */
274
- function parseArrayEnv(value: string | undefined): string[] | undefined {
275
- if (!value) return undefined;
276
- return value
277
- .split(',')
278
- .map(item => item.trim())
279
- .filter(Boolean);
280
- }
281
-
282
- /**
283
- * Parse array or string environment variable
284
- */
285
- function parseArrayOrString(value: string | undefined): string | string[] | boolean | undefined {
286
- if (!value) return undefined;
287
-
288
- // If it contains commas, treat as array
289
- if (value.includes(',')) {
290
- return parseArrayEnv(value);
291
- }
292
-
293
- // Special boolean values
294
- if (value === 'true') return true;
295
- if (value === 'false') return false;
296
-
297
- return value;
298
- }
299
-
300
- /**
301
- * Get environment variable with multiple possible names
302
- */
303
- function getEnvVar(...names: (string | undefined)[]): string | undefined {
304
- for (const name of names) {
305
- if (name && process.env[name]) {
306
- return process.env[name];
307
- }
632
+ process.exit(1);
308
633
  }
309
- return undefined;
310
634
  }