@lenne.tech/nest-server 11.31.2 → 11.31.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/.claude/rules/architecture.md +2 -0
  2. package/.claude/rules/configurable-features.md +2 -0
  3. package/CLAUDE.md +28 -1
  4. package/FRAMEWORK-API.md +4 -1
  5. package/dist/config.env.d.ts +4 -0
  6. package/dist/config.env.js +32 -2
  7. package/dist/config.env.js.map +1 -1
  8. package/dist/core/common/helpers/logging.helper.d.ts +1 -0
  9. package/dist/core/common/helpers/logging.helper.js +12 -0
  10. package/dist/core/common/helpers/logging.helper.js.map +1 -1
  11. package/dist/core/common/helpers/meta.helper.d.ts +1 -0
  12. package/dist/core/common/helpers/meta.helper.js +32 -1
  13. package/dist/core/common/helpers/meta.helper.js.map +1 -1
  14. package/dist/core/common/interfaces/server-options.interface.d.ts +9 -0
  15. package/dist/core/common/services/email.service.d.ts +3 -1
  16. package/dist/core/common/services/email.service.js +33 -2
  17. package/dist/core/common/services/email.service.js.map +1 -1
  18. package/dist/core/common/services/template.service.js +9 -4
  19. package/dist/core/common/services/template.service.js.map +1 -1
  20. package/dist/core/modules/hub/core-hub-actions.controller.d.ts +22 -0
  21. package/dist/core/modules/hub/core-hub-actions.controller.js +141 -0
  22. package/dist/core/modules/hub/core-hub-actions.controller.js.map +1 -0
  23. package/dist/core/modules/hub/core-hub-html.service.d.ts +8 -0
  24. package/dist/core/modules/hub/core-hub-html.service.js +123 -0
  25. package/dist/core/modules/hub/core-hub-html.service.js.map +1 -0
  26. package/dist/core/modules/hub/core-hub.controller.d.ts +56 -0
  27. package/dist/core/modules/hub/core-hub.controller.js +398 -0
  28. package/dist/core/modules/hub/core-hub.controller.js.map +1 -0
  29. package/dist/core/modules/hub/core-hub.module.d.ts +17 -0
  30. package/dist/core/modules/hub/core-hub.module.js +109 -0
  31. package/dist/core/modules/hub/core-hub.module.js.map +1 -0
  32. package/dist/core/modules/hub/core-hub.service.d.ts +18 -0
  33. package/dist/core/modules/hub/core-hub.service.js +153 -0
  34. package/dist/core/modules/hub/core-hub.service.js.map +1 -0
  35. package/dist/core/modules/hub/helpers/hub-client-js.helper.d.ts +1 -0
  36. package/dist/core/modules/hub/helpers/hub-client-js.helper.js +755 -0
  37. package/dist/core/modules/hub/helpers/hub-client-js.helper.js.map +1 -0
  38. package/dist/core/modules/hub/helpers/hub-command-shape.helper.d.ts +1 -0
  39. package/dist/core/modules/hub/helpers/hub-command-shape.helper.js +33 -0
  40. package/dist/core/modules/hub/helpers/hub-command-shape.helper.js.map +1 -0
  41. package/dist/core/modules/hub/helpers/hub-mask.helper.d.ts +1 -0
  42. package/dist/core/modules/hub/helpers/hub-mask.helper.js +46 -0
  43. package/dist/core/modules/hub/helpers/hub-mask.helper.js.map +1 -0
  44. package/dist/core/modules/hub/helpers/hub-mermaid.helper.d.ts +10 -0
  45. package/dist/core/modules/hub/helpers/hub-mermaid.helper.js +35 -0
  46. package/dist/core/modules/hub/helpers/hub-mermaid.helper.js.map +1 -0
  47. package/dist/core/modules/hub/helpers/hub-shell.helper.d.ts +9 -0
  48. package/dist/core/modules/hub/helpers/hub-shell.helper.js +62 -0
  49. package/dist/core/modules/hub/helpers/hub-shell.helper.js.map +1 -0
  50. package/dist/core/modules/hub/hub-action-messages.d.ts +17 -0
  51. package/dist/core/modules/hub/hub-action-messages.js +21 -0
  52. package/dist/core/modules/hub/hub-action-messages.js.map +1 -0
  53. package/dist/core/modules/hub/hub-config.helper.d.ts +11 -0
  54. package/dist/core/modules/hub/hub-config.helper.js +164 -0
  55. package/dist/core/modules/hub/hub-config.helper.js.map +1 -0
  56. package/dist/core/modules/hub/hub-nav.d.ts +12 -0
  57. package/dist/core/modules/hub/hub-nav.js +43 -0
  58. package/dist/core/modules/hub/hub-nav.js.map +1 -0
  59. package/dist/core/modules/hub/hub-ring-buffer.d.ts +22 -0
  60. package/dist/core/modules/hub/hub-ring-buffer.js +59 -0
  61. package/dist/core/modules/hub/hub-ring-buffer.js.map +1 -0
  62. package/dist/core/modules/hub/hub.constants.d.ts +18 -0
  63. package/dist/core/modules/hub/hub.constants.js +22 -0
  64. package/dist/core/modules/hub/hub.constants.js.map +1 -0
  65. package/dist/core/modules/hub/index.d.ts +25 -0
  66. package/dist/core/modules/hub/index.js +42 -0
  67. package/dist/core/modules/hub/index.js.map +1 -0
  68. package/dist/core/modules/hub/interfaces/hub-config.interface.d.ts +115 -0
  69. package/dist/core/modules/hub/interfaces/hub-config.interface.js +3 -0
  70. package/dist/core/modules/hub/interfaces/hub-config.interface.js.map +1 -0
  71. package/dist/core/modules/hub/interfaces/hub-panels.interface.d.ts +231 -0
  72. package/dist/core/modules/hub/interfaces/hub-panels.interface.js +3 -0
  73. package/dist/core/modules/hub/interfaces/hub-panels.interface.js.map +1 -0
  74. package/dist/core/modules/hub/middleware/hub-trace.middleware.d.ts +8 -0
  75. package/dist/core/modules/hub/middleware/hub-trace.middleware.js +50 -0
  76. package/dist/core/modules/hub/middleware/hub-trace.middleware.js.map +1 -0
  77. package/dist/core/modules/hub/services/core-hub-actions.service.d.ts +48 -0
  78. package/dist/core/modules/hub/services/core-hub-actions.service.js +136 -0
  79. package/dist/core/modules/hub/services/core-hub-actions.service.js.map +1 -0
  80. package/dist/core/modules/hub/services/core-hub-db.service.d.ts +19 -0
  81. package/dist/core/modules/hub/services/core-hub-db.service.js +180 -0
  82. package/dist/core/modules/hub/services/core-hub-db.service.js.map +1 -0
  83. package/dist/core/modules/hub/services/core-hub-email.service.d.ts +24 -0
  84. package/dist/core/modules/hub/services/core-hub-email.service.js +148 -0
  85. package/dist/core/modules/hub/services/core-hub-email.service.js.map +1 -0
  86. package/dist/core/modules/hub/services/core-hub-mailbox.service.d.ts +20 -0
  87. package/dist/core/modules/hub/services/core-hub-mailbox.service.js +103 -0
  88. package/dist/core/modules/hub/services/core-hub-mailbox.service.js.map +1 -0
  89. package/dist/core/modules/hub/services/core-hub-migrations.service.d.ts +25 -0
  90. package/dist/core/modules/hub/services/core-hub-migrations.service.js +115 -0
  91. package/dist/core/modules/hub/services/core-hub-migrations.service.js.map +1 -0
  92. package/dist/core/modules/hub/services/core-hub-sources.service.d.ts +28 -0
  93. package/dist/core/modules/hub/services/core-hub-sources.service.js +187 -0
  94. package/dist/core/modules/hub/services/core-hub-sources.service.js.map +1 -0
  95. package/dist/core/modules/hub/services/hub-log-buffer.service.d.ts +24 -0
  96. package/dist/core/modules/hub/services/hub-log-buffer.service.js +210 -0
  97. package/dist/core/modules/hub/services/hub-log-buffer.service.js.map +1 -0
  98. package/dist/core/modules/hub/services/hub-query-profiler.service.d.ts +38 -0
  99. package/dist/core/modules/hub/services/hub-query-profiler.service.js +235 -0
  100. package/dist/core/modules/hub/services/hub-query-profiler.service.js.map +1 -0
  101. package/dist/core/modules/hub/services/hub-trace-buffer.service.d.ts +18 -0
  102. package/dist/core/modules/hub/services/hub-trace-buffer.service.js +123 -0
  103. package/dist/core/modules/hub/services/hub-trace-buffer.service.js.map +1 -0
  104. package/dist/core.module.js +42 -1
  105. package/dist/core.module.js.map +1 -1
  106. package/dist/index.d.ts +1 -0
  107. package/dist/index.js +1 -0
  108. package/dist/index.js.map +1 -1
  109. package/dist/tsconfig.build.tsbuildinfo +1 -1
  110. package/docs/REQUEST-LIFECYCLE.md +1 -0
  111. package/migration-guides/11.31.2-to-11.31.3.md +135 -0
  112. package/package.json +5 -4
  113. package/src/config.env.ts +75 -2
  114. package/src/core/common/helpers/logging.helper.spec.ts +61 -0
  115. package/src/core/common/helpers/logging.helper.ts +48 -0
  116. package/src/core/common/helpers/meta.helper.ts +46 -1
  117. package/src/core/common/interfaces/server-options.interface.ts +46 -0
  118. package/src/core/common/services/email.service.ts +33 -1
  119. package/src/core/common/services/template.service.ts +21 -16
  120. package/src/core/modules/hub/INTEGRATION-CHECKLIST.md +64 -0
  121. package/src/core/modules/hub/README.md +159 -0
  122. package/src/core/modules/hub/core-hub-actions.controller.ts +137 -0
  123. package/src/core/modules/hub/core-hub-html.service.ts +135 -0
  124. package/src/core/modules/hub/core-hub.controller.ts +286 -0
  125. package/src/core/modules/hub/core-hub.module.spec.ts +108 -0
  126. package/src/core/modules/hub/core-hub.module.ts +159 -0
  127. package/src/core/modules/hub/core-hub.service.ts +169 -0
  128. package/src/core/modules/hub/helpers/hub-client-js.helper.ts +768 -0
  129. package/src/core/modules/hub/helpers/hub-command-shape.helper.spec.ts +48 -0
  130. package/src/core/modules/hub/helpers/hub-command-shape.helper.ts +47 -0
  131. package/src/core/modules/hub/helpers/hub-mask.helper.spec.ts +67 -0
  132. package/src/core/modules/hub/helpers/hub-mask.helper.ts +78 -0
  133. package/src/core/modules/hub/helpers/hub-mermaid.helper.spec.ts +54 -0
  134. package/src/core/modules/hub/helpers/hub-mermaid.helper.ts +62 -0
  135. package/src/core/modules/hub/helpers/hub-shell.helper.spec.ts +106 -0
  136. package/src/core/modules/hub/helpers/hub-shell.helper.ts +90 -0
  137. package/src/core/modules/hub/hub-action-messages.ts +47 -0
  138. package/src/core/modules/hub/hub-config.helper.spec.ts +108 -0
  139. package/src/core/modules/hub/hub-config.helper.ts +233 -0
  140. package/src/core/modules/hub/hub-nav.ts +66 -0
  141. package/src/core/modules/hub/hub-ring-buffer.spec.ts +95 -0
  142. package/src/core/modules/hub/hub-ring-buffer.ts +101 -0
  143. package/src/core/modules/hub/hub.constants.ts +84 -0
  144. package/src/core/modules/hub/index.ts +25 -0
  145. package/src/core/modules/hub/interfaces/hub-config.interface.ts +265 -0
  146. package/src/core/modules/hub/interfaces/hub-panels.interface.ts +186 -0
  147. package/src/core/modules/hub/middleware/hub-trace.middleware.ts +45 -0
  148. package/src/core/modules/hub/services/core-hub-actions.service.ts +133 -0
  149. package/src/core/modules/hub/services/core-hub-db.service.ts +185 -0
  150. package/src/core/modules/hub/services/core-hub-email.service.ts +158 -0
  151. package/src/core/modules/hub/services/core-hub-mailbox.service.spec.ts +116 -0
  152. package/src/core/modules/hub/services/core-hub-mailbox.service.ts +121 -0
  153. package/src/core/modules/hub/services/core-hub-migrations.service.ts +112 -0
  154. package/src/core/modules/hub/services/core-hub-sources.service.ts +194 -0
  155. package/src/core/modules/hub/services/hub-log-buffer.service.ts +252 -0
  156. package/src/core/modules/hub/services/hub-query-profiler.service.ts +274 -0
  157. package/src/core/modules/hub/services/hub-trace-buffer.service.spec.ts +112 -0
  158. package/src/core/modules/hub/services/hub-trace-buffer.service.ts +134 -0
  159. package/src/core.module.ts +61 -1
  160. package/src/index.ts +6 -0
@@ -213,6 +213,7 @@ JWT-based authentication for existing projects:
213
213
  | **Health Check Module** | `GET /health` + GraphQL `healthCheck` query |
214
214
  | **Error Code Module** | Centralized error registry with unique IDs |
215
215
  | **Permissions Report** | Interactive HTML dashboard, JSON, and Markdown reports |
216
+ | **Hub (Operator Cockpit)** | Build-free ADMIN-gated dashboard at `/hub` (config-gated per environment). Adds an optional HTTP trace middleware (registered by `CoreHubModule.configure()` only when traces are enabled), a chaining `Logger.overrideLogger()` delegate for the log buffer, an optional `EmailService` capture hook (`HUB_EMAIL_CAPTURE` token) for the mailbox, and — when the query profiler is enabled — opts the MongoDB driver into `monitorCommands` from `core.module.ts`. See `src/core/modules/hub/README.md`. |
216
217
  | **System Setup Module** | Initial admin creation for fresh deployments |
217
218
  | **Cron Jobs** | `CoreCronJobsService` with timezone/UTC offset support |
218
219
  | **Model Documentation** | Auto-generated model docs via `ModelDocService` |
@@ -0,0 +1,135 @@
1
+ # Migration Guide: 11.31.2 → 11.31.3
2
+
3
+ ## Overview
4
+
5
+ | Category | Effort | Required? |
6
+ |----------|--------|-----------|
7
+ | **New Feature** | Opt-in | No — purely additive |
8
+ | Breaking Changes | None | — |
9
+ | Config Changes | Optional | Only if you enable the Hub |
10
+
11
+ This release adds the **Hub** — a build-free, ADMIN-gated operator cockpit served directly by the
12
+ framework at `/hub`. It is **disabled by default** and must be switched on per environment, so
13
+ existing projects are unaffected until they opt in. No code changes are required to update.
14
+
15
+ It also includes a **security patch**: three moderate `axios` advisories (recursion DoS + prototype
16
+ pollution, transitive via the Brevo and Mailjet mail SDKs) are closed by pinning `axios` to `1.18.1`.
17
+ Nothing to do on your side — just update.
18
+
19
+ ## Quick Migration
20
+
21
+ ```bash
22
+ # Update package
23
+ pnpm update @lenne.tech/nest-server
24
+
25
+ # Verify build
26
+ pnpm run build
27
+
28
+ # Run tests
29
+ pnpm test
30
+ ```
31
+
32
+ Nothing else is required — the Hub stays dormant (every `/hub` route answers 404) until you add a
33
+ `hub` block to a config environment.
34
+
35
+ ## What's New in 11.31.3
36
+
37
+ ### Hub — admin area / operator cockpit
38
+
39
+ A dependency-free dashboard of runtime information and admin tools, rendered as server-side HTML +
40
+ a vanilla-JS SPA (no React, no build step), so it works identically in npm- and vendor-mode.
41
+
42
+ **16 panels:** Dashboard, Diagnostics, Logs, Request Traces, Query Performance, Cron Jobs, Database,
43
+ Models/ERD, Migrations, Files (GridFS), Config (secrets masked), Auth Migration (Legacy→IAM), Error
44
+ Codes, Email Preview, Mailbox, AI. Each has a `*.json` sidecar (the stable data contract) the client
45
+ polls; optional sources degrade to an "unavailable" state instead of erroring.
46
+
47
+ **3 runtime collectors** (in-memory ring buffers, per-app-instance, parallel-test-safe):
48
+ - **Logs** — a chaining `Logger.overrideLogger()` delegate (no main.ts change), secrets redacted.
49
+ - **Traces** — an Express middleware bound only when the collector is enabled (zero cost otherwise).
50
+ - **Queries** — MongoDB driver command monitoring; records value-free query SHAPES (N+1 templates),
51
+ never values. Enabling it opts the driver into `monitorCommands`.
52
+
53
+ **Built-in Mailbox** — a Mailpit-style capture of outgoing mail hooked into `EmailService`. In
54
+ `mode: 'capture'` mail is recorded and NOT sent (dev/test); in `mode: 'copy'` it is sent AND a copy
55
+ is recorded (with tokens/links redacted in the stored copy). `capture` throws at startup in
56
+ `production`/`staging` so it can never silently swallow real mail.
57
+
58
+ **Admin actions** (opt-out via `hub.actions: false`) — migrations run/rollback, GridFS file delete,
59
+ cron start/stop/trigger, collector-buffer clear, test mail. Every mutating request requires the
60
+ `X-Hub-Request: 1` header (CSRF defense) and destructive ones a server-validated type-to-confirm
61
+ keyword; each writes a `[HUB-ACTION] <action> by user <id>` audit line.
62
+
63
+ #### Enable it
64
+
65
+ The Hub is **never enabled implicitly** — switch it on per environment in `config.env.ts`:
66
+
67
+ ```typescript
68
+ // config.env.ts (development / local)
69
+ hub: {
70
+ collectors: { queries: true }, // opt-in query profiler
71
+ mailbox: { mode: 'capture' }, // capture outgoing mail locally (Mailpit replacement)
72
+ },
73
+
74
+ // production — usually omit `hub` entirely. If you do want it (still ADMIN-gated),
75
+ // never use mailbox capture mode there:
76
+ // hub: { mailbox: false },
77
+ ```
78
+
79
+ Boolean shorthand `hub: true` enables it at `/hub`, admin-only, with default collectors. See
80
+ `IHubConfig` for every option. Env-var equivalents exist via `NSC__HUB__*` (see `.env.example`).
81
+
82
+ Then sign in as a user with `RoleEnum.ADMIN` and open `/hub`. The shell is self-sufficient: if you
83
+ are not authenticated it shows a login form that posts to `loginEndpoint` (default
84
+ `/iam/sign-in/email`), so an admin can sign in directly at the API without the frontend. In a
85
+ fullstack setup, a cross-subdomain session cookie from the app login already authenticates `/hub`.
86
+
87
+ #### Customization (optional)
88
+
89
+ Replace any Hub controller/service with a project subclass via `CoreModule.forRoot`:
90
+
91
+ ```typescript
92
+ CoreModule.forRoot(envConfig, {
93
+ hub: {
94
+ controller: MyHubController, // extends CoreHubController
95
+ actionsController: MyHubActionsController, // extends CoreHubActionsController
96
+ service: MyHubService, // extends CoreHubService
97
+ htmlService: MyHubHtmlService, // extends CoreHubHtmlService
98
+ actionsService: MyHubActionsService, // extends CoreHubActionsService
99
+ },
100
+ });
101
+ ```
102
+
103
+ ## Breaking Changes
104
+
105
+ None. The Hub is opt-in and disabled by default.
106
+
107
+ ## Compatibility Notes
108
+
109
+ | Pattern | Status | Notes |
110
+ |---------|--------|-------|
111
+ | Projects that never set `hub` | Unaffected | Every `/hub` route answers 404 |
112
+ | Custom `EmailService` subclass | Compatible | The mailbox capture hook is injected optionally (`@Optional()`) — absent when the Hub/mailbox is off, so there is zero cost and no behavior change |
113
+ | No auth system enabled | ⚠️ Action required | The Hub relies on the app-wide `RolesGuard`/`BetterAuthRolesGuard` to enforce `@Roles(ADMIN)`. With BetterAuth and legacy auth both disabled, no guard runs and the gate is inert — keep an auth module enabled or leave the Hub disabled |
114
+ | `queries` collector enabled | Compatible | Opts the MongoDB driver into `monitorCommands` from `core.module.ts` when the consumer has not set it |
115
+
116
+ ## Troubleshooting
117
+
118
+ | Issue | Fix |
119
+ |-------|-----|
120
+ | `/hub` → 404 everywhere | `hub` is not set in that environment's config block — add `hub: true` (or an options object) |
121
+ | Startup error mentioning mailbox `capture` | `mailbox.mode: 'capture'` is forbidden in `production`/`staging` — use `copy` or disable the mailbox there |
122
+ | Query panel empty | The queries collector is opt-in — set `hub.collectors.queries: true` |
123
+ | Cron panel empty | Import `ScheduleModule.forRoot()` in your ServerModule |
124
+ | Hub reachable without login | You disabled all auth systems, so no roles guard runs — see the Compatibility note above |
125
+
126
+ ## Module Documentation
127
+
128
+ - `src/core/modules/hub/README.md` — full panel/action/collector/security reference
129
+ - `src/core/modules/hub/INTEGRATION-CHECKLIST.md` — config-only setup + common mistakes
130
+ - `.claude/rules/configurable-features.md` — Hub row in the configurable-features table
131
+
132
+ ## References
133
+
134
+ - Configurable-features pattern: `.claude/rules/configurable-features.md`
135
+ - Request lifecycle (middleware + logger override + email hook): `docs/REQUEST-LIFECYCLE.md`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/nest-server",
3
- "version": "11.31.2",
3
+ "version": "11.31.3",
4
4
  "description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
5
5
  "keywords": [
6
6
  "node",
@@ -23,9 +23,9 @@
23
23
  "build:dev": "pnpm run build",
24
24
  "c": "pnpm run check",
25
25
  "check": "node scripts/check.mjs",
26
- "check:raw": "pnpm install --frozen-lockfile && pnpm audit && pnpm run format:check && pnpm run lint && pnpm run check:swc-tdz && pnpm test && pnpm run build && pnpm run check:manifest && bash scripts/check-server-start.sh",
27
- "check:fix": "pnpm install && pnpm audit --fix && pnpm run format && pnpm run lint:fix && pnpm run check:swc-tdz && pnpm test && pnpm run build && pnpm run check:manifest && bash scripts/check-server-start.sh",
28
- "check:naf": "pnpm install && pnpm run format && pnpm run lint:fix && pnpm run check:swc-tdz && pnpm test && pnpm run build && pnpm run check:manifest && bash scripts/check-server-start.sh",
26
+ "check:raw": "pnpm install --frozen-lockfile && pnpm audit && pnpm run format:check && pnpm run lint && pnpm run typecheck:tests && pnpm run check:swc-tdz && pnpm test && pnpm run build && pnpm run check:manifest && bash scripts/check-server-start.sh",
27
+ "check:fix": "pnpm install && pnpm audit --fix && pnpm run format && pnpm run lint:fix && pnpm run typecheck:tests && pnpm run check:swc-tdz && pnpm test && pnpm run build && pnpm run check:manifest && bash scripts/check-server-start.sh",
28
+ "check:naf": "pnpm install && pnpm run format && pnpm run lint:fix && pnpm run typecheck:tests && pnpm run check:swc-tdz && pnpm test && pnpm run build && pnpm run check:manifest && bash scripts/check-server-start.sh",
29
29
  "check:manifest": "node scripts/check-package-manifest.mjs",
30
30
  "check:swc-tdz": "nest build -b swc -p tsconfig.swc-tdz.json && node scripts/check-swc-tdz.mjs",
31
31
  "cf": "pnpm run check:fix",
@@ -65,6 +65,7 @@
65
65
  "vitest:unit:cov": "vitest run --coverage --config vitest.config.ts",
66
66
  "test:unit:watch": "vitest --config vitest.config.ts",
67
67
  "test:types": "tsc --noEmit --skipLibCheck -p tests/types/tsconfig.json",
68
+ "typecheck:tests": "tsc --noEmit -p tsconfig.tests.json",
68
69
  "test:cleanup": "find tests -type f \\( -name '*.txt' -o -name '*.bin' \\) -not -name '.gitkeep' -delete && echo 'Test artifacts cleaned up'",
69
70
  "watch": "npm-watch"
70
71
  },
package/src/config.env.ts CHANGED
@@ -20,6 +20,7 @@ import { IServerOptions } from './core/common/interfaces/server-options.interfac
20
20
  // which is purely cosmetic and carries promotional content. Warnings (`⚠`) for genuine
21
21
  // misconfiguration still surface.
22
22
  dotenv.config({ quiet: true });
23
+
23
24
  const config: { [env: string]: IServerOptions } = {
24
25
  // ===========================================================================
25
26
  // CI environment
@@ -141,6 +142,10 @@ const config: { [env: string]: IServerOptions } = {
141
142
  modelDocumentation: false,
142
143
  uri: process.env.MONGODB_URI || 'mongodb://127.0.0.1/nest-server-ci',
143
144
  },
145
+ hub: {
146
+ collectors: { queries: { warnMs: 1 } },
147
+ mailbox: { mode: 'capture' },
148
+ },
144
149
  permissions: true,
145
150
  port: Number(process.env.PORT) || 3000,
146
151
  security: {
@@ -260,6 +265,14 @@ const config: { [env: string]: IServerOptions } = {
260
265
  modelDocumentation: false,
261
266
  uri: process.env.MONGODB_URI || 'mongodb://127.0.0.1/nest-server-dev',
262
267
  },
268
+ // Hub (operator cockpit) — OPTIONAL and enabled PER ENVIRONMENT (it is never on implicitly).
269
+ // `queries` (query profiler) and `mailbox` are opt-in; the whole thing stays ADMIN-gated.
270
+ // See src/core/modules/hub/README.md. In production it is off by default (see that block for how
271
+ // to enable it safely). Also toggleable via `NSC__HUB__*` env vars (see .env.example).
272
+ hub: {
273
+ collectors: { queries: true },
274
+ mailbox: { mode: 'capture' },
275
+ },
263
276
  permissions: true,
264
277
  port: Number(process.env.PORT) || 3000,
265
278
  security: {
@@ -405,6 +418,10 @@ const config: { [env: string]: IServerOptions } = {
405
418
  modelDocumentation: false,
406
419
  uri: process.env.MONGODB_URI || 'mongodb://127.0.0.1/nest-server-e2e',
407
420
  },
421
+ hub: {
422
+ collectors: { queries: { warnMs: 1 } },
423
+ mailbox: { mode: 'capture' },
424
+ },
408
425
  permissions: true,
409
426
  port: Number(process.env.PORT) || 3000,
410
427
  security: {
@@ -559,9 +576,14 @@ const config: { [env: string]: IServerOptions } = {
559
576
  modelDocumentation: true,
560
577
  uri: process.env.MONGODB_URI || 'mongodb://127.0.0.1/nest-server-local',
561
578
  },
562
- permissions: {
563
- role: false,
579
+ hub: {
580
+ collectors: { queries: true },
581
+ mailbox: { mode: 'capture' },
564
582
  },
583
+ // Authored intent: ADMIN-gated (reach it via the Hub's "Routes / Permissions" panel). The
584
+ // permissions-follows-hub invariant is enforced centrally by enforcePermissionsSecurity() below —
585
+ // e.g. if the Hub here is disabled, that pass flips this to a public localhost-only report.
586
+ permissions: true,
565
587
  port: Number(process.env.PORT) || 3000,
566
588
  security: {
567
589
  checkResponseInterceptor: {
@@ -665,6 +687,18 @@ const config: { [env: string]: IServerOptions } = {
665
687
  },
666
688
  enabled: true,
667
689
  },
690
+ // Hub (operator cockpit) is OFF in production by default (never enabled implicitly). To turn it
691
+ // on here — it stays ADMIN-gated — uncomment the block below. Note the production guard-rails:
692
+ // - mailbox: NEVER 'capture' in production/staging (it suppresses real mail; it throws at
693
+ // startup). Use `false`, or `{ mode: 'copy' }` if you want a redacted audit copy.
694
+ // - collectors.queries: opt-in (it opts the MongoDB driver into command monitoring).
695
+ // The standalone permissions report follows the Hub (see enforcePermissionsSecurity below), so
696
+ // enabling the Hub also makes the ADMIN-gated "Routes / Permissions" panel available.
697
+ // See src/core/modules/hub/README.md; also configurable via NSC__HUB__* (see .env.example).
698
+ // hub: {
699
+ // collectors: { queries: false },
700
+ // mailbox: false,
701
+ // },
668
702
  ignoreSelectionsForPopulate: true,
669
703
  jwt: {
670
704
  refresh: {
@@ -716,6 +750,45 @@ const config: { [env: string]: IServerOptions } = {
716
750
  },
717
751
  };
718
752
 
753
+ /**
754
+ * Enforce the permissions-report security invariant across every environment.
755
+ *
756
+ * The report is the full authorization map (every route + its required roles + `@Restricted` field
757
+ * rules) — a reconnaissance goldmine. The Hub does NOT need it (its "Routes / Permissions" panel
758
+ * reuses the scanner but degrades gracefully when the module is absent), so the rule is about the
759
+ * DANGEROUS direction: the report must never become a standalone, reachable surface.
760
+ *
761
+ * Derived per environment from THAT env's Hub state:
762
+ * - Hub OFF, non-`local` → report NOT registered (no in-app consumer → pure attack surface). This
763
+ * makes the combination "non-local + Hub off + visible permissions" impossible to configure.
764
+ * - Hub OFF, `local` → public (`role: false`) report allowed — frictionless, safe ONLY because
765
+ * `local` is a non-reachable localhost env with no admin surface.
766
+ * - Hub ON → left as authored (ADMIN when enabled, or off — the panel just degrades),
767
+ * EXCEPT a public (`role: false`) report is forced back to ADMIN in every non-`local` stage.
768
+ *
769
+ * NOTE: runs on the statically-authored config; `NSC__HUB__*` / `NSC__PERMISSIONS__*` env overrides
770
+ * are applied afterwards by getEnvironmentConfig and take precedence.
771
+ */
772
+ export function enforcePermissionsSecurity(cfg: { [env: string]: IServerOptions }): void {
773
+ for (const [envName, opts] of Object.entries(cfg)) {
774
+ const hub = opts.hub;
775
+ const hubOn =
776
+ hub === true || (typeof hub === 'object' && hub !== null && (hub as { enabled?: boolean }).enabled !== false);
777
+ const perms = opts.permissions;
778
+ const isPublic = typeof perms === 'object' && perms !== null && (perms as { role?: unknown }).role === false;
779
+
780
+ if (!hubOn) {
781
+ // Hub off: only a public localhost report (in `local`) is allowed; never register it elsewhere.
782
+ opts.permissions = envName === 'local' ? { role: false } : false;
783
+ } else if (envName !== 'local' && isPublic) {
784
+ // Reachable stage with the Hub on: the report must never be public — force the ADMIN gate.
785
+ opts.permissions = true;
786
+ }
787
+ }
788
+ }
789
+
790
+ enforcePermissionsSecurity(config);
791
+
719
792
  /**
720
793
  * Export config merged with other configs and environment variables as default
721
794
  */
@@ -0,0 +1,61 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { redactSensitiveText } from './logging.helper';
4
+
5
+ describe('redactSensitiveText', () => {
6
+ it('redacts JWTs appearing anywhere in a line', () => {
7
+ const jwt = 'eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U';
8
+ const out = redactSensitiveText(`token issued: ${jwt} done`);
9
+
10
+ expect(out).not.toContain(jwt);
11
+ expect(out).toContain('token issued:');
12
+ expect(out).toContain('done');
13
+ });
14
+
15
+ it('redacts Authorization Bearer values', () => {
16
+ const out = redactSensitiveText('Authorization: Bearer abcdef1234567890secret');
17
+
18
+ expect(out).not.toContain('abcdef1234567890secret');
19
+ expect(out.toLowerCase()).toContain('authorization');
20
+ });
21
+
22
+ it('redacts key/value secrets regardless of separator or quoting', () => {
23
+ expect(redactSensitiveText('password=SuperSecret123')).not.toContain('SuperSecret123');
24
+ expect(redactSensitiveText('token: "abcd1234efgh"')).not.toContain('abcd1234efgh');
25
+ expect(redactSensitiveText("apiKey='pk_live_9999'")).not.toContain('pk_live_9999');
26
+ expect(redactSensitiveText('client_secret=verySecretValue')).not.toContain('verySecretValue');
27
+ });
28
+
29
+ it('redacts cookie headers', () => {
30
+ const out = redactSensitiveText('cookie: session=abc123; other=xyz789');
31
+
32
+ expect(out).not.toContain('abc123');
33
+ expect(out).not.toContain('xyz789');
34
+ });
35
+
36
+ it('redacts reset/verification tokens carried as a URL PATH segment (not key=value)', () => {
37
+ const link = 'https://app.example.com/verify/AbCdEf0123456789XyZtoken please click';
38
+ const out = redactSensitiveText(link);
39
+ expect(out).not.toContain('AbCdEf0123456789XyZtoken');
40
+ expect(out).toContain('/verify/');
41
+ // also for reset / set-password style links
42
+ expect(redactSensitiveText('/reset/SUPERSECRETVALUE1234567890')).not.toContain('SUPERSECRETVALUE1234567890');
43
+ expect(redactSensitiveText('/set-password/ZZZ1112223334445556667')).not.toContain('ZZZ1112223334445556667');
44
+ });
45
+
46
+ it('leaves harmless short path segments untouched', () => {
47
+ // Only long token-like segments (>= 16 chars) are masked — normal routes stay readable.
48
+ const text = 'GET /verify/email done';
49
+ expect(redactSensitiveText(text)).toBe(text);
50
+ });
51
+
52
+ it('leaves harmless text untouched', () => {
53
+ const text = 'User signed in successfully with id 507f1f77bcf86cd799439011';
54
+ expect(redactSensitiveText(text)).toBe(text);
55
+ });
56
+
57
+ it('handles empty / non-secret input without throwing', () => {
58
+ expect(redactSensitiveText('')).toBe('');
59
+ expect(redactSensitiveText('plain message')).toBe('plain message');
60
+ });
61
+ });
@@ -132,3 +132,51 @@ export function maskToken(token: null | string | undefined): string {
132
132
  }
133
133
  return `${token.substring(0, 4)}...${token.substring(token.length - 4)}`;
134
134
  }
135
+
136
+ /**
137
+ * Redacts secrets from a free-form log line.
138
+ *
139
+ * Composes the individual maskers above into a single regex-based pass for text where the sensitive
140
+ * value is not at a known position (log messages, error strings). Applied by the Hub log collector
141
+ * before truncation, so a truncation cut can never leak the prefix of a secret.
142
+ *
143
+ * Covers: JWTs anywhere in the line, `Authorization: Bearer …`, common `key=value` secrets
144
+ * (password/token/secret/apiKey/client_secret/refresh_token/…), and cookie headers.
145
+ *
146
+ * @param text - The log line to redact
147
+ * @returns The line with secret values replaced by masked placeholders
148
+ *
149
+ * @example
150
+ * redactSensitiveText('password=hunter2') // 'password=***'
151
+ */
152
+ export function redactSensitiveText(text: string): string {
153
+ if (!text) {
154
+ return text;
155
+ }
156
+ return (
157
+ text
158
+ // JWTs anywhere in the line
159
+ .replace(/eyJ[\w-]{6,}\.[\w-]{6,}\.[\w-]{4,}/g, (match) => maskToken(match))
160
+ // reset/verification/invite tokens carried as a URL PATH segment (e.g. /verify/<token>,
161
+ // /reset/<token>) — these have no `key=value`, so the query-style rule below misses them.
162
+ .replace(
163
+ /(\/(?:verify|reset|confirm|activate|invite|magic-?link|set-password|change-email)\/)([A-Za-z0-9._~-]{16,})/gi,
164
+ (_m, prefix, token) => `${prefix}${maskToken(token)}`,
165
+ )
166
+ // authorization: Bearer xyz / Authorization=xyz
167
+ .replace(
168
+ /(authorization["']?\s*[:=]\s*["']?)(?:Bearer\s+)?([^\s"',;]+)/gi,
169
+ (_m, prefix, value) => `${prefix}${maskToken(value)}`,
170
+ )
171
+ // cookie headers (mask every value, keep names)
172
+ .replace(
173
+ /((?:^|[\s,;])cookie["']?\s*[:=]\s*)([^\n]+)/gi,
174
+ (_m, prefix, value) => `${prefix}${maskCookieHeader(value)}`,
175
+ )
176
+ // key/value secrets: password=..., token: "...", apiKey=..., secret=..., client_secret=..., refresh_token=...
177
+ .replace(
178
+ /((?:password|passwd|pwd|token|secret|api[-_]?key|client[-_]?secret|refresh[-_]?token|access[-_]?token|private[-_]?key)["']?\s*[:=]\s*["']?)([^\s"',;&}]+)/gi,
179
+ (_m, prefix, value) => `${prefix}${maskSensitive(value)}`,
180
+ )
181
+ );
182
+ }
@@ -15,12 +15,18 @@
15
15
  * @see getBuildInfo
16
16
  */
17
17
 
18
+ import { existsSync, readFileSync } from 'fs';
19
+ import { join } from 'path';
20
+
18
21
  /** Default environment variable the build commit SHA is baked into. */
19
22
  export const DEFAULT_COMMIT_ENV = 'APP_VERSION_COMMIT';
20
23
 
21
24
  /** Defined value returned when no commit could be resolved. */
22
25
  export const UNKNOWN_COMMIT = 'unknown';
23
26
 
27
+ /** Cached package.json version lookup (`null` = looked up and not found). */
28
+ let cachedVersion: null | string | undefined;
29
+
24
30
  /**
25
31
  * Build identity of the running process.
26
32
  */
@@ -64,6 +70,45 @@ export function getBuildInfo(options: { commitEnvName?: string; env?: string; ve
64
70
  return {
65
71
  commit: getCommit(options.commitEnvName),
66
72
  env: options.env,
67
- version: options.version || UNKNOWN_COMMIT,
73
+ version: options.version || getVersion(),
68
74
  };
69
75
  }
76
+
77
+ /**
78
+ * Resolve the semantic version of the running app from its `package.json`.
79
+ *
80
+ * Reads the `version` field of the nearest `package.json` (searching up from the current working
81
+ * directory), cached after the first lookup. This is the deployed app's own version — in a consumer
82
+ * project that is the API's version, in this repo it is the framework version. Falls back to
83
+ * `'unknown'` when no `package.json` / version can be resolved.
84
+ *
85
+ * Prefer an explicit `IServerOptions.version` when set; this is the zero-config default.
86
+ */
87
+ export function getVersion(): string {
88
+ if (cachedVersion !== undefined) {
89
+ return cachedVersion ?? UNKNOWN_COMMIT;
90
+ }
91
+ cachedVersion = null;
92
+ try {
93
+ // Walk up from cwd so a monorepo `projects/api` started from the repo root still resolves.
94
+ let dir = process.cwd();
95
+ for (let i = 0; i < 6; i++) {
96
+ const pkgPath = join(dir, 'package.json');
97
+ if (existsSync(pkgPath)) {
98
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as { version?: string };
99
+ if (typeof pkg.version === 'string' && pkg.version) {
100
+ cachedVersion = pkg.version;
101
+ break;
102
+ }
103
+ }
104
+ const parent = join(dir, '..');
105
+ if (parent === dir) {
106
+ break;
107
+ }
108
+ dir = parent;
109
+ }
110
+ } catch {
111
+ /* ignore — fall back to 'unknown' */
112
+ }
113
+ return cachedVersion ?? UNKNOWN_COMMIT;
114
+ }
@@ -18,6 +18,7 @@ import * as SMTPTransport from 'nodemailer/lib/smtp-transport';
18
18
  import type * as StreamTransport from 'nodemailer/lib/stream-transport';
19
19
 
20
20
  import { Falsy } from '../types/falsy.type';
21
+ import { IHubConfig } from '../../modules/hub/interfaces/hub-config.interface';
21
22
  import { IPermissions } from '../../modules/permissions/interfaces/permissions.interface';
22
23
  import { CronJobConfigWithTimeZone } from './cron-job-config-with-time-zone.interface';
23
24
  import { CronJobConfigWithUtcOffset } from './cron-job-config-with-utc-offset.interface';
@@ -1847,6 +1848,24 @@ export interface IServerOptions {
1847
1848
  */
1848
1849
  hostname?: string;
1849
1850
 
1851
+ /**
1852
+ * Hub admin area (operator cockpit).
1853
+ *
1854
+ * A build-free, ADMIN-gated dashboard of runtime information and admin tools (dashboard,
1855
+ * diagnostics, logs, request traces, query profiler, migrations, DB stats, models/ERD, files,
1856
+ * cron jobs, error codes, masked config, auth-migration progress, email preview, built-in mailbox,
1857
+ * AI panel).
1858
+ *
1859
+ * Follows "presence implies enabled" — but is NEVER enabled implicitly. It must be switched on per
1860
+ * environment (here or via `NSC__HUB__*`); no environment inherits it.
1861
+ * - `true` / `{}`: enabled at `/hub`, admin-only, default collectors (logs + traces on, queries off)
1862
+ * - `{ path: 'admin/hub', collectors: { queries: true } }`: custom path + query profiling
1863
+ * - `{ enabled: false }` / `undefined`: disabled — every Hub route answers 404
1864
+ *
1865
+ * @default undefined (disabled)
1866
+ */
1867
+ hub?: boolean | IHubConfig;
1868
+
1850
1869
  /**
1851
1870
  * Ignore selections in fieldSelection
1852
1871
  * [ConfigService must be integrated in ModuleService]
@@ -3312,4 +3331,31 @@ export interface ICoreModuleOverrides {
3312
3331
  controller?: Type<any>;
3313
3332
  service?: Type<any>;
3314
3333
  };
3334
+
3335
+ /**
3336
+ * Override Hub controllers and/or services with project-specific subclasses.
3337
+ *
3338
+ * - `actionsController` must extend `CoreHubActionsController`
3339
+ * - `actionsService` must extend `CoreHubActionsService`
3340
+ * - `controller` must extend `CoreHubController`
3341
+ * - `htmlService` must extend `CoreHubHtmlService`
3342
+ * - `service` must extend `CoreHubService`
3343
+ *
3344
+ * @example
3345
+ * ```typescript
3346
+ * {
3347
+ * hub: {
3348
+ * service: MyHubService,
3349
+ * htmlService: MyHubHtmlService,
3350
+ * },
3351
+ * }
3352
+ * ```
3353
+ */
3354
+ hub?: {
3355
+ actionsController?: Type<any>;
3356
+ actionsService?: Type<any>;
3357
+ controller?: Type<any>;
3358
+ htmlService?: Type<any>;
3359
+ service?: Type<any>;
3360
+ };
3315
3361
  }
@@ -1,12 +1,14 @@
1
1
  import { createHash } from 'crypto';
2
- import { Injectable, OnModuleDestroy } from '@nestjs/common';
2
+ import { Inject, Injectable, OnModuleDestroy, Optional } from '@nestjs/common';
3
3
  import nodemailer = require('nodemailer');
4
4
  import { Attachment } from 'nodemailer/lib/mailer';
5
5
 
6
6
  import { isNonEmptyString, isTrue, returnFalse } from '../helpers/input.helper';
7
7
  import { MailTransportOptions } from '../interfaces/server-options.interface';
8
+ import { HUB_EMAIL_CAPTURE } from '../../modules/hub/hub.constants';
8
9
  import { ConfigService } from './config.service';
9
10
  import { TemplateService } from './template.service';
11
+ import type { IHubEmailCapture } from '../../modules/hub/interfaces/hub-config.interface';
10
12
 
11
13
  /**
12
14
  * Email service
@@ -26,6 +28,9 @@ export class EmailService implements OnModuleDestroy {
26
28
  constructor(
27
29
  protected configService: ConfigService,
28
30
  protected templateService: TemplateService,
31
+ // Optional Hub mailbox hook. When the Hub mailbox is enabled it captures outgoing mail (and, in
32
+ // capture mode, suppresses the actual send). Undefined otherwise — zero cost.
33
+ @Optional() @Inject(HUB_EMAIL_CAPTURE) protected readonly emailCapture?: IHubEmailCapture,
29
34
  ) {}
30
35
 
31
36
  onModuleDestroy(): void {
@@ -104,6 +109,33 @@ export class EmailService implements OnModuleDestroy {
104
109
  }
105
110
  }
106
111
 
112
+ // Hub mailbox capture (Mailpit-style). Runs after templates are rendered, before the transport.
113
+ // In capture mode it records the mail and suppresses the send (returns a jsonTransport-like ack).
114
+ // Fully guarded: a broken mailbox hook must never break (or crash) the mail path.
115
+ if (this.emailCapture) {
116
+ let skipTransport = false;
117
+ try {
118
+ skipTransport = this.emailCapture.capture({
119
+ from: `"${senderName}" <${senderEmail}>`,
120
+ html,
121
+ subject,
122
+ templateName: htmlTemplate ?? textTemplate,
123
+ text,
124
+ to: Array.isArray(recipients) ? recipients.join(', ') : recipients,
125
+ });
126
+ } catch {
127
+ /* mailbox capture must never break mail sending */
128
+ }
129
+ if (skipTransport) {
130
+ return {
131
+ accepted: Array.isArray(recipients) ? recipients : [recipients],
132
+ captured: true,
133
+ messageId: 'hub-mailbox',
134
+ rejected: [],
135
+ };
136
+ }
137
+ }
138
+
107
139
  // Reuse transporter if SMTP config hasn't changed (avoids creating new connections per email)
108
140
  // Use hash instead of raw JSON to avoid keeping credentials as a string in memory
109
141
  const smtpKey = createHash('sha256').update(JSON.stringify(smtp)).digest('hex');
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
2
2
  import { TemplateFunction } from 'ejs';
3
3
  import ejs = require('ejs');
4
4
  import fs = require('fs');
5
- import { join } from 'path';
5
+ import { resolve, sep } from 'path';
6
6
 
7
7
  import { ConfigService } from './config.service';
8
8
 
@@ -36,27 +36,32 @@ export class TemplateService {
36
36
  * @param filePath Directory names (separated via '/' if template is in subdirectory) + name of the template file without extension
37
37
  */
38
38
  protected async getTemplate(filePath: string): Promise<TemplateFunction> {
39
- return new Promise<TemplateFunction>((resolve, reject) => {
39
+ // Resolve the template file to an absolute path and enforce that it stays within the configured
40
+ // templates directory. Legitimate template names never contain '..'; this guard is a defense in
41
+ // depth against path traversal for any caller that forwards user-influenced template names.
42
+ const baseDir = resolve(this.configService.getFastButReadOnly('templates.path'));
43
+ const fullPath = resolve(baseDir, `${filePath}.ejs`);
44
+ if (fullPath !== baseDir && !fullPath.startsWith(baseDir + sep)) {
45
+ throw new Error(`Invalid template path "${filePath}".`);
46
+ }
47
+
48
+ return new Promise<TemplateFunction>((res, reject) => {
40
49
  // Get template from cache
41
50
  if (this.templates[filePath]) {
42
- resolve(this.templates[filePath]);
51
+ res(this.templates[filePath]);
43
52
  return;
44
53
  }
45
54
 
46
55
  // Get template file
47
- fs.readFile(
48
- `${join(this.configService.getFastButReadOnly('templates.path'), filePath)}.ejs`,
49
- { encoding: 'utf8' },
50
- (err, data) => {
51
- if (err) {
52
- reject(err);
53
- } else {
54
- // Compile and return template
55
- this.templates[filePath] = ejs.compile(data);
56
- resolve(this.templates[filePath]);
57
- }
58
- },
59
- );
56
+ fs.readFile(fullPath, { encoding: 'utf8' }, (err, data) => {
57
+ if (err) {
58
+ reject(err);
59
+ } else {
60
+ // Compile and return template
61
+ this.templates[filePath] = ejs.compile(data);
62
+ res(this.templates[filePath]);
63
+ }
64
+ });
60
65
  });
61
66
  }
62
67
  }