@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
@@ -0,0 +1,64 @@
1
+ # Hub Integration Checklist
2
+
3
+ The Hub is **config-only** — no files need to be created in the consuming project. It is auto-registered
4
+ by `CoreModule.forRoot()` when `hub` is present in the config.
5
+
6
+ ## Reference Implementation
7
+
8
+ - Local: `node_modules/@lenne.tech/nest-server/src/core/modules/hub/`
9
+ - The framework's own e2e config (`src/config.env.ts`) enables it in `local`/`development`/`e2e`/`ci`.
10
+
11
+ ## Quick Setup
12
+
13
+ ### 1. Enable per environment
14
+
15
+ **Edit:** `src/config.env.ts`
16
+
17
+ ```typescript
18
+ // development / local
19
+ hub: {
20
+ collectors: { queries: true }, // opt-in query profiler
21
+ mailbox: { mode: 'capture' }, // capture outgoing mail locally (Mailpit replacement)
22
+ },
23
+
24
+ // production — usually omit `hub` entirely, or (if you want it, still ADMIN-gated):
25
+ // hub: { collectors: { queries: false }, mailbox: false },
26
+ ```
27
+
28
+ That's it. Sign in as a user with `RoleEnum.ADMIN` and open `/hub`.
29
+
30
+ ## Optional Enhancements
31
+
32
+ | Want | Do |
33
+ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
34
+ | **Cron panel populated** | Import `ScheduleModule.forRoot()` in your ServerModule (usually already present). |
35
+ | **Query profiler** | Set `hub.collectors.queries: true` (opts the driver into `monitorCommands`). |
36
+ | **Migrations run/rollback** | Set `hub.migrations.dir` to your migrations directory (default `./migrations`). In compiled deployments point it at the built JS. Use `lockCollectionName` for cluster safety. |
37
+ | **Custom look / behavior** | Pass `overrides.hub.{controller,actionsController,service,htmlService,actionsService}` to `CoreModule.forRoot()`. |
38
+
39
+ ## Verification Checklist
40
+
41
+ - [ ] `pnpm run build` succeeds
42
+ - [ ] Sign in as ADMIN → `GET /hub` returns the dashboard HTML
43
+ - [ ] A non-admin user gets `403`; an anonymous request gets `401`
44
+ - [ ] `GET /hub/dashboard.json` returns build/memory/features
45
+ - [ ] `GET /hub/config.json` shows `***` for secrets (never the real values)
46
+ - [ ] Mutating actions without `X-Hub-Request: 1` return `403`
47
+
48
+ ## Common Mistakes
49
+
50
+ | Mistake | Symptom | Fix |
51
+ | ------------------------------------------------ | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
52
+ | `hub` not set in the target environment | `/hub` → 404 | Add `hub: true` (or an options object) to that env's config block |
53
+ | `mailbox.mode: 'capture'` in production | Startup error | Use `mode: 'copy'` or disable the mailbox in production — capture suppresses all mail |
54
+ | Query panel empty | queries collector off (default) | `hub.collectors.queries: true` |
55
+ | Cron panel empty | `ScheduleModule.forRoot()` not imported | Import it in your ServerModule |
56
+ | Hub path collides with a project route | 404 / wrong page | Set a distinct `hub.path` (e.g. `admin/hub`) |
57
+ | No roles guard registered (auth system disabled) | Every `/hub` sidecar/action returns 200 for anonymous requests | The Hub relies on the framework's `RolesGuard`/`BetterAuthRolesGuard` (registered by the auth/BetterAuth module) to enforce `@Roles(RoleEnum.ADMIN)`. If you disable BetterAuth AND legacy auth, no guard runs and the ADMIN gate is inert. Keep an auth module enabled, or do not expose the Hub. |
58
+
59
+ > **Auth dependency (why):** the Hub does not register its own guard — it assigns `@Roles(hub.roles)`
60
+ > (default `ADMIN`) as metadata and depends on the app-wide `RolesGuard` / `BetterAuthRolesGuard` to
61
+ > read it. That guard ships with the auth/BetterAuth modules. In a normal project one of them is
62
+ > active, so the gate works out of the box; but an app that runs with _no_ auth system has no guard
63
+ > to enforce the roles, and the sidecars/actions would be reachable unauthenticated. Either keep an
64
+ > auth module enabled or leave the Hub disabled in such a setup.
@@ -0,0 +1,159 @@
1
+ # Hub — Admin Area (Operator Cockpit)
2
+
3
+ A build-free, ADMIN-gated dashboard of runtime information and admin tools, served directly by the
4
+ framework. Inspired by the sister project [nest-base](https://github.com/lenneTech/nest-base)'s Hub,
5
+ but adapted to this stack (NestJS + GraphQL + Mongoose) and shipped as **dependency-free server-side
6
+ HTML + a vanilla-JS SPA** (no React, no build step) so it works identically in npm- and vendor-mode.
7
+
8
+ ## Enable it
9
+
10
+ The Hub is **never enabled implicitly** — switch it on per environment:
11
+
12
+ ```typescript
13
+ // config.env.ts
14
+ {
15
+ hub: true, // enabled at /hub, admin-only, default collectors
16
+ // or, with options:
17
+ hub: {
18
+ path: 'hub', // default 'hub'
19
+ collectors: { queries: true }, // query profiler is opt-in (enables driver command monitoring)
20
+ mailbox: { mode: 'capture' }, // built-in Mailpit-style mail capture (dev/test)
21
+ },
22
+ }
23
+ ```
24
+
25
+ Do NOT set `hub` in the `production` block unless you intend the cockpit to be reachable there
26
+ (it stays ADMIN-gated). See `IHubConfig` in `interfaces/hub-config.interface.ts` for every option.
27
+
28
+ ## Panels
29
+
30
+ | Panel | Route | Source |
31
+ | -------------------- | --------------------- | --------------------------------------------------------------- |
32
+ | Dashboard | `/hub` | health, build info, memory, feature matrix, links |
33
+ | Diagnostics | `/hub/diagnostics` | heap/rss, node/platform, collector buffer levels |
34
+ | Logs | `/hub/logs` | in-memory log ring buffer (redacted) |
35
+ | Request Traces | `/hub/traces` | HTTP timing middleware |
36
+ | Query Performance | `/hub/queries` | MongoDB driver command monitoring (opt-in) |
37
+ | Cron Jobs | `/hub/cron` | `@nestjs/schedule` `SchedulerRegistry` (optional) |
38
+ | Database | `/hub/db` | dbStats / per-collection collStats |
39
+ | Models / ERD | `/hub/models` | Mongoose schemas → Mermaid ER diagram |
40
+ | Migrations | `/hub/migrations` | `MigrationRunner` status + run/rollback |
41
+ | Files | `/hub/files` | GridFS listing + delete |
42
+ | Config | `/hub/config` | full config, secrets masked |
43
+ | Auth Migration | `/hub/auth-migration` | Legacy → IAM progress (BetterAuth, optional) |
44
+ | Routes / Permissions | `/hub/routes` | route + role + `@Restricted` map (Permissions module, optional) |
45
+ | Error Codes | `/hub/error-codes` | de/en catalog (ErrorCode module, optional) |
46
+ | Email Preview | `/hub/emails` | EJS templates rendered with sample data |
47
+ | Mailbox | `/hub/mailbox` | captured outgoing mail (Mailpit replacement) |
48
+ | AI | `/hub/ai` | AI usage summary (AI module, optional) |
49
+
50
+ Each panel has a `*.json` sidecar (the stable data contract) that the client polls. Optional sources
51
+ degrade to an "unavailable" state instead of erroring.
52
+
53
+ ## Actions (mutating)
54
+
55
+ Enabled by default (`actions: true`). Every mutating request requires the `X-Hub-Request: 1` header
56
+ (CSRF defense) and destructive ones a server-validated `confirm` keyword:
57
+
58
+ | Action | Endpoint | Confirm |
59
+ | ----------------------- | ------------------------------------------ | ------------ |
60
+ | Run pending migrations | `POST /hub/actions/migrations/run` | `RUN` |
61
+ | Rollback last migration | `POST /hub/actions/migrations/down` | `DOWN` |
62
+ | Delete GridFS file | `DELETE /hub/actions/files/:id` | the filename |
63
+ | Cron start/stop/trigger | `POST /hub/actions/cron/:name/:action` | the job name |
64
+ | Clear collector buffer | `POST /hub/actions/collectors/:name/clear` | `CLEAR` |
65
+ | Send test mail | `POST /hub/actions/email/test` | — |
66
+
67
+ Every action writes an audit line: `[HUB-ACTION] <action> by user <id>`.
68
+
69
+ ## Security
70
+
71
+ - **Auth**: the DATA sidecars (`*.json`) and all actions are `@Roles(RoleEnum.ADMIN)` (configurable
72
+ via `hub.roles`; `false` = public — dangerous). The **shell** (page routes) is public chrome only —
73
+ it shows a **login form** when the data is 401, so the Hub is self-sufficient: an admin can sign in
74
+ directly at the API (email/password → `loginEndpoint`, default `/iam/sign-in/email` → session cookie)
75
+ without the frontend, with a token-paste fallback for cookie-less setups. In fullstack, a
76
+ cross-subdomain session cookie from the app login already authenticates `/hub` — no separate login.
77
+ A **"Sign out"** button in the topbar POSTs to `hub.logoutEndpoint` (default `/iam/sign-out`),
78
+ clears the session cookie + any pasted token, and returns to the login gate. The logout endpoint is
79
+ delivered only in the ADMIN-gated `session.json` payload, never in the public shell.
80
+ - **Public-shell trade-off (by design):** because the shell must render before authentication, its
81
+ HTML source reveals the panel structure, the environment name (shown on the login card) and any
82
+ configured external links (`hub.links.*`) to an unauthenticated request. This is intentional — the
83
+ panel structure is already discoverable via the public `hub.js` — and carries no data: every
84
+ `*.json` sidecar and every action stays ADMIN-gated. Do not place secrets in `hub.links.*` (e.g.
85
+ an internal Mailpit URL) if the shell's origin is reachable by untrusted clients.
86
+ - **Action errors**: mutating actions return plain admin-facing messages (not `ErrorCode` catalog
87
+ entries) by design — the `ErrorCode` i18n module is optional and the Hub cannot hard-depend on it.
88
+ The messages are centralized in `hub-action-messages.ts` (`HubActionMessage`) so the wording stays
89
+ consistent and reviewable in one place.
90
+ - **CSP**: strict per-request nonce; no `unsafe-inline`; `X-Frame-Options: DENY`; `no-store`.
91
+ - **Secrets**: the config viewer deep-clones and masks by key pattern + `security.secretFields`.
92
+ - **Interceptor safety**: sidecars return pre-serialized JSON strings, so the global response
93
+ interceptors never walk/mutate live config.
94
+ - **Mailbox guard**: `mode: 'capture'` throws at startup in any **reachable** environment (anything
95
+ whose `env` is not `local`/`development`/`test`/`ci`/`e2e`) — it suppresses outgoing mail.
96
+ - **Public-access guard**: `roles: false` (no auth check — public config viewer, logs and destructive
97
+ actions) throws at startup in any **reachable** environment unless `hub.allowPublicAccessInProduction:
98
+ true` is set explicitly. This closes the single most dangerous Hub misconfiguration (a `roles: false`
99
+ copied from a local config into a reachable environment). The reachable check is fail-safe: it treats
100
+ every env name except the known local/test set as reachable, so a custom name (`prod`, `preprod`,
101
+ `staging-2`, …) cannot bypass it. Only acknowledge public access behind a fully-controlled network
102
+ boundary (VPN / IP allow-list / authenticating reverse proxy).
103
+ - **No-guard warning**: the Hub registers no guard of its own — its ADMIN gate is enforced by the
104
+ app-wide roles guard from BetterAuth (IAM) or the legacy Auth module. If the Hub is enabled and
105
+ gated but neither is active (and you have not registered your own `APP_GUARD`), `CoreModule` logs a
106
+ loud startup warning, because nothing would then enforce the gate.
107
+ - **CSRF**: mutating actions require the `X-Hub-Request` custom header, which forces a CORS preflight
108
+ that a restrictive allowlist rejects for foreign origins — **that header** (not the type-to-confirm
109
+ keyword, which is public UX safety, not a token) is the CSRF barrier. It holds with the default
110
+ `SameSite=Lax` session cookie or Bearer-token auth. Do NOT combine `cors.allowAll: true` with a
111
+ `SameSite=None` cookie (cross-subdomain fullstack): that lets any origin pass the preflight — pin
112
+ `cors` to your `appUrl` instead.
113
+ - **Redaction is best-effort (ADMIN-only surfaces)**: the config viewer masks by key pattern +
114
+ `security.secretFields` (a secret under an unusual key can slip); the logs/queries collectors and
115
+ the copy-mode mailbox redact patterned secrets (JWT / Bearer / `key=value` / cookie / reset-link
116
+ path tokens) but cannot catch an arbitrary value logged without a recognizable shape. All are
117
+ ADMIN-gated — treat Hub access as equivalent to config/log read access.
118
+
119
+ ### Routes / Permissions panel vs. the standalone `/permissions` endpoint
120
+
121
+ The **Routes / Permissions** panel renders the same data as the standalone permissions module — the
122
+ full security map (every route + its required roles + `@Restricted` field rules) — but _inside_ the
123
+ Hub, so it inherits the Hub's ADMIN gate, per-environment opt-in and strict CSP. It reuses the
124
+ permissions **scanner** (`CorePermissionsService`), so it needs the `permissions` module enabled
125
+ (`config.permissions`); otherwise the panel degrades to an "unavailable" state.
126
+
127
+ The two surfaces are **independent** and answer the common "how do I reach it?" questions:
128
+
129
+ | Situation | How to reach the report |
130
+ | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
131
+ | Hub **on**, `permissions` **on** | Hub → **Routes / Permissions** panel (ADMIN, login-gated by the Hub). The standalone `/permissions` also works per its own `role` (default ADMIN). |
132
+ | Hub **off**, `permissions` **on** (ADMIN) | The standalone `/permissions` is ADMIN-gated but has **no login page** — reach it with an ADMIN **session cookie** (be logged in via the app) or a **bearer token** (`Authorization: Bearer <jwt>` from `POST /iam/sign-in/email`), exactly like any other ADMIN endpoint. |
133
+ | Hub **off**, want frictionless **local** access | Set `permissions: { role: false }` — public, no auth. Legitimate **only** on a local, non-network-reachable machine, as a conscious opt-in. Never ship it to a reachable environment: the report is a reconnaissance goldmine. |
134
+ | `permissions` **off** | No report anywhere: `/permissions` is not registered (404) and the Hub panel shows "unavailable". |
135
+
136
+ **Security note:** the permissions report exposes your entire authorization model. Keep it ADMIN
137
+ (the default) everywhere it could be reachable; use `role: false` only behind a network boundary you
138
+ fully control. In production the framework's own config does not register the permissions module at all.
139
+
140
+ ## Collectors
141
+
142
+ Three in-memory ring buffers (fixed capacity, no timers, per-app-instance — parallel-test-safe):
143
+
144
+ - **Logs** — installs a chaining `Logger.overrideLogger()` delegate (no main.ts change), restored on shutdown.
145
+ - **Traces** — an Express middleware registered only when enabled (zero cost otherwise).
146
+ - **Queries** — MongoDB driver command monitoring; records value-free query SHAPES (N+1 templates), never values.
147
+ Enabling it opts the driver into `monitorCommands` from `core.module.ts`.
148
+
149
+ ## Overrides
150
+
151
+ ```typescript
152
+ CoreModule.forRoot(envConfig, {
153
+ hub: { service: MyHubService, htmlService: MyHubHtmlService },
154
+ });
155
+ ```
156
+
157
+ Fields: `controller`, `actionsController`, `service`, `htmlService`, `actionsService`.
158
+
159
+ See [INTEGRATION-CHECKLIST.md](./INTEGRATION-CHECKLIST.md) for setup in a consumer project.
@@ -0,0 +1,137 @@
1
+ import {
2
+ BadRequestException,
3
+ Body,
4
+ Controller,
5
+ Delete,
6
+ ForbiddenException,
7
+ Headers,
8
+ Param,
9
+ Post,
10
+ } from '@nestjs/common';
11
+
12
+ import { HubActionMessage } from './hub-action-messages';
13
+ import { CoreHubActionsService } from './services/core-hub-actions.service';
14
+
15
+ /** Body shape shared by the confirm-guarded actions. */
16
+ interface ConfirmBody {
17
+ confirm?: string;
18
+ }
19
+
20
+ /**
21
+ * The Hub's mutating actions (migrations, file delete, cron control, buffer clears, test mail).
22
+ *
23
+ * Registered as a SEPARATE controller only when `hub.actions !== false`, so disabling actions simply
24
+ * removes the routes (404). Path + roles metadata are assigned at runtime in `CoreHubModule.forRoot()`
25
+ * exactly like the read controller.
26
+ *
27
+ * Every mutating request must carry the `X-Hub-Request: 1` header (CSRF defense in depth: a custom
28
+ * header makes the request non-simple, so a cross-origin attempt triggers a CORS preflight the
29
+ * framework will not approve). Destructive actions additionally require a server-validated `confirm`
30
+ * keyword that the UI makes the operator type.
31
+ *
32
+ * Overridable via `overrides.hub.actionsController`.
33
+ */
34
+ @Controller()
35
+ export class CoreHubActionsController {
36
+ constructor(protected readonly actions: CoreHubActionsService) {}
37
+
38
+ @Post('actions/collectors/:name/clear')
39
+ async clearCollector(
40
+ @Param('name') name: string,
41
+ @Body() body: ConfirmBody,
42
+ @Headers('x-hub-request') hubHeader: string | undefined,
43
+ ): Promise<unknown> {
44
+ this.requireHubRequest(hubHeader);
45
+ this.requireConfirm(body, 'CLEAR');
46
+ if (!['logs', 'mailbox', 'queries', 'traces'].includes(name)) {
47
+ throw new BadRequestException(HubActionMessage.unknownCollector);
48
+ }
49
+ return this.wrap(() =>
50
+ Promise.resolve(this.actions.clearBuffer(name as 'logs' | 'mailbox' | 'queries' | 'traces')),
51
+ );
52
+ }
53
+
54
+ @Post('actions/cron/:name/:action')
55
+ async cron(
56
+ @Param('name') name: string,
57
+ @Param('action') action: string,
58
+ @Body() body: ConfirmBody,
59
+ @Headers('x-hub-request') hubHeader: string | undefined,
60
+ ): Promise<unknown> {
61
+ this.requireHubRequest(hubHeader);
62
+ if (!['start', 'stop', 'trigger'].includes(action)) {
63
+ throw new BadRequestException(HubActionMessage.unknownCronAction);
64
+ }
65
+ this.requireConfirm(body, name);
66
+ return this.wrap(() => Promise.resolve(this.actions.controlCron(name, action as 'start' | 'stop' | 'trigger')));
67
+ }
68
+
69
+ @Delete('actions/files/:id')
70
+ async deleteFile(
71
+ @Param('id') id: string,
72
+ @Body() body: ConfirmBody,
73
+ @Headers('x-hub-request') hubHeader: string | undefined,
74
+ ): Promise<unknown> {
75
+ this.requireHubRequest(hubHeader);
76
+ if (!body?.confirm) {
77
+ throw new BadRequestException(HubActionMessage.confirmationFilenameRequired);
78
+ }
79
+ return this.wrap(() => this.actions.deleteFile(id, body.confirm as string));
80
+ }
81
+
82
+ @Post('actions/email/test')
83
+ async emailTest(
84
+ @Body() body: { locale?: string; template?: string; to?: string },
85
+ @Headers('x-hub-request') hubHeader: string | undefined,
86
+ ): Promise<unknown> {
87
+ this.requireHubRequest(hubHeader);
88
+ if (!body?.to) {
89
+ throw new BadRequestException(HubActionMessage.recipientRequired);
90
+ }
91
+ return this.wrap(() => this.actions.sendTestEmail(body.to as string, body.template, body.locale));
92
+ }
93
+
94
+ @Post('actions/migrations/down')
95
+ async migrationsDown(
96
+ @Body() body: ConfirmBody,
97
+ @Headers('x-hub-request') hubHeader: string | undefined,
98
+ ): Promise<unknown> {
99
+ this.requireHubRequest(hubHeader);
100
+ this.requireConfirm(body, 'DOWN');
101
+ return this.wrap(() => this.actions.rollbackMigration());
102
+ }
103
+
104
+ @Post('actions/migrations/run')
105
+ async migrationsRun(
106
+ @Body() body: ConfirmBody,
107
+ @Headers('x-hub-request') hubHeader: string | undefined,
108
+ ): Promise<unknown> {
109
+ this.requireHubRequest(hubHeader);
110
+ this.requireConfirm(body, 'RUN');
111
+ return this.wrap(() => this.actions.runMigrations());
112
+ }
113
+
114
+ /** Validate the server-side confirmation keyword. */
115
+ protected requireConfirm(body: ConfirmBody, expected: string): void {
116
+ if (body?.confirm !== expected) {
117
+ throw new BadRequestException(HubActionMessage.confirmationKeywordMismatch(expected));
118
+ }
119
+ }
120
+
121
+ /** CSRF defense: mutating requests must carry the X-Hub-Request header. */
122
+ protected requireHubRequest(header: string | undefined): void {
123
+ if (!header) {
124
+ throw new ForbiddenException(HubActionMessage.missingHubRequestHeader);
125
+ }
126
+ }
127
+
128
+ /** Run an action, mapping domain errors to 400 (admin-facing plain message). */
129
+ protected async wrap(fn: () => Promise<unknown>): Promise<unknown> {
130
+ try {
131
+ const result = await fn();
132
+ return { ok: true, timestamp: new Date().toISOString(), ...(result as object) };
133
+ } catch (error) {
134
+ throw new BadRequestException(error instanceof Error ? error.message : HubActionMessage.actionFailed);
135
+ }
136
+ }
137
+ }
@@ -0,0 +1,135 @@
1
+ import { Inject, Injectable } from '@nestjs/common';
2
+
3
+ import { HUB_CONFIG } from './hub.constants';
4
+
5
+ /**
6
+ * Per-process cache-bust token for the client bundle (`hub.js?v=<token>`). It changes on every server
7
+ * start, so the browser refetches the script after a restart/redeploy (a dev code change is never
8
+ * masked by the immutable cache), while a single boot keeps the long-lived cache. The app version is
9
+ * deliberately NOT part of this public URL — it would leak the version to unauthenticated requests
10
+ * (the version is shown only in the ADMIN-gated, client-built topbar from session.json).
11
+ */
12
+ const HUB_ASSET_TOKEN = Date.now().toString(36);
13
+ import { getHubClientJs } from './helpers/hub-client-js.helper';
14
+ import { escapeHtml, escapeJsString, HUB_NONCE_PLACEHOLDER } from './helpers/hub-shell.helper';
15
+ import { ResolvedHubConfig } from './interfaces/hub-config.interface';
16
+
17
+ /**
18
+ * Builds the Hub's single, self-contained HTML shell. Every page route serves the same shell (the
19
+ * client router picks the active panel from the URL), so this is stateless apart from the injected
20
+ * config. All server-provided values are escaped; the auth token (when present) is embedded as a JS
21
+ * string so sidecar fetches carry it in cookie-less setups.
22
+ *
23
+ * Overridable: a project may replace it via `overrides.hub.htmlService` to re-skin the cockpit.
24
+ */
25
+ @Injectable()
26
+ export class CoreHubHtmlService {
27
+ constructor(@Inject(HUB_CONFIG) protected readonly config: ResolvedHubConfig) {}
28
+
29
+ /** The shared client runtime script — served verbatim from `GET /{hub}/hub.js`. */
30
+ buildClientScript(): string {
31
+ return getHubClientJs();
32
+ }
33
+
34
+ /**
35
+ * Build the full shell HTML with `__CSP_NONCE__` placeholders. The controller replaces the
36
+ * placeholders per request via `injectNonce()`.
37
+ *
38
+ * @param authHeader - the incoming `Authorization` header, embedded so client fetches can replay it
39
+ */
40
+ buildShell(authHeader?: string): string {
41
+ const base = '/' + this.config.path;
42
+
43
+ const bootstrap = [
44
+ `window.__HUB_BASE__='${escapeJsString(base)}';`,
45
+ `window.__HUB_POLL_MS__=${Number(this.config.pollIntervalMs) || 5000};`,
46
+ `window.__HUB_LOGIN__='${escapeJsString(this.config.loginEndpoint)}';`,
47
+ authHeader ? `window.__HUB_TOKEN__='${escapeJsString(authHeader)}';` : '',
48
+ ].join('');
49
+
50
+ // SECURITY (self-sufficient login): the shell is PUBLIC because it must render the login form
51
+ // before authentication. It therefore carries NO cockpit chrome — no navigation, no environment
52
+ // badge, no version, no external links. The client builds all of that ONLY after a successful
53
+ // ADMIN auth check, from the ADMIN-gated `session.json` payload. So an unauthenticated request to
54
+ // any /hub page reveals nothing of the Hub's structure; #hub-app stays empty until login.
55
+ return `<!DOCTYPE html>
56
+ <html lang="en">
57
+ <head>
58
+ <meta charset="UTF-8">
59
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
60
+ <title>Hub · nest-server</title>
61
+ <style nonce="${HUB_NONCE_PLACEHOLDER}">${this.css()}</style>
62
+ </head>
63
+ <body class="hub-locked">
64
+ <div id="hub-gate" class="hub-gate"></div>
65
+ <div id="hub-app"></div>
66
+ <dialog id="hub-confirm" class="hub-dialog"></dialog>
67
+ <script nonce="${HUB_NONCE_PLACEHOLDER}">${bootstrap}</script>
68
+ <script nonce="${HUB_NONCE_PLACEHOLDER}" src="${escapeHtml(base)}/hub.js?v=${HUB_ASSET_TOKEN}"></script>
69
+ </body>
70
+ </html>`;
71
+ }
72
+
73
+ protected css(): string {
74
+ return `
75
+ :root{--bg:#0b0d12;--panel:#141821;--panel2:#1b212d;--border:#242c3a;--text:#e6ebf2;--muted:#8b97a8;--accent:#4f8cff;--ok:#3fb950;--warn:#d29922;--err:#f85149}
76
+ *{box-sizing:border-box}
77
+ body{margin:0;background:var(--bg);color:var(--text);font:14px/1.5 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif}
78
+ .hub-layout{display:flex;min-height:100vh}
79
+ .hub-nav{width:230px;flex:0 0 230px;background:var(--panel);border-right:1px solid var(--border);padding:16px 10px;overflow-y:auto}
80
+ .hub-brand{font-weight:700;font-size:18px;padding:4px 10px 14px;display:flex;align-items:center;gap:8px}
81
+ .hub-env{font-size:10px;text-transform:uppercase;letter-spacing:.05em;padding:2px 6px;border-radius:4px;background:var(--panel2);color:var(--muted)}
82
+ .hub-env-production{background:#3d1418;color:#ff7b72}
83
+ .hub-nav h3{font-size:11px;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);margin:16px 10px 4px}
84
+ .hub-nav a{display:block;padding:6px 10px;border-radius:6px;color:var(--text);text-decoration:none;font-size:13px}
85
+ .hub-nav a:hover{background:var(--panel2)}
86
+ .hub-nav a.active{background:var(--accent);color:#fff}
87
+ .hub-nav a.hub-nav-disabled{opacity:.38;cursor:not-allowed}
88
+ .hub-nav a.hub-nav-disabled:hover{background:none}
89
+ .hub-nav a.hub-ext::after{content:' ↗';color:var(--muted)}
90
+ .hub-main{flex:1;min-width:0;display:flex;flex-direction:column}
91
+ .hub-topbar{display:flex;align-items:center;justify-content:space-between;padding:14px 22px;border-bottom:1px solid var(--border)}
92
+ .hub-topbar h1{font-size:18px;margin:0}
93
+ .hub-meta{color:var(--muted);font-size:12px;display:flex;align-items:center;gap:8px}
94
+ .hub-dot{width:9px;height:9px;border-radius:50%;display:inline-block;background:var(--muted)}
95
+ .hub-dot-ok{background:var(--ok)}.hub-dot-err{background:var(--err)}
96
+ .hub-content{padding:22px;overflow:auto}
97
+ .hub-tiles{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:12px;margin-bottom:20px}
98
+ .hub-tile{background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:14px}
99
+ .hub-tile-label{color:var(--muted);font-size:12px}
100
+ .hub-tile-value{font-size:22px;font-weight:600;margin-top:4px}
101
+ .hub-tile-value.ok{color:var(--ok)}.hub-tile-value.warn{color:var(--warn)}.hub-tile-value.err{color:var(--err)}
102
+ .hub-tile-sub{color:var(--muted);font-size:11px;margin-top:4px}
103
+ .hub-table{width:100%;border-collapse:collapse;background:var(--panel);border:1px solid var(--border);border-radius:8px;overflow:hidden}
104
+ .hub-table th{text-align:left;padding:8px 12px;background:var(--panel2);color:var(--muted);font-size:12px;font-weight:600;cursor:pointer}
105
+ .hub-table td{padding:8px 12px;border-top:1px solid var(--border);font-size:13px}
106
+ .hub-table tr:hover td{background:var(--panel2)}
107
+ .hub-json{background:var(--panel);border:1px solid var(--border);border-radius:8px;padding:14px;overflow:auto;font:12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace;white-space:pre;max-width:100%}
108
+ .hub-empty{color:var(--muted);padding:40px;text-align:center;border:1px dashed var(--border);border-radius:8px}
109
+ .hub-btn{background:var(--panel2);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:6px 12px;font-size:13px;cursor:pointer}
110
+ .hub-btn:hover{border-color:var(--accent)}
111
+ .hub-btn:disabled{opacity:.5;cursor:not-allowed}
112
+ .hub-btn-primary{background:var(--accent);border-color:var(--accent);color:#fff}
113
+ .hub-btn-danger{background:var(--err);border-color:var(--err);color:#fff}
114
+ .hub-signout{padding:4px 10px;font-size:12px;margin-left:4px}
115
+ .hub-input{width:100%;background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:8px;color:var(--text);margin:8px 0;font-size:13px}
116
+ .hub-dialog{background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:12px;padding:20px;max-width:440px;width:90%}
117
+ .hub-dialog::backdrop{background:rgba(0,0,0,.6)}
118
+ .hub-dialog h3{margin:0 0 8px}
119
+ .hub-dialog-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:12px}
120
+ .hub-hint{color:var(--muted);font-size:12px}
121
+ .hub-chip{display:inline-block;padding:1px 7px;border-radius:10px;font-size:11px;background:var(--panel2);color:var(--muted)}
122
+ .hub-chip-ok{background:#132d1c;color:var(--ok)}.hub-chip-warn{background:#2d2611;color:var(--warn)}.hub-chip-err{background:#3d1418;color:var(--err)}
123
+ .hub-toast{position:fixed;bottom:20px;right:20px;background:var(--panel2);border:1px solid var(--accent);color:var(--text);padding:12px 16px;border-radius:8px;font-size:13px;z-index:9999;box-shadow:0 6px 20px rgba(0,0,0,.4)}
124
+ .hub-toast-err{border-color:var(--err)}
125
+ /* Auth gate: nothing of the app is visible until authenticated. The client builds the layout into
126
+ #hub-app only after a successful auth check, so body.hub-locked just hides the (empty) app root. */
127
+ .hub-gate{display:none;position:fixed;inset:0;background:var(--bg);z-index:100;align-items:center;justify-content:center;padding:20px}
128
+ .hub-locked .hub-gate{display:flex}
129
+ .hub-locked #hub-app{display:none}
130
+ .hub-gate-card{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:28px;width:360px;max-width:100%}
131
+ .hub-gate-card h2{margin:0 0 4px}
132
+ .hub-gate-brand{font-weight:700;font-size:20px;margin-bottom:18px;display:flex;align-items:center;gap:8px}
133
+ `;
134
+ }
135
+ }