@lenne.tech/nest-server 11.6.1 → 11.7.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 (120) hide show
  1. package/dist/config.env.js +132 -0
  2. package/dist/config.env.js.map +1 -1
  3. package/dist/core/common/decorators/graphql-populate.decorator.d.ts +2 -2
  4. package/dist/core/common/decorators/restricted.decorator.d.ts +1 -0
  5. package/dist/core/common/decorators/restricted.decorator.js +1 -1
  6. package/dist/core/common/decorators/restricted.decorator.js.map +1 -1
  7. package/dist/core/common/helpers/filter.helper.d.ts +9 -9
  8. package/dist/core/common/helpers/filter.helper.js +2 -4
  9. package/dist/core/common/helpers/filter.helper.js.map +1 -1
  10. package/dist/core/common/helpers/gridfs.helper.js +3 -3
  11. package/dist/core/common/helpers/gridfs.helper.js.map +1 -1
  12. package/dist/core/common/helpers/input.helper.d.ts +1 -0
  13. package/dist/core/common/helpers/input.helper.js +1 -1
  14. package/dist/core/common/helpers/input.helper.js.map +1 -1
  15. package/dist/core/common/interfaces/server-options.interface.d.ts +51 -0
  16. package/dist/core/common/services/crud.service.d.ts +16 -16
  17. package/dist/core/common/services/crud.service.js +1 -1
  18. package/dist/core/common/services/crud.service.js.map +1 -1
  19. package/dist/core/modules/auth/auth-guard-strategy.enum.d.ts +1 -0
  20. package/dist/core/modules/auth/auth-guard-strategy.enum.js +1 -0
  21. package/dist/core/modules/auth/auth-guard-strategy.enum.js.map +1 -1
  22. package/dist/core/modules/auth/guards/auth.guard.js +11 -5
  23. package/dist/core/modules/auth/guards/auth.guard.js.map +1 -1
  24. package/dist/core/modules/auth/tokens.decorator.d.ts +1 -1
  25. package/dist/core/modules/better-auth/better-auth-auth.model.d.ts +9 -0
  26. package/dist/core/modules/better-auth/better-auth-auth.model.js +63 -0
  27. package/dist/core/modules/better-auth/better-auth-auth.model.js.map +1 -0
  28. package/dist/core/modules/better-auth/better-auth-models.d.ts +43 -0
  29. package/dist/core/modules/better-auth/better-auth-models.js +181 -0
  30. package/dist/core/modules/better-auth/better-auth-models.js.map +1 -0
  31. package/dist/core/modules/better-auth/better-auth-rate-limit.middleware.d.ts +12 -0
  32. package/dist/core/modules/better-auth/better-auth-rate-limit.middleware.js +70 -0
  33. package/dist/core/modules/better-auth/better-auth-rate-limit.middleware.js.map +1 -0
  34. package/dist/core/modules/better-auth/better-auth-rate-limiter.service.d.ts +32 -0
  35. package/dist/core/modules/better-auth/better-auth-rate-limiter.service.js +173 -0
  36. package/dist/core/modules/better-auth/better-auth-rate-limiter.service.js.map +1 -0
  37. package/dist/core/modules/better-auth/better-auth-user.mapper.d.ts +43 -0
  38. package/dist/core/modules/better-auth/better-auth-user.mapper.js +159 -0
  39. package/dist/core/modules/better-auth/better-auth-user.mapper.js.map +1 -0
  40. package/dist/core/modules/better-auth/better-auth.config.d.ts +9 -0
  41. package/dist/core/modules/better-auth/better-auth.config.js +254 -0
  42. package/dist/core/modules/better-auth/better-auth.config.js.map +1 -0
  43. package/dist/core/modules/better-auth/better-auth.middleware.d.ts +20 -0
  44. package/dist/core/modules/better-auth/better-auth.middleware.js +79 -0
  45. package/dist/core/modules/better-auth/better-auth.middleware.js.map +1 -0
  46. package/dist/core/modules/better-auth/better-auth.module.d.ts +38 -0
  47. package/dist/core/modules/better-auth/better-auth.module.js +253 -0
  48. package/dist/core/modules/better-auth/better-auth.module.js.map +1 -0
  49. package/dist/core/modules/better-auth/better-auth.resolver.d.ts +45 -0
  50. package/dist/core/modules/better-auth/better-auth.resolver.js +221 -0
  51. package/dist/core/modules/better-auth/better-auth.resolver.js.map +1 -0
  52. package/dist/core/modules/better-auth/better-auth.service.d.ts +37 -0
  53. package/dist/core/modules/better-auth/better-auth.service.js +148 -0
  54. package/dist/core/modules/better-auth/better-auth.service.js.map +1 -0
  55. package/dist/core/modules/better-auth/better-auth.types.d.ts +39 -0
  56. package/dist/core/modules/better-auth/better-auth.types.js +26 -0
  57. package/dist/core/modules/better-auth/better-auth.types.js.map +1 -0
  58. package/dist/core/modules/better-auth/core-better-auth.controller.d.ts +66 -0
  59. package/dist/core/modules/better-auth/core-better-auth.controller.js +491 -0
  60. package/dist/core/modules/better-auth/core-better-auth.controller.js.map +1 -0
  61. package/dist/core/modules/better-auth/core-better-auth.resolver.d.ts +59 -0
  62. package/dist/core/modules/better-auth/core-better-auth.resolver.js +538 -0
  63. package/dist/core/modules/better-auth/core-better-auth.resolver.js.map +1 -0
  64. package/dist/core/modules/better-auth/index.d.ts +13 -0
  65. package/dist/core/modules/better-auth/index.js +30 -0
  66. package/dist/core/modules/better-auth/index.js.map +1 -0
  67. package/dist/core/modules/user/core-user.model.d.ts +2 -0
  68. package/dist/core/modules/user/core-user.model.js +21 -0
  69. package/dist/core/modules/user/core-user.model.js.map +1 -1
  70. package/dist/core.module.js +7 -0
  71. package/dist/core.module.js.map +1 -1
  72. package/dist/index.d.ts +1 -0
  73. package/dist/index.js +1 -0
  74. package/dist/index.js.map +1 -1
  75. package/dist/server/modules/better-auth/better-auth.controller.d.ts +10 -0
  76. package/dist/server/modules/better-auth/better-auth.controller.js +36 -0
  77. package/dist/server/modules/better-auth/better-auth.controller.js.map +1 -0
  78. package/dist/server/modules/better-auth/better-auth.module.d.ts +9 -0
  79. package/dist/server/modules/better-auth/better-auth.module.js +44 -0
  80. package/dist/server/modules/better-auth/better-auth.module.js.map +1 -0
  81. package/dist/server/modules/better-auth/better-auth.resolver.d.ts +45 -0
  82. package/dist/server/modules/better-auth/better-auth.resolver.js +221 -0
  83. package/dist/server/modules/better-auth/better-auth.resolver.js.map +1 -0
  84. package/dist/server/modules/file/file-info.model.d.ts +71 -3
  85. package/dist/server/modules/user/user.model.d.ts +169 -3
  86. package/dist/server/server.module.js +6 -1
  87. package/dist/server/server.module.js.map +1 -1
  88. package/dist/tsconfig.build.tsbuildinfo +1 -1
  89. package/package.json +21 -22
  90. package/src/config.env.ts +139 -1
  91. package/src/core/common/decorators/restricted.decorator.ts +2 -2
  92. package/src/core/common/helpers/filter.helper.ts +15 -17
  93. package/src/core/common/helpers/gridfs.helper.ts +5 -5
  94. package/src/core/common/helpers/input.helper.ts +2 -2
  95. package/src/core/common/interfaces/server-options.interface.ts +377 -20
  96. package/src/core/common/services/crud.service.ts +22 -22
  97. package/src/core/modules/auth/auth-guard-strategy.enum.ts +1 -0
  98. package/src/core/modules/auth/guards/auth.guard.ts +20 -6
  99. package/src/core/modules/better-auth/README.md +1422 -0
  100. package/src/core/modules/better-auth/better-auth-auth.model.ts +69 -0
  101. package/src/core/modules/better-auth/better-auth-models.ts +140 -0
  102. package/src/core/modules/better-auth/better-auth-rate-limit.middleware.ts +113 -0
  103. package/src/core/modules/better-auth/better-auth-rate-limiter.service.ts +326 -0
  104. package/src/core/modules/better-auth/better-auth-user.mapper.ts +269 -0
  105. package/src/core/modules/better-auth/better-auth.config.ts +488 -0
  106. package/src/core/modules/better-auth/better-auth.middleware.ts +111 -0
  107. package/src/core/modules/better-auth/better-auth.module.ts +474 -0
  108. package/src/core/modules/better-auth/better-auth.resolver.ts +213 -0
  109. package/src/core/modules/better-auth/better-auth.service.ts +314 -0
  110. package/src/core/modules/better-auth/better-auth.types.ts +90 -0
  111. package/src/core/modules/better-auth/core-better-auth.controller.ts +605 -0
  112. package/src/core/modules/better-auth/core-better-auth.resolver.ts +705 -0
  113. package/src/core/modules/better-auth/index.ts +32 -0
  114. package/src/core/modules/user/core-user.model.ts +29 -0
  115. package/src/core.module.ts +13 -0
  116. package/src/index.ts +6 -0
  117. package/src/server/modules/better-auth/better-auth.controller.ts +41 -0
  118. package/src/server/modules/better-auth/better-auth.module.ts +88 -0
  119. package/src/server/modules/better-auth/better-auth.resolver.ts +201 -0
  120. package/src/server/server.module.ts +10 -1
@@ -0,0 +1,69 @@
1
+ import { Field, ObjectType } from '@nestjs/graphql';
2
+
3
+ import { Restricted } from '../../common/decorators/restricted.decorator';
4
+ import { RoleEnum } from '../../common/enums/role.enum';
5
+ import { BetterAuthSessionInfoModel, BetterAuthUserModel } from './better-auth-models';
6
+
7
+ /**
8
+ * Better-Auth Authentication Response Model
9
+ *
10
+ * This model is returned by Better-Auth sign-in/sign-up mutations
11
+ * and provides a structure compatible with the existing auth system
12
+ * while supporting Better-Auth specific features like 2FA.
13
+ */
14
+ @ObjectType({ description: 'Better-Auth Authentication Response' })
15
+ @Restricted(RoleEnum.S_EVERYONE)
16
+ export class BetterAuthAuthModel {
17
+ /**
18
+ * Whether the authentication was successful
19
+ */
20
+ @Field(() => Boolean, { description: 'Whether authentication was successful' })
21
+ success: boolean;
22
+
23
+ /**
24
+ * Whether 2FA verification is required
25
+ * When true, the client should prompt for 2FA code and call betterAuthVerify2FA
26
+ */
27
+ @Field(() => Boolean, {
28
+ description: 'Whether 2FA verification is required to complete sign-in',
29
+ nullable: true,
30
+ })
31
+ requiresTwoFactor?: boolean;
32
+
33
+ /**
34
+ * JWT token (only present if JWT plugin is enabled)
35
+ * Use this for Bearer token authentication
36
+ */
37
+ @Field(() => String, {
38
+ description: 'JWT token for Bearer authentication (if JWT plugin enabled)',
39
+ nullable: true,
40
+ })
41
+ token?: string;
42
+
43
+ /**
44
+ * Authenticated user
45
+ */
46
+ @Field(() => BetterAuthUserModel, {
47
+ description: 'Authenticated user',
48
+ nullable: true,
49
+ })
50
+ user?: BetterAuthUserModel;
51
+
52
+ /**
53
+ * Session information
54
+ */
55
+ @Field(() => BetterAuthSessionInfoModel, {
56
+ description: 'Session information',
57
+ nullable: true,
58
+ })
59
+ session?: BetterAuthSessionInfoModel;
60
+
61
+ /**
62
+ * Error message if authentication failed
63
+ */
64
+ @Field(() => String, {
65
+ description: 'Error message if authentication failed',
66
+ nullable: true,
67
+ })
68
+ error?: string;
69
+ }
@@ -0,0 +1,140 @@
1
+ import { Field, ObjectType } from '@nestjs/graphql';
2
+
3
+ import { Restricted } from '../../common/decorators/restricted.decorator';
4
+ import { RoleEnum } from '../../common/enums/role.enum';
5
+
6
+ /**
7
+ * Better-Auth User Model for GraphQL
8
+ */
9
+ @ObjectType({ description: 'Better-Auth User' })
10
+ @Restricted(RoleEnum.S_EVERYONE)
11
+ export class BetterAuthUserModel {
12
+ @Field(() => String, { description: 'User ID' })
13
+ id: string;
14
+
15
+ @Field(() => String, { description: 'IAM provider user ID (e.g., Better-Auth)', nullable: true })
16
+ iamId?: string;
17
+
18
+ @Field(() => String, { description: 'Email address' })
19
+ email: string;
20
+
21
+ @Field(() => String, { description: 'Display name', nullable: true })
22
+ name?: string;
23
+
24
+ @Field(() => Boolean, { description: 'Email verified status', nullable: true })
25
+ emailVerified?: boolean;
26
+
27
+ @Field(() => Boolean, { description: 'User verified status', nullable: true })
28
+ verified?: boolean;
29
+
30
+ @Field(() => [String], { description: 'User roles', nullable: true })
31
+ roles?: string[];
32
+ }
33
+
34
+ /**
35
+ * Better-Auth Session Model for GraphQL
36
+ */
37
+ @ObjectType({ description: 'Better-Auth Session' })
38
+ @Restricted(RoleEnum.S_USER)
39
+ export class BetterAuthSessionModel {
40
+ @Field(() => String, { description: 'Session ID' })
41
+ id: string;
42
+
43
+ @Field(() => Date, { description: 'Session expiration date' })
44
+ expiresAt: Date;
45
+
46
+ @Field(() => BetterAuthUserModel, { description: 'Session user' })
47
+ user: BetterAuthUserModel;
48
+ }
49
+
50
+ /**
51
+ * Better-Auth Session Info (simplified for responses)
52
+ */
53
+ @ObjectType({ description: 'Better-Auth Session Info' })
54
+ @Restricted(RoleEnum.S_EVERYONE)
55
+ export class BetterAuthSessionInfoModel {
56
+ @Field(() => String, { description: 'Session ID', nullable: true })
57
+ id?: string;
58
+
59
+ @Field(() => String, { description: 'Session token', nullable: true })
60
+ token?: string;
61
+
62
+ @Field(() => Date, { description: 'Session expiration date', nullable: true })
63
+ expiresAt?: Date;
64
+ }
65
+
66
+ /**
67
+ * Better-Auth 2FA Setup Model (returned when enabling 2FA)
68
+ */
69
+ @ObjectType({ description: 'Better-Auth 2FA setup data' })
70
+ @Restricted(RoleEnum.S_USER)
71
+ export class BetterAuth2FASetupModel {
72
+ @Field(() => Boolean, { description: 'Whether the operation was successful' })
73
+ success: boolean;
74
+
75
+ @Field(() => String, { description: 'TOTP URI for QR code generation', nullable: true })
76
+ totpUri?: string;
77
+
78
+ @Field(() => [String], { description: 'Backup codes for account recovery', nullable: true })
79
+ backupCodes?: string[];
80
+
81
+ @Field(() => String, { description: 'Error message if failed', nullable: true })
82
+ error?: string;
83
+ }
84
+
85
+ /**
86
+ * Better-Auth Passkey Model
87
+ */
88
+ @ObjectType({ description: 'Better-Auth Passkey' })
89
+ @Restricted(RoleEnum.S_USER)
90
+ export class BetterAuthPasskeyModel {
91
+ @Field(() => String, { description: 'Passkey ID' })
92
+ id: string;
93
+
94
+ @Field(() => String, { description: 'Passkey name', nullable: true })
95
+ name?: string;
96
+
97
+ @Field(() => String, { description: 'Credential ID' })
98
+ credentialId: string;
99
+
100
+ @Field(() => Date, { description: 'Creation date' })
101
+ createdAt: Date;
102
+ }
103
+
104
+ /**
105
+ * Better-Auth Passkey Registration Challenge
106
+ */
107
+ @ObjectType({ description: 'Better-Auth Passkey registration challenge' })
108
+ @Restricted(RoleEnum.S_USER)
109
+ export class BetterAuthPasskeyChallengeModel {
110
+ @Field(() => Boolean, { description: 'Whether the operation was successful' })
111
+ success: boolean;
112
+
113
+ @Field(() => String, { description: 'Challenge data (JSON)', nullable: true })
114
+ challenge?: string;
115
+
116
+ @Field(() => String, { description: 'Error message if failed', nullable: true })
117
+ error?: string;
118
+ }
119
+
120
+ /**
121
+ * Better-Auth features status
122
+ */
123
+ @ObjectType({ description: 'Better-Auth features status' })
124
+ @Restricted(RoleEnum.S_EVERYONE)
125
+ export class BetterAuthFeaturesModel {
126
+ @Field(() => Boolean, { description: 'Whether Better-Auth is enabled' })
127
+ enabled: boolean;
128
+
129
+ @Field(() => Boolean, { description: 'Whether JWT plugin is enabled' })
130
+ jwt: boolean;
131
+
132
+ @Field(() => Boolean, { description: 'Whether 2FA is enabled' })
133
+ twoFactor: boolean;
134
+
135
+ @Field(() => Boolean, { description: 'Whether Passkey is enabled' })
136
+ passkey: boolean;
137
+
138
+ @Field(() => [String], { description: 'List of enabled social providers' })
139
+ socialProviders: string[];
140
+ }
@@ -0,0 +1,113 @@
1
+ import { HttpException, HttpStatus, Injectable, NestMiddleware } from '@nestjs/common';
2
+ import { NextFunction, Request, Response } from 'express';
3
+
4
+ import { BetterAuthRateLimiter, RateLimitResult } from './better-auth-rate-limiter.service';
5
+ import { BetterAuthService } from './better-auth.service';
6
+
7
+ /**
8
+ * Middleware that applies rate limiting to Better-Auth endpoints
9
+ *
10
+ * This middleware:
11
+ * 1. Checks if rate limiting is enabled
12
+ * 2. Extracts the client IP address
13
+ * 3. Checks the rate limit for the current request
14
+ * 4. Returns 429 Too Many Requests if limit exceeded
15
+ * 5. Adds rate limit headers to the response
16
+ *
17
+ * Configuration is done via betterAuth.rateLimit in environment config.
18
+ *
19
+ * @example
20
+ * ```typescript
21
+ * // In config.env.ts
22
+ * betterAuth: {
23
+ * enabled: true,
24
+ * rateLimit: {
25
+ * enabled: true,
26
+ * max: 10,
27
+ * windowSeconds: 60,
28
+ * },
29
+ * }
30
+ * ```
31
+ */
32
+ @Injectable()
33
+ export class BetterAuthRateLimitMiddleware implements NestMiddleware {
34
+ constructor(
35
+ private readonly rateLimiter: BetterAuthRateLimiter,
36
+ private readonly betterAuthService: BetterAuthService,
37
+ ) {}
38
+
39
+ use(req: Request, res: Response, next: NextFunction) {
40
+ // Skip if Better-Auth is not enabled
41
+ if (!this.betterAuthService.isEnabled()) {
42
+ return next();
43
+ }
44
+
45
+ // Skip if rate limiting is not enabled
46
+ if (!this.rateLimiter.isEnabled()) {
47
+ return next();
48
+ }
49
+
50
+ // Get client IP
51
+ const ip = this.getClientIp(req);
52
+
53
+ // Get the path relative to basePath
54
+ const basePath = this.betterAuthService.getBasePath();
55
+ const path = req.path.startsWith(basePath) ? req.path.substring(basePath.length) : req.path;
56
+
57
+ // Check rate limit
58
+ const result = this.rateLimiter.check(ip, path);
59
+
60
+ // Add rate limit headers
61
+ this.addRateLimitHeaders(res, result);
62
+
63
+ // If not allowed, return 429
64
+ if (!result.allowed) {
65
+ throw new HttpException(
66
+ {
67
+ error: 'Too Many Requests',
68
+ message: this.rateLimiter.getMessage(),
69
+ retryAfter: result.resetIn,
70
+ statusCode: HttpStatus.TOO_MANY_REQUESTS,
71
+ },
72
+ HttpStatus.TOO_MANY_REQUESTS,
73
+ );
74
+ }
75
+
76
+ next();
77
+ }
78
+
79
+ /**
80
+ * Extract client IP address from request
81
+ * Handles proxied requests via X-Forwarded-For header
82
+ */
83
+ private getClientIp(req: Request): string {
84
+ // Check X-Forwarded-For header (for proxied requests)
85
+ const forwardedFor = req.headers['x-forwarded-for'];
86
+ if (forwardedFor) {
87
+ const ips = Array.isArray(forwardedFor) ? forwardedFor[0] : forwardedFor.split(',')[0];
88
+ return ips.trim();
89
+ }
90
+
91
+ // Check X-Real-IP header (nginx)
92
+ const realIp = req.headers['x-real-ip'];
93
+ if (realIp) {
94
+ return Array.isArray(realIp) ? realIp[0] : realIp;
95
+ }
96
+
97
+ // Fall back to connection remote address
98
+ return req.ip || req.socket?.remoteAddress || 'unknown';
99
+ }
100
+
101
+ /**
102
+ * Add standard rate limit headers to response
103
+ */
104
+ private addRateLimitHeaders(res: Response, result: RateLimitResult): void {
105
+ res.setHeader('X-RateLimit-Limit', result.limit.toString());
106
+ res.setHeader('X-RateLimit-Remaining', result.remaining.toString());
107
+ res.setHeader('X-RateLimit-Reset', result.resetIn.toString());
108
+
109
+ if (!result.allowed) {
110
+ res.setHeader('Retry-After', result.resetIn.toString());
111
+ }
112
+ }
113
+ }
@@ -0,0 +1,326 @@
1
+ import { Injectable, Logger } from '@nestjs/common';
2
+
3
+ import { IBetterAuthRateLimit } from '../../common/interfaces/server-options.interface';
4
+
5
+ /**
6
+ * Result of a rate limit check
7
+ */
8
+ export interface RateLimitResult {
9
+ /**
10
+ * Whether the request is allowed
11
+ */
12
+ allowed: boolean;
13
+
14
+ /**
15
+ * Current request count in the window
16
+ */
17
+ current: number;
18
+
19
+ /**
20
+ * Maximum requests allowed
21
+ */
22
+ limit: number;
23
+
24
+ /**
25
+ * Number of remaining requests in the window
26
+ */
27
+ remaining: number;
28
+
29
+ /**
30
+ * Seconds until the rate limit resets
31
+ */
32
+ resetIn: number;
33
+ }
34
+
35
+ /**
36
+ * Rate limit entry for tracking requests
37
+ */
38
+ interface RateLimitEntry {
39
+ count: number;
40
+ resetTime: number;
41
+ }
42
+
43
+ /**
44
+ * Default rate limiting configuration
45
+ */
46
+ const DEFAULT_CONFIG: Required<IBetterAuthRateLimit> = {
47
+ enabled: false,
48
+ max: 10,
49
+ message: 'Too many requests, please try again later.',
50
+ skipEndpoints: ['/session', '/callback'],
51
+ strictEndpoints: ['/sign-in', '/sign-up', '/forgot-password', '/reset-password'],
52
+ windowSeconds: 60,
53
+ };
54
+
55
+ /**
56
+ * In-memory rate limiter for Better-Auth endpoints
57
+ *
58
+ * This service provides rate limiting to protect against brute-force attacks
59
+ * on authentication endpoints. It uses an in-memory store with automatic cleanup.
60
+ *
61
+ * Features:
62
+ * - Configurable request limits and time windows
63
+ * - Stricter limits for sensitive endpoints (sign-in, sign-up, etc.)
64
+ * - Skip list for endpoints that don't need rate limiting
65
+ * - Automatic cleanup of expired entries
66
+ * - IP-based tracking
67
+ *
68
+ * @example
69
+ * ```typescript
70
+ * const result = rateLimiter.check('192.168.1.1', '/iam/sign-in');
71
+ * if (!result.allowed) {
72
+ * throw new TooManyRequestsException(rateLimiter.getMessage());
73
+ * }
74
+ * ```
75
+ */
76
+ @Injectable()
77
+ export class BetterAuthRateLimiter {
78
+ private readonly logger = new Logger(BetterAuthRateLimiter.name);
79
+ private readonly store = new Map<string, RateLimitEntry>();
80
+ private config: Required<IBetterAuthRateLimit> = DEFAULT_CONFIG;
81
+ private cleanupInterval: NodeJS.Timeout | null = null;
82
+
83
+ constructor() {
84
+ // Start cleanup interval (every 5 minutes)
85
+ this.startCleanup();
86
+ }
87
+
88
+ /**
89
+ * Configure the rate limiter
90
+ *
91
+ * @param config - Rate limiting configuration
92
+ */
93
+ configure(config: IBetterAuthRateLimit | undefined): void {
94
+ this.config = {
95
+ ...DEFAULT_CONFIG,
96
+ ...config,
97
+ // Ensure arrays are properly merged
98
+ skipEndpoints: config?.skipEndpoints ?? DEFAULT_CONFIG.skipEndpoints,
99
+ strictEndpoints: config?.strictEndpoints ?? DEFAULT_CONFIG.strictEndpoints,
100
+ };
101
+
102
+ if (this.config.enabled) {
103
+ this.logger.log(`Rate limiting enabled: ${this.config.max} requests per ${this.config.windowSeconds}s`);
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Check if a request is allowed under the rate limit
109
+ *
110
+ * @param ip - Client IP address
111
+ * @param path - Request path (relative to basePath)
112
+ * @returns Rate limit check result
113
+ */
114
+ check(ip: string, path: string): RateLimitResult {
115
+ // If rate limiting is disabled, always allow
116
+ if (!this.config.enabled) {
117
+ return {
118
+ allowed: true,
119
+ current: 0,
120
+ limit: Infinity,
121
+ remaining: Infinity,
122
+ resetIn: 0,
123
+ };
124
+ }
125
+
126
+ // Check if this endpoint should skip rate limiting
127
+ if (this.shouldSkip(path)) {
128
+ return {
129
+ allowed: true,
130
+ current: 0,
131
+ limit: Infinity,
132
+ remaining: Infinity,
133
+ resetIn: 0,
134
+ };
135
+ }
136
+
137
+ // Determine the limit for this endpoint
138
+ const limit = this.getLimit(path);
139
+ const key = this.getKey(ip, path);
140
+ const now = Date.now();
141
+
142
+ // Get or create entry
143
+ let entry = this.store.get(key);
144
+
145
+ if (!entry || now >= entry.resetTime) {
146
+ // Create new entry or reset expired one
147
+ entry = {
148
+ count: 1,
149
+ resetTime: now + this.config.windowSeconds * 1000,
150
+ };
151
+ this.store.set(key, entry);
152
+
153
+ return {
154
+ allowed: true,
155
+ current: 1,
156
+ limit,
157
+ remaining: limit - 1,
158
+ resetIn: this.config.windowSeconds,
159
+ };
160
+ }
161
+
162
+ // Increment count
163
+ entry.count++;
164
+
165
+ const resetIn = Math.ceil((entry.resetTime - now) / 1000);
166
+ const allowed = entry.count <= limit;
167
+ const remaining = Math.max(0, limit - entry.count);
168
+
169
+ if (!allowed) {
170
+ this.logger.warn(`Rate limit exceeded for IP ${this.maskIp(ip)} on ${path}: ${entry.count}/${limit}`);
171
+ }
172
+
173
+ return {
174
+ allowed,
175
+ current: entry.count,
176
+ limit,
177
+ remaining,
178
+ resetIn,
179
+ };
180
+ }
181
+
182
+ /**
183
+ * Get the configured error message
184
+ */
185
+ getMessage(): string {
186
+ return this.config.message;
187
+ }
188
+
189
+ /**
190
+ * Check if rate limiting is enabled
191
+ */
192
+ isEnabled(): boolean {
193
+ return this.config.enabled;
194
+ }
195
+
196
+ /**
197
+ * Reset rate limit for a specific IP (useful for testing or admin override)
198
+ *
199
+ * @param ip - Client IP address
200
+ */
201
+ reset(ip: string): void {
202
+ // Remove all entries for this IP
203
+ for (const key of this.store.keys()) {
204
+ if (key.startsWith(`${ip}:`)) {
205
+ this.store.delete(key);
206
+ }
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Clear all rate limit entries (useful for testing)
212
+ */
213
+ clear(): void {
214
+ this.store.clear();
215
+ }
216
+
217
+ /**
218
+ * Get statistics about the rate limiter
219
+ */
220
+ getStats(): { activeEntries: number; enabled: boolean } {
221
+ return {
222
+ activeEntries: this.store.size,
223
+ enabled: this.config.enabled,
224
+ };
225
+ }
226
+
227
+ /**
228
+ * Stop the cleanup interval (for graceful shutdown)
229
+ */
230
+ onModuleDestroy(): void {
231
+ if (this.cleanupInterval) {
232
+ clearInterval(this.cleanupInterval);
233
+ this.cleanupInterval = null;
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Determine if an endpoint should skip rate limiting
239
+ */
240
+ private shouldSkip(path: string): boolean {
241
+ return this.config.skipEndpoints.some((skip) => path === skip || path.endsWith(skip) || path.includes(skip));
242
+ }
243
+
244
+ /**
245
+ * Get the rate limit for an endpoint
246
+ * Strict endpoints get half the normal limit
247
+ */
248
+ private getLimit(path: string): number {
249
+ const isStrict = this.config.strictEndpoints.some(
250
+ (strict) => path === strict || path.endsWith(strict) || path.includes(strict),
251
+ );
252
+
253
+ return isStrict ? Math.ceil(this.config.max / 2) : this.config.max;
254
+ }
255
+
256
+ /**
257
+ * Generate a unique key for rate limiting
258
+ * Uses IP + endpoint category to allow different limits per endpoint type
259
+ */
260
+ private getKey(ip: string, path: string): string {
261
+ // Group similar endpoints together
262
+ const endpoint = this.normalizeEndpoint(path);
263
+ return `${ip}:${endpoint}`;
264
+ }
265
+
266
+ /**
267
+ * Normalize endpoint path for consistent grouping
268
+ */
269
+ private normalizeEndpoint(path: string): string {
270
+ // Remove query strings
271
+ const cleanPath = path.split('?')[0];
272
+
273
+ // Group callback endpoints
274
+ if (cleanPath.includes('/callback/')) {
275
+ return 'callback';
276
+ }
277
+
278
+ // Extract the last segment as the endpoint identifier
279
+ const segments = cleanPath.split('/').filter(Boolean);
280
+ return segments[segments.length - 1] || 'root';
281
+ }
282
+
283
+ /**
284
+ * Mask IP address for logging (privacy)
285
+ */
286
+ private maskIp(ip: string): string {
287
+ if (ip.includes('.')) {
288
+ // IPv4: show first two octets
289
+ const parts = ip.split('.');
290
+ return `${parts[0]}.${parts[1]}.*.*`;
291
+ }
292
+ // IPv6: show first segment
293
+ const parts = ip.split(':');
294
+ return `${parts[0]}:****`;
295
+ }
296
+
297
+ /**
298
+ * Start periodic cleanup of expired entries
299
+ */
300
+ private startCleanup(): void {
301
+ // Clean up every 5 minutes
302
+ this.cleanupInterval = setInterval(
303
+ () => {
304
+ const now = Date.now();
305
+ let cleaned = 0;
306
+
307
+ for (const [key, entry] of this.store.entries()) {
308
+ if (now >= entry.resetTime) {
309
+ this.store.delete(key);
310
+ cleaned++;
311
+ }
312
+ }
313
+
314
+ if (cleaned > 0) {
315
+ this.logger.debug(`Cleaned up ${cleaned} expired rate limit entries`);
316
+ }
317
+ },
318
+ 5 * 60 * 1000,
319
+ );
320
+
321
+ // Prevent the interval from keeping the process alive
322
+ if (this.cleanupInterval.unref) {
323
+ this.cleanupInterval.unref();
324
+ }
325
+ }
326
+ }