@alliance-droid/svelte-auth-core 1.0.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 (178) hide show
  1. package/dist/adapter-context.d.ts +19 -0
  2. package/dist/adapter-context.d.ts.map +1 -0
  3. package/dist/adapter-context.js +68 -0
  4. package/dist/adapter-context.js.map +1 -0
  5. package/dist/adapters/__tests__/adapter-tests.d.ts +7 -0
  6. package/dist/adapters/__tests__/adapter-tests.d.ts.map +1 -0
  7. package/dist/adapters/__tests__/adapter-tests.js +206 -0
  8. package/dist/adapters/__tests__/adapter-tests.js.map +1 -0
  9. package/dist/adapters/adapter.d.ts +60 -0
  10. package/dist/adapters/adapter.d.ts.map +1 -0
  11. package/dist/adapters/adapter.js +2 -0
  12. package/dist/adapters/adapter.js.map +1 -0
  13. package/dist/adapters/filesystem-adapter.d.ts +26 -0
  14. package/dist/adapters/filesystem-adapter.d.ts.map +1 -0
  15. package/dist/adapters/filesystem-adapter.js +148 -0
  16. package/dist/adapters/filesystem-adapter.js.map +1 -0
  17. package/dist/adapters/index.d.ts +6 -0
  18. package/dist/adapters/index.d.ts.map +1 -0
  19. package/dist/adapters/index.js +5 -0
  20. package/dist/adapters/index.js.map +1 -0
  21. package/dist/adapters/mongodb-adapter.d.ts +27 -0
  22. package/dist/adapters/mongodb-adapter.d.ts.map +1 -0
  23. package/dist/adapters/mongodb-adapter.js +213 -0
  24. package/dist/adapters/mongodb-adapter.js.map +1 -0
  25. package/dist/adapters/postgres-adapter.d.ts +30 -0
  26. package/dist/adapters/postgres-adapter.d.ts.map +1 -0
  27. package/dist/adapters/postgres-adapter.js +237 -0
  28. package/dist/adapters/postgres-adapter.js.map +1 -0
  29. package/dist/adapters/sqlite-adapter.d.ts +26 -0
  30. package/dist/adapters/sqlite-adapter.d.ts.map +1 -0
  31. package/dist/adapters/sqlite-adapter.js +261 -0
  32. package/dist/adapters/sqlite-adapter.js.map +1 -0
  33. package/dist/auth.d.ts +48 -0
  34. package/dist/auth.d.ts.map +1 -0
  35. package/dist/auth.js +205 -0
  36. package/dist/auth.js.map +1 -0
  37. package/dist/client-jwt.d.ts +30 -0
  38. package/dist/client-jwt.d.ts.map +1 -0
  39. package/dist/client-jwt.js +57 -0
  40. package/dist/client-jwt.js.map +1 -0
  41. package/dist/client-store.d.ts +31 -0
  42. package/dist/client-store.d.ts.map +1 -0
  43. package/dist/client-store.js +122 -0
  44. package/dist/client-store.js.map +1 -0
  45. package/dist/cors.d.ts +48 -0
  46. package/dist/cors.d.ts.map +1 -0
  47. package/dist/cors.js +88 -0
  48. package/dist/cors.js.map +1 -0
  49. package/dist/csrf.d.ts +57 -0
  50. package/dist/csrf.d.ts.map +1 -0
  51. package/dist/csrf.js +95 -0
  52. package/dist/csrf.js.map +1 -0
  53. package/dist/db.d.ts +22 -0
  54. package/dist/db.d.ts.map +1 -0
  55. package/dist/db.js +43 -0
  56. package/dist/db.js.map +1 -0
  57. package/dist/index.d.ts +35 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +36 -0
  60. package/dist/index.js.map +1 -0
  61. package/dist/input-validation.d.ts +78 -0
  62. package/dist/input-validation.d.ts.map +1 -0
  63. package/dist/input-validation.js +238 -0
  64. package/dist/input-validation.js.map +1 -0
  65. package/dist/oauth-callback.d.ts +31 -0
  66. package/dist/oauth-callback.d.ts.map +1 -0
  67. package/dist/oauth-callback.js +254 -0
  68. package/dist/oauth-callback.js.map +1 -0
  69. package/dist/oauth-providers.d.ts +92 -0
  70. package/dist/oauth-providers.d.ts.map +1 -0
  71. package/dist/oauth-providers.js +213 -0
  72. package/dist/oauth-providers.js.map +1 -0
  73. package/dist/oauth-types.d.ts +77 -0
  74. package/dist/oauth-types.d.ts.map +1 -0
  75. package/dist/oauth-types.js +2 -0
  76. package/dist/oauth-types.js.map +1 -0
  77. package/dist/password.d.ts +31 -0
  78. package/dist/password.d.ts.map +1 -0
  79. package/dist/password.js +54 -0
  80. package/dist/password.js.map +1 -0
  81. package/dist/providers/github-oauth.d.ts +58 -0
  82. package/dist/providers/github-oauth.d.ts.map +1 -0
  83. package/dist/providers/github-oauth.js +230 -0
  84. package/dist/providers/github-oauth.js.map +1 -0
  85. package/dist/providers/google-oauth.d.ts +46 -0
  86. package/dist/providers/google-oauth.d.ts.map +1 -0
  87. package/dist/providers/google-oauth.js +177 -0
  88. package/dist/providers/google-oauth.js.map +1 -0
  89. package/dist/providers/oidc-oauth.d.ts +85 -0
  90. package/dist/providers/oidc-oauth.d.ts.map +1 -0
  91. package/dist/providers/oidc-oauth.js +301 -0
  92. package/dist/providers/oidc-oauth.js.map +1 -0
  93. package/dist/rate-limit.d.ts +36 -0
  94. package/dist/rate-limit.d.ts.map +1 -0
  95. package/dist/rate-limit.js +88 -0
  96. package/dist/rate-limit.js.map +1 -0
  97. package/dist/rate-limiting.d.ts +113 -0
  98. package/dist/rate-limiting.d.ts.map +1 -0
  99. package/dist/rate-limiting.js +221 -0
  100. package/dist/rate-limiting.js.map +1 -0
  101. package/dist/security-headers.d.ts +54 -0
  102. package/dist/security-headers.d.ts.map +1 -0
  103. package/dist/security-headers.js +123 -0
  104. package/dist/security-headers.js.map +1 -0
  105. package/dist/session.d.ts +13 -0
  106. package/dist/session.d.ts.map +1 -0
  107. package/dist/session.js +33 -0
  108. package/dist/session.js.map +1 -0
  109. package/dist/sql-injection-prevention.d.ts +94 -0
  110. package/dist/sql-injection-prevention.d.ts.map +1 -0
  111. package/dist/sql-injection-prevention.js +222 -0
  112. package/dist/sql-injection-prevention.js.map +1 -0
  113. package/dist/token.d.ts +22 -0
  114. package/dist/token.d.ts.map +1 -0
  115. package/dist/token.js +31 -0
  116. package/dist/token.js.map +1 -0
  117. package/dist/types.d.ts +81 -0
  118. package/dist/types.d.ts.map +1 -0
  119. package/dist/types.js +2 -0
  120. package/dist/types.js.map +1 -0
  121. package/dist/user.d.ts +33 -0
  122. package/dist/user.d.ts.map +1 -0
  123. package/dist/user.js +144 -0
  124. package/dist/user.js.map +1 -0
  125. package/package.json +48 -0
  126. package/src/adapter-context.ts +72 -0
  127. package/src/adapters/__tests__/adapter-tests.ts +254 -0
  128. package/src/adapters/__tests__/filesystem-adapter.test.ts +48 -0
  129. package/src/adapters/__tests__/mongodb-adapter.test.ts +64 -0
  130. package/src/adapters/__tests__/postgres-adapter.test.ts +62 -0
  131. package/src/adapters/__tests__/sqlite-adapter.test.ts +103 -0
  132. package/src/adapters/__tests__/test-fs-adapter.json +4 -0
  133. package/src/adapters/adapter.ts +72 -0
  134. package/src/adapters/filesystem-adapter.ts +153 -0
  135. package/src/adapters/index.ts +5 -0
  136. package/src/adapters/mongodb-adapter.ts +208 -0
  137. package/src/adapters/postgres-adapter.ts +261 -0
  138. package/src/adapters/sqlite-adapter.ts +284 -0
  139. package/src/auth.ts +239 -0
  140. package/src/client-jwt.test.ts +137 -0
  141. package/src/client-jwt.ts +67 -0
  142. package/src/client-store.test.ts +149 -0
  143. package/src/client-store.ts +144 -0
  144. package/src/cors.test.ts +175 -0
  145. package/src/cors.ts +115 -0
  146. package/src/csrf.test.ts +226 -0
  147. package/src/csrf.ts +126 -0
  148. package/src/db.ts +57 -0
  149. package/src/index.ts +143 -0
  150. package/src/input-validation.test.ts +347 -0
  151. package/src/input-validation.ts +307 -0
  152. package/src/integration.test.ts +322 -0
  153. package/src/oauth-callback.test.ts +282 -0
  154. package/src/oauth-callback.ts +323 -0
  155. package/src/oauth-providers.ts +232 -0
  156. package/src/oauth-types.ts +82 -0
  157. package/src/password.test.ts +89 -0
  158. package/src/password.ts +62 -0
  159. package/src/providers/github-oauth.test.ts +290 -0
  160. package/src/providers/github-oauth.ts +226 -0
  161. package/src/providers/google-oauth.test.ts +240 -0
  162. package/src/providers/google-oauth.ts +166 -0
  163. package/src/providers/oidc-oauth.test.ts +367 -0
  164. package/src/providers/oidc-oauth.ts +302 -0
  165. package/src/rate-limit.test.ts +308 -0
  166. package/src/rate-limit.ts +118 -0
  167. package/src/rate-limiting.test.ts +390 -0
  168. package/src/rate-limiting.ts +275 -0
  169. package/src/security-headers.test.ts +242 -0
  170. package/src/security-headers.ts +160 -0
  171. package/src/security-penetration.test.ts +705 -0
  172. package/src/session.ts +42 -0
  173. package/src/sql-injection-prevention.test.ts +337 -0
  174. package/src/sql-injection-prevention.ts +272 -0
  175. package/src/token.test.ts +67 -0
  176. package/src/token.ts +34 -0
  177. package/src/types.ts +87 -0
  178. package/src/user.ts +165 -0
@@ -0,0 +1,308 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import {
3
+ checkRateLimit,
4
+ resetRateLimit,
5
+ getRateLimitStatus,
6
+ clearAllRateLimits,
7
+ extractClientIp,
8
+ type RateLimitConfig
9
+ } from './rate-limit';
10
+
11
+ describe('Rate Limiting', () => {
12
+ const loginConfig: RateLimitConfig = {
13
+ maxAttempts: 5,
14
+ windowMs: 10 * 60 * 1000 // 10 minutes
15
+ };
16
+
17
+ const registrationConfig: RateLimitConfig = {
18
+ maxAttempts: 3,
19
+ windowMs: 60 * 60 * 1000 // 1 hour
20
+ };
21
+
22
+ const passwordResetConfig: RateLimitConfig = {
23
+ maxAttempts: 2,
24
+ windowMs: 60 * 60 * 1000 // 1 hour
25
+ };
26
+
27
+ beforeEach(() => {
28
+ clearAllRateLimits();
29
+ });
30
+
31
+ describe('checkRateLimit', () => {
32
+ it('should allow requests under the limit', () => {
33
+ const result1 = checkRateLimit('user@example.com', loginConfig);
34
+ expect(result1.allowed).toBe(true);
35
+ expect(result1.remaining).toBe(4);
36
+
37
+ const result2 = checkRateLimit('user@example.com', loginConfig);
38
+ expect(result2.allowed).toBe(true);
39
+ expect(result2.remaining).toBe(3);
40
+ });
41
+
42
+ it('should block requests exceeding the limit', () => {
43
+ // Make max attempts
44
+ for (let i = 0; i < loginConfig.maxAttempts; i++) {
45
+ checkRateLimit('user@example.com', loginConfig);
46
+ }
47
+
48
+ // Next attempt should be blocked
49
+ const result = checkRateLimit('user@example.com', loginConfig);
50
+ expect(result.allowed).toBe(false);
51
+ expect(result.remaining).toBe(0);
52
+ });
53
+
54
+ it('should enforce login rate limits (5 attempts/10 min)', () => {
55
+ const key = 'test@example.com';
56
+
57
+ for (let i = 0; i < 5; i++) {
58
+ const result = checkRateLimit(key, loginConfig);
59
+ expect(result.allowed).toBe(true);
60
+ }
61
+
62
+ // 6th attempt should fail
63
+ const result = checkRateLimit(key, loginConfig);
64
+ expect(result.allowed).toBe(false);
65
+ });
66
+
67
+ it('should enforce registration rate limits (3/hour per IP)', () => {
68
+ const key = '192.168.1.1';
69
+
70
+ for (let i = 0; i < 3; i++) {
71
+ const result = checkRateLimit(key, registrationConfig);
72
+ expect(result.allowed).toBe(true);
73
+ }
74
+
75
+ // 4th attempt should fail
76
+ const result = checkRateLimit(key, registrationConfig);
77
+ expect(result.allowed).toBe(false);
78
+ });
79
+
80
+ it('should enforce password reset rate limits (2/hour)', () => {
81
+ const key = 'reset@example.com';
82
+
83
+ for (let i = 0; i < 2; i++) {
84
+ const result = checkRateLimit(key, passwordResetConfig);
85
+ expect(result.allowed).toBe(true);
86
+ }
87
+
88
+ // 3rd attempt should fail
89
+ const result = checkRateLimit(key, passwordResetConfig);
90
+ expect(result.allowed).toBe(false);
91
+ });
92
+
93
+ it('should track resetAt timestamp', () => {
94
+ const now = Date.now();
95
+ const result = checkRateLimit('user@example.com', loginConfig);
96
+
97
+ expect(result.resetAt).toBeGreaterThan(now);
98
+ expect(result.resetAt).toBeLessThanOrEqual(now + loginConfig.windowMs);
99
+ });
100
+
101
+ it('should maintain separate limits for different keys', () => {
102
+ const result1 = checkRateLimit('user1@example.com', loginConfig);
103
+ const result2 = checkRateLimit('user2@example.com', loginConfig);
104
+
105
+ expect(result1.allowed).toBe(true);
106
+ expect(result1.remaining).toBe(4);
107
+
108
+ expect(result2.allowed).toBe(true);
109
+ expect(result2.remaining).toBe(4);
110
+ });
111
+ });
112
+
113
+ describe('resetRateLimit', () => {
114
+ it('should reset rate limit for a key', () => {
115
+ const key = 'user@example.com';
116
+
117
+ // Hit rate limit
118
+ for (let i = 0; i < loginConfig.maxAttempts; i++) {
119
+ checkRateLimit(key, loginConfig);
120
+ }
121
+
122
+ const blockedResult = checkRateLimit(key, loginConfig);
123
+ expect(blockedResult.allowed).toBe(false);
124
+
125
+ // Reset the limit
126
+ resetRateLimit(key);
127
+
128
+ // Should be able to attempt again
129
+ const allowedResult = checkRateLimit(key, loginConfig);
130
+ expect(allowedResult.allowed).toBe(true);
131
+ });
132
+
133
+ it('should allow resetting before hitting limit', () => {
134
+ const key = 'user@example.com';
135
+
136
+ checkRateLimit(key, loginConfig);
137
+ checkRateLimit(key, loginConfig);
138
+
139
+ // Reset early
140
+ resetRateLimit(key);
141
+
142
+ // Status should be reset
143
+ const status = getRateLimitStatus(key, loginConfig);
144
+ expect(status.attempts).toBe(0);
145
+ expect(status.remaining).toBe(loginConfig.maxAttempts);
146
+ });
147
+ });
148
+
149
+ describe('getRateLimitStatus', () => {
150
+ it('should return correct status for a key', () => {
151
+ const key = 'user@example.com';
152
+
153
+ checkRateLimit(key, loginConfig);
154
+ checkRateLimit(key, loginConfig);
155
+
156
+ const status = getRateLimitStatus(key, loginConfig);
157
+
158
+ expect(status.attempts).toBe(2);
159
+ expect(status.remaining).toBe(3);
160
+ });
161
+
162
+ it('should return full remaining attempts for untouched key', () => {
163
+ const status = getRateLimitStatus('new@example.com', loginConfig);
164
+
165
+ expect(status.attempts).toBe(0);
166
+ expect(status.remaining).toBe(loginConfig.maxAttempts);
167
+ });
168
+
169
+ it('should provide resetAt timestamp', () => {
170
+ const key = 'user@example.com';
171
+ checkRateLimit(key, loginConfig);
172
+
173
+ const status = getRateLimitStatus(key, loginConfig);
174
+ expect(status.resetAt).toBeGreaterThan(Date.now());
175
+ });
176
+ });
177
+
178
+ describe('clearAllRateLimits', () => {
179
+ it('should clear all stored rate limits', () => {
180
+ const key1 = 'user1@example.com';
181
+ const key2 = 'user2@example.com';
182
+
183
+ checkRateLimit(key1, loginConfig);
184
+ checkRateLimit(key2, loginConfig);
185
+
186
+ clearAllRateLimits();
187
+
188
+ const status1 = getRateLimitStatus(key1, loginConfig);
189
+ const status2 = getRateLimitStatus(key2, loginConfig);
190
+
191
+ expect(status1.attempts).toBe(0);
192
+ expect(status2.attempts).toBe(0);
193
+ });
194
+ });
195
+
196
+ describe('extractClientIp', () => {
197
+ it('should extract IP from x-forwarded-for header', () => {
198
+ const headers: Record<string, string | string[] | undefined> = {
199
+ 'x-forwarded-for': '192.168.1.1, 10.0.0.1'
200
+ };
201
+
202
+ const ip = extractClientIp(headers);
203
+ expect(ip).toBe('192.168.1.1');
204
+ });
205
+
206
+ it('should extract IP from x-real-ip header', () => {
207
+ const headers: Record<string, string | string[] | undefined> = {
208
+ 'x-real-ip': '192.168.1.2'
209
+ };
210
+
211
+ const ip = extractClientIp(headers);
212
+ expect(ip).toBe('192.168.1.2');
213
+ });
214
+
215
+ it('should return unknown if no IP headers present', () => {
216
+ const headers: Record<string, string | string[] | undefined> = {};
217
+
218
+ const ip = extractClientIp(headers);
219
+ expect(ip).toBe('unknown');
220
+ });
221
+
222
+ it('should handle whitespace in x-forwarded-for', () => {
223
+ const headers: Record<string, string | string[] | undefined> = {
224
+ 'x-forwarded-for': ' 192.168.1.3 , 10.0.0.2'
225
+ };
226
+
227
+ const ip = extractClientIp(headers);
228
+ expect(ip).toBe('192.168.1.3');
229
+ });
230
+ });
231
+
232
+ describe('Rate limit expiration', () => {
233
+ it('should clean up expired entries', () => {
234
+ const key = 'user@example.com';
235
+ const shortConfig: RateLimitConfig = {
236
+ maxAttempts: 2,
237
+ windowMs: 100 // 100ms window
238
+ };
239
+
240
+ // Make attempts
241
+ checkRateLimit(key, shortConfig);
242
+ checkRateLimit(key, shortConfig);
243
+
244
+ // Should be blocked
245
+ let result = checkRateLimit(key, shortConfig);
246
+ expect(result.allowed).toBe(false);
247
+
248
+ // Wait for window to expire
249
+ return new Promise((resolve) => {
250
+ setTimeout(() => {
251
+ // After expiration, should be allowed again
252
+ result = checkRateLimit(key, shortConfig);
253
+ expect(result.allowed).toBe(true);
254
+ resolve(undefined);
255
+ }, 150);
256
+ });
257
+ });
258
+ });
259
+
260
+ describe('Integration scenarios', () => {
261
+ it('should handle login flow with failed then successful attempt', () => {
262
+ const key = 'test@example.com';
263
+
264
+ // Simulate failed login attempts
265
+ for (let i = 0; i < 5; i++) {
266
+ const result = checkRateLimit(key, loginConfig);
267
+ expect(result.allowed).toBe(true);
268
+ }
269
+
270
+ // Should be rate limited
271
+ let result = checkRateLimit(key, loginConfig);
272
+ expect(result.allowed).toBe(false);
273
+
274
+ // After successful login (outside rate limit), reset
275
+ resetRateLimit(key);
276
+
277
+ // Should be able to login again
278
+ result = checkRateLimit(key, loginConfig);
279
+ expect(result.allowed).toBe(true);
280
+ });
281
+
282
+ it('should handle multiple endpoints with different limits', () => {
283
+ const email = 'user@example.com';
284
+ const ip = '192.168.1.1';
285
+
286
+ // Login limit - use operation-specific key
287
+ const loginKey = `login:${email}`;
288
+ checkRateLimit(loginKey, loginConfig);
289
+ const loginStatus = getRateLimitStatus(loginKey, loginConfig);
290
+ expect(loginStatus.remaining).toBe(4);
291
+
292
+ // Registration limit (IP based)
293
+ const regKey = `register:${ip}`;
294
+ checkRateLimit(regKey, registrationConfig);
295
+ const regStatus = getRateLimitStatus(regKey, registrationConfig);
296
+ expect(regStatus.remaining).toBe(2);
297
+
298
+ // Password reset limit (email based)
299
+ const resetKey = `reset:${email}`;
300
+ checkRateLimit(resetKey, passwordResetConfig);
301
+ const resetStatus = getRateLimitStatus(resetKey, passwordResetConfig);
302
+ expect(resetStatus.remaining).toBe(1);
303
+
304
+ // Verify they are independent
305
+ expect(loginStatus.remaining).not.toBe(regStatus.remaining);
306
+ });
307
+ });
308
+ });
@@ -0,0 +1,118 @@
1
+ interface RateLimitEntry {
2
+ key: string;
3
+ count: number;
4
+ resetAt: number;
5
+ }
6
+
7
+ export interface RateLimitConfig {
8
+ maxAttempts: number;
9
+ windowMs: number; // Time window in milliseconds
10
+ }
11
+
12
+ const RATE_LIMITS: Record<string, RateLimitEntry[]> = {};
13
+
14
+ /**
15
+ * Check if a request should be rate limited
16
+ * @param key - Unique identifier for the rate limit (e.g., email, IP address)
17
+ * @param config - Rate limit configuration
18
+ * @returns Object with remaining attempts and resetTime
19
+ */
20
+ export function checkRateLimit(
21
+ key: string,
22
+ config: RateLimitConfig
23
+ ): { allowed: boolean; remaining: number; resetAt: number } {
24
+ const now = Date.now();
25
+
26
+ // Initialize rate limit storage for this key if not exists
27
+ if (!RATE_LIMITS[key]) {
28
+ RATE_LIMITS[key] = [];
29
+ }
30
+
31
+ // Remove expired entries
32
+ RATE_LIMITS[key] = RATE_LIMITS[key].filter((entry) => entry.resetAt > now);
33
+
34
+ // Count current attempts
35
+ const currentCount = RATE_LIMITS[key].length;
36
+
37
+ if (currentCount >= config.maxAttempts) {
38
+ // Get the earliest reset time
39
+ const resetAt = Math.min(...RATE_LIMITS[key].map((e) => e.resetAt));
40
+ return {
41
+ allowed: false,
42
+ remaining: 0,
43
+ resetAt
44
+ };
45
+ }
46
+
47
+ // Add new attempt
48
+ const resetAt = now + config.windowMs;
49
+ RATE_LIMITS[key].push({
50
+ key,
51
+ count: 1,
52
+ resetAt
53
+ });
54
+
55
+ return {
56
+ allowed: true,
57
+ remaining: config.maxAttempts - currentCount - 1,
58
+ resetAt
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Reset rate limit for a key (e.g., after successful login)
64
+ */
65
+ export function resetRateLimit(key: string): void {
66
+ if (RATE_LIMITS[key]) {
67
+ delete RATE_LIMITS[key];
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Get current rate limit status for a key
73
+ */
74
+ export function getRateLimitStatus(key: string, config: RateLimitConfig) {
75
+ const now = Date.now();
76
+
77
+ if (!RATE_LIMITS[key]) {
78
+ return {
79
+ attempts: 0,
80
+ remaining: config.maxAttempts,
81
+ resetAt: now + config.windowMs
82
+ };
83
+ }
84
+
85
+ // Remove expired entries
86
+ RATE_LIMITS[key] = RATE_LIMITS[key].filter((entry) => entry.resetAt > now);
87
+
88
+ const currentCount = RATE_LIMITS[key].length;
89
+ const resetAt = RATE_LIMITS[key].length > 0 ? Math.max(...RATE_LIMITS[key].map((e) => e.resetAt)) : now + config.windowMs;
90
+
91
+ return {
92
+ attempts: currentCount,
93
+ remaining: Math.max(0, config.maxAttempts - currentCount),
94
+ resetAt
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Clear all rate limits (useful for testing)
100
+ */
101
+ export function clearAllRateLimits(): void {
102
+ for (const key in RATE_LIMITS) {
103
+ delete RATE_LIMITS[key];
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Extract IP address from headers
109
+ */
110
+ export function extractClientIp(
111
+ headers: Record<string, string | string[] | undefined>
112
+ ): string {
113
+ const forwarded = headers['x-forwarded-for'];
114
+ if (typeof forwarded === 'string') {
115
+ return forwarded.split(',')[0].trim();
116
+ }
117
+ return headers['x-real-ip'] as string || 'unknown';
118
+ }