@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
@@ -1,6 +1,14 @@
1
- import { Injectable, Logger } from '@nestjs/common';
1
+ import { Injectable, Logger, Optional } from '@nestjs/common';
2
2
 
3
3
  import { IBetterAuthRateLimit } from '../../common/interfaces/server-options.interface';
4
+ import { CoreRedisService } from '../../common/services/core-redis.service';
5
+ import {
6
+ InMemoryRateLimitStore,
7
+ rateLimitKey,
8
+ rateLimitKeyPrefix,
9
+ RateLimitStore,
10
+ RedisRateLimitStore,
11
+ } from '../../common/services/rate-limit-store';
4
12
 
5
13
  /**
6
14
  * Result of a rate limit check
@@ -32,14 +40,6 @@ export interface RateLimitResult {
32
40
  resetIn: number;
33
41
  }
34
42
 
35
- /**
36
- * Rate limit entry for tracking requests
37
- */
38
- interface RateLimitEntry {
39
- count: number;
40
- resetTime: number;
41
- }
42
-
43
43
  /**
44
44
  * Default rate limiting configuration
45
45
  */
@@ -54,10 +54,11 @@ const DEFAULT_CONFIG: Required<IBetterAuthRateLimit> = {
54
54
  };
55
55
 
56
56
  /**
57
- * In-memory rate limiter for Better-Auth endpoints
57
+ * Rate limiter for Better-Auth endpoints
58
58
  *
59
59
  * This service provides rate limiting to protect against brute-force attacks
60
- * on authentication endpoints. It uses an in-memory store with automatic cleanup.
60
+ * on authentication endpoints. Counters live in a {@link RateLimitStore}: shared
61
+ * via Redis when `ServerOptions.redis` is configured, process-local otherwise.
61
62
  *
62
63
  * Features:
63
64
  * - Configurable request limits and time windows
@@ -68,7 +69,7 @@ const DEFAULT_CONFIG: Required<IBetterAuthRateLimit> = {
68
69
  *
69
70
  * @example
70
71
  * ```typescript
71
- * const result = rateLimiter.check('192.168.1.1', '/iam/sign-in');
72
+ * const result = await rateLimiter.check('192.168.1.1', '/iam/sign-in');
72
73
  * if (!result.allowed) {
73
74
  * throw new TooManyRequestsException(rateLimiter.getMessage());
74
75
  * }
@@ -76,15 +77,11 @@ const DEFAULT_CONFIG: Required<IBetterAuthRateLimit> = {
76
77
  */
77
78
  @Injectable()
78
79
  export class CoreBetterAuthRateLimiter {
79
- private readonly logger = new Logger(CoreBetterAuthRateLimiter.name);
80
- private readonly store = new Map<string, RateLimitEntry>();
81
- private config: Required<IBetterAuthRateLimit> = DEFAULT_CONFIG;
82
- private cleanupInterval: NodeJS.Timeout | null = null;
83
-
84
- constructor() {
85
- // Start cleanup interval (every 5 minutes)
86
- this.startCleanup();
87
- }
80
+ protected readonly logger = new Logger(CoreBetterAuthRateLimiter.name);
81
+ protected config: Required<IBetterAuthRateLimit> = DEFAULT_CONFIG;
82
+ protected store?: RateLimitStore;
83
+
84
+ constructor(@Optional() protected readonly coreRedisService?: CoreRedisService) {}
88
85
 
89
86
  /**
90
87
  * Configure the rate limiter
@@ -112,7 +109,7 @@ export class CoreBetterAuthRateLimiter {
112
109
  * @param path - Request path (relative to basePath)
113
110
  * @returns Rate limit check result
114
111
  */
115
- check(ip: string, path: string): RateLimitResult {
112
+ async check(ip: string, path: string): Promise<RateLimitResult> {
116
113
  // If rate limiting is disabled, always allow
117
114
  if (!this.config.enabled) {
118
115
  return {
@@ -137,48 +134,18 @@ export class CoreBetterAuthRateLimiter {
137
134
 
138
135
  // Determine the limit for this endpoint
139
136
  const limit = this.getLimit(path);
140
- const key = this.getKey(ip, path);
141
- const now = Date.now();
142
-
143
- // Get or create entry
144
- let entry = this.store.get(key);
145
-
146
- if (!entry || now >= entry.resetTime) {
147
- // Evict oldest entries if store exceeds maxEntries
148
- if (!entry && this.store.size >= this.config.maxEntries) {
149
- this.evictOldest();
150
- }
151
-
152
- // Create new entry or reset expired one
153
- entry = {
154
- count: 1,
155
- resetTime: now + this.config.windowSeconds * 1000,
156
- };
157
- this.store.set(key, entry);
137
+ const { count, resetIn } = await this.getStore().hit(this.getKey(ip, path), this.config.windowSeconds);
158
138
 
159
- return {
160
- allowed: true,
161
- current: 1,
162
- limit,
163
- remaining: limit - 1,
164
- resetIn: this.config.windowSeconds,
165
- };
166
- }
167
-
168
- // Increment count
169
- entry.count++;
170
-
171
- const resetIn = Math.ceil((entry.resetTime - now) / 1000);
172
- const allowed = entry.count <= limit;
173
- const remaining = Math.max(0, limit - entry.count);
139
+ const allowed = count <= limit;
140
+ const remaining = Math.max(0, limit - count);
174
141
 
175
142
  if (!allowed) {
176
- this.logger.warn(`Rate limit exceeded for IP ${this.maskIp(ip)} on ${path}: ${entry.count}/${limit}`);
143
+ this.logger.warn(`Rate limit exceeded for IP ${this.maskIp(ip)} on ${path}: ${count}/${limit}`);
177
144
  }
178
145
 
179
146
  return {
180
147
  allowed,
181
- current: entry.count,
148
+ current: count,
182
149
  limit,
183
150
  remaining,
184
151
  resetIn,
@@ -204,74 +171,53 @@ export class CoreBetterAuthRateLimiter {
204
171
  *
205
172
  * @param ip - Client IP address
206
173
  */
207
- reset(ip: string): void {
208
- // Remove all entries for this IP
209
- for (const key of this.store.keys()) {
210
- if (key.startsWith(`${ip}:`)) {
211
- this.store.delete(key);
212
- }
213
- }
174
+ async reset(ip: string): Promise<void> {
175
+ await this.getStore().resetByPrefix(rateLimitKeyPrefix(ip));
214
176
  }
215
177
 
216
178
  /**
217
179
  * Clear all rate limit entries (useful for testing)
218
180
  */
219
- clear(): void {
220
- this.store.clear();
181
+ async clear(): Promise<void> {
182
+ await this.getStore().clear();
221
183
  }
222
184
 
223
185
  /**
224
186
  * Get statistics about the rate limiter
187
+ *
188
+ * `activeEntries` is `-1` when a Redis store is in use — the entry count is
189
+ * not cheaply known there.
225
190
  */
226
191
  getStats(): { activeEntries: number; enabled: boolean } {
227
192
  return {
228
- activeEntries: this.store.size,
193
+ activeEntries: this.store?.size() ?? 0,
229
194
  enabled: this.config.enabled,
230
195
  };
231
196
  }
232
197
 
233
198
  /**
234
- * Stop the cleanup interval (for graceful shutdown)
199
+ * Stop the in-memory cleanup interval (for graceful shutdown)
235
200
  */
236
201
  onModuleDestroy(): void {
237
- if (this.cleanupInterval) {
238
- clearInterval(this.cleanupInterval);
239
- this.cleanupInterval = null;
240
- }
202
+ // Not `instanceof InMemoryRateLimitStore`: a RedisRateLimitStore owns an in-memory fallback
203
+ // whose cleanup interval would otherwise outlive the app.
204
+ this.store?.destroy?.();
241
205
  }
242
206
 
243
207
  /**
244
- * Evict the oldest entries when the store exceeds maxEntries.
245
- * First removes all expired entries, then removes entries closest to expiry
246
- * until the store is at 90% capacity.
208
+ * Lazily select the counter store: Redis when configured and enabled,
209
+ * process-local otherwise.
247
210
  */
248
- private evictOldest(): void {
249
- const now = Date.now();
250
- let evicted = 0;
251
-
252
- // First pass: remove all expired entries
253
- for (const [key, entry] of this.store.entries()) {
254
- if (now >= entry.resetTime) {
255
- this.store.delete(key);
256
- evicted++;
257
- }
258
- }
259
-
260
- // If still over limit, remove entries with earliest resetTime (oldest)
261
- if (this.store.size >= this.config.maxEntries) {
262
- const targetSize = Math.floor(this.config.maxEntries * 0.9);
263
- const entries = [...this.store.entries()].sort((a, b) => a[1].resetTime - b[1].resetTime);
264
-
265
- for (const [key] of entries) {
266
- if (this.store.size <= targetSize) break;
267
- this.store.delete(key);
268
- evicted++;
269
- }
270
- }
271
-
272
- if (evicted > 0) {
273
- this.logger.warn(`Evicted ${evicted} rate limit entries (store was at capacity: ${this.config.maxEntries})`);
211
+ protected getStore(): RateLimitStore {
212
+ if (!this.store) {
213
+ // `maxEntries` bounds BOTH stores: process-local entries here, distinct Redis counter keys
214
+ // per window there. A project that tightened the cap must not have it silently ignored the
215
+ // moment `redis` is configured.
216
+ this.store = this.coreRedisService?.enabled
217
+ ? new RedisRateLimitStore(this.coreRedisService, 'better-auth', this.config.maxEntries)
218
+ : new InMemoryRateLimitStore(this.config.maxEntries);
274
219
  }
220
+ return this.store;
275
221
  }
276
222
 
277
223
  /**
@@ -300,7 +246,10 @@ export class CoreBetterAuthRateLimiter {
300
246
  private getKey(ip: string, path: string): string {
301
247
  // Group similar endpoints together
302
248
  const endpoint = this.normalizeEndpoint(path);
303
- return `${ip}:${endpoint}`;
249
+ // rateLimitKey(), not string concatenation: an IP that itself contains the separator (every
250
+ // IPv6 address does) would otherwise straddle the ip/endpoint boundary and share a counter
251
+ // with a different caller.
252
+ return rateLimitKey(ip, endpoint);
304
253
  }
305
254
 
306
255
  /**
@@ -333,34 +282,4 @@ export class CoreBetterAuthRateLimiter {
333
282
  const parts = ip.split(':');
334
283
  return `${parts[0]}:****`;
335
284
  }
336
-
337
- /**
338
- * Start periodic cleanup of expired entries
339
- */
340
- private startCleanup(): void {
341
- // Clean up every 5 minutes
342
- this.cleanupInterval = setInterval(
343
- () => {
344
- const now = Date.now();
345
- let cleaned = 0;
346
-
347
- for (const [key, entry] of this.store.entries()) {
348
- if (now >= entry.resetTime) {
349
- this.store.delete(key);
350
- cleaned++;
351
- }
352
- }
353
-
354
- if (cleaned > 0) {
355
- this.logger.debug(`Cleaned up ${cleaned} expired rate limit entries`);
356
- }
357
- },
358
- 5 * 60 * 1000,
359
- );
360
-
361
- // Prevent the interval from keeping the process alive
362
- if (this.cleanupInterval.unref) {
363
- this.cleanupInterval.unref();
364
- }
365
- }
366
285
  }
@@ -658,7 +658,13 @@ export class CoreBetterAuthUserMapper {
658
658
  lastName: sessionUser.name.split(' ').slice(1).join(' '),
659
659
  }),
660
660
  ...(sessionUser.emailVerified !== undefined && { verified: sessionUser.emailVerified }),
661
- ...(sessionUser.image && { avatar: sessionUser.image }),
661
+ // Deliberately NOT `avatar: sessionUser.image`. `image` is a Better-Auth BUILT-IN column,
662
+ // so it is client-writable through `POST /iam/update-user` — it is not covered by
663
+ // PROTECTED_INPUT_FALSE_KEYS, which only locks the fields this framework adds. Copying it
664
+ // here wrote an attacker-chosen value into `avatar` through a NATIVE driver update, past
665
+ // both the `S_NO_ONE` input restriction on the field and every Mongoose plugin. Since the
666
+ // avatar endpoint deletes the file the previous value points at, that turned "set your own
667
+ // image" into "delete any file in the store". The avatar upload is the only writer.
662
668
  iamId: sessionUser.id,
663
669
  updatedAt: new Date(),
664
670
  ...processedAdditionalData,
@@ -0,0 +1,108 @@
1
+ # File Module Integration Checklist
2
+
3
+ ## Reference Implementation
4
+
5
+ - Local: `node_modules/@lenne.tech/nest-server/src/server/modules/file/`
6
+ - GitHub: https://github.com/lenneTech/nest-server/tree/develop/src/server/modules/file
7
+
8
+ ## Required Files (Create in Order)
9
+
10
+ ### 1. FileService
11
+
12
+ **Create:** `src/server/modules/file/file.service.ts`
13
+ **Copy from:** Reference implementation.
14
+
15
+ Override `checkRights()` here if any file needs a per-file rule (owner, tenant, published flag).
16
+ Roles alone cannot express that — see step 4.
17
+
18
+ ### 2. FileController
19
+
20
+ **Create:** `src/server/modules/file/file.controller.ts`
21
+ **Copy from:** Reference implementation.
22
+
23
+ **Do NOT override `getFileById` / `getFile`.** Role metadata lives on the function object, so an
24
+ override carries its own and opts the route out of `file.downloadRoles` permanently. Worse, an
25
+ override that omits `@Get(...)` unregisters the route entirely — 404 for everyone, with no error
26
+ and no warning. Add your project's own endpoints (upload, info, delete) alongside the inherited
27
+ ones instead.
28
+
29
+ ### 3. FileModule
30
+
31
+ **Create:** `src/server/modules/file/file.module.ts`
32
+ **Copy from:** Reference implementation.
33
+
34
+ Register `FileController` and `FileService`. `CoreFileResolver` is **not** registered by any core
35
+ module — add it (or a subclass) yourself only if you want the GraphQL surface.
36
+
37
+ ### 4. Configure access
38
+
39
+ **Edit:** `src/config.env.ts`
40
+
41
+ ```typescript
42
+ file: {
43
+ storage: 'gridfs', // 'filesystem' | 'gridfs' | 's3' — omit to derive it
44
+ storageDir: 'uploads/files', // only for 'filesystem'
45
+ downloadRoles: [RoleEnum.ADMIN], // GET /files/id/:id, GET /files/:filename, getFileInfo
46
+ uploadRoles: [RoleEnum.ADMIN], // uploadFile, uploadFiles (GraphQL)
47
+ deleteRoles: [RoleEnum.ADMIN], // deleteFile (GraphQL)
48
+ },
49
+ tus: {
50
+ roles: [RoleEnum.S_USER], // resumable uploads
51
+ },
52
+ ```
53
+
54
+ **WHY you may not need `storage` at all:** left unset it is derived — `'s3'` when `s3.bucket` is
55
+ configured, else `'gridfs'` when a database is, else `'filesystem'`. Set it when you want the choice
56
+ pinned regardless of what else is in the config. Either way, an unavailable store **fails the boot**
57
+ instead of silently falling back — see the module README, "Storage drivers".
58
+
59
+ **WHY these default to ADMIN:** the file store is one bucket shared by every feature of the project
60
+ — GridFS or S3, whichever `file.storage` selects — and the ObjectIds naming its blobs are not secrets:
61
+ one upload of your own bounds the id space enough to brute-force neighbours. Anything wider is a
62
+ decision worth making explicitly.
63
+
64
+ **WHY roles are not enough:** they answer "may this caller reach the endpoint", never "may this
65
+ caller have THIS file". For the latter, write an owner or tenant into the metadata at upload time
66
+ (`createFile(file, { metadata: { ownerId } })`) and compare it in `checkRights()` via
67
+ `getRawFileInfo()` / `getRawFileInfoByName()`. The public `getFileInfo()` strips restricted fields
68
+ and is unusable for the decision. `getRawFileInfo()` checks S3 metadata first, then GridFS, so the
69
+ same rule works under either `file.storage`.
70
+
71
+ ### 5. Decide how the frontend fetches files
72
+
73
+ A browser `<img src>` or `<a download>` cannot send an `Authorization` header, and a CORS preflight
74
+ sends no credentials at all. With anything stricter than `S_EVERYONE`, markup-driven requests only
75
+ work when the session travels as a **cookie** on a same-site request.
76
+
77
+ If some files are genuinely public and others are not, do not solve it with a role list. Either
78
+ record a visibility flag in the metadata and branch in `checkRights()`, or expose a separate public
79
+ route for exactly the public files and leave the core routes gated.
80
+
81
+ ## Verification Checklist
82
+
83
+ - [ ] `pnpm run build` succeeds
84
+ - [ ] Anonymous `GET /files/id/<id>` answers **401**
85
+ - [ ] A signed-in non-privileged user answers **403** (not 401 — a 401 makes SPA auth layers log the
86
+ user out)
87
+ - [ ] A caller holding a configured role downloads successfully
88
+ - [ ] If you set `file.downloadRoles`, the value actually takes effect — if it does not, something
89
+ in your controller is overriding the member (see step 2)
90
+ - [ ] TUS upload works for a signed-in user and is refused anonymously
91
+ - [ ] The boot log names the driver you expect (`[CoreFileStorage] File storage: …`)
92
+ - [ ] With `file.storage: 's3'`: a per-file rule in `checkRights()` still fires (it reads S3 metadata
93
+ via `getRawFileInfo()`), and any presigned-download expiry is short enough for your audience
94
+ - [ ] With `file.storage: 'filesystem'`: `storageDir` is a **mounted volume** if the container is
95
+ ever restarted or scaled — the directory is pod-local
96
+ - [ ] `OPTIONS /tus` answers without credentials (browser preflight)
97
+ - [ ] Avatars / images in the frontend still render for the roles that should see them
98
+
99
+ ## Common Mistakes
100
+
101
+ | Mistake | Symptom | Fix |
102
+ | -------------------------------------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
103
+ | Overriding `getFileById` to add Swagger decorators | `file.downloadRoles` silently ignored | Don't override; document at class level |
104
+ | Overriding without re-declaring `@Get(...)` | Route 404s for everyone, no error logged | Don't override |
105
+ | `downloadRoles: []` | Warning logged, default applied | Use a non-empty array; `[]` would read as "no roles required" and open the route |
106
+ | Expecting `metadata` back from `getFileInfo()` | `undefined` | Use `getRawFileInfo()` inside `checkRights()` |
107
+ | `downloadRoles: ['member']` with multiTenancy | Works from code, fails from `<img>` | Both file classes carry `@SkipTenantCheck()`; roles resolve against `user.roles`. Use `checkRights()` for tenant rules |
108
+ | Signed-in user can upload via TUS but not download | 403 on their own file | `tus.roles` and `file.downloadRoles` are separate. Add an owner to the metadata and authorize per file |