@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lenne.tech/nest-server",
3
- "version": "11.32.3",
3
+ "version": "11.33.0",
4
4
  "description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
5
5
  "keywords": [
6
6
  "node",
@@ -15,21 +15,20 @@
15
15
  "license": "MIT",
16
16
  "scripts": {
17
17
  "build": "rimraf dist && nest build && pnpm run build:copy-types && pnpm run build:copy-templates && pnpm run build:add-type-references && pnpm run build:framework-api",
18
- "build:framework-api": "tsx scripts/generate-framework-api.ts",
19
- "build:copy-types": "mkdir -p dist/types && cp src/types/*.d.ts dist/types/",
20
- "build:copy-templates": "mkdir -p dist/core/modules/migrate/templates && cp src/core/modules/migrate/templates/migration-project.template.ts dist/core/modules/migrate/templates/",
21
18
  "build:add-type-references": "node scripts/add-type-references.js",
22
- "build:pack": "pnpm pack && echo 'use file:/ROOT_PATH_TO_TGZ_FILE to integrate the package'",
19
+ "build:copy-templates": "mkdir -p dist/core/modules/migrate/templates && cp src/core/modules/migrate/templates/migration-project.template.ts dist/core/modules/migrate/templates/",
20
+ "build:copy-types": "mkdir -p dist/types && cp src/types/*.d.ts dist/types/",
23
21
  "build:dev": "pnpm run build",
22
+ "build:framework-api": "tsx scripts/generate-framework-api.ts",
23
+ "build:pack": "pnpm pack && echo 'use file:/ROOT_PATH_TO_TGZ_FILE to integrate the package'",
24
24
  "c": "pnpm run check",
25
+ "cf": "pnpm run check:fix",
25
26
  "check": "node scripts/check.mjs",
26
- "check:raw": "pnpm install --frozen-lockfile && pnpm run spectaql:sync && pnpm audit && pnpm run format:check && pnpm run lint && pnpm run typecheck:tests && pnpm run check:swc-tdz && pnpm test && pnpm run build && pnpm run check:manifest && bash scripts/check-server-start.sh",
27
27
  "check:fix": "pnpm install && pnpm run spectaql:sync && pnpm audit --fix && pnpm run format && pnpm run lint:fix && pnpm run typecheck:tests && pnpm run check:swc-tdz && pnpm test && pnpm run build && pnpm run check:manifest && bash scripts/check-server-start.sh",
28
- "check:naf": "pnpm install && pnpm run spectaql:sync && pnpm run format && pnpm run lint:fix && pnpm run typecheck:tests && pnpm run check:swc-tdz && pnpm test && pnpm run build && pnpm run check:manifest && bash scripts/check-server-start.sh",
29
28
  "check:manifest": "node scripts/check-package-manifest.mjs",
29
+ "check:naf": "pnpm install && pnpm run spectaql:sync && pnpm run format && pnpm run lint:fix && pnpm run typecheck:tests && pnpm run check:swc-tdz && pnpm test && pnpm run build && pnpm run check:manifest && bash scripts/check-server-start.sh",
30
+ "check:raw": "pnpm install --frozen-lockfile && pnpm run spectaql:sync && pnpm audit && pnpm run format:check && pnpm run lint && pnpm run typecheck:tests && pnpm run check:swc-tdz && pnpm test && pnpm run build && pnpm run check:manifest && bash scripts/check-server-start.sh",
30
31
  "check:swc-tdz": "nest build -b swc -p tsconfig.swc-tdz.json && node scripts/check-swc-tdz.mjs",
31
- "spectaql:sync": "node extras/update-spectaql-version.mjs",
32
- "cf": "pnpm run check:fix",
33
32
  "cnaf": "pnpm run check:naf",
34
33
  "docs": "pnpm run docs:ci && open http://127.0.0.1:8080/ && open ./public/index.html && compodoc -p tsconfig.json -s ",
35
34
  "docs:bootstrap": "node extras/update-spectaql-version.mjs && node scripts/run-spectaql.mjs",
@@ -38,36 +37,40 @@
38
37
  "format:check": "oxfmt --check src/ scripts/",
39
38
  "lint": "oxlint --ignore-path .oxlintignore src/ tests/ scripts/",
40
39
  "lint:fix": "oxlint --fix --fix-suggestions --ignore-path .oxlintignore src/ tests/ scripts/",
40
+ "prepack": "pnpm run prestart:prod",
41
+ "prepublishOnly": "pnpm run lint && pnpm run test:ci",
41
42
  "prestart:prod": "pnpm run build",
43
+ "preversion": "pnpm run lint",
42
44
  "reinit": "rimraf pnpm-lock.yaml && rimraf node_modules && pnpm install && pnpm run lint && pnpm run test:e2e && pnpm run test:ci && pnpm run build",
43
45
  "reinit:clean": "rimraf pnpm-lock.yaml && rimraf node_modules && pnpm store prune && pnpm install && pnpm run test:e2e && pnpm run build",
44
46
  "reinit:force": "rimraf pnpm-lock.yaml && rimraf node_modules && pnpm store prune && pnpm install --force && pnpm run test:e2e",
45
47
  "reinit:legacy": "rimraf pnpm-lock.yaml && rimraf node_modules && pnpm store prune && pnpm install && pnpm run test:e2e",
48
+ "spectaql:sync": "node extras/update-spectaql-version.mjs",
46
49
  "start": "pnpm run start:local",
47
- "start:prod": "NODE_ENV=production node dist/main.js",
48
- "start:nodemon": "ts-node -r tsconfig-paths/register src/main.ts",
49
50
  "start:debug": "nodemon --config nodemon-debug.json",
50
51
  "start:dev": "nodemon",
51
52
  "start:dev:swc": "nest start -b swc -w --type-check",
52
53
  "start:local": "NODE_ENV=local nodemon",
53
54
  "start:local:swc": "NODE_ENV=local nest start -b swc -w --type-check",
55
+ "start:nodemon": "ts-node -r tsconfig-paths/register src/main.ts",
56
+ "start:prod": "NODE_ENV=production node dist/main.js",
54
57
  "test": "pnpm run vitest:unit && pnpm run vitest",
55
58
  "test:ci": "pnpm run vitest:unit && pnpm run vitest:ci",
59
+ "test:cleanup": "find tests -type f \\( -name '*.txt' -o -name '*.bin' -o -name '*.png' \\) -not -name '.gitkeep' -delete && echo 'Test artifacts cleaned up'",
56
60
  "test:cov": "pnpm run vitest:unit:cov && pnpm run vitest:cov",
57
61
  "test:e2e": "pnpm run vitest",
58
- "prepack": "pnpm run prestart:prod",
59
- "prepublishOnly": "pnpm run lint && pnpm run test:ci",
60
- "preversion": "pnpm run lint",
62
+ "test:infra": "node scripts/test-infra.mjs up",
63
+ "test:infra:down": "node scripts/test-infra.mjs down",
64
+ "test:infra:status": "node scripts/test-infra.mjs status",
65
+ "test:types": "tsc --noEmit --skipLibCheck -p tests/types/tsconfig.json",
66
+ "test:unit:watch": "vitest --config vitest.config.ts",
67
+ "typecheck:tests": "tsc --noEmit -p tsconfig.tests.json",
61
68
  "vitest": "NODE_ENV=e2e vitest run --config vitest-e2e.config.ts",
62
69
  "vitest:ci": "NODE_ENV=ci vitest run --config vitest-e2e.config.ts",
63
70
  "vitest:cov": "NODE_ENV=e2e vitest run --coverage --config vitest-e2e.config.ts",
64
- "vitest:watch": "NODE_ENV=e2e vitest --config vitest-e2e.config.ts",
65
71
  "vitest:unit": "vitest run --config vitest.config.ts",
66
72
  "vitest:unit:cov": "vitest run --coverage --config vitest.config.ts",
67
- "test:unit:watch": "vitest --config vitest.config.ts",
68
- "test:types": "tsc --noEmit --skipLibCheck -p tests/types/tsconfig.json",
69
- "typecheck:tests": "tsc --noEmit -p tsconfig.tests.json",
70
- "test:cleanup": "find tests -type f \\( -name '*.txt' -o -name '*.bin' \\) -not -name '.gitkeep' -delete && echo 'Test artifacts cleaned up'",
73
+ "vitest:watch": "NODE_ENV=e2e vitest --config vitest-e2e.config.ts",
71
74
  "watch": "npm-watch"
72
75
  },
73
76
  "repository": {
@@ -85,25 +88,25 @@
85
88
  "dependencies": {
86
89
  "@apollo/server": "5.5.1",
87
90
  "@as-integrations/express5": "1.1.2",
88
- "@better-auth/passkey": "1.6.23",
91
+ "@better-auth/passkey": "1.6.26",
89
92
  "@getbrevo/brevo": "6.0.2",
90
- "@modelcontextprotocol/sdk": "1.29.0",
93
+ "@modelcontextprotocol/sdk": "1.30.0",
91
94
  "@nestjs/apollo": "13.4.2",
92
- "@nestjs/common": "11.1.28",
93
- "@nestjs/core": "11.1.28",
95
+ "@nestjs/common": "11.1.29",
96
+ "@nestjs/core": "11.1.29",
94
97
  "@nestjs/graphql": "13.4.2",
95
98
  "@nestjs/jwt": "11.0.2",
96
99
  "@nestjs/mongoose": "11.0.4",
97
100
  "@nestjs/passport": "11.0.5",
98
- "@nestjs/platform-express": "11.1.28",
101
+ "@nestjs/platform-express": "11.1.29",
99
102
  "@nestjs/schedule": "6.1.3",
100
103
  "@nestjs/swagger": "11.4.6",
101
104
  "@nestjs/terminus": "11.1.1",
102
- "@tus/file-store": "2.1.0",
103
- "@tus/server": "2.4.2",
105
+ "@tus/file-store": "2.1.1",
106
+ "@tus/server": "2.4.3",
104
107
  "@types/supertest": "7.2.1",
105
108
  "bcrypt": "6.0.0",
106
- "better-auth": "1.6.23",
109
+ "better-auth": "1.6.26",
107
110
  "class-transformer": "0.5.1",
108
111
  "class-validator": "0.15.1",
109
112
  "compression": "1.8.1",
@@ -113,19 +116,19 @@
113
116
  "ejs": "6.0.1",
114
117
  "express": "5.2.1",
115
118
  "graphql": "16.14.0",
116
- "graphql-query-complexity": "1.1.1",
119
+ "graphql-query-complexity": "2.0.0",
117
120
  "graphql-subscriptions": "3.0.0",
118
121
  "graphql-upload": "15.0.2",
119
- "graphql-ws": "6.1.0",
120
- "jose": "6.2.3",
121
- "js-sha256": "0.12.0",
122
+ "graphql-ws": "6.2.1",
123
+ "jose": "6.2.8",
124
+ "js-sha256": "1.0.0",
122
125
  "json-to-graphql-query": "2.3.0",
123
126
  "lodash": "4.18.1",
124
127
  "mongodb": "7.5.0",
125
- "mongoose": "9.8.0",
128
+ "mongoose": "9.9.1",
126
129
  "multer": "2.2.0",
127
130
  "node-mailjet": "6.0.11",
128
- "nodemailer": "9.0.3",
131
+ "nodemailer": "9.0.5",
129
132
  "passport": "0.7.0",
130
133
  "passport-jwt": "4.0.1",
131
134
  "reflect-metadata": "0.2.2",
@@ -133,43 +136,72 @@
133
136
  "rxjs": "7.8.2",
134
137
  "supertest": "7.2.2",
135
138
  "ts-morph": "28.0.0",
136
- "ws": "8.21.1",
139
+ "ws": "8.21.3",
137
140
  "yuml-diagram": "1.2.0"
138
141
  },
142
+ "peerDependencies": {
143
+ "@aws-sdk/client-s3": ">=3.1045.0 <4",
144
+ "@aws-sdk/s3-request-presigner": ">=3.1045.0 <4",
145
+ "@tus/s3-store": ">=2.0.5 <3",
146
+ "bullmq": ">=5.16.0 <7",
147
+ "ioredis": ">=5.0.0 <7"
148
+ },
149
+ "peerDependenciesMeta": {
150
+ "@aws-sdk/client-s3": {
151
+ "optional": true
152
+ },
153
+ "@aws-sdk/s3-request-presigner": {
154
+ "optional": true
155
+ },
156
+ "@tus/s3-store": {
157
+ "optional": true
158
+ },
159
+ "bullmq": {
160
+ "optional": true
161
+ },
162
+ "ioredis": {
163
+ "optional": true
164
+ }
165
+ },
139
166
  "devDependencies": {
167
+ "@aws-sdk/client-s3": "3.1106.0",
168
+ "@aws-sdk/s3-request-presigner": "3.1106.0",
140
169
  "@compodoc/compodoc": "2.0.0",
141
170
  "@nestjs/cli": "11.0.24",
142
171
  "@nestjs/schematics": "11.1.0",
143
172
  "@nestjs/testing": "11.1.28",
144
173
  "@swc/cli": "0.8.1",
145
- "@swc/core": "1.15.46",
174
+ "@swc/core": "1.15.47",
175
+ "@tus/s3-store": "2.0.5",
146
176
  "@types/compression": "1.8.1",
147
177
  "@types/cookie-parser": "1.4.10",
148
178
  "@types/ejs": "3.1.5",
149
179
  "@types/express": "5.0.6",
150
- "@types/lodash": "4.17.24",
180
+ "@types/lodash": "4.17.25",
151
181
  "@types/multer": "2.2.0",
152
- "@types/node": "26.1.1",
182
+ "@types/node": "26.2.0",
153
183
  "@types/nodemailer": "8.0.1",
154
184
  "@types/passport": "1.0.17",
155
185
  "@vitest/coverage-v8": "4.1.10",
156
186
  "@vitest/ui": "4.1.10",
157
187
  "ansi-colors": "4.1.3",
188
+ "bullmq": "6.0.9",
158
189
  "find-file-up": "2.0.1",
159
190
  "husky": "9.1.7",
191
+ "ioredis": "6.0.0",
160
192
  "nodemon": "3.1.14",
161
193
  "npm-watch": "0.13.0",
162
194
  "otpauth": "9.5.1",
163
- "oxfmt": "0.59.0",
164
- "oxlint": "1.74.0",
195
+ "oxfmt": "0.62.0",
196
+ "oxlint": "1.77.0",
165
197
  "rimraf": "6.1.3",
166
198
  "ts-node": "10.9.2",
167
199
  "tsconfig-paths": "4.2.0",
168
- "tsx": "4.23.1",
200
+ "tsx": "4.23.11",
169
201
  "tus-js-client": "4.3.1",
170
202
  "typescript": "5.9.3",
171
- "unplugin-swc": "1.5.9",
172
- "vite": "8.1.5",
203
+ "unplugin-swc": "1.5.10",
204
+ "vite": "8.2.1",
173
205
  "vite-plugin-node": "8.0.0",
174
206
  "vitest": "4.1.10"
175
207
  },
@@ -0,0 +1,90 @@
1
+ /**
2
+ * `Content-Disposition` rendering for a client-supplied filename.
3
+ *
4
+ * An import-free LEAF on purpose. Two call sites in different layers need the identical value —
5
+ * the file controller's streaming branch (`src/core/modules/file/`) and the S3 presigned-URL
6
+ * branch (`src/core/common/services/`) — and `src/core/common/**` must not depend on
7
+ * `src/core/modules/**`. A leaf serves both without an import edge in the wrong direction, and
8
+ * without a cycle to reason about (see `.claude/rules/architecture.md` → "DI Token Placement
9
+ * (SWC-Safe)").
10
+ *
11
+ * Sharing it is the point, not just tidiness: while the logic was duplicated, the SAME file
12
+ * downloaded under a DIFFERENT name depending on whether `s3.presignedDownloads` was enabled.
13
+ */
14
+
15
+ /**
16
+ * Characters an RFC 8187 ext-value may carry unescaped (`attr-char`).
17
+ *
18
+ * `encodeURIComponent` is close but not equal: it leaves `'`, `(`, `)` and `*`
19
+ * unescaped, and none of those is an `attr-char`. A filename like
20
+ * `O'Brien (final)*.pdf` therefore produced a MALFORMED `filename*` parameter,
21
+ * which a strict parser is entitled to drop — falling back to the ASCII
22
+ * `filename` and losing the very name the ext-value exists to preserve.
23
+ * Over-escaping is always allowed (`value-chars = *( pct-encoded / attr-char )`),
24
+ * so escaping these four extra is the whole fix.
25
+ */
26
+ const NON_ATTR_CHAR = /['()*]/g;
27
+
28
+ /**
29
+ * Everything that may NOT stand in the quoted `filename` fallback.
30
+ *
31
+ * The complement is printable US-ASCII (0x20–0x7E) minus `"` (0x22) and `\`
32
+ * (0x5C) — the two characters that would end the quoted-string early or start a
33
+ * quoted-pair. So the allowed ranges are 0x20–0x21, 0x23–0x5B and 0x5D–0x7E, and
34
+ * everything else (non-ASCII, CR/LF, other control characters, DEL) is replaced
35
+ * with `_`.
36
+ *
37
+ * Replacing rather than deleting is deliberate: it keeps the name's shape
38
+ * recognisable (`Übersicht.pdf` → `_bersicht.pdf`) instead of silently welding
39
+ * neighbouring words together, and it can never produce an empty parameter.
40
+ */
41
+ const NON_QUOTABLE_ASCII = /[^\x20-\x21\x23-\x5B\x5D-\x7E]/g;
42
+
43
+ /**
44
+ * Percent-encode a filename as an RFC 8187 ext-value (the `filename*` parameter).
45
+ */
46
+ function toExtValue(filename: string): string {
47
+ return encodeURIComponent(filename).replace(
48
+ NON_ATTR_CHAR,
49
+ (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`,
50
+ );
51
+ }
52
+
53
+ /**
54
+ * Build the `Content-Disposition` value for a client-supplied filename.
55
+ *
56
+ * Used for BOTH file delivery paths — the streamed response and the S3 presigned URL's
57
+ * `response-content-disposition` — so a file keeps its name whichever one serves it.
58
+ *
59
+ * Exported so the rendering can be unit-tested without a Nest context, and so a
60
+ * project overriding `CoreFileController.setFileHeaders` can reuse it.
61
+ *
62
+ * Filenames are client-supplied on both the multer path (`multerFileToUpload()`
63
+ * keeps `originalname`, where the old diskStorage path generated the name
64
+ * server-side) and the tus path (`Upload-Metadata`), so this value has to hold
65
+ * up against a hostile name as well as merely an awkward one.
66
+ *
67
+ * Two parameters, because they answer to two different specs and one client set
68
+ * each:
69
+ *
70
+ * - `filename` is an RFC 6266 quoted-string, and NOTHING percent-decodes it.
71
+ * Percent-encoding it protects nothing the quotes do not already handle — it
72
+ * only makes a client without `filename*` support save
73
+ * `Jahresbericht%202024.pdf`. So it is sanitised to quotable ASCII and left
74
+ * literal.
75
+ * - `filename*` is the RFC 8187 ext-value that carries the real, non-ASCII name
76
+ * for every client that understands it, and it wins over `filename` per RFC
77
+ * 6266 §4.3 when both are present.
78
+ *
79
+ * Injection is closed by the quoting plus the sanitiser, not by the encoding.
80
+ * Before 11.33.0 this header was emitted bare (`filename=${file.filename}`), so a
81
+ * `"` or a `; ` let the caller append or replace parameters outright — and an
82
+ * appended `filename*` WINS over the one meant here. A CR/LF is worse than that:
83
+ * it makes Node's `setHeader` throw `ERR_INVALID_CHAR`, turning that one file's
84
+ * download into a permanent 500.
85
+ */
86
+ export function buildContentDisposition(filename?: string): string {
87
+ const name = filename || 'download';
88
+ const quotable = name.replace(NON_QUOTABLE_ASCII, '_');
89
+ return `attachment; filename="${quotable}"; filename*=UTF-8''${toExtValue(name)}`;
90
+ }
@@ -1,6 +1,85 @@
1
1
  import { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface';
2
- import { diskStorage } from 'multer';
2
+ import { randomBytes } from 'crypto';
3
+ import { diskStorage, memoryStorage } from 'multer';
3
4
  import { extname } from 'path';
5
+ import { Readable } from 'stream';
6
+
7
+ import type { FileUploadSource } from '../../modules/file/interfaces/file-upload.interface';
8
+
9
+ /**
10
+ * What an upload endpoint accepts: exact mimetypes and exact file extensions.
11
+ *
12
+ * Prefer this over the legacy `RegExp` form. A single expression `.test()`ed
13
+ * against BOTH the mimetype and the extension searches for a SUBSTRING, so every
14
+ * alternative matches anywhere inside either value. An allow-list of `text` /
15
+ * `txt` therefore also accepts `text/html` and `text/xml`, and one containing
16
+ * `md` accepts every mimetype with "md" in it. Consumers hit this in practice:
17
+ * a filter documented as "no html" happily accepted a file named `x.txt` that
18
+ * was sent as `text/html`.
19
+ *
20
+ * Two separate sets also remove the reason the expression could not simply be
21
+ * anchored: it had to carry mimetype FRAGMENTS (`wordprocessingml`, `ms-excel`)
22
+ * next to bare extensions, and no anchoring satisfies both at once.
23
+ */
24
+ export interface UploadAllowList {
25
+ /** Allowed file extensions, lowercase and WITH the leading dot. */
26
+ extensions: readonly string[];
27
+ /** Allowed mimetypes, lowercase and without parameters. */
28
+ mimeTypes: readonly string[];
29
+ }
30
+
31
+ /**
32
+ * Default for image uploads — the exact-matching equivalent of the legacy
33
+ * `/jpeg|jpg|png/`.
34
+ */
35
+ export const IMAGE_UPLOAD_ALLOW_LIST: UploadAllowList = {
36
+ extensions: ['.jpeg', '.jpg', '.png'],
37
+ mimeTypes: ['image/jpeg', 'image/png'],
38
+ };
39
+
40
+ /**
41
+ * Types a browser may execute as script when it renders the stored file.
42
+ *
43
+ * These are rejected by {@link multerFileFilter} REGARDLESS of the allow-list,
44
+ * because the danger does not depend on what a given endpoint meant to accept:
45
+ * an upload served back from the API origin with one of these content types
46
+ * runs in that origin, with the victim's session. The check is what makes the
47
+ * legacy `RegExp` path safe for existing consumers without breaking their
48
+ * expressions — see `allowScriptableTypes` to opt out.
49
+ */
50
+ export const SCRIPTABLE_UPLOAD_MIME_TYPES: readonly string[] = [
51
+ 'application/javascript',
52
+ 'application/xhtml+xml',
53
+ 'application/xml',
54
+ 'image/svg+xml',
55
+ 'text/html',
56
+ 'text/javascript',
57
+ 'text/xml',
58
+ ];
59
+
60
+ /** Extensions matching {@link SCRIPTABLE_UPLOAD_MIME_TYPES}. */
61
+ export const SCRIPTABLE_UPLOAD_EXTENSIONS: readonly string[] = [
62
+ '.htm',
63
+ '.html',
64
+ '.js',
65
+ '.mjs',
66
+ '.svg',
67
+ '.xhtml',
68
+ '.xml',
69
+ ];
70
+
71
+ /** Options for {@link multerFileFilter}. */
72
+ export interface MulterFileFilterOptions {
73
+ /**
74
+ * Accept markup and script types too (`text/html`, `image/svg+xml`, …).
75
+ *
76
+ * Only set this when the stored file is never served from an origin that
77
+ * carries a session — e.g. a separate download host, or a route that always
78
+ * answers with `Content-Disposition: attachment` AND
79
+ * `X-Content-Type-Options: nosniff`.
80
+ */
81
+ allowScriptableTypes?: boolean;
82
+ }
4
83
 
5
84
  /**
6
85
  * Helper class for inputs
@@ -18,14 +97,18 @@ export default class FileHelper {
18
97
  /**
19
98
  * Get function to filter files for multer with a certain mimetype & extname
20
99
  */
21
- public static multerFileFilter(fileTypeRegex = /jpeg|jpg|png/) {
22
- return multerFileFilter(fileTypeRegex);
100
+ public static multerFileFilter(
101
+ accept: RegExp | UploadAllowList = IMAGE_UPLOAD_ALLOW_LIST,
102
+ options?: MulterFileFilterOptions,
103
+ ) {
104
+ return multerFileFilter(accept, options);
23
105
  }
24
106
 
25
107
  /**
26
108
  * Get multer options for image upload
27
109
  */
28
110
  public static multerOptionsForImageUpload(options: {
111
+ allowList?: UploadAllowList;
29
112
  destination?: string;
30
113
  fileSize?: number;
31
114
  fileTypeRegex?: RegExp;
@@ -35,38 +118,98 @@ export default class FileHelper {
35
118
  }
36
119
 
37
120
  /**
38
- * Get function to filter files for multer with a certain mimetype & extname
121
+ * Reduce a reported mimetype to the bare type: lowercase, trimmed, without the
122
+ * `; charset=…` parameters a user agent may append.
123
+ */
124
+ function normalizeMimeType(value: string): string {
125
+ return String(value || '')
126
+ .split(';')[0]
127
+ .trim()
128
+ .toLowerCase();
129
+ }
130
+
131
+ /**
132
+ * Get a multer `fileFilter` that accepts only the given mimetypes / extensions.
133
+ *
134
+ * Pass an {@link UploadAllowList} — both the mimetype and the extension must
135
+ * appear in it, each compared as a WHOLE value. The two conditions are
136
+ * independent: either one alone rejects the file, while a pair that is odd yet
137
+ * individually allowed (`report.txt` announced as `application/pdf`) passes.
138
+ * An extension→mimetype MAPPING is deliberately not enforced: user agents
139
+ * genuinely disagree about office and audio types (macOS reports `.csv` as
140
+ * `text/plain`), so a mapping rejects legitimate uploads.
141
+ *
142
+ * A `RegExp` is still accepted for backwards compatibility but is
143
+ * **deprecated**: it is `.test()`ed against both values and therefore matches
144
+ * SUBSTRINGS, which is how `te?xt` ends up accepting `text/html`. Whichever form
145
+ * is used, the types in {@link SCRIPTABLE_UPLOAD_MIME_TYPES} /
146
+ * {@link SCRIPTABLE_UPLOAD_EXTENSIONS} are rejected first unless
147
+ * `options.allowScriptableTypes` is set — that is what closes the hole for
148
+ * expressions that already exist in consumer projects.
149
+ *
150
+ * Rejections are reported as a real `Error`. Passing a bare string (as this
151
+ * helper did before) leaves multer with an "error" that has no `message`, which
152
+ * NestJS's `transformException` cannot map to a 4xx.
39
153
  */
40
- export function multerFileFilter(fileTypeRegex = /jpeg|jpg|png/) {
154
+ export function multerFileFilter(
155
+ accept: RegExp | UploadAllowList = IMAGE_UPLOAD_ALLOW_LIST,
156
+ options?: MulterFileFilterOptions,
157
+ ) {
41
158
  return (req, file, cb) => {
42
- const mimetype = fileTypeRegex.test(file.mimetype);
43
- const extName = fileTypeRegex.test(extname(file.originalname).toLowerCase());
159
+ const mimeType = normalizeMimeType(file?.mimetype);
160
+ const extension = extname(String(file?.originalname || '')).toLowerCase();
161
+
162
+ if (
163
+ !options?.allowScriptableTypes &&
164
+ (SCRIPTABLE_UPLOAD_MIME_TYPES.includes(mimeType) || SCRIPTABLE_UPLOAD_EXTENSIONS.includes(extension))
165
+ ) {
166
+ return cb(new Error(`File upload rejected: ${mimeType || 'unknown type'} may execute as script`));
167
+ }
44
168
 
45
- if (mimetype && extName) {
169
+ const accepted =
170
+ accept instanceof RegExp
171
+ ? accept.test(mimeType) && accept.test(extension)
172
+ : accept.mimeTypes.includes(mimeType) && accept.extensions.includes(extension);
173
+
174
+ if (accepted) {
46
175
  return cb(null, true);
47
176
  }
48
- cb(`Error: File upload only supports the following filetypes - ${fileTypeRegex}`);
177
+ cb(new Error(`File upload only supports the following filetypes - ${describeAccept(accept)}`));
49
178
  };
50
179
  }
51
180
 
181
+ /** Render the accepted types for the rejection message. */
182
+ function describeAccept(accept: RegExp | UploadAllowList): string {
183
+ return accept instanceof RegExp ? String(accept) : accept.extensions.join(', ');
184
+ }
185
+
52
186
  /**
53
187
  * Get multer options for image upload
188
+ *
189
+ * Pass `allowList` for exact matching; `fileTypeRegex` is deprecated (see
190
+ * {@link multerFileFilter}). When neither is set, {@link IMAGE_UPLOAD_ALLOW_LIST}
191
+ * applies.
54
192
  */
55
193
  export function multerOptionsForImageUpload(options: {
194
+ allowList?: UploadAllowList;
56
195
  destination?: string;
57
196
  fileSize?: number;
58
197
  fileTypeRegex?: RegExp;
198
+ memory?: boolean;
59
199
  }): MulterOptions {
60
200
  // Set config
61
201
  const config = {
62
202
  fileSize: 1024 * 1024, // 1MB
63
- fileTypeRegex: /jpeg|jpg|png/, // Images only
64
203
  ...options,
65
204
  };
66
205
 
206
+ // An explicit regex keeps precedence so existing callers behave as before
207
+ // (minus the scriptable types); otherwise the exact-matching default applies.
208
+ const accept: RegExp | UploadAllowList = config.fileTypeRegex ?? config.allowList ?? IMAGE_UPLOAD_ALLOW_LIST;
209
+
67
210
  return {
68
211
  // File filter
69
- fileFilter: config.fileTypeRegex ? multerFileFilter(config.fileTypeRegex) : undefined,
212
+ fileFilter: multerFileFilter(accept),
70
213
 
71
214
  // Limits
72
215
  limits: {
@@ -76,15 +219,53 @@ export function multerOptionsForImageUpload(options: {
76
219
 
77
220
  // Automatic storage handling
78
221
  // For configuration see https://github.com/expressjs/multer#storage
79
- storage: diskStorage({
80
- // Destination for uploaded file
81
- // If destination is not set file will be buffered and can be processed
82
- // in the method
83
- destination: config.destination ? config.destination : undefined,
84
-
85
- // Generated random file name
86
- filename: multerRandomFileName(),
87
- }),
222
+ //
223
+ // `memory: true` keeps the upload in RAM so the handler can hand it straight
224
+ // to a central store (GridFS/S3) via `multerFileToUpload()`. Disk storage
225
+ // writes to the POD's filesystem, which another replica cannot read and a
226
+ // restart discards see `memoryStorage()` below.
227
+ storage: config.memory
228
+ ? memoryStorage()
229
+ : diskStorage({
230
+ // Destination for uploaded file
231
+ // If destination is not set file will be buffered and can be processed
232
+ // in the method
233
+ destination: config.destination ? config.destination : undefined,
234
+
235
+ // Generated random file name
236
+ filename: multerRandomFileName(),
237
+ }),
238
+ };
239
+ }
240
+
241
+ /**
242
+ * Adapt a multer upload to the shape `CoreFileService` consumes.
243
+ *
244
+ * Lets a REST/multer endpoint write to the same central storage (GridFS or S3)
245
+ * as the GraphQL upload path, instead of the pod-local disk that multer's
246
+ * `diskStorage` produces. Requires `memory: true` on the multer options — a
247
+ * disk-stored file has no `buffer`, and this throws rather than silently
248
+ * storing an empty file.
249
+ *
250
+ * The returned `createReadStream` is callable more than once: it builds a fresh
251
+ * stream over the same buffer each time, matching graphql-upload's contract.
252
+ */
253
+ export function multerFileToUpload(file: {
254
+ buffer?: Buffer;
255
+ mimetype?: string;
256
+ originalname?: string;
257
+ }): FileUploadSource {
258
+ if (!file?.buffer) {
259
+ throw new Error(
260
+ 'multerFileToUpload() needs an in-memory upload: pass `memory: true` to the multer options ' +
261
+ '(a disk-stored file has no buffer).',
262
+ );
263
+ }
264
+ return {
265
+ createReadStream: () => Readable.from(file.buffer),
266
+ encoding: '7bit',
267
+ filename: file.originalname || randomBytes(16).toString('hex'),
268
+ mimetype: file.mimetype || 'application/octet-stream',
88
269
  };
89
270
  }
90
271