@morojs/moro 1.0.3 → 1.2.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/README.md +57 -2
- package/dist/core/auth/morojs-adapter.d.ts +94 -0
- package/dist/core/auth/morojs-adapter.js +288 -0
- package/dist/core/auth/morojs-adapter.js.map +1 -0
- package/dist/core/config/file-loader.d.ts +18 -0
- package/dist/core/config/file-loader.js +345 -0
- package/dist/core/config/file-loader.js.map +1 -0
- package/dist/core/config/index.d.ts +6 -0
- package/dist/core/config/index.js +15 -0
- package/dist/core/config/index.js.map +1 -1
- package/dist/core/config/loader.d.ts +2 -1
- package/dist/core/config/loader.js +15 -2
- package/dist/core/config/loader.js.map +1 -1
- package/dist/core/config/utils.js +50 -3
- package/dist/core/config/utils.js.map +1 -1
- package/dist/core/http/http-server.d.ts +2 -0
- package/dist/core/http/http-server.js +52 -9
- package/dist/core/http/http-server.js.map +1 -1
- package/dist/core/middleware/built-in/auth-helpers.d.ts +124 -0
- package/dist/core/middleware/built-in/auth-helpers.js +338 -0
- package/dist/core/middleware/built-in/auth-helpers.js.map +1 -0
- package/dist/core/middleware/built-in/auth-providers.d.ts +125 -0
- package/dist/core/middleware/built-in/auth-providers.js +394 -0
- package/dist/core/middleware/built-in/auth-providers.js.map +1 -0
- package/dist/core/middleware/built-in/auth.d.ts +29 -1
- package/dist/core/middleware/built-in/auth.js +259 -16
- package/dist/core/middleware/built-in/auth.js.map +1 -1
- package/dist/core/middleware/built-in/index.d.ts +3 -1
- package/dist/core/middleware/built-in/index.js +19 -1
- package/dist/core/middleware/built-in/index.js.map +1 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +11 -2
- package/dist/index.js.map +1 -1
- package/dist/moro.d.ts +1 -0
- package/dist/moro.js +19 -1
- package/dist/moro.js.map +1 -1
- package/dist/types/auth.d.ts +367 -0
- package/dist/types/auth.js +28 -0
- package/dist/types/auth.js.map +1 -0
- package/package.json +6 -2
- package/src/core/auth/README.md +339 -0
- package/src/core/auth/morojs-adapter.ts +402 -0
- package/src/core/config/file-loader.ts +398 -0
- package/src/core/config/index.ts +18 -0
- package/src/core/config/loader.ts +18 -2
- package/src/core/config/utils.ts +53 -3
- package/src/core/http/http-server.ts +61 -10
- package/src/core/middleware/built-in/auth-helpers.ts +401 -0
- package/src/core/middleware/built-in/auth-providers.ts +480 -0
- package/src/core/middleware/built-in/auth.ts +306 -16
- package/src/core/middleware/built-in/index.ts +22 -0
- package/src/index.ts +30 -1
- package/src/moro.ts +29 -1
- package/src/types/auth.ts +440 -0
- package/tsconfig.json +1 -1
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
// Configuration File Loader - Load moro.config.js/ts files
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { AppConfig } from './schema';
|
|
5
|
+
import { createFrameworkLogger } from '../logger';
|
|
6
|
+
|
|
7
|
+
const logger = createFrameworkLogger('ConfigFile');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Supported configuration file names in order of preference
|
|
11
|
+
*/
|
|
12
|
+
const CONFIG_FILES = ['moro.config.js', 'moro.config.ts'] as const;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Find and load configuration from moro.config.js (synchronously)
|
|
16
|
+
* @param cwd Current working directory to search for config files
|
|
17
|
+
* @returns Partial configuration object or null if no config file found
|
|
18
|
+
*/
|
|
19
|
+
export function loadConfigFileSync(cwd: string = process.cwd()): Partial<AppConfig> | null {
|
|
20
|
+
const configFile = findConfigFile(cwd);
|
|
21
|
+
|
|
22
|
+
if (!configFile) {
|
|
23
|
+
logger.debug('No configuration file found');
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Only support .js files for synchronous loading to avoid complexity
|
|
28
|
+
if (!configFile.endsWith('.js')) {
|
|
29
|
+
logger.debug(
|
|
30
|
+
'Found config file, but only JavaScript files are supported in sync mode. Use loadConfigFile() for TypeScript support.'
|
|
31
|
+
);
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
logger.debug(`Loading configuration from: ${configFile}`);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// Clear module cache to ensure fresh load
|
|
39
|
+
delete require.cache[require.resolve(configFile)];
|
|
40
|
+
|
|
41
|
+
const config = require(configFile);
|
|
42
|
+
const configData = config.default || config;
|
|
43
|
+
|
|
44
|
+
if (!configData || typeof configData !== 'object') {
|
|
45
|
+
logger.warn(`Configuration file ${configFile} did not export a valid configuration object`);
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
logger.info(`Configuration loaded from: ${configFile}`);
|
|
50
|
+
return configData;
|
|
51
|
+
} catch (error) {
|
|
52
|
+
logger.error(`Failed to load configuration file ${configFile}:`, String(error));
|
|
53
|
+
logger.warn('Falling back to environment variable configuration');
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Find and load configuration from moro.config.js or moro.config.ts (async)
|
|
60
|
+
* @param cwd Current working directory to search for config files
|
|
61
|
+
* @returns Partial configuration object or null if no config file found
|
|
62
|
+
*/
|
|
63
|
+
export async function loadConfigFile(
|
|
64
|
+
cwd: string = process.cwd()
|
|
65
|
+
): Promise<Partial<AppConfig> | null> {
|
|
66
|
+
const configFile = findConfigFile(cwd);
|
|
67
|
+
|
|
68
|
+
if (!configFile) {
|
|
69
|
+
logger.debug('No configuration file found');
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
logger.debug(`Loading configuration from: ${configFile}`);
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const config = await importConfigFile(configFile);
|
|
77
|
+
|
|
78
|
+
if (!config || typeof config !== 'object') {
|
|
79
|
+
logger.warn(`Configuration file ${configFile} did not export a valid configuration object`);
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
logger.info(`Configuration loaded from: ${configFile}`);
|
|
84
|
+
return config;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
logger.error(`Failed to load configuration file ${configFile}:`, String(error));
|
|
87
|
+
logger.warn('Falling back to environment variable configuration');
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Find the first existing configuration file in the given directory
|
|
94
|
+
*/
|
|
95
|
+
function findConfigFile(cwd: string): string | null {
|
|
96
|
+
for (const fileName of CONFIG_FILES) {
|
|
97
|
+
const filePath = join(cwd, fileName);
|
|
98
|
+
if (existsSync(filePath)) {
|
|
99
|
+
return filePath;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Dynamically import a configuration file
|
|
107
|
+
*/
|
|
108
|
+
async function importConfigFile(filePath: string): Promise<Partial<AppConfig> | null> {
|
|
109
|
+
const isTypeScript = filePath.endsWith('.ts');
|
|
110
|
+
|
|
111
|
+
if (isTypeScript) {
|
|
112
|
+
// For TypeScript files, we need to handle ts-node/tsx or similar
|
|
113
|
+
await setupTypeScriptLoader();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
// Use dynamic import to load the configuration
|
|
118
|
+
const configModule = await import(filePath);
|
|
119
|
+
|
|
120
|
+
// Handle both default export and module.exports
|
|
121
|
+
const config = configModule.default || configModule;
|
|
122
|
+
|
|
123
|
+
return config;
|
|
124
|
+
} catch (error) {
|
|
125
|
+
// If TypeScript loading fails, provide helpful error message
|
|
126
|
+
if (
|
|
127
|
+
isTypeScript &&
|
|
128
|
+
error instanceof Error &&
|
|
129
|
+
error.message.includes('Unknown file extension')
|
|
130
|
+
) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`Failed to load TypeScript config file. Make sure you have ts-node installed: npm install --save-dev ts-node`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Setup TypeScript loader for .ts config files
|
|
141
|
+
*/
|
|
142
|
+
async function setupTypeScriptLoader(): Promise<void> {
|
|
143
|
+
try {
|
|
144
|
+
// Try to register ts-node if available
|
|
145
|
+
const tsNode = await import('ts-node');
|
|
146
|
+
if (!tsNode.register) {
|
|
147
|
+
// ts-node might already be registered
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
tsNode.register({
|
|
152
|
+
transpileOnly: true,
|
|
153
|
+
compilerOptions: {
|
|
154
|
+
module: 'commonjs',
|
|
155
|
+
target: 'es2020',
|
|
156
|
+
moduleResolution: 'node',
|
|
157
|
+
allowSyntheticDefaultImports: true,
|
|
158
|
+
esModuleInterop: true,
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
} catch (error) {
|
|
162
|
+
// ts-node not available, try other methods or fail gracefully
|
|
163
|
+
logger.debug('ts-node not available for TypeScript config loading');
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Convert a configuration object to environment variable mappings
|
|
169
|
+
* This function flattens the config object and sets corresponding environment variables
|
|
170
|
+
*/
|
|
171
|
+
export function applyConfigAsEnvironmentVariables(config: Partial<AppConfig>): void {
|
|
172
|
+
if (!config || typeof config !== 'object') {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Apply server configuration
|
|
177
|
+
if (config.server) {
|
|
178
|
+
setEnvIfNotSet('PORT', config.server.port?.toString());
|
|
179
|
+
setEnvIfNotSet('HOST', config.server.host);
|
|
180
|
+
setEnvIfNotSet('NODE_ENV', config.server.environment);
|
|
181
|
+
setEnvIfNotSet('MAX_CONNECTIONS', config.server.maxConnections?.toString());
|
|
182
|
+
setEnvIfNotSet('REQUEST_TIMEOUT', config.server.timeout?.toString());
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Apply database configuration
|
|
186
|
+
if (config.database) {
|
|
187
|
+
setEnvIfNotSet('DATABASE_URL', config.database.url);
|
|
188
|
+
|
|
189
|
+
if (config.database.redis) {
|
|
190
|
+
setEnvIfNotSet('REDIS_URL', config.database.redis.url);
|
|
191
|
+
setEnvIfNotSet('REDIS_MAX_RETRIES', config.database.redis.maxRetries?.toString());
|
|
192
|
+
setEnvIfNotSet('REDIS_RETRY_DELAY', config.database.redis.retryDelay?.toString());
|
|
193
|
+
setEnvIfNotSet('REDIS_KEY_PREFIX', config.database.redis.keyPrefix);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (config.database.mysql) {
|
|
197
|
+
setEnvIfNotSet('MYSQL_HOST', config.database.mysql.host);
|
|
198
|
+
setEnvIfNotSet('MYSQL_PORT', config.database.mysql.port?.toString());
|
|
199
|
+
setEnvIfNotSet('MYSQL_DATABASE', config.database.mysql.database);
|
|
200
|
+
setEnvIfNotSet('MYSQL_USERNAME', config.database.mysql.username);
|
|
201
|
+
setEnvIfNotSet('MYSQL_PASSWORD', config.database.mysql.password);
|
|
202
|
+
setEnvIfNotSet('MYSQL_CONNECTION_LIMIT', config.database.mysql.connectionLimit?.toString());
|
|
203
|
+
setEnvIfNotSet('MYSQL_ACQUIRE_TIMEOUT', config.database.mysql.acquireTimeout?.toString());
|
|
204
|
+
setEnvIfNotSet('MYSQL_TIMEOUT', config.database.mysql.timeout?.toString());
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Apply service discovery configuration
|
|
209
|
+
if (config.serviceDiscovery) {
|
|
210
|
+
setEnvIfNotSet('SERVICE_DISCOVERY_ENABLED', config.serviceDiscovery.enabled?.toString());
|
|
211
|
+
setEnvIfNotSet('DISCOVERY_TYPE', config.serviceDiscovery.type);
|
|
212
|
+
setEnvIfNotSet('CONSUL_URL', config.serviceDiscovery.consulUrl);
|
|
213
|
+
setEnvIfNotSet('K8S_NAMESPACE', config.serviceDiscovery.kubernetesNamespace);
|
|
214
|
+
setEnvIfNotSet(
|
|
215
|
+
'HEALTH_CHECK_INTERVAL',
|
|
216
|
+
config.serviceDiscovery.healthCheckInterval?.toString()
|
|
217
|
+
);
|
|
218
|
+
setEnvIfNotSet('DISCOVERY_RETRY_ATTEMPTS', config.serviceDiscovery.retryAttempts?.toString());
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Apply logging configuration
|
|
222
|
+
if (config.logging) {
|
|
223
|
+
setEnvIfNotSet('LOG_LEVEL', config.logging.level);
|
|
224
|
+
setEnvIfNotSet('LOG_FORMAT', config.logging.format);
|
|
225
|
+
setEnvIfNotSet('LOG_COLORS', config.logging.enableColors?.toString());
|
|
226
|
+
setEnvIfNotSet('LOG_TIMESTAMP', config.logging.enableTimestamp?.toString());
|
|
227
|
+
setEnvIfNotSet('LOG_CONTEXT', config.logging.enableContext?.toString());
|
|
228
|
+
|
|
229
|
+
if (config.logging.outputs) {
|
|
230
|
+
setEnvIfNotSet('LOG_CONSOLE', config.logging.outputs.console?.toString());
|
|
231
|
+
|
|
232
|
+
if (config.logging.outputs.file) {
|
|
233
|
+
setEnvIfNotSet('LOG_FILE_ENABLED', config.logging.outputs.file.enabled?.toString());
|
|
234
|
+
setEnvIfNotSet('LOG_FILE_PATH', config.logging.outputs.file.path);
|
|
235
|
+
setEnvIfNotSet('LOG_FILE_MAX_SIZE', config.logging.outputs.file.maxSize);
|
|
236
|
+
setEnvIfNotSet('LOG_FILE_MAX_FILES', config.logging.outputs.file.maxFiles?.toString());
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (config.logging.outputs.webhook) {
|
|
240
|
+
setEnvIfNotSet('LOG_WEBHOOK_ENABLED', config.logging.outputs.webhook.enabled?.toString());
|
|
241
|
+
setEnvIfNotSet('LOG_WEBHOOK_URL', config.logging.outputs.webhook.url);
|
|
242
|
+
if (config.logging.outputs.webhook.headers) {
|
|
243
|
+
setEnvIfNotSet(
|
|
244
|
+
'LOG_WEBHOOK_HEADERS',
|
|
245
|
+
JSON.stringify(config.logging.outputs.webhook.headers)
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Apply module defaults
|
|
253
|
+
if (config.modules) {
|
|
254
|
+
if (config.modules.cache) {
|
|
255
|
+
setEnvIfNotSet('CACHE_ENABLED', config.modules.cache.enabled?.toString());
|
|
256
|
+
setEnvIfNotSet('DEFAULT_CACHE_TTL', config.modules.cache.defaultTtl?.toString());
|
|
257
|
+
setEnvIfNotSet('CACHE_MAX_SIZE', config.modules.cache.maxSize?.toString());
|
|
258
|
+
setEnvIfNotSet('CACHE_STRATEGY', config.modules.cache.strategy);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (config.modules.rateLimit) {
|
|
262
|
+
setEnvIfNotSet('RATE_LIMIT_ENABLED', config.modules.rateLimit.enabled?.toString());
|
|
263
|
+
setEnvIfNotSet(
|
|
264
|
+
'DEFAULT_RATE_LIMIT_REQUESTS',
|
|
265
|
+
config.modules.rateLimit.defaultRequests?.toString()
|
|
266
|
+
);
|
|
267
|
+
setEnvIfNotSet(
|
|
268
|
+
'DEFAULT_RATE_LIMIT_WINDOW',
|
|
269
|
+
config.modules.rateLimit.defaultWindow?.toString()
|
|
270
|
+
);
|
|
271
|
+
setEnvIfNotSet(
|
|
272
|
+
'RATE_LIMIT_SKIP_SUCCESS',
|
|
273
|
+
config.modules.rateLimit.skipSuccessfulRequests?.toString()
|
|
274
|
+
);
|
|
275
|
+
setEnvIfNotSet(
|
|
276
|
+
'RATE_LIMIT_SKIP_FAILED',
|
|
277
|
+
config.modules.rateLimit.skipFailedRequests?.toString()
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (config.modules.validation) {
|
|
282
|
+
setEnvIfNotSet('VALIDATION_ENABLED', config.modules.validation.enabled?.toString());
|
|
283
|
+
setEnvIfNotSet(
|
|
284
|
+
'VALIDATION_STRIP_UNKNOWN',
|
|
285
|
+
config.modules.validation.stripUnknown?.toString()
|
|
286
|
+
);
|
|
287
|
+
setEnvIfNotSet('VALIDATION_ABORT_EARLY', config.modules.validation.abortEarly?.toString());
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Apply security configuration
|
|
292
|
+
if (config.security) {
|
|
293
|
+
if (config.security.cors) {
|
|
294
|
+
setEnvIfNotSet('CORS_ENABLED', config.security.cors.enabled?.toString());
|
|
295
|
+
if (typeof config.security.cors.origin === 'string') {
|
|
296
|
+
setEnvIfNotSet('CORS_ORIGIN', config.security.cors.origin);
|
|
297
|
+
} else if (Array.isArray(config.security.cors.origin)) {
|
|
298
|
+
setEnvIfNotSet('CORS_ORIGIN', config.security.cors.origin.join(','));
|
|
299
|
+
} else if (typeof config.security.cors.origin === 'boolean') {
|
|
300
|
+
setEnvIfNotSet('CORS_ORIGIN', config.security.cors.origin.toString());
|
|
301
|
+
}
|
|
302
|
+
setEnvIfNotSet('CORS_METHODS', config.security.cors.methods?.join(','));
|
|
303
|
+
setEnvIfNotSet('CORS_HEADERS', config.security.cors.allowedHeaders?.join(','));
|
|
304
|
+
setEnvIfNotSet('CORS_CREDENTIALS', config.security.cors.credentials?.toString());
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (config.security.helmet) {
|
|
308
|
+
setEnvIfNotSet('HELMET_ENABLED', config.security.helmet.enabled?.toString());
|
|
309
|
+
setEnvIfNotSet('HELMET_CSP', config.security.helmet.contentSecurityPolicy?.toString());
|
|
310
|
+
setEnvIfNotSet('HELMET_HSTS', config.security.helmet.hsts?.toString());
|
|
311
|
+
setEnvIfNotSet('HELMET_NO_SNIFF', config.security.helmet.noSniff?.toString());
|
|
312
|
+
setEnvIfNotSet('HELMET_FRAMEGUARD', config.security.helmet.frameguard?.toString());
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (config.security.rateLimit?.global) {
|
|
316
|
+
setEnvIfNotSet(
|
|
317
|
+
'GLOBAL_RATE_LIMIT_ENABLED',
|
|
318
|
+
config.security.rateLimit.global.enabled?.toString()
|
|
319
|
+
);
|
|
320
|
+
setEnvIfNotSet(
|
|
321
|
+
'GLOBAL_RATE_LIMIT_REQUESTS',
|
|
322
|
+
config.security.rateLimit.global.requests?.toString()
|
|
323
|
+
);
|
|
324
|
+
setEnvIfNotSet(
|
|
325
|
+
'GLOBAL_RATE_LIMIT_WINDOW',
|
|
326
|
+
config.security.rateLimit.global.window?.toString()
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Apply external services configuration
|
|
332
|
+
if (config.external) {
|
|
333
|
+
if (config.external.stripe) {
|
|
334
|
+
setEnvIfNotSet('STRIPE_SECRET_KEY', config.external.stripe.secretKey);
|
|
335
|
+
setEnvIfNotSet('STRIPE_PUBLISHABLE_KEY', config.external.stripe.publishableKey);
|
|
336
|
+
setEnvIfNotSet('STRIPE_WEBHOOK_SECRET', config.external.stripe.webhookSecret);
|
|
337
|
+
setEnvIfNotSet('STRIPE_API_VERSION', config.external.stripe.apiVersion);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (config.external.paypal) {
|
|
341
|
+
setEnvIfNotSet('PAYPAL_CLIENT_ID', config.external.paypal.clientId);
|
|
342
|
+
setEnvIfNotSet('PAYPAL_CLIENT_SECRET', config.external.paypal.clientSecret);
|
|
343
|
+
setEnvIfNotSet('PAYPAL_WEBHOOK_ID', config.external.paypal.webhookId);
|
|
344
|
+
setEnvIfNotSet('PAYPAL_ENVIRONMENT', config.external.paypal.environment);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (config.external.smtp) {
|
|
348
|
+
setEnvIfNotSet('SMTP_HOST', config.external.smtp.host);
|
|
349
|
+
setEnvIfNotSet('SMTP_PORT', config.external.smtp.port?.toString());
|
|
350
|
+
setEnvIfNotSet('SMTP_SECURE', config.external.smtp.secure?.toString());
|
|
351
|
+
setEnvIfNotSet('SMTP_USERNAME', config.external.smtp.username);
|
|
352
|
+
setEnvIfNotSet('SMTP_PASSWORD', config.external.smtp.password);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Apply performance configuration
|
|
357
|
+
if (config.performance) {
|
|
358
|
+
if (config.performance.compression) {
|
|
359
|
+
setEnvIfNotSet('COMPRESSION_ENABLED', config.performance.compression.enabled?.toString());
|
|
360
|
+
setEnvIfNotSet('COMPRESSION_LEVEL', config.performance.compression.level?.toString());
|
|
361
|
+
setEnvIfNotSet('COMPRESSION_THRESHOLD', config.performance.compression.threshold?.toString());
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (config.performance.circuitBreaker) {
|
|
365
|
+
setEnvIfNotSet(
|
|
366
|
+
'CIRCUIT_BREAKER_ENABLED',
|
|
367
|
+
config.performance.circuitBreaker.enabled?.toString()
|
|
368
|
+
);
|
|
369
|
+
setEnvIfNotSet(
|
|
370
|
+
'CIRCUIT_BREAKER_THRESHOLD',
|
|
371
|
+
config.performance.circuitBreaker.failureThreshold?.toString()
|
|
372
|
+
);
|
|
373
|
+
setEnvIfNotSet(
|
|
374
|
+
'CIRCUIT_BREAKER_RESET',
|
|
375
|
+
config.performance.circuitBreaker.resetTimeout?.toString()
|
|
376
|
+
);
|
|
377
|
+
setEnvIfNotSet(
|
|
378
|
+
'CIRCUIT_BREAKER_MONITOR',
|
|
379
|
+
config.performance.circuitBreaker.monitoringPeriod?.toString()
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (config.performance.clustering) {
|
|
384
|
+
setEnvIfNotSet('CLUSTERING_ENABLED', config.performance.clustering.enabled?.toString());
|
|
385
|
+
setEnvIfNotSet('CLUSTER_WORKERS', config.performance.clustering.workers?.toString());
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Set environment variable only if it's not already set
|
|
392
|
+
* This ensures environment variables take precedence over config file values
|
|
393
|
+
*/
|
|
394
|
+
function setEnvIfNotSet(key: string, value: string | undefined): void {
|
|
395
|
+
if (value !== undefined && process.env[key] === undefined) {
|
|
396
|
+
process.env[key] = value;
|
|
397
|
+
}
|
|
398
|
+
}
|
package/src/core/config/index.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
export * from './schema';
|
|
3
3
|
export * from './loader';
|
|
4
4
|
export * from './utils';
|
|
5
|
+
export * from './file-loader';
|
|
5
6
|
|
|
6
7
|
// Re-export common Zod utilities for configuration
|
|
7
8
|
export { z } from 'zod';
|
|
@@ -9,6 +10,7 @@ export { z } from 'zod';
|
|
|
9
10
|
// Main configuration loading function
|
|
10
11
|
import { loadConfig } from './loader';
|
|
11
12
|
import type { AppConfig } from './schema';
|
|
13
|
+
import { setConfig } from './utils';
|
|
12
14
|
|
|
13
15
|
// Global configuration instance
|
|
14
16
|
let globalConfig: AppConfig | null = null;
|
|
@@ -23,6 +25,10 @@ export function initializeConfig(): AppConfig {
|
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
globalConfig = loadConfig();
|
|
28
|
+
|
|
29
|
+
// Also set the config for utils functions
|
|
30
|
+
setConfig(globalConfig);
|
|
31
|
+
|
|
26
32
|
return globalConfig;
|
|
27
33
|
}
|
|
28
34
|
|
|
@@ -43,3 +49,15 @@ export function getGlobalConfig(): AppConfig {
|
|
|
43
49
|
export function isConfigInitialized(): boolean {
|
|
44
50
|
return globalConfig !== null;
|
|
45
51
|
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Reset the global configuration state (for testing purposes)
|
|
55
|
+
* @internal
|
|
56
|
+
*/
|
|
57
|
+
export function resetConfig(): void {
|
|
58
|
+
globalConfig = null;
|
|
59
|
+
|
|
60
|
+
// Also reset the utils config (by setting it to null via direct access)
|
|
61
|
+
const { setConfig } = require('./utils');
|
|
62
|
+
setConfig(null as any);
|
|
63
|
+
}
|
|
@@ -2,15 +2,31 @@
|
|
|
2
2
|
import { ZodError } from 'zod';
|
|
3
3
|
import { ConfigSchema, AppConfig } from './schema';
|
|
4
4
|
import { createFrameworkLogger } from '../logger';
|
|
5
|
+
import { loadConfigFileSync, applyConfigAsEnvironmentVariables } from './file-loader';
|
|
5
6
|
|
|
6
7
|
const logger = createFrameworkLogger('Config');
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
|
-
* Load and validate configuration from environment variables
|
|
10
|
+
* Load and validate configuration from config files and environment variables
|
|
11
|
+
* Priority: Environment Variables > Config File > Schema Defaults
|
|
10
12
|
* @returns Validated and typed application configuration
|
|
11
13
|
*/
|
|
12
14
|
export function loadConfig(): AppConfig {
|
|
13
|
-
logger.debug('Loading configuration from environment variables');
|
|
15
|
+
logger.debug('Loading configuration from config files and environment variables');
|
|
16
|
+
|
|
17
|
+
// First, try to load from config file and apply as environment variables (synchronously)
|
|
18
|
+
try {
|
|
19
|
+
const fileConfig = loadConfigFileSync();
|
|
20
|
+
if (fileConfig) {
|
|
21
|
+
logger.debug('Applying config file values as environment variables');
|
|
22
|
+
applyConfigAsEnvironmentVariables(fileConfig);
|
|
23
|
+
}
|
|
24
|
+
} catch (error) {
|
|
25
|
+
logger.warn(
|
|
26
|
+
'Config file loading failed, continuing with environment variables only:',
|
|
27
|
+
String(error)
|
|
28
|
+
);
|
|
29
|
+
}
|
|
14
30
|
|
|
15
31
|
// Map environment variables to configuration structure
|
|
16
32
|
const envConfig = {
|
package/src/core/config/utils.ts
CHANGED
|
@@ -26,6 +26,42 @@ export function getConfig(): AppConfig {
|
|
|
26
26
|
return appConfig;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Coerce environment variable string values to appropriate types
|
|
31
|
+
*/
|
|
32
|
+
function coerceEnvironmentValue(value: string): any {
|
|
33
|
+
// Handle boolean values
|
|
34
|
+
if (value.toLowerCase() === 'true') return true;
|
|
35
|
+
if (value.toLowerCase() === 'false') return false;
|
|
36
|
+
|
|
37
|
+
// Handle numeric values
|
|
38
|
+
if (/^\d+$/.test(value)) {
|
|
39
|
+
const num = parseInt(value, 10);
|
|
40
|
+
return num;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (/^\d+\.\d+$/.test(value)) {
|
|
44
|
+
const num = parseFloat(value);
|
|
45
|
+
return num;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Handle JSON objects/arrays
|
|
49
|
+
if (
|
|
50
|
+
(value.startsWith('{') && value.endsWith('}')) ||
|
|
51
|
+
(value.startsWith('[') && value.endsWith(']'))
|
|
52
|
+
) {
|
|
53
|
+
try {
|
|
54
|
+
return JSON.parse(value);
|
|
55
|
+
} catch {
|
|
56
|
+
// If JSON parsing fails, return as string
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Return as string for all other cases
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
|
|
29
65
|
/**
|
|
30
66
|
* Create module-specific configuration with environment override support
|
|
31
67
|
*/
|
|
@@ -34,9 +70,17 @@ export function createModuleConfig<T>(
|
|
|
34
70
|
defaultConfig: Partial<T>,
|
|
35
71
|
envPrefix?: string
|
|
36
72
|
): T {
|
|
37
|
-
|
|
73
|
+
// Try to get global config, but don't fail if not initialized
|
|
74
|
+
let globalConfig = {};
|
|
75
|
+
try {
|
|
76
|
+
const { getGlobalConfig } = require('./index');
|
|
77
|
+
globalConfig = getGlobalConfig();
|
|
78
|
+
} catch {
|
|
79
|
+
// Global config not initialized - use empty object (module config can still work independently)
|
|
80
|
+
globalConfig = {};
|
|
81
|
+
}
|
|
38
82
|
|
|
39
|
-
// Build environment configuration object
|
|
83
|
+
// Build environment configuration object with type coercion
|
|
40
84
|
const envConfig: Record<string, any> = {};
|
|
41
85
|
|
|
42
86
|
if (envPrefix) {
|
|
@@ -48,14 +92,20 @@ export function createModuleConfig<T>(
|
|
|
48
92
|
.toLowerCase()
|
|
49
93
|
.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
50
94
|
|
|
51
|
-
|
|
95
|
+
const envValue = process.env[key];
|
|
96
|
+
if (envValue !== undefined) {
|
|
97
|
+
// Attempt basic type coercion for common types
|
|
98
|
+
envConfig[configKey] = coerceEnvironmentValue(envValue);
|
|
99
|
+
}
|
|
52
100
|
}
|
|
53
101
|
});
|
|
54
102
|
}
|
|
55
103
|
|
|
56
104
|
// Merge default config, global defaults, and environment overrides
|
|
105
|
+
// Priority: environment variables > global config > default config
|
|
57
106
|
const mergedConfig = {
|
|
58
107
|
...defaultConfig,
|
|
108
|
+
...globalConfig, // Now actually using global config!
|
|
59
109
|
...envConfig,
|
|
60
110
|
};
|
|
61
111
|
|
|
@@ -16,6 +16,7 @@ export class MoroHttpServer {
|
|
|
16
16
|
private compressionEnabled = true;
|
|
17
17
|
private compressionThreshold = 1024;
|
|
18
18
|
private logger = createFrameworkLogger('HttpServer');
|
|
19
|
+
private hookManager: any;
|
|
19
20
|
|
|
20
21
|
constructor() {
|
|
21
22
|
this.server = createServer(this.handleRequest.bind(this));
|
|
@@ -26,6 +27,11 @@ export class MoroHttpServer {
|
|
|
26
27
|
this.globalMiddleware.push(middleware);
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
// Set hooks manager for request processing
|
|
31
|
+
setHookManager(hookManager: any): void {
|
|
32
|
+
this.hookManager = hookManager;
|
|
33
|
+
}
|
|
34
|
+
|
|
29
35
|
// Routing methods
|
|
30
36
|
get(path: string, ...handlers: (Middleware | HttpHandler)[]): void {
|
|
31
37
|
this.addRoute('GET', path, handlers);
|
|
@@ -94,6 +100,14 @@ export class MoroHttpServer {
|
|
|
94
100
|
httpReq.body = await this.parseBody(req);
|
|
95
101
|
}
|
|
96
102
|
|
|
103
|
+
// Execute hooks before request processing
|
|
104
|
+
if (this.hookManager) {
|
|
105
|
+
await this.hookManager.execute('request', {
|
|
106
|
+
request: httpReq,
|
|
107
|
+
response: httpRes,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
97
111
|
// Execute global middleware first
|
|
98
112
|
await this.executeMiddleware(this.globalMiddleware, httpReq, httpRes);
|
|
99
113
|
|
|
@@ -124,6 +138,14 @@ export class MoroHttpServer {
|
|
|
124
138
|
// Execute handler
|
|
125
139
|
await route.handler(httpReq, httpRes);
|
|
126
140
|
} catch (error) {
|
|
141
|
+
// Debug: Log the actual error and where it came from
|
|
142
|
+
console.log('🚨 MoroJS Request Error Details:');
|
|
143
|
+
console.log('📍 Error type:', typeof error);
|
|
144
|
+
console.log('📍 Error message:', error instanceof Error ? error.message : String(error));
|
|
145
|
+
console.log('📍 Error stack:', error instanceof Error ? error.stack : 'No stack trace');
|
|
146
|
+
console.log('📍 Request path:', req.url);
|
|
147
|
+
console.log('📍 Request method:', req.method);
|
|
148
|
+
|
|
127
149
|
this.logger.error('Request error', 'RequestHandler', {
|
|
128
150
|
error: error instanceof Error ? error.message : String(error),
|
|
129
151
|
requestId: httpReq.requestId,
|
|
@@ -132,11 +154,39 @@ export class MoroHttpServer {
|
|
|
132
154
|
});
|
|
133
155
|
|
|
134
156
|
if (!httpRes.headersSent) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
157
|
+
// Ensure response is properly enhanced before using custom methods
|
|
158
|
+
if (typeof httpRes.status === 'function' && typeof httpRes.json === 'function') {
|
|
159
|
+
httpRes.status(500).json({
|
|
160
|
+
success: false,
|
|
161
|
+
error: 'Internal server error',
|
|
162
|
+
requestId: httpReq.requestId,
|
|
163
|
+
});
|
|
164
|
+
} else {
|
|
165
|
+
// Ultra-defensive fallback - check each method individually
|
|
166
|
+
if (typeof httpRes.setHeader === 'function') {
|
|
167
|
+
httpRes.statusCode = 500;
|
|
168
|
+
httpRes.setHeader('Content-Type', 'application/json');
|
|
169
|
+
} else {
|
|
170
|
+
// Even setHeader doesn't exist - object is completely wrong
|
|
171
|
+
console.error(
|
|
172
|
+
'❌ Response object is not a proper ServerResponse:',
|
|
173
|
+
typeof httpRes,
|
|
174
|
+
Object.keys(httpRes)
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (typeof httpRes.end === 'function') {
|
|
179
|
+
httpRes.end(
|
|
180
|
+
JSON.stringify({
|
|
181
|
+
success: false,
|
|
182
|
+
error: 'Internal server error',
|
|
183
|
+
requestId: httpReq.requestId,
|
|
184
|
+
})
|
|
185
|
+
);
|
|
186
|
+
} else {
|
|
187
|
+
console.error('❌ Cannot send error response - end() method missing');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
140
190
|
}
|
|
141
191
|
}
|
|
142
192
|
}
|
|
@@ -174,6 +224,12 @@ export class MoroHttpServer {
|
|
|
174
224
|
private enhanceResponse(res: ServerResponse): HttpResponse {
|
|
175
225
|
const httpRes = res as HttpResponse;
|
|
176
226
|
|
|
227
|
+
// BULLETPROOF status method - always works
|
|
228
|
+
httpRes.status = (code: number) => {
|
|
229
|
+
httpRes.statusCode = code;
|
|
230
|
+
return httpRes;
|
|
231
|
+
};
|
|
232
|
+
|
|
177
233
|
httpRes.json = async (data: any) => {
|
|
178
234
|
if (httpRes.headersSent) return;
|
|
179
235
|
|
|
@@ -205,11 +261,6 @@ export class MoroHttpServer {
|
|
|
205
261
|
httpRes.end(buffer);
|
|
206
262
|
};
|
|
207
263
|
|
|
208
|
-
httpRes.status = (code: number) => {
|
|
209
|
-
httpRes.statusCode = code;
|
|
210
|
-
return httpRes;
|
|
211
|
-
};
|
|
212
|
-
|
|
213
264
|
httpRes.send = (data: string | Buffer) => {
|
|
214
265
|
if (httpRes.headersSent) return;
|
|
215
266
|
|