@lenne.tech/nest-server 11.32.4 → 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.
- package/.claude/rules/architecture.md +16 -0
- package/.claude/rules/configurable-features.md +11 -3
- package/.claude/rules/testing.md +101 -1
- package/CLAUDE.md +5 -0
- package/FRAMEWORK-API.md +58 -3
- package/dist/core/common/helpers/content-disposition.helper.d.ts +1 -0
- package/dist/core/common/helpers/content-disposition.helper.js +14 -0
- package/dist/core/common/helpers/content-disposition.helper.js.map +1 -0
- package/dist/core/common/helpers/file.helper.d.ts +7 -0
- package/dist/core/common/helpers/file.helper.js +21 -4
- package/dist/core/common/helpers/file.helper.js.map +1 -1
- package/dist/core/common/helpers/graceful-shutdown.helper.d.ts +2 -0
- package/dist/core/common/helpers/graceful-shutdown.helper.js +55 -0
- package/dist/core/common/helpers/graceful-shutdown.helper.js.map +1 -0
- package/dist/core/common/helpers/project-name.helper.d.ts +2 -0
- package/dist/core/common/helpers/project-name.helper.js +38 -0
- package/dist/core/common/helpers/project-name.helper.js.map +1 -0
- package/dist/core/common/interfaces/cron-job-config.interface.d.ts +1 -0
- package/dist/core/common/interfaces/server-options.interface.d.ts +39 -0
- package/dist/core/common/services/core-cron-jobs.initializer.d.ts +10 -0
- package/dist/core/common/services/core-cron-jobs.initializer.js +46 -0
- package/dist/core/common/services/core-cron-jobs.initializer.js.map +1 -0
- package/dist/core/common/services/core-cron-jobs.registry.d.ts +8 -0
- package/dist/core/common/services/core-cron-jobs.registry.js +12 -0
- package/dist/core/common/services/core-cron-jobs.registry.js.map +1 -0
- package/dist/core/common/services/core-cron-jobs.service.d.ts +37 -7
- package/dist/core/common/services/core-cron-jobs.service.js +278 -27
- package/dist/core/common/services/core-cron-jobs.service.js.map +1 -1
- package/dist/core/common/services/core-redis-pubsub.d.ts +20 -0
- package/dist/core/common/services/core-redis-pubsub.js +112 -0
- package/dist/core/common/services/core-redis-pubsub.js.map +1 -0
- package/dist/core/common/services/core-redis.service.d.ts +25 -0
- package/dist/core/common/services/core-redis.service.js +154 -0
- package/dist/core/common/services/core-redis.service.js.map +1 -0
- package/dist/core/common/services/core-s3.service.d.ts +51 -0
- package/dist/core/common/services/core-s3.service.js +261 -0
- package/dist/core/common/services/core-s3.service.js.map +1 -0
- package/dist/core/common/services/core-trust-proxy.initializer.d.ts +16 -0
- package/dist/core/common/services/core-trust-proxy.initializer.js +77 -0
- package/dist/core/common/services/core-trust-proxy.initializer.js.map +1 -0
- package/dist/core/common/services/rate-limit-store.d.ts +61 -0
- package/dist/core/common/services/rate-limit-store.js +235 -0
- package/dist/core/common/services/rate-limit-store.js.map +1 -0
- package/dist/core/modules/ai/core-ai-mcp.controller.d.ts +19 -3
- package/dist/core/modules/ai/core-ai-mcp.controller.js +148 -18
- package/dist/core/modules/ai/core-ai-mcp.controller.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai.service.d.ts +7 -2
- package/dist/core/modules/ai/services/core-ai.service.js +24 -20
- package/dist/core/modules/ai/services/core-ai.service.js.map +1 -1
- package/dist/core/modules/auth/core-auth.module.js +4 -1
- package/dist/core/modules/auth/core-auth.module.js.map +1 -1
- package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.d.ts +1 -1
- package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js +3 -14
- package/dist/core/modules/auth/guards/legacy-auth-rate-limit.guard.js.map +1 -1
- package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.d.ts +12 -10
- package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js +30 -59
- package/dist/core/modules/auth/services/legacy-auth-rate-limiter.service.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.d.ts +8 -2
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js +118 -45
- package/dist/core/modules/better-auth/core-better-auth-email-verification.service.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.d.ts +1 -1
- package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js +2 -11
- package/dist/core/modules/better-auth/core-better-auth-rate-limit.middleware.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.d.ts +12 -10
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js +30 -85
- package/dist/core/modules/better-auth/core-better-auth-rate-limiter.service.js.map +1 -1
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js +0 -1
- package/dist/core/modules/better-auth/core-better-auth-user.mapper.js.map +1 -1
- package/dist/core/modules/file/core-file.controller.d.ts +11 -2
- package/dist/core/modules/file/core-file.controller.js +52 -15
- package/dist/core/modules/file/core-file.controller.js.map +1 -1
- package/dist/core/modules/file/core-file.resolver.d.ts +4 -4
- package/dist/core/modules/file/core-file.resolver.js +23 -16
- package/dist/core/modules/file/core-file.resolver.js.map +1 -1
- package/dist/core/modules/file/core-file.service.d.ts +41 -7
- package/dist/core/modules/file/core-file.service.js +227 -7
- package/dist/core/modules/file/core-file.service.js.map +1 -1
- package/dist/core/modules/file/file-metadata.helper.d.ts +16 -0
- package/dist/core/modules/file/file-metadata.helper.js +33 -0
- package/dist/core/modules/file/file-metadata.helper.js.map +1 -0
- package/dist/core/modules/file/file-roles.helper.d.ts +4 -0
- package/dist/core/modules/file/file-roles.helper.js +53 -0
- package/dist/core/modules/file/file-roles.helper.js.map +1 -0
- package/dist/core/modules/file/file-storage.helper.d.ts +13 -0
- package/dist/core/modules/file/file-storage.helper.js +65 -0
- package/dist/core/modules/file/file-storage.helper.js.map +1 -0
- package/dist/core/modules/file/filesystem-file.helper.d.ts +22 -0
- package/dist/core/modules/file/filesystem-file.helper.js +77 -0
- package/dist/core/modules/file/filesystem-file.helper.js.map +1 -0
- package/dist/core/modules/file/interfaces/file-service-options.interface.d.ts +1 -0
- package/dist/core/modules/file/interfaces/file-upload.interface.d.ts +7 -1
- package/dist/core/modules/file/s3-file.helper.d.ts +37 -0
- package/dist/core/modules/file/s3-file.helper.js +81 -0
- package/dist/core/modules/file/s3-file.helper.js.map +1 -0
- package/dist/core/modules/hub/core-hub.controller.d.ts +8 -8
- package/dist/core/modules/hub/core-hub.controller.js +24 -22
- package/dist/core/modules/hub/core-hub.controller.js.map +1 -1
- package/dist/core/modules/hub/helpers/hub-mask.helper.js +2 -2
- package/dist/core/modules/hub/helpers/hub-mask.helper.js.map +1 -1
- package/dist/core/modules/hub/hub-buffer.d.ts +23 -0
- package/dist/core/modules/hub/hub-buffer.js +101 -0
- package/dist/core/modules/hub/hub-buffer.js.map +1 -0
- package/dist/core/modules/hub/services/core-hub-mailbox.service.d.ts +7 -5
- package/dist/core/modules/hub/services/core-hub-mailbox.service.js +14 -10
- package/dist/core/modules/hub/services/core-hub-mailbox.service.js.map +1 -1
- package/dist/core/modules/hub/services/hub-log-buffer.service.d.ts +5 -2
- package/dist/core/modules/hub/services/hub-log-buffer.service.js +17 -8
- package/dist/core/modules/hub/services/hub-log-buffer.service.js.map +1 -1
- package/dist/core/modules/hub/services/hub-query-profiler.service.d.ts +4 -2
- package/dist/core/modules/hub/services/hub-query-profiler.service.js +13 -7
- package/dist/core/modules/hub/services/hub-query-profiler.service.js.map +1 -1
- package/dist/core/modules/hub/services/hub-trace-buffer.service.d.ts +4 -2
- package/dist/core/modules/hub/services/hub-trace-buffer.service.js +13 -8
- package/dist/core/modules/hub/services/hub-trace-buffer.service.js.map +1 -1
- package/dist/core/modules/migrate/helpers/migration.helper.d.ts +2 -6
- package/dist/core/modules/migrate/helpers/migration.helper.js +4 -4
- package/dist/core/modules/migrate/helpers/migration.helper.js.map +1 -1
- package/dist/core/modules/migrate/migration-runner.d.ts +1 -0
- package/dist/core/modules/migrate/migration-runner.js +4 -0
- package/dist/core/modules/migrate/migration-runner.js.map +1 -1
- package/dist/core/modules/migrate/mongo-state-store.d.ts +1 -0
- package/dist/core/modules/migrate/mongo-state-store.js +98 -9
- package/dist/core/modules/migrate/mongo-state-store.js.map +1 -1
- package/dist/core/modules/system-setup/core-system-setup.service.d.ts +2 -0
- package/dist/core/modules/system-setup/core-system-setup.service.js +31 -1
- package/dist/core/modules/system-setup/core-system-setup.service.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.guard.d.ts +17 -3
- package/dist/core/modules/tenant/core-tenant.guard.js +92 -15
- package/dist/core/modules/tenant/core-tenant.guard.js.map +1 -1
- package/dist/core/modules/tenant/core-tenant.module.js +3 -4
- package/dist/core/modules/tenant/core-tenant.module.js.map +1 -1
- package/dist/core/modules/tus/core-tus.controller.d.ts +2 -0
- package/dist/core/modules/tus/core-tus.controller.js +29 -3
- package/dist/core/modules/tus/core-tus.controller.js.map +1 -1
- package/dist/core/modules/tus/core-tus.service.d.ts +24 -1
- package/dist/core/modules/tus/core-tus.service.js +182 -28
- package/dist/core/modules/tus/core-tus.service.js.map +1 -1
- package/dist/core/modules/tus/interfaces/tus-config.interface.js +3 -0
- package/dist/core/modules/tus/interfaces/tus-config.interface.js.map +1 -1
- package/dist/core/modules/tus/tus-redis-locker.d.ts +25 -0
- package/dist/core/modules/tus/tus-redis-locker.js +87 -0
- package/dist/core/modules/tus/tus-redis-locker.js.map +1 -0
- package/dist/core/modules/tus/tus.module.d.ts +1 -0
- package/dist/core/modules/tus/tus.module.js +30 -4
- package/dist/core/modules/tus/tus.module.js.map +1 -1
- package/dist/core.module.js +18 -1
- package/dist/core.module.js.map +1 -1
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -1
- package/dist/main.js +2 -1
- package/dist/main.js.map +1 -1
- package/dist/server/modules/file/file.controller.js.map +1 -1
- package/dist/server/modules/file/file.resolver.js +1 -12
- package/dist/server/modules/file/file.resolver.js.map +1 -1
- package/dist/server/modules/file/file.service.d.ts +5 -1
- package/dist/server/modules/file/file.service.js +12 -3
- package/dist/server/modules/file/file.service.js.map +1 -1
- package/dist/server/modules/user/avatar.controller.d.ts +5 -1
- package/dist/server/modules/user/avatar.controller.js +26 -10
- package/dist/server/modules/user/avatar.controller.js.map +1 -1
- package/dist/server/modules/user/user.model.js +3 -2
- package/dist/server/modules/user/user.model.js.map +1 -1
- package/dist/server/modules/user/user.module.js +6 -2
- package/dist/server/modules/user/user.module.js.map +1 -1
- package/dist/server/modules/user/user.service.d.ts +1 -1
- package/dist/server/modules/user/user.service.js +6 -13
- package/dist/server/modules/user/user.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/docs/REQUEST-LIFECYCLE.md +57 -8
- package/docs/security-overrides.md +21 -3
- package/migration-guides/11.32.3-to-11.32.4.md +15 -3
- package/migration-guides/11.32.x-to-11.33.x.md +1551 -0
- package/package.json +74 -42
- package/src/core/common/helpers/content-disposition.helper.ts +90 -0
- package/src/core/common/helpers/file.helper.ts +53 -10
- package/src/core/common/helpers/graceful-shutdown.helper.ts +116 -0
- package/src/core/common/helpers/project-name.helper.ts +71 -0
- package/src/core/common/interfaces/cron-job-config.interface.ts +16 -0
- package/src/core/common/interfaces/server-options.interface.ts +439 -2
- package/src/core/common/services/core-cron-jobs.initializer.ts +39 -0
- package/src/core/common/services/core-cron-jobs.registry.ts +45 -0
- package/src/core/common/services/core-cron-jobs.service.ts +589 -47
- package/src/core/common/services/core-redis-pubsub.ts +183 -0
- package/src/core/common/services/core-redis.service.ts +249 -0
- package/src/core/common/services/core-s3.service.ts +465 -0
- package/src/core/common/services/core-trust-proxy.initializer.ts +112 -0
- package/src/core/common/services/rate-limit-store.ts +492 -0
- package/src/core/modules/ai/README.md +25 -0
- package/src/core/modules/ai/core-ai-mcp.controller.ts +253 -22
- package/src/core/modules/ai/services/core-ai.service.ts +33 -24
- package/src/core/modules/auth/core-auth.module.ts +7 -1
- package/src/core/modules/auth/guards/legacy-auth-rate-limit.guard.ts +12 -21
- package/src/core/modules/auth/services/legacy-auth-rate-limiter.service.ts +49 -93
- package/src/core/modules/better-auth/core-better-auth-email-verification.service.ts +202 -66
- package/src/core/modules/better-auth/core-better-auth-rate-limit.middleware.ts +11 -18
- package/src/core/modules/better-auth/core-better-auth-rate-limiter.service.ts +51 -132
- package/src/core/modules/better-auth/core-better-auth-user.mapper.ts +7 -1
- package/src/core/modules/file/INTEGRATION-CHECKLIST.md +108 -0
- package/src/core/modules/file/README.md +277 -53
- package/src/core/modules/file/core-file.controller.ts +168 -12
- package/src/core/modules/file/core-file.resolver.ts +46 -12
- package/src/core/modules/file/core-file.service.ts +482 -16
- package/src/core/modules/file/file-metadata.helper.ts +142 -0
- package/src/core/modules/file/file-roles.helper.ts +110 -0
- package/src/core/modules/file/file-storage.helper.ts +163 -0
- package/src/core/modules/file/filesystem-file.helper.ts +184 -0
- package/src/core/modules/file/interfaces/file-service-options.interface.ts +14 -0
- package/src/core/modules/file/interfaces/file-upload.interface.ts +17 -1
- package/src/core/modules/file/s3-file.helper.ts +209 -0
- package/src/core/modules/hub/README.md +48 -0
- package/src/core/modules/hub/core-hub.controller.ts +18 -16
- package/src/core/modules/hub/helpers/hub-mask.helper.ts +21 -4
- package/src/core/modules/hub/hub-buffer.ts +198 -0
- package/src/core/modules/hub/services/core-hub-mailbox.service.ts +22 -12
- package/src/core/modules/hub/services/hub-log-buffer.service.ts +24 -8
- package/src/core/modules/hub/services/hub-query-profiler.service.ts +9 -6
- package/src/core/modules/hub/services/hub-trace-buffer.service.ts +14 -9
- package/src/core/modules/migrate/README.md +33 -6
- package/src/core/modules/migrate/helpers/migration.helper.ts +13 -6
- package/src/core/modules/migrate/migration-runner.ts +10 -1
- package/src/core/modules/migrate/mongo-state-store.ts +179 -11
- package/src/core/modules/system-setup/README.md +7 -2
- package/src/core/modules/system-setup/core-system-setup.service.ts +95 -1
- package/src/core/modules/tenant/README.md +21 -1
- package/src/core/modules/tenant/core-tenant.guard.ts +135 -14
- package/src/core/modules/tenant/core-tenant.module.ts +8 -4
- package/src/core/modules/tus/INTEGRATION-CHECKLIST.md +93 -21
- package/src/core/modules/tus/README.md +35 -2
- package/src/core/modules/tus/core-tus.controller.ts +64 -12
- package/src/core/modules/tus/core-tus.service.ts +358 -34
- package/src/core/modules/tus/interfaces/tus-config.interface.ts +9 -0
- package/src/core/modules/tus/tus-redis-locker.ts +139 -0
- package/src/core/modules/tus/tus.module.ts +78 -6
- package/src/core.module.ts +26 -1
- package/src/index.ts +10 -0
- package/src/main.ts +7 -1
- package/src/server/modules/file/file.controller.ts +7 -3
- package/src/server/modules/file/file.resolver.ts +5 -17
- package/src/server/modules/file/file.service.ts +42 -3
- package/src/server/modules/user/avatar.controller.ts +39 -9
- package/src/server/modules/user/user.model.ts +9 -3
- package/src/server/modules/user/user.module.ts +12 -3
- package/src/server/modules/user/user.service.ts +20 -17
- package/src/test/README.md +7 -2
- package/src/core/common/helpers/file.helper.spec.ts +0 -145
- package/src/core/common/helpers/logging.helper.spec.ts +0 -61
- package/src/core/common/helpers/process-diagnostics.helper.spec.ts +0 -310
- package/src/core/common/services/brevo.service.spec.ts +0 -266
- package/src/core/modules/file/core-file.controller.spec.ts +0 -164
- package/src/core/modules/hub/core-hub.module.spec.ts +0 -108
- package/src/core/modules/hub/helpers/hub-command-shape.helper.spec.ts +0 -48
- package/src/core/modules/hub/helpers/hub-mask.helper.spec.ts +0 -67
- package/src/core/modules/hub/helpers/hub-mermaid.helper.spec.ts +0 -61
- package/src/core/modules/hub/helpers/hub-shell.helper.spec.ts +0 -106
- package/src/core/modules/hub/hub-config.helper.spec.ts +0 -108
- package/src/core/modules/hub/hub-ring-buffer.spec.ts +0 -95
- package/src/core/modules/hub/services/core-hub-mailbox.service.spec.ts +0 -116
- package/src/core/modules/hub/services/hub-trace-buffer.service.spec.ts +0 -112
- package/src/core/modules/migrate/helpers/migration.helper.spec.ts +0 -85
|
@@ -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
|
-
*
|
|
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.
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
|
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
|
-
|
|
160
|
-
|
|
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}: ${
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
*
|
|
245
|
-
*
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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 |
|