aegisnode 0.0.1

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.
@@ -0,0 +1,37 @@
1
+ class MemoryCache {
2
+ constructor() {
3
+ this.storage = new Map();
4
+ }
5
+
6
+ set(key, value) {
7
+ this.storage.set(key, value);
8
+ return value;
9
+ }
10
+
11
+ get(key) {
12
+ return this.storage.get(key);
13
+ }
14
+
15
+ delete(key) {
16
+ return this.storage.delete(key);
17
+ }
18
+
19
+ clear() {
20
+ this.storage.clear();
21
+ }
22
+ }
23
+
24
+ export function createCache(cacheConfig, logger) {
25
+ if (!cacheConfig?.enabled) {
26
+ logger.info('Cache disabled by configuration.');
27
+ return null;
28
+ }
29
+
30
+ const driver = String(cacheConfig.driver || 'memory').toLowerCase();
31
+ if (driver !== 'memory') {
32
+ throw new Error(`Unsupported cache driver: ${driver}. Start with memory and add custom loader support.`);
33
+ }
34
+
35
+ logger.info('Cache initialized with driver %s', driver);
36
+ return new MemoryCache();
37
+ }
@@ -0,0 +1,482 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { pathToFileURL } from 'url';
4
+
5
+ const BASE_PROCESS_ENV = new Map(Object.entries(process.env));
6
+ const FRAMEWORK_LOADED_ENV_KEYS = new Set();
7
+
8
+ function isPlainObject(value) {
9
+ return Object.prototype.toString.call(value) === '[object Object]';
10
+ }
11
+
12
+ function normalizeEnvironmentName(value, fallback = 'development') {
13
+ if (typeof value === 'string' && value.trim().length > 0) {
14
+ return value.trim();
15
+ }
16
+ return fallback;
17
+ }
18
+
19
+ function resetFrameworkLoadedEnv() {
20
+ for (const key of FRAMEWORK_LOADED_ENV_KEYS) {
21
+ if (BASE_PROCESS_ENV.has(key)) {
22
+ process.env[key] = BASE_PROCESS_ENV.get(key);
23
+ continue;
24
+ }
25
+ delete process.env[key];
26
+ }
27
+
28
+ FRAMEWORK_LOADED_ENV_KEYS.clear();
29
+ }
30
+
31
+ function decodeQuotedEnvValue(value, quote) {
32
+ const inner = value.slice(1, -1);
33
+
34
+ if (quote === "'") {
35
+ return inner;
36
+ }
37
+
38
+ return inner
39
+ .replace(/\\n/g, '\n')
40
+ .replace(/\\r/g, '\r')
41
+ .replace(/\\t/g, '\t')
42
+ .replace(/\\"/g, '"')
43
+ .replace(/\\\\/g, '\\');
44
+ }
45
+
46
+ function normalizeUnquotedEnvValue(value) {
47
+ const commentIndex = value.search(/\s#/);
48
+ const rawValue = commentIndex >= 0 ? value.slice(0, commentIndex) : value;
49
+ return rawValue.trim();
50
+ }
51
+
52
+ function parseEnvContent(content) {
53
+ const parsed = {};
54
+ const source = String(content || '');
55
+
56
+ for (const rawLine of source.split(/\r?\n/)) {
57
+ const line = rawLine.trim();
58
+ if (!line || line.startsWith('#')) {
59
+ continue;
60
+ }
61
+
62
+ const match = rawLine.match(/^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)\s*$/);
63
+ if (!match) {
64
+ continue;
65
+ }
66
+
67
+ const [, key, rawValue = ''] = match;
68
+ let value = rawValue.trim();
69
+
70
+ if (
71
+ value.length >= 2
72
+ && ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")))
73
+ ) {
74
+ value = decodeQuotedEnvValue(value, value[0]);
75
+ } else {
76
+ value = normalizeUnquotedEnvValue(value);
77
+ }
78
+
79
+ parsed[key] = value;
80
+ }
81
+
82
+ return parsed;
83
+ }
84
+
85
+ function applyEnvEntries(entries) {
86
+ for (const [key, value] of Object.entries(entries)) {
87
+ if (BASE_PROCESS_ENV.has(key)) {
88
+ continue;
89
+ }
90
+
91
+ process.env[key] = value;
92
+ FRAMEWORK_LOADED_ENV_KEYS.add(key);
93
+ }
94
+ }
95
+
96
+ function loadEnvFile(filePath, logger = null) {
97
+ if (!fs.existsSync(filePath)) {
98
+ return;
99
+ }
100
+
101
+ const parsed = parseEnvContent(fs.readFileSync(filePath, 'utf8'));
102
+ applyEnvEntries(parsed);
103
+
104
+ if (logger) {
105
+ logger.debug('Environment file loaded: %s', filePath);
106
+ }
107
+ }
108
+
109
+ export function loadEnvironmentFiles(rootDir, logger = null) {
110
+ resetFrameworkLoadedEnv();
111
+
112
+ const baseEnvFiles = [
113
+ path.join(rootDir, '.env'),
114
+ path.join(rootDir, '.env.local'),
115
+ ];
116
+
117
+ for (const filePath of baseEnvFiles) {
118
+ loadEnvFile(filePath, logger);
119
+ }
120
+
121
+ const targetEnv = normalizeEnvironmentName(process.env.NODE_ENV, 'development');
122
+ const envSpecificFiles = [
123
+ path.join(rootDir, `.env.${targetEnv}`),
124
+ path.join(rootDir, `.env.${targetEnv}.local`),
125
+ ];
126
+
127
+ for (const filePath of envSpecificFiles) {
128
+ loadEnvFile(filePath, logger);
129
+ }
130
+ }
131
+
132
+ function applyEnvironmentOverrides(config, logger = null) {
133
+ if (!isPlainObject(config)) {
134
+ return config;
135
+ }
136
+
137
+ const targetEnv = normalizeEnvironmentName(
138
+ config.env,
139
+ normalizeEnvironmentName(process.env.NODE_ENV, 'development'),
140
+ );
141
+ const environments = isPlainObject(config.environments) ? config.environments : {};
142
+ const defaultOverride = environments.default;
143
+ const envOverride = environments[targetEnv];
144
+
145
+ let merged = {
146
+ ...config,
147
+ env: targetEnv,
148
+ };
149
+
150
+ if (isPlainObject(defaultOverride)) {
151
+ merged = deepMerge(merged, defaultOverride);
152
+ }
153
+
154
+ if (isPlainObject(envOverride)) {
155
+ merged = deepMerge(merged, envOverride);
156
+ }
157
+
158
+ merged.env = targetEnv;
159
+
160
+ if (logger && (isPlainObject(defaultOverride) || isPlainObject(envOverride))) {
161
+ logger.debug('Environment overrides applied for env "%s"', targetEnv);
162
+ }
163
+
164
+ return merged;
165
+ }
166
+
167
+ export function deepMerge(base, extension) {
168
+ if (!isPlainObject(base) || !isPlainObject(extension)) {
169
+ return extension;
170
+ }
171
+
172
+ const merged = { ...base };
173
+ for (const key of Object.keys(extension)) {
174
+ const left = merged[key];
175
+ const right = extension[key];
176
+
177
+ if (Array.isArray(left) && Array.isArray(right)) {
178
+ merged[key] = [...right];
179
+ continue;
180
+ }
181
+
182
+ if (isPlainObject(left) && isPlainObject(right)) {
183
+ merged[key] = deepMerge(left, right);
184
+ continue;
185
+ }
186
+
187
+ merged[key] = right;
188
+ }
189
+
190
+ return merged;
191
+ }
192
+
193
+ export function normalizeAppEntry(entry) {
194
+ if (typeof entry === 'string') {
195
+ return {
196
+ name: entry,
197
+ mount: `/${entry}`,
198
+ };
199
+ }
200
+
201
+ if (!entry || typeof entry !== 'object' || typeof entry.name !== 'string') {
202
+ throw new Error(`Invalid app entry in settings. Expected string or { name, mount } object, got: ${JSON.stringify(entry)}`);
203
+ }
204
+
205
+ const mount = entry.mount ? String(entry.mount) : `/${entry.name}`;
206
+
207
+ return {
208
+ name: entry.name,
209
+ mount: mount === '/' ? '/' : `/${mount.replace(/^\/+/, '').replace(/\/+$/, '')}`,
210
+ };
211
+ }
212
+
213
+ export function normalizeApps(apps) {
214
+ if (!Array.isArray(apps)) {
215
+ return [];
216
+ }
217
+ return apps.map(normalizeAppEntry);
218
+ }
219
+
220
+ export function defaultConfig(rootDir) {
221
+ const appName = path.basename(rootDir);
222
+
223
+ return {
224
+ appName,
225
+ env: process.env.NODE_ENV || 'development',
226
+ host: process.env.HOST || '0.0.0.0',
227
+ port: process.env.PORT ? Number(process.env.PORT) : 3000,
228
+ trustProxy: false,
229
+ https: {
230
+ enabled: false,
231
+ key: null,
232
+ cert: null,
233
+ ca: null,
234
+ pfx: null,
235
+ keyPath: '',
236
+ certPath: '',
237
+ caPath: null,
238
+ pfxPath: '',
239
+ passphrase: '',
240
+ options: {},
241
+ },
242
+ rootDir,
243
+ staticDir: null,
244
+ maintenance: {
245
+ enabled: false,
246
+ statusCode: 503,
247
+ route: null,
248
+ html: '',
249
+ excludePaths: [],
250
+ retryAfter: null,
251
+ },
252
+ templates: {
253
+ enabled: true,
254
+ engine: 'ejs',
255
+ dir: 'templates',
256
+ base: 'base',
257
+ },
258
+ i18n: {
259
+ enabled: false,
260
+ defaultLocale: 'en',
261
+ fallbackLocale: 'en',
262
+ supported: ['en'],
263
+ queryParam: 'lang',
264
+ cookieName: 'aegis_locale',
265
+ detectFromHeader: true,
266
+ detectFromCookie: true,
267
+ detectFromQuery: true,
268
+ translations: {},
269
+ },
270
+ helpers: {
271
+ locale: 'en-US',
272
+ money: {
273
+ currency: 'USD',
274
+ },
275
+ },
276
+ security: {
277
+ appSecret: '',
278
+ headers: {
279
+ enabled: true,
280
+ csp: {
281
+ enabled: true,
282
+ reportOnly: false,
283
+ directives: {},
284
+ },
285
+ },
286
+ ddos: {
287
+ enabled: true,
288
+ windowMs: 60000,
289
+ maxRequests: 120,
290
+ message: 'Too many requests, please try again later.',
291
+ statusCode: 429,
292
+ standardHeaders: true,
293
+ legacyHeaders: false,
294
+ skipSuccessfulRequests: false,
295
+ skipFailedRequests: false,
296
+ trustProxy: false,
297
+ skipPaths: ['/health'],
298
+ },
299
+ csrf: {
300
+ enabled: true,
301
+ rejectForms: true,
302
+ rejectUnsafeMethods: true,
303
+ cookieName: '_aegis_csrf',
304
+ fieldName: '_csrf',
305
+ headerName: 'x-csrf-token',
306
+ requireSignedCookie: true,
307
+ sameSite: 'lax',
308
+ secure: 'auto',
309
+ httpOnly: true,
310
+ path: '/',
311
+ },
312
+ },
313
+ logging: {
314
+ level: process.env.LOG_LEVEL || 'info',
315
+ },
316
+ database: {
317
+ enabled: false,
318
+ dialect: 'pg',
319
+ config: {},
320
+ options: {},
321
+ },
322
+ cache: {
323
+ enabled: true,
324
+ driver: 'memory',
325
+ options: {},
326
+ },
327
+ mail: {
328
+ enabled: false,
329
+ defaults: {
330
+ from: '',
331
+ replyTo: '',
332
+ },
333
+ transport: {},
334
+ verifyOnStartup: false,
335
+ },
336
+ websocket: {
337
+ enabled: true,
338
+ cors: {
339
+ origin: false,
340
+ },
341
+ },
342
+ uploads: {
343
+ enabled: true,
344
+ dir: 'uploads',
345
+ createDir: true,
346
+ preserveExtension: true,
347
+ maxFileSize: 5 * 1024 * 1024,
348
+ maxFiles: 5,
349
+ maxFields: 50,
350
+ maxFieldSize: 1024 * 1024,
351
+ allowedMimeTypes: [],
352
+ allowedExtensions: [],
353
+ allowApiMultipart: true,
354
+ },
355
+ api: {
356
+ apps: [],
357
+ disableCsrf: true,
358
+ requireJsonForUnsafeMethods: true,
359
+ noStoreHeaders: true,
360
+ },
361
+ auth: {
362
+ enabled: false,
363
+ provider: 'jwt',
364
+ tablePrefix: 'aegisnode',
365
+ storage: {
366
+ driver: 'cache',
367
+ filePath: 'storage/aegisnode-auth-store.json',
368
+ tableName: 'aegisnode_auth_store',
369
+ },
370
+ jwt: {
371
+ secret: '',
372
+ algorithm: 'HS256',
373
+ expiresIn: '15m',
374
+ refreshExpiresIn: '7d',
375
+ issuer: appName,
376
+ audience: appName,
377
+ },
378
+ oauth2: {
379
+ accessTokenTtlSeconds: 3600,
380
+ refreshTokenTtlSeconds: 1209600,
381
+ authorizationCodeTtlSeconds: 600,
382
+ rotateRefreshToken: true,
383
+ requireClientSecret: true,
384
+ requirePkce: true,
385
+ allowPlainPkce: false,
386
+ grants: ['authorization_code', 'refresh_token', 'client_credentials'],
387
+ defaultScopes: [],
388
+ clientAuthMethod: 'client_secret_basic',
389
+ server: {
390
+ enabled: true,
391
+ basePath: '/oauth',
392
+ authorizePath: '/oauth/authorize',
393
+ tokenPath: '/oauth/token',
394
+ introspectionPath: '/oauth/introspect',
395
+ revocationPath: '/oauth/revoke',
396
+ metadataPath: '/.well-known/oauth-authorization-server',
397
+ issuer: '',
398
+ autoApprove: true,
399
+ requireAuthenticatedUser: true,
400
+ requireConsent: false,
401
+ allowHttp: false,
402
+ },
403
+ },
404
+ },
405
+ swagger: {
406
+ enabled: false,
407
+ docsPath: '/docs',
408
+ jsonPath: '/openapi.json',
409
+ documentPath: 'openapi.json',
410
+ explorer: true,
411
+ },
412
+ architecture: {
413
+ strictLayers: false,
414
+ },
415
+ autoMountApps: false,
416
+ loaders: [],
417
+ apps: [],
418
+ };
419
+ }
420
+
421
+ async function importDefaultIfExists(filePath) {
422
+ if (!fs.existsSync(filePath)) {
423
+ return null;
424
+ }
425
+
426
+ const moduleUrl = `${pathToFileURL(filePath).href}?t=${Date.now()}`;
427
+ const loaded = await import(moduleUrl);
428
+ return loaded?.default ?? null;
429
+ }
430
+
431
+ export async function loadProjectConfig(rootDir, logger = null) {
432
+ loadEnvironmentFiles(rootDir, logger);
433
+ const config = defaultConfig(rootDir);
434
+
435
+ const settingsFile = path.join(rootDir, 'settings.js');
436
+ const settingsDir = path.join(rootDir, 'settings');
437
+ const indexFile = path.join(settingsDir, 'index.js');
438
+ const dbFile = path.join(settingsDir, 'db.js');
439
+ const cacheFile = path.join(settingsDir, 'cache.js');
440
+ const appsFile = path.join(settingsDir, 'apps.js');
441
+
442
+ const [settingsConfig, indexConfig, dbConfig, cacheConfig, appsConfig] = await Promise.all([
443
+ importDefaultIfExists(settingsFile),
444
+ importDefaultIfExists(indexFile),
445
+ importDefaultIfExists(dbFile),
446
+ importDefaultIfExists(cacheFile),
447
+ importDefaultIfExists(appsFile),
448
+ ]);
449
+
450
+ let merged = config;
451
+
452
+ if (settingsConfig && isPlainObject(settingsConfig)) {
453
+ merged = deepMerge(merged, settingsConfig);
454
+ }
455
+
456
+ if (indexConfig && isPlainObject(indexConfig)) {
457
+ merged = deepMerge(merged, indexConfig);
458
+ }
459
+
460
+ if (dbConfig && isPlainObject(dbConfig)) {
461
+ merged.database = deepMerge(merged.database, dbConfig);
462
+ }
463
+
464
+ if (cacheConfig && isPlainObject(cacheConfig)) {
465
+ merged.cache = deepMerge(merged.cache, cacheConfig);
466
+ }
467
+
468
+ if (appsConfig && (!Array.isArray(merged.apps) || merged.apps.length === 0)) {
469
+ merged.apps = normalizeApps(appsConfig);
470
+ } else {
471
+ merged.apps = normalizeApps(merged.apps);
472
+ }
473
+
474
+ merged = applyEnvironmentOverrides(merged, logger);
475
+ merged.apps = normalizeApps(merged.apps);
476
+
477
+ if (logger) {
478
+ logger.debug('Configuration loaded for project root: %s', rootDir);
479
+ }
480
+
481
+ return merged;
482
+ }
@@ -0,0 +1,43 @@
1
+ export class Container {
2
+ constructor(parent = null) {
3
+ this.parent = parent;
4
+ this.registry = new Map();
5
+ }
6
+
7
+ set(token, value) {
8
+ this.registry.set(token, value);
9
+ return value;
10
+ }
11
+
12
+ has(token) {
13
+ return this.registry.has(token) || Boolean(this.parent?.has(token));
14
+ }
15
+
16
+ get(token) {
17
+ if (this.registry.has(token)) {
18
+ return this.registry.get(token);
19
+ }
20
+
21
+ if (this.parent) {
22
+ return this.parent.get(token);
23
+ }
24
+
25
+ throw new Error(`Container token not found: ${token}`);
26
+ }
27
+
28
+ getOrNull(token) {
29
+ try {
30
+ return this.get(token);
31
+ } catch {
32
+ return null;
33
+ }
34
+ }
35
+
36
+ createChild() {
37
+ return new Container(this);
38
+ }
39
+ }
40
+
41
+ export function createContainer() {
42
+ return new Container();
43
+ }