@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
@@ -40,7 +40,9 @@ The `CoreModule` is a dynamic module that bootstraps the entire framework:
40
40
  | **Dual API Support** | GraphQL and REST in the same application |
41
41
  | **Security Pipeline** | 4 global interceptors, global validation pipe, middleware stack |
42
42
  | **Mongoose Plugins** | Auto-registration of ID, password, audit, and role guard plugins |
43
- | **GraphQL Subscriptions** | WebSocket support with JWT/session authentication |
43
+ | **GraphQL Subscriptions** | WebSocket support with JWT/session authentication (cluster-wide when `redis` is configured — see the Subscriptions row under GraphQL Features) |
44
+ | **Central Redis (optional)** | `CoreRedisService` — globally provided **and exported** by `CoreModule`, always present but **inert unless `redis` is configured** ("presence implies enabled"). Injected with `@Optional()`; every consumer keeps a process-local fallback. One service serves all features: shared client (`getClient()`), one cached subscriber (`getSubscriber()` — a subscribing client cannot run commands), dedicated connections (`createClient(label)`); all tracked and quit on shutdown. Keys are namespaced by the framework per key via `key(...)`, **not** through ioredis's own `keyPrefix` (that would collide with BullMQ's prefix). Requires the OPTIONAL peer `ioredis` — configured but missing **fails the boot**. Switches on: exact cross-replica rate limits, cron deduplication, `CoreRedisPubSub` as `PUB_SUB`, tenant-cache invalidation broadcast, Hub collector mirroring, MCP session registry |
45
+ | **Central S3 (optional)** | `CoreS3Service` — globally provided **and exported** by `CoreModule`, inert unless `s3` is configured *and* names a `bucket` (a bucket-less block is ignored with a warning). Backs `file.storage: 's3'` and TUS staging (`tus.s3Staging`). Requires the OPTIONAL peers `@aws-sdk/client-s3` (+ `@aws-sdk/s3-request-presigner` for `presignedDownloads`) — configured but missing **fails the boot** |
44
46
  | **Configuration System** | `config.env.ts` with ENV variables, `NEST_SERVER_CONFIG` JSON, `NSC__*` prefixes |
45
47
  | **Cookie Handling** | Enabled by default (`cookies: true`), configurable via `ICookiesConfig` with `exposeTokenInBody` option |
46
48
  | **Unified CORS** | Single `cors` config propagates to GraphQL, REST, and BetterAuth layers |
@@ -61,7 +63,7 @@ Modern OAuth-compatible authentication with plugin architecture:
61
63
  | **Social Login** | OAuth providers: Google, GitHub, Apple, Discord, etc. (plugin) |
62
64
  | **Email Verification** | Configurable email verification flow |
63
65
  | **Sign-Up Validation** | Custom validation hooks for registration |
64
- | **Rate Limiting** | Per-endpoint rate limits (configurable) |
66
+ | **Rate Limiting** | Per-endpoint rate limits (`betterAuth.rateLimit`, configurable). Counters live behind a `RateLimitStore`: `RedisRateLimitStore` when `redis` is configured, so `max` is enforced **exactly across replicas** instead of `max × replicas`; otherwise the process-local `InMemoryRateLimitStore` as before. `check()` / `reset()` / `clear()` are **async** since 11.33.0. On a Redis outage it degrades to the in-memory counter and logs once per transition — never a 500, never "allowed". Counters are keyed on `request.ip`, which Express derives from `X-Forwarded-For` only as far as `trust proxy` allows — set `trustProxy` (§ServerOptions) behind a reverse proxy or every client resolves to the proxy and shares ONE bucket; unset with a limiter enabled logs a boot warning |
65
67
  | **Cross-Subdomain Cookies** | Automatic cookie domain configuration |
66
68
  | **Organization / Multi-Tenant** | Teams and organization management (plugin) |
67
69
  | **3 Registration Patterns** | Zero-config, overrides parameter, or manual (`autoRegister: false`) |
@@ -75,7 +77,7 @@ JWT-based authentication for existing projects:
75
77
  | **JWT Authentication** | Bearer token auth with Passport strategies |
76
78
  | **Refresh Tokens** | Automatic token renewal |
77
79
  | **Sign In / Sign Up / Logout** | GraphQL mutations + REST endpoints |
78
- | **Rate Limiting** | Configurable per-endpoint rate limits |
80
+ | **Rate Limiting** | Configurable per-endpoint rate limits (`auth.rateLimit`). Same `RateLimitStore` selection, async signatures and Redis-outage degradation as the BetterAuth row above (namespace `legacy-auth`) |
79
81
  | **Legacy Endpoint Controls** | Disable legacy endpoints after migration (`auth.legacyEndpoints`) |
80
82
  | **Migration Tracking** | `betterAuthMigrationStatus` query for monitoring |
81
83
 
@@ -158,9 +160,10 @@ JWT-based authentication for existing projects:
158
160
  | Feature | Description |
159
161
  |---------|-------------|
160
162
  | **File Module** | Upload/download with MongoDB GridFS storage |
161
- | **REST Endpoints** | `GET /files/:id`, `POST /files/upload`, `DELETE /files/:id` |
162
- | **GraphQL Endpoints** | `uploadFile`, `file`, `fileByFilename`, `deleteFile` |
163
- | **TUS Module** | Resumable uploads via tus.io protocol (creation, termination, expiration) |
163
+ | **REST Endpoints** | `GET /files/id/:id`, `GET /files/:filename` (core, gated by `file.downloadRoles`, default ADMIN); `POST /files/upload`, `DELETE /files/:id` (project-specific) |
164
+ | **GraphQL Endpoints** | `getFileInfo` (`file.downloadRoles`), `uploadFile` / `uploadFiles` (`file.uploadRoles`), `deleteFile` (`file.deleteRoles`) — all default ADMIN |
165
+ | **File access control** | Roles are the coarse filter; per-file rules go in `CoreFileService.checkRights()` using metadata written at upload time. Both file classes carry `@SkipTenantCheck()` — GridFS is not tenant-scoped |
166
+ | **TUS Module** | Resumable uploads via tus.io protocol (creation, termination, expiration), gated by `tus.roles` (default `S_USER`); `OPTIONS` stays public for the CORS preflight |
164
167
  | **GridFS Migration** | Completed TUS uploads auto-migrate to GridFS |
165
168
  | **CORS Support** | Automatic CORS headers for browser uploads |
166
169
 
@@ -201,7 +204,7 @@ JWT-based authentication for existing projects:
201
204
  |---------|-------------|
202
205
  | **Apollo Server** | Full GraphQL server with schema-first or code-first |
203
206
  | **Custom Scalars** | `Date`, `DateTime` (timestamp), `JSON`, `Any` |
204
- | **Subscriptions** | WebSocket support via `graphql-ws` with auth |
207
+ | **Subscriptions** | WebSocket support via `graphql-ws` with auth. The `PUB_SUB` provider is built from a factory: `CoreRedisPubSub` when `redis` is configured (delivery is then cluster-wide), the in-memory `PubSub` otherwise (delivery only to clients connected to the publishing replica). **Constraint once Redis is in play: every published payload must be JSON-serializable** — it crosses the wire as JSON, so `Date`, class instances, `Map`/`Set` and `undefined` do not survive the round trip. An in-process `PubSub` never had this constraint, so a payload that worked on one replica can silently lose fields on a cluster. Publish plain objects and ISO strings |
205
208
  | **Complexity Analysis** | Query cost calculation to prevent DoS attacks |
206
209
  | **Enum Registration** | `registerEnum()` helper for GraphQL enum types |
207
210
  | **Upload Support** | `graphqlUploadExpress()` for multipart file uploads |
@@ -214,7 +217,8 @@ JWT-based authentication for existing projects:
214
217
  | **Error Code Module** | Centralized error registry with unique IDs |
215
218
  | **Permissions Report** | Interactive HTML dashboard, JSON, and Markdown reports |
216
219
  | **Hub (Operator Cockpit)** | Build-free ADMIN-gated dashboard at `/hub` (config-gated per environment). Adds an optional HTTP trace middleware (registered by `CoreHubModule.configure()` only when traces are enabled), a chaining `Logger.overrideLogger()` delegate for the log buffer, an optional `EmailService` capture hook (`HUB_EMAIL_CAPTURE` token) for the mailbox, and — when the query profiler is enabled — opts the MongoDB driver into `monitorCommands` from `core.module.ts`. See `src/core/modules/hub/README.md`. |
217
- | **Process Diagnostics** | Opt-in process-level exit diagnostics (`installProcessDiagnostics()` + `handleFatalBootstrapError`, `src/core/common/helpers/process-diagnostics.helper.ts`). Wired into `main.ts` — **NOT** into `CoreModule.forRoot()`, because it must run before `NestFactory.create()` and installs a `process.exit(1)` path that must never arm inside `Test.createTestingModule()`. Logs unhandled rejections without crashing (configurable), uncaught exceptions before the exit, non-zero exit codes, and labels SIGTERM/SIGINT/SIGHUP/SIGQUIT as external terminations. Pair with `server.enableShutdownHooks()` see the helper docblock |
220
+ | **Process Diagnostics** | Opt-in process-level exit diagnostics (`installProcessDiagnostics()` + `handleFatalBootstrapError`, `src/core/common/helpers/process-diagnostics.helper.ts`). Wired into `main.ts` — **NOT** into `CoreModule.forRoot()`, because it must run before `NestFactory.create()` and installs a `process.exit(1)` path that must never arm inside `Test.createTestingModule()`. Logs unhandled rejections without crashing (configurable), uncaught exceptions before the exit, non-zero exit codes, and labels SIGTERM/SIGINT/SIGHUP/SIGQUIT as external terminations. Pair with `installGracefulShutdown(server)` — **not** with `server.enableShutdownHooks()`, see the Graceful Shutdown row below |
221
+ | **Graceful Shutdown** | `installGracefulShutdown(app)` (`src/core/common/helpers/graceful-shutdown.helper.ts`), wired in `main.ts`. It **REPLACES** `server.enableShutdownHooks()` and must not be used alongside it: with `shutdownDelayMs` set, Nest would register its own listener for the same signals and close the app in parallel with the wait, so the delay silently never happens. At `shutdownDelayMs: 0` (the default) the helper simply *is* `enableShutdownHooks()`, so the single call is correct either way. With a delay it waits **inside the SIGTERM/SIGINT handler, before `close()` is entered** — a NestJS lifecycle hook cannot do this, because `close()` runs `onModuleDestroy` → `beforeApplicationShutdown` → dispose → `onApplicationShutdown`, i.e. a delay in a hook would wait with every module already torn down while the socket still accepts. A second signal cancels the pending wait and closes immediately. Warns above 10 000 ms, capped at 60 000 ms — keep it below the orchestrator grace period (Compose 10 s, Kubernetes 30 s) and below `installProcessDiagnostics()`'s 30 s force-exit |
218
222
  | **System Setup Module** | Initial admin creation for fresh deployments |
219
223
  | **Cron Jobs** | `CoreCronJobsService` with timezone/UTC offset support |
220
224
  | **Model Documentation** | Auto-generated model docs via `ModelDocService` |
@@ -425,6 +429,51 @@ The following diagram shows the exact order of execution from HTTP request to re
425
429
  +---------------------+
426
430
  ```
427
431
 
432
+ ### Shutdown Flow (SIGTERM / SIGINT)
433
+
434
+ The mirror image of the request flow, and the one place where an ordering mistake is invisible until
435
+ a rolling deploy drops requests. Installed in `main.ts` by `installGracefulShutdown(server)` — which
436
+ **replaces** `server.enableShutdownHooks()`, never accompanies it.
437
+
438
+ ```
439
+ SIGTERM / SIGINT
440
+ |
441
+ v
442
+ +-------------------------------------------------------------+
443
+ | installGracefulShutdown() signal handler |
444
+ | |
445
+ | shutdownDelayMs === 0 (default) |
446
+ | -> this IS app.enableShutdownHooks(): close() immediately |
447
+ | |
448
+ | shutdownDelayMs > 0 |
449
+ | -> stay FULLY HEALTHY for N ms (routes still served), |
450
+ | so the load balancer can finish deregistering |
451
+ | -> a second signal cancels the wait and closes now |
452
+ +----------------------------+---------------------------------+
453
+ | (only after the wait)
454
+ v
455
+ +-------------------------------------------------------------+
456
+ | app.close() |
457
+ | 1. onModuleDestroy |
458
+ | 2. beforeApplicationShutdown |
459
+ | 3. dispose (HTTP server socket closes HERE) |
460
+ | 4. onApplicationShutdown |
461
+ | - CoreRedisService quits every tracked connection |
462
+ +-------------------------------------------------------------+
463
+ ```
464
+
465
+ **Why the wait cannot be a lifecycle hook:** the socket only closes at step 3, so a delay placed in
466
+ `beforeApplicationShutdown` (step 2) would keep accepting traffic with every module already torn
467
+ down — strictly worse than not waiting at all.
468
+
469
+ **Why both together is a bug:** with `enableShutdownHooks()` also installed, Nest registers its own
470
+ listener for the same signals and enters `close()` in parallel with the wait. Nothing errors; the
471
+ delay simply never happens.
472
+
473
+ | Knob | Default | Notes |
474
+ |------|---------|-------|
475
+ | `shutdownDelayMs` | `0` (no delay, no log) | Warns above `10000`, capped at `60000`. Keep it below the orchestrator grace period (Compose `stop_grace_period` 10 s, Kubernetes `terminationGracePeriodSeconds` 30 s) **and** below `installProcessDiagnostics()`'s 30 s force-exit — exceed any and the process is SIGKILLed mid-wait with no hook running. Non-numeric/negative behaves like `0` |
476
+
428
477
  ---
429
478
 
430
479
  ## Phase 1: Incoming Request
@@ -11,16 +11,18 @@ A green `pnpm audit` inside the framework repo says nothing about your tree.
11
11
 
12
12
  ## What this concretely means for you
13
13
 
14
- The framework pulls in two transitive packages that resolve to a **vulnerable** version unless you
14
+ The framework pulls in three transitive packages that resolve to a **vulnerable** version unless you
15
15
  override them yourself:
16
16
 
17
17
  | Package | Advisory | Why it cannot resolve forward on its own |
18
18
  |---------|----------|------------------------------------------|
19
19
  | `ws` | [GHSA-96hv-2xvq-fx4p](https://github.com/advisories/GHSA-96hv-2xvq-fx4p) — high: memory-exhaustion DoS + uninitialized memory disclosure. Patched `>=8.21.0` | `@nestjs/graphql` declares `"ws": "8.20.1"` — an **exact pin**, not a caret. No amount of updating moves it |
20
20
  | `@hono/node-server` | [GHSA-frvp-7c67-39w9](https://github.com/advisories/GHSA-frvp-7c67-39w9) (static-file path traversal) + [GHSA-9mqv-5hh9-4cgg](https://github.com/advisories/GHSA-9mqv-5hh9-4cgg) (unauthenticated memory leak). Patched `>=2.0.10` | `@modelcontextprotocol/sdk` declares `^1.19.9` and ships **no 1.x fix line**, so the fix is only available across a major |
21
+ | `js-yaml` | [GHSA-pm4m-ph32-ghv5](https://github.com/advisories/GHSA-pm4m-ph32-ghv5) — high: exponential parsing time in flow collections (DoS). Patched `>=5.2.2` | `@nestjs/swagger` declares `"js-yaml": "5.2.1"` — an **exact pin**, the same shape as the `ws` case. It cannot resolve forward |
21
22
 
22
23
  `@nestjs/graphql` is a plain `dependencies` entry, so `ws` is installed even when you run with
23
- `graphQl: false`. Neither package is optional in practice.
24
+ `graphQl: false`. `@nestjs/swagger` is likewise a plain `dependencies` entry. None of the three is
25
+ optional in practice.
24
26
 
25
27
  ## The fix
26
28
 
@@ -31,8 +33,9 @@ Add this to your project's `pnpm-workspace.yaml` (pnpm 11+; in pnpm 10 and earli
31
33
  overrides:
32
34
  # @nestjs/graphql exact-pins ws@8.20.1 (GHSA-96hv-2xvq-fx4p, high, patched >=8.21.0).
33
35
  # An exact pin cannot resolve forward — the override is the only fix.
36
+ # Keep the target in LOCKSTEP with your direct `ws` dependency (see the note below).
34
37
  # Remove once @nestjs/graphql stops pinning it.
35
- 'ws@>=8.0.0 <8.21.0': '8.21.1'
38
+ 'ws@>=8.0.0 <8.21.0': '8.21.3'
36
39
 
37
40
  # @modelcontextprotocol/sdk declares @hono/node-server ^1.19.9 with no 1.x fix line
38
41
  # (GHSA-frvp-7c67-39w9, GHSA-9mqv-5hh9-4cgg). Deliberately a CROSS-MAJOR override.
@@ -40,8 +43,30 @@ overrides:
40
43
  # `(fetchCallback, options?)` is unchanged in 2.x. Engines >=20 and peer hono@^4 both fit.
41
44
  # Remove once @modelcontextprotocol/sdk moves its own range to ^2.
42
45
  '@hono/node-server@<2.0.10': '2.0.11'
46
+
47
+ # @nestjs/swagger exact-pins js-yaml@5.2.1 (GHSA-pm4m-ph32-ghv5, high, patched >=5.2.2).
48
+ # Same shape as the ws entry: an exact pin cannot resolve forward.
49
+ # Remove once @nestjs/swagger stops pinning it.
50
+ 'js-yaml@>=5.0.0 <5.2.2': '5.2.2'
43
51
  ```
44
52
 
53
+ ### The `ws` target must stay in lockstep with the declared `ws` version
54
+
55
+ `8.21.3` is not an arbitrary "latest patch" — it is the version `@lenne.tech/nest-server` declares as
56
+ an ordinary **dependency**, so it is already in your tree whether or not you list `ws` yourself.
57
+ The override target has to equal it.
58
+
59
+ Under `nodeLinker: hoisted` (what this framework and the starters use), a lower target does not
60
+ merely leave you one patch behind: it puts a **second, older `ws` copy** in the tree next to the
61
+ declared one. The transitive consumers resolve to the override target, the declared dependency stays
62
+ where its `package.json` pins it, and you now carry two `ws` versions — of which only one is visible
63
+ when you read a manifest. The target was raised `8.21.1` → `8.21.3` on 2026-08-10 for exactly this
64
+ reason.
65
+
66
+ **Rule:** whenever the `ws` version declared by `@lenne.tech/nest-server` (or by your own
67
+ `package.json`, if you list it) moves, move this override target with it in the same commit. The
68
+ same applies to any other override whose package also appears as a declared dependency.
69
+
45
70
  Then:
46
71
 
47
72
  ```bash
@@ -52,8 +77,8 @@ pnpm test # nothing should regress
52
77
 
53
78
  Commit `package.json`/`pnpm-workspace.yaml` **and** `pnpm-lock.yaml` together.
54
79
 
55
- > Projects generated from `nest-server-starter` or `lt-monorepo` already carry both entries. This
56
- > page is for projects that predate that, or that were assembled by hand.
80
+ > Projects generated from `nest-server-starter` or `lt-monorepo` already carry all three entries.
81
+ > This page is for projects that predate that, or that were assembled by hand.
57
82
 
58
83
  ## Rules for writing your own overrides
59
84
 
@@ -0,0 +1,335 @@
1
+ # Migration Guide: 11.32.3 → 11.32.4
2
+
3
+ ## Overview
4
+
5
+ | Category | Details |
6
+ |----------|---------|
7
+ | **Breaking Changes** | None in signatures. Six **runtime behaviour** changes you inherit automatically — see §1, §2, §4, §5, §6, §7 |
8
+ | **New Features** | `assertGridFsFileComplete()` — verify a stored GridFS file's chunk completeness; `UploadAllowList` — exact-matching upload filters, with `SCRIPTABLE_UPLOAD_MIME_TYPES` / `SCRIPTABLE_UPLOAD_EXTENSIONS` and the `allowScriptableTypes` opt-out |
9
+ | **Bugfixes** | GridFS uploads no longer report success for incomplete files, no longer hang on an unreadable source, and no longer leak their connection; file downloads answer an honest 404 instead of dropping the socket; the migrate CLI now actually terminates and no longer truncates its own output; the upload filter no longer matches mimetypes and extensions as SUBSTRINGS (`te?xt` accepted `text/html`), no longer reports rejections as a bare string, and `multerOptionsForImageUpload` no longer silently disables all filtering when `fileTypeRegex` is `undefined` |
10
+ | **Migration Effort** | No code changes for most projects, and nothing to configure. Read §1 if you have seed migrations that upload assets, §4 if any test asserts on download error messages, §5–§7 if your project accepts uploads |
11
+
12
+ ---
13
+
14
+ ## Quick Migration
15
+
16
+ ```bash
17
+ pnpm update @lenne.tech/nest-server@11.32.4
18
+ pnpm run build
19
+ pnpm test
20
+ ```
21
+
22
+ No configuration change is required. Existing `fileTypeRegex` arguments keep working.
23
+
24
+ **Vendor-mode projects:** four modified files under `src/core/`
25
+ (`common/helpers/file.helper.ts`, `modules/file/core-file.controller.ts`,
26
+ `modules/migrate/cli/migrate-cli.ts`, `modules/migrate/helpers/migration.helper.ts`) plus
27
+ `bin/migrate.js`. No moved files — there is no atomic file-set hazard for a partial sync.
28
+
29
+ ---
30
+
31
+ ## 1. `uploadFileToGridFS()` now fails loudly instead of quietly
32
+
33
+ Previously the helper resolved as soon as the write stream emitted `'finish'`. That event says the
34
+ stream ended — it does **not** prove every chunk document is durably stored. A connection lost at the
35
+ wrong moment left a files document promising more bytes than existed, the migration reported success,
36
+ and the defect surfaced much later as a broken download from a record that looked perfectly healthy.
37
+
38
+ It now verifies chunk completeness before resolving, and **rejects** when the file is incomplete
39
+ (removing the incomplete file so retries do not accumulate orphans).
40
+
41
+ **What you may notice:** a seed migration that previously "succeeded" while silently storing a broken
42
+ asset now fails.
43
+
44
+ **What that does to a deployment depends on the `docker-entrypoint.sh` your project copied — check
45
+ yours, and check its default.**
46
+
47
+ | Your entrypoint | A failed migration |
48
+ |-----------------|--------------------|
49
+ | The one shipped with this framework | Refuses the start (`MIGRATE_FAILURE_POLICY` defaults to `abort`) |
50
+ | From `nest-server-starter` 11.32.4 or newer | Starts the server anyway — the same variable exists, but it defaults to `warn`. Set `MIGRATE_FAILURE_POLICY=abort` per stage to refuse the start |
51
+ | Older, without that variable | Starts the server anyway, with no way to change it short of updating the file |
52
+
53
+ In the two lower rows the failure is visible only in the container log, and the broken asset still
54
+ ships — so if you deploy from a starter-derived project, decide on `abort` explicitly rather than
55
+ assuming this release stops a bad boot for you.
56
+
57
+ **Failing is intentional** — finding exactly this is why migrations run before the server. To recover:
58
+
59
+ ```bash
60
+ # 1. Identify the incomplete file from the migration error, which names it:
61
+ # GridFS file 'logo.png' is incomplete: 1 of 3 chunks stored (id 6a6b…)
62
+ # 2. Verify the source asset is actually present and readable in the image/checkout.
63
+ # 3. Re-run. The helper deletes its own incomplete upload, but a file left behind by
64
+ # an OLDER version of the helper must be removed manually:
65
+ # db.getCollection('<bucket>.files').deleteOne({ _id: ObjectId('…') })
66
+ # db.getCollection('<bucket>.chunks').deleteMany({ files_id: ObjectId('…') })
67
+ ```
68
+
69
+ Two more changes to the same helper, both strictly better and requiring no action:
70
+
71
+ - **An unreadable source rejects instead of hanging.** `pipe()` does not forward read-stream errors,
72
+ so a missing file used to leave the promise pending forever — a migration that never returned.
73
+ It now rejects with the underlying `ENOENT`.
74
+ - **The connection is always closed.** The client it opens is registered and closed on every path.
75
+ A leaked client keeps an SDAM monitor timer alive, which kept the whole CLI from exiting.
76
+
77
+ **Note on empty files:** GridFS stores a zero-byte file with **no** chunk documents at all, and the
78
+ completeness check accounts for that. Uploading an empty placeholder asset is valid and passes.
79
+
80
+ ---
81
+
82
+ ## 2. `migrate up` now terminates explicitly
83
+
84
+ The CLI drains stdout/stderr and then exits, instead of waiting for the event loop to empty. A single
85
+ handle left behind by MongoDB, GridFS or the state store used to keep the process alive after the work
86
+ was done: the CLI printed "All migrations completed successfully" and never returned. On a developer
87
+ machine that is invisible; in CI the job blocks until its timeout, and a container that runs
88
+ migrations before starting the server never reaches the server at all.
89
+
90
+ Exit codes are unchanged (`0` success, `1` failure), and the drain happens **before** the exit, so no
91
+ output is lost — `process.exit()` does not flush an asynchronous pipe, which is exactly what Docker's
92
+ log driver and CI log collectors are.
93
+
94
+ **Action required: none**, unless you invoke the CLI in a non-standard way. If you call the CLI
95
+ module directly rather than through the `migrate` / `nest-migrate` bin, use the new exported
96
+ `runCli()` (which drains and exits) instead of `main()` (which merely resolves):
97
+
98
+ ```typescript
99
+ // Before — resolves, but relies on the event loop draining by itself
100
+ const { main } = require('@lenne.tech/nest-server/dist/core/modules/migrate/cli/migrate-cli');
101
+ main();
102
+
103
+ // After
104
+ const { runCli } = require('@lenne.tech/nest-server/dist/core/modules/migrate/cli/migrate-cli');
105
+ void runCli();
106
+ ```
107
+
108
+ The shipped `bin/migrate.js` already prefers `runCli()` and falls back to `main()`, so a mixed
109
+ version pair keeps working.
110
+
111
+ ---
112
+
113
+ ## 3. New: `assertGridFsFileComplete()`
114
+
115
+ Exported for direct use when you need to check a file that something else wrote — a restored dump,
116
+ another service, a manual upload:
117
+
118
+ ```typescript
119
+ import { assertGridFsFileComplete, getDb } from '@lenne.tech/nest-server';
120
+
121
+ const db = await getDb(process.env.MONGODB_URL);
122
+ await assertGridFsFileComplete(db, 'images', fileId, 'logo.png'); // throws if incomplete
123
+ ```
124
+
125
+ It counts chunk documents rather than reading bytes back, so a chunk that was written but truncated
126
+ is not detected. See `src/core/modules/migrate/README.md`.
127
+
128
+ ---
129
+
130
+ ## 4. File downloads: error responses changed
131
+
132
+ Two changes on `GET /files/id/:id` and `GET /files/:filename`.
133
+
134
+ **(a) A GridFS read error is now a 404, not a dropped socket.** The stream error previously went
135
+ unhandled, Node destroyed the socket mid-response, and a reverse proxy turned that into
136
+ **502 Bad Gateway** — reading as "the server is down" while every other route answered normally.
137
+ The response is now:
138
+
139
+ ```json
140
+ { "error": "Not Found", "message": "#LTNS_0500: File not found", "statusCode": 404 }
141
+ ```
142
+
143
+ Once bytes are already on the wire there is no status left to send, so the connection is still closed
144
+ — at that point it genuinely is a truncated transfer.
145
+
146
+ **(b) Exception messages now carry their ErrorCode.** The controller uses the framework registry
147
+ instead of raw strings, matching every other core module:
148
+
149
+ | Before | After |
150
+ |--------|-------|
151
+ | `'File not found'` | `ErrorCode.FILE_NOT_FOUND` → `'#LTNS_0500: File not found'` |
152
+ | `'Missing file ID for download'` | `ErrorCode.REQUIRED_FIELD_MISSING` → `'#LTNS_0301: Required field missing'` |
153
+
154
+ **Action required:** if a test asserts on the exact message, loosen it to a substring match — the old
155
+ text is still contained in the new one. Frontends using `useLtErrorTranslation()` gain a translated
156
+ message where they previously showed raw English.
157
+
158
+ **Also:** a project that restricted downloads by overriding `CoreFileService.checkRights()` used to
159
+ get a **500** (the refusal produced `null.pipe(res)` → `TypeError`). It is now a **404** —
160
+ deliberately the same answer as an unknown id, so the endpoint cannot be used to probe which files
161
+ exist.
162
+
163
+ To customise the status, body or logging of the streaming error path, override the new
164
+ `protected pipeFileToResponse()` method on your controller:
165
+
166
+ ```typescript
167
+ export class FileController extends CoreFileController {
168
+ protected override pipeFileToResponse(stream: Readable, res: Response): Response {
169
+ // e.g. report a different status, or add your own telemetry
170
+ return super.pipeFileToResponse(stream, res);
171
+ }
172
+ }
173
+ ```
174
+
175
+ ---
176
+
177
+ ## 5. The upload filter matched substrings, not values
178
+
179
+ **What changed:** `multerFileFilter` compared the mimetype and the extension by
180
+ `RegExp.test()` — a substring search. Every alternative therefore matched
181
+ anywhere inside either value.
182
+
183
+ **Why it matters:** an allow-list containing `text` or `txt` also accepted
184
+ `text/html` and `text/xml`. A file named `x.txt` and sent as `text/html` passed
185
+ both halves of a filter whose own comment said "no html". `md` matched every
186
+ mimetype containing "md", `zip` every one containing "zip". The single
187
+ alternative was never the bug — the substring semantics were.
188
+
189
+ Anchoring the expression was not an option: the SAME expression was tested
190
+ against two different value spaces, so it had to carry mimetype FRAGMENTS
191
+ (`wordprocessingml`, `ms-excel`) next to bare extensions, and no `^…$` satisfies
192
+ both at once.
193
+
194
+ **What to do:** nothing, if your filter only ever listed image formats — those
195
+ never matched markup. If your filter lists text-ish or document formats, move to
196
+ the new exact-matching form:
197
+
198
+ ```ts
199
+ // Before — substring matching
200
+ multerOptionsForImageUpload({ fileTypeRegex: /jpeg|jpg|png|pdf|te?xt|csv/ });
201
+
202
+ // After — whole-value matching
203
+ multerOptionsForImageUpload({
204
+ allowList: {
205
+ extensions: ['.csv', '.jpeg', '.jpg', '.pdf', '.png', '.txt'],
206
+ mimeTypes: ['application/pdf', 'image/jpeg', 'image/png', 'text/csv', 'text/plain'],
207
+ },
208
+ });
209
+ ```
210
+
211
+ The two conditions stay **independent**: either one alone rejects the file, but a
212
+ pair that is odd yet individually allowed (`report.txt` announced as
213
+ `application/pdf`) passes. An extension→mimetype MAPPING is deliberately not
214
+ enforced — user agents genuinely disagree about office and audio types (macOS
215
+ reports `.csv` as `text/plain`), so a mapping rejects legitimate uploads.
216
+
217
+ `fileTypeRegex` still works and is not removed; it is marked deprecated.
218
+
219
+ ---
220
+
221
+ ## 6. Scriptable types are now rejected on BOTH forms
222
+
223
+ **What changed:** `text/html`, `text/xml`, `application/xhtml+xml`,
224
+ `image/svg+xml`, JavaScript types and the matching extensions (`.html`, `.svg`,
225
+ `.js`, `.xml`, …) are rejected before the allow-list is consulted — including
226
+ when a legacy `fileTypeRegex` would have matched them.
227
+
228
+ **Why it matters:** this is what closes §5 for expressions that already exist in
229
+ consumer projects, without anyone having to rewrite them. The danger does not
230
+ depend on what an endpoint meant to accept: a stored upload served back from the
231
+ API origin with one of these content types executes in that origin, with the
232
+ victim's session.
233
+
234
+ **What to do:** if you deliberately accept SVG logos or HTML fragments, opt out
235
+ explicitly:
236
+
237
+ ```ts
238
+ multerFileFilter(
239
+ { extensions: ['.svg'], mimeTypes: ['image/svg+xml'] },
240
+ { allowScriptableTypes: true },
241
+ );
242
+ ```
243
+
244
+ Only do that when the file is never served from an origin that carries a session
245
+ — e.g. a separate download host, or a route that always answers with
246
+ `Content-Disposition: attachment` **and** `X-Content-Type-Options: nosniff`.
247
+
248
+ ---
249
+
250
+ ## 7. `fileTypeRegex: undefined` no longer disables filtering
251
+
252
+ **What changed:** `multerOptionsForImageUpload` built its config as
253
+ `{ fileTypeRegex: /jpeg|jpg|png/, ...options }` and then installed a filter only
254
+ `if (config.fileTypeRegex)`. Passing the key explicitly as `undefined`
255
+ overwrote the default, so **no filter was installed at all** and every file type
256
+ was accepted — on a helper named "ImageUpload".
257
+
258
+ **Why it matters:** this is easy to trigger by accident rather than intent:
259
+
260
+ ```ts
261
+ // The author meant "then just the standard image types".
262
+ // Before: they got "then everything".
263
+ multerOptionsForImageUpload({ fileTypeRegex: allowSvg ? /jpeg|jpg|png|svg/ : undefined });
264
+ ```
265
+
266
+ Any optional variable threaded into that option had the same effect.
267
+
268
+ **What to do:** if a project relied on this to accept arbitrary types, it now
269
+ gets JPEG/PNG only and those uploads start failing. Pass an explicit `allowList`
270
+ naming what the endpoint really accepts. There is intentionally no "accept
271
+ everything" switch — an upload endpoint that takes any type should not be built
272
+ on `multerOptionsForImageUpload`.
273
+
274
+ ---
275
+
276
+ ## 8. Upload rejections are a real `Error`
277
+
278
+ **What changed:** the filter called `cb('Error: File upload only supports …')`
279
+ with a bare **string**. It now passes an `Error`.
280
+
281
+ **Why it matters:** a string has no `message`, so NestJS's `transformException`
282
+ could not map it and the request surfaced as a 500 instead of a 4xx.
283
+
284
+ **What to do:** nothing, unless a test asserts on the exact error value. Assert
285
+ on `error.message` instead.
286
+
287
+ ---
288
+
289
+ ## 9. Dependency housekeeping (no action required)
290
+
291
+ - `js-yaml` gained an override for [GHSA-pm4m-ph32-ghv5](https://github.com/advisories/GHSA-pm4m-ph32-ghv5)
292
+ (high). **`@nestjs/swagger` exact-pins `js-yaml@5.2.1`, so your project needs this override too** —
293
+ a framework override does not reach consumer trees. See
294
+ [`docs/security-overrides.md`](../docs/security-overrides.md), which now lists all three affected
295
+ packages.
296
+ - The `minimatch` override range was narrowed to `>=9.0.0`; majors 5–8 export a callable function and
297
+ would break under a forced lift to 10.
298
+
299
+ ---
300
+
301
+ ## Troubleshooting
302
+
303
+ | Symptom | Cause | Fix |
304
+ |---------|-------|-----|
305
+ | Migration now fails with `is incomplete: N of M chunks stored` | The asset was already broken in the database; the old helper never checked | See §1 — verify the source asset, remove the orphan, re-run |
306
+ | Migration now fails with `ENOENT` | The source path was always wrong; it used to hang instead of failing | Fix the path. It is resolved against the **helper module's** directory |
307
+ | A test asserting `'File not found'` fails | The message now carries its ErrorCode prefix | Match on a substring, or on `ErrorCode.FILE_NOT_FOUND` |
308
+ | A download that returned 500 now returns 404 | `checkRights()` refusal is handled properly instead of crashing | Intended — see §4 |
309
+ | An upload fails with `may execute as script` | The type is markup or script and is now rejected regardless of the filter | Intended — see §6, and opt out only under the conditions named there |
310
+ | An upload of a document type that used to pass now fails | The filter matched it as a substring before | Move to `allowList` naming the type exactly — see §5 |
311
+ | Uploads that accepted every type now accept JPEG/PNG only | `fileTypeRegex: undefined` no longer disables the filter | Pass an explicit `allowList` — see §7 |
312
+ | A test asserting on the filter's rejection string fails | Rejections are an `Error` now, not a string | Assert on `error.message` — see §8 |
313
+
314
+ ---
315
+
316
+ ## Verification
317
+
318
+ ```bash
319
+ pnpm test
320
+ ```
321
+
322
+ `src/core/common/helpers/file.helper.spec.ts` covers the substring class (values that merely CONTAIN
323
+ an allowed token), the scriptable-type rejection on both forms, the opt-out, and that the legacy
324
+ `RegExp` form still accepts what it accepted before.
325
+ `src/core/modules/migrate/helpers/migration.helper.spec.ts` and
326
+ `tests/migrate/upload-file-to-gridfs.e2e-spec.ts` cover the completeness check against a real GridFS
327
+ bucket; `src/core/modules/file/core-file.controller.spec.ts` covers the download error paths.
328
+
329
+ ---
330
+
331
+ ## Related Documentation
332
+
333
+ - [`src/core/modules/migrate/README.md`](../src/core/modules/migrate/README.md) — `uploadFileToGridFS()`, `assertGridFsFileComplete()`
334
+ - [`src/core/modules/file/README.md`](../src/core/modules/file/README.md) — download endpoints, error responses
335
+ - [`docs/security-overrides.md`](../docs/security-overrides.md) — overrides consumers must replicate