@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
@@ -4,14 +4,260 @@ File upload and download functionality with MongoDB GridFS storage.
4
4
 
5
5
  ## Endpoints
6
6
 
7
- ### Public Endpoints (via CoreFileController)
7
+ ### Download Endpoints (via CoreFileController)
8
8
 
9
- | Method | Endpoint | Description |
10
- | ------ | ------------------ | ------------------------- |
11
- | GET | `/files/id/:id` | Download file by ID |
12
- | GET | `/files/:filename` | Download file by filename |
9
+ | Method | Endpoint | Description | Gated by |
10
+ | ------ | ------------------ | ------------------------- | --------------- |
11
+ | GET | `/files/id/:id` | Download file by ID | `downloadRoles` |
12
+ | GET | `/files/:filename` | Download file by filename | `downloadRoles` |
13
13
 
14
- **Note:** These endpoints are public (`S_EVERYONE`) by default. Projects can restrict access by extending `CoreFileController`.
14
+ ### GraphQL members (via CoreFileResolver)
15
+
16
+ | Member | Kind | Gated by |
17
+ | ------------- | -------- | --------------- |
18
+ | `getFileInfo` | Query | `downloadRoles` |
19
+ | `uploadFile` | Mutation | `uploadRoles` |
20
+ | `uploadFiles` | Mutation | `uploadRoles` |
21
+ | `deleteFile` | Mutation | `deleteRoles` |
22
+
23
+ > `CoreFileResolver` is registered by no core module. It only takes effect in a project that
24
+ > registers it (or a subclass) itself — enabling GraphQL alone does not.
25
+
26
+ ---
27
+
28
+ ## Configuration
29
+
30
+ One `file` object configures the module — where the bytes live and who may reach them:
31
+
32
+ ```typescript
33
+ file: {
34
+ storage: 'gridfs', // 'filesystem' | 'gridfs' | 's3' — omit to derive it
35
+ storageDir: 'uploads/files', // only for 'filesystem'
36
+ downloadRoles: [RoleEnum.ADMIN], // GET /files/id/:id, GET /files/:filename, getFileInfo
37
+ uploadRoles: [RoleEnum.ADMIN], // uploadFile, uploadFiles
38
+ deleteRoles: [RoleEnum.ADMIN], // deleteFile
39
+ },
40
+ ```
41
+
42
+ `storage` and the role knobs are orthogonal: one picks the driver, the others the access.
43
+
44
+ ## Storage drivers
45
+
46
+ Three equivalent options. They differ only in where the bytes end up:
47
+
48
+ | Driver | Bytes | Survives a restart | Shared between replicas | Needs |
49
+ | -------------- | ------------------------- | ------------------------ | ----------------------- | ---------------------------------- |
50
+ | `'s3'` | S3-compatible bucket | yes | **yes** | `s3` config + `@aws-sdk/client-s3` |
51
+ | `'gridfs'` | MongoDB GridFS | yes | yes | nothing beyond the database |
52
+ | `'filesystem'` | local disk (`storageDir`) | only on a mounted volume | **no** | nothing |
53
+
54
+ **Metadata always lives in the database**, whichever driver holds the bytes. Filename, content type,
55
+ length and the custom `metadata` a per-file rule reads have to be queryable — `findFileInfo()` filters
56
+ and pages over them, `checkRights()` reads them per request. A directory listing answers none of
57
+ that, and sidecar files would reinvent an index the database already is. So `'filesystem'` moves the
58
+ bytes off the database, not the bookkeeping.
59
+
60
+ ### Choosing the driver
61
+
62
+ **Set it explicitly and it is enforced.** If the chosen store is not available, the boot **fails**:
63
+
64
+ ```
65
+ file.storage is set to 's3', but that storage is not available.
66
+ Configure `s3` (bucket, credentials/endpoint) and install `@aws-sdk/client-s3`, …
67
+ ```
68
+
69
+ That is deliberate. The previous behaviour fell back to GridFS whenever S3 was selected but
70
+ unusable — the application kept working, so nothing looked broken, while files landed in a store the
71
+ operator did not believe they were in. Afterwards nobody can tell which file went where.
72
+
73
+ **Leave it unset and it is derived**, most capable first:
74
+
75
+ 1. `'s3'` — when `s3.bucket` is configured
76
+ 2. `'gridfs'` — when a database is configured
77
+ 3. `'filesystem'` — when neither is
78
+
79
+ A configured-but-**unreachable** database is an error in its own right (Mongoose fails the boot), never
80
+ a reason to fall through to the disk. Only a database that is not configured **at all** reaches step 3 —
81
+ which today is theoretical, since `CoreModule` always registers Mongoose.
82
+
83
+ A derived driver is enforced too: `s3.bucket` in the config makes S3 the default, but the bytes still
84
+ go nowhere unless your `FileService` forwards the services to `super()`:
85
+
86
+ ```typescript
87
+ super(connection, 'fs', { configService, s3Service });
88
+ ```
89
+
90
+ ### Switching drivers is forward-only
91
+
92
+ Reads consult **every** store, so files written under a previous driver stay readable and there is no
93
+ migration step and no cut-over moment. New files go to the active driver; `findFileInfo()` returns
94
+ the union, paged once over the merged result.
95
+
96
+ The boot log names the driver in use, so it never has to be inferred from where files stopped
97
+ appearing:
98
+
99
+ ```
100
+ [CoreFileStorage] File storage: s3 (defaulted — s3.bucket is configured)
101
+ ```
102
+
103
+ ---
104
+
105
+ ## Access control
106
+
107
+ Two layers, and they answer different questions.
108
+
109
+ ### 1. Roles — _may this caller reach the endpoint at all?_
110
+
111
+ All three role knobs take **plain role strings**, so your own project roles work exactly as they
112
+ would in a hand-written `@Roles()`.
113
+
114
+ All three default to `[ADMIN]`. That default is restrictive on purpose: the file store is a **single
115
+ bucket shared by every feature** of the project — GridFS or S3, whichever `file.storage` selects —
116
+ and the ids naming its blobs are not secrets. An id is an ObjectId (4-byte timestamp, 5-byte
117
+ per-process random, 3-byte counter), so one upload of your own discloses the per-process value in
118
+ full and bounds the counter, collapsing the id space to a brute-forceable range. `/files/:filename`
119
+ is weaker still: it resolves the **first** match for a name a caller may be able to guess.
120
+
121
+ Three properties worth knowing:
122
+
123
+ - **ADMIN always keeps access.** Handler and class roles are UNIONed (`mergeRolesMetadata`), and both
124
+ classes carry a class-level `@Roles(ADMIN)`. So `downloadRoles: ['editor']` grants editors _in
125
+ addition to_ admins. These knobs cannot exclude admins.
126
+ - **`[]` is rejected, not honoured.** An all-empty role set reads to the guards as "no roles
127
+ required" and would OPEN the route. An empty array logs a warning and falls back to the default.
128
+ - **Roles are checked against `user.roles`, never `membership.role`.** Both classes carry
129
+ `@SkipTenantCheck()`. No store is reached through Mongoose — GridFS goes through the native driver,
130
+ the S3 and filesystem metadata live in their own `s3-files` / `filesystem-files` collections — so
131
+ `mongooseTenantPlugin` never scopes them. None is tenant-scoped, and a role name alone therefore
132
+ cannot express a per-tenant rule. Use layer 2 for that.
133
+
134
+ ### 2. `checkRights()` — _may this caller have THIS file?_
135
+
136
+ Roles cannot express "…but only their own". That is what the service hook is for:
137
+
138
+ ```typescript
139
+ export class FileService extends CoreFileService {
140
+ protected override async checkRights(
141
+ input: any,
142
+ options?: FileServiceOptions & { checkInputType: FileInputCheckType },
143
+ ): Promise<boolean> {
144
+ if (options?.checkInputType !== 'id' || options.force) {
145
+ return true;
146
+ }
147
+ if (options.currentUser?.hasRole([RoleEnum.ADMIN])) {
148
+ return true;
149
+ }
150
+ const raw = await this.getRawFileInfo(input);
151
+ return !!raw && String(raw.metadata?.ownerId) === String(options.currentUser?.id);
152
+ }
153
+ }
154
+ ```
155
+
156
+ Three pieces make this work, and all three are needed:
157
+
158
+ 1. **Write the metadata at upload time** — `createFile(file, { metadata: { ownerId: user.id } })`.
159
+ `CoreFileService.createFile()` passes `serviceOptions.metadata` straight to the storage driver,
160
+ so it works for both GridFS and S3.
161
+ 2. **Read it back with `getRawFileInfo()`** (or `getRawFileInfoByName()`), not `getFileInfo()`. The
162
+ public getter runs `prepareOutput` → `check()`, which strips fields the caller may not see —
163
+ including the very field the decision rests on. `getRawFileInfo()` consults every metadata store
164
+ in the same order `getFileInfo()` uses, so a rule sees the file the download would actually
165
+ serve — under any driver.
166
+ 3. **`options.currentUser` is supplied by the core controller and resolver** for every member.
167
+
168
+ When `checkRights()` refuses, `getFileStream()` returns `null` and the controller answers **404** —
169
+ deliberately the same answer as an unknown id, so the endpoint cannot be used to probe which files
170
+ exist. Do not turn that into a 403 in an override without accepting that trade-off.
171
+
172
+ ### Serving files to a browser `<img>` / `<a download>`
173
+
174
+ A markup-driven request cannot send an `Authorization` header, and a CORS preflight sends no
175
+ credentials at all. So anything stricter than `S_EVERYONE` only works from markup when the session
176
+ travels as a **cookie** on a same-site request.
177
+
178
+ If some files are genuinely public and others are not, the answer is not a role list — it is a
179
+ per-file rule. Record a visibility flag in the metadata at upload time and branch on it in
180
+ `checkRights()`, or expose a separate public route for exactly the files that are meant to be
181
+ public and leave these two gated.
182
+
183
+ **Presigned S3 downloads** (`file.storage: 's3'` with presigned downloads enabled) sidestep the
184
+ `<img>` problem, because the browser fetches the object directly from S3. Understand what you are
185
+ issuing, though: the URL is a **bearer capability** — anyone holding it can fetch the object until
186
+ it expires, with no session. Authorization happens once, when the URL is issued (through the
187
+ `resolveFile()` call that precedes the redirect on `GET /files/id/:id`, and the
188
+ `getFileInfoByName()` call on `GET /files/:filename`; both run the same `checkRights()`). Keep the
189
+ expiry short, and do not enable it for files whose audience is narrower than "anyone who was ever
190
+ given the link".
191
+
192
+ ### Overriding: read this before you re-declare a member
193
+
194
+ `CoreFileController` and `CoreFileResolver` are meant to be extended — but role metadata lives on
195
+ the **function object**, so an override carries its own and thereby **opts out of `file.*Roles`
196
+ entirely**. Two consequences that bite in practice:
197
+
198
+ ```typescript
199
+ // WRONG — the route DISAPPEARS. Nest reads PATH_METADATA off the subclass
200
+ // function, which has none, so it is never registered: 404 for everyone,
201
+ // no error, no warning.
202
+ override async getFileById(@Param('id') id: string, @Res() res: Response) {
203
+ return super.getFileById(id, res);
204
+ }
205
+
206
+ // WRONG — pins the policy and silently ignores `file.downloadRoles` forever.
207
+ // This exact shape kept generated projects public after the framework default
208
+ // had already been closed.
209
+ @Get('id/:id')
210
+ @Roles(RoleEnum.S_EVERYONE)
211
+ override async getFileById(@Param('id') id: string, @Res() res: Response) {
212
+ return super.getFileById(id, res);
213
+ }
214
+
215
+ // RIGHT — don't override at all. Configure it:
216
+ // file: { downloadRoles: [RoleEnum.S_USER] }
217
+ // and put per-file rules in checkRights().
218
+ ```
219
+
220
+ A class-level `@Roles()` on your subclass cannot relax an inherited member either: the inherited
221
+ function carries its own handler-level roles, and the two are unioned rather than overridden.
222
+
223
+ ### If you override `getFileInfo()`: `GET /files/id/:id` no longer calls it (11.33.0)
224
+
225
+ Up to 11.32.x, `GET /files/id/:id` called the public `CoreFileService.getFileInfo()` and then let
226
+ `getFileStream()` work out on its own which store held the bytes. With three stores to consult
227
+ (§ Storage drivers) that resolves the same id up to three times per download, so since 11.33.0 the
228
+ route calls **`resolveFile()`** instead, which answers the metadata and the store in one pass.
229
+
230
+ An override of `getFileInfo()` is therefore **no longer on that route's path**. It is still honoured
231
+ by every other caller it ever had — `deleteFile()`, `duplicateById()` and any project code that
232
+ calls it, such as the `GET /files/info/:id` endpoint projects usually add — which is exactly what
233
+ makes the gap easy to miss: the behaviour disappears on one route while everything else keeps it.
234
+ (The GraphQL `getFileInfo(filename:)` member resolves by NAME and has always gone through
235
+ `getFileInfoByName()`, so it is unaffected either way.)
236
+
237
+ This does **not** weaken authorization. `resolveFile()` runs the same `checkRights()` with the same
238
+ `serviceOptions`, and answers `null` on refusal, which the controller turns into the same 404. Only
239
+ work you added **on top** of the base `getFileInfo()` — a decorated field, a counter, a log line —
240
+ stops happening on the id route.
241
+
242
+ The remedy is to override `resolveFile()` as well, keeping the two consistent:
243
+
244
+ ```typescript
245
+ export class FileService extends CoreFileService {
246
+ override async getFileInfo(id: string | Types.ObjectId, serviceOptions?: FileServiceOptions) {
247
+ return this.decorate(await super.getFileInfo(id, serviceOptions));
248
+ }
249
+
250
+ // Same treatment for the download route. `store` must be passed through
251
+ // untouched — the controller hands it to getFileStream() to pick the store.
252
+ override async resolveFile(id: string | Types.ObjectId, serviceOptions?: FileServiceOptions) {
253
+ const resolved = await super.resolveFile(id, serviceOptions);
254
+ return resolved && { ...resolved, info: this.decorate(resolved.info) };
255
+ }
256
+ }
257
+ ```
258
+
259
+ Per-file **authorization** needs none of this: it belongs in `checkRights()`, which both methods
260
+ call.
15
261
 
16
262
  ### Admin Endpoints (project-specific)
17
263
 
@@ -46,44 +292,71 @@ export class FileController extends CoreFileController {
46
292
  }
47
293
  ```
48
294
 
49
- ### Restrict Download Access
295
+ ### Error responses
296
+
297
+ | Situation | Status | Body |
298
+ | -------------------------------------------------------------------------------------- | ------ | ----------------------------------------------------------------------------------------------------- |
299
+ | Unknown id / filename, or `checkRights()` refused | `404` | `NotFoundException` with `ErrorCode.FILE_NOT_FOUND` |
300
+ | Missing id / filename in the route | `400` | `BadRequestException` with `ErrorCode.REQUIRED_FIELD_MISSING` |
301
+ | GridFS read fails **before** any byte was sent (file document exists, chunks are gone) | `404` | `{ "error": "Not Found", "message": "<FILE_NOT_FOUND>", "statusCode": 404 }` |
302
+ | GridFS read fails **after** streaming started | — | The connection is closed; a truncated transfer is the only signal left once the status is on the wire |
303
+
304
+ The mid-stream failure case is handled by `pipeFileToResponse()`. Without it the stream error would
305
+ go unhandled, Node would destroy the socket, and a reverse proxy would report **502 Bad Gateway** —
306
+ i.e. "the server is down", while every other route keeps answering. On the error path the headers
307
+ describing the file (`Content-Type`, `Content-Disposition`, `Cache-Control`, `ETag`) are removed, so
308
+ the JSON body is not labelled as the image it failed to deliver. The error itself is logged
309
+ server-side even though the client answer stays deliberately generic.
310
+
311
+ To change the status, the body or the logging, override the `protected pipeFileToResponse()` method
312
+ on the controller rather than the exported function of the same name.
50
313
 
51
- To require authentication for downloads, override the inherited methods:
314
+ ### Upload filtering
315
+
316
+ Upload endpoints are project-specific, but the filter they install comes from the framework
317
+ (`multerOptionsForImageUpload()` / `multerFileFilter()` in `common/helpers/file.helper.ts`). Name what
318
+ the endpoint accepts as an `UploadAllowList` — both the mimetype and the extension are compared as
319
+ WHOLE values:
52
320
 
53
321
  ```typescript
54
- @Controller('files')
55
- @Roles(RoleEnum.ADMIN)
56
- export class FileController extends CoreFileController {
57
- constructor(protected override readonly fileService: FileService) {
58
- super(fileService);
59
- }
322
+ @UseInterceptors(FileInterceptor('file', multerOptionsForImageUpload({
323
+ allowList: {
324
+ extensions: ['.jpeg', '.jpg', '.pdf', '.png'],
325
+ mimeTypes: ['application/pdf', 'image/jpeg', 'image/png'],
326
+ },
327
+ })))
328
+ ```
60
329
 
61
- // Override to require authentication for ID-based download
62
- @Get('id/:id')
63
- @Roles(RoleEnum.S_USER) // Require logged-in user
64
- override async getFileById(@Param('id') id: string, @Res() res: Response) {
65
- return super.getFileById(id, res);
66
- }
330
+ The two conditions are **independent**: either one alone rejects the file, while a pair that is odd
331
+ yet individually allowed (`report.txt` announced as `application/pdf`) passes. An extension→mimetype
332
+ MAPPING is deliberately not enforced, because user agents genuinely disagree about office and audio
333
+ types (macOS reports `.csv` as `text/plain`) and a mapping would reject legitimate uploads.
67
334
 
68
- // Override to require authentication for filename-based download
69
- @Get(':filename')
70
- @Roles(RoleEnum.S_USER)
71
- override async getFile(@Param('filename') filename: string, @Res() res: Response) {
72
- return super.getFile(filename, res);
73
- }
74
- }
335
+ The legacy `fileTypeRegex` option still works and keeps precedence, but is **deprecated**: one
336
+ expression is `.test()`ed against both the mimetype and the extension, so every alternative matches
337
+ as a SUBSTRING — an allow-list containing `te?xt` also accepts `text/html`.
338
+
339
+ Types a browser may execute as script (`text/html`, `image/svg+xml`, `application/xhtml+xml`, XML and
340
+ JavaScript types, plus the matching extensions) are rejected **before** the allow-list is consulted,
341
+ on both forms. A stored upload served back from the API origin with one of these content types runs
342
+ in that origin, with the victim's session. Opt out only when the file never reaches an origin that
343
+ carries a session:
344
+
345
+ ```typescript
346
+ multerFileFilter({ extensions: ['.svg'], mimeTypes: ['image/svg+xml'] }, { allowScriptableTypes: true });
75
347
  ```
76
348
 
77
349
  ---
78
350
 
79
351
  ## GraphQL Support
80
352
 
81
- File operations are also available via GraphQL through `CoreFileResolver`:
353
+ File operations are also available via GraphQL through `CoreFileResolver` — in a project that
354
+ registers it. See [Access control](#access-control) for which knob gates which member.
82
355
 
83
356
  ```graphql
84
- # Query file by ID
357
+ # Read file info by filename (downloadRoles)
85
358
  query {
86
- file(id: "...") {
359
+ getFileInfo(filename: "...") {
87
360
  id
88
361
  filename
89
362
  contentType
@@ -91,24 +364,23 @@ query {
91
364
  }
92
365
  }
93
366
 
94
- # Query file by filename
95
- query {
96
- fileByFilename(filename: "...") {
367
+ # Upload a file via the GraphQL Upload scalar (uploadRoles)
368
+ mutation {
369
+ uploadFile(file: Upload!) {
97
370
  id
98
371
  filename
99
- contentType
100
372
  }
101
373
  }
102
374
 
103
- # Upload file (via GraphQL Upload scalar)
375
+ # Upload several files (uploadRoles)
104
376
  mutation {
105
- uploadFile(file: Upload!) {
377
+ uploadFiles(files: [Upload!]!) {
106
378
  id
107
379
  filename
108
380
  }
109
381
  }
110
382
 
111
- # Delete file
383
+ # Delete by filename (deleteRoles)
112
384
  mutation {
113
385
  deleteFile(filename: "...") {
114
386
  id
@@ -116,21 +388,32 @@ mutation {
116
388
  }
117
389
  ```
118
390
 
391
+ > Earlier revisions of this file documented `file(id:)` and `fileByFilename(filename:)`. Neither has
392
+ > ever existed on `CoreFileResolver`; the query is `getFileInfo(filename:)`.
393
+
119
394
  ---
120
395
 
121
396
  ## Integration with TUS
122
397
 
123
- Files uploaded via TUS are automatically stored in GridFS and can be accessed through the same endpoints:
398
+ Files uploaded via TUS land in the same GridFS bucket and are read back through the same endpoints
399
+ which means the same `downloadRoles` gate applies:
124
400
 
125
401
  ```bash
126
- # After TUS upload completes, download by ID
402
+ # After the TUS upload completes, download by ID (requires downloadRoles)
127
403
  GET /files/id/<gridfs-file-id>
128
404
 
129
- # Or by filename (if unique)
405
+ # Or by filename, if unique (requires downloadRoles)
130
406
  GET /files/<original-filename>
131
407
  ```
132
408
 
133
- **Recommendation:** Use ID-based downloads for TUS uploads as filenames may not be unique.
409
+ **Recommendation:** use ID-based downloads for TUS uploads, since filenames may not be unique.
410
+
411
+ **Watch the pairing.** TUS uploads are gated separately by `tus.roles` (default `S_USER`). The
412
+ common "user uploads their own file, then views it" flow therefore needs both sides to line up: a
413
+ signed-in user may upload, but with the default `downloadRoles: [ADMIN]` they cannot read the result
414
+ back. Either widen `downloadRoles`, or — better — write an owner into the metadata at upload time
415
+ and authorize per file in `checkRights()`. TUS uploads already carry `metadata.tusUploadId` and the
416
+ original TUS metadata, so there is a natural place to add one.
134
417
 
135
418
  ---
136
419