@lenne.tech/nest-server 11.32.3 → 11.33.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 (262) hide show
  1. package/.claude/rules/architecture.md +16 -0
  2. package/.claude/rules/configurable-features.md +11 -3
  3. package/.claude/rules/testing.md +101 -1
  4. package/CLAUDE.md +5 -0
  5. package/FRAMEWORK-API.md +58 -3
  6. package/bin/migrate.js +13 -3
  7. package/dist/core/common/helpers/content-disposition.helper.d.ts +1 -0
  8. package/dist/core/common/helpers/content-disposition.helper.js +14 -0
  9. package/dist/core/common/helpers/content-disposition.helper.js.map +1 -0
  10. package/dist/core/common/helpers/file.helper.d.ts +21 -2
  11. package/dist/core/common/helpers/file.helper.js +69 -13
  12. package/dist/core/common/helpers/file.helper.js.map +1 -1
  13. package/dist/core/common/helpers/graceful-shutdown.helper.d.ts +2 -0
  14. package/dist/core/common/helpers/graceful-shutdown.helper.js +55 -0
  15. package/dist/core/common/helpers/graceful-shutdown.helper.js.map +1 -0
  16. package/dist/core/common/helpers/project-name.helper.d.ts +2 -0
  17. package/dist/core/common/helpers/project-name.helper.js +38 -0
  18. package/dist/core/common/helpers/project-name.helper.js.map +1 -0
  19. package/dist/core/common/interfaces/cron-job-config.interface.d.ts +1 -0
  20. package/dist/core/common/interfaces/server-options.interface.d.ts +39 -0
  21. package/dist/core/common/services/core-cron-jobs.initializer.d.ts +10 -0
  22. package/dist/core/common/services/core-cron-jobs.initializer.js +46 -0
  23. package/dist/core/common/services/core-cron-jobs.initializer.js.map +1 -0
  24. package/dist/core/common/services/core-cron-jobs.registry.d.ts +8 -0
  25. package/dist/core/common/services/core-cron-jobs.registry.js +12 -0
  26. package/dist/core/common/services/core-cron-jobs.registry.js.map +1 -0
  27. package/dist/core/common/services/core-cron-jobs.service.d.ts +37 -7
  28. package/dist/core/common/services/core-cron-jobs.service.js +278 -27
  29. package/dist/core/common/services/core-cron-jobs.service.js.map +1 -1
  30. package/dist/core/common/services/core-redis-pubsub.d.ts +20 -0
  31. package/dist/core/common/services/core-redis-pubsub.js +112 -0
  32. package/dist/core/common/services/core-redis-pubsub.js.map +1 -0
  33. package/dist/core/common/services/core-redis.service.d.ts +25 -0
  34. package/dist/core/common/services/core-redis.service.js +154 -0
  35. package/dist/core/common/services/core-redis.service.js.map +1 -0
  36. package/dist/core/common/services/core-s3.service.d.ts +51 -0
  37. package/dist/core/common/services/core-s3.service.js +261 -0
  38. package/dist/core/common/services/core-s3.service.js.map +1 -0
  39. package/dist/core/common/services/core-trust-proxy.initializer.d.ts +16 -0
  40. package/dist/core/common/services/core-trust-proxy.initializer.js +77 -0
  41. package/dist/core/common/services/core-trust-proxy.initializer.js.map +1 -0
  42. package/dist/core/common/services/rate-limit-store.d.ts +61 -0
  43. package/dist/core/common/services/rate-limit-store.js +235 -0
  44. package/dist/core/common/services/rate-limit-store.js.map +1 -0
  45. package/dist/core/modules/ai/core-ai-mcp.controller.d.ts +19 -3
  46. package/dist/core/modules/ai/core-ai-mcp.controller.js +148 -18
  47. package/dist/core/modules/ai/core-ai-mcp.controller.js.map +1 -1
  48. package/dist/core/modules/ai/services/core-ai.service.d.ts +7 -2
  49. package/dist/core/modules/ai/services/core-ai.service.js +24 -20
  50. package/dist/core/modules/ai/services/core-ai.service.js.map +1 -1
  51. package/dist/core/modules/auth/core-auth.module.js +4 -1
  52. package/dist/core/modules/auth/core-auth.module.js.map +1 -1
  53. package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.d.ts +1 -1
  54. package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js +3 -14
  55. package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js.map +1 -1
  56. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.d.ts +12 -10
  57. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js +30 -59
  58. package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js.map +1 -1
  59. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.d.ts +8 -2
  60. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +118 -45
  61. package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -1
  62. package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.d.ts +1 -1
  63. package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js +2 -11
  64. package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js.map +1 -1
  65. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.d.ts +12 -10
  66. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js +30 -85
  67. package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js.map +1 -1
  68. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +0 -1
  69. package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
  70. package/dist/core/modules/file/core-file.controller.d.ts +15 -3
  71. package/dist/core/modules/file/core-file.controller.js +90 -20
  72. package/dist/core/modules/file/core-file.controller.js.map +1 -1
  73. package/dist/core/modules/file/core-file.resolver.d.ts +4 -4
  74. package/dist/core/modules/file/core-file.resolver.js +23 -16
  75. package/dist/core/modules/file/core-file.resolver.js.map +1 -1
  76. package/dist/core/modules/file/core-file.service.d.ts +41 -7
  77. package/dist/core/modules/file/core-file.service.js +227 -7
  78. package/dist/core/modules/file/core-file.service.js.map +1 -1
  79. package/dist/core/modules/file/file-metadata.helper.d.ts +16 -0
  80. package/dist/core/modules/file/file-metadata.helper.js +33 -0
  81. package/dist/core/modules/file/file-metadata.helper.js.map +1 -0
  82. package/dist/core/modules/file/file-roles.helper.d.ts +4 -0
  83. package/dist/core/modules/file/file-roles.helper.js +53 -0
  84. package/dist/core/modules/file/file-roles.helper.js.map +1 -0
  85. package/dist/core/modules/file/file-storage.helper.d.ts +13 -0
  86. package/dist/core/modules/file/file-storage.helper.js +65 -0
  87. package/dist/core/modules/file/file-storage.helper.js.map +1 -0
  88. package/dist/core/modules/file/filesystem-file.helper.d.ts +22 -0
  89. package/dist/core/modules/file/filesystem-file.helper.js +77 -0
  90. package/dist/core/modules/file/filesystem-file.helper.js.map +1 -0
  91. package/dist/core/modules/file/interfaces/file-service-options.interface.d.ts +1 -0
  92. package/dist/core/modules/file/interfaces/file-upload.interface.d.ts +7 -1
  93. package/dist/core/modules/file/s3-file.helper.d.ts +37 -0
  94. package/dist/core/modules/file/s3-file.helper.js +81 -0
  95. package/dist/core/modules/file/s3-file.helper.js.map +1 -0
  96. package/dist/core/modules/hub/core-hub.controller.d.ts +8 -8
  97. package/dist/core/modules/hub/core-hub.controller.js +24 -22
  98. package/dist/core/modules/hub/core-hub.controller.js.map +1 -1
  99. package/dist/core/modules/hub/helpers/hub-mask.helper.js +2 -2
  100. package/dist/core/modules/hub/helpers/hub-mask.helper.js.map +1 -1
  101. package/dist/core/modules/hub/hub-buffer.d.ts +23 -0
  102. package/dist/core/modules/hub/hub-buffer.js +101 -0
  103. package/dist/core/modules/hub/hub-buffer.js.map +1 -0
  104. package/dist/core/modules/hub/services/core-hub-mailbox.service.d.ts +7 -5
  105. package/dist/core/modules/hub/services/core-hub-mailbox.service.js +14 -10
  106. package/dist/core/modules/hub/services/core-hub-mailbox.service.js.map +1 -1
  107. package/dist/core/modules/hub/services/hub-log-buffer.service.d.ts +5 -2
  108. package/dist/core/modules/hub/services/hub-log-buffer.service.js +17 -8
  109. package/dist/core/modules/hub/services/hub-log-buffer.service.js.map +1 -1
  110. package/dist/core/modules/hub/services/hub-query-profiler.service.d.ts +4 -2
  111. package/dist/core/modules/hub/services/hub-query-profiler.service.js +13 -7
  112. package/dist/core/modules/hub/services/hub-query-profiler.service.js.map +1 -1
  113. package/dist/core/modules/hub/services/hub-trace-buffer.service.d.ts +4 -2
  114. package/dist/core/modules/hub/services/hub-trace-buffer.service.js +13 -8
  115. package/dist/core/modules/hub/services/hub-trace-buffer.service.js.map +1 -1
  116. package/dist/core/modules/migrate/cli/migrate-cli.d.ts +3 -1
  117. package/dist/core/modules/migrate/cli/migrate-cli.js +29 -4
  118. package/dist/core/modules/migrate/cli/migrate-cli.js.map +1 -1
  119. package/dist/core/modules/migrate/helpers/migration.helper.d.ts +3 -6
  120. package/dist/core/modules/migrate/helpers/migration.helper.js +55 -8
  121. package/dist/core/modules/migrate/helpers/migration.helper.js.map +1 -1
  122. package/dist/core/modules/migrate/migration-runner.d.ts +1 -0
  123. package/dist/core/modules/migrate/migration-runner.js +4 -0
  124. package/dist/core/modules/migrate/migration-runner.js.map +1 -1
  125. package/dist/core/modules/migrate/mongo-state-store.d.ts +1 -0
  126. package/dist/core/modules/migrate/mongo-state-store.js +98 -9
  127. package/dist/core/modules/migrate/mongo-state-store.js.map +1 -1
  128. package/dist/core/modules/system-setup/core-system-setup.service.d.ts +2 -0
  129. package/dist/core/modules/system-setup/core-system-setup.service.js +31 -1
  130. package/dist/core/modules/system-setup/core-system-setup.service.js.map +1 -1
  131. package/dist/core/modules/tenant/core-tenant.guard.d.ts +17 -3
  132. package/dist/core/modules/tenant/core-tenant.guard.js +92 -15
  133. package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
  134. package/dist/core/modules/tenant/core-tenant.module.js +3 -4
  135. package/dist/core/modules/tenant/core-tenant.module.js.map +1 -1
  136. package/dist/core/modules/tus/core-tus.controller.d.ts +2 -0
  137. package/dist/core/modules/tus/core-tus.controller.js +29 -3
  138. package/dist/core/modules/tus/core-tus.controller.js.map +1 -1
  139. package/dist/core/modules/tus/core-tus.service.d.ts +24 -1
  140. package/dist/core/modules/tus/core-tus.service.js +182 -28
  141. package/dist/core/modules/tus/core-tus.service.js.map +1 -1
  142. package/dist/core/modules/tus/interfaces/tus-config.interface.js +3 -0
  143. package/dist/core/modules/tus/interfaces/tus-config.interface.js.map +1 -1
  144. package/dist/core/modules/tus/tus-redis-locker.d.ts +25 -0
  145. package/dist/core/modules/tus/tus-redis-locker.js +87 -0
  146. package/dist/core/modules/tus/tus-redis-locker.js.map +1 -0
  147. package/dist/core/modules/tus/tus.module.d.ts +1 -0
  148. package/dist/core/modules/tus/tus.module.js +30 -4
  149. package/dist/core/modules/tus/tus.module.js.map +1 -1
  150. package/dist/core.module.js +18 -1
  151. package/dist/core.module.js.map +1 -1
  152. package/dist/index.d.ts +10 -0
  153. package/dist/index.js +10 -0
  154. package/dist/index.js.map +1 -1
  155. package/dist/main.js +2 -1
  156. package/dist/main.js.map +1 -1
  157. package/dist/server/modules/file/file.controller.js.map +1 -1
  158. package/dist/server/modules/file/file.resolver.js +1 -12
  159. package/dist/server/modules/file/file.resolver.js.map +1 -1
  160. package/dist/server/modules/file/file.service.d.ts +5 -1
  161. package/dist/server/modules/file/file.service.js +12 -3
  162. package/dist/server/modules/file/file.service.js.map +1 -1
  163. package/dist/server/modules/user/avatar.controller.d.ts +5 -1
  164. package/dist/server/modules/user/avatar.controller.js +26 -10
  165. package/dist/server/modules/user/avatar.controller.js.map +1 -1
  166. package/dist/server/modules/user/user.model.js +3 -2
  167. package/dist/server/modules/user/user.model.js.map +1 -1
  168. package/dist/server/modules/user/user.module.js +6 -2
  169. package/dist/server/modules/user/user.module.js.map +1 -1
  170. package/dist/server/modules/user/user.service.d.ts +1 -1
  171. package/dist/server/modules/user/user.service.js +6 -13
  172. package/dist/server/modules/user/user.service.js.map +1 -1
  173. package/dist/tsconfig.build.tsbuildinfo +1 -1
  174. package/docs/REQUEST-LIFECYCLE.md +57 -8
  175. package/docs/security-overrides.md +30 -5
  176. package/migration-guides/11.32.3-to-11.32.4.md +335 -0
  177. package/migration-guides/11.32.x-to-11.33.x.md +1551 -0
  178. package/package.json +74 -42
  179. package/src/core/common/helpers/content-disposition.helper.ts +90 -0
  180. package/src/core/common/helpers/file.helper.ts +201 -20
  181. package/src/core/common/helpers/graceful-shutdown.helper.ts +116 -0
  182. package/src/core/common/helpers/project-name.helper.ts +71 -0
  183. package/src/core/common/interfaces/cron-job-config.interface.ts +16 -0
  184. package/src/core/common/interfaces/server-options.interface.ts +439 -2
  185. package/src/core/common/services/core-cron-jobs.initializer.ts +39 -0
  186. package/src/core/common/services/core-cron-jobs.registry.ts +45 -0
  187. package/src/core/common/services/core-cron-jobs.service.ts +589 -47
  188. package/src/core/common/services/core-redis-pubsub.ts +183 -0
  189. package/src/core/common/services/core-redis.service.ts +249 -0
  190. package/src/core/common/services/core-s3.service.ts +465 -0
  191. package/src/core/common/services/core-trust-proxy.initializer.ts +112 -0
  192. package/src/core/common/services/rate-limit-store.ts +492 -0
  193. package/src/core/modules/ai/README.md +25 -0
  194. package/src/core/modules/ai/core-ai-mcp.controller.ts +253 -22
  195. package/src/core/modules/ai/services/core-ai.service.ts +33 -24
  196. package/src/core/modules/auth/core-auth.module.ts +7 -1
  197. package/src/core/modules/auth/guards/legacy-auth-rate-limit.guard.ts +12 -21
  198. package/src/core/modules/auth/services/legacy-auth-rate-limiter.service.ts +49 -93
  199. package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +202 -66
  200. package/src/core/modules/better-auth/core-better-auth-rate-limit.middleware.ts +11 -18
  201. package/src/core/modules/better-auth/core-better-auth-rate-limiter.service.ts +51 -132
  202. package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +7 -1
  203. package/src/core/modules/file/INTEGRATION-CHECKLIST.md +108 -0
  204. package/src/core/modules/file/README.md +324 -41
  205. package/src/core/modules/file/core-file.controller.ts +267 -19
  206. package/src/core/modules/file/core-file.resolver.ts +46 -12
  207. package/src/core/modules/file/core-file.service.ts +482 -16
  208. package/src/core/modules/file/file-metadata.helper.ts +142 -0
  209. package/src/core/modules/file/file-roles.helper.ts +110 -0
  210. package/src/core/modules/file/file-storage.helper.ts +163 -0
  211. package/src/core/modules/file/filesystem-file.helper.ts +184 -0
  212. package/src/core/modules/file/interfaces/file-service-options.interface.ts +14 -0
  213. package/src/core/modules/file/interfaces/file-upload.interface.ts +17 -1
  214. package/src/core/modules/file/s3-file.helper.ts +209 -0
  215. package/src/core/modules/hub/README.md +48 -0
  216. package/src/core/modules/hub/core-hub.controller.ts +18 -16
  217. package/src/core/modules/hub/helpers/hub-mask.helper.ts +21 -4
  218. package/src/core/modules/hub/hub-buffer.ts +198 -0
  219. package/src/core/modules/hub/services/core-hub-mailbox.service.ts +22 -12
  220. package/src/core/modules/hub/services/hub-log-buffer.service.ts +24 -8
  221. package/src/core/modules/hub/services/hub-query-profiler.service.ts +9 -6
  222. package/src/core/modules/hub/services/hub-trace-buffer.service.ts +14 -9
  223. package/src/core/modules/migrate/README.md +68 -6
  224. package/src/core/modules/migrate/cli/migrate-cli.ts +69 -6
  225. package/src/core/modules/migrate/helpers/migration.helper.ts +144 -10
  226. package/src/core/modules/migrate/migration-runner.ts +10 -1
  227. package/src/core/modules/migrate/mongo-state-store.ts +179 -11
  228. package/src/core/modules/system-setup/README.md +7 -2
  229. package/src/core/modules/system-setup/core-system-setup.service.ts +95 -1
  230. package/src/core/modules/tenant/README.md +21 -1
  231. package/src/core/modules/tenant/core-tenant.guard.ts +135 -14
  232. package/src/core/modules/tenant/core-tenant.module.ts +8 -4
  233. package/src/core/modules/tus/INTEGRATION-CHECKLIST.md +93 -21
  234. package/src/core/modules/tus/README.md +35 -2
  235. package/src/core/modules/tus/core-tus.controller.ts +64 -12
  236. package/src/core/modules/tus/core-tus.service.ts +358 -34
  237. package/src/core/modules/tus/interfaces/tus-config.interface.ts +9 -0
  238. package/src/core/modules/tus/tus-redis-locker.ts +139 -0
  239. package/src/core/modules/tus/tus.module.ts +78 -6
  240. package/src/core.module.ts +26 -1
  241. package/src/index.ts +10 -0
  242. package/src/main.ts +7 -1
  243. package/src/server/modules/file/file.controller.ts +7 -3
  244. package/src/server/modules/file/file.resolver.ts +5 -17
  245. package/src/server/modules/file/file.service.ts +42 -3
  246. package/src/server/modules/user/avatar.controller.ts +39 -9
  247. package/src/server/modules/user/user.model.ts +9 -3
  248. package/src/server/modules/user/user.module.ts +12 -3
  249. package/src/server/modules/user/user.service.ts +20 -17
  250. package/src/test/README.md +7 -2
  251. package/src/core/common/helpers/logging.helper.spec.ts +0 -61
  252. package/src/core/common/helpers/process-diagnostics.helper.spec.ts +0 -310
  253. package/src/core/common/services/brevo.service.spec.ts +0 -266
  254. package/src/core/modules/hub/core-hub.module.spec.ts +0 -108
  255. package/src/core/modules/hub/helpers/hub-command-shape.helper.spec.ts +0 -48
  256. package/src/core/modules/hub/helpers/hub-mask.helper.spec.ts +0 -67
  257. package/src/core/modules/hub/helpers/hub-mermaid.helper.spec.ts +0 -61
  258. package/src/core/modules/hub/helpers/hub-shell.helper.spec.ts +0 -106
  259. package/src/core/modules/hub/hub-config.helper.spec.ts +0 -108
  260. package/src/core/modules/hub/hub-ring-buffer.spec.ts +0 -95
  261. package/src/core/modules/hub/services/core-hub-mailbox.service.spec.ts +0 -116
  262. package/src/core/modules/hub/services/hub-trace-buffer.service.spec.ts +0 -112
@@ -38,6 +38,19 @@ Key areas: JWT, MongoDB, GraphQL, email, security, static assets
38
38
  | **Security** | Response/security interceptors, input validation pipes |
39
39
  | **Scalars** | Custom GraphQL scalars (Date, JSON, Any) |
40
40
  | **Services** | CRUD operations, email (Mailjet/SMTP), template rendering |
41
+ | **Central infrastructure (optional)** | `CoreRedisService` + `CoreRedisPubSub` (`redis` config), `CoreS3Service` (`s3` config), `RateLimitStore`, the `installGracefulShutdown()` helper. All inert without their config — see below |
42
+
43
+ ### Optional central infrastructure
44
+
45
+ `CoreRedisService` and `CoreS3Service` are always provided and exported by `CoreModule`, but stay
46
+ **inert unless configured** (`redis` / `s3`, "presence implies enabled"). Their client libraries are
47
+ OPTIONAL peer dependencies, lazy-imported at bootstrap, so a project that uses neither installs
48
+ nothing extra. They exist so process-local state can become shared state when a deployment runs more
49
+ than one replica: rate-limit counters, cron deduplication, GraphQL subscriptions, tenant-cache
50
+ invalidation, Hub collectors and file storage all pick the distributed path automatically when the
51
+ config is present, and keep their previous single-process behaviour when it is not. Inject them with
52
+ `@Optional()` and always keep the non-configured fallback path — that is the pattern every core
53
+ consumer follows. See `.claude/rules/configurable-features.md` for the full option reference.
41
54
 
42
55
  ## Core Modules (`src/core/modules/`)
43
56
 
@@ -98,6 +111,8 @@ Whether such a cycle throws depends on **which module the graph is entered throu
98
111
 
99
112
  Repo-wide cycles went from **10 → 5**, and **every DI token in `src/core/` now lives in an import-free leaf**. The five that remain are, per an SWC-emit audit, **not runtime cycles at all** — type-only imports that madge reports but both compilers erase (their emits are empty).
100
113
 
114
+ Since 11.33.0 madge reports a **sixth**: `server/modules/user/user.module.ts ↔ server/modules/file/file.module.ts`. That one IS a real runtime cycle, and it is deliberate — the avatar upload needs `FileService` to reach the central file storage while `FileModule` already imported `UserModule`. It is a NestJS *module* cycle, not a TDZ hazard: both sides declare it with `forwardRef()`, and nothing on either side dereferences the other at class-definition time. It lives in `src/core/`-external `src/server/`, which vendor consumers do not receive.
115
+
101
116
  Both invariants are enforced by `tests/unit/import-cycle-invariants.spec.ts`, which fails if a token reappears in a `*.module.ts` / `*.service.ts` or if a leaf grows an import. That matters, because the guard below catches the *crash*, not the *disarming* of a safety property — those are different things, and only the second one is silent.
102
117
 
103
118
  | Module | Token / type leaf |
@@ -107,6 +122,7 @@ Both invariants are enforced by `tests/unit/import-cycle-invariants.spec.ts`, wh
107
122
  | `tus` | `tus.constants.ts` (`TUS_CONFIG`) |
108
123
  | `tenant` | `core-tenant.enums.ts` |
109
124
  | `auth` | `interfaces/auth-provider.interface.ts` |
125
+ | `common/services` | `core-cron-jobs.registry.ts` (the cron infrastructure refs `CoreCronJobs` reads — `import type` only, so its emit is empty). Unlike the better-auth registry it IS barrel-exported: `setCronJobsInfrastructure()` / `getCronJobsInfrastructure()` are public, because a project may register its own connection or Redis service |
110
126
  | `common/helpers` | `id.helper.ts` (ID cluster, out of `db.helper`) + `clone.helper.ts` (`clone`/`deepFreeze`, out of `input.helper`) |
111
127
  | `common/inputs` | `FilterInput` + `CombinedFilterInput` merged into `filter.input.ts` — declaration order is load-bearing |
112
128
  | `common/decorators` | `restricted.decorator` is on **zero** cycles; its exports are hoisted `function` declarations (TDZ-immune) as defense in depth |
@@ -248,8 +248,8 @@ This pattern is currently applied to:
248
248
 
249
249
  | Feature | Config Path | Pattern | Default Values |
250
250
  |---------|-------------|---------|----------------|
251
- | Legacy Auth Rate Limiting | `auth.rateLimit` | Presence Implies Enabled | `max: 10`, `windowSeconds: 60` |
252
- | BetterAuth Rate Limiting | `betterAuth.rateLimit` | Presence Implies Enabled | `max: 10`, `windowSeconds: 60` |
251
+ | Legacy Auth Rate Limiting | `auth.rateLimit` | Presence Implies Enabled | `max: 10`, `windowSeconds: 60`. Counters live in a `RateLimitStore`: `RedisRateLimitStore` when `redis` is configured (limit enforced EXACTLY across replicas instead of `max × replicas`), else `InMemoryRateLimitStore` (previous behavior). On a Redis outage it degrades to the in-memory counter and logs once per transition — never a 500, never "allowed". `check()` / `reset()` / `clear()` are ASYNC since 11.33.0; `getStats().activeEntries` is `-1` on the Redis store |
252
+ | BetterAuth Rate Limiting | `betterAuth.rateLimit` | Presence Implies Enabled | `max: 10`, `windowSeconds: 60`. Same `RateLimitStore` selection, degradation and async signatures as the Legacy Auth row (namespace `better-auth`) |
253
253
  | BetterAuth JWT Plugin | `betterAuth.jwt` | Boolean Shorthand | `expiresIn: '15m'` |
254
254
  | BetterAuth 2FA Plugin | `betterAuth.twoFactor` | Boolean Shorthand | `appName: 'Nest Server'` |
255
255
  | BetterAuth Passkey Plugin | `betterAuth.passkey` | Boolean Shorthand | `rpName: 'Nest Server'` |
@@ -265,12 +265,20 @@ This pattern is currently applied to:
265
265
  | Response Model Interceptor | `security.responseModelInterceptor` | Boolean Shorthand | `true` (enabled), `debug: false` |
266
266
  | Translate Response Interceptor | `security.translateResponseInterceptor` | Boolean Shorthand | `true` (enabled) |
267
267
  | Secret Fields Removal | `security.secretFields` | Array | `['password', 'verificationToken', ...]` |
268
- | Multi-Tenancy | `multiTenancy` | Presence Implies Enabled | `headerName: 'x-tenant-id'`, `membershipModel: 'TenantMember'`, `adminBypass: true`, `excludeSchemas: []`, `roleHierarchy: { member: 1, manager: 2, owner: 3 }`, `cacheTtlMs: 30000` (0 disables, process-local). System roles (`S_EVERYONE`, `S_USER`, `S_VERIFIED`) are checked as OR alternatives before real roles; method-level system roles take precedence; membership validated for context when system role grants access + header present. Hierarchy roles use level comparison, normal roles use exact match. Use `DefaultHR` or `createHierarchyRoles()` for type-safe role constants. Bypass: `RequestContext.runWithBypassTenantGuard()`. Cache invalidation: `CoreTenantGuard.invalidateUser(userId)` / `invalidateAll()` |
268
+ | Multi-Tenancy | `multiTenancy` | Presence Implies Enabled | `headerName: 'x-tenant-id'`, `membershipModel: 'TenantMember'`, `adminBypass: true`, `excludeSchemas: []`, `roleHierarchy: { member: 1, manager: 2, owner: 3 }`, `cacheTtlMs: 30000` (0 disables, process-local). System roles (`S_EVERYONE`, `S_USER`, `S_VERIFIED`) are checked as OR alternatives before real roles; method-level system roles take precedence; membership validated for context when system role grants access + header present. Hierarchy roles use level comparison, normal roles use exact match. Use `DefaultHR` or `createHierarchyRoles()` for type-safe role constants. Bypass: `RequestContext.runWithBypassTenantGuard()`. Cache invalidation: `invalidateUser(userId)` / `invalidateAll()` are **instance** methods on the singleton `CoreTenantGuard` — inject it and call `this.tenantGuard?.invalidateUser(userId)`, never `CoreTenantGuard.invalidateUser(...)` (there is no static). With `redis` configured, both additionally BROADCAST the invalidation to every replica (`<keyPrefix>:tenant-cache:invalidate` pub/sub); without Redis they clear the local process only, so other replicas stay stale until `cacheTtlMs`. A received broadcast clears locally without re-publishing |
269
269
  | BetterAuth Tenant Skip | `betterAuth.skipTenantCheck` | Explicit Boolean | `true` (default). When `true` and no `X-Tenant-Id` header is sent, IAM endpoints (controller + resolver) skip `CoreTenantGuard` tenant validation. When header IS present, normal membership validation runs regardless. Set `false` for tenant-aware auth scenarios (subdomain-based, invite links, SSO per tenant) |
270
270
  | Debug Process Input | `debugProcessInput` | Explicit Boolean | `false` (default). When `true`, logs a debug message when `prepareInput()` changes the input type during `process()`. Has performance cost due to `JSON.stringify` on every `process()` call — enable only for debugging |
271
271
  | JSONTransport Production Guard | `email.smtp` with `jsonTransport` | Runtime Guard | Throws `Error` when `email.smtp` has a truthy `jsonTransport` property in `production` or `staging` environments (read from config `env` field). JSONTransport silently discards all outgoing mail — the guard prevents accidental misconfiguration that causes password-reset, 2FA, and verification emails to vanish. Use `{ jsonTransport: true }` only in CI/e2e/local environments |
272
272
  | Cookies | `cookies` | Boolean Shorthand (default true) | `true` (enabled), `exposeTokenInBody: false`. When enabled: loads `cookie-parser`, sets CORS `credentials: true`, sets signed httpOnly session cookies. When `exposeTokenInBody: true`: token stays in response body alongside cookies (for hybrid JWT+Cookie auth). JWT via `Authorization: Bearer` always works independently. **BetterAuth cookie name (since v11.27.6):** `createBetterAuthInstance()` pins `advanced.useSecureCookies: false` so BetterAuth's native handlers read the same UNPREFIXED `<cookiePrefix>.session_token` the helper writes (fixes a `401` split-brain on 2FA/passkey/`/token`); the `Secure` attribute is still applied on an `https://` baseURL via `advanced.defaultCookieAttributes`. Opt back into the `__Secure-` prefix with `betterAuth.options.advanced.useSecureCookies: true` only when BetterAuth manages cookies entirely |
273
273
  | CORS | `cors` | Boolean Shorthand | `enabled: true`, `allowAll: false`, `deriveAppUrl: true`. Origins come from `appUrl`/`baseUrl`, resolved by the shared `resolveServerUrls()` helper (`cookies.helper.ts`) that ALL three CORS layers use (GraphQL, REST, BetterAuth `trustedOrigins`) — they can no longer drift. `appUrl` resolution: explicit → derived from a **host-split** localhost `baseUrl` (its `api.` label strips to a sibling host: `https://api.crm.localhost` → `https://crm.localhost`, as served by `lt dev up`; the port is preserved) → localhost default (`http://localhost:3001`, only for `env: local`/`ci`/`e2e` with a **port-split** localhost `baseUrl` — one host, API `:3000`, app `:3001`; `https://api.localhost` strips to the bare `localhost` the API already answers on and is therefore a port split, not a host split) → derived from `baseUrl` by stripping a leading `api.` label (`https://api.example.com` → `https://example.com`). **Security:** the derived origin receives credentialed CORS; set `deriveAppUrl: false` when the apex domain is not trusted, then list the frontend origin via `appUrl`/`allowedOrigins` (a host-split localhost `baseUrl` then falls back to the localhost default). The derivation never yields a bare TLD (`https://api.dev` unchanged) and never emits the opaque `null` origin (non-http(s) `baseUrl` passes through verbatim). `allowAll: true` mirrors the request origin for REST/GraphQL, but BetterAuth's `trustedOrigins` still resolve to `[appUrl]` (+ passkey origins) — an origin check has no "allow everything" mode, so a separately hosted frontend must appear in `appUrl`/`allowedOrigins` (or set `betterAuth.trustedOrigins` explicitly). `enabled: false` disables CORS on all layers including BetterAuth (`trustedOrigins: []`, which still trusts BetterAuth's own `baseURL`). Explicit `betterAuth.trustedOrigins` always takes precedence |
274
+ | Central Redis | `redis` | Boolean Shorthand + Presence Implies Enabled | `host: 'localhost'`, `port: 6379`, `db: 0`, `keyPrefix: <package.json name, slugified>` (per APPLICATION, not per framework — a constant default silently collides when two apps share one Redis; set it explicitly when sharing IS intended); `url` (takes precedence over host/port/db/credentials), `username`/`password`, `options` (passed to the ioredis constructor). `true` / `{}` enables with defaults; `{ enabled: false }` pre-configures without enabling; **absent = every consumer keeps its process-local fallback** (that fallback is the whole backward-compatibility story of 11.33.0). Requires the OPTIONAL peer `ioredis` — configured-but-missing **fails the boot** with a named error rather than crashing on first use. `keyPrefix` is applied by the framework per key, NOT as ioredis `keyPrefix` (that would collide with BullMQ's own prefix). One `CoreRedisService` serves all features: shared client (`getClient()`), one cached subscriber (`getSubscriber()` — a subscribing client cannot run commands), dedicated connections (`createClient(label)`); all are tracked and quit on shutdown. Switches on automatically: exact cross-replica rate limits via `RedisRateLimitStore` (see the Legacy Auth / BetterAuth / AI rate-limit rows), cron dedup (see Cron Job Deduplication row), `CoreRedisPubSub` as `PUB_SUB` for cluster-wide GraphQL subscriptions (**payloads must be JSON-serializable — `Date`, class instances, `Map`/`Set`, `undefined` do not survive**), tenant-cache invalidation broadcast, Hub collector mirroring, MCP session registry (turns a wrong-replica request from a misleading `404` into a `409` — `/ai/mcp` still REQUIRES sticky sessions, sessions are not portable) |
275
+ | File Access Roles | `file.downloadRoles`, `file.uploadRoles`, `file.deleteRoles` | Config-Driven Role List | `[RoleEnum.ADMIN]` each. **These are the only breaking change a single-replica project gets in 11.33.0** — six members moved from `@Roles(S_EVERYONE)` to these knobs: `GET /files/id/:id`, `GET /files/:filename` and `getFileInfo` (`downloadRoles`), `uploadFile` / `uploadFiles` (`uploadRoles`), `deleteFile` (`deleteRoles`). Plain role STRINGS, not `RoleEnum` members, so project roles work (`['company-admin', 'editor']`). Applied at boot by `applyFileRoles()` via `Reflect.defineMetadata('roles', …)` on the base-class methods — the same runtime mechanism `CorePermissionsModule` uses, because the value is only known from config. **`[]`, a non-array, or an array holding a non-string is REJECTED with a warning and the default applies** — an all-empty role set reads to the guards as "no roles required" and would OPEN the route, the exact opposite of the intent. **ADMIN is always unioned in**: both `CoreFileController` and `CoreFileResolver` carry a class-level `@Roles(RoleEnum.ADMIN)` and the guards UNION class + handler metadata, so these knobs can grant but never exclude admins. Both classes also carry `@SkipTenantCheck()` — GridFS and the S3 metadata collection are reached outside Mongoose, so `mongooseTenantPlugin` never scopes them and a role name alone cannot express a per-tenant rule; roles resolve against `user.roles`, never `membership.role`. **A subclass that OVERRIDES a member opts out permanently** (decorator metadata lives on the function object, and an override is a different function) — inherit the member instead. Roles are the coarse filter only; per-file rules belong in `CoreFileService.checkRights()`, which now receives `currentUser` and can read raw metadata via `getRawFileInfo()`. Implementation: `src/core/modules/file/file-roles.helper.ts` (`FILE_ROLE_DEFAULTS`, `applyFileRoles`) |
276
+ | TUS Roles | `tus.roles` | Config-Driven Role List | `[RoleEnum.S_USER]` (was `S_EVERYONE` — breaking). A TUS upload writes into the SAME store the download routes guard, and the termination extension (on by default) can delete from it, so anonymous writes into a store only privileged callers may read is the wrong way round. Applied by `TusModule.applyRoles()` onto the registered controller class **and** onto `handleTus` / `handleTusWithId`. Same rejection rule as the file roles: `[]` / non-array / non-string → warning + `DEFAULT_TUS_CONFIG.roles`. **`OPTIONS` is deliberately exempt**: `handleTusOptions` / `handleTusOptionsWithId` keep their own handler-level `@Roles(RoleEnum.S_EVERYONE)`, because that is the CORS preflight — browsers send it WITHOUT credentials, and it returns server capabilities only. Gating it would make every browser upload fail before the first byte. `CoreTusController` also carries `@SkipTenantCheck()`. A custom controller is covered as long as it INHERITS the handlers; one that re-declares `@All()` / `@Roles()` carries its own metadata and thereby opts out — the documented way to hard-code a policy config must not be able to change. **Set `roles: [RoleEnum.S_EVERYONE]` explicitly if you accept attachments on a public form.** Implementation: `src/core/modules/tus/tus.module.ts`, `src/core/modules/tus/interfaces/tus-config.interface.ts` |
277
+ | File Storage | `file.storage` + `file.storageDir` + `s3` | Explicit Enum with DERIVED default | `'filesystem' \| 'gridfs' \| 's3'`. **Unset → derived**, most capable first: `'s3'` when `s3.bucket` is set, else `'gridfs'` when `mongoose.uri` is set, else `'filesystem'`. **Set → enforced**: an unavailable store FAILS THE BOOT (`assertFileStorageAvailable()`), it never falls back — a silent fallback puts files in a store the operator does not believe they are in, unrecoverably. A DERIVED driver is enforced too (`s3.bucket` set but `s3Service` not forwarded to `super()` → boot error). Metadata always lives in MongoDB whichever driver holds the bytes (`fs.files` / `s3-files` / `filesystem-files`) — it has to stay queryable for `findFileInfo()` and `checkRights()`. Reads consult ALL stores so switching drivers is forward-only with no migration; writes go to the active driver only. `'filesystem'` is pod-local: not shared between replicas, lost on restart unless `storageDir` is a mounted volume. `s3`: `bucket` (required — the one thing S3 cannot default, hence the eligibility test), `region: 'us-east-1'`, `forcePathStyle: false`, `stagingBucket: bucket`, `presignedDownloads: false` (`true`/`{}` → `expiresInSeconds: 300`), `endpoint` (MinIO/RustFS), `accessKeyId`/`secretAccessKey` (omit → AWS default credential chain), `enabled: false` to pre-configure. Requires the OPTIONAL peer `@aws-sdk/client-s3`, plus `@aws-sdk/s3-request-presigner` for presigned downloads. `presignedDownloads` makes `GET /files/id/:id` answer `302` to a time-limited S3 URL instead of streaming — the URL is a bearer capability, authorized once at issue time. The resolved driver is logged at boot (`[CoreFileStorage] File storage: …`) |
278
+ | TUS S3 Staging | `tus.s3Staging` | Explicit Boolean (default ON when S3 configured) | `true` when `s3` is configured, otherwise inert. Stages in-progress uploads in `s3.stagingBucket` via `@tus/s3-store` instead of `tus.uploadDir` on local disk, so resumable uploads survive replica restarts and need no sticky sessions. Set `false` to force local disk. Missing OPTIONAL peer `@tus/s3-store` → warning + fall back to local disk (NOT a boot failure). **Give the staging bucket a lifecycle rule expiring incomplete multipart uploads** — the framework's own expiration cleanup is skipped in S3 mode (S3 is the right place for that policy), so aborted uploads otherwise accumulate parts nothing removes |
279
+ | Cron Job Deduplication | per job: `distributed` in `CronJobConfig` | Explicit Boolean | **`true` when `redis` is configured, otherwise `false`** — a single-replica project that upgrades must not silently gain a `cron-locks` collection, a lease write per tick, and a new way for a tick to be skipped. A Redis-less multi-replica fleet opts in per job with `distributed: true` (MongoDB lease). Mechanism: BullMQ job scheduler when Redis + the OPTIONAL peer `bullmq` are present AND `cronTime` is a string without `utcOffset`; otherwise local timer + lease (Redis `SET NX`, else a TTL-indexed `cron-locks` document). Tick lease TTL 3600 s. **Leases fail open** — an unreachable lease store runs the tick everywhere rather than stopping all scheduled work fleet-wide. **`runOnInit` (default `true`) deduplicates over a FIXED per-job key with a 300 s TTL**, because replicas do not share a boot instant: replicas booting within 5 min run the startup tick once between them, and a replica restarting inside that window SKIPS its startup tick — set `distributed: false` on jobs whose `runOnInit` work is per-process (warming a process-local cache). No constructor change needed: `CoreModule` fills `core-cron-jobs.registry.ts` via `CoreCronJobsInitializer` and `CoreCronJobs` reads it lazily; explicit `{ connection, redisService }` in `CoreCronJobsOptions` wins. With neither source it warns once and every replica runs every tick |
280
+ | Shutdown Delay | `shutdownDelayMs` | Numeric Sentinel — Family B (`0` = off) | `0` (default, no delay, no log). Waits N ms **in the SIGTERM/SIGINT handler, before `close()` is entered**, so a load balancer can finish deregistering while the instance is still fully healthy. NOT a lifecycle hook: `close()` runs `onModuleDestroy` → `beforeApplicationShutdown` → dispose → `onApplicationShutdown`, so a delay in `beforeApplicationShutdown` would wait with every module already torn down while the socket still accepts — worse than no delay. **Requires `installGracefulShutdown(app)` in main.ts, which REPLACES `server.enableShutdownHooks()`** — keeping both makes Nest close the app in parallel with the wait, so the delay silently never happens. At delay `0` the helper IS `enableShutdownHooks()`. **Keep the value well below the orchestrator grace period AND leave room for the drain that follows**: Compose `stop_grace_period` 10s, Kubernetes `terminationGracePeriodSeconds` 30s, and `installProcessDiagnostics()` force-exits after 30s — exceed any and the process is SIGKILLed mid-wait with no hook running. Warns above `10000`, capped at `60000`. Non-numeric / negative values behave like `0` |
281
+ | Trust Proxy | `trustProxy` | Explicit Value (pass-through, Express default) | `false` (Express's own default — the forwarded chain is not trusted). Passed verbatim to `app.set('trust proxy', …)` by `CoreTrustProxyInitializer`, a `CoreModule` provider, so a consumer inherits it by upgrading and needs no `main.ts` edit. Accepts `false` / a hop count (`1`, `2`) / `'loopback'` / a subnet list — **not** Express's predicate function: the value must survive `NEST_SERVER_CONFIG` / `NSC__*` (JSON) and the ConfigService deep clone. **This is what makes `request.ip` correct, and every IP-keyed rate limit depends on it**: unset behind Caddy/nginx/an ingress, `req.ip` is the PROXY address for every request, so all clients share ONE bucket and `auth.rateLimit.max` throttles everybody at once — exactly fleet-wide once `redis` is configured. Trusting MORE hops than exist is the opposite failure: a client prepends its own header entry and picks a fresh bucket per request. Applied at module init (inside `app.init()`/`listen()`, i.e. AFTER `main.ts`), so a configured value wins over a hand-written `app.set()`; an UNSET value is never applied, which keeps `app.set('trust proxy', fn)` in `main.ts` available as the escape hatch for the predicate form. **Unset + an IP-keyed limiter enabled (`auth.rateLimit` / `betterAuth.rateLimit`) logs a boot warning naming the shared-bucket consequence**; `trustProxy: false` is the explicit "nothing proxies me" answer that silences it. The AI limiter keys on the user id and is unaffected |
274
282
  | AI Assistant | `ai` | Presence Implies Enabled | Core: `maxIterations: 5`, `defaultMode: 'auto'` (or `'plan'`), `rateLimit` (presence implies enabled: `max: 20`, `windowSeconds: 60`), `systemPrompt`, `documentation` (injected into the system prompt), `encryptionSecret`. **DB-backed LLM connections** (`aiConnections`, admin CRUD) with AES-256-GCM-encrypted API keys (`AiCryptoService`, secret from `ai.encryptionSecret` / `NSC__AI__ENCRYPTION_SECRET` / `SECRETS_ENCRYPTION_KEY`; `apiKeyEncrypted` is a global `secretFields` entry, never returned — only `hasApiKey`); optional `defaultConnection` one-time seed. **Provider abstraction** (`ILlmProvider`, default `OpenAiCompatibleProvider` for any OpenAI-compatible endpoint via `fetch`; per-connection `supportsNativeTools`/`supportsJsonResponse` capabilities, emulated tool calling when native tools are unavailable). **Tool registry** (`AiToolRegistry`, tools self-register, role-filtered; tools may be `mutating`/`destructive` and define `authorize()` for pre-flight data-level checks). **Plan mode** (`input.mode: 'plan'`): full plan → pre-flight authorize ALL steps → all-or-nothing execution with a translated (de/en) error when any step is not permitted. **Confirmation policy**: `confirmation.mutating: { default, enforced }` + client `input.requireConfirmation` (ignored when enforced); `destructive` always confirms. **Client metadata** (`input.metadata`: URL/nav/console logs, untrusted+capped). **Multi-turn conversations** (`aiConversations`, owner-scoped). **SSE streaming** (`POST /ai/stream`). **Audit** (`audit: false` → persist to `aiInteractions`, admin-readable). **Token budgets** (`budget: { period: 'day'|'month'|'none', user: { maxTokens?, maxPrompts? }, tenant: { maxTokens?, maxPrompts? } }`, requires audit): per-user AND per-tenant limits with config defaults; admins override per user/tenant at runtime (`aiBudgetLimits`, `CoreAiBudgetService`). Resolution: override → default → unlimited (missing/0 = unlimited). Enforced before the run (HTTP 429 + translated). Each response carries a compact `budget` summary (promptTokens, usedTokens, remainingTokens, resetAt); full breakdown via `aiUsage` query / `GET /ai/usage`. **Self-optimizing prompts**: the system prompt is assembled from keyed fragments (`CoreAiPromptBuilderService` ships built-in defaults; works with zero rows). Admin-editable overrides per slot (`aiSlots`, admin CRUD, `/ai/slots`) scoped by `key`/`locale`/`capability`/`tenantId`, with tenant override/reset semantics and placeholder tokens resolved at run time via the placeholder registry. **Governed learning loop** (`promptLearning: { enabled: true, autoApply: false }`): tool errors record `suggested` hints (`aiPromptHints`, admin CRUD, `/ai/prompt-hints`) that only reach the prompt once admin-approved (or auto-approved when `autoApply`); hints only ADD guidance, never relax permissions. **Context window** (`contextWindow`, default 8192; auto-detected per connection via `ILlmProvider.detectContextWindow()` — Ollama `/api/show` probe / known-model table / Claude alias — and persisted): per-user/session history is trimmed (oldest non-system turns dropped, last truncated) and tool-results capped to `maxToolResultChars` (default 12000) so a session never overflows the model. A connection's window can be seeded via `ai.defaultConnection.contextWindow` (validated: a non-positive/non-integer value is dropped with a warning). **Capability drift check** (`capabilityDriftCheck`, default `false`): opt-in boot self-check that probes each enabled connection with an EXPLICIT `supportsNativeTools`/`supportsJsonResponse` (built with those flags cleared so the endpoint is actually re-probed) and logs a warning on mismatch — the stored value is never changed. OFF by default because it makes outbound calls to the LLM endpoints on every boot; also skipped in the ci/e2e runners. **Deferred tool schemas** (`deferToolSchemas`, default `false`): the system-prompt tool catalog then lists only tool NAMES + descriptions instead of full JSON schemas, and the model fetches a schema on demand via the built-in `search_tools` meta-tool — with a large registry the schemas alone can dominate a small context window. `deferToolSummaryChars` (default `0` = untruncated) additionally caps each description in that DEFERRED catalog: whole sentences up to the cap (always at least the first), word-boundary cut when the first sentence already exceeds it, and a `…` marker appended ON TOP of the cap. The default of `0` keeps the saving opt-in, so enabling `deferToolSchemas` alone never changes what a description says; set roughly 200–400 alongside it to actually reclaim the context. Both apply to EMULATED providers only — a connection with `supportsNativeTools: true` receives every full description + schema via `buildToolSchemas()` regardless, so truncation and the banner are skipped there rather than asserting a cut the tool payload contradicts. The omitted tail is where preconditions and role restrictions usually live — the catalog banner tells the model to fetch the full text via `search_tools` first, but this is model GUIDANCE only: authorization is enforced server-side by the registry's role filter (`forUser()`), the execution-time re-check, and the `mutating`/`destructive` flags read by the confirmation gate — never by what the catalog shows. (`AiTool.authorize()` runs in PLAN MODE only; in auto mode and over MCP, data-level checks must live inside `execute()`.) **MCP server** (`mcp: false` → `/ai/mcp` Streamable HTTP, Bearer auth, lazy `@modelcontextprotocol/sdk`; `mcp: { oauth: true, oauthSecret }` adds OAuth 2.1 — HMAC tokens + PKCE S256 + dynamic registration via `mountAiMcpOAuth(app)` in main.ts). Overrides via `CoreModule.forRoot(env, { ai: { budgetService, connectionResolver, connectionService, controller, conversationService, interactionService, mcpClientService, modeService, placeholderRegistry, preferenceService, promptBuilder, promptHintService, promptService, resolver, service, slotService, toolGrantService, toolPolicyService } })` |
275
283
 
276
284
  ## Module Override Pattern (via `ICoreModuleOverrides`)
@@ -6,14 +6,73 @@
6
6
 
7
7
  | Runner | Config | Test files | Needs MongoDB |
8
8
  |--------|--------|-----------|:-------------:|
9
- | Unit | `vitest.config.ts` | `src/**/*.spec.ts`, `tests/unit/**/*.spec.ts` | No |
9
+ | Unit | `vitest.config.ts` | `tests/unit/**/*.spec.ts` | No |
10
10
  | E2E | `vitest-e2e.config.ts` | `tests/**/*.e2e-spec.ts`, `tests/stories/**/*.story.test.ts` | Yes |
11
11
 
12
+ **Test files NEVER live in `src/`.** Not a style preference — `src/` is this framework's shipping
13
+ artifact: `package.json` → `files` ships all of `src` recursively into the npm tarball, and
14
+ vendor-mode consumers copy `src/core/` into their own tree as first-class project code (the CLI's
15
+ `convertCloneToVendored` applies no spec filter). A co-located spec therefore reaches every consumer
16
+ as a test file they neither run nor maintain, re-delivered on every core update. Co-location is a
17
+ fine default for an application; for a library whose `src/` IS the delivery, separation wins.
18
+
19
+ Enforced twice: the unit runner's glob no longer looks at `src/` at all, and
20
+ `tests/unit/test-file-placement.spec.ts` fails on any `.spec.ts`/`.test.ts` found there — so a
21
+ misplaced test surfaces as a failure rather than as silently skipped coverage.
22
+
23
+ | Kind | Where | Suffix |
24
+ |------|-------|--------|
25
+ | Unit | `tests/unit/` | `*.spec.ts` |
26
+ | E2E / integration | `tests/` | `*.e2e-spec.ts` |
27
+ | Story (e2e-grade) | `tests/stories/` | `*.story.test.ts` |
28
+ | Type-only (compiled, never run) | `tests/types/` | `*.type-test.ts` |
29
+
12
30
  A file matching neither pattern would run nowhere. `tests/unit/test-file-routing.spec.ts` asserts
13
31
  that every `*.spec.ts` / `*.test.ts` in the repo is claimed by **exactly one** runner, so a
14
32
  mis-named suite fails the build instead of silently passing. Type-only tests
15
33
  (`tests/types/*.type-test.ts`) are compiled by `pnpm run test:types`, never executed.
16
34
 
35
+ ## Infrastructure containers (Redis + S3)
36
+
37
+ **Seven** e2e suites talk to a **real** Redis and/or a **real** S3-compatible store rather than a
38
+ mock, and they **fail loudly** when it is missing — a silently skipped infrastructure test is how an
39
+ untested driver ships:
40
+
41
+ | Suite | Needs |
42
+ |-------|-------|
43
+ | `tests/redis-infra.e2e-spec.ts` | Redis |
44
+ | `tests/redis-consumers.e2e-spec.ts` | Redis |
45
+ | `tests/multi-replica.e2e-spec.ts` | Redis |
46
+ | `tests/s3-infra.e2e-spec.ts` | S3 |
47
+ | `tests/file-storage-s3.e2e-spec.ts` | S3 |
48
+ | `tests/redis-s3-bootstrap.e2e-spec.ts` | Redis + S3 |
49
+ | `tests/graceful-shutdown.e2e-spec.ts` | Redis + S3 |
50
+
51
+ `tests/global-setup.ts` starts both containers automatically, so `pnpm test` works from a clean
52
+ machine with no manual docker command. It is idempotent (a running container is reused) and never
53
+ fatal: without Docker every other suite still runs, and the seven above report their own actionable
54
+ error.
55
+
56
+ ```bash
57
+ pnpm run test:infra # start + wait for readiness (also runs automatically)
58
+ pnpm run test:infra:status # what is running
59
+ pnpm run test:infra:down # stop and remove
60
+ ```
61
+
62
+ | | Redis | S3 (RustFS) |
63
+ |---|---|---|
64
+ | Port | 6380 (6379 is an auth-protected TurboOps Redis on lt dev machines) | 9102 |
65
+ | Container | `nest-server-2985-redis` | `nest-server-2985-rustfs` |
66
+
67
+ **CI provisions its own** containers in `.github/workflows/*.yml` and sets `LT_TEST_INFRA=0` so the
68
+ automatic start stays out of the way. Same images, same ports — only who starts them differs. Set
69
+ `LT_TEST_INFRA=0` locally too if you want to manage them yourself.
70
+
71
+ **Bucket cleanup:** every suite that creates a bucket names it per run and removes it in `afterAll`
72
+ via `tests/helpers/s3-test-cleanup.ts`. Emptying and deleting belong together — a bucket cannot be
73
+ deleted while it holds objects — which is why that lives in one helper rather than per suite. Before
74
+ it existed, two empty buckets leaked into the store per run.
75
+
17
76
  ## Running Tests
18
77
 
19
78
  ```bash
@@ -49,6 +108,47 @@ pnpm run test:cleanup
49
108
  - DB lifecycle (`tests/db-lifecycle.reporter.ts`): run passes → DB dropped immediately + stale run DBs from crashed/failed runs collected; run fails → DB kept for debugging. Additionally `tests/global-setup.ts` runs a **startup sweep** (shared `isStaleTestDb()` predicate, dead-PID/age guarded) — leftovers are removed when the NEXT run starts, which survives SIGKILL (check watchdog) and `--reporter` CLI overrides. An externally set `MONGODB_URI` (CI) opts out of the scheme.
50
109
  - Run governor (`tests/e2e-run-slots.ts`): machine-wide slot dir (`<tmpdir>/lt-e2e-run-slots`) caps concurrent e2e runs across ALL lt projects/sessions (default 2 on ≥8 cores). Further runs wait, logging `[e2e-governor] waiting…` every 15s (keeps the check watchdog fed — a queued run is NOT hung). The e2e config counts foreign slots at load time and drops to low-resource mode (reduced forks, raised timeouts) when another run is active — deterministic, unlike the lagging 1-min load average (kept as second signal). Knobs: `LT_E2E_MAX_RUNS` (0 disables), `LT_E2E_SLOT_DIR`, `LT_E2E_SLOT_TIMEOUT` (fail-open).
51
110
  - `retry: 2` (e2e) is deliberate — with `retry: 5`, one spec file with broken app/socket state ground through 6 attempts × 30s timeout × 22 tests ≈ an hour at 0% CPU (looked like a deadlock; the check watchdog killed it). Never raise retry to paper over contention.
111
+ - Infrastructure containers (E2E only, the **seven** specs listed under "Infrastructure containers"
112
+ above) round-trip against a REAL Redis and/or a REAL S3-compatible store.
113
+ `redis-s3-bootstrap` boots the assembled `CoreModule` with both
114
+ configured — the only test that covers the WIRING, which fakes and directly-constructed
115
+ services cannot: an unresolvable provider or a lifecycle hook that throws on a real
116
+ connection would pass every other spec and fail on a consumer's first `nest start`. `multi-replica` is the acceptance test for the
117
+ distributed features: it builds TWO independent service instances sharing one Redis and
118
+ asserts the properties a second replica must preserve — a scheduled tick and a startup tick
119
+ each run exactly once, one rate limit is enforced instead of one per replica, and a severed
120
+ Redis still yields a decision instead of an error. Single-instance specs against fakes cannot
121
+ show any of that: a limiter counting per process, or a lease key differing per instance,
122
+ passes them and fails here. `redis-consumers` covers the framework's own Redis consumers seen
123
+ from two replicas, `file-storage-s3` runs `CoreFileService` against a real bucket, and
124
+ `graceful-shutdown` needs real connections for `installGracefulShutdown()` to close.
125
+ Five of the seven preflight the connection in `beforeAll` and throw a written diagnosis —
126
+ `redis-infra`, `s3-infra` and `multi-replica` quote the full `docker run` line, `redis-consumers`
127
+ and `file-storage-s3` name the port and how to start it. So a forgotten container is a ~2s clear
128
+ error instead of a 43s opaque `MaxRetriesPerRequestError`. The other two fail fast without a
129
+ custom message: `graceful-shutdown` probes with `connectTimeout: 2000` /
130
+ `maxRetriesPerRequest: 1` and lets the raw connect error surface, and `redis-s3-bootstrap` has no
131
+ separate probe at all — its `beforeAll` IS the boot.
132
+
133
+ ```bash
134
+ docker run -d --name nest-server-2985-redis -p 6380:6379 redis:7.4-alpine
135
+ docker run -d --name nest-server-2985-rustfs -p 9102:9000 -e RUSTFS_ROOT_USER=rustfs -e RUSTFS_ROOT_PASSWORD=rustfs-secret -e RUSTFS_VOLUMES=/data rustfs/rustfs:1.0.0-rc.1 server /data
136
+ ```
137
+
138
+ Both tags are pinned, and these hints must stay identical to the tags in
139
+ `scripts/test-infra.mjs` / `.github/actions/test-infra/action.yml`. `containerMatches()` compares
140
+ the running container's image against the pinned one, so a hint that says `:latest` starts a
141
+ container the next `pnpm test` tears down and recreates.
142
+
143
+ | Service | Port | Overridable via |
144
+ |---------|------|-----------------|
145
+ | Redis | 6380 | `REDIS_HOST`, `REDIS_PORT` |
146
+ | RustFS (S3) | 9102 | `S3_ENDPOINT`, `S3_ACCESS_KEY`, `S3_SECRET_KEY` |
147
+
148
+ **Why 6380 and not 6379:** on lt dev machines 6379 is occupied by an auth-protected TurboOps
149
+ Redis. Binding the test container to 6380 keeps the two apart — no auth setup, no cross-wiring.
150
+ CI (`.github/workflows/build.yml` / `publish.yml`) maps the same ports and waits for readiness
151
+ before installing dependencies.
52
152
  - Test helper: `src/test/test.helper.ts`
53
153
  - Coverage: Collected from `src/**/*.{ts,js}`. The two runners are separate vitest processes, so
54
154
  they write separate reports (`coverage/unit`, `coverage/e2e`) rather than overwriting each other.
package/CLAUDE.md CHANGED
@@ -142,6 +142,7 @@ pnpm run reinit # Clean reinstall + tests + build
142
142
  - `src/core/modules/` - Auth, BetterAuth, ErrorCode, File, HealthCheck, Hub, Migrate, Permissions, SystemSetup, Tus, User
143
143
  - **Hub** - Build-free ADMIN-gated operator cockpit at `/hub` (config-gated per environment; 16 panels, runtime collectors, mailbox, admin actions). See `src/core/modules/hub/README.md`.
144
144
  - **Permissions** - ADMIN-gated security-map report (routes + roles + `@Restricted` fields); also surfaced in the Hub's "Routes / Permissions" panel.
145
+ - **Optional central infrastructure** (11.33.0+) - `CoreRedisService` (`redis` config) and `CoreS3Service` (`s3` config) turn process-local state into shared state for multi-replica deployments: rate-limit counters, cron deduplication, GraphQL subscriptions, tenant-cache invalidation, Hub collectors, file storage (`file.storage: 's3'`; the driver is otherwise derived — S3 → GridFS → filesystem — and an unavailable one fails the boot rather than falling back) and TUS staging. Both are inert without their config, and their client libraries are **optional peer dependencies** (`ioredis`, `bullmq`, `@aws-sdk/client-s3`, `@aws-sdk/s3-request-presigner`, `@tus/s3-store`) — a project that uses neither installs nothing extra. When adding a distributed feature, always keep the non-configured fallback path.
145
146
 
146
147
  See `.claude/rules/architecture.md` for detailed documentation.
147
148
  See [`docs/REQUEST-LIFECYCLE.md`](docs/REQUEST-LIFECYCLE.md) for the complete request lifecycle, security architecture, and interceptor/decorator reference.
@@ -236,6 +237,10 @@ file is missing (default: tolerate with a warning) — see `src/core/modules/mig
236
237
 
237
238
  1. **All code, comments, documentation in English**
238
239
  2. **Run tests before completing changes** - `pnpm test`
240
+ 2b. **Never put test files in `src/`** — unit tests go to `tests/unit/*.spec.ts`, e2e to
241
+ `tests/*.e2e-spec.ts`, stories to `tests/stories/*.story.test.ts`. `src/` ships in the npm
242
+ tarball and is copied verbatim into vendor-mode consumer projects, so a co-located spec becomes
243
+ part of THEIR codebase. Enforced by `tests/unit/test-file-placement.spec.ts`.
239
244
  3. **Follow existing patterns** for consistency
240
245
  4. **Never store S_ roles** in user.roles array
241
246
  5. **Use Module Inheritance Pattern** for core modules
package/FRAMEWORK-API.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @lenne.tech/nest-server — Framework API Reference
2
2
 
3
- > Auto-generated from source code on 2026-07-24 (v11.32.3)
3
+ > Auto-generated from source code on 2026-08-11 (v11.33.0)
4
4
  > File: `FRAMEWORK-API.md` — compact, machine-readable API surface for Claude Code
5
5
 
6
6
  ## CoreModule.forRoot()
@@ -29,6 +29,7 @@
29
29
  - `version?`: `string | undefined` — Semantic version of the running build (e.g. from package.json / meta.json).
30
30
  - `errorCode?`: `IErrorCode | undefined` — Configuration for the error code module
31
31
  - `execAfterInit?`: `string | undefined` — Exec a command after server is initialized
32
+ - `file?`: `IFileConfig | undefined` — Configuration of the file module: where the bytes live (`storage`) and who
32
33
  - `filter?`: `{ maxLimit?: number; } | undefined` — Filter configuration and defaults
33
34
  - `graphQl?`: `false | { driver?: ApolloDriverConfig; enableSubscriptionAuth?: boolean; maxC...` — Configuration of the GraphQL module
34
35
  - `healthCheck?`: `{ configs?: { build?: { enabled?: boolean; }; database?: { enabled?: boolean;...` — Whether to activate health check endpoints
@@ -42,11 +43,15 @@
42
43
  - `multiTenancy?`: `IMultiTenancy | undefined` (default: `undefined (disabled)`) — Multi-tenancy configuration for tenant-based data isolation.
43
44
  - `permissions?`: `boolean | IPermissions | undefined` (default: `undefined (disabled)`) — Permissions report module (development tool).
44
45
  - `port?`: `number | undefined` — Port number of the server
46
+ - `redis?`: `boolean | IRedisConfig | undefined` (default: `undefined (disabled)`) — Optional central Redis connection used by all distributed features
47
+ - `s3?`: `IS3Config | undefined` (default: `undefined (disabled)`) — Optional central S3-compatible object storage (AWS S3, MinIO, ...).
45
48
  - `security?`: `{ checkResponseInterceptor?: boolean | { checkObjectItself?: boolean; debug?:...` — Configuration for security pipes and interceptors
46
49
  - `sha256?`: `boolean | undefined` — Whether to enable verification and automatic encryption for received passwords that are not in sha256 format
50
+ - `shutdownDelayMs?`: `number | undefined` (default: `0 (no delay)`) — Delay in milliseconds between receiving a shutdown signal and starting the
47
51
  - `staticAssets?`: `{ options?: ServeStaticOptions; path?: string; } | undefined` — Configuration for useStaticAssets
48
52
  - `systemSetup?`: `ISystemSetup | undefined` — System setup configuration for initial admin creation.
49
53
  - `templates?`: `{ engine?: string; path?: string; } | undefined` — Templates
54
+ - `trustProxy?`: `string | number | boolean | string[] | undefined` (default: `false (Express default — the forwarded chain is not trusted)`) — Express `trust proxy` setting — how far up the `X-Forwarded-For` chain this app believes.
50
55
  - `tus?`: `boolean | ITusConfig | undefined` — TUS resumable upload configuration.
51
56
 
52
57
  ### IBetterAuth (type alias: IBetterAuthWithoutPasskey | IBetterAuthWithPasskey)
@@ -145,6 +150,56 @@ When `passkey` is enabled, `trustedOrigins` is required (compile-time enforcemen
145
150
  - `supportsNativeTools?`: `boolean | undefined` — Native function/tool-calling support. Omit to auto-detect by probing the
146
151
  - `supportsVision?`: `boolean | undefined` — Whether the model supports image input.
147
152
 
153
+ ### IFileConfig
154
+
155
+ - `deleteRoles?`: `string[] | undefined` (default: `['admin']`) — Roles allowed to DELETE files (`deleteFile` mutation).
156
+ - `downloadRoles?`: `string[] | undefined` (default: `['admin']`) — Roles allowed to DOWNLOAD files and read file info
157
+ - `storage?`: `"filesystem" | "gridfs" | "s3" | undefined` (default: `derived — see above`) — Storage driver for CoreFileService. Three equivalent options:
158
+ - `storageDir?`: `string | undefined` (default: `'uploads/files'`) — Directory for the `'filesystem'` storage driver.
159
+ - `uploadRoles?`: `string[] | undefined` (default: `['admin']`) — Roles allowed to UPLOAD files (`uploadFile` / `uploadFiles` mutations).
160
+
161
+ ### IRedisConfig
162
+
163
+ - `db?`: `number | undefined` (default: `0`) — Redis database index
164
+ - `enabled?`: `boolean | undefined` (default: `true (when config object is present)`) — Whether Redis is enabled.
165
+ - `host?`: `string | undefined` (default: `'localhost'`) — Redis host
166
+ - `keyPrefix?`: `string | undefined` (default: `the slugified `name` from package.json, or 'nest-server' if unreadable`) — Prefix prepended to every framework-managed Redis key
167
+ - `options?`: `Record<string, unknown> | undefined` (default: `undefined`) — Additional ioredis options passed through to the client constructor
168
+ - `password?`: `string | undefined` (default: `undefined (no auth)`) — Redis password
169
+ - `port?`: `number | undefined` (default: `6379`) — Redis port
170
+ - `url?`: `string | undefined` (default: `undefined`) — Full Redis connection URL (e.g. 'redis://user:pass@host:6379/0').
171
+ - `username?`: `string | undefined` (default: `undefined`) — Redis username (Redis 6+ ACL)
172
+
173
+ ### IS3Config
174
+
175
+ - `accessKeyId?`: `string | undefined` (default: `undefined`) — Access key ID.
176
+ - `autoCreateBucket?`: `boolean | undefined` (default: `false`) — Create the configured buckets at startup when they do not exist.
177
+ - `bucket`: `string` — Bucket for files stored via CoreFileService
178
+ - `enabled?`: `boolean | undefined` (default: `true (when config object is present)`) — Whether S3 is enabled.
179
+ - `endpoint?`: `string | undefined` (default: `undefined`) — Custom endpoint URL for S3-compatible services (MinIO, RustFS, ...).
180
+ - `forcePathStyle?`: `boolean | undefined` (default: `false`) — Use path-style addressing (required by most self-hosted S3 services).
181
+ - `presignedDownloads?`: `boolean | { expiresInSeconds?: number; } | undefined` (default: `false`) — Serve downloads as presigned URL redirects instead of streaming
182
+ - `region?`: `string | undefined` (default: `'us-east-1'`) — AWS region
183
+ - `secretAccessKey?`: `string | undefined` (default: `undefined`) — Secret access key.
184
+ - `stagingBucket?`: `string | undefined` (default: `same as `bucket``) — Bucket used as staging area for resumable TUS uploads.
185
+
186
+ ### ITusConfig
187
+
188
+ - `allowedHeaders?`: `string[] | undefined` (default: `[] (no additional headers needed)`) — Additional allowed HTTP headers for TUS requests (beyond
189
+ - `allowedTypes?`: `string[] | undefined` (default: `undefined (all types allowed)`) — Allowed MIME types for uploads.
190
+ - `checksum?`: `boolean | undefined` (default: `true`) — Checksum extension configuration.
191
+ - `concatenation?`: `boolean | undefined` (default: `true`) — Concatenation extension configuration.
192
+ - `creation?`: `boolean | ITusCreationConfig | undefined` (default: `true`) — Creation extension configuration.
193
+ - `creationWithUpload?`: `boolean | undefined` (default: `true`) — Creation With Upload extension configuration.
194
+ - `enabled?`: `boolean | undefined` (default: `true (enabled by default)`) — Whether tus uploads are enabled.
195
+ - `expiration?`: `boolean | ITusExpirationConfig | undefined` (default: `{ expiresIn: '24h' }`) — Expiration extension configuration.
196
+ - `maxSize?`: `number | undefined` (default: `50 * 1024 * 1024 * 1024 (50 GB)`) — Maximum upload size in bytes
197
+ - `path?`: `string | undefined` (default: `'/tus'`) — Base path for tus endpoints
198
+ - `roles?`: `string[] | undefined` (default: `['s_user']`) — Roles allowed to use the tus endpoints (create, write, read offset, terminate).
199
+ - `s3Staging?`: `boolean | undefined` (default: `true (when S3 is usable)`) — Stage upload chunks in the configured S3 bucket (`IServerOptions.s3`,
200
+ - `termination?`: `boolean | undefined` (default: `true`) — Termination extension configuration.
201
+ - `uploadDir?`: `string | undefined` (default: `'uploads/tus'`) — Directory for temporary upload chunks.
202
+
148
203
  ### ICoreModuleOverrides
149
204
 
150
205
  - `ai?`: `{ budgetService?: Type<any>; connectionResolver?: Type<any>; connectionServic...` — Override AI module collaborators with project-specific subclasses.
@@ -190,7 +245,7 @@ When `passkey` is enabled, `trustedOrigins` is required (compile-time enforcemen
190
245
 
191
246
  - `enabled?`: `boolean | undefined` (default: `false`) — Whether rate limiting is enabled
192
247
  - `max?`: `number | undefined` (default: `10`) — Maximum number of requests within the time window
193
- - `maxEntries?`: `number | undefined` (default: `10000`) — Maximum number of entries in the in-memory rate limit store.
248
+ - `maxEntries?`: `number | undefined` (default: `10000`) — Maximum number of distinct counters this limiter may hold — the bound on the keyspace a
194
249
  - `message?`: `string | undefined` — Custom message when rate limit is exceeded
195
250
  - `skipEndpoints?`: `string[] | undefined` — Endpoints to skip rate limiting entirely
196
251
  - `strictEndpoints?`: `string[] | undefined` — Endpoints to apply stricter rate limiting (e.g., sign-in, sign-up)
@@ -287,7 +342,7 @@ Generic: `CrudService<Model, CreateInput, UpdateInput>`
287
342
  | `auth` | — | `src/core/modules/auth/` |
288
343
  | `better-auth` | README, CHECKLIST | `src/core/modules/better-auth/` |
289
344
  | `error-code` | CHECKLIST | `src/core/modules/error-code/` |
290
- | `file` | README | `src/core/modules/file/` |
345
+ | `file` | README, CHECKLIST | `src/core/modules/file/` |
291
346
  | `health-check` | — | `src/core/modules/health-check/` |
292
347
  | `hub` | README, CHECKLIST | `src/core/modules/hub/` |
293
348
  | `migrate` | README | `src/core/modules/migrate/` |
package/bin/migrate.js CHANGED
@@ -93,7 +93,17 @@ if (require.main === module) {
93
93
  }
94
94
  }
95
95
 
96
- // Load and run the CLI
97
- const { main } = require(cliPath);
98
- main();
96
+ // Load and run the CLI.
97
+ //
98
+ // Prefer runCli(): it drains stdout and then exits explicitly, so a handle left
99
+ // behind by MongoDB/GridFS cannot keep the process alive after the work is done
100
+ // (a migration step that never returns blocks the container before the server
101
+ // ever starts). main() is the fallback for a dist built before runCli existed —
102
+ // it resolves normally but relies on the event loop draining by itself.
103
+ const cli = require(cliPath);
104
+ const run = cli.runCli || cli.main;
105
+ Promise.resolve(run()).catch((error) => {
106
+ console.error('Fatal error:', error);
107
+ process.exitCode = 1;
108
+ });
99
109
  }
@@ -0,0 +1 @@
1
+ export declare function buildContentDisposition(filename?: string): string;
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildContentDisposition = buildContentDisposition;
4
+ const NON_ATTR_CHAR = /['()*]/g;
5
+ const NON_QUOTABLE_ASCII = /[^\x20-\x21\x23-\x5B\x5D-\x7E]/g;
6
+ function toExtValue(filename) {
7
+ return encodeURIComponent(filename).replace(NON_ATTR_CHAR, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
8
+ }
9
+ function buildContentDisposition(filename) {
10
+ const name = filename || 'download';
11
+ const quotable = name.replace(NON_QUOTABLE_ASCII, '_');
12
+ return `attachment; filename="${quotable}"; filename*=UTF-8''${toExtValue(name)}`;
13
+ }
14
+ //# sourceMappingURL=content-disposition.helper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content-disposition.helper.js","sourceRoot":"","sources":["../../../../src/core/common/helpers/content-disposition.helper.ts"],"names":[],"mappings":";;AAqFA,0DAIC;AAhED,MAAM,aAAa,GAAG,SAAS,CAAC;AAehC,MAAM,kBAAkB,GAAG,iCAAiC,CAAC;AAK7D,SAAS,UAAU,CAAC,QAAgB;IAClC,OAAO,kBAAkB,CAAC,QAAQ,CAAC,CAAC,OAAO,CACzC,aAAa,EACb,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,EAAE,CAC9D,CAAC;AACJ,CAAC;AAmCD,SAAgB,uBAAuB,CAAC,QAAiB;IACvD,MAAM,IAAI,GAAG,QAAQ,IAAI,UAAU,CAAC;IACpC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,kBAAkB,EAAE,GAAG,CAAC,CAAC;IACvD,OAAO,yBAAyB,QAAQ,uBAAuB,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;AACpF,CAAC"}
@@ -1,17 +1,36 @@
1
1
  import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
2
+ import type { FileUploadSource } from '../../modules/file/interfaces/file-upload.interface';
3
+ export interface UploadAllowList {
4
+ extensions: readonly string[];
5
+ mimeTypes: readonly string[];
6
+ }
7
+ export declare const IMAGE_UPLOAD_ALLOW_LIST: UploadAllowList;
8
+ export declare const SCRIPTABLE_UPLOAD_MIME_TYPES: readonly string[];
9
+ export declare const SCRIPTABLE_UPLOAD_EXTENSIONS: readonly string[];
10
+ export interface MulterFileFilterOptions {
11
+ allowScriptableTypes?: boolean;
12
+ }
2
13
  export default class FileHelper {
3
14
  static multerRandomFileName(numberOfDigits?: number): (req: any, file: any, cb: any) => void;
4
- static multerFileFilter(fileTypeRegex?: RegExp): (req: any, file: any, cb: any) => any;
15
+ static multerFileFilter(accept?: RegExp | UploadAllowList, options?: MulterFileFilterOptions): (req: any, file: any, cb: any) => any;
5
16
  static multerOptionsForImageUpload(options: {
17
+ allowList?: UploadAllowList;
6
18
  destination?: string;
7
19
  fileSize?: number;
8
20
  fileTypeRegex?: RegExp;
9
21
  }): MulterOptions;
10
22
  }
11
- export declare function multerFileFilter(fileTypeRegex?: RegExp): (req: any, file: any, cb: any) => any;
23
+ export declare function multerFileFilter(accept?: RegExp | UploadAllowList, options?: MulterFileFilterOptions): (req: any, file: any, cb: any) => any;
12
24
  export declare function multerOptionsForImageUpload(options: {
25
+ allowList?: UploadAllowList;
13
26
  destination?: string;
14
27
  fileSize?: number;
15
28
  fileTypeRegex?: RegExp;
29
+ memory?: boolean;
16
30
  }): MulterOptions;
31
+ export declare function multerFileToUpload(file: {
32
+ buffer?: Buffer;
33
+ mimetype?: string;
34
+ originalname?: string;
35
+ }): FileUploadSource;
17
36
  export declare function multerRandomFileName(numberOfDigits?: number): (req: any, file: any, cb: any) => void;
@@ -1,47 +1,103 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SCRIPTABLE_UPLOAD_EXTENSIONS = exports.SCRIPTABLE_UPLOAD_MIME_TYPES = exports.IMAGE_UPLOAD_ALLOW_LIST = void 0;
3
4
  exports.multerFileFilter = multerFileFilter;
4
5
  exports.multerOptionsForImageUpload = multerOptionsForImageUpload;
6
+ exports.multerFileToUpload = multerFileToUpload;
5
7
  exports.multerRandomFileName = multerRandomFileName;
8
+ const crypto_1 = require("crypto");
6
9
  const multer_1 = require("multer");
7
10
  const path_1 = require("path");
11
+ const stream_1 = require("stream");
12
+ exports.IMAGE_UPLOAD_ALLOW_LIST = {
13
+ extensions: ['.jpeg', '.jpg', '.png'],
14
+ mimeTypes: ['image/jpeg', 'image/png'],
15
+ };
16
+ exports.SCRIPTABLE_UPLOAD_MIME_TYPES = [
17
+ 'application/javascript',
18
+ 'application/xhtml+xml',
19
+ 'application/xml',
20
+ 'image/svg+xml',
21
+ 'text/html',
22
+ 'text/javascript',
23
+ 'text/xml',
24
+ ];
25
+ exports.SCRIPTABLE_UPLOAD_EXTENSIONS = [
26
+ '.htm',
27
+ '.html',
28
+ '.js',
29
+ '.mjs',
30
+ '.svg',
31
+ '.xhtml',
32
+ '.xml',
33
+ ];
8
34
  class FileHelper {
9
35
  static multerRandomFileName(numberOfDigits = 32) {
10
36
  return multerRandomFileName(numberOfDigits);
11
37
  }
12
- static multerFileFilter(fileTypeRegex = /jpeg|jpg|png/) {
13
- return multerFileFilter(fileTypeRegex);
38
+ static multerFileFilter(accept = exports.IMAGE_UPLOAD_ALLOW_LIST, options) {
39
+ return multerFileFilter(accept, options);
14
40
  }
15
41
  static multerOptionsForImageUpload(options) {
16
42
  return multerOptionsForImageUpload(options);
17
43
  }
18
44
  }
19
45
  exports.default = FileHelper;
20
- function multerFileFilter(fileTypeRegex = /jpeg|jpg|png/) {
46
+ function normalizeMimeType(value) {
47
+ return String(value || '')
48
+ .split(';')[0]
49
+ .trim()
50
+ .toLowerCase();
51
+ }
52
+ function multerFileFilter(accept = exports.IMAGE_UPLOAD_ALLOW_LIST, options) {
21
53
  return (req, file, cb) => {
22
- const mimetype = fileTypeRegex.test(file.mimetype);
23
- const extName = fileTypeRegex.test((0, path_1.extname)(file.originalname).toLowerCase());
24
- if (mimetype && extName) {
54
+ const mimeType = normalizeMimeType(file?.mimetype);
55
+ const extension = (0, path_1.extname)(String(file?.originalname || '')).toLowerCase();
56
+ if (!options?.allowScriptableTypes &&
57
+ (exports.SCRIPTABLE_UPLOAD_MIME_TYPES.includes(mimeType) || exports.SCRIPTABLE_UPLOAD_EXTENSIONS.includes(extension))) {
58
+ return cb(new Error(`File upload rejected: ${mimeType || 'unknown type'} may execute as script`));
59
+ }
60
+ const accepted = accept instanceof RegExp
61
+ ? accept.test(mimeType) && accept.test(extension)
62
+ : accept.mimeTypes.includes(mimeType) && accept.extensions.includes(extension);
63
+ if (accepted) {
25
64
  return cb(null, true);
26
65
  }
27
- cb(`Error: File upload only supports the following filetypes - ${fileTypeRegex}`);
66
+ cb(new Error(`File upload only supports the following filetypes - ${describeAccept(accept)}`));
28
67
  };
29
68
  }
69
+ function describeAccept(accept) {
70
+ return accept instanceof RegExp ? String(accept) : accept.extensions.join(', ');
71
+ }
30
72
  function multerOptionsForImageUpload(options) {
31
73
  const config = {
32
74
  fileSize: 1024 * 1024,
33
- fileTypeRegex: /jpeg|jpg|png/,
34
75
  ...options,
35
76
  };
77
+ const accept = config.fileTypeRegex ?? config.allowList ?? exports.IMAGE_UPLOAD_ALLOW_LIST;
36
78
  return {
37
- fileFilter: config.fileTypeRegex ? multerFileFilter(config.fileTypeRegex) : undefined,
79
+ fileFilter: multerFileFilter(accept),
38
80
  limits: {
39
81
  fileSize: config.fileSize ? config.fileSize : undefined,
40
82
  },
41
- storage: (0, multer_1.diskStorage)({
42
- destination: config.destination ? config.destination : undefined,
43
- filename: multerRandomFileName(),
44
- }),
83
+ storage: config.memory
84
+ ? (0, multer_1.memoryStorage)()
85
+ : (0, multer_1.diskStorage)({
86
+ destination: config.destination ? config.destination : undefined,
87
+ filename: multerRandomFileName(),
88
+ }),
89
+ };
90
+ }
91
+ function multerFileToUpload(file) {
92
+ if (!file?.buffer) {
93
+ throw new Error('multerFileToUpload() needs an in-memory upload: pass `memory: true` to the multer options ' +
94
+ '(a disk-stored file has no buffer).');
95
+ }
96
+ return {
97
+ createReadStream: () => stream_1.Readable.from(file.buffer),
98
+ encoding: '7bit',
99
+ filename: file.originalname || (0, crypto_1.randomBytes)(16).toString('hex'),
100
+ mimetype: file.mimetype || 'application/octet-stream',
45
101
  };
46
102
  }
47
103
  function multerRandomFileName(numberOfDigits = 32) {
@@ -1 +1 @@
1
- {"version":3,"file":"file.helper.js","sourceRoot":"","sources":["../../../../src/core/common/helpers/file.helper.ts"],"names":[],"mappings":";;AAuCA,4CAUC;AAKD,kEAkCC;AAMD,oDAYC;AAzGD,mCAAqC;AACrC,+BAA+B;AAM/B,MAAqB,UAAU;IAKtB,MAAM,CAAC,oBAAoB,CAAC,cAAc,GAAG,EAAE;QACpD,OAAO,oBAAoB,CAAC,cAAc,CAAC,CAAC;IAC9C,CAAC;IAKM,MAAM,CAAC,gBAAgB,CAAC,aAAa,GAAG,cAAc;QAC3D,OAAO,gBAAgB,CAAC,aAAa,CAAC,CAAC;IACzC,CAAC;IAKM,MAAM,CAAC,2BAA2B,CAAC,OAIzC;QACC,OAAO,2BAA2B,CAAC,OAAO,CAAC,CAAC;IAC9C,CAAC;CACF;AA1BD,6BA0BC;AAKD,SAAgB,gBAAgB,CAAC,aAAa,GAAG,cAAc;IAC7D,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE;QACvB,MAAM,QAAQ,GAAG,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACnD,MAAM,OAAO,GAAG,aAAa,CAAC,IAAI,CAAC,IAAA,cAAO,EAAC,IAAI,CAAC,YAAY,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;QAE7E,IAAI,QAAQ,IAAI,OAAO,EAAE,CAAC;YACxB,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACxB,CAAC;QACD,EAAE,CAAC,8DAA8D,aAAa,EAAE,CAAC,CAAC;IACpF,CAAC,CAAC;AACJ,CAAC;AAKD,SAAgB,2BAA2B,CAAC,OAI3C;IAEC,MAAM,MAAM,GAAG;QACb,QAAQ,EAAE,IAAI,GAAG,IAAI;QACrB,aAAa,EAAE,cAAc;QAC7B,GAAG,OAAO;KACX,CAAC;IAEF,OAAO;QAEL,UAAU,EAAE,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,gBAAgB,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,SAAS;QAGrF,MAAM,EAAE;YAEN,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS;SACxD;QAID,OAAO,EAAE,IAAA,oBAAW,EAAC;YAInB,WAAW,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;YAGhE,QAAQ,EAAE,oBAAoB,EAAE;SACjC,CAAC;KACH,CAAC;AACJ,CAAC;AAMD,SAAgB,oBAAoB,CAAC,cAAc,GAAG,EAAE;IACtD,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE;QAEvB,MAAM,UAAU,GAAG,KAAK,CAAC,cAAc,CAAC;aACrC,IAAI,CAAC,IAAI,CAAC;aACV,GAAG,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;aACtD,IAAI,CAAC,EAAE,CAAC,CAAC;QAIZ,EAAE,CAAC,IAAI,EAAE,GAAG,UAAU,GAAG,IAAA,cAAO,EAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;IACzD,CAAC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"file.helper.js","sourceRoot":"","sources":["../../../../src/core/common/helpers/file.helper.ts"],"names":[],"mappings":";;;AAyJA,4CAyBC;AAcD,kEA8CC;AAcD,gDAiBC;AAMD,oDAYC;AA9RD,mCAAqC;AACrC,mCAAoD;AACpD,+BAA+B;AAC/B,mCAAkC;AA8BrB,QAAA,uBAAuB,GAAoB;IACtD,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC;IACrC,SAAS,EAAE,CAAC,YAAY,EAAE,WAAW,CAAC;CACvC,CAAC;AAYW,QAAA,4BAA4B,GAAsB;IAC7D,wBAAwB;IACxB,uBAAuB;IACvB,iBAAiB;IACjB,eAAe;IACf,WAAW;IACX,iBAAiB;IACjB,UAAU;CACX,CAAC;AAGW,QAAA,4BAA4B,GAAsB;IAC7D,MAAM;IACN,OAAO;IACP,KAAK;IACL,MAAM;IACN,MAAM;IACN,QAAQ;IACR,MAAM;CACP,CAAC;AAmBF,MAAqB,UAAU;IAKtB,MAAM,CAAC,oBAAoB,CAAC,cAAc,GAAG,EAAE;QACpD,OAAO,oBAAoB,CAAC,cAAc,CAAC,CAAC;IAC9C,CAAC;IAKM,MAAM,CAAC,gBAAgB,CAC5B,SAAmC,+BAAuB,EAC1D,OAAiC;QAEjC,OAAO,gBAAgB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC3C,CAAC;IAKM,MAAM,CAAC,2BAA2B,CAAC,OAKzC;QACC,OAAO,2BAA2B,CAAC,OAAO,CAAC,CAAC;IAC9C,CAAC;CACF;AA9BD,6BA8BC;AAMD,SAAS,iBAAiB,CAAC,KAAa;IACtC,OAAO,MAAM,CAAC,KAAK,IAAI,EAAE,CAAC;SACvB,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;SACb,IAAI,EAAE;SACN,WAAW,EAAE,CAAC;AACnB,CAAC;AAyBD,SAAgB,gBAAgB,CAC9B,SAAmC,+BAAuB,EAC1D,OAAiC;IAEjC,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE;QACvB,MAAM,QAAQ,GAAG,iBAAiB,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QACnD,MAAM,SAAS,GAAG,IAAA,cAAO,EAAC,MAAM,CAAC,IAAI,EAAE,YAAY,IAAI,EAAE,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;QAE1E,IACE,CAAC,OAAO,EAAE,oBAAoB;YAC9B,CAAC,oCAA4B,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,oCAA4B,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,EACrG,CAAC;YACD,OAAO,EAAE,CAAC,IAAI,KAAK,CAAC,yBAAyB,QAAQ,IAAI,cAAc,wBAAwB,CAAC,CAAC,CAAC;QACpG,CAAC;QAED,MAAM,QAAQ,GACZ,MAAM,YAAY,MAAM;YACtB,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC;YACjD,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;QAEnF,IAAI,QAAQ,EAAE,CAAC;YACb,OAAO,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QACxB,CAAC;QACD,EAAE,CAAC,IAAI,KAAK,CAAC,uDAAuD,cAAc,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC;IACjG,CAAC,CAAC;AACJ,CAAC;AAGD,SAAS,cAAc,CAAC,MAAgC;IACtD,OAAO,MAAM,YAAY,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAClF,CAAC;AASD,SAAgB,2BAA2B,CAAC,OAM3C;IAEC,MAAM,MAAM,GAAG;QACb,QAAQ,EAAE,IAAI,GAAG,IAAI;QACrB,GAAG,OAAO;KACX,CAAC;IAIF,MAAM,MAAM,GAA6B,MAAM,CAAC,aAAa,IAAI,MAAM,CAAC,SAAS,IAAI,+BAAuB,CAAC;IAE7G,OAAO;QAEL,UAAU,EAAE,gBAAgB,CAAC,MAAM,CAAC;QAGpC,MAAM,EAAE;YAEN,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS;SACxD;QASD,OAAO,EAAE,MAAM,CAAC,MAAM;YACpB,CAAC,CAAC,IAAA,sBAAa,GAAE;YACjB,CAAC,CAAC,IAAA,oBAAW,EAAC;gBAIV,WAAW,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;gBAGhE,QAAQ,EAAE,oBAAoB,EAAE;aACjC,CAAC;KACP,CAAC;AACJ,CAAC;AAcD,SAAgB,kBAAkB,CAAC,IAIlC;IACC,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CACb,4FAA4F;YAC1F,qCAAqC,CACxC,CAAC;IACJ,CAAC;IACD,OAAO;QACL,gBAAgB,EAAE,GAAG,EAAE,CAAC,iBAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC;QAClD,QAAQ,EAAE,MAAM;QAChB,QAAQ,EAAE,IAAI,CAAC,YAAY,IAAI,IAAA,oBAAW,EAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC;QAC9D,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,0BAA0B;KACtD,CAAC;AACJ,CAAC;AAMD,SAAgB,oBAAoB,CAAC,cAAc,GAAG,EAAE;IACtD,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE;QAEvB,MAAM,UAAU,GAAG,KAAK,CAAC,cAAc,CAAC;aACrC,IAAI,CAAC,IAAI,CAAC;aACV,GAAG,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;aACtD,IAAI,CAAC,EAAE,CAAC,CAAC;QAIZ,EAAE,CAAC,IAAI,EAAE,GAAG,UAAU,GAAG,IAAA,cAAO,EAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;IACzD,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,2 @@
1
+ import { INestApplication } from '@nestjs/common';
2
+ export declare function installGracefulShutdown<T extends INestApplication>(app: T): T;