aegisnode 0.0.3 → 0.0.5

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