aegisnode 0.0.2 → 0.0.4

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 CHANGED
@@ -43,6 +43,7 @@ It keeps the development model simple, but adds enough structure and tooling to
43
43
  Core features:
44
44
 
45
45
  - CLI generators (`startproject`, `createapp`, `runserver`)
46
+ - Startup entry generator (`generateloader`)
46
47
  - Project health checker (`doctor`)
47
48
  - Dependency updater (`updatedeps`)
48
49
  - Maintenance mode with custom HTML responses
@@ -91,13 +92,14 @@ aegisnode runserver --project blog
91
92
  aegisnode createapp users --project blog
92
93
  aegisnode generate view profile --app users --project blog
93
94
  aegisnode generate route profile --app users --project blog
95
+ aegisnode generateloader --project blog
94
96
  aegisnode doctor --project blog
95
97
  aegisnode updatedeps --project blog
96
98
  ```
97
99
 
98
100
  `cd blog` is optional. You can run commands from parent folder with `--project blog`.
99
101
 
100
- `createapp`, `generate`, `runserver`, `doctor`, and `updatedeps` are project-level commands.
102
+ `createapp`, `generate`, `runserver`, `generateloader`, `doctor`, and `updatedeps` are project-level commands.
101
103
  Run them from the project root; do not `cd` into `apps/<app>`.
102
104
  Startup mode rules:
103
105
  - Development (`env === development`): start with `aegisnode runserver` only.
@@ -162,11 +164,20 @@ aegisnode doctor
162
164
 
163
165
  `doctor` checks:
164
166
  - Project structure (`settings.js`, `routes.js`, app folders)
167
+ - Startup entry files (`app.js`, `loader.cjs`), with production errors when `loader.cjs` is missing
165
168
  - App declarations vs filesystem
166
169
  - Security baseline (`appSecret`, csrf/headers/ddos toggles)
167
170
  - Auth safety checks (JWT secret, OAuth2 `allowHttp` in production)
168
171
  - Template directory availability
169
172
 
173
+ Regenerate project startup entry files if needed:
174
+
175
+ ```bash
176
+ aegisnode generateloader
177
+ ```
178
+
179
+ This restores `loader.cjs` and also recreates `app.js` if it is missing.
180
+
170
181
  Update project dependencies to the current npm `latest` dist-tag:
171
182
 
172
183
  ```bash
@@ -223,7 +234,7 @@ Access environment values anywhere with `process.env`:
223
234
  export default {
224
235
  port: process.env.PORT ? Number(process.env.PORT) : 3000,
225
236
  security: {
226
- appSecret: process.env.APP_SECRET || '',
237
+ appSecret: process.env.APP_SECRET || '<generated-at-scaffold-time>',
227
238
  },
228
239
  };
229
240
  ```
@@ -240,7 +251,7 @@ export default {
240
251
  port: process.env.PORT ? Number(process.env.PORT) : 3000,
241
252
  trustProxy: false,
242
253
  security: {
243
- appSecret: process.env.APP_SECRET || '',
254
+ appSecret: process.env.APP_SECRET || '<generated-at-scaffold-time>',
244
255
  },
245
256
  logging: {
246
257
  level: process.env.LOG_LEVEL || 'info',
@@ -265,1877 +276,1961 @@ export default {
265
276
 
266
277
  Notes:
267
278
  - Keep `AEGIS_APPS_START/END` markers; `createapp` updates this list automatically.
268
- - `startproject` also writes a local `.env` with a generated `APP_SECRET`.
279
+ - `startproject` writes a local `.env` with a generated `APP_SECRET` and also embeds the same generated secret in `settings.js` as a fallback.
269
280
  - Add optional blocks manually only when needed: `https`, `templates`, `i18n`, `helpers`, `staticDir`, `websocket`, `uploads`, `mail`, `auth`, `api`, `swagger`, `loaders`, `environments`, `architecture`, `security.headers/ddos/csrf`.
270
281
  - Any section you omit uses framework defaults from `src/runtime/config.js`.
271
282
 
272
- <!-- SETTINGS_REFERENCE_START -->
273
- ## Full Settings Reference
283
+ ## Core Concepts And App Structure
274
284
 
275
- All fields below are supported in `settings.js`. If you omit a field, AegisNode uses the runtime default.
285
+ ### App File Usage Examples
276
286
 
277
- Merge order used at startup:
278
- 1. Framework defaults (`defaultConfig`)
279
- 2. `settings.js`
280
- 3. Legacy `settings/index.js` (if present)
281
- 4. Legacy `settings/db.js` (merged into `database`)
282
- 5. Legacy `settings/cache.js` (merged into `cache`)
283
- 6. Legacy `settings/apps.js` (used only when `settings.js` does not define apps)
284
- 7. `environments.default`
285
- 8. `environments[env]` where `env = settings.env` (fallback `NODE_ENV`, then `development`)
287
+ Each generated app usually contains:
288
+ - `apps/<app>/views.js`
289
+ - `apps/<app>/models.js`
290
+ - `apps/<app>/services.js`
291
+ - `apps/<app>/subscribers.js`
292
+ - `apps/<app>/routes.js`
286
293
 
287
- ### Top-Level
294
+ Usage by file:
295
+ - `views.js`: HTTP handlers (`req`, `res`, `next`). Default signature can be context-first: `handler({ service, validator, services, validators, ... }, req, res, next)`.
296
+ - `models.js`: data access layer only (SQL/NoSQL operations).
297
+ - `services.js`: business logic layer; orchestrates models.
298
+ - `subscribers.js`: event listeners (for example `app.booted`, `ws.connection`, custom events).
299
+ - `routes.js`: route mapping only (`route.get(...)`, `route.post(...)`, `route.use(...)`) to view handlers.
288
300
 
289
- | Key | Type / Default | Description |
290
- | --- | --- | --- |
291
- | `appName` | `string` / folder name | Application name used in logs and defaults. |
292
- | `env` | `string` / `process.env.NODE_ENV || 'development'` | Active environment key for `environments` overrides. |
293
- | `host` | `string` / `process.env.HOST || '0.0.0.0'` | Bind host for HTTP server. |
294
- | `port` | `number` / `process.env.PORT || 3000` | Bind port for HTTP server. |
295
- | `trustProxy` | `boolean \| number \| string` / `false` | Express `trust proxy` value. Set this when HTTPS is terminated by a reverse proxy/load balancer. |
296
- | `https` | `object \| false` / see HTTPS table | Direct TLS server settings for Node-hosted HTTPS. |
297
- | `staticDir` | `string \| null` / `null` | Static assets directory, relative to project root (if set). |
298
- | `templates` | `object \| false` / see templates table | EJS template engine + layout settings. |
299
- | `i18n` | `object` / see i18n table | Built-in locale detection + translator bridge (`req.aegis.t`, injected `i18n.t`). |
300
- | `helpers` | `object` / see helpers table | Runtime helper defaults (for example currency/locale for `helpers.money`). |
301
- | `security` | `object` / see security tables | Security headers, DDoS limiter, CSRF settings, app secret. |
302
- | `logging` | `object` / `{ level: 'info' }` | Runtime logger level. |
303
- | `database` | `object` / see database table | SQL or MongoDB connection settings. |
304
- | `cache` | `object` / `{ enabled: true, driver: 'memory' }` | Cache backend settings. |
305
- | `websocket` | `object` / `{ enabled: true, cors: { origin: false } }` | Socket.IO server options. |
306
- | `uploads` | `object` / see uploads table | Built-in file upload middleware settings used by `route.upload`. |
307
- | `mail` | `object` / see mail table | Nodemailer-backed mail manager available as injected `mail` and `req.aegis.mail`. |
308
- | `api` | `object` / see API table | API-app middleware behavior (JSON enforcement, no-store, CSRF skip for API mounts). |
309
- | `auth` | `object` / see auth tables | JWT or OAuth2 provider settings. |
310
- | `swagger` | `object` / see swagger table | OpenAPI JSON + Swagger UI settings. |
311
- | `architecture` | `object` / `{ strictLayers: false }` | Layering enforcement mode. |
312
- | `autoMountApps` | `boolean` / `false` | Auto-mount each app route file from `settings.apps`. |
313
- | `loaders` | `array` / `[]` | Startup loaders run before routes mounting. |
314
- | `apps` | `array` / `[]` | Declared apps with mount points. |
315
- | `environments` | `object` / `{}` | Environment-specific deep overrides. |
301
+ Route modules are mapping-only (`register(route)`).
302
+ Framework context is injected into handlers as first argument (when handler uses 4 args): `{ service, validator, services, models, validators, auth, mail, helpers, i18n, events, ... }`.
303
+ `req.aegis` is also available.
304
+ `service`/`validator` are app-scoped conveniences. For root/non-app routes, use `services.get('<app>.<name>')` / `validators.get('<app>.<name>')`, or create an app-scoped accessor with `services.forApp('<app>')`.
316
305
 
317
- Notes:
318
- - `rootDir` is internal and set by runtime; do not manage it manually.
319
- - Arrays are replaced (not merged) during deep merge.
306
+ What “app-scoped” means:
307
+ - In app routes (for example inside `apps/users/routes.js`), `{ service }` resolves to that app service.
308
+ - In root/global routes (`routes.js`), there is no single app context, so use `{ services }` and fetch with `services.forApp('<app>').get('<name>')` or `services.get('<app>.<name>')`.
320
309
 
321
- ### HTTPS (`https`)
310
+ Injected runtime dependencies:
322
311
 
323
- Use this only when Node should serve HTTPS directly. If HTTPS is handled by Passenger, Nginx, Apache, or another proxy, keep `https.enabled` off and set top-level `trustProxy` instead.
312
+ AegisNode injects resolved runtime objects instead of asking app layers to import framework internals. `config` is the resolved runtime config from `settings.js` plus defaults and runtime overrides.
324
313
 
325
- | Key | Type / Default | Description |
326
- | --- | --- | --- |
327
- | `enabled` | `boolean` / `false` | Create an HTTPS server instead of HTTP. |
328
- | `key` | `string \| Buffer` / `null` | TLS private key content. |
329
- | `cert` | `string \| Buffer` / `null` | TLS certificate content. |
330
- | `ca` | `string \| Buffer \| array` / `null` | Optional CA/intermediate certificate content. |
331
- | `pfx` | `string \| Buffer` / `null` | PFX/PKCS#12 archive content. Use instead of `key` + `cert`. |
332
- | `keyPath` | `string` / `''` | Path to TLS private key, relative to project root or absolute. |
333
- | `certPath` | `string` / `''` | Path to TLS certificate, relative to project root or absolute. |
334
- | `caPath` | `string \| string[]` / `null` | Optional CA/intermediate certificate path(s). |
335
- | `pfxPath` | `string` / `''` | Path to PFX/PKCS#12 archive. |
336
- | `passphrase` | `string` / `''` | Optional passphrase for encrypted key/PFX files. |
337
- | `options` | `object` / `{}` | Extra Node `https.createServer` options (for example `minVersion`). |
314
+ Available by layer:
315
+ - Views/handlers (`views.js` or any context-first route/controller action): `appName`, `app`, `config`, `env`, `i18n`, `mail`, `logger`, `events`, `cache`, `io`, `auth`, `helpers`, `jlive`, `upload`, `services`, `models`, `validators`, `service`, `model`, `validator`, `database`, `dbClient`
316
+ - Services (`constructor({ ... })`): `appName`, `config`, `env`, `i18n`, `mail`, `logger`, `events`, `cache`, `io`, `auth`, `helpers`, `jlive`, `models`, `validators`, `services`
317
+ - Models (`constructor({ ... })`): `appName`, `config`, `env`, `i18n`, `mail`, `logger`, `events`, `cache`, `io`, `helpers`, `jlive`, `dbClient`, `database`
318
+ - Validators (`constructor({ ... })`): `appName`, `config`, `env`, `i18n`, `mail`, `logger`, `events`, `cache`, `io`, `auth`, `helpers`, `jlive`, `dbClient`, `database`
319
+ - Subscribers (`export default function ({ ... })`): `appName`, `rootDir`, `config`, `env`, `i18n`, `mail`, `logger`, `events`, `cache`, `io`, `auth`, `helpers`, `jlive`, `upload`, `services`, `models`, `validators`, `database`, `dbClient`, `app`, `server`, `templates`, `protocol`, `container`, `declaredAppNames`
320
+ - Controllers (`constructor({ ... })`): `appName`, `rootDir`, `config`, `env`, `i18n`, `mail`, `logger`, `events`, `cache`, `io`, `auth`, `helpers`, `jlive`, `upload`, `services`, `models`, `validators`, `database`, `dbClient`, `container`, `app`
321
+ - Loaders (`loaders` entry function): `rootDir`, `config`, `env`, `i18n`, `mail`, `logger`, `events`, `cache`, `io`, `auth`, `helpers`, `jlive`, `upload`, `services`, `models`, `validators`, `database`, `dbClient`, `app`, `server`, `templates`, `protocol`, `container`, `declaredAppNames`, `options`
322
+ - Request bridge (`req.aegis`): `config`, `env`, `i18n`, `locale`, `localeSource`, `t`, `setLocale`, `logger`, `events`, `cache`, `io`, `auth`, `mail`, `helpers`, `jlive`, `upload`, `services`, `models`, `validators`, `database`, `dbClient`, `appName`, `app`
323
+ - Template locals: `helpers`, `jlive`, `t`, `locale`, `i18n`, `money`, `number`, `dateTime`, `timeElapsed`, `timeDifference`, `breakStr`
338
324
 
339
- Direct HTTPS example:
325
+ Key meanings:
326
+
327
+ | Key | Description |
328
+ | --- | --- |
329
+ | `config` | Resolved runtime config from `settings.js`, framework defaults, environment overrides, and runtime overrides. |
330
+ | `env` | Frozen environment snapshot (`process.env` plus runtime additions such as `APP_SECRET`). |
331
+ | `i18n` | Translator bridge. During a request it follows the active request locale; outside a request it falls back to `defaultLocale` unless you pass `{ locale }`. |
332
+ | `mail` | Mail manager. Use `mail.send({ to, subject, text/html })` or `mail.sendMail(...)`. |
333
+ | `logger` | Runtime logger instance. |
334
+ | `events` | Event bus used by subscribers and app code. |
335
+ | `cache` | Cache backend instance (memory by default). |
336
+ | `io` | Socket.IO server instance when websocket support is enabled. |
337
+ | `auth` | Auth manager for JWT/OAuth2 flows. |
338
+ | `helpers` | Runtime helper functions such as `money`, `number`, `dateTime`, and `timeElapsed`. |
339
+ | `jlive` | jlive bridge instance. |
340
+ | `upload` | Upload manager used by `route.upload`. |
341
+ | `services` | Layer accessor used to fetch services by app/name. |
342
+ | `models` | Layer accessor used to fetch models by app/name. |
343
+ | `validators` | Layer accessor used to fetch validators by app/name. |
344
+ | `service` | App-scoped convenience service for the current app only. |
345
+ | `model` | App-scoped convenience model for the current app only. |
346
+ | `validator` | App-scoped convenience validator for the current app only. |
347
+ | `database` | Database runtime wrapper. |
348
+ | `dbClient` | Low-level database/query client. |
349
+ | `appName` | Current app name. |
350
+ | `app` | Current app metadata/context. |
351
+ | `rootDir` | Absolute project root. |
352
+ | `server` | HTTP/HTTPS server instance. |
353
+ | `templates` | Resolved template-engine configuration. |
354
+ | `protocol` | Server protocol (`http` or `https`). |
355
+ | `container` | Internal DI container. |
356
+ | `declaredAppNames` | Set of apps declared in config/routes. |
357
+ | `options` | Loader-specific options object from a `{ path, options }` loader entry. |
358
+ | `locale` | Active request locale. Available on `req.aegis` and template locals. |
359
+ | `localeSource` | Where the current locale came from (`query`, `cookie`, `header`, `manual`, or `disabled`). |
360
+ | `t` | Convenience translator shortcut for the current request/template scope. |
361
+ | `setLocale` | Request helper used to change and optionally persist the active locale. |
340
362
 
341
363
  ```js
364
+ // routes.js (root/global)
342
365
  export default {
343
- host: '0.0.0.0',
344
- port: 3443,
345
- https: {
346
- enabled: true,
347
- keyPath: 'certs/localhost-key.pem',
348
- certPath: 'certs/localhost-cert.pem',
349
- options: {
350
- minVersion: 'TLSv1.2',
351
- },
366
+ register(route) {
367
+ route.get('/dashboard', async ({ services }, req, res, next) => {
368
+ try {
369
+ const usersService = services.forApp('users').get('users');
370
+ const ordersService = services.forApp('orders').get('orders');
371
+ res.json({
372
+ users: await usersService.list(),
373
+ orders: await ordersService.list(),
374
+ });
375
+ } catch (error) {
376
+ next(error);
377
+ }
378
+ });
352
379
  },
353
380
  };
354
381
  ```
355
382
 
356
- Reverse-proxy HTTPS example:
383
+ Example `views.js`:
357
384
 
358
385
  ```js
359
- export default {
360
- host: '127.0.0.1',
361
- port: 3000,
362
- trustProxy: 1,
363
- };
364
- ```
386
+ class UsersView {
387
+ static async index({ service }, req, res, next) {
388
+ try {
389
+ const data = await service.listUsers();
390
+ res.json({ data });
391
+ } catch (error) {
392
+ next(error);
393
+ }
394
+ }
365
395
 
366
- Notes:
367
- - `https` requires either `pfx`/`pfxPath` or both `key`/`keyPath` and `cert`/`certPath`.
368
- - Paths resolve from project root unless absolute.
369
- - `trustProxy` affects `req.secure`, `req.protocol`, secure cookies, and OAuth2 secure transport checks.
370
- - Prefer `1`, a subnet, or another exact Express `trust proxy` value instead of `true` when rate limiting is enabled.
396
+ static async create({ service, validator }, req, res, next) {
397
+ try {
398
+ const payload = validator.create(req.body || {});
399
+ const created = await service.createUser(payload);
400
+ res.status(201).json({ data: created });
401
+ } catch (error) {
402
+ next(error);
403
+ }
404
+ }
371
405
 
372
- ### Templates (`templates`)
406
+ static async tools({ service, helpers, jlive }, req, res, next) {
407
+ try {
408
+ const stats = await service.stats();
409
+ res.json({
410
+ stats,
411
+ total: helpers.money(1299.5, { currency: 'USD' }),
412
+ elapsed: helpers.timeElapsed(Date.now() - 60_000),
413
+ token: jlive.generate(16),
414
+ });
415
+ } catch (error) {
416
+ next(error);
417
+ }
418
+ }
419
+ }
373
420
 
374
- | Key | Type / Default | Description |
375
- | --- | --- | --- |
376
- | `enabled` | `boolean` / `true` | Enable template engine. |
377
- | `engine` | `string` / `'ejs'` | Template engine. Only `ejs` is supported. |
378
- | `dir` | `string` / `'templates'` | Templates folder (absolute or relative to project root). |
379
- | `base` | `string \| false \| null` / `'base'` | Default layout template (without `.ejs`). Set `false`/`null` to disable layout wrapping globally. |
380
- | `appBases` | `object` / `{}` | Per-app layout override map: `{ appName: 'layout/name' }`. Set app value to `false`/`null` to disable layout for that app only. |
381
- | `locals` | `object \| function` / `{}` | Global locals. If function, signature is `({ req, res, helpers, jlive, env }) => object`. |
421
+ export default UsersView;
422
+ ```
382
423
 
383
- Layout notes:
384
- - `res.render('view', data)` renders `view.ejs` and wraps it with `base.ejs` (or configured layout).
385
- - Per-app layout override: `templates.appBases = { users: 'users/base', admin: 'admin/base' }`.
386
- - In layout, both `<%- body %>` and `<%- content %>` are available.
387
- - Per-render layout override: pass `layout: 'custom-layout'` or `layout: false` in locals.
424
+ Example `models.js`:
388
425
 
389
- ### Internationalization (`i18n`)
426
+ ```js
427
+ class UsersModel {
428
+ constructor({ dbClient }) {
429
+ this.dbClient = dbClient;
430
+ }
390
431
 
391
- | Key | Type / Default | Description |
392
- | --- | --- | --- |
393
- | `enabled` | `boolean` / `false` | Enable built-in i18n translator bridge. |
394
- | `defaultLocale` | `string` / `'en'` | Default locale used when detection fails. |
395
- | `fallbackLocale` | `string` / `'en'` | Fallback locale used when key is missing in active locale. |
396
- | `supported` | `string[]` / `['en']` | Allowed locales. Values normalize to lowercase (for example `en-US` -> `en-us`). |
397
- | `queryParam` | `string` / `'lang'` | Query parameter used for locale selection (for example `?lang=fr`). |
398
- | `cookieName` | `string` / `'aegis_locale'` | Cookie used to persist locale. |
399
- | `detectFromHeader` | `boolean` / `true` | Enable locale detection from `Accept-Language` header. |
400
- | `detectFromCookie` | `boolean` / `true` | Enable locale detection from configured cookie. |
401
- | `detectFromQuery` | `boolean` / `true` | Enable locale detection from query parameter. |
402
- | `translations` | `object` / `{}` | Translation map by locale. Values can be objects or JSON file paths: `{ en: { ... }, fr: 'locales/fr.json' }`. Alias keys `locales` and `messages` are also accepted. |
403
- | `translationsFile` | `string` / unset | Path to a JSON file containing all locales (example: `{ "en": {...}, "fr": {...} }`). Inline `translations` overrides file values for same locale keys. |
432
+ async list() {
433
+ return [{ id: '1', name: 'Alice' }];
434
+ }
404
435
 
405
- i18n notes:
406
- - Detection order: query -> cookie -> `Accept-Language` -> `defaultLocale`.
407
- - Use dotted keys like `home.title`.
408
- - Placeholder interpolation supports `{name}` style tokens.
409
- - Relative JSON paths resolve from project root (`settings.js` location).
410
- - Injected `i18n` is available in handlers, services, models, validators, controllers, subscribers, and loaders. Use `i18n.t('key', vars, { locale })`.
411
- - During a request, injected `i18n.t(...)` follows the active request locale. Outside a request, it falls back to `defaultLocale`.
436
+ async create(payload) {
437
+ return { id: '2', ...payload };
438
+ }
439
+ }
412
440
 
413
- ### Helpers Defaults (`helpers`)
441
+ export default { users: UsersModel };
442
+ ```
414
443
 
415
- | Key | Type / Default | Description |
416
- | --- | --- | --- |
417
- | `locale` | `string` / `'en-US'` | Default locale used by runtime helpers when locale is not passed explicitly. |
418
- | `money` | `object` / `{ currency: 'USD' }` | Default money formatting settings used by `helpers.money`. |
419
- | `money.currency` | `string` / `'USD'` | Default currency code for `helpers.money(amount)` when no currency option is provided. |
420
- | `money.locale` | `string` / `helpers.locale` | Locale override only for `helpers.money`. |
421
- | `money.currencyDisplay` | `'symbol' | 'code' | 'name' | 'narrowSymbol'` / `'symbol'` | Currency display style passed to `Intl.NumberFormat`. |
422
- | `money.minimumFractionDigits` | `number` / unset | Optional minimum fraction digits for money formatting. |
423
- | `money.maximumFractionDigits` | `number` / unset | Optional maximum fraction digits for money formatting. |
424
-
425
- Helpers defaults notes:
426
- - Per-call options always override these defaults.
427
- - If `helpers.locale` is not set, AegisNode falls back to `i18n.defaultLocale` for helper locale.
428
- - Legacy shorthand keys are also accepted for compatibility: `helpers.currency`, top-level `currency`, and `app.currency`.
444
+ Example `services.js`:
429
445
 
430
- ### Security (`security`)
446
+ ```js
447
+ class UsersService {
448
+ constructor({ models, env }) {
449
+ this.usersModel = models.get('users');
450
+ this.env = env;
451
+ }
431
452
 
432
- | Key | Type / Default | Description |
433
- | --- | --- | --- |
434
- | `appSecret` | `string` / `''` | Shared secret for signing security artifacts. Use at least 16 chars. |
435
- | `headers` | `object` / see headers table | Helmet + CSP configuration. |
436
- | `ddos` | `object` / see ddos table | `express-rate-limit` based protection. |
437
- | `csrf` | `object` / see csrf table | CSRF cookie/token behavior. |
453
+ async listUsers() {
454
+ return this.usersModel.list();
455
+ }
438
456
 
439
- #### Security Headers (`security.headers`)
457
+ async createUser(payload) {
458
+ return this.usersModel.create(payload);
459
+ }
460
+ }
440
461
 
441
- | Key | Type / Default | Description |
442
- | --- | --- | --- |
443
- | `enabled` | `boolean` / `true` | Enable Helmet middleware. |
444
- | `csp` | `object` / see CSP table | Content Security Policy behavior. |
462
+ export default { users: UsersService };
463
+ ```
445
464
 
446
- #### CSP (`security.headers.csp`)
465
+ Example `subscribers.js`:
447
466
 
448
- | Key | Type / Default | Description |
449
- | --- | --- | --- |
450
- | `enabled` | `boolean` / `true` | Enable CSP header from Helmet. |
451
- | `reportOnly` | `boolean` / `false` | Use report-only mode. |
452
- | `directives` | `object` / `{}` | Directive overrides. Set a directive to `false`/`null` to remove it. |
467
+ ```js
468
+ export default function registerUsersSubscribers({ events, logger }) {
469
+ events.subscribe('app.booted', ({ appName }) => {
470
+ logger.info('[users] booted: %s', appName);
471
+ });
472
+ }
473
+ ```
453
474
 
454
- Default CSP base includes safe defaults such as `defaultSrc 'self'`, `objectSrc 'none'`, `frameAncestors 'none'`, and websocket-aware `connectSrc`.
475
+ Injected `env` is also available in:
476
+ - view handler context: `static index({ env }, req, res) { ... }`
477
+ - model constructors: `constructor({ dbClient, env }) { ... }`
478
+ - subscribers: `export default function ({ events, env }) { ... }`
479
+ - request runtime bridge: `req.aegis.env`
455
480
 
456
- Allow multiple external domains by adding them per directive (not globally):
481
+ Example `routes.js`:
457
482
 
458
483
  ```js
459
- security: {
460
- headers: {
461
- csp: {
462
- directives: {
463
- scriptSrc: ["'self'", 'https://cdn.jsdelivr.net', 'https://unpkg.com'],
464
- scriptSrcElem: ["'self'", 'https://cdn.jsdelivr.net', 'https://unpkg.com'],
465
- styleSrc: ["'self'", "'unsafe-inline'", 'https://cdn.jsdelivr.net'],
466
- styleSrcElem: ["'self'", 'https://cdn.jsdelivr.net'],
467
- imgSrc: ["'self'", 'data:', 'https://cdn.jsdelivr.net'],
468
- connectSrc: ["'self'", 'https://api.example.com', 'wss://socket.example.com'],
469
- },
470
- },
484
+ import UsersView from './views.js';
485
+
486
+ export default {
487
+ appName: 'users',
488
+ register(route) {
489
+ route.get('/home', UsersView.home);
490
+ route.get('/', UsersView.index);
491
+ route.post('/', UsersView.create);
471
492
  },
472
- },
493
+ };
473
494
  ```
474
495
 
475
- Notes:
476
- - Add each origin to the exact directive needed (scripts, styles, images, API/WebSocket connections).
477
- - If browser reports a `script-src-elem` violation, whitelist the domain in `scriptSrcElem`.
478
- - Prefer explicit origins instead of `*` in production.
496
+ ## Common Tasks And Feature Guides
479
497
 
480
- Google Fonts example:
498
+ ### File Uploads
499
+
500
+ AegisNode provides built-in upload middleware on route API as `route.upload`.
501
+
502
+ Storage location:
503
+ - Default folder: `<project-root>/uploads`
504
+ - Change with `settings.uploads.dir` (relative or absolute path)
505
+
506
+ Recommended upload settings:
481
507
 
482
508
  ```js
483
- security: {
484
- headers: {
485
- csp: {
486
- directives: {
487
- styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'],
488
- styleSrcElem: ["'self'", 'https://fonts.googleapis.com'],
489
- fontSrc: ["'self'", 'data:', 'https://fonts.gstatic.com'],
490
- },
491
- },
492
- },
509
+ uploads: {
510
+ enabled: true,
511
+ dir: 'storage/uploads',
512
+ createDir: true,
513
+ preserveExtension: true,
514
+ maxFileSize: '5mb',
515
+ maxFiles: 5,
516
+ maxFields: 50,
517
+ maxFieldSize: '1mb',
518
+ allowedMimeTypes: ['image/png', 'image/jpeg'],
519
+ allowedExtensions: ['.png', '.jpg', '.jpeg'],
520
+ allowApiMultipart: true,
493
521
  },
494
522
  ```
495
523
 
496
- #### DDoS / Rate Limit (`security.ddos`)
524
+ Route middleware modes:
497
525
 
498
- | Key | Type / Default | Description |
499
- | --- | --- | --- |
500
- | `enabled` | `boolean` / `true` | Enable rate limiter. |
501
- | `windowMs` | `number` / `60000` | Rate limit window in milliseconds. |
502
- | `maxRequests` | `number` / `120` | Max requests per window per key. |
503
- | `message` | `string` / `'Too many requests, please try again later.'` | JSON error message text. |
504
- | `statusCode` | `number` / `429` | Response status code when limited. |
505
- | `standardHeaders` | `boolean` / `true` | Emit modern rate-limit headers. |
506
- | `legacyHeaders` | `boolean` / `false` | Emit legacy `X-RateLimit-*` headers. |
507
- | `skipSuccessfulRequests` | `boolean` / `false` | Do not count successful responses. |
508
- | `skipFailedRequests` | `boolean` / `false` | Do not count failed responses. |
509
- | `trustProxy` | `boolean \| number \| string` / `false` | Legacy alias for top-level `trustProxy`. Still supported for backward compatibility. |
510
- | `store` | `object \| null` / `null` | Custom rate-limit store implementation. |
511
- | `skipPaths` | `string[]` / `['/health']` | Path prefixes excluded from limiter. |
526
+ ```js
527
+ import UsersView from './views.js';
512
528
 
513
- #### CSRF (`security.csrf`)
529
+ export default {
530
+ appName: 'users',
531
+ register(route) {
532
+ // One file -> req.file
533
+ route.post('/avatar', route.upload.single('avatar'), UsersView.uploadAvatar);
514
534
 
515
- | Key | Type / Default | Description |
516
- | --- | --- | --- |
517
- | `enabled` | `boolean` / `true` | Enable CSRF middleware. |
518
- | `rejectForms` | `boolean` / `true` | Enforce CSRF on form submissions. |
519
- | `rejectUnsafeMethods` | `boolean` / `true` | Enforce CSRF for unsafe methods (`POST/PUT/PATCH/DELETE`) in general. |
520
- | `cookieName` | `string` / `'_aegis_csrf'` | CSRF cookie name. |
521
- | `fieldName` | `string` / `'_csrf'` | Body form field name for CSRF token. |
522
- | `headerName` | `string` / `'x-csrf-token'` | Header token key (normalized lowercase). |
523
- | `requireSignedCookie` | `boolean` / `true` | Require signed CSRF cookie values. |
524
- | `sameSite` | `'lax' \| 'strict' \| 'none' \| false` / `'lax'` | CSRF cookie same-site mode. |
525
- | `secure` | `boolean \| 'auto'` / `'auto'` | Secure cookie flag (`auto` respects request security). |
526
- | `httpOnly` | `boolean` / `true` | Make CSRF cookie httpOnly. |
527
- | `path` | `string` / `'/'` | CSRF cookie path. |
535
+ // Many files from one input name -> req.files (array)
536
+ route.post('/gallery', route.upload.array('photos', 6), UsersView.uploadGallery);
528
537
 
529
- CSRF notes:
530
- - With `requireSignedCookie: true` (default), `security.appSecret` must be strong (minimum 16 chars) or startup fails.
531
- - CSRF is skipped for configured API app mounts when `api.disableCsrf: true`.
532
- - CSRF is skipped on built-in OAuth2 server endpoints.
538
+ // Many named file inputs -> req.files.<fieldName> (array)
539
+ route.post(
540
+ '/documents',
541
+ route.upload.fields([
542
+ { name: 'avatar', maxCount: 1 },
543
+ { name: 'docs', maxCount: 3 },
544
+ ]),
545
+ UsersView.uploadDocuments,
546
+ );
533
547
 
534
- ### Logging (`logging`)
548
+ // Accept all file fields -> req.files (array)
549
+ route.post('/any-upload', route.upload.any(), UsersView.uploadAny);
535
550
 
536
- | Key | Type / Default | Description |
537
- | --- | --- | --- |
538
- | `level` | `'error' \| 'warn' \| 'info' \| 'debug' \| 'trace'` / `'info'` | Logger verbosity threshold. |
551
+ // No files, parse multipart text fields only
552
+ route.post('/multipart-no-file', route.upload.none(), UsersView.multipartNoFile);
553
+ },
554
+ };
555
+ ```
539
556
 
540
- ### Database (`database`)
557
+ `req` payload shape:
558
+ - `single()`: `req.file` + `req.body`
559
+ - `array()`: `req.files` (array) + `req.body`
560
+ - `fields()`: `req.files` object (`req.files.avatar`, `req.files.docs`, ...) + `req.body`
541
561
 
542
- | Key | Type / Default | Description |
543
- | --- | --- | --- |
544
- | `enabled` | `boolean` / `false` | Enable database bootstrap. |
545
- | `dialect` | `string` / `'pg'` | SQL: `mysql`, `pg`/`postgres`/`postgresql`, `sqlite`, `mssql`, `oracle`; NoSQL: `mongo`/`mongodb`/`mongoose`. |
546
- | `config` | `object` / `{}` | Connection options passed to database driver. |
547
- | `options` | `object` / `{}` | Extra options (used directly for Mongo when enabled). |
548
- | `uri` | `string` / unset | Legacy Mongo URI fallback (still accepted). Prefer `config.connectionString`. |
562
+ Custom route with form fields + file:
549
563
 
550
- Mongo config shortcuts accepted in `database.config`:
551
- - `connectionString` (preferred)
552
- - or `server`/`host`, `port`, `database`/`dbName`, `user`/`username`, `password`
564
+ ```js
565
+ // apps/users/routes.js
566
+ import UsersView from './views.js';
553
567
 
554
- ### Cache (`cache`)
568
+ export default {
569
+ appName: 'users',
570
+ register(route) {
571
+ route.post('/profile/update', route.upload.single('avatar'), UsersView.updateProfile);
572
+ },
573
+ };
574
+ ```
555
575
 
556
- | Key | Type / Default | Description |
557
- | --- | --- | --- |
558
- | `enabled` | `boolean` / `true` | Enable cache service. |
559
- | `driver` | `string` / `'memory'` | Cache driver. Currently only `memory` is built-in. |
560
- | `options` | `object` / `{}` | Reserved for future/custom drivers. |
576
+ ```js
577
+ // apps/users/views.js
578
+ class UsersView {
579
+ static updateProfile(_context, req, res) {
580
+ const { username, bio } = req.body;
581
+ const avatar = req.file || null;
561
582
 
562
- ### WebSocket (`websocket`)
583
+ return res.json({
584
+ username,
585
+ bio,
586
+ avatar: avatar ? {
587
+ name: avatar.filename,
588
+ originalName: avatar.originalname,
589
+ mimeType: avatar.mimetype,
590
+ size: avatar.size,
591
+ path: avatar.path,
592
+ } : null,
593
+ });
594
+ }
595
+ }
563
596
 
564
- | Key | Type / Default | Description |
565
- | --- | --- | --- |
566
- | `enabled` | `boolean` / `true` | Enable Socket.IO server. |
567
- | `cors` | `object` / `{ origin: false }` | Passed directly to Socket.IO `cors` option. |
597
+ export default UsersView;
598
+ ```
568
599
 
569
- ### Uploads (`uploads`)
600
+ ```html
601
+ <form action="/users/profile/update" method="POST" enctype="multipart/form-data">
602
+ <%= csrfToken %>
603
+ <input name="username" />
604
+ <textarea name="bio"></textarea>
605
+ <input type="file" name="avatar" />
606
+ <button type="submit">Save</button>
607
+ </form>
608
+ ```
570
609
 
571
- | Key | Type / Default | Description |
572
- | --- | --- | --- |
573
- | `enabled` | `boolean` / `true` | Enable built-in upload manager. |
574
- | `dir` | `string` / `'uploads'` | Upload destination folder (absolute or relative to project root). |
575
- | `createDir` | `boolean` / `true` | Create upload directory automatically on boot. |
576
- | `preserveExtension` | `boolean` / `true` | Preserve original file extension in generated file name. |
577
- | `maxFileSize` | `number \| string` / `5 * 1024 * 1024` | Per-file max size in bytes. String units like `'5mb'` are accepted. |
578
- | `maxFiles` | `number` / `5` | Max file count per request. |
579
- | `maxFields` | `number` / `50` | Max non-file form fields per request. |
580
- | `maxFieldSize` | `number \| string` / `1024 * 1024` | Max size per form field value. String units are accepted. |
581
- | `allowedMimeTypes` | `string[] \| string` / `[]` | Allowed MIME types. Empty means allow all. |
582
- | `allowedExtensions` | `string[] \| string` / `[]` | Allowed file extensions (`.jpg`, `.pdf`, ...). Empty means allow all. |
583
- | `allowApiMultipart` | `boolean` / `true` | Allow `multipart/form-data` on API mounts even when `api.requireJsonForUnsafeMethods` is true. |
610
+ Upload limits and rejections:
611
+ - Per-file size limit from `uploads.maxFileSize` returns `413` when exceeded.
612
+ - Total files limit from `uploads.maxFiles` returns `413` when exceeded.
613
+ - `allowedMimeTypes` / `allowedExtensions` mismatch returns `415`.
584
614
 
585
- ### Mail (`mail`)
615
+ Important behavior:
616
+ - If `uploads.enabled=false`, using `route.upload.*` throws at route registration.
617
+ - For API mounts, multipart is allowed only when `uploads.allowApiMultipart=true`.
618
+ - For non-API form submissions, CSRF token is required by default.
586
619
 
587
- | Key | Type / Default | Description |
588
- | --- | --- | --- |
589
- | `enabled` | `boolean` / `false` | Enable the built-in mail manager. |
590
- | `defaults` | `object` / `{ from: '', replyTo: '' }` | Default message fields merged into every outgoing mail payload. |
591
- | `defaults.from` | `string` / `''` | Default sender used when `mail.send(...)` omits `from`. |
592
- | `defaults.replyTo` | `string` / `''` | Default `replyTo` header for outgoing mail. |
593
- | `transport` | `object \| string` / `{}` | Nodemailer transport config or SMTP connection URL passed to `nodemailer.createTransport(...)`. |
594
- | `transporter` | `object \| null` / `null` | Prebuilt transporter object with `sendMail()`. Useful in tests or custom integrations. |
595
- | `transportFactory` | `function \| null` / `null` | Factory that returns a transporter object with `sendMail()`. |
596
- | `verifyOnStartup` | `boolean` / `false` | Call `transporter.verify()` during boot when the transporter supports it. |
620
+ ### API Apps
597
621
 
598
- Mail notes:
599
- - The runtime uses [Nodemailer](https://nodemailer.com/).
600
- - `mail.send(payload)` and `mail.sendMail(payload)` are aliases.
601
- - Each payload must include at least one of `to`, `cc`, or `bcc`.
602
- - Each payload must include `from`, or configure `mail.defaults.from`.
603
- - Injected `mail` is available in handlers, services, models, validators, controllers, subscribers, and loaders, plus `req.aegis.mail`.
622
+ `api` does not create a separate app type. You still build a normal AegisNode app with `routes.js`, `views.js`, `services.js`, and `validators.js`.
623
+ The `api` setting only changes middleware behavior for selected app mounts.
604
624
 
605
- ### API (`api`)
625
+ Think of it this way:
626
+ - `api` controls request/response behavior for an app mount.
627
+ - `auth` controls who can access routes and how tokens are issued/verified.
628
+ - You can use `api` without auth, auth without `api`, or both together.
606
629
 
607
- | Key | Type / Default | Description |
608
- | --- | --- | --- |
609
- | `apps` | `string[]` / `[]` | App names treated as API apps. Mounts are resolved from `settings.apps`. |
610
- | `disableCsrf` | `boolean` / `true` | Skip CSRF checks for API app mounts. |
611
- | `requireJsonForUnsafeMethods` | `boolean` / `true` | Reject unsafe API payloads unless `Content-Type` is JSON (`415`). Multipart is allowed when `uploads.allowApiMultipart=true`. |
612
- | `noStoreHeaders` | `boolean` / `true` | Set `Cache-Control: no-store` on API responses. |
630
+ Common combinations:
631
+ - `api` only: public or internal JSON endpoints with no token auth.
632
+ - `api` + JWT: first-party SPA/mobile/frontend calling your own backend.
633
+ - `api` + OAuth2: third-party clients, machine-to-machine access, or standards-based authorization flows.
613
634
 
614
- API notes:
615
- - `api.apps` contains app names from `settings.apps`, not URL paths.
616
- - Marking an app as API does not generate REST routes or change the app file structure. It only applies API middleware to that app mount.
617
- - The effective API mount comes from `settings.apps[].mount` (or `/${app}` by default). Keep that aligned with your `route.use(...)` mount when `autoMountApps` is off.
635
+ Quick start:
618
636
 
619
- ### Auth (`auth`)
637
+ 1. Declare the app in `settings.apps` and give it a mount.
638
+ 2. Add that app name to `api.apps`.
639
+ 3. Mount the app at the same path in `routes.js` when `autoMountApps` is off.
640
+ 4. Return JSON from your handlers.
641
+ 5. Send JSON for unsafe methods unless you intentionally allow multipart uploads.
620
642
 
621
- | Key | Type / Default | Description |
622
- | --- | --- | --- |
623
- | `enabled` | `boolean` / `false` | Enable auth manager. |
624
- | `provider` | `'jwt' \| 'oauth2'` / `'jwt'` | Active auth provider. |
625
- | `tablePrefix` | `string` / `'aegisnode'` | Prefix for auth storage namespaces/tables; sanitized to lowercase `[a-z0-9_]`. |
626
- | `storage` | `object` / see storage table | Persistence backend for auth state. |
627
- | `jwt` | `object` / see jwt table | JWT configuration. |
628
- | `oauth2` | `object` / see oauth2 table | OAuth2 server and token configuration. |
643
+ Example `settings.js`:
629
644
 
630
- #### Auth Storage (`auth.storage`)
645
+ ```js
646
+ export default {
647
+ apps: [
648
+ { name: 'users', mount: '/users' },
649
+ ],
650
+ api: {
651
+ apps: ['users'],
652
+ disableCsrf: true,
653
+ requireJsonForUnsafeMethods: true,
654
+ noStoreHeaders: true,
655
+ },
656
+ };
657
+ ```
631
658
 
632
- | Key | Type / Default | Description |
633
- | --- | --- | --- |
634
- | `driver` | `'cache' \| 'memory' \| 'file' \| 'database'` / `'cache'` | Storage backend for revocations/clients/tokens. |
635
- | `filePath` | `string` / `'storage/aegisnode-auth-store.json'` | Used when `driver: 'file'`. Relative to project root if not absolute. |
636
- | `tableName` | `string` / `'aegisnode_auth_store'` (prefix-based) | Used when `driver: 'database'` for SQL table or Mongo collection. |
637
- | `collectionName` | `string` / alias only | Legacy alias; if set and `tableName` missing, it becomes `tableName`. |
659
+ Example root `routes.js`:
638
660
 
639
- #### JWT (`auth.jwt`)
661
+ ```js
662
+ import users from './apps/users/routes.js';
640
663
 
641
- | Key | Type / Default | Description |
642
- | --- | --- | --- |
643
- | `secret` | `string` / `security.appSecret` fallback | Signing secret. Required for JWT auth. |
644
- | `algorithm` | `'HS256' \| 'HS384' \| 'HS512'` / `'HS256'` | JWT HMAC algorithm. |
645
- | `issuer` | `string` / `appName` | JWT issuer claim. |
646
- | `audience` | `string` / `appName` | JWT audience claim. |
647
- | `expiresIn` | `string` / `'15m'` | Access token TTL. |
648
- | `refreshExpiresIn` | `string` / `'7d'` | Refresh token TTL. |
664
+ export default {
665
+ register(route) {
666
+ route.use('/users', users); // keep this aligned with settings.apps[].mount
667
+ },
668
+ };
669
+ ```
649
670
 
650
- #### OAuth2 Core (`auth.oauth2`)
671
+ Example `apps/users/routes.js`:
651
672
 
652
- | Key | Type / Default | Description |
653
- | --- | --- | --- |
654
- | `accessTokenTtlSeconds` | `number` / `3600` | Access token TTL in seconds. |
655
- | `refreshTokenTtlSeconds` | `number` / `1209600` | Refresh token TTL in seconds. |
656
- | `authorizationCodeTtlSeconds` | `number` / `600` | Authorization code TTL in seconds. |
657
- | `rotateRefreshToken` | `boolean` / `true` | Rotate refresh token on refresh flow. |
658
- | `requireClientSecret` | `boolean` / `true` | Require secret for confidential client flows. |
659
- | `requirePkce` | `boolean` / `true` | Require PKCE for authorization_code flow. |
660
- | `allowPlainPkce` | `boolean` / `false` | Allow PKCE `plain` method. |
661
- | `grants` | `string[]` / `['authorization_code','refresh_token','client_credentials']` | Enabled OAuth2 grants. |
662
- | `defaultScopes` | `string[]` / `[]` | Scopes assigned when none requested/provided. |
663
- | `clientAuthMethod` | `'client_secret_basic' \| 'client_secret_post' \| 'none'` / `'client_secret_basic'` | Default client authentication method. |
664
- | `server` | `object` / see OAuth2 server table | Built-in authorization server endpoint settings. |
673
+ ```js
674
+ import UsersView from './views.js';
665
675
 
666
- #### OAuth2 Server (`auth.oauth2.server`)
676
+ export default {
677
+ appName: 'users',
678
+ register(route) {
679
+ route.get('/', UsersView.index);
680
+ route.post('/', UsersView.create);
681
+ },
682
+ };
683
+ ```
667
684
 
668
- | Key | Type / Default | Description |
669
- | --- | --- | --- |
670
- | `enabled` | `boolean` / `true` | Mount built-in OAuth2 endpoints. |
671
- | `basePath` | `string` / `'/oauth'` | Base path used to derive endpoint defaults. |
672
- | `authorizePath` | `string` / `'/oauth/authorize'` | Authorization endpoint path. |
673
- | `tokenPath` | `string` / `'/oauth/token'` | Token endpoint path. |
674
- | `introspectionPath` | `string` / `'/oauth/introspect'` | Introspection endpoint path. |
675
- | `revocationPath` | `string` / `'/oauth/revoke'` | Revocation endpoint path. |
676
- | `metadataPath` | `string` / `'/.well-known/oauth-authorization-server'` | OAuth2 metadata endpoint path. |
677
- | `issuer` | `string` / `server.baseUrl` fallback, then `appName` | Issuer value in OAuth2 metadata/tokens. |
678
- | `baseUrl` | `string` / optional alias | Alias used as fallback for `issuer` when `issuer` is empty. |
679
- | `autoApprove` | `boolean` / `true` | Auto-approve auth requests once subject is resolved. |
680
- | `requireAuthenticatedUser` | `boolean` / `true` | Require authenticated user/subject for authorize endpoint. |
681
- | `requireConsent` | `boolean` / `false` | Require explicit consent before issuing auth code. |
682
- | `allowSubjectFromParams` | `boolean` / `false` | Allow subject from request params (`subject`/`user_id`). Keep disabled in production. |
683
- | `allowHttp` | `boolean` / `false` | Allow OAuth2 endpoints on non-HTTPS requests. Keep `false` in production. |
684
- | `resolveSubject` | `function \| null` / `null` | Hook to resolve subject: `({ req, params, client }) => string|null`. |
685
- | `resolveConsent` | `function \| null` / `null` | Hook to resolve consent: `({ req, params, client, subject }) => boolean`. |
685
+ Example `apps/users/views.js`:
686
686
 
687
- ### Swagger (`swagger`)
687
+ ```js
688
+ class UsersView {
689
+ static async index({ service }, req, res, next) {
690
+ try {
691
+ const users = await service.list();
692
+ res.json({ data: users });
693
+ } catch (error) {
694
+ next(error);
695
+ }
696
+ }
688
697
 
689
- | Key | Type / Default | Description |
690
- | --- | --- | --- |
691
- | `enabled` | `boolean` / `false` | Enable Swagger UI + OpenAPI JSON endpoints. |
692
- | `docsPath` | `string` / `'/docs'` | Swagger UI route. |
693
- | `jsonPath` | `string` / `'/openapi.json'` | OpenAPI JSON route. |
694
- | `document` | `object \| null` / `null` | Inline OpenAPI document object. |
695
- | `documentPath` | `string` / `'openapi.json'` | JSON file path used when `document` is not provided. |
696
- | `explorer` | `boolean` / `true` | Enable Swagger UI explorer mode. |
698
+ static async create({ service, validator }, req, res, next) {
699
+ try {
700
+ const payload = validator.create(req.body || {});
701
+ const created = await service.create(payload);
702
+ res.status(201).json({ data: created });
703
+ } catch (error) {
704
+ next(error);
705
+ }
706
+ }
707
+ }
697
708
 
698
- ### Architecture (`architecture`)
709
+ export default UsersView;
710
+ ```
699
711
 
700
- | Key | Type / Default | Description |
701
- | --- | --- | --- |
702
- | `strictLayers` | `boolean` / `false` | Enforce `route -> validator -> service -> model` restrictions. |
712
+ Example requests:
703
713
 
704
- ### Auto Mount (`autoMountApps`)
714
+ ```bash
715
+ curl http://127.0.0.1:3000/users
705
716
 
706
- | Key | Type / Default | Description |
707
- | --- | --- | --- |
708
- | `autoMountApps` | `boolean` / `false` | If `true`, each `apps/<name>/routes.js` is mounted automatically from `settings.apps`. If `false`, central `routes.js` controls mounting. |
717
+ curl -X POST http://127.0.0.1:3000/users \
718
+ -H "Content-Type: application/json" \
719
+ -d '{"name":"Alice"}'
720
+ ```
709
721
 
710
- ### Loaders (`loaders`)
722
+ What the API middleware changes:
723
+ - `POST`, `PUT`, `PATCH`, and `DELETE` with a request body must use `application/json` when `requireJsonForUnsafeMethods: true`.
724
+ - `multipart/form-data` is still allowed for API mounts when `uploads.allowApiMultipart: true`.
725
+ - CSRF is skipped only for configured API app mounts when `disableCsrf: true`.
726
+ - API responses get `Cache-Control: no-store` when `noStoreHeaders: true`.
711
727
 
712
- `loaders` accepts an array of entries run in order during startup:
728
+ What it does not change:
729
+ - It does not auto-generate CRUD endpoints.
730
+ - It does not force a separate `controllers/` or `api/` folder.
731
+ - It does not convert a view into JSON automatically; your handler still decides what to return.
713
732
 
714
- 1. Function entry:
733
+ ### API And Auth Together
715
734
 
716
- ```js
717
- loaders: [
718
- async ({ logger, options, config }) => {
719
- logger.info('loader ran');
720
- },
721
- ]
722
- ```
735
+ `api` and `auth` are separate features that are often used together:
723
736
 
724
- 2. Path entry (relative or absolute):
737
+ - `api` makes an app behave like an API mount: JSON body enforcement, optional CSRF skip, and `Cache-Control: no-store`.
738
+ - `auth` adds token issuance, token verification, route protection, client registration, and revocation/introspection behavior.
725
739
 
726
- ```js
727
- loaders: ['loaders/init-db.js']
728
- ```
740
+ Examples:
741
+ - Public JSON API: enable `api`, leave `auth.enabled` off.
742
+ - Protected JSON API for your own frontend/mobile app: enable `api` and `auth.provider = 'jwt'`.
743
+ - Protected partner/developer API: enable `api` and `auth.provider = 'oauth2'`.
729
744
 
730
- 3. Object entry with options:
745
+ Rule of thumb:
746
+ - If you only need JSON routes, use `api`.
747
+ - If you need authenticated access, add `auth`.
748
+ - If outside clients need a standard auth protocol, choose OAuth2 instead of rolling custom JWT login flows.
731
749
 
732
- ```js
733
- loaders: [
734
- { path: 'loaders/init-queue.js', options: { queue: 'jobs' } },
735
- ]
736
- ```
750
+ Quick comparison:
737
751
 
738
- Each loader module must export a function (default export preferred).
739
- Each loader receives the shared runtime context documented in the injection matrix above, plus `options` from the loader entry.
752
+ | Setup | Use it when | What you configure |
753
+ | --- | --- | --- |
754
+ | `api` only | You need JSON endpoints without token auth. Good for public read APIs or trusted internal services. | `api.apps = [...]` |
755
+ | `api` + JWT | Your own frontend/mobile app talks to your backend and you control both sides. | `api.apps = [...]`, `auth.enabled = true`, `auth.provider = 'jwt'`, plus your own login/token routes |
756
+ | `api` + OAuth2 | External clients, partner apps, or machine clients need standard token flows. | `api.apps = [...]`, `auth.enabled = true`, `auth.provider = 'oauth2'` |
740
757
 
741
- ### Apps (`apps`)
758
+ ### Database Config
742
759
 
743
- `apps` supports two forms:
760
+ Use `database.config` for every dialect (SQL and MongoDB/Mongoose):
744
761
 
745
762
  ```js
746
- apps: ['users', 'billing']
763
+ database: {
764
+ enabled: true,
765
+ dialect: 'pg', // pg | mysql | mssql | sqlite | oracle | mongo | mongodb | mongoose
766
+ config: {
767
+ // SQL example:
768
+ server: 'localhost',
769
+ port: 5432,
770
+ user: 'postgres',
771
+ password: 'postgres',
772
+ database: 'appdb',
773
+
774
+ // Mongo example:
775
+ // connectionString: 'mongodb://localhost:27017/appdb',
776
+ // or:
777
+ // server: 'localhost',
778
+ // port: 27017,
779
+ // database: 'appdb',
780
+ // user: 'mongo_user',
781
+ // password: 'mongo_pass',
782
+ },
783
+ options: {},
784
+ },
747
785
  ```
748
786
 
749
- or
787
+ Legacy note:
788
+ - `database.uri` is still accepted for MongoDB, but `database.config.connectionString` is preferred.
789
+
790
+ Model usage for `mongo` / `mongodb` / `mongoose`:
791
+ - `dbClient` is a QueryMesh client, so you can use the same fluent API as SQL models.
750
792
 
751
793
  ```js
752
- apps: [
753
- { name: 'users', mount: '/users' },
754
- { name: 'billing', mount: '/payments' },
755
- ]
794
+ class UsersModel {
795
+ constructor({ dbClient }) {
796
+ this.db = dbClient;
797
+ }
798
+
799
+ async list() {
800
+ return this.db.table('users').select(['id', 'name']).get();
801
+ }
802
+ }
756
803
  ```
757
804
 
758
- Rules:
759
- - `name` is required in object form.
760
- - `mount` defaults to `/${name}`.
761
- - Mounts are normalized to a single leading slash (except root `/`).
762
- - App route modules must be declared in `settings.apps` before they can load/mount.
805
+ Mongo `_id` / `ObjectId` handling:
806
+ - Use built-in helper `helpers.toObjectId(...)` before filtering on `_id`.
807
+ - Validate with `helpers.isObjectId(...)` when needed.
808
+ - Keep this conversion in model/service layer.
809
+ - If your collection stores string `_id` values (not native Mongo `ObjectId`), skip conversion.
763
810
 
764
- ### Environment Overrides (`environments`)
811
+ ```js
812
+ class UsersModel {
813
+ constructor({ dbClient, helpers }) {
814
+ this.db = dbClient;
815
+ this.helpers = helpers;
816
+ }
765
817
 
766
- Example:
818
+ async findById(id) {
819
+ const _id = this.helpers.toObjectId(id);
820
+ if (!_id) throw new Error('Invalid Mongo ObjectId');
821
+ return this.db.table('users').where('_id', '=', _id).first();
822
+ }
823
+ }
824
+ ```
825
+
826
+ ### Environment Overrides (Single settings.js)
827
+
828
+ You can keep a single `settings.js` and define per-environment overrides:
767
829
 
768
830
  ```js
769
- environments: {
770
- default: {
771
- logging: { level: 'debug' },
831
+ export default {
832
+ env: process.env.NODE_ENV || 'development',
833
+ logging: {
834
+ level: 'info',
772
835
  },
773
- production: {
774
- logging: { level: 'warn' },
775
- security: { ddos: { maxRequests: 80 } },
836
+ security: {
837
+ ddos: {
838
+ maxRequests: 120,
839
+ },
776
840
  },
777
- }
841
+ environments: {
842
+ development: {
843
+ logging: { level: 'debug' },
844
+ },
845
+ production: {
846
+ logging: { level: 'warn' },
847
+ security: { ddos: { maxRequests: 80 } },
848
+ },
849
+ },
850
+ };
778
851
  ```
779
852
 
780
853
  Behavior:
781
- - Base config loads first.
782
- - `environments.default` is merged next (if present).
783
- - `environments[env]` is merged last.
784
- - Arrays in overrides replace full arrays from base.
854
+ - Base config is loaded first.
855
+ - `environments.default` is applied (if present).
856
+ - `environments[env]` is applied last.
857
+ - `env` comes from `settings.env` (fallback: `NODE_ENV`, then `development`).
785
858
 
786
- <!-- SETTINGS_REFERENCE_END -->
859
+ ### Auth (JWT Or OAuth2)
787
860
 
788
- ## Core Concepts And App Structure
861
+ `auth` is independent from `api`.
862
+ You can protect normal web routes, API routes, or both.
863
+ If your app is already listed in `api.apps`, adding `auth` simply means those JSON routes can now require tokens.
789
864
 
790
- ### App File Usage Examples
865
+ Choose the provider based on who is calling your app:
791
866
 
792
- Each generated app usually contains:
793
- - `apps/<app>/views.js`
794
- - `apps/<app>/models.js`
795
- - `apps/<app>/services.js`
796
- - `apps/<app>/subscribers.js`
797
- - `apps/<app>/routes.js`
867
+ - `provider: 'jwt'`
868
+ Best for first-party apps you control.
869
+ You create your own login/token/refresh/logout routes and call `auth.issue(...)` yourself.
870
+ - `provider: 'oauth2'`
871
+ Best when you need a standard authorization server.
872
+ AegisNode mounts `/oauth/*` endpoints for you and supports `authorization_code` + PKCE, `client_credentials`, and `refresh_token`.
798
873
 
799
- Usage by file:
800
- - `views.js`: HTTP handlers (`req`, `res`, `next`). Default signature can be context-first: `handler({ service, validator, services, validators, ... }, req, res, next)`.
801
- - `models.js`: data access layer only (SQL/NoSQL operations).
802
- - `services.js`: business logic layer; orchestrates models.
803
- - `subscribers.js`: event listeners (for example `app.booted`, `ws.connection`, custom events).
804
- - `routes.js`: route mapping only (`route.get(...)`, `route.post(...)`, `route.use(...)`) to view handlers.
874
+ Quick decision guide:
875
+ - Use JWT when your own frontend/mobile app talks only to your backend.
876
+ - Use OAuth2 when external clients, partner apps, or machine-to-machine integrations need standard token flows.
877
+ - Use `auth.middleware()` to protect routes in both modes.
805
878
 
806
- Route modules are mapping-only (`register(route)`).
807
- Framework context is injected into handlers as first argument (when handler uses 4 args): `{ service, validator, services, models, validators, auth, mail, helpers, i18n, events, ... }`.
808
- `req.aegis` is also available.
809
- `service`/`validator` are app-scoped conveniences. For root/non-app routes, use `services.get('<app>.<name>')` / `validators.get('<app>.<name>')`, or create an app-scoped accessor with `services.forApp('<app>')`.
879
+ Enable auth in `settings.js`:
810
880
 
811
- What “app-scoped” means:
812
- - In app routes (for example inside `apps/users/routes.js`), `{ service }` resolves to that app service.
813
- - In root/global routes (`routes.js`), there is no single app context, so use `{ services }` and fetch with `services.forApp('<app>').get('<name>')` or `services.get('<app>.<name>')`.
881
+ ```js
882
+ auth: {
883
+ enabled: true,
884
+ provider: 'jwt', // or 'oauth2'
885
+ tablePrefix: 'aegisnode',
886
+ storage: {
887
+ // cache | memory | file | database
888
+ driver: 'cache',
889
+ filePath: 'storage/aegisnode-auth-store.json',
890
+ // Used by database driver for both SQL table and Mongo collection.
891
+ tableName: 'aegisnode_auth_store',
892
+ },
893
+ jwt: {
894
+ secret: 'replace-with-strong-secret',
895
+ algorithm: 'HS256',
896
+ expiresIn: '15m',
897
+ refreshExpiresIn: '7d',
898
+ issuer: 'blog',
899
+ audience: 'blog',
900
+ },
901
+ oauth2: {
902
+ accessTokenTtlSeconds: 3600,
903
+ refreshTokenTtlSeconds: 1209600,
904
+ authorizationCodeTtlSeconds: 600,
905
+ rotateRefreshToken: true,
906
+ requireClientSecret: true,
907
+ requirePkce: true,
908
+ allowPlainPkce: false,
909
+ grants: ['authorization_code', 'refresh_token', 'client_credentials'],
910
+ defaultScopes: [],
911
+ clientAuthMethod: 'client_secret_basic',
912
+ server: {
913
+ enabled: true,
914
+ basePath: '/oauth',
915
+ authorizePath: '/oauth/authorize',
916
+ tokenPath: '/oauth/token',
917
+ introspectionPath: '/oauth/introspect',
918
+ revocationPath: '/oauth/revoke',
919
+ metadataPath: '/.well-known/oauth-authorization-server',
920
+ issuer: '',
921
+ autoApprove: true,
922
+ requireAuthenticatedUser: true,
923
+ requireConsent: false,
924
+ allowHttp: false,
925
+ },
926
+ },
927
+ },
928
+ ```
814
929
 
815
- Injected runtime dependencies:
930
+ `tablePrefix` is used for auth storage names when enabled:
931
+ - `${tablePrefix}_users`
932
+ - `${tablePrefix}_jwt_revocations`
933
+ - `${tablePrefix}_oauth_clients`
934
+ - `${tablePrefix}_oauth_authorization_codes`
935
+ - `${tablePrefix}_oauth_access_tokens`
936
+ - `${tablePrefix}_oauth_refresh_tokens`
816
937
 
817
- AegisNode injects resolved runtime objects instead of asking app layers to import framework internals. `config` is the resolved runtime config from `settings.js` plus defaults and runtime overrides.
938
+ By default, these names are used as key namespaces.
939
+ - With `storage.driver = 'cache'` or `memory`, they are in-memory/cache key prefixes.
940
+ - With `storage.driver = 'database'`, they are prefixes stored inside `auth.storage.tableName` (used as SQL table name or Mongo collection name).
818
941
 
819
- Available by layer:
820
- - Views/handlers (`views.js` or any context-first route/controller action): `appName`, `app`, `config`, `env`, `i18n`, `mail`, `logger`, `events`, `cache`, `io`, `auth`, `helpers`, `jlive`, `upload`, `services`, `models`, `validators`, `service`, `model`, `validator`, `database`, `dbClient`
821
- - Services (`constructor({ ... })`): `appName`, `config`, `env`, `i18n`, `mail`, `logger`, `events`, `cache`, `io`, `auth`, `helpers`, `jlive`, `models`, `validators`, `services`
822
- - Models (`constructor({ ... })`): `appName`, `config`, `env`, `i18n`, `mail`, `logger`, `events`, `cache`, `io`, `helpers`, `jlive`, `dbClient`, `database`
823
- - Validators (`constructor({ ... })`): `appName`, `config`, `env`, `i18n`, `mail`, `logger`, `events`, `cache`, `io`, `auth`, `helpers`, `jlive`, `dbClient`, `database`
824
- - Subscribers (`export default function ({ ... })`): `appName`, `rootDir`, `config`, `env`, `i18n`, `mail`, `logger`, `events`, `cache`, `io`, `auth`, `helpers`, `jlive`, `upload`, `services`, `models`, `validators`, `database`, `dbClient`, `app`, `server`, `templates`, `protocol`, `container`, `declaredAppNames`
825
- - Controllers (`constructor({ ... })`): `appName`, `rootDir`, `config`, `env`, `i18n`, `mail`, `logger`, `events`, `cache`, `io`, `auth`, `helpers`, `jlive`, `upload`, `services`, `models`, `validators`, `database`, `dbClient`, `container`, `app`
826
- - Loaders (`loaders` entry function): `rootDir`, `config`, `env`, `i18n`, `mail`, `logger`, `events`, `cache`, `io`, `auth`, `helpers`, `jlive`, `upload`, `services`, `models`, `validators`, `database`, `dbClient`, `app`, `server`, `templates`, `protocol`, `container`, `declaredAppNames`, `options`
827
- - Request bridge (`req.aegis`): `config`, `env`, `i18n`, `locale`, `localeSource`, `t`, `setLocale`, `logger`, `events`, `cache`, `io`, `auth`, `mail`, `helpers`, `jlive`, `upload`, `services`, `models`, `validators`, `database`, `dbClient`, `appName`, `app`
828
- - Template locals: `helpers`, `jlive`, `t`, `locale`, `i18n`, `money`, `number`, `dateTime`, `timeElapsed`, `timeDifference`, `breakStr`
942
+ Restart behavior:
943
+ - `auth.enabled = true`: auth manager is available in context after restart.
944
+ - `auth.enabled = false`: auth manager stays safe; `auth.middleware()` returns `503` instead of crashing boot.
945
+ - `auth.storage.driver = 'file'`: OAuth2 clients/tokens and JWT revocations persist across restarts.
946
+ - `auth.storage.driver = 'database'`: OAuth2 clients/tokens and JWT revocations persist in your configured `database` backend.
829
947
 
830
- Key meanings:
948
+ JWT usage in routes:
831
949
 
832
- | Key | Description |
833
- | --- | --- |
834
- | `config` | Resolved runtime config from `settings.js`, framework defaults, environment overrides, and runtime overrides. |
835
- | `env` | Frozen environment snapshot (`process.env` plus runtime additions such as `APP_SECRET`). |
836
- | `i18n` | Translator bridge. During a request it follows the active request locale; outside a request it falls back to `defaultLocale` unless you pass `{ locale }`. |
837
- | `mail` | Mail manager. Use `mail.send({ to, subject, text/html })` or `mail.sendMail(...)`. |
838
- | `logger` | Runtime logger instance. |
839
- | `events` | Event bus used by subscribers and app code. |
840
- | `cache` | Cache backend instance (memory by default). |
841
- | `io` | Socket.IO server instance when websocket support is enabled. |
842
- | `auth` | Auth manager for JWT/OAuth2 flows. |
843
- | `helpers` | Runtime helper functions such as `money`, `number`, `dateTime`, and `timeElapsed`. |
844
- | `jlive` | jlive bridge instance. |
845
- | `upload` | Upload manager used by `route.upload`. |
846
- | `services` | Layer accessor used to fetch services by app/name. |
847
- | `models` | Layer accessor used to fetch models by app/name. |
848
- | `validators` | Layer accessor used to fetch validators by app/name. |
849
- | `service` | App-scoped convenience service for the current app only. |
850
- | `model` | App-scoped convenience model for the current app only. |
851
- | `validator` | App-scoped convenience validator for the current app only. |
852
- | `database` | Database runtime wrapper. |
853
- | `dbClient` | Low-level database/query client. |
854
- | `appName` | Current app name. |
855
- | `app` | Current app metadata/context. |
856
- | `rootDir` | Absolute project root. |
857
- | `server` | HTTP/HTTPS server instance. |
858
- | `templates` | Resolved template-engine configuration. |
859
- | `protocol` | Server protocol (`http` or `https`). |
860
- | `container` | Internal DI container. |
861
- | `declaredAppNames` | Set of apps declared in config/routes. |
862
- | `options` | Loader-specific options object from a `{ path, options }` loader entry. |
863
- | `locale` | Active request locale. Available on `req.aegis` and template locals. |
864
- | `localeSource` | Where the current locale came from (`query`, `cookie`, `header`, `manual`, or `disabled`). |
865
- | `t` | Convenience translator shortcut for the current request/template scope. |
866
- | `setLocale` | Request helper used to change and optionally persist the active locale. |
950
+ - JWT does not create login routes for you.
951
+ - You define the endpoints that authenticate users and issue tokens.
952
+ - This is usually the simplest choice for a private API used only by your own frontend/mobile app.
867
953
 
868
954
  ```js
869
- // routes.js (root/global)
870
955
  export default {
871
956
  register(route) {
872
- route.get('/dashboard', async ({ services }, req, res, next) => {
873
- try {
874
- const usersService = services.forApp('users').get('users');
875
- const ordersService = services.forApp('orders').get('orders');
876
- res.json({
877
- users: await usersService.list(),
878
- orders: await ordersService.list(),
879
- });
880
- } catch (error) {
881
- next(error);
882
- }
957
+ const authGuard = (req, res, next) => req.aegis.auth.middleware()(req, res, next);
958
+
959
+ route.get('/auth/token', (req, res) => {
960
+ const token = req.aegis.auth.issue({ subject: 'u1', scope: ['read:users'] });
961
+ res.json({ token });
962
+ });
963
+
964
+ route.get('/auth/me', authGuard, (req, res) => {
965
+ res.json({ user: req.auth });
883
966
  });
884
967
  },
885
968
  };
886
969
  ```
887
970
 
888
- Example `views.js`:
971
+ OAuth2 built-in authorization server endpoints (when `auth.provider='oauth2'` and `auth.oauth2.server.enabled=true`):
889
972
 
890
- ```js
891
- class UsersView {
892
- static async index({ service }, req, res, next) {
893
- try {
894
- const data = await service.listUsers();
895
- res.json({ data });
896
- } catch (error) {
897
- next(error);
898
- }
899
- }
973
+ - `GET /oauth/authorize`
974
+ - `POST /oauth/authorize`
975
+ - `POST /oauth/token`
976
+ - `POST /oauth/introspect`
977
+ - `POST /oauth/revoke`
978
+ - `GET /.well-known/oauth-authorization-server`
900
979
 
901
- static async create({ service, validator }, req, res, next) {
902
- try {
903
- const payload = validator.create(req.body || {});
904
- const created = await service.createUser(payload);
905
- res.status(201).json({ data: created });
906
- } catch (error) {
907
- next(error);
908
- }
909
- }
980
+ Flows supported:
981
+ - `authorization_code` (with PKCE)
982
+ - `client_credentials`
983
+ - `refresh_token`
910
984
 
911
- static async tools({ service, helpers, jlive }, req, res, next) {
912
- try {
913
- const stats = await service.stats();
914
- res.json({
915
- stats,
916
- total: helpers.money(1299.5, { currency: 'USD' }),
917
- elapsed: helpers.timeElapsed(Date.now() - 60_000),
918
- token: jlive.generate(16),
919
- });
920
- } catch (error) {
921
- next(error);
922
- }
923
- }
924
- }
985
+ OAuth2 is the better choice when:
986
+ - you need standards-based client registration and token exchange,
987
+ - you need machine clients as well as browser/mobile clients,
988
+ - or third parties must integrate without depending on your custom JWT login route shape.
925
989
 
926
- export default UsersView;
927
- ```
990
+ #### Route Usage (JWT vs OAuth2)
928
991
 
929
- Example `models.js`:
992
+ `startproject` gives you one root route file: `routes.js`.
993
+ All your custom HTTP routes are defined there (or in app routes you mount with `route.use(...)`).
930
994
 
931
995
  ```js
932
- class UsersModel {
933
- constructor({ dbClient }) {
934
- this.dbClient = dbClient;
935
- }
936
-
937
- async list() {
938
- return [{ id: '1', name: 'Alice' }];
939
- }
996
+ // routes.js
997
+ import users from './apps/users/routes.js';
940
998
 
941
- async create(payload) {
942
- return { id: '2', ...payload };
943
- }
944
- }
999
+ export default {
1000
+ register(route) {
1001
+ route.use('/users', users);
945
1002
 
946
- export default { users: UsersModel };
1003
+ // Your custom auth/business routes
1004
+ route.post('/auth/login', (req, res) => {
1005
+ const token = req.aegis.auth.issue({ subject: 'u1' });
1006
+ res.json({ token });
1007
+ });
1008
+ },
1009
+ };
947
1010
  ```
948
1011
 
949
- Example `services.js`:
950
-
951
- ```js
952
- class UsersService {
953
- constructor({ models, env }) {
954
- this.usersModel = models.get('users');
955
- this.env = env;
956
- }
1012
+ How this behaves:
1013
+ - `provider: 'jwt'`: Aegis does not create JWT endpoints automatically. You define login/token/refresh/logout routes yourself in `routes.js` (or mounted app routes).
1014
+ - `provider: 'oauth2'`: Aegis auto-mounts OAuth2 server endpoints (`/oauth/authorize`, `/oauth/token`, `/oauth/introspect`, `/oauth/revoke`, metadata). You only define your own extra routes (for example admin client setup, protected APIs, business routes).
1015
+ - Do not reuse built-in OAuth2 endpoint paths for your own handlers when OAuth2 server is enabled.
957
1016
 
958
- async listUsers() {
959
- return this.usersModel.list();
960
- }
1017
+ Typical setup patterns:
1018
+ - API + JWT:
1019
+ `api.apps = ['users']`, `auth.provider = 'jwt'`, custom `/auth/login`, protect `/users/*` with `req.aegis.auth.middleware()`.
1020
+ - API + OAuth2:
1021
+ `api.apps = ['users']`, `auth.provider = 'oauth2'`, use built-in `/oauth/token`, protect `/users/*` with `req.aegis.auth.middleware()`.
1022
+ - Web app + JWT:
1023
+ no `api` block required if routes are normal form/web routes, but you can still use JWT for selected endpoints.
961
1024
 
962
- async createUser(payload) {
963
- return this.usersModel.create(payload);
964
- }
965
- }
1025
+ #### OAuth2 Full Usage
966
1026
 
967
- export default { users: UsersService };
968
- ```
1027
+ 1. Register clients (server-side only)
969
1028
 
970
- Example `subscribers.js`:
1029
+ Register clients programmatically with `auth.registerClient(...)`.
1030
+ Do not expose this publicly in production without admin protection.
971
1031
 
972
1032
  ```js
973
- export default function registerUsersSubscribers({ events, logger }) {
974
- events.subscribe('app.booted', ({ appName }) => {
975
- logger.info('[users] booted: %s', appName);
976
- });
977
- }
1033
+ export default {
1034
+ register(route) {
1035
+ route.post('/admin/oauth/setup-clients', (req, res) => {
1036
+ const webClient = req.aegis.auth.registerClient({
1037
+ clientId: 'web',
1038
+ clientSecret: 'secret',
1039
+ redirectUris: ['https://client.example.com/callback'],
1040
+ grants: ['authorization_code', 'refresh_token'],
1041
+ scopes: ['read:users'],
1042
+ });
1043
+
1044
+ const machineClient = req.aegis.auth.registerClient({
1045
+ clientId: 'machine',
1046
+ clientSecret: 'machine-secret',
1047
+ grants: ['client_credentials'],
1048
+ scopes: ['read:users'],
1049
+ });
1050
+
1051
+ res.json({ webClient, machineClient });
1052
+ });
1053
+ },
1054
+ };
978
1055
  ```
979
1056
 
980
- Injected `env` is also available in:
981
- - view handler context: `static index({ env }, req, res) { ... }`
982
- - model constructors: `constructor({ dbClient, env }) { ... }`
983
- - subscribers: `export default function ({ events, env }) { ... }`
984
- - request runtime bridge: `req.aegis.env`
1057
+ Notes:
1058
+ - Secret is stored hashed (scrypt).
1059
+ - Returned client object does not include the secret/hash.
1060
+ - `authorization_code` clients must have at least one `redirectUri`.
985
1061
 
986
- Example `routes.js`:
1062
+ 2. Authorization Code + PKCE flow
1063
+
1064
+ Create PKCE verifier/challenge:
987
1065
 
988
1066
  ```js
989
- import UsersView from './views.js';
1067
+ import crypto from 'crypto';
990
1068
 
991
- export default {
992
- appName: 'users',
993
- register(route) {
994
- route.get('/home', UsersView.home);
995
- route.get('/', UsersView.index);
996
- route.post('/', UsersView.create);
997
- },
998
- };
1069
+ function b64url(buffer) {
1070
+ return Buffer.from(buffer).toString('base64')
1071
+ .replace(/\+/g, '-')
1072
+ .replace(/\//g, '_')
1073
+ .replace(/=+$/g, '');
1074
+ }
1075
+
1076
+ const codeVerifier = b64url(crypto.randomBytes(48));
1077
+ const codeChallenge = b64url(crypto.createHash('sha256').update(codeVerifier).digest());
999
1078
  ```
1000
1079
 
1001
- ## Common Tasks And Feature Guides
1080
+ Redirect user-agent to authorize endpoint:
1002
1081
 
1003
- ### File Uploads
1082
+ ```txt
1083
+ GET /oauth/authorize
1084
+ ?response_type=code
1085
+ &client_id=web
1086
+ &redirect_uri=https%3A%2F%2Fclient.example.com%2Fcallback
1087
+ &scope=read%3Ausers
1088
+ &state=abc123
1089
+ &code_challenge=<CODE_CHALLENGE>
1090
+ &code_challenge_method=S256
1091
+ ```
1004
1092
 
1005
- AegisNode provides built-in upload middleware on route API as `route.upload`.
1093
+ The server redirects back to:
1006
1094
 
1007
- Storage location:
1008
- - Default folder: `<project-root>/uploads`
1009
- - Change with `settings.uploads.dir` (relative or absolute path)
1095
+ ```txt
1096
+ https://client.example.com/callback?code=<AUTH_CODE>&state=abc123
1097
+ ```
1010
1098
 
1011
- Recommended upload settings:
1099
+ Exchange code for tokens:
1012
1100
 
1013
- ```js
1014
- uploads: {
1015
- enabled: true,
1016
- dir: 'storage/uploads',
1017
- createDir: true,
1018
- preserveExtension: true,
1019
- maxFileSize: '5mb',
1020
- maxFiles: 5,
1021
- maxFields: 50,
1022
- maxFieldSize: '1mb',
1023
- allowedMimeTypes: ['image/png', 'image/jpeg'],
1024
- allowedExtensions: ['.png', '.jpg', '.jpeg'],
1025
- allowApiMultipart: true,
1026
- },
1101
+ ```bash
1102
+ curl -X POST http://127.0.0.1:3000/oauth/token \
1103
+ -H "Content-Type: application/x-www-form-urlencoded" \
1104
+ -u web:secret \
1105
+ -d "grant_type=authorization_code" \
1106
+ -d "code=<AUTH_CODE>" \
1107
+ -d "redirect_uri=https://client.example.com/callback" \
1108
+ -d "code_verifier=<CODE_VERIFIER>"
1027
1109
  ```
1028
1110
 
1029
- Route middleware modes:
1111
+ Response shape:
1030
1112
 
1031
- ```js
1032
- import UsersView from './views.js';
1113
+ ```json
1114
+ {
1115
+ "access_token": "...",
1116
+ "token_type": "Bearer",
1117
+ "expires_in": 3600,
1118
+ "scope": "read:users",
1119
+ "refresh_token": "...",
1120
+ "refresh_expires_in": 1209600
1121
+ }
1122
+ ```
1033
1123
 
1124
+ 3. Protect API routes with OAuth2 access tokens
1125
+
1126
+ ```js
1034
1127
  export default {
1035
- appName: 'users',
1036
1128
  register(route) {
1037
- // One file -> req.file
1038
- route.post('/avatar', route.upload.single('avatar'), UsersView.uploadAvatar);
1039
-
1040
- // Many files from one input name -> req.files (array)
1041
- route.post('/gallery', route.upload.array('photos', 6), UsersView.uploadGallery);
1042
-
1043
- // Many named file inputs -> req.files.<fieldName> (array)
1044
- route.post(
1045
- '/documents',
1046
- route.upload.fields([
1047
- { name: 'avatar', maxCount: 1 },
1048
- { name: 'docs', maxCount: 3 },
1049
- ]),
1050
- UsersView.uploadDocuments,
1051
- );
1052
-
1053
- // Accept all file fields -> req.files (array)
1054
- route.post('/any-upload', route.upload.any(), UsersView.uploadAny);
1055
-
1056
- // No files, parse multipart text fields only
1057
- route.post('/multipart-no-file', route.upload.none(), UsersView.multipartNoFile);
1129
+ const authGuard = (req, res, next) => req.aegis.auth.middleware()(req, res, next);
1130
+ route.get('/users/me', authGuard, (req, res) => {
1131
+ res.json({
1132
+ sub: req.auth.sub || null,
1133
+ clientId: req.auth.clientId,
1134
+ scope: req.auth.scope,
1135
+ });
1136
+ });
1058
1137
  },
1059
1138
  };
1060
1139
  ```
1061
1140
 
1062
- `req` payload shape:
1063
- - `single()`: `req.file` + `req.body`
1064
- - `array()`: `req.files` (array) + `req.body`
1065
- - `fields()`: `req.files` object (`req.files.avatar`, `req.files.docs`, ...) + `req.body`
1066
-
1067
- Custom route with form fields + file:
1068
-
1069
- ```js
1070
- // apps/users/routes.js
1071
- import UsersView from './views.js';
1141
+ Use token:
1072
1142
 
1073
- export default {
1074
- appName: 'users',
1075
- register(route) {
1076
- route.post('/profile/update', route.upload.single('avatar'), UsersView.updateProfile);
1077
- },
1078
- };
1143
+ ```bash
1144
+ curl http://127.0.0.1:3000/users/me \
1145
+ -H "Authorization: Bearer <ACCESS_TOKEN>"
1079
1146
  ```
1080
1147
 
1081
- ```js
1082
- // apps/users/views.js
1083
- class UsersView {
1084
- static updateProfile(_context, req, res) {
1085
- const { username, bio } = req.body;
1086
- const avatar = req.file || null;
1087
-
1088
- return res.json({
1089
- username,
1090
- bio,
1091
- avatar: avatar ? {
1092
- name: avatar.filename,
1093
- originalName: avatar.originalname,
1094
- mimeType: avatar.mimetype,
1095
- size: avatar.size,
1096
- path: avatar.path,
1097
- } : null,
1098
- });
1099
- }
1100
- }
1148
+ 4. Refresh token flow
1101
1149
 
1102
- export default UsersView;
1150
+ ```bash
1151
+ curl -X POST http://127.0.0.1:3000/oauth/token \
1152
+ -H "Content-Type: application/x-www-form-urlencoded" \
1153
+ -u web:secret \
1154
+ -d "grant_type=refresh_token" \
1155
+ -d "refresh_token=<REFRESH_TOKEN>"
1103
1156
  ```
1104
1157
 
1105
- ```html
1106
- <form action="/users/profile/update" method="POST" enctype="multipart/form-data">
1107
- <%= csrfToken %>
1108
- <input name="username" />
1109
- <textarea name="bio"></textarea>
1110
- <input type="file" name="avatar" />
1111
- <button type="submit">Save</button>
1112
- </form>
1158
+ 5. Client Credentials flow (machine-to-machine)
1159
+
1160
+ ```bash
1161
+ curl -X POST http://127.0.0.1:3000/oauth/token \
1162
+ -H "Content-Type: application/x-www-form-urlencoded" \
1163
+ -u machine:machine-secret \
1164
+ -d "grant_type=client_credentials" \
1165
+ -d "scope=read:users"
1113
1166
  ```
1114
1167
 
1115
- Upload limits and rejections:
1116
- - Per-file size limit from `uploads.maxFileSize` returns `413` when exceeded.
1117
- - Total files limit from `uploads.maxFiles` returns `413` when exceeded.
1118
- - `allowedMimeTypes` / `allowedExtensions` mismatch returns `415`.
1168
+ This returns access token only (no refresh token).
1119
1169
 
1120
- Important behavior:
1121
- - If `uploads.enabled=false`, using `route.upload.*` throws at route registration.
1122
- - For API mounts, multipart is allowed only when `uploads.allowApiMultipart=true`.
1123
- - For non-API form submissions, CSRF token is required by default.
1170
+ 6. Introspection and revocation
1124
1171
 
1125
- ### API Apps
1172
+ Introspection:
1126
1173
 
1127
- `api` does not create a separate app type. You still build a normal AegisNode app with `routes.js`, `views.js`, `services.js`, and `validators.js`.
1128
- The `api` setting only changes middleware behavior for selected app mounts.
1174
+ ```bash
1175
+ curl -X POST http://127.0.0.1:3000/oauth/introspect \
1176
+ -H "Content-Type: application/x-www-form-urlencoded" \
1177
+ -u web:secret \
1178
+ -d "token=<ACCESS_TOKEN>"
1179
+ ```
1129
1180
 
1130
- Think of it this way:
1131
- - `api` controls request/response behavior for an app mount.
1132
- - `auth` controls who can access routes and how tokens are issued/verified.
1133
- - You can use `api` without auth, auth without `api`, or both together.
1181
+ Revocation:
1134
1182
 
1135
- Common combinations:
1136
- - `api` only: public or internal JSON endpoints with no token auth.
1137
- - `api` + JWT: first-party SPA/mobile/frontend calling your own backend.
1138
- - `api` + OAuth2: third-party clients, machine-to-machine access, or standards-based authorization flows.
1183
+ ```bash
1184
+ curl -X POST http://127.0.0.1:3000/oauth/revoke \
1185
+ -H "Content-Type: application/x-www-form-urlencoded" \
1186
+ -u web:secret \
1187
+ -d "token=<ACCESS_OR_REFRESH_TOKEN>"
1188
+ ```
1139
1189
 
1140
- Quick start:
1190
+ 7. Custom subject/consent resolution
1141
1191
 
1142
- 1. Declare the app in `settings.apps` and give it a mount.
1143
- 2. Add that app name to `api.apps`.
1144
- 3. Mount the app at the same path in `routes.js` when `autoMountApps` is off.
1145
- 4. Return JSON from your handlers.
1146
- 5. Send JSON for unsafe methods unless you intentionally allow multipart uploads.
1192
+ By default, `/oauth/authorize` resolves subject from:
1193
+ - `req.user.id`
1194
+ - `req.user.sub`
1195
+ - `req.auth.sub`
1196
+ - `subject`/`user_id` query/body params
1147
1197
 
1148
- Example `settings.js`:
1198
+ You can override with hooks in `settings.js`:
1149
1199
 
1150
1200
  ```js
1151
- export default {
1152
- apps: [
1153
- { name: 'users', mount: '/users' },
1154
- ],
1155
- api: {
1156
- apps: ['users'],
1157
- disableCsrf: true,
1158
- requireJsonForUnsafeMethods: true,
1159
- noStoreHeaders: true,
1201
+ auth: {
1202
+ provider: 'oauth2',
1203
+ oauth2: {
1204
+ server: {
1205
+ resolveSubject: ({ req }) => req.user?.id || '',
1206
+ resolveConsent: ({ req, client, subject }) => {
1207
+ // return true to approve, false to deny
1208
+ return true;
1209
+ },
1210
+ },
1160
1211
  },
1161
- };
1212
+ },
1162
1213
  ```
1163
1214
 
1164
- Example root `routes.js`:
1215
+ 8. Production checklist
1165
1216
 
1166
- ```js
1167
- import users from './apps/users/routes.js';
1217
+ - Keep `auth.oauth2.server.allowHttp = false` (default).
1218
+ - Use HTTPS with trusted reverse proxy config.
1219
+ - Keep `requirePkce = true`.
1220
+ - Use strong client secrets and rotate regularly.
1221
+ - Restrict client setup endpoints to admins only.
1222
+ - Set explicit `defaultScopes` and per-client scopes.
1223
+ - Set `issuer` to your public auth server URL.
1168
1224
 
1169
- export default {
1170
- register(route) {
1171
- route.use('/users', users); // keep this aligned with settings.apps[].mount
1172
- },
1173
- };
1174
- ```
1225
+ Implementation notes:
1226
+ - OAuth2 server in AegisNode is framework-native (custom implementation).
1227
+ - CSRF checks are skipped for OAuth2 server endpoints (`/oauth/*` + metadata) by design.
1228
+ - This is OAuth2 (not OpenID Connect); no `id_token` endpoint/flow.
1175
1229
 
1176
- Example `apps/users/routes.js`:
1230
+ ### Swagger (OpenAPI UI)
1177
1231
 
1178
- ```js
1179
- import UsersView from './views.js';
1232
+ Enable Swagger in `settings.js`:
1180
1233
 
1181
- export default {
1182
- appName: 'users',
1183
- register(route) {
1184
- route.get('/', UsersView.index);
1185
- route.post('/', UsersView.create);
1186
- },
1187
- };
1234
+ ```js
1235
+ swagger: {
1236
+ enabled: true,
1237
+ docsPath: '/docs',
1238
+ jsonPath: '/openapi.json',
1239
+ documentPath: 'openapi.json',
1240
+ explorer: true,
1241
+ },
1188
1242
  ```
1189
1243
 
1190
- Example `apps/users/views.js`:
1244
+ Behavior:
1245
+ - UI available at `docsPath` (default `/docs`).
1246
+ - OpenAPI JSON available at `jsonPath` (default `/openapi.json`).
1247
+ - If `openapi.json` exists in project root, it is loaded.
1248
+ - If no file is found, AegisNode serves a default minimal OpenAPI document.
1191
1249
 
1192
- ```js
1193
- class UsersView {
1194
- static async index({ service }, req, res, next) {
1195
- try {
1196
- const users = await service.list();
1197
- res.json({ data: users });
1198
- } catch (error) {
1199
- next(error);
1200
- }
1201
- }
1250
+ ### Templates (EJS + base.ejs)
1202
1251
 
1203
- static async create({ service, validator }, req, res, next) {
1204
- try {
1205
- const payload = validator.create(req.body || {});
1206
- const created = await service.create(payload);
1207
- res.status(201).json({ data: created });
1208
- } catch (error) {
1209
- next(error);
1210
- }
1211
- }
1212
- }
1252
+ Set template config in `settings.js`:
1213
1253
 
1214
- export default UsersView;
1254
+ ```js
1255
+ templates: {
1256
+ enabled: true,
1257
+ engine: 'ejs',
1258
+ dir: 'templates',
1259
+ base: 'base',
1260
+ appBases: {
1261
+ users: 'users/base',
1262
+ admin: 'admin/base',
1263
+ },
1264
+ }
1215
1265
  ```
1216
1266
 
1217
- Example requests:
1267
+ Then in a route handler:
1218
1268
 
1219
- ```bash
1220
- curl http://127.0.0.1:3000/users
1269
+ ```js
1270
+ route.get('/', (req, res) => {
1271
+ res.render('home', {
1272
+ title: 'Home',
1273
+ message: 'Welcome',
1274
+ });
1275
+ });
1276
+ ```
1221
1277
 
1222
- curl -X POST http://127.0.0.1:3000/users \
1223
- -H "Content-Type: application/json" \
1224
- -d '{"name":"Alice"}'
1225
- ```
1278
+ `home.ejs` is rendered first, then injected into `base.ejs`.
1279
+ Use `<%- content %>` (or `<%- body %>`) in your `base.ejs` to print page content.
1226
1280
 
1227
- What the API middleware changes:
1228
- - `POST`, `PUT`, `PATCH`, and `DELETE` with a request body must use `application/json` when `requireJsonForUnsafeMethods: true`.
1229
- - `multipart/form-data` is still allowed for API mounts when `uploads.allowApiMultipart: true`.
1230
- - CSRF is skipped only for configured API app mounts when `disableCsrf: true`.
1231
- - API responses get `Cache-Control: no-store` when `noStoreHeaders: true`.
1281
+ ### Internationalization (i18n)
1232
1282
 
1233
- What it does not change:
1234
- - It does not auto-generate CRUD endpoints.
1235
- - It does not force a separate `controllers/` or `api/` folder.
1236
- - It does not convert a view into JSON automatically; your handler still decides what to return.
1283
+ Configure i18n in `settings.js`:
1237
1284
 
1238
- ### API And Auth Together
1285
+ ```js
1286
+ i18n: {
1287
+ enabled: true,
1288
+ defaultLocale: 'en',
1289
+ fallbackLocale: 'en',
1290
+ supported: ['en', 'fr'],
1291
+ queryParam: 'lang',
1292
+ translations: {
1293
+ en: {
1294
+ home: {
1295
+ title: 'Welcome {name}',
1296
+ },
1297
+ },
1298
+ fr: {
1299
+ home: {
1300
+ title: 'Bienvenue {name}',
1301
+ },
1302
+ },
1303
+ },
1304
+ }
1305
+ ```
1239
1306
 
1240
- `api` and `auth` are separate features that are often used together:
1307
+ You can also load locale JSON files directly (no import needed):
1241
1308
 
1242
- - `api` makes an app behave like an API mount: JSON body enforcement, optional CSRF skip, and `Cache-Control: no-store`.
1243
- - `auth` adds token issuance, token verification, route protection, client registration, and revocation/introspection behavior.
1309
+ ```js
1310
+ i18n: {
1311
+ enabled: true,
1312
+ defaultLocale: 'en',
1313
+ supported: ['en', 'fr'],
1314
+ translations: {
1315
+ en: 'locales/en.json',
1316
+ fr: 'locales/fr.json',
1317
+ },
1318
+ // optional single-file source (inline `translations` wins per locale key):
1319
+ // translationsFile: 'locales/all.json',
1320
+ }
1321
+ ```
1244
1322
 
1245
- Examples:
1246
- - Public JSON API: enable `api`, leave `auth.enabled` off.
1247
- - Protected JSON API for your own frontend/mobile app: enable `api` and `auth.provider = 'jwt'`.
1248
- - Protected partner/developer API: enable `api` and `auth.provider = 'oauth2'`.
1323
+ Route usage:
1249
1324
 
1250
- Rule of thumb:
1251
- - If you only need JSON routes, use `api`.
1252
- - If you need authenticated access, add `auth`.
1253
- - If outside clients need a standard auth protocol, choose OAuth2 instead of rolling custom JWT login flows.
1325
+ ```js
1326
+ route.get('/i18n-demo', (req, res) => {
1327
+ // Auto-detected from query/cookie/header.
1328
+ res.json({
1329
+ locale: req.aegis.locale,
1330
+ title: req.aegis.t('home.title', { name: 'Jason' }),
1331
+ });
1332
+ });
1333
+ ```
1254
1334
 
1255
- Quick comparison:
1335
+ Choosing the API:
1256
1336
 
1257
- | Setup | Use it when | What you configure |
1258
- | --- | --- | --- |
1259
- | `api` only | You need JSON endpoints without token auth. Good for public read APIs or trusted internal services. | `api.apps = [...]` |
1260
- | `api` + JWT | Your own frontend/mobile app talks to your backend and you control both sides. | `api.apps = [...]`, `auth.enabled = true`, `auth.provider = 'jwt'`, plus your own login/token routes |
1261
- | `api` + OAuth2 | External clients, partner apps, or machine clients need standard token flows. | `api.apps = [...]`, `auth.enabled = true`, `auth.provider = 'oauth2'` |
1337
+ - `req.aegis.t('home.title')`
1338
+ Shortcut for `req.aegis.i18n.t('home.title')`. Use this in routes/views when you only need a translated string.
1339
+ - `req.aegis.i18n`
1340
+ Request-scoped i18n object. Use this when you also need locale metadata or helpers such as `locale`, `localeSource`, `setLocale(...)`, `resolveLocale(...)`, or `forLocale(...)`.
1341
+ - Injected `i18n` in handlers/services/models/validators/controllers/subscribers/loaders
1342
+ Runtime-injected i18n bridge. During an HTTP request, `i18n.t(...)` resolves with the same active locale as `req.aegis.i18n.t(...)`, so the translation result is the same.
1262
1343
 
1263
- ### Database Config
1344
+ Important differences:
1264
1345
 
1265
- Use `database.config` for every dialect (SQL and MongoDB/Mongoose):
1346
+ - `req.aegis.t` and `req.aegis.i18n.t` return the same translation for the current request.
1347
+ - Injected `i18n.t(...)` in a service/model/validator/subscriber is not the same object as `req.aegis.i18n`, but during a request it produces the same translation result for the same key/options.
1348
+ - Outside a request, injected `i18n.t(...)` falls back to `defaultLocale`.
1349
+ - In background jobs, loaders, or boot-time code, pass an explicit locale when needed: `i18n.t('home.title', { name: 'Jason' }, { locale: 'fr' })`.
1350
+
1351
+ Service/model usage:
1266
1352
 
1267
1353
  ```js
1268
- database: {
1269
- enabled: true,
1270
- dialect: 'pg', // pg | mysql | mssql | sqlite | oracle | mongo | mongodb | mongoose
1271
- config: {
1272
- // SQL example:
1273
- server: 'localhost',
1274
- port: 5432,
1275
- user: 'postgres',
1276
- password: 'postgres',
1277
- database: 'appdb',
1354
+ class UsersService {
1355
+ constructor({ i18n }) {
1356
+ this.i18n = i18n;
1357
+ }
1278
1358
 
1279
- // Mongo example:
1280
- // connectionString: 'mongodb://localhost:27017/appdb',
1281
- // or:
1282
- // server: 'localhost',
1283
- // port: 27017,
1284
- // database: 'appdb',
1285
- // user: 'mongo_user',
1286
- // password: 'mongo_pass',
1287
- },
1288
- options: {},
1289
- },
1359
+ greeting(name) {
1360
+ return this.i18n.t('home.title', { name });
1361
+ }
1362
+ }
1290
1363
  ```
1291
1364
 
1292
- Legacy note:
1293
- - `database.uri` is still accepted for MongoDB, but `database.config.connectionString` is preferred.
1365
+ Template usage:
1294
1366
 
1295
- Model usage for `mongo` / `mongodb` / `mongoose`:
1296
- - `dbClient` is a QueryMesh client, so you can use the same fluent API as SQL models.
1367
+ ```ejs
1368
+ <html lang="<%= locale %>">
1369
+ <body>
1370
+ <h1><%= t('home.title', { name: 'Jason' }) %></h1>
1371
+ </body>
1372
+ </html>
1373
+ ```
1297
1374
 
1298
- ```js
1299
- class UsersModel {
1300
- constructor({ dbClient }) {
1301
- this.db = dbClient;
1302
- }
1375
+ Manual locale switch inside a request:
1303
1376
 
1304
- async list() {
1305
- return this.db.table('users').select(['id', 'name']).get();
1306
- }
1307
- }
1377
+ ```js
1378
+ route.get('/fr', (req, res) => {
1379
+ req.aegis.setLocale('fr'); // persists in i18n cookie by default
1380
+ res.send(req.aegis.t('home.title', { name: 'Jason' }));
1381
+ });
1308
1382
  ```
1309
1383
 
1310
- Mongo `_id` / `ObjectId` handling:
1311
- - Use built-in helper `helpers.toObjectId(...)` before filtering on `_id`.
1312
- - Validate with `helpers.isObjectId(...)` when needed.
1313
- - Keep this conversion in model/service layer.
1314
- - If your collection stores string `_id` values (not native Mongo `ObjectId`), skip conversion.
1384
+ Persist user-selected language as default:
1315
1385
 
1316
1386
  ```js
1317
- class UsersModel {
1318
- constructor({ dbClient, helpers }) {
1319
- this.db = dbClient;
1320
- this.helpers = helpers;
1321
- }
1387
+ route.post('/lang', (req, res) => {
1388
+ const selected = String(req.body?.lang || '').trim();
1389
+ req.aegis.setLocale(selected); // writes i18n cookie (aegis_locale by default)
1390
+ res.redirect('back');
1391
+ });
1392
+ ```
1322
1393
 
1323
- async findById(id) {
1324
- const _id = this.helpers.toObjectId(id);
1325
- if (!_id) throw new Error('Invalid Mongo ObjectId');
1326
- return this.db.table('users').where('_id', '=', _id).first();
1327
- }
1328
- }
1394
+ Language picker template (keeps selected option):
1395
+
1396
+ ```ejs
1397
+ <form method="post" action="/lang">
1398
+ <select name="lang">
1399
+ <option value="en" <%= locale === 'en' ? 'selected' : '' %>>English</option>
1400
+ <option value="fr" <%= locale === 'fr' ? 'selected' : '' %>>Français</option>
1401
+ </select>
1402
+ <button type="submit">Change</button>
1403
+ </form>
1329
1404
  ```
1330
1405
 
1331
- ### Environment Overrides (Single settings.js)
1406
+ Notes:
1407
+ - `defaultLocale` is used only when user has no saved locale.
1408
+ - After selection, cookie locale becomes the default for that user on next requests.
1409
+ - `?lang=fr` also persists automatically when `detectFromQuery` is enabled.
1410
+ - Templates get `t`, `locale`, and `i18n` in locals.
1332
1411
 
1333
- You can keep a single `settings.js` and define per-environment overrides:
1412
+ ### Mail
1413
+
1414
+ Configure mail transport in `settings.js`:
1334
1415
 
1335
1416
  ```js
1336
1417
  export default {
1337
- env: process.env.NODE_ENV || 'development',
1338
- logging: {
1339
- level: 'info',
1340
- },
1341
- security: {
1342
- ddos: {
1343
- maxRequests: 120,
1344
- },
1345
- },
1346
- environments: {
1347
- development: {
1348
- logging: { level: 'debug' },
1418
+ mail: {
1419
+ enabled: true,
1420
+ defaults: {
1421
+ from: 'noreply@example.com',
1422
+ replyTo: 'support@example.com',
1349
1423
  },
1350
- production: {
1351
- logging: { level: 'warn' },
1352
- security: { ddos: { maxRequests: 80 } },
1424
+ transport: {
1425
+ host: process.env.SMTP_HOST,
1426
+ port: process.env.SMTP_PORT ? Number(process.env.SMTP_PORT) : 587,
1427
+ secure: false,
1428
+ auth: {
1429
+ user: process.env.SMTP_USER,
1430
+ pass: process.env.SMTP_PASS,
1431
+ },
1353
1432
  },
1433
+ verifyOnStartup: true,
1354
1434
  },
1355
1435
  };
1356
1436
  ```
1357
1437
 
1358
- Behavior:
1359
- - Base config is loaded first.
1360
- - `environments.default` is applied (if present).
1361
- - `environments[env]` is applied last.
1362
- - `env` comes from `settings.env` (fallback: `NODE_ENV`, then `development`).
1363
-
1364
- ### Auth (JWT Or OAuth2)
1365
-
1366
- `auth` is independent from `api`.
1367
- You can protect normal web routes, API routes, or both.
1368
- If your app is already listed in `api.apps`, adding `auth` simply means those JSON routes can now require tokens.
1369
-
1370
- Choose the provider based on who is calling your app:
1371
-
1372
- - `provider: 'jwt'`
1373
- Best for first-party apps you control.
1374
- You create your own login/token/refresh/logout routes and call `auth.issue(...)` yourself.
1375
- - `provider: 'oauth2'`
1376
- Best when you need a standard authorization server.
1377
- AegisNode mounts `/oauth/*` endpoints for you and supports `authorization_code` + PKCE, `client_credentials`, and `refresh_token`.
1438
+ Available mail APIs:
1378
1439
 
1379
- Quick decision guide:
1380
- - Use JWT when your own frontend/mobile app talks only to your backend.
1381
- - Use OAuth2 when external clients, partner apps, or machine-to-machine integrations need standard token flows.
1382
- - Use `auth.middleware()` to protect routes in both modes.
1440
+ - Injected `mail` in handlers/services/models/validators/controllers/subscribers/loaders
1441
+ Shared runtime mail manager. Use `mail.send(...)` or `mail.sendMail(...)`.
1442
+ - `req.aegis.mail`
1443
+ Request bridge to the same mail manager used in handler context.
1383
1444
 
1384
- Enable auth in `settings.js`:
1445
+ Handler usage:
1385
1446
 
1386
1447
  ```js
1387
- auth: {
1388
- enabled: true,
1389
- provider: 'jwt', // or 'oauth2'
1390
- tablePrefix: 'aegisnode',
1391
- storage: {
1392
- // cache | memory | file | database
1393
- driver: 'cache',
1394
- filePath: 'storage/aegisnode-auth-store.json',
1395
- // Used by database driver for both SQL table and Mongo collection.
1396
- tableName: 'aegisnode_auth_store',
1397
- },
1398
- jwt: {
1399
- secret: 'replace-with-strong-secret',
1400
- algorithm: 'HS256',
1401
- expiresIn: '15m',
1402
- refreshExpiresIn: '7d',
1403
- issuer: 'blog',
1404
- audience: 'blog',
1405
- },
1406
- oauth2: {
1407
- accessTokenTtlSeconds: 3600,
1408
- refreshTokenTtlSeconds: 1209600,
1409
- authorizationCodeTtlSeconds: 600,
1410
- rotateRefreshToken: true,
1411
- requireClientSecret: true,
1412
- requirePkce: true,
1413
- allowPlainPkce: false,
1414
- grants: ['authorization_code', 'refresh_token', 'client_credentials'],
1415
- defaultScopes: [],
1416
- clientAuthMethod: 'client_secret_basic',
1417
- server: {
1418
- enabled: true,
1419
- basePath: '/oauth',
1420
- authorizePath: '/oauth/authorize',
1421
- tokenPath: '/oauth/token',
1422
- introspectionPath: '/oauth/introspect',
1423
- revocationPath: '/oauth/revoke',
1424
- metadataPath: '/.well-known/oauth-authorization-server',
1425
- issuer: '',
1426
- autoApprove: true,
1427
- requireAuthenticatedUser: true,
1428
- requireConsent: false,
1429
- allowHttp: false,
1430
- },
1431
- },
1432
- },
1433
- ```
1434
-
1435
- `tablePrefix` is used for auth storage names when enabled:
1436
- - `${tablePrefix}_users`
1437
- - `${tablePrefix}_jwt_revocations`
1438
- - `${tablePrefix}_oauth_clients`
1439
- - `${tablePrefix}_oauth_authorization_codes`
1440
- - `${tablePrefix}_oauth_access_tokens`
1441
- - `${tablePrefix}_oauth_refresh_tokens`
1442
-
1443
- By default, these names are used as key namespaces.
1444
- - With `storage.driver = 'cache'` or `memory`, they are in-memory/cache key prefixes.
1445
- - With `storage.driver = 'database'`, they are prefixes stored inside `auth.storage.tableName` (used as SQL table name or Mongo collection name).
1446
-
1447
- Restart behavior:
1448
- - `auth.enabled = true`: auth manager is available in context after restart.
1449
- - `auth.enabled = false`: auth manager stays safe; `auth.middleware()` returns `503` instead of crashing boot.
1450
- - `auth.storage.driver = 'file'`: OAuth2 clients/tokens and JWT revocations persist across restarts.
1451
- - `auth.storage.driver = 'database'`: OAuth2 clients/tokens and JWT revocations persist in your configured `database` backend.
1448
+ route.post('/contact', async ({ mail }, req, res, next) => {
1449
+ try {
1450
+ const info = await mail.send({
1451
+ to: 'support@example.com',
1452
+ subject: 'Contact form',
1453
+ text: req.body?.message || '',
1454
+ html: `<p>${req.body?.message || ''}</p>`,
1455
+ });
1452
1456
 
1453
- JWT usage in routes:
1457
+ res.status(202).json({ messageId: info.messageId });
1458
+ } catch (error) {
1459
+ next(error);
1460
+ }
1461
+ });
1462
+ ```
1454
1463
 
1455
- - JWT does not create login routes for you.
1456
- - You define the endpoints that authenticate users and issue tokens.
1457
- - This is usually the simplest choice for a private API used only by your own frontend/mobile app.
1464
+ Service usage:
1458
1465
 
1459
1466
  ```js
1460
- export default {
1461
- register(route) {
1462
- const authGuard = (req, res, next) => req.aegis.auth.middleware()(req, res, next);
1463
-
1464
- route.get('/auth/token', (req, res) => {
1465
- const token = req.aegis.auth.issue({ subject: 'u1', scope: ['read:users'] });
1466
- res.json({ token });
1467
- });
1467
+ class UsersService {
1468
+ constructor({ mail }) {
1469
+ this.mail = mail;
1470
+ }
1468
1471
 
1469
- route.get('/auth/me', authGuard, (req, res) => {
1470
- res.json({ user: req.auth });
1472
+ async sendWelcome(user) {
1473
+ return this.mail.send({
1474
+ to: user.email,
1475
+ subject: 'Welcome',
1476
+ html: `<h1>Hello ${user.name}</h1><p>Your account is ready.</p>`,
1471
1477
  });
1472
- },
1473
- };
1478
+ }
1479
+ }
1474
1480
  ```
1475
1481
 
1476
- OAuth2 built-in authorization server endpoints (when `auth.provider='oauth2'` and `auth.oauth2.server.enabled=true`):
1477
-
1478
- - `GET /oauth/authorize`
1479
- - `POST /oauth/authorize`
1480
- - `POST /oauth/token`
1481
- - `POST /oauth/introspect`
1482
- - `POST /oauth/revoke`
1483
- - `GET /.well-known/oauth-authorization-server`
1484
-
1485
- Flows supported:
1486
- - `authorization_code` (with PKCE)
1487
- - `client_credentials`
1488
- - `refresh_token`
1482
+ Notes:
1483
+ - `mail.send(...)` and `mail.sendMail(...)` are the same method.
1484
+ - Messages must include at least one of `to`, `cc`, or `bcc`.
1485
+ - Messages must include `from`, or configure `mail.defaults.from`.
1486
+ - For tests or custom providers, you can set `mail.transporter` or `mail.transportFactory` instead of `mail.transport`.
1489
1487
 
1490
- OAuth2 is the better choice when:
1491
- - you need standards-based client registration and token exchange,
1492
- - you need machine clients as well as browser/mobile clients,
1493
- - or third parties must integrate without depending on your custom JWT login route shape.
1488
+ ### Helpers And jlive
1494
1489
 
1495
- #### Route Usage (JWT vs OAuth2)
1490
+ Helpers and the `jlive` bridge are available in request context (`req.aegis`) and in EJS locals.
1491
+ They are also available in:
1492
+ - Service constructors (`constructor({ helpers, jlive, env, i18n, models, ... })`)
1493
+ - Model constructors (`constructor({ helpers, jlive, env, i18n, dbClient, ... })`)
1494
+ - Subscribers context (`registerSubscribers({ helpers, jlive, env, i18n, events, ... })`)
1495
+ - Any view/handler via request bridge: `req.aegis.helpers`, `req.aegis.jlive`, `req.aegis.env`, `req.aegis.locale`, `req.aegis.t`, `req.aegis.i18n`
1496
1496
 
1497
- `startproject` gives you one root route file: `routes.js`.
1498
- All your custom HTTP routes are defined there (or in app routes you mount with `route.use(...)`).
1497
+ Set helper defaults in `settings.js` (currency/locale):
1499
1498
 
1500
1499
  ```js
1501
- // routes.js
1502
- import users from './apps/users/routes.js';
1503
-
1504
- export default {
1505
- register(route) {
1506
- route.use('/users', users);
1507
-
1508
- // Your custom auth/business routes
1509
- route.post('/auth/login', (req, res) => {
1510
- const token = req.aegis.auth.issue({ subject: 'u1' });
1511
- res.json({ token });
1512
- });
1500
+ helpers: {
1501
+ locale: 'fr-FR',
1502
+ money: {
1503
+ currency: 'EUR',
1504
+ currencyDisplay: 'code',
1513
1505
  },
1514
- };
1506
+ },
1515
1507
  ```
1516
1508
 
1517
- How this behaves:
1518
- - `provider: 'jwt'`: Aegis does not create JWT endpoints automatically. You define login/token/refresh/logout routes yourself in `routes.js` (or mounted app routes).
1519
- - `provider: 'oauth2'`: Aegis auto-mounts OAuth2 server endpoints (`/oauth/authorize`, `/oauth/token`, `/oauth/introspect`, `/oauth/revoke`, metadata). You only define your own extra routes (for example admin client setup, protected APIs, business routes).
1520
- - Do not reuse built-in OAuth2 endpoint paths for your own handlers when OAuth2 server is enabled.
1521
-
1522
- Typical setup patterns:
1523
- - API + JWT:
1524
- `api.apps = ['users']`, `auth.provider = 'jwt'`, custom `/auth/login`, protect `/users/*` with `req.aegis.auth.middleware()`.
1525
- - API + OAuth2:
1526
- `api.apps = ['users']`, `auth.provider = 'oauth2'`, use built-in `/oauth/token`, protect `/users/*` with `req.aegis.auth.middleware()`.
1527
- - Web app + JWT:
1528
- no `api` block required if routes are normal form/web routes, but you can still use JWT for selected endpoints.
1529
-
1530
- #### OAuth2 Full Usage
1531
-
1532
- 1. Register clients (server-side only)
1509
+ Then `helpers.money(2500)` automatically uses your configured defaults unless you pass per-call overrides.
1533
1510
 
1534
- Register clients programmatically with `auth.registerClient(...)`.
1535
- Do not expose this publicly in production without admin protection.
1511
+ Route usage:
1536
1512
 
1537
1513
  ```js
1538
1514
  export default {
1539
1515
  register(route) {
1540
- route.post('/admin/oauth/setup-clients', (req, res) => {
1541
- const webClient = req.aegis.auth.registerClient({
1542
- clientId: 'web',
1543
- clientSecret: 'secret',
1544
- redirectUris: ['https://client.example.com/callback'],
1545
- grants: ['authorization_code', 'refresh_token'],
1546
- scopes: ['read:users'],
1547
- });
1548
-
1549
- const machineClient = req.aegis.auth.registerClient({
1550
- clientId: 'machine',
1551
- clientSecret: 'machine-secret',
1552
- grants: ['client_credentials'],
1553
- scopes: ['read:users'],
1516
+ route.get('/tools', (req, res) => {
1517
+ res.json({
1518
+ price: req.aegis.helpers.money(1299.5, { currency: 'USD' }),
1519
+ createdAgo: req.aegis.helpers.timeElapsed(Date.now() - 60_000),
1520
+ createdAgoShort: req.aegis.helpers.timeElapsed(Math.floor(Date.now() / 1000) - 60, true),
1521
+ progress: req.aegis.helpers.timeDifference(65, 0, 100),
1522
+ summary: req.aegis.helpers.breakStr('AegisNode framework helper utilities', 18, '...', true),
1523
+ objectIdValid: req.aegis.helpers.isObjectId('507f1f77bcf86cd799439011'),
1524
+ objectIdString: req.aegis.helpers.toObjectId('507f1f77bcf86cd799439011')?.toString() || null,
1525
+ secret: req.aegis.jlive.generate(32),
1526
+ jliveAvailable: req.aegis.jlive.available,
1554
1527
  });
1555
-
1556
- res.json({ webClient, machineClient });
1557
1528
  });
1558
1529
  },
1559
1530
  };
1560
1531
  ```
1561
1532
 
1562
- Notes:
1563
- - Secret is stored hashed (scrypt).
1564
- - Returned client object does not include the secret/hash.
1565
- - `authorization_code` clients must have at least one `redirectUri`.
1533
+ Template usage (`res.render(...)`):
1566
1534
 
1567
- 2. Authorization Code + PKCE flow
1535
+ ```ejs
1536
+ <h1><%= title %></h1>
1537
+ <p>Total: <%= money(1299.5, { currency: 'USD' }) %></p>
1538
+ <p>Updated: <%= timeElapsed(updatedAt) %></p>
1539
+ <p>Progress: <%= timeDifference(65, 0, 100) %>%</p>
1540
+ <p>Summary: <%= breakStr("AegisNode framework helper utilities", 18, "...", true) %></p>
1541
+ <p>With helper object: <%= helpers.number(1000000) %></p>
1542
+ ```
1568
1543
 
1569
- Create PKCE verifier/challenge:
1544
+ Available EJS locals:
1545
+ - `helpers`
1546
+ - `jlive`
1547
+ - `money`
1548
+ - `number`
1549
+ - `dateTime`
1550
+ - `timeElapsed`
1551
+ - `timeDifference`
1552
+ - `breakStr`
1553
+ - `isObjectId`
1554
+ - `toObjectId`
1555
+ - `locale`
1556
+ - `t`
1557
+ - `csrfToken` (raw hidden input HTML)
1558
+ - `csrfValue` (token string)
1570
1559
 
1571
- ```js
1572
- import crypto from 'crypto';
1560
+ `timeElapsed` supports both styles:
1561
+ - `timeElapsed(value, { now, locale, numeric })` (Intl relative style)
1562
+ - `timeElapsed(unixTime, true)` (short legacy-style mode)
1573
1563
 
1574
- function b64url(buffer) {
1575
- return Buffer.from(buffer).toString('base64')
1576
- .replace(/\+/g, '-')
1577
- .replace(/\//g, '_')
1578
- .replace(/=+$/g, '');
1579
- }
1564
+ Mongo id helpers:
1565
+ - `isObjectId(value)` validates Mongo ObjectId format.
1566
+ - `toObjectId(value)` returns a Mongo ObjectId instance or `null` when invalid.
1580
1567
 
1581
- const codeVerifier = b64url(crypto.randomBytes(48));
1582
- const codeChallenge = b64url(crypto.createHash('sha256').update(codeVerifier).digest());
1583
- ```
1568
+ `jlive` behavior:
1569
+ - If `jlive` package is installed, bridge uses its methods.
1570
+ - If not installed, `jlive.generate()` still works (crypto fallback), while crypto methods throw `JLIVE_UNAVAILABLE`.
1584
1571
 
1585
- Redirect user-agent to authorize endpoint:
1586
-
1587
- ```txt
1588
- GET /oauth/authorize
1589
- ?response_type=code
1590
- &client_id=web
1591
- &redirect_uri=https%3A%2F%2Fclient.example.com%2Fcallback
1592
- &scope=read%3Ausers
1593
- &state=abc123
1594
- &code_challenge=<CODE_CHALLENGE>
1595
- &code_challenge_method=S256
1596
- ```
1597
-
1598
- The server redirects back to:
1599
-
1600
- ```txt
1601
- https://client.example.com/callback?code=<AUTH_CODE>&state=abc123
1602
- ```
1603
-
1604
- Exchange code for tokens:
1605
-
1606
- ```bash
1607
- curl -X POST http://127.0.0.1:3000/oauth/token \
1608
- -H "Content-Type: application/x-www-form-urlencoded" \
1609
- -u web:secret \
1610
- -d "grant_type=authorization_code" \
1611
- -d "code=<AUTH_CODE>" \
1612
- -d "redirect_uri=https://client.example.com/callback" \
1613
- -d "code_verifier=<CODE_VERIFIER>"
1614
- ```
1572
+ ### Template Locals From Settings
1615
1573
 
1616
- Response shape:
1574
+ You can inject custom functions/classes into all template renders from `settings.js`:
1617
1575
 
1618
- ```json
1619
- {
1620
- "access_token": "...",
1621
- "token_type": "Bearer",
1622
- "expires_in": 3600,
1623
- "scope": "read:users",
1624
- "refresh_token": "...",
1625
- "refresh_expires_in": 1209600
1626
- }
1576
+ ```js
1577
+ templates: {
1578
+ enabled: true,
1579
+ engine: 'ejs',
1580
+ dir: 'templates',
1581
+ base: 'base',
1582
+ locals: {
1583
+ formatCurrency: (value) => '$' + Number(value || 0).toFixed(2),
1584
+ ViewBag: class ViewBag {
1585
+ constructor(title) {
1586
+ this.title = title;
1587
+ }
1588
+ },
1589
+ },
1590
+ },
1627
1591
  ```
1628
1592
 
1629
- 3. Protect API routes with OAuth2 access tokens
1593
+ You can also pass already-defined class/function references by name (object shorthand):
1630
1594
 
1631
1595
  ```js
1596
+ import { formatCurrency, ViewBag } from './app/template-locals.js';
1597
+
1632
1598
  export default {
1633
- register(route) {
1634
- const authGuard = (req, res, next) => req.aegis.auth.middleware()(req, res, next);
1635
- route.get('/users/me', authGuard, (req, res) => {
1636
- res.json({
1637
- sub: req.auth.sub || null,
1638
- clientId: req.auth.clientId,
1639
- scope: req.auth.scope,
1640
- });
1641
- });
1599
+ templates: {
1600
+ locals: { formatCurrency, ViewBag },
1642
1601
  },
1643
1602
  };
1644
1603
  ```
1645
1604
 
1646
- Use token:
1647
-
1648
- ```bash
1649
- curl http://127.0.0.1:3000/users/me \
1650
- -H "Authorization: Bearer <ACCESS_TOKEN>"
1651
- ```
1605
+ Note:
1606
+ - Use JS references/imports (as above).
1607
+ - String names like `{ formatCurrency: 'formatCurrency' }` are not auto-resolved.
1652
1608
 
1653
- 4. Refresh token flow
1609
+ Then in EJS:
1654
1610
 
1655
- ```bash
1656
- curl -X POST http://127.0.0.1:3000/oauth/token \
1657
- -H "Content-Type: application/x-www-form-urlencoded" \
1658
- -u web:secret \
1659
- -d "grant_type=refresh_token" \
1660
- -d "refresh_token=<REFRESH_TOKEN>"
1611
+ ```ejs
1612
+ <p><%= formatCurrency(123.45) %></p>
1613
+ <p><%= new ViewBag('Dashboard').title %></p>
1661
1614
  ```
1662
1615
 
1663
- 5. Client Credentials flow (machine-to-machine)
1664
-
1665
- ```bash
1666
- curl -X POST http://127.0.0.1:3000/oauth/token \
1667
- -H "Content-Type: application/x-www-form-urlencoded" \
1668
- -u machine:machine-secret \
1669
- -d "grant_type=client_credentials" \
1670
- -d "scope=read:users"
1671
- ```
1616
+ <!-- SETTINGS_REFERENCE_START -->
1617
+ ## Full Settings Reference
1672
1618
 
1673
- This returns access token only (no refresh token).
1619
+ All fields below are supported in `settings.js`. If you omit a field, AegisNode uses the runtime default.
1674
1620
 
1675
- 6. Introspection and revocation
1621
+ Merge order used at startup:
1622
+ 1. Framework defaults (`defaultConfig`)
1623
+ 2. `settings.js`
1624
+ 3. Legacy `settings/index.js` (if present)
1625
+ 4. Legacy `settings/db.js` (merged into `database`)
1626
+ 5. Legacy `settings/cache.js` (merged into `cache`)
1627
+ 6. Legacy `settings/apps.js` (used only when `settings.js` does not define apps)
1628
+ 7. `environments.default`
1629
+ 8. `environments[env]` where `env = settings.env` (fallback `NODE_ENV`, then `development`)
1676
1630
 
1677
- Introspection:
1631
+ ### Top-Level
1678
1632
 
1679
- ```bash
1680
- curl -X POST http://127.0.0.1:3000/oauth/introspect \
1681
- -H "Content-Type: application/x-www-form-urlencoded" \
1682
- -u web:secret \
1683
- -d "token=<ACCESS_TOKEN>"
1684
- ```
1633
+ | Key | Type / Default | Description |
1634
+ | --- | --- | --- |
1635
+ | `appName` | `string` / folder name | Application name used in logs and defaults. |
1636
+ | `env` | `string` / `process.env.NODE_ENV || 'development'` | Active environment key for `environments` overrides. |
1637
+ | `host` | `string` / `process.env.HOST || '0.0.0.0'` | Bind host for HTTP server. |
1638
+ | `port` | `number` / `process.env.PORT || 3000` | Bind port for HTTP server. |
1639
+ | `trustProxy` | `boolean \| number \| string` / `false` | Express `trust proxy` value. Set this when HTTPS is terminated by a reverse proxy/load balancer. |
1640
+ | `https` | `object \| false` / see HTTPS table | Direct TLS server settings for Node-hosted HTTPS. |
1641
+ | `staticDir` | `string \| null` / `null` | Static assets directory, relative to project root (if set). |
1642
+ | `templates` | `object \| false` / see templates table | EJS template engine + layout settings. |
1643
+ | `i18n` | `object` / see i18n table | Built-in locale detection + translator bridge (`req.aegis.t`, injected `i18n.t`). |
1644
+ | `helpers` | `object` / see helpers table | Runtime helper defaults (for example currency/locale for `helpers.money`). |
1645
+ | `security` | `object` / see security tables | Security headers, DDoS limiter, CSRF settings, app secret. |
1646
+ | `logging` | `object` / `{ level: 'info' }` | Runtime logger level. |
1647
+ | `database` | `object` / see database table | SQL or MongoDB connection settings. |
1648
+ | `cache` | `object` / `{ enabled: true, driver: 'memory' }` | Cache backend settings. |
1649
+ | `websocket` | `object` / `{ enabled: true, cors: { origin: false } }` | Socket.IO server options. |
1650
+ | `uploads` | `object` / see uploads table | Built-in file upload middleware settings used by `route.upload`. |
1651
+ | `mail` | `object` / see mail table | Nodemailer-backed mail manager available as injected `mail` and `req.aegis.mail`. |
1652
+ | `api` | `object` / see API table | API-app middleware behavior (JSON enforcement, no-store, CSRF skip for API mounts). |
1653
+ | `auth` | `object` / see auth tables | JWT or OAuth2 provider settings. |
1654
+ | `swagger` | `object` / see swagger table | OpenAPI JSON + Swagger UI settings. |
1655
+ | `architecture` | `object` / `{ strictLayers: false }` | Layering enforcement mode. |
1656
+ | `autoMountApps` | `boolean` / `false` | Auto-mount each app route file from `settings.apps`. |
1657
+ | `loaders` | `array` / `[]` | Startup loaders run before routes mounting. |
1658
+ | `apps` | `array` / `[]` | Declared apps with mount points. |
1659
+ | `environments` | `object` / `{}` | Environment-specific deep overrides. |
1685
1660
 
1686
- Revocation:
1661
+ Notes:
1662
+ - `rootDir` is internal and set by runtime; do not manage it manually.
1663
+ - Arrays are replaced (not merged) during deep merge.
1687
1664
 
1688
- ```bash
1689
- curl -X POST http://127.0.0.1:3000/oauth/revoke \
1690
- -H "Content-Type: application/x-www-form-urlencoded" \
1691
- -u web:secret \
1692
- -d "token=<ACCESS_OR_REFRESH_TOKEN>"
1693
- ```
1665
+ ### HTTPS (`https`)
1694
1666
 
1695
- 7. Custom subject/consent resolution
1667
+ Use this only when Node should serve HTTPS directly. If HTTPS is handled by Passenger, Nginx, Apache, or another proxy, keep `https.enabled` off and set top-level `trustProxy` instead.
1696
1668
 
1697
- By default, `/oauth/authorize` resolves subject from:
1698
- - `req.user.id`
1699
- - `req.user.sub`
1700
- - `req.auth.sub`
1701
- - `subject`/`user_id` query/body params
1669
+ | Key | Type / Default | Description |
1670
+ | --- | --- | --- |
1671
+ | `enabled` | `boolean` / `false` | Create an HTTPS server instead of HTTP. |
1672
+ | `key` | `string \| Buffer` / `null` | TLS private key content. |
1673
+ | `cert` | `string \| Buffer` / `null` | TLS certificate content. |
1674
+ | `ca` | `string \| Buffer \| array` / `null` | Optional CA/intermediate certificate content. |
1675
+ | `pfx` | `string \| Buffer` / `null` | PFX/PKCS#12 archive content. Use instead of `key` + `cert`. |
1676
+ | `keyPath` | `string` / `''` | Path to TLS private key, relative to project root or absolute. |
1677
+ | `certPath` | `string` / `''` | Path to TLS certificate, relative to project root or absolute. |
1678
+ | `caPath` | `string \| string[]` / `null` | Optional CA/intermediate certificate path(s). |
1679
+ | `pfxPath` | `string` / `''` | Path to PFX/PKCS#12 archive. |
1680
+ | `passphrase` | `string` / `''` | Optional passphrase for encrypted key/PFX files. |
1681
+ | `options` | `object` / `{}` | Extra Node `https.createServer` options (for example `minVersion`). |
1702
1682
 
1703
- You can override with hooks in `settings.js`:
1683
+ Direct HTTPS example:
1704
1684
 
1705
1685
  ```js
1706
- auth: {
1707
- provider: 'oauth2',
1708
- oauth2: {
1709
- server: {
1710
- resolveSubject: ({ req }) => req.user?.id || '',
1711
- resolveConsent: ({ req, client, subject }) => {
1712
- // return true to approve, false to deny
1713
- return true;
1714
- },
1686
+ export default {
1687
+ host: '0.0.0.0',
1688
+ port: 3443,
1689
+ https: {
1690
+ enabled: true,
1691
+ keyPath: 'certs/localhost-key.pem',
1692
+ certPath: 'certs/localhost-cert.pem',
1693
+ options: {
1694
+ minVersion: 'TLSv1.2',
1715
1695
  },
1716
1696
  },
1717
- },
1697
+ };
1718
1698
  ```
1719
1699
 
1720
- 8. Production checklist
1721
-
1722
- - Keep `auth.oauth2.server.allowHttp = false` (default).
1723
- - Use HTTPS with trusted reverse proxy config.
1724
- - Keep `requirePkce = true`.
1725
- - Use strong client secrets and rotate regularly.
1726
- - Restrict client setup endpoints to admins only.
1727
- - Set explicit `defaultScopes` and per-client scopes.
1728
- - Set `issuer` to your public auth server URL.
1729
-
1730
- Implementation notes:
1731
- - OAuth2 server in AegisNode is framework-native (custom implementation).
1732
- - CSRF checks are skipped for OAuth2 server endpoints (`/oauth/*` + metadata) by design.
1733
- - This is OAuth2 (not OpenID Connect); no `id_token` endpoint/flow.
1734
-
1735
- ### Swagger (OpenAPI UI)
1736
-
1737
- Enable Swagger in `settings.js`:
1700
+ Reverse-proxy HTTPS example:
1738
1701
 
1739
1702
  ```js
1740
- swagger: {
1741
- enabled: true,
1742
- docsPath: '/docs',
1743
- jsonPath: '/openapi.json',
1744
- documentPath: 'openapi.json',
1745
- explorer: true,
1746
- },
1703
+ export default {
1704
+ host: '127.0.0.1',
1705
+ port: 3000,
1706
+ trustProxy: 1,
1707
+ };
1747
1708
  ```
1748
1709
 
1749
- Behavior:
1750
- - UI available at `docsPath` (default `/docs`).
1751
- - OpenAPI JSON available at `jsonPath` (default `/openapi.json`).
1752
- - If `openapi.json` exists in project root, it is loaded.
1753
- - If no file is found, AegisNode serves a default minimal OpenAPI document.
1710
+ Notes:
1711
+ - `https` requires either `pfx`/`pfxPath` or both `key`/`keyPath` and `cert`/`certPath`.
1712
+ - Paths resolve from project root unless absolute.
1713
+ - `trustProxy` affects `req.secure`, `req.protocol`, secure cookies, and OAuth2 secure transport checks.
1714
+ - Prefer `1`, a subnet, or another exact Express `trust proxy` value instead of `true` when rate limiting is enabled.
1754
1715
 
1755
- ### Templates (EJS + base.ejs)
1716
+ ### Templates (`templates`)
1756
1717
 
1757
- Set template config in `settings.js`:
1718
+ | Key | Type / Default | Description |
1719
+ | --- | --- | --- |
1720
+ | `enabled` | `boolean` / `true` | Enable template engine. |
1721
+ | `engine` | `string` / `'ejs'` | Template engine. Only `ejs` is supported. |
1722
+ | `dir` | `string` / `'templates'` | Templates folder (absolute or relative to project root). |
1723
+ | `base` | `string \| false \| null` / `'base'` | Default layout template (without `.ejs`). Set `false`/`null` to disable layout wrapping globally. |
1724
+ | `appBases` | `object` / `{}` | Per-app layout override map: `{ appName: 'layout/name' }`. Set app value to `false`/`null` to disable layout for that app only. |
1725
+ | `locals` | `object \| function` / `{}` | Global locals. If function, signature is `({ req, res, helpers, jlive, env }) => object`. |
1758
1726
 
1759
- ```js
1760
- templates: {
1761
- enabled: true,
1762
- engine: 'ejs',
1763
- dir: 'templates',
1764
- base: 'base',
1765
- appBases: {
1766
- users: 'users/base',
1767
- admin: 'admin/base',
1768
- },
1769
- }
1770
- ```
1727
+ Layout notes:
1728
+ - `res.render('view', data)` renders `view.ejs` and wraps it with `base.ejs` (or configured layout).
1729
+ - Per-app layout override: `templates.appBases = { users: 'users/base', admin: 'admin/base' }`.
1730
+ - In layout, both `<%- body %>` and `<%- content %>` are available.
1731
+ - Per-render layout override: pass `layout: 'custom-layout'` or `layout: false` in locals.
1771
1732
 
1772
- Then in a route handler:
1733
+ ### Internationalization (`i18n`)
1773
1734
 
1774
- ```js
1775
- route.get('/', (req, res) => {
1776
- res.render('home', {
1777
- title: 'Home',
1778
- message: 'Welcome',
1779
- });
1780
- });
1781
- ```
1735
+ | Key | Type / Default | Description |
1736
+ | --- | --- | --- |
1737
+ | `enabled` | `boolean` / `false` | Enable built-in i18n translator bridge. |
1738
+ | `defaultLocale` | `string` / `'en'` | Default locale used when detection fails. |
1739
+ | `fallbackLocale` | `string` / `'en'` | Fallback locale used when key is missing in active locale. |
1740
+ | `supported` | `string[]` / `['en']` | Allowed locales. Values normalize to lowercase (for example `en-US` -> `en-us`). |
1741
+ | `queryParam` | `string` / `'lang'` | Query parameter used for locale selection (for example `?lang=fr`). |
1742
+ | `cookieName` | `string` / `'aegis_locale'` | Cookie used to persist locale. |
1743
+ | `detectFromHeader` | `boolean` / `true` | Enable locale detection from `Accept-Language` header. |
1744
+ | `detectFromCookie` | `boolean` / `true` | Enable locale detection from configured cookie. |
1745
+ | `detectFromQuery` | `boolean` / `true` | Enable locale detection from query parameter. |
1746
+ | `translations` | `object` / `{}` | Translation map by locale. Values can be objects or JSON file paths: `{ en: { ... }, fr: 'locales/fr.json' }`. Alias keys `locales` and `messages` are also accepted. |
1747
+ | `translationsFile` | `string` / unset | Path to a JSON file containing all locales (example: `{ "en": {...}, "fr": {...} }`). Inline `translations` overrides file values for same locale keys. |
1782
1748
 
1783
- `home.ejs` is rendered first, then injected into `base.ejs`.
1784
- Use `<%- content %>` (or `<%- body %>`) in your `base.ejs` to print page content.
1749
+ i18n notes:
1750
+ - Detection order: query -> cookie -> `Accept-Language` -> `defaultLocale`.
1751
+ - Use dotted keys like `home.title`.
1752
+ - Placeholder interpolation supports `{name}` style tokens.
1753
+ - Relative JSON paths resolve from project root (`settings.js` location).
1754
+ - Injected `i18n` is available in handlers, services, models, validators, controllers, subscribers, and loaders. Use `i18n.t('key', vars, { locale })`.
1755
+ - During a request, injected `i18n.t(...)` follows the active request locale. Outside a request, it falls back to `defaultLocale`.
1785
1756
 
1786
- ### Internationalization (i18n)
1757
+ ### Helpers Defaults (`helpers`)
1787
1758
 
1788
- Configure i18n in `settings.js`:
1759
+ | Key | Type / Default | Description |
1760
+ | --- | --- | --- |
1761
+ | `locale` | `string` / `'en-US'` | Default locale used by runtime helpers when locale is not passed explicitly. |
1762
+ | `money` | `object` / `{ currency: 'USD' }` | Default money formatting settings used by `helpers.money`. |
1763
+ | `money.currency` | `string` / `'USD'` | Default currency code for `helpers.money(amount)` when no currency option is provided. |
1764
+ | `money.locale` | `string` / `helpers.locale` | Locale override only for `helpers.money`. |
1765
+ | `money.currencyDisplay` | `'symbol' | 'code' | 'name' | 'narrowSymbol'` / `'symbol'` | Currency display style passed to `Intl.NumberFormat`. |
1766
+ | `money.minimumFractionDigits` | `number` / unset | Optional minimum fraction digits for money formatting. |
1767
+ | `money.maximumFractionDigits` | `number` / unset | Optional maximum fraction digits for money formatting. |
1768
+
1769
+ Helpers defaults notes:
1770
+ - Per-call options always override these defaults.
1771
+ - If `helpers.locale` is not set, AegisNode falls back to `i18n.defaultLocale` for helper locale.
1772
+ - Legacy shorthand keys are also accepted for compatibility: `helpers.currency`, top-level `currency`, and `app.currency`.
1773
+
1774
+ ### Security (`security`)
1775
+
1776
+ | Key | Type / Default | Description |
1777
+ | --- | --- | --- |
1778
+ | `appSecret` | `string` / `''` | Shared secret for signing security artifacts. Use at least 16 chars. |
1779
+ | `headers` | `object` / see headers table | Helmet + CSP configuration. |
1780
+ | `ddos` | `object` / see ddos table | `express-rate-limit` based protection. |
1781
+ | `csrf` | `object` / see csrf table | CSRF cookie/token behavior. |
1782
+
1783
+ #### Security Headers (`security.headers`)
1784
+
1785
+ | Key | Type / Default | Description |
1786
+ | --- | --- | --- |
1787
+ | `enabled` | `boolean` / `true` | Enable Helmet middleware. |
1788
+ | `csp` | `object` / see CSP table | Content Security Policy behavior. |
1789
+
1790
+ #### CSP (`security.headers.csp`)
1791
+
1792
+ | Key | Type / Default | Description |
1793
+ | --- | --- | --- |
1794
+ | `enabled` | `boolean` / `true` | Enable CSP header from Helmet. |
1795
+ | `reportOnly` | `boolean` / `false` | Use report-only mode. |
1796
+ | `directives` | `object` / `{}` | Directive overrides. Set a directive to `false`/`null` to remove it. |
1797
+
1798
+ Default CSP base includes safe defaults such as `defaultSrc 'self'`, `objectSrc 'none'`, `frameAncestors 'none'`, and websocket-aware `connectSrc`.
1799
+
1800
+ Allow multiple external domains by adding them per directive (not globally):
1789
1801
 
1790
1802
  ```js
1791
- i18n: {
1792
- enabled: true,
1793
- defaultLocale: 'en',
1794
- fallbackLocale: 'en',
1795
- supported: ['en', 'fr'],
1796
- queryParam: 'lang',
1797
- translations: {
1798
- en: {
1799
- home: {
1800
- title: 'Welcome {name}',
1801
- },
1802
- },
1803
- fr: {
1804
- home: {
1805
- title: 'Bienvenue {name}',
1803
+ security: {
1804
+ headers: {
1805
+ csp: {
1806
+ directives: {
1807
+ scriptSrc: ["'self'", 'https://cdn.jsdelivr.net', 'https://unpkg.com'],
1808
+ scriptSrcElem: ["'self'", 'https://cdn.jsdelivr.net', 'https://unpkg.com'],
1809
+ styleSrc: ["'self'", "'unsafe-inline'", 'https://cdn.jsdelivr.net'],
1810
+ styleSrcElem: ["'self'", 'https://cdn.jsdelivr.net'],
1811
+ imgSrc: ["'self'", 'data:', 'https://cdn.jsdelivr.net'],
1812
+ connectSrc: ["'self'", 'https://api.example.com', 'wss://socket.example.com'],
1806
1813
  },
1807
1814
  },
1808
1815
  },
1809
- }
1816
+ },
1810
1817
  ```
1811
1818
 
1812
- You can also load locale JSON files directly (no import needed):
1819
+ Notes:
1820
+ - Add each origin to the exact directive needed (scripts, styles, images, API/WebSocket connections).
1821
+ - If browser reports a `script-src-elem` violation, whitelist the domain in `scriptSrcElem`.
1822
+ - Prefer explicit origins instead of `*` in production.
1823
+
1824
+ Google Fonts example:
1813
1825
 
1814
1826
  ```js
1815
- i18n: {
1816
- enabled: true,
1817
- defaultLocale: 'en',
1818
- supported: ['en', 'fr'],
1819
- translations: {
1820
- en: 'locales/en.json',
1821
- fr: 'locales/fr.json',
1827
+ security: {
1828
+ headers: {
1829
+ csp: {
1830
+ directives: {
1831
+ styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'],
1832
+ styleSrcElem: ["'self'", 'https://fonts.googleapis.com'],
1833
+ fontSrc: ["'self'", 'data:', 'https://fonts.gstatic.com'],
1834
+ },
1835
+ },
1822
1836
  },
1823
- // optional single-file source (inline `translations` wins per locale key):
1824
- // translationsFile: 'locales/all.json',
1825
- }
1837
+ },
1826
1838
  ```
1827
1839
 
1828
- Route usage:
1840
+ #### DDoS / Rate Limit (`security.ddos`)
1829
1841
 
1830
- ```js
1831
- route.get('/i18n-demo', (req, res) => {
1832
- // Auto-detected from query/cookie/header.
1833
- res.json({
1834
- locale: req.aegis.locale,
1835
- title: req.aegis.t('home.title', { name: 'Jason' }),
1836
- });
1837
- });
1838
- ```
1842
+ | Key | Type / Default | Description |
1843
+ | --- | --- | --- |
1844
+ | `enabled` | `boolean` / `true` | Enable rate limiter. |
1845
+ | `windowMs` | `number` / `60000` | Rate limit window in milliseconds. |
1846
+ | `maxRequests` | `number` / `120` | Max requests per window per key. |
1847
+ | `message` | `string` / `'Too many requests, please try again later.'` | JSON error message text. |
1848
+ | `statusCode` | `number` / `429` | Response status code when limited. |
1849
+ | `standardHeaders` | `boolean` / `true` | Emit modern rate-limit headers. |
1850
+ | `legacyHeaders` | `boolean` / `false` | Emit legacy `X-RateLimit-*` headers. |
1851
+ | `skipSuccessfulRequests` | `boolean` / `false` | Do not count successful responses. |
1852
+ | `skipFailedRequests` | `boolean` / `false` | Do not count failed responses. |
1853
+ | `trustProxy` | `boolean \| number \| string` / `false` | Legacy alias for top-level `trustProxy`. Still supported for backward compatibility. |
1854
+ | `store` | `object \| null` / `null` | Custom rate-limit store implementation. |
1855
+ | `skipPaths` | `string[]` / `['/health']` | Path prefixes excluded from limiter. |
1839
1856
 
1840
- Choosing the API:
1857
+ #### CSRF (`security.csrf`)
1841
1858
 
1842
- - `req.aegis.t('home.title')`
1843
- Shortcut for `req.aegis.i18n.t('home.title')`. Use this in routes/views when you only need a translated string.
1844
- - `req.aegis.i18n`
1845
- Request-scoped i18n object. Use this when you also need locale metadata or helpers such as `locale`, `localeSource`, `setLocale(...)`, `resolveLocale(...)`, or `forLocale(...)`.
1846
- - Injected `i18n` in handlers/services/models/validators/controllers/subscribers/loaders
1847
- Runtime-injected i18n bridge. During an HTTP request, `i18n.t(...)` resolves with the same active locale as `req.aegis.i18n.t(...)`, so the translation result is the same.
1859
+ | Key | Type / Default | Description |
1860
+ | --- | --- | --- |
1861
+ | `enabled` | `boolean` / `true` | Enable CSRF middleware. |
1862
+ | `rejectForms` | `boolean` / `true` | Enforce CSRF on form submissions. |
1863
+ | `rejectUnsafeMethods` | `boolean` / `true` | Enforce CSRF for unsafe methods (`POST/PUT/PATCH/DELETE`) in general. |
1864
+ | `cookieName` | `string` / `'_aegis_csrf'` | CSRF cookie name. |
1865
+ | `fieldName` | `string` / `'_csrf'` | Body form field name for CSRF token. |
1866
+ | `headerName` | `string` / `'x-csrf-token'` | Header token key (normalized lowercase). |
1867
+ | `requireSignedCookie` | `boolean` / `true` | Require signed CSRF cookie values. |
1868
+ | `sameSite` | `'lax' \| 'strict' \| 'none' \| false` / `'lax'` | CSRF cookie same-site mode. |
1869
+ | `secure` | `boolean \| 'auto'` / `'auto'` | Secure cookie flag (`auto` respects request security). |
1870
+ | `httpOnly` | `boolean` / `true` | Make CSRF cookie httpOnly. |
1871
+ | `path` | `string` / `'/'` | CSRF cookie path. |
1848
1872
 
1849
- Important differences:
1873
+ CSRF notes:
1874
+ - With `requireSignedCookie: true` (default), `security.appSecret` must be strong (minimum 16 chars) or startup fails.
1875
+ - CSRF is skipped for configured API app mounts when `api.disableCsrf: true`.
1876
+ - CSRF is skipped on built-in OAuth2 server endpoints.
1850
1877
 
1851
- - `req.aegis.t` and `req.aegis.i18n.t` return the same translation for the current request.
1852
- - Injected `i18n.t(...)` in a service/model/validator/subscriber is not the same object as `req.aegis.i18n`, but during a request it produces the same translation result for the same key/options.
1853
- - Outside a request, injected `i18n.t(...)` falls back to `defaultLocale`.
1854
- - In background jobs, loaders, or boot-time code, pass an explicit locale when needed: `i18n.t('home.title', { name: 'Jason' }, { locale: 'fr' })`.
1878
+ ### Logging (`logging`)
1855
1879
 
1856
- Service/model usage:
1880
+ | Key | Type / Default | Description |
1881
+ | --- | --- | --- |
1882
+ | `level` | `'error' \| 'warn' \| 'info' \| 'debug' \| 'trace'` / `'info'` | Logger verbosity threshold. |
1857
1883
 
1858
- ```js
1859
- class UsersService {
1860
- constructor({ i18n }) {
1861
- this.i18n = i18n;
1862
- }
1884
+ ### Database (`database`)
1863
1885
 
1864
- greeting(name) {
1865
- return this.i18n.t('home.title', { name });
1866
- }
1867
- }
1868
- ```
1886
+ | Key | Type / Default | Description |
1887
+ | --- | --- | --- |
1888
+ | `enabled` | `boolean` / `false` | Enable database bootstrap. |
1889
+ | `dialect` | `string` / `'pg'` | SQL: `mysql`, `pg`/`postgres`/`postgresql`, `sqlite`, `mssql`, `oracle`; NoSQL: `mongo`/`mongodb`/`mongoose`. |
1890
+ | `config` | `object` / `{}` | Connection options passed to database driver. |
1891
+ | `options` | `object` / `{}` | Extra options (used directly for Mongo when enabled). |
1892
+ | `uri` | `string` / unset | Legacy Mongo URI fallback (still accepted). Prefer `config.connectionString`. |
1869
1893
 
1870
- Template usage:
1894
+ Mongo config shortcuts accepted in `database.config`:
1895
+ - `connectionString` (preferred)
1896
+ - or `server`/`host`, `port`, `database`/`dbName`, `user`/`username`, `password`
1871
1897
 
1872
- ```ejs
1873
- <html lang="<%= locale %>">
1874
- <body>
1875
- <h1><%= t('home.title', { name: 'Jason' }) %></h1>
1876
- </body>
1877
- </html>
1878
- ```
1898
+ ### Cache (`cache`)
1879
1899
 
1880
- Manual locale switch inside a request:
1900
+ | Key | Type / Default | Description |
1901
+ | --- | --- | --- |
1902
+ | `enabled` | `boolean` / `true` | Enable cache service. |
1903
+ | `driver` | `string` / `'memory'` | Cache driver. Currently only `memory` is built-in. |
1904
+ | `options` | `object` / `{}` | Reserved for future/custom drivers. |
1881
1905
 
1882
- ```js
1883
- route.get('/fr', (req, res) => {
1884
- req.aegis.setLocale('fr'); // persists in i18n cookie by default
1885
- res.send(req.aegis.t('home.title', { name: 'Jason' }));
1886
- });
1887
- ```
1906
+ ### WebSocket (`websocket`)
1888
1907
 
1889
- Persist user-selected language as default:
1908
+ | Key | Type / Default | Description |
1909
+ | --- | --- | --- |
1910
+ | `enabled` | `boolean` / `true` | Enable Socket.IO server. |
1911
+ | `cors` | `object` / `{ origin: false }` | Passed directly to Socket.IO `cors` option. |
1890
1912
 
1891
- ```js
1892
- route.post('/lang', (req, res) => {
1893
- const selected = String(req.body?.lang || '').trim();
1894
- req.aegis.setLocale(selected); // writes i18n cookie (aegis_locale by default)
1895
- res.redirect('back');
1896
- });
1897
- ```
1913
+ ### Uploads (`uploads`)
1898
1914
 
1899
- Language picker template (keeps selected option):
1915
+ | Key | Type / Default | Description |
1916
+ | --- | --- | --- |
1917
+ | `enabled` | `boolean` / `true` | Enable built-in upload manager. |
1918
+ | `dir` | `string` / `'uploads'` | Upload destination folder (absolute or relative to project root). |
1919
+ | `createDir` | `boolean` / `true` | Create upload directory automatically on boot. |
1920
+ | `preserveExtension` | `boolean` / `true` | Preserve original file extension in generated file name. |
1921
+ | `maxFileSize` | `number \| string` / `5 * 1024 * 1024` | Per-file max size in bytes. String units like `'5mb'` are accepted. |
1922
+ | `maxFiles` | `number` / `5` | Max file count per request. |
1923
+ | `maxFields` | `number` / `50` | Max non-file form fields per request. |
1924
+ | `maxFieldSize` | `number \| string` / `1024 * 1024` | Max size per form field value. String units are accepted. |
1925
+ | `allowedMimeTypes` | `string[] \| string` / `[]` | Allowed MIME types. Empty means allow all. |
1926
+ | `allowedExtensions` | `string[] \| string` / `[]` | Allowed file extensions (`.jpg`, `.pdf`, ...). Empty means allow all. |
1927
+ | `allowApiMultipart` | `boolean` / `true` | Allow `multipart/form-data` on API mounts even when `api.requireJsonForUnsafeMethods` is true. |
1900
1928
 
1901
- ```ejs
1902
- <form method="post" action="/lang">
1903
- <select name="lang">
1904
- <option value="en" <%= locale === 'en' ? 'selected' : '' %>>English</option>
1905
- <option value="fr" <%= locale === 'fr' ? 'selected' : '' %>>Français</option>
1906
- </select>
1907
- <button type="submit">Change</button>
1908
- </form>
1909
- ```
1929
+ ### Mail (`mail`)
1930
+
1931
+ | Key | Type / Default | Description |
1932
+ | --- | --- | --- |
1933
+ | `enabled` | `boolean` / `false` | Enable the built-in mail manager. |
1934
+ | `defaults` | `object` / `{ from: '', replyTo: '' }` | Default message fields merged into every outgoing mail payload. |
1935
+ | `defaults.from` | `string` / `''` | Default sender used when `mail.send(...)` omits `from`. |
1936
+ | `defaults.replyTo` | `string` / `''` | Default `replyTo` header for outgoing mail. |
1937
+ | `transport` | `object \| string` / `{}` | Nodemailer transport config or SMTP connection URL passed to `nodemailer.createTransport(...)`. |
1938
+ | `transporter` | `object \| null` / `null` | Prebuilt transporter object with `sendMail()`. Useful in tests or custom integrations. |
1939
+ | `transportFactory` | `function \| null` / `null` | Factory that returns a transporter object with `sendMail()`. |
1940
+ | `verifyOnStartup` | `boolean` / `false` | Call `transporter.verify()` during boot when the transporter supports it. |
1941
+
1942
+ Mail notes:
1943
+ - The runtime uses [Nodemailer](https://nodemailer.com/).
1944
+ - `mail.send(payload)` and `mail.sendMail(payload)` are aliases.
1945
+ - Each payload must include at least one of `to`, `cc`, or `bcc`.
1946
+ - Each payload must include `from`, or configure `mail.defaults.from`.
1947
+ - Injected `mail` is available in handlers, services, models, validators, controllers, subscribers, and loaders, plus `req.aegis.mail`.
1948
+
1949
+ ### API (`api`)
1950
+
1951
+ | Key | Type / Default | Description |
1952
+ | --- | --- | --- |
1953
+ | `apps` | `string[]` / `[]` | App names treated as API apps. Mounts are resolved from `settings.apps`. |
1954
+ | `disableCsrf` | `boolean` / `true` | Skip CSRF checks for API app mounts. |
1955
+ | `requireJsonForUnsafeMethods` | `boolean` / `true` | Reject unsafe API payloads unless `Content-Type` is JSON (`415`). Multipart is allowed when `uploads.allowApiMultipart=true`. |
1956
+ | `noStoreHeaders` | `boolean` / `true` | Set `Cache-Control: no-store` on API responses. |
1957
+
1958
+ API notes:
1959
+ - `api.apps` contains app names from `settings.apps`, not URL paths.
1960
+ - Marking an app as API does not generate REST routes or change the app file structure. It only applies API middleware to that app mount.
1961
+ - The effective API mount comes from `settings.apps[].mount` (or `/${app}` by default). Keep that aligned with your `route.use(...)` mount when `autoMountApps` is off.
1962
+
1963
+ ### Auth (`auth`)
1964
+
1965
+ | Key | Type / Default | Description |
1966
+ | --- | --- | --- |
1967
+ | `enabled` | `boolean` / `false` | Enable auth manager. |
1968
+ | `provider` | `'jwt' \| 'oauth2'` / `'jwt'` | Active auth provider. |
1969
+ | `tablePrefix` | `string` / `'aegisnode'` | Prefix for auth storage namespaces/tables; sanitized to lowercase `[a-z0-9_]`. |
1970
+ | `storage` | `object` / see storage table | Persistence backend for auth state. |
1971
+ | `jwt` | `object` / see jwt table | JWT configuration. |
1972
+ | `oauth2` | `object` / see oauth2 table | OAuth2 server and token configuration. |
1973
+
1974
+ #### Auth Storage (`auth.storage`)
1975
+
1976
+ | Key | Type / Default | Description |
1977
+ | --- | --- | --- |
1978
+ | `driver` | `'cache' \| 'memory' \| 'file' \| 'database'` / `'cache'` | Storage backend for revocations/clients/tokens. |
1979
+ | `filePath` | `string` / `'storage/aegisnode-auth-store.json'` | Used when `driver: 'file'`. Relative to project root if not absolute. |
1980
+ | `tableName` | `string` / `'aegisnode_auth_store'` (prefix-based) | Used when `driver: 'database'` for SQL table or Mongo collection. |
1981
+ | `collectionName` | `string` / alias only | Legacy alias; if set and `tableName` missing, it becomes `tableName`. |
1982
+
1983
+ #### JWT (`auth.jwt`)
1984
+
1985
+ | Key | Type / Default | Description |
1986
+ | --- | --- | --- |
1987
+ | `secret` | `string` / `security.appSecret` fallback | Signing secret. Required for JWT auth. |
1988
+ | `algorithm` | `'HS256' \| 'HS384' \| 'HS512'` / `'HS256'` | JWT HMAC algorithm. |
1989
+ | `issuer` | `string` / `appName` | JWT issuer claim. |
1990
+ | `audience` | `string` / `appName` | JWT audience claim. |
1991
+ | `expiresIn` | `string` / `'15m'` | Access token TTL. |
1992
+ | `refreshExpiresIn` | `string` / `'7d'` | Refresh token TTL. |
1993
+
1994
+ #### OAuth2 Core (`auth.oauth2`)
1995
+
1996
+ | Key | Type / Default | Description |
1997
+ | --- | --- | --- |
1998
+ | `accessTokenTtlSeconds` | `number` / `3600` | Access token TTL in seconds. |
1999
+ | `refreshTokenTtlSeconds` | `number` / `1209600` | Refresh token TTL in seconds. |
2000
+ | `authorizationCodeTtlSeconds` | `number` / `600` | Authorization code TTL in seconds. |
2001
+ | `rotateRefreshToken` | `boolean` / `true` | Rotate refresh token on refresh flow. |
2002
+ | `requireClientSecret` | `boolean` / `true` | Require secret for confidential client flows. |
2003
+ | `requirePkce` | `boolean` / `true` | Require PKCE for authorization_code flow. |
2004
+ | `allowPlainPkce` | `boolean` / `false` | Allow PKCE `plain` method. |
2005
+ | `grants` | `string[]` / `['authorization_code','refresh_token','client_credentials']` | Enabled OAuth2 grants. |
2006
+ | `defaultScopes` | `string[]` / `[]` | Scopes assigned when none requested/provided. |
2007
+ | `clientAuthMethod` | `'client_secret_basic' \| 'client_secret_post' \| 'none'` / `'client_secret_basic'` | Default client authentication method. |
2008
+ | `server` | `object` / see OAuth2 server table | Built-in authorization server endpoint settings. |
2009
+
2010
+ #### OAuth2 Server (`auth.oauth2.server`)
2011
+
2012
+ | Key | Type / Default | Description |
2013
+ | --- | --- | --- |
2014
+ | `enabled` | `boolean` / `true` | Mount built-in OAuth2 endpoints. |
2015
+ | `basePath` | `string` / `'/oauth'` | Base path used to derive endpoint defaults. |
2016
+ | `authorizePath` | `string` / `'/oauth/authorize'` | Authorization endpoint path. |
2017
+ | `tokenPath` | `string` / `'/oauth/token'` | Token endpoint path. |
2018
+ | `introspectionPath` | `string` / `'/oauth/introspect'` | Introspection endpoint path. |
2019
+ | `revocationPath` | `string` / `'/oauth/revoke'` | Revocation endpoint path. |
2020
+ | `metadataPath` | `string` / `'/.well-known/oauth-authorization-server'` | OAuth2 metadata endpoint path. |
2021
+ | `issuer` | `string` / `server.baseUrl` fallback, then `appName` | Issuer value in OAuth2 metadata/tokens. |
2022
+ | `baseUrl` | `string` / optional alias | Alias used as fallback for `issuer` when `issuer` is empty. |
2023
+ | `autoApprove` | `boolean` / `true` | Auto-approve auth requests once subject is resolved. |
2024
+ | `requireAuthenticatedUser` | `boolean` / `true` | Require authenticated user/subject for authorize endpoint. |
2025
+ | `requireConsent` | `boolean` / `false` | Require explicit consent before issuing auth code. |
2026
+ | `allowSubjectFromParams` | `boolean` / `false` | Allow subject from request params (`subject`/`user_id`). Keep disabled in production. |
2027
+ | `allowHttp` | `boolean` / `false` | Allow OAuth2 endpoints on non-HTTPS requests. Keep `false` in production. |
2028
+ | `resolveSubject` | `function \| null` / `null` | Hook to resolve subject: `({ req, params, client }) => string|null`. |
2029
+ | `resolveConsent` | `function \| null` / `null` | Hook to resolve consent: `({ req, params, client, subject }) => boolean`. |
2030
+
2031
+ ### Swagger (`swagger`)
2032
+
2033
+ | Key | Type / Default | Description |
2034
+ | --- | --- | --- |
2035
+ | `enabled` | `boolean` / `false` | Enable Swagger UI + OpenAPI JSON endpoints. |
2036
+ | `docsPath` | `string` / `'/docs'` | Swagger UI route. |
2037
+ | `jsonPath` | `string` / `'/openapi.json'` | OpenAPI JSON route. |
2038
+ | `document` | `object \| null` / `null` | Inline OpenAPI document object. |
2039
+ | `documentPath` | `string` / `'openapi.json'` | JSON file path used when `document` is not provided. |
2040
+ | `explorer` | `boolean` / `true` | Enable Swagger UI explorer mode. |
2041
+
2042
+ ### Architecture (`architecture`)
1910
2043
 
1911
- Notes:
1912
- - `defaultLocale` is used only when user has no saved locale.
1913
- - After selection, cookie locale becomes the default for that user on next requests.
1914
- - `?lang=fr` also persists automatically when `detectFromQuery` is enabled.
1915
- - Templates get `t`, `locale`, and `i18n` in locals.
2044
+ | Key | Type / Default | Description |
2045
+ | --- | --- | --- |
2046
+ | `strictLayers` | `boolean` / `false` | Enforce `route -> validator -> service -> model` restrictions. |
1916
2047
 
1917
- ### Mail
2048
+ ### Auto Mount (`autoMountApps`)
1918
2049
 
1919
- Configure mail transport in `settings.js`:
2050
+ | Key | Type / Default | Description |
2051
+ | --- | --- | --- |
2052
+ | `autoMountApps` | `boolean` / `false` | If `true`, each `apps/<name>/routes.js` is mounted automatically from `settings.apps`. If `false`, central `routes.js` controls mounting. |
2053
+
2054
+ ### Loaders (`loaders`)
2055
+
2056
+ `loaders` accepts an array of entries run in order during startup:
2057
+
2058
+ 1. Function entry:
1920
2059
 
1921
2060
  ```js
1922
- export default {
1923
- mail: {
1924
- enabled: true,
1925
- defaults: {
1926
- from: 'noreply@example.com',
1927
- replyTo: 'support@example.com',
1928
- },
1929
- transport: {
1930
- host: process.env.SMTP_HOST,
1931
- port: process.env.SMTP_PORT ? Number(process.env.SMTP_PORT) : 587,
1932
- secure: false,
1933
- auth: {
1934
- user: process.env.SMTP_USER,
1935
- pass: process.env.SMTP_PASS,
1936
- },
1937
- },
1938
- verifyOnStartup: true,
2061
+ loaders: [
2062
+ async ({ logger, options, config }) => {
2063
+ logger.info('loader ran');
1939
2064
  },
1940
- };
2065
+ ]
1941
2066
  ```
1942
2067
 
1943
- Available mail APIs:
1944
-
1945
- - Injected `mail` in handlers/services/models/validators/controllers/subscribers/loaders
1946
- Shared runtime mail manager. Use `mail.send(...)` or `mail.sendMail(...)`.
1947
- - `req.aegis.mail`
1948
- Request bridge to the same mail manager used in handler context.
1949
-
1950
- Handler usage:
2068
+ 2. Path entry (relative or absolute):
1951
2069
 
1952
2070
  ```js
1953
- route.post('/contact', async ({ mail }, req, res, next) => {
1954
- try {
1955
- const info = await mail.send({
1956
- to: 'support@example.com',
1957
- subject: 'Contact form',
1958
- text: req.body?.message || '',
1959
- html: `<p>${req.body?.message || ''}</p>`,
1960
- });
1961
-
1962
- res.status(202).json({ messageId: info.messageId });
1963
- } catch (error) {
1964
- next(error);
1965
- }
1966
- });
2071
+ loaders: ['loaders/init-db.js']
1967
2072
  ```
1968
2073
 
1969
- Service usage:
2074
+ 3. Object entry with options:
1970
2075
 
1971
2076
  ```js
1972
- class UsersService {
1973
- constructor({ mail }) {
1974
- this.mail = mail;
1975
- }
1976
-
1977
- async sendWelcome(user) {
1978
- return this.mail.send({
1979
- to: user.email,
1980
- subject: 'Welcome',
1981
- html: `<h1>Hello ${user.name}</h1><p>Your account is ready.</p>`,
1982
- });
1983
- }
1984
- }
2077
+ loaders: [
2078
+ { path: 'loaders/init-queue.js', options: { queue: 'jobs' } },
2079
+ ]
1985
2080
  ```
1986
2081
 
1987
- Notes:
1988
- - `mail.send(...)` and `mail.sendMail(...)` are the same method.
1989
- - Messages must include at least one of `to`, `cc`, or `bcc`.
1990
- - Messages must include `from`, or configure `mail.defaults.from`.
1991
- - For tests or custom providers, you can set `mail.transporter` or `mail.transportFactory` instead of `mail.transport`.
2082
+ Each loader module must export a function (default export preferred).
2083
+ Each loader receives the shared runtime context documented in the injection matrix above, plus `options` from the loader entry.
1992
2084
 
1993
- ### Helpers And jlive
2085
+ ### Apps (`apps`)
1994
2086
 
1995
- Helpers and the `jlive` bridge are available in request context (`req.aegis`) and in EJS locals.
1996
- They are also available in:
1997
- - Service constructors (`constructor({ helpers, jlive, env, i18n, models, ... })`)
1998
- - Model constructors (`constructor({ helpers, jlive, env, i18n, dbClient, ... })`)
1999
- - Subscribers context (`registerSubscribers({ helpers, jlive, env, i18n, events, ... })`)
2000
- - Any view/handler via request bridge: `req.aegis.helpers`, `req.aegis.jlive`, `req.aegis.env`, `req.aegis.locale`, `req.aegis.t`, `req.aegis.i18n`
2087
+ `apps` supports two forms:
2001
2088
 
2002
- Set helper defaults in `settings.js` (currency/locale):
2089
+ ```js
2090
+ apps: ['users', 'billing']
2091
+ ```
2092
+
2093
+ or
2003
2094
 
2004
2095
  ```js
2005
- helpers: {
2006
- locale: 'fr-FR',
2007
- money: {
2008
- currency: 'EUR',
2009
- currencyDisplay: 'code',
2010
- },
2011
- },
2096
+ apps: [
2097
+ { name: 'users', mount: '/users' },
2098
+ { name: 'billing', mount: '/payments' },
2099
+ ]
2012
2100
  ```
2013
2101
 
2014
- Then `helpers.money(2500)` automatically uses your configured defaults unless you pass per-call overrides.
2102
+ Rules:
2103
+ - `name` is required in object form.
2104
+ - `mount` defaults to `/${name}`.
2105
+ - Mounts are normalized to a single leading slash (except root `/`).
2106
+ - App route modules must be declared in `settings.apps` before they can load/mount.
2015
2107
 
2016
- Route usage:
2108
+ ### Environment Overrides (`environments`)
2109
+
2110
+ Example:
2017
2111
 
2018
2112
  ```js
2019
- export default {
2020
- register(route) {
2021
- route.get('/tools', (req, res) => {
2022
- res.json({
2023
- price: req.aegis.helpers.money(1299.5, { currency: 'USD' }),
2024
- createdAgo: req.aegis.helpers.timeElapsed(Date.now() - 60_000),
2025
- createdAgoShort: req.aegis.helpers.timeElapsed(Math.floor(Date.now() / 1000) - 60, true),
2026
- progress: req.aegis.helpers.timeDifference(65, 0, 100),
2027
- summary: req.aegis.helpers.breakStr('AegisNode framework helper utilities', 18, '...', true),
2028
- objectIdValid: req.aegis.helpers.isObjectId('507f1f77bcf86cd799439011'),
2029
- objectIdString: req.aegis.helpers.toObjectId('507f1f77bcf86cd799439011')?.toString() || null,
2030
- secret: req.aegis.jlive.generate(32),
2031
- jliveAvailable: req.aegis.jlive.available,
2032
- });
2033
- });
2113
+ environments: {
2114
+ default: {
2115
+ logging: { level: 'debug' },
2034
2116
  },
2035
- };
2117
+ production: {
2118
+ logging: { level: 'warn' },
2119
+ security: { ddos: { maxRequests: 80 } },
2120
+ },
2121
+ }
2036
2122
  ```
2037
2123
 
2038
- Template usage (`res.render(...)`):
2039
-
2040
- ```ejs
2041
- <h1><%= title %></h1>
2042
- <p>Total: <%= money(1299.5, { currency: 'USD' }) %></p>
2043
- <p>Updated: <%= timeElapsed(updatedAt) %></p>
2044
- <p>Progress: <%= timeDifference(65, 0, 100) %>%</p>
2045
- <p>Summary: <%= breakStr("AegisNode framework helper utilities", 18, "...", true) %></p>
2046
- <p>With helper object: <%= helpers.number(1000000) %></p>
2047
- ```
2124
+ Behavior:
2125
+ - Base config loads first.
2126
+ - `environments.default` is merged next (if present).
2127
+ - `environments[env]` is merged last.
2128
+ - Arrays in overrides replace full arrays from base.
2048
2129
 
2049
- Available EJS locals:
2050
- - `helpers`
2051
- - `jlive`
2052
- - `money`
2053
- - `number`
2054
- - `dateTime`
2055
- - `timeElapsed`
2056
- - `timeDifference`
2057
- - `breakStr`
2058
- - `isObjectId`
2059
- - `toObjectId`
2060
- - `locale`
2061
- - `t`
2062
- - `csrfToken` (raw hidden input HTML)
2063
- - `csrfValue` (token string)
2130
+ <!-- SETTINGS_REFERENCE_END -->
2064
2131
 
2065
- `timeElapsed` supports both styles:
2066
- - `timeElapsed(value, { now, locale, numeric })` (Intl relative style)
2067
- - `timeElapsed(unixTime, true)` (short legacy-style mode)
2132
+ ## Runtime Patterns And Advanced Topics
2068
2133
 
2069
- Mongo id helpers:
2070
- - `isObjectId(value)` validates Mongo ObjectId format.
2071
- - `toObjectId(value)` returns a Mongo ObjectId instance or `null` when invalid.
2134
+ ### Middleware
2072
2135
 
2073
- `jlive` behavior:
2074
- - If `jlive` package is installed, bridge uses its methods.
2075
- - If not installed, `jlive.generate()` still works (crypto fallback), while crypto methods throw `JLIVE_UNAVAILABLE`.
2136
+ Route API supports Express-style middleware chains:
2076
2137
 
2077
- ### Template Locals From Settings
2138
+ ```js
2139
+ route.get('/secured', authGuard, UsersView.index);
2140
+ route.post('/users', validateBody, createUser);
2141
+ ```
2078
2142
 
2079
- You can inject custom functions/classes into all template renders from `settings.js`:
2143
+ You can also use `route.use()`:
2080
2144
 
2081
2145
  ```js
2082
- templates: {
2083
- enabled: true,
2084
- engine: 'ejs',
2085
- dir: 'templates',
2086
- base: 'base',
2087
- locals: {
2088
- formatCurrency: (value) => '$' + Number(value || 0).toFixed(2),
2089
- ViewBag: class ViewBag {
2090
- constructor(title) {
2091
- this.title = title;
2092
- }
2093
- },
2094
- },
2095
- },
2146
+ route.use(requestLogger);
2147
+ route.use('/admin', adminGuard, adminRoutes);
2096
2148
  ```
2097
2149
 
2098
- You can also pass already-defined class/function references by name (object shorthand):
2150
+ Basic middleware example:
2099
2151
 
2100
2152
  ```js
2101
- import { formatCurrency, ViewBag } from './app/template-locals.js';
2153
+ function requestLogger(req, res, next) {
2154
+ console.log(req.method, req.originalUrl);
2155
+ next();
2156
+ }
2157
+
2158
+ function requireModerator(req, res, next) {
2159
+ if (!req.auth?.roles?.includes('moderator')) {
2160
+ return res.status(403).json({ error: 'Moderator access required' });
2161
+ }
2162
+ next();
2163
+ }
2102
2164
 
2103
2165
  export default {
2104
- templates: {
2105
- locals: { formatCurrency, ViewBag },
2166
+ register(route) {
2167
+ route.use(requestLogger);
2168
+ route.get('/threads/:id/edit', requireModerator, ThreadsView.edit);
2169
+ route.use('/admin', requireModerator, adminRoutes);
2106
2170
  },
2107
2171
  };
2108
2172
  ```
2109
2173
 
2110
- Note:
2111
- - Use JS references/imports (as above).
2112
- - String names like `{ formatCurrency: 'formatCurrency' }` are not auto-resolved.
2174
+ AegisNode-specific middleware with injected runtime context:
2113
2175
 
2114
- Then in EJS:
2176
+ ```js
2177
+ async function forumContext({ helpers, i18n, logger }, req, res, next) {
2178
+ logger.info(`Forum request: ${req.method} ${req.originalUrl}`);
2179
+ res.locals.t = i18n.t;
2180
+ res.locals.formatNumber = helpers.number;
2181
+ next();
2182
+ }
2115
2183
 
2116
- ```ejs
2117
- <p><%= formatCurrency(123.45) %></p>
2118
- <p><%= new ViewBag('Dashboard').title %></p>
2184
+ export default {
2185
+ register(route) {
2186
+ route.use(forumContext);
2187
+ route.get('/threads', ThreadsView.index);
2188
+ },
2189
+ };
2119
2190
  ```
2120
2191
 
2121
- ## Runtime Patterns And Advanced Topics
2122
-
2123
- ### Middleware
2192
+ Injected middleware context can include:
2193
+ - `config`
2194
+ - `env`
2195
+ - `i18n`
2196
+ - `mail`
2197
+ - `logger`
2198
+ - `events`
2199
+ - `cache`
2200
+ - `io`
2201
+ - `auth`
2202
+ - `helpers`
2203
+ - `upload`
2204
+ - `services`
2205
+ - `models`
2206
+ - `validators`
2207
+ - `service`
2208
+ - `model`
2209
+ - `validator`
2210
+ - `database`
2211
+ - `dbClient`
2124
2212
 
2125
- Route API supports Express-style middleware chains:
2213
+ Auth middleware example:
2126
2214
 
2127
2215
  ```js
2128
- route.get('/secured', authGuard, UsersView.index);
2129
- route.post('/users', validateBody, createUser);
2216
+ export default {
2217
+ register(route) {
2218
+ const authGuard = (req, res, next) => req.aegis.auth.middleware()(req, res, next);
2219
+ route.get('/account', authGuard, UsersView.account);
2220
+ },
2221
+ };
2130
2222
  ```
2131
2223
 
2132
- You can also use `route.use()`:
2224
+ Uploads also work as route middleware:
2133
2225
 
2134
2226
  ```js
2135
- route.use(requestLogger);
2136
- route.use('/admin', adminGuard, adminRoutes);
2227
+ route.post('/avatar', route.upload.single('avatar'), UsersView.uploadAvatar);
2137
2228
  ```
2138
2229
 
2230
+ Important note:
2231
+ - In AegisNode, a 4-argument function is treated as `(context, req, res, next)`.
2232
+ - Do not use Express error-handler shape `(err, req, res, next)` in route chains, because the first argument is reserved for injected runtime context.
2233
+
2139
2234
  ### Validators (Between Route And Service)
2140
2235
 
2141
2236
  Each app can define `apps/<app>/validators.js`.
@@ -2428,7 +2523,7 @@ security: {
2428
2523
  ```
2429
2524
 
2430
2525
  `security.appSecret` should be strong (at least 16 chars). It is used to sign CSRF cookies and is required when `security.csrf.requireSignedCookie` is true (default).
2431
- New projects load it from `.env` through `APP_SECRET` by default.
2526
+ New projects load it from `.env` through `APP_SECRET` by default, and the generated `settings.js` also contains the same scaffold-time secret as a fallback literal.
2432
2527
  If neither `APP_SECRET` nor `security.appSecret` is set, AegisNode generates a fallback secret and persists it to `.aegis/app-secret`.
2433
2528
  If you run behind a reverse proxy, prefer top-level `trustProxy` (for example `1`) so client IP, secure-cookie detection, and HTTPS-aware auth logic are correct.
2434
2529
 
@@ -2447,6 +2542,8 @@ For forms, include token from `csrfToken`:
2447
2542
  </form>
2448
2543
  ```
2449
2544
 
2545
+ For `multipart/form-data` routes, use `route.upload.*` so AegisNode can parse the multipart body and validate the hidden `_csrf` field before your handler runs.
2546
+
2450
2547
  If you need the raw token string (for AJAX header), use `csrfValue`.
2451
2548
  For API clients (Postman/mobile) you can either send `x-csrf-token` or set `security.csrf.rejectUnsafeMethods = false`.
2452
2549