@lenne.tech/nest-server 11.31.2 → 11.32.0

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 (181) hide show
  1. package/.claude/rules/architecture.md +2 -0
  2. package/.claude/rules/configurable-features.md +2 -0
  3. package/.claude/rules/versioning.md +5 -8
  4. package/CLAUDE.md +31 -4
  5. package/FRAMEWORK-API.md +7 -2
  6. package/README.md +1 -0
  7. package/dist/config.env.d.ts +4 -0
  8. package/dist/config.env.js +32 -2
  9. package/dist/config.env.js.map +1 -1
  10. package/dist/core/common/helpers/logging.helper.d.ts +1 -0
  11. package/dist/core/common/helpers/logging.helper.js +12 -0
  12. package/dist/core/common/helpers/logging.helper.js.map +1 -1
  13. package/dist/core/common/helpers/meta.helper.d.ts +1 -0
  14. package/dist/core/common/helpers/meta.helper.js +32 -1
  15. package/dist/core/common/helpers/meta.helper.js.map +1 -1
  16. package/dist/core/common/helpers/process-diagnostics.helper.d.ts +18 -0
  17. package/dist/core/common/helpers/process-diagnostics.helper.js +88 -0
  18. package/dist/core/common/helpers/process-diagnostics.helper.js.map +1 -0
  19. package/dist/core/common/interfaces/server-options.interface.d.ts +12 -0
  20. package/dist/core/common/services/brevo.service.d.ts +7 -1
  21. package/dist/core/common/services/brevo.service.js +37 -16
  22. package/dist/core/common/services/brevo.service.js.map +1 -1
  23. package/dist/core/common/services/email.service.d.ts +3 -1
  24. package/dist/core/common/services/email.service.js +33 -2
  25. package/dist/core/common/services/email.service.js.map +1 -1
  26. package/dist/core/common/services/template.service.js +9 -4
  27. package/dist/core/common/services/template.service.js.map +1 -1
  28. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +9 -4
  29. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -1
  30. package/dist/core/modules/hub/core-hub-actions.controller.d.ts +22 -0
  31. package/dist/core/modules/hub/core-hub-actions.controller.js +141 -0
  32. package/dist/core/modules/hub/core-hub-actions.controller.js.map +1 -0
  33. package/dist/core/modules/hub/core-hub-html.service.d.ts +8 -0
  34. package/dist/core/modules/hub/core-hub-html.service.js +123 -0
  35. package/dist/core/modules/hub/core-hub-html.service.js.map +1 -0
  36. package/dist/core/modules/hub/core-hub.controller.d.ts +56 -0
  37. package/dist/core/modules/hub/core-hub.controller.js +398 -0
  38. package/dist/core/modules/hub/core-hub.controller.js.map +1 -0
  39. package/dist/core/modules/hub/core-hub.module.d.ts +17 -0
  40. package/dist/core/modules/hub/core-hub.module.js +109 -0
  41. package/dist/core/modules/hub/core-hub.module.js.map +1 -0
  42. package/dist/core/modules/hub/core-hub.service.d.ts +18 -0
  43. package/dist/core/modules/hub/core-hub.service.js +153 -0
  44. package/dist/core/modules/hub/core-hub.service.js.map +1 -0
  45. package/dist/core/modules/hub/helpers/hub-client-js.helper.d.ts +1 -0
  46. package/dist/core/modules/hub/helpers/hub-client-js.helper.js +755 -0
  47. package/dist/core/modules/hub/helpers/hub-client-js.helper.js.map +1 -0
  48. package/dist/core/modules/hub/helpers/hub-command-shape.helper.d.ts +1 -0
  49. package/dist/core/modules/hub/helpers/hub-command-shape.helper.js +33 -0
  50. package/dist/core/modules/hub/helpers/hub-command-shape.helper.js.map +1 -0
  51. package/dist/core/modules/hub/helpers/hub-mask.helper.d.ts +1 -0
  52. package/dist/core/modules/hub/helpers/hub-mask.helper.js +46 -0
  53. package/dist/core/modules/hub/helpers/hub-mask.helper.js.map +1 -0
  54. package/dist/core/modules/hub/helpers/hub-mermaid.helper.d.ts +10 -0
  55. package/dist/core/modules/hub/helpers/hub-mermaid.helper.js +35 -0
  56. package/dist/core/modules/hub/helpers/hub-mermaid.helper.js.map +1 -0
  57. package/dist/core/modules/hub/helpers/hub-shell.helper.d.ts +9 -0
  58. package/dist/core/modules/hub/helpers/hub-shell.helper.js +62 -0
  59. package/dist/core/modules/hub/helpers/hub-shell.helper.js.map +1 -0
  60. package/dist/core/modules/hub/hub-action-messages.d.ts +17 -0
  61. package/dist/core/modules/hub/hub-action-messages.js +21 -0
  62. package/dist/core/modules/hub/hub-action-messages.js.map +1 -0
  63. package/dist/core/modules/hub/hub-config.helper.d.ts +11 -0
  64. package/dist/core/modules/hub/hub-config.helper.js +164 -0
  65. package/dist/core/modules/hub/hub-config.helper.js.map +1 -0
  66. package/dist/core/modules/hub/hub-nav.d.ts +12 -0
  67. package/dist/core/modules/hub/hub-nav.js +43 -0
  68. package/dist/core/modules/hub/hub-nav.js.map +1 -0
  69. package/dist/core/modules/hub/hub-ring-buffer.d.ts +22 -0
  70. package/dist/core/modules/hub/hub-ring-buffer.js +59 -0
  71. package/dist/core/modules/hub/hub-ring-buffer.js.map +1 -0
  72. package/dist/core/modules/hub/hub.constants.d.ts +18 -0
  73. package/dist/core/modules/hub/hub.constants.js +22 -0
  74. package/dist/core/modules/hub/hub.constants.js.map +1 -0
  75. package/dist/core/modules/hub/index.d.ts +25 -0
  76. package/dist/core/modules/hub/index.js +42 -0
  77. package/dist/core/modules/hub/index.js.map +1 -0
  78. package/dist/core/modules/hub/interfaces/hub-config.interface.d.ts +115 -0
  79. package/dist/core/modules/hub/interfaces/hub-config.interface.js +3 -0
  80. package/dist/core/modules/hub/interfaces/hub-config.interface.js.map +1 -0
  81. package/dist/core/modules/hub/interfaces/hub-panels.interface.d.ts +231 -0
  82. package/dist/core/modules/hub/interfaces/hub-panels.interface.js +3 -0
  83. package/dist/core/modules/hub/interfaces/hub-panels.interface.js.map +1 -0
  84. package/dist/core/modules/hub/middleware/hub-trace.middleware.d.ts +8 -0
  85. package/dist/core/modules/hub/middleware/hub-trace.middleware.js +50 -0
  86. package/dist/core/modules/hub/middleware/hub-trace.middleware.js.map +1 -0
  87. package/dist/core/modules/hub/services/core-hub-actions.service.d.ts +48 -0
  88. package/dist/core/modules/hub/services/core-hub-actions.service.js +136 -0
  89. package/dist/core/modules/hub/services/core-hub-actions.service.js.map +1 -0
  90. package/dist/core/modules/hub/services/core-hub-db.service.d.ts +19 -0
  91. package/dist/core/modules/hub/services/core-hub-db.service.js +180 -0
  92. package/dist/core/modules/hub/services/core-hub-db.service.js.map +1 -0
  93. package/dist/core/modules/hub/services/core-hub-email.service.d.ts +24 -0
  94. package/dist/core/modules/hub/services/core-hub-email.service.js +148 -0
  95. package/dist/core/modules/hub/services/core-hub-email.service.js.map +1 -0
  96. package/dist/core/modules/hub/services/core-hub-mailbox.service.d.ts +20 -0
  97. package/dist/core/modules/hub/services/core-hub-mailbox.service.js +103 -0
  98. package/dist/core/modules/hub/services/core-hub-mailbox.service.js.map +1 -0
  99. package/dist/core/modules/hub/services/core-hub-migrations.service.d.ts +25 -0
  100. package/dist/core/modules/hub/services/core-hub-migrations.service.js +115 -0
  101. package/dist/core/modules/hub/services/core-hub-migrations.service.js.map +1 -0
  102. package/dist/core/modules/hub/services/core-hub-sources.service.d.ts +28 -0
  103. package/dist/core/modules/hub/services/core-hub-sources.service.js +187 -0
  104. package/dist/core/modules/hub/services/core-hub-sources.service.js.map +1 -0
  105. package/dist/core/modules/hub/services/hub-log-buffer.service.d.ts +24 -0
  106. package/dist/core/modules/hub/services/hub-log-buffer.service.js +210 -0
  107. package/dist/core/modules/hub/services/hub-log-buffer.service.js.map +1 -0
  108. package/dist/core/modules/hub/services/hub-query-profiler.service.d.ts +38 -0
  109. package/dist/core/modules/hub/services/hub-query-profiler.service.js +235 -0
  110. package/dist/core/modules/hub/services/hub-query-profiler.service.js.map +1 -0
  111. package/dist/core/modules/hub/services/hub-trace-buffer.service.d.ts +18 -0
  112. package/dist/core/modules/hub/services/hub-trace-buffer.service.js +123 -0
  113. package/dist/core/modules/hub/services/hub-trace-buffer.service.js.map +1 -0
  114. package/dist/core.module.js +42 -1
  115. package/dist/core.module.js.map +1 -1
  116. package/dist/index.d.ts +2 -0
  117. package/dist/index.js +2 -0
  118. package/dist/index.js.map +1 -1
  119. package/dist/main.js +5 -2
  120. package/dist/main.js.map +1 -1
  121. package/dist/tsconfig.build.tsbuildinfo +1 -1
  122. package/docs/REQUEST-LIFECYCLE.md +2 -0
  123. package/docs/brevo-manual-test.md +166 -0
  124. package/docs/security-overrides.md +90 -0
  125. package/migration-guides/11.31.2-to-11.31.3.md +135 -0
  126. package/migration-guides/11.31.3-to-11.32.0.md +254 -0
  127. package/package.json +16 -14
  128. package/src/config.env.ts +75 -2
  129. package/src/core/common/helpers/logging.helper.spec.ts +61 -0
  130. package/src/core/common/helpers/logging.helper.ts +48 -0
  131. package/src/core/common/helpers/meta.helper.ts +46 -1
  132. package/src/core/common/helpers/process-diagnostics.helper.spec.ts +310 -0
  133. package/src/core/common/helpers/process-diagnostics.helper.ts +321 -0
  134. package/src/core/common/interfaces/server-options.interface.ts +78 -0
  135. package/src/core/common/services/brevo.service.spec.ts +266 -0
  136. package/src/core/common/services/brevo.service.ts +100 -17
  137. package/src/core/common/services/email.service.ts +33 -1
  138. package/src/core/common/services/template.service.ts +21 -16
  139. package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +14 -4
  140. package/src/core/modules/hub/INTEGRATION-CHECKLIST.md +64 -0
  141. package/src/core/modules/hub/README.md +159 -0
  142. package/src/core/modules/hub/core-hub-actions.controller.ts +137 -0
  143. package/src/core/modules/hub/core-hub-html.service.ts +135 -0
  144. package/src/core/modules/hub/core-hub.controller.ts +286 -0
  145. package/src/core/modules/hub/core-hub.module.spec.ts +108 -0
  146. package/src/core/modules/hub/core-hub.module.ts +159 -0
  147. package/src/core/modules/hub/core-hub.service.ts +169 -0
  148. package/src/core/modules/hub/helpers/hub-client-js.helper.ts +768 -0
  149. package/src/core/modules/hub/helpers/hub-command-shape.helper.spec.ts +48 -0
  150. package/src/core/modules/hub/helpers/hub-command-shape.helper.ts +47 -0
  151. package/src/core/modules/hub/helpers/hub-mask.helper.spec.ts +67 -0
  152. package/src/core/modules/hub/helpers/hub-mask.helper.ts +78 -0
  153. package/src/core/modules/hub/helpers/hub-mermaid.helper.spec.ts +54 -0
  154. package/src/core/modules/hub/helpers/hub-mermaid.helper.ts +62 -0
  155. package/src/core/modules/hub/helpers/hub-shell.helper.spec.ts +106 -0
  156. package/src/core/modules/hub/helpers/hub-shell.helper.ts +90 -0
  157. package/src/core/modules/hub/hub-action-messages.ts +47 -0
  158. package/src/core/modules/hub/hub-config.helper.spec.ts +108 -0
  159. package/src/core/modules/hub/hub-config.helper.ts +233 -0
  160. package/src/core/modules/hub/hub-nav.ts +66 -0
  161. package/src/core/modules/hub/hub-ring-buffer.spec.ts +95 -0
  162. package/src/core/modules/hub/hub-ring-buffer.ts +101 -0
  163. package/src/core/modules/hub/hub.constants.ts +84 -0
  164. package/src/core/modules/hub/index.ts +25 -0
  165. package/src/core/modules/hub/interfaces/hub-config.interface.ts +265 -0
  166. package/src/core/modules/hub/interfaces/hub-panels.interface.ts +186 -0
  167. package/src/core/modules/hub/middleware/hub-trace.middleware.ts +45 -0
  168. package/src/core/modules/hub/services/core-hub-actions.service.ts +133 -0
  169. package/src/core/modules/hub/services/core-hub-db.service.ts +185 -0
  170. package/src/core/modules/hub/services/core-hub-email.service.ts +158 -0
  171. package/src/core/modules/hub/services/core-hub-mailbox.service.spec.ts +116 -0
  172. package/src/core/modules/hub/services/core-hub-mailbox.service.ts +121 -0
  173. package/src/core/modules/hub/services/core-hub-migrations.service.ts +112 -0
  174. package/src/core/modules/hub/services/core-hub-sources.service.ts +194 -0
  175. package/src/core/modules/hub/services/hub-log-buffer.service.ts +252 -0
  176. package/src/core/modules/hub/services/hub-query-profiler.service.ts +274 -0
  177. package/src/core/modules/hub/services/hub-trace-buffer.service.spec.ts +112 -0
  178. package/src/core/modules/hub/services/hub-trace-buffer.service.ts +134 -0
  179. package/src/core.module.ts +61 -1
  180. package/src/index.ts +7 -0
  181. package/src/main.ts +22 -3
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.32.0",
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,20 +23,21 @@
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 run spectaql:sync && 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 run spectaql:sync && 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 spectaql:sync && 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
+ "spectaql:sync": "node extras/update-spectaql-version.mjs",
31
32
  "cf": "pnpm run check:fix",
32
33
  "cnaf": "pnpm run check:naf",
33
34
  "docs": "pnpm run docs:ci && open http://127.0.0.1:8080/ && open ./public/index.html && compodoc -p tsconfig.json -s ",
34
35
  "docs:bootstrap": "node extras/update-spectaql-version.mjs && node scripts/run-spectaql.mjs",
35
36
  "docs:ci": "ts-node ./scripts/init-server.ts && pnpm run docs:bootstrap && compodoc -p tsconfig.json",
36
- "format": "oxfmt --write src/",
37
- "format:check": "oxfmt --check src/",
38
- "lint": "oxlint --ignore-path .oxlintignore src/ tests/",
39
- "lint:fix": "oxlint --fix --fix-suggestions --ignore-path .oxlintignore src/ tests/",
37
+ "format": "oxfmt --write src/ scripts/",
38
+ "format:check": "oxfmt --check src/ scripts/",
39
+ "lint": "oxlint --ignore-path .oxlintignore src/ tests/ scripts/",
40
+ "lint:fix": "oxlint --fix --fix-suggestions --ignore-path .oxlintignore src/ tests/ scripts/",
40
41
  "prestart:prod": "pnpm run build",
41
42
  "reinit": "rimraf pnpm-lock.yaml && rimraf node_modules && pnpm install && pnpm run lint && pnpm run test:e2e && pnpm run test:ci && pnpm run build",
42
43
  "reinit:clean": "rimraf pnpm-lock.yaml && rimraf node_modules && pnpm store prune && pnpm install && pnpm run test:e2e && pnpm run build",
@@ -65,6 +66,7 @@
65
66
  "vitest:unit:cov": "vitest run --coverage --config vitest.config.ts",
66
67
  "test:unit:watch": "vitest --config vitest.config.ts",
67
68
  "test:types": "tsc --noEmit --skipLibCheck -p tests/types/tsconfig.json",
69
+ "typecheck:tests": "tsc --noEmit -p tsconfig.tests.json",
68
70
  "test:cleanup": "find tests -type f \\( -name '*.txt' -o -name '*.bin' \\) -not -name '.gitkeep' -delete && echo 'Test artifacts cleaned up'",
69
71
  "watch": "npm-watch"
70
72
  },
@@ -84,7 +86,7 @@
84
86
  "@apollo/server": "5.5.1",
85
87
  "@as-integrations/express5": "1.1.2",
86
88
  "@better-auth/passkey": "1.6.23",
87
- "@getbrevo/brevo": "3.0.4",
89
+ "@getbrevo/brevo": "6.0.2",
88
90
  "@modelcontextprotocol/sdk": "1.29.0",
89
91
  "@nestjs/apollo": "13.4.2",
90
92
  "@nestjs/common": "11.1.28",
@@ -98,7 +100,7 @@
98
100
  "@nestjs/swagger": "11.4.6",
99
101
  "@nestjs/terminus": "11.1.1",
100
102
  "@tus/file-store": "2.1.0",
101
- "@tus/server": "2.4.1",
103
+ "@tus/server": "2.4.2",
102
104
  "@types/supertest": "7.2.1",
103
105
  "bcrypt": "6.0.0",
104
106
  "better-auth": "1.6.23",
@@ -116,11 +118,11 @@
116
118
  "graphql-upload": "15.0.2",
117
119
  "graphql-ws": "6.1.0",
118
120
  "jose": "6.2.3",
119
- "js-sha256": "0.11.1",
121
+ "js-sha256": "0.12.0",
120
122
  "json-to-graphql-query": "2.3.0",
121
123
  "lodash": "4.18.1",
122
- "mongodb": "7.2.0",
123
- "mongoose": "9.7.4",
124
+ "mongodb": "7.5.0",
125
+ "mongoose": "9.8.0",
124
126
  "multer": "2.2.0",
125
127
  "node-mailjet": "6.0.11",
126
128
  "nodemailer": "9.0.3",
@@ -140,7 +142,7 @@
140
142
  "@nestjs/schematics": "11.1.0",
141
143
  "@nestjs/testing": "11.1.28",
142
144
  "@swc/cli": "0.8.1",
143
- "@swc/core": "1.15.43",
145
+ "@swc/core": "1.15.46",
144
146
  "@types/compression": "1.8.1",
145
147
  "@types/cookie-parser": "1.4.10",
146
148
  "@types/ejs": "3.1.5",
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
+ }