servcraft 0.1.0 → 0.1.3

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 (217) hide show
  1. package/.claude/settings.local.json +30 -0
  2. package/.github/CODEOWNERS +18 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +46 -0
  4. package/.github/dependabot.yml +59 -0
  5. package/.github/workflows/ci.yml +188 -0
  6. package/.github/workflows/release.yml +195 -0
  7. package/AUDIT.md +602 -0
  8. package/LICENSE +21 -0
  9. package/README.md +1102 -1
  10. package/dist/cli/index.cjs +2026 -2168
  11. package/dist/cli/index.cjs.map +1 -1
  12. package/dist/cli/index.js +2026 -2168
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/index.cjs +595 -616
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +114 -52
  17. package/dist/index.d.ts +114 -52
  18. package/dist/index.js +595 -616
  19. package/dist/index.js.map +1 -1
  20. package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
  21. package/docs/DATABASE_MULTI_ORM.md +399 -0
  22. package/docs/PHASE1_BREAKDOWN.md +346 -0
  23. package/docs/PROGRESS.md +550 -0
  24. package/docs/modules/ANALYTICS.md +226 -0
  25. package/docs/modules/API-VERSIONING.md +252 -0
  26. package/docs/modules/AUDIT.md +192 -0
  27. package/docs/modules/AUTH.md +431 -0
  28. package/docs/modules/CACHE.md +346 -0
  29. package/docs/modules/EMAIL.md +254 -0
  30. package/docs/modules/FEATURE-FLAG.md +291 -0
  31. package/docs/modules/I18N.md +294 -0
  32. package/docs/modules/MEDIA-PROCESSING.md +281 -0
  33. package/docs/modules/MFA.md +266 -0
  34. package/docs/modules/NOTIFICATION.md +311 -0
  35. package/docs/modules/OAUTH.md +237 -0
  36. package/docs/modules/PAYMENT.md +804 -0
  37. package/docs/modules/QUEUE.md +540 -0
  38. package/docs/modules/RATE-LIMIT.md +339 -0
  39. package/docs/modules/SEARCH.md +288 -0
  40. package/docs/modules/SECURITY.md +327 -0
  41. package/docs/modules/SESSION.md +382 -0
  42. package/docs/modules/SWAGGER.md +305 -0
  43. package/docs/modules/UPLOAD.md +296 -0
  44. package/docs/modules/USER.md +505 -0
  45. package/docs/modules/VALIDATION.md +294 -0
  46. package/docs/modules/WEBHOOK.md +270 -0
  47. package/docs/modules/WEBSOCKET.md +691 -0
  48. package/package.json +53 -38
  49. package/prisma/schema.prisma +395 -1
  50. package/src/cli/commands/add-module.ts +520 -87
  51. package/src/cli/commands/db.ts +3 -4
  52. package/src/cli/commands/docs.ts +256 -6
  53. package/src/cli/commands/generate.ts +12 -19
  54. package/src/cli/commands/init.ts +384 -214
  55. package/src/cli/index.ts +0 -4
  56. package/src/cli/templates/repository.ts +6 -1
  57. package/src/cli/templates/routes.ts +6 -21
  58. package/src/cli/utils/docs-generator.ts +6 -7
  59. package/src/cli/utils/env-manager.ts +717 -0
  60. package/src/cli/utils/field-parser.ts +16 -7
  61. package/src/cli/utils/interactive-prompt.ts +223 -0
  62. package/src/cli/utils/template-manager.ts +346 -0
  63. package/src/config/database.config.ts +183 -0
  64. package/src/config/env.ts +0 -10
  65. package/src/config/index.ts +0 -14
  66. package/src/core/server.ts +1 -1
  67. package/src/database/adapters/mongoose.adapter.ts +132 -0
  68. package/src/database/adapters/prisma.adapter.ts +118 -0
  69. package/src/database/connection.ts +190 -0
  70. package/src/database/interfaces/database.interface.ts +85 -0
  71. package/src/database/interfaces/index.ts +7 -0
  72. package/src/database/interfaces/repository.interface.ts +129 -0
  73. package/src/database/models/mongoose/index.ts +7 -0
  74. package/src/database/models/mongoose/payment.schema.ts +347 -0
  75. package/src/database/models/mongoose/user.schema.ts +154 -0
  76. package/src/database/prisma.ts +1 -4
  77. package/src/database/redis.ts +101 -0
  78. package/src/database/repositories/mongoose/index.ts +7 -0
  79. package/src/database/repositories/mongoose/payment.repository.ts +380 -0
  80. package/src/database/repositories/mongoose/user.repository.ts +255 -0
  81. package/src/database/seed.ts +6 -1
  82. package/src/index.ts +9 -20
  83. package/src/middleware/security.ts +2 -6
  84. package/src/modules/analytics/analytics.routes.ts +80 -0
  85. package/src/modules/analytics/analytics.service.ts +364 -0
  86. package/src/modules/analytics/index.ts +18 -0
  87. package/src/modules/analytics/types.ts +180 -0
  88. package/src/modules/api-versioning/index.ts +15 -0
  89. package/src/modules/api-versioning/types.ts +86 -0
  90. package/src/modules/api-versioning/versioning.middleware.ts +120 -0
  91. package/src/modules/api-versioning/versioning.routes.ts +54 -0
  92. package/src/modules/api-versioning/versioning.service.ts +189 -0
  93. package/src/modules/audit/audit.repository.ts +206 -0
  94. package/src/modules/audit/audit.service.ts +27 -59
  95. package/src/modules/auth/auth.controller.ts +2 -2
  96. package/src/modules/auth/auth.middleware.ts +3 -9
  97. package/src/modules/auth/auth.routes.ts +10 -107
  98. package/src/modules/auth/auth.service.ts +126 -23
  99. package/src/modules/auth/index.ts +3 -4
  100. package/src/modules/cache/cache.service.ts +367 -0
  101. package/src/modules/cache/index.ts +10 -0
  102. package/src/modules/cache/types.ts +44 -0
  103. package/src/modules/email/email.service.ts +3 -10
  104. package/src/modules/email/templates.ts +2 -8
  105. package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
  106. package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
  107. package/src/modules/feature-flag/feature-flag.service.ts +566 -0
  108. package/src/modules/feature-flag/index.ts +20 -0
  109. package/src/modules/feature-flag/types.ts +192 -0
  110. package/src/modules/i18n/i18n.middleware.ts +186 -0
  111. package/src/modules/i18n/i18n.routes.ts +191 -0
  112. package/src/modules/i18n/i18n.service.ts +456 -0
  113. package/src/modules/i18n/index.ts +18 -0
  114. package/src/modules/i18n/types.ts +118 -0
  115. package/src/modules/media-processing/index.ts +17 -0
  116. package/src/modules/media-processing/media-processing.routes.ts +111 -0
  117. package/src/modules/media-processing/media-processing.service.ts +245 -0
  118. package/src/modules/media-processing/types.ts +156 -0
  119. package/src/modules/mfa/index.ts +20 -0
  120. package/src/modules/mfa/mfa.repository.ts +206 -0
  121. package/src/modules/mfa/mfa.routes.ts +595 -0
  122. package/src/modules/mfa/mfa.service.ts +572 -0
  123. package/src/modules/mfa/totp.ts +150 -0
  124. package/src/modules/mfa/types.ts +57 -0
  125. package/src/modules/notification/index.ts +20 -0
  126. package/src/modules/notification/notification.repository.ts +356 -0
  127. package/src/modules/notification/notification.service.ts +483 -0
  128. package/src/modules/notification/types.ts +119 -0
  129. package/src/modules/oauth/index.ts +20 -0
  130. package/src/modules/oauth/oauth.repository.ts +219 -0
  131. package/src/modules/oauth/oauth.routes.ts +446 -0
  132. package/src/modules/oauth/oauth.service.ts +293 -0
  133. package/src/modules/oauth/providers/apple.provider.ts +250 -0
  134. package/src/modules/oauth/providers/facebook.provider.ts +181 -0
  135. package/src/modules/oauth/providers/github.provider.ts +248 -0
  136. package/src/modules/oauth/providers/google.provider.ts +189 -0
  137. package/src/modules/oauth/providers/twitter.provider.ts +214 -0
  138. package/src/modules/oauth/types.ts +94 -0
  139. package/src/modules/payment/index.ts +19 -0
  140. package/src/modules/payment/payment.repository.ts +733 -0
  141. package/src/modules/payment/payment.routes.ts +390 -0
  142. package/src/modules/payment/payment.service.ts +354 -0
  143. package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
  144. package/src/modules/payment/providers/paypal.provider.ts +190 -0
  145. package/src/modules/payment/providers/stripe.provider.ts +215 -0
  146. package/src/modules/payment/types.ts +140 -0
  147. package/src/modules/queue/cron.ts +438 -0
  148. package/src/modules/queue/index.ts +87 -0
  149. package/src/modules/queue/queue.routes.ts +600 -0
  150. package/src/modules/queue/queue.service.ts +842 -0
  151. package/src/modules/queue/types.ts +222 -0
  152. package/src/modules/queue/workers.ts +366 -0
  153. package/src/modules/rate-limit/index.ts +59 -0
  154. package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
  155. package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
  156. package/src/modules/rate-limit/rate-limit.service.ts +348 -0
  157. package/src/modules/rate-limit/stores/memory.store.ts +165 -0
  158. package/src/modules/rate-limit/stores/redis.store.ts +322 -0
  159. package/src/modules/rate-limit/types.ts +153 -0
  160. package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
  161. package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
  162. package/src/modules/search/adapters/memory.adapter.ts +278 -0
  163. package/src/modules/search/index.ts +21 -0
  164. package/src/modules/search/search.service.ts +234 -0
  165. package/src/modules/search/types.ts +214 -0
  166. package/src/modules/security/index.ts +40 -0
  167. package/src/modules/security/sanitize.ts +223 -0
  168. package/src/modules/security/security-audit.service.ts +388 -0
  169. package/src/modules/security/security.middleware.ts +398 -0
  170. package/src/modules/session/index.ts +3 -0
  171. package/src/modules/session/session.repository.ts +159 -0
  172. package/src/modules/session/session.service.ts +340 -0
  173. package/src/modules/session/types.ts +38 -0
  174. package/src/modules/swagger/index.ts +7 -1
  175. package/src/modules/swagger/schema-builder.ts +16 -4
  176. package/src/modules/swagger/swagger.service.ts +9 -10
  177. package/src/modules/swagger/types.ts +0 -2
  178. package/src/modules/upload/index.ts +14 -0
  179. package/src/modules/upload/types.ts +83 -0
  180. package/src/modules/upload/upload.repository.ts +199 -0
  181. package/src/modules/upload/upload.routes.ts +311 -0
  182. package/src/modules/upload/upload.service.ts +448 -0
  183. package/src/modules/user/index.ts +3 -3
  184. package/src/modules/user/user.controller.ts +15 -9
  185. package/src/modules/user/user.repository.ts +237 -113
  186. package/src/modules/user/user.routes.ts +39 -164
  187. package/src/modules/user/user.service.ts +4 -3
  188. package/src/modules/validation/validator.ts +12 -17
  189. package/src/modules/webhook/index.ts +91 -0
  190. package/src/modules/webhook/retry.ts +196 -0
  191. package/src/modules/webhook/signature.ts +135 -0
  192. package/src/modules/webhook/types.ts +181 -0
  193. package/src/modules/webhook/webhook.repository.ts +358 -0
  194. package/src/modules/webhook/webhook.routes.ts +442 -0
  195. package/src/modules/webhook/webhook.service.ts +457 -0
  196. package/src/modules/websocket/features.ts +504 -0
  197. package/src/modules/websocket/index.ts +106 -0
  198. package/src/modules/websocket/middlewares.ts +298 -0
  199. package/src/modules/websocket/types.ts +181 -0
  200. package/src/modules/websocket/websocket.service.ts +692 -0
  201. package/src/utils/errors.ts +7 -0
  202. package/src/utils/pagination.ts +4 -1
  203. package/tests/helpers/db-check.ts +79 -0
  204. package/tests/integration/auth-redis.test.ts +94 -0
  205. package/tests/integration/cache-redis.test.ts +387 -0
  206. package/tests/integration/mongoose-repositories.test.ts +410 -0
  207. package/tests/integration/payment-prisma.test.ts +637 -0
  208. package/tests/integration/queue-bullmq.test.ts +417 -0
  209. package/tests/integration/user-prisma.test.ts +441 -0
  210. package/tests/integration/websocket-socketio.test.ts +552 -0
  211. package/tests/setup.ts +11 -9
  212. package/vitest.config.ts +3 -8
  213. package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
  214. package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
  215. package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
  216. package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
  217. package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +0 -5
@@ -0,0 +1,322 @@
1
+ /**
2
+ * Redis Rate Limit Store
3
+ * Supports distributed rate limiting across multiple instances
4
+ *
5
+ * Features:
6
+ * - Atomic increment operations via Lua scripts
7
+ * - Sliding window algorithm support
8
+ * - Token bucket algorithm support
9
+ * - Automatic TTL-based expiration
10
+ */
11
+ import type { Redis } from 'ioredis';
12
+ import type { RateLimitStore, RateLimitEntry } from '../types.js';
13
+ import { getRedis } from '../../../database/redis.js';
14
+ import { logger } from '../../../core/logger.js';
15
+
16
+ const RATE_LIMIT_PREFIX = 'ratelimit:';
17
+
18
+ export class RedisStore implements RateLimitStore {
19
+ private redis: Redis;
20
+ private prefix: string;
21
+
22
+ constructor(redis?: Redis, prefix = RATE_LIMIT_PREFIX) {
23
+ this.redis = redis || getRedis();
24
+ this.prefix = prefix;
25
+ }
26
+
27
+ private buildKey(key: string): string {
28
+ return `${this.prefix}${key}`;
29
+ }
30
+
31
+ async get(key: string): Promise<RateLimitEntry | null> {
32
+ try {
33
+ const data = await this.redis.get(this.buildKey(key));
34
+ if (!data) return null;
35
+
36
+ const entry = JSON.parse(data) as RateLimitEntry;
37
+
38
+ // Check if expired
39
+ if (entry.resetAt && entry.resetAt < Date.now()) {
40
+ await this.reset(key);
41
+ return null;
42
+ }
43
+
44
+ return entry;
45
+ } catch (error) {
46
+ logger.error({ err: error, key }, 'Error getting rate limit entry');
47
+ return null;
48
+ }
49
+ }
50
+
51
+ async set(key: string, entry: RateLimitEntry, ttlMs?: number): Promise<void> {
52
+ try {
53
+ const fullKey = this.buildKey(key);
54
+ const resetAt = entry.resetAt ?? Date.now() + (ttlMs ?? 60000);
55
+ const ttlSeconds = Math.max(1, Math.ceil((resetAt - Date.now()) / 1000));
56
+
57
+ await this.redis.setex(fullKey, ttlSeconds, JSON.stringify(entry));
58
+ } catch (error) {
59
+ logger.error({ err: error, key }, 'Error setting rate limit entry');
60
+ }
61
+ }
62
+
63
+ async increment(key: string, windowMs: number): Promise<RateLimitEntry> {
64
+ const fullKey = this.buildKey(key);
65
+ const now = Date.now();
66
+ const ttlSeconds = Math.ceil(windowMs / 1000);
67
+
68
+ try {
69
+ // Use Lua script for atomic increment
70
+ const script = `
71
+ local key = KEYS[1]
72
+ local now = tonumber(ARGV[1])
73
+ local windowMs = tonumber(ARGV[2])
74
+ local ttl = tonumber(ARGV[3])
75
+
76
+ local data = redis.call('GET', key)
77
+ local entry
78
+
79
+ if data then
80
+ entry = cjson.decode(data)
81
+ if now - entry.startTime >= windowMs then
82
+ entry = {
83
+ count = 1,
84
+ startTime = now,
85
+ firstRequest = now,
86
+ lastRequest = now,
87
+ resetAt = now + windowMs
88
+ }
89
+ else
90
+ entry.count = entry.count + 1
91
+ entry.lastRequest = now
92
+ end
93
+ else
94
+ entry = {
95
+ count = 1,
96
+ startTime = now,
97
+ firstRequest = now,
98
+ lastRequest = now,
99
+ resetAt = now + windowMs
100
+ }
101
+ end
102
+
103
+ redis.call('SETEX', key, ttl, cjson.encode(entry))
104
+ return cjson.encode(entry)
105
+ `;
106
+
107
+ const result = await this.redis.eval(
108
+ script,
109
+ 1,
110
+ fullKey,
111
+ now.toString(),
112
+ windowMs.toString(),
113
+ ttlSeconds.toString()
114
+ );
115
+
116
+ return JSON.parse(result as string) as RateLimitEntry;
117
+ } catch (error) {
118
+ // Fallback to non-atomic increment
119
+ logger.warn({ err: error, key }, 'Lua script failed, using fallback increment');
120
+ return this.fallbackIncrement(key, windowMs);
121
+ }
122
+ }
123
+
124
+ private async fallbackIncrement(key: string, windowMs: number): Promise<RateLimitEntry> {
125
+ const now = Date.now();
126
+ const current = await this.get(key);
127
+
128
+ if (!current) {
129
+ const entry: RateLimitEntry = {
130
+ count: 1,
131
+ startTime: now,
132
+ resetAt: now + windowMs,
133
+ firstRequest: now,
134
+ lastRequest: now,
135
+ };
136
+ await this.set(key, entry, windowMs);
137
+ return entry;
138
+ }
139
+
140
+ // Check if window expired
141
+ if (now - current.startTime >= windowMs) {
142
+ const entry: RateLimitEntry = {
143
+ count: 1,
144
+ startTime: now,
145
+ resetAt: now + windowMs,
146
+ firstRequest: now,
147
+ lastRequest: now,
148
+ };
149
+ await this.set(key, entry, windowMs);
150
+ return entry;
151
+ }
152
+
153
+ current.count++;
154
+ current.lastRequest = now;
155
+ await this.set(key, current);
156
+
157
+ return current;
158
+ }
159
+
160
+ async reset(key: string): Promise<void> {
161
+ try {
162
+ await this.redis.del(this.buildKey(key));
163
+ } catch (error) {
164
+ logger.error({ err: error, key }, 'Error resetting rate limit entry');
165
+ }
166
+ }
167
+
168
+ async clear(): Promise<void> {
169
+ try {
170
+ const pattern = `${this.prefix}*`;
171
+ const keys = await this.redis.keys(pattern);
172
+ if (keys.length > 0) {
173
+ await this.redis.del(...keys);
174
+ }
175
+ } catch (error) {
176
+ logger.error({ err: error }, 'Error clearing rate limit entries');
177
+ }
178
+ }
179
+
180
+ async cleanup(): Promise<void> {
181
+ // Redis handles TTL-based expiration automatically
182
+ }
183
+
184
+ /**
185
+ * Sliding window increment using sorted sets
186
+ * More accurate than fixed window, prevents burst at window edges
187
+ */
188
+ async slidingWindowIncrement(key: string, windowMs: number): Promise<RateLimitEntry> {
189
+ const fullKey = `${this.prefix}sw:${key}`;
190
+ const now = Date.now();
191
+ const windowStart = now - windowMs;
192
+ const ttlSeconds = Math.ceil(windowMs / 1000) + 1;
193
+
194
+ try {
195
+ const script = `
196
+ local key = KEYS[1]
197
+ local now = tonumber(ARGV[1])
198
+ local windowStart = tonumber(ARGV[2])
199
+ local ttl = tonumber(ARGV[3])
200
+
201
+ redis.call('ZREMRANGEBYSCORE', key, '-inf', windowStart)
202
+ redis.call('ZADD', key, now, now .. '-' .. math.random(1000000))
203
+
204
+ local count = redis.call('ZCARD', key)
205
+ local first = redis.call('ZRANGE', key, 0, 0, 'WITHSCORES')
206
+ local firstTs = first[2] and tonumber(first[2]) or now
207
+
208
+ redis.call('EXPIRE', key, ttl)
209
+
210
+ return {count, firstTs, now}
211
+ `;
212
+
213
+ const result = (await this.redis.eval(
214
+ script,
215
+ 1,
216
+ fullKey,
217
+ now.toString(),
218
+ windowStart.toString(),
219
+ ttlSeconds.toString()
220
+ )) as [number, number, number];
221
+
222
+ return {
223
+ count: result[0],
224
+ startTime: result[1],
225
+ firstRequest: result[1],
226
+ lastRequest: result[2],
227
+ resetAt: result[1] + windowMs,
228
+ };
229
+ } catch (error) {
230
+ logger.warn({ err: error, key }, 'Sliding window failed, using fallback');
231
+ return this.increment(key, windowMs);
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Token bucket implementation
237
+ * Allows controlled bursts while maintaining average rate
238
+ */
239
+ async tokenBucketIncrement(
240
+ key: string,
241
+ maxTokens: number,
242
+ refillRate: number,
243
+ refillIntervalMs: number
244
+ ): Promise<RateLimitEntry> {
245
+ const fullKey = `${this.prefix}tb:${key}`;
246
+ const now = Date.now();
247
+ const ttlSeconds = 3600;
248
+
249
+ try {
250
+ const script = `
251
+ local key = KEYS[1]
252
+ local now = tonumber(ARGV[1])
253
+ local maxTokens = tonumber(ARGV[2])
254
+ local refillRate = tonumber(ARGV[3])
255
+ local refillIntervalMs = tonumber(ARGV[4])
256
+ local ttl = tonumber(ARGV[5])
257
+
258
+ local data = redis.call('GET', key)
259
+ local entry
260
+
261
+ if data then
262
+ entry = cjson.decode(data)
263
+
264
+ local timePassed = now - (entry.lastRefill or now)
265
+ local tokensToAdd = math.floor(timePassed / refillIntervalMs) * refillRate
266
+
267
+ entry.tokens = math.min(maxTokens, (entry.tokens or 0) + tokensToAdd)
268
+ entry.lastRefill = now
269
+
270
+ if entry.tokens > 0 then
271
+ entry.tokens = entry.tokens - 1
272
+ entry.count = (entry.count or 0) + 1
273
+ end
274
+ else
275
+ entry = {
276
+ count = 1,
277
+ startTime = now,
278
+ tokens = maxTokens - 1,
279
+ lastRefill = now,
280
+ firstRequest = now,
281
+ lastRequest = now
282
+ }
283
+ end
284
+
285
+ entry.lastRequest = now
286
+
287
+ redis.call('SETEX', key, ttl, cjson.encode(entry))
288
+ return cjson.encode(entry)
289
+ `;
290
+
291
+ const result = await this.redis.eval(
292
+ script,
293
+ 1,
294
+ fullKey,
295
+ now.toString(),
296
+ maxTokens.toString(),
297
+ refillRate.toString(),
298
+ refillIntervalMs.toString(),
299
+ ttlSeconds.toString()
300
+ );
301
+
302
+ return JSON.parse(result as string) as RateLimitEntry;
303
+ } catch (error) {
304
+ logger.warn({ err: error, key }, 'Token bucket failed, using fallback');
305
+ return this.increment(key, refillIntervalMs);
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Get store statistics
311
+ */
312
+ async getStats(): Promise<{ keyCount: number }> {
313
+ try {
314
+ const pattern = `${this.prefix}*`;
315
+ const keys = await this.redis.keys(pattern);
316
+ return { keyCount: keys.length };
317
+ } catch (error) {
318
+ logger.error({ err: error }, 'Error getting rate limit stats');
319
+ return { keyCount: 0 };
320
+ }
321
+ }
322
+ }
@@ -0,0 +1,153 @@
1
+ export type RateLimitAlgorithm = 'fixed-window' | 'sliding-window' | 'token-bucket';
2
+
3
+ export interface RateLimitConfig {
4
+ /** Default requests per window */
5
+ max: number;
6
+ /** Time window in milliseconds */
7
+ windowMs: number;
8
+ /** Algorithm to use */
9
+ algorithm?: RateLimitAlgorithm;
10
+ /** Skip rate limiting for these IPs */
11
+ whitelist?: string[];
12
+ /** Always block these IPs */
13
+ blacklist?: string[];
14
+ /** Custom key generator */
15
+ keyGenerator?: (request: RateLimitRequest) => string;
16
+ /** Skip rate limiting based on request */
17
+ skip?: (request: RateLimitRequest) => boolean | Promise<boolean>;
18
+ /** Handler when rate limit is exceeded */
19
+ onLimitReached?: (request: RateLimitRequest, info: RateLimitInfo) => void | Promise<void>;
20
+ /** Store for rate limit data */
21
+ store?: RateLimitStore;
22
+ /** Include headers in response */
23
+ headers?: boolean;
24
+ /** Custom message when rate limited */
25
+ message?: string | ((info: RateLimitInfo) => string);
26
+ /** Status code when rate limited (default: 429) */
27
+ statusCode?: number;
28
+ /** Custom limits per route/IP */
29
+ customLimits?: Record<string, { max: number; windowMs: number }>;
30
+ }
31
+
32
+ export interface RateLimitRule {
33
+ /** Rule identifier */
34
+ id: string;
35
+ /** Route pattern (glob or regex) */
36
+ pattern: string;
37
+ /** HTTP methods (empty = all) */
38
+ methods?: string[];
39
+ /** Max requests */
40
+ max: number;
41
+ /** Time window in ms */
42
+ windowMs: number;
43
+ /** Apply to specific roles only */
44
+ roles?: string[];
45
+ /** Override key generator for this rule */
46
+ keyGenerator?: (request: RateLimitRequest) => string;
47
+ /** Priority (higher = checked first) */
48
+ priority?: number;
49
+ /** Is rule enabled */
50
+ enabled?: boolean;
51
+ }
52
+
53
+ export interface RateLimitRequest {
54
+ ip: string;
55
+ method: string;
56
+ url: string;
57
+ path: string;
58
+ userId?: string;
59
+ userRole?: string;
60
+ headers: Record<string, string | string[] | undefined>;
61
+ }
62
+
63
+ export interface RateLimitInfo {
64
+ /** Total requests allowed */
65
+ limit: number;
66
+ /** Remaining requests in current window */
67
+ remaining: number;
68
+ /** Time when the rate limit resets (Unix timestamp) */
69
+ resetAt?: number;
70
+ /** Time when the rate limit resets (Unix timestamp) - alias */
71
+ resetTime?: number;
72
+ /** Milliseconds until reset */
73
+ retryAfter?: number;
74
+ /** Whether limit is exceeded */
75
+ exceeded?: boolean;
76
+ /** Current request count */
77
+ current?: number;
78
+ /** Request count */
79
+ count?: number;
80
+ /** First request time */
81
+ firstRequest?: number;
82
+ /** Last request time */
83
+ lastRequest?: number;
84
+ }
85
+
86
+ export interface RateLimitEntry {
87
+ /** Request count */
88
+ count: number;
89
+ /** Window start time */
90
+ startTime: number;
91
+ /** For sliding window: request timestamps */
92
+ timestamps?: number[];
93
+ /** For token bucket: available tokens */
94
+ tokens?: number;
95
+ /** Last refill time for token bucket */
96
+ lastRefill?: number;
97
+ /** Reset time */
98
+ resetAt?: number;
99
+ /** Last request time */
100
+ lastRequest?: number;
101
+ /** First request time */
102
+ firstRequest?: number;
103
+ }
104
+
105
+ export interface RateLimitResult {
106
+ /** Whether the request is allowed */
107
+ allowed: boolean;
108
+ /** Total requests allowed */
109
+ limit: number;
110
+ /** Remaining requests in current window */
111
+ remaining: number;
112
+ /** Time when the rate limit resets (Unix timestamp) */
113
+ resetAt: number;
114
+ /** Seconds until reset (optional, only when rate limited) */
115
+ retryAfter?: number;
116
+ }
117
+
118
+ export interface RateLimitStore {
119
+ /** Get rate limit entry */
120
+ get(key: string): Promise<RateLimitEntry | null>;
121
+ /** Set rate limit entry */
122
+ set(key: string, entry: RateLimitEntry, ttlMs?: number): Promise<void>;
123
+ /** Increment counter and return new value */
124
+ increment(key: string, windowMs: number): Promise<RateLimitEntry>;
125
+ /** Reset a key */
126
+ reset(key: string): Promise<void>;
127
+ /** Clear all entries */
128
+ clear(): Promise<void>;
129
+ /** Cleanup expired entries (optional) */
130
+ cleanup?(): Promise<void>;
131
+ }
132
+
133
+ export interface BlacklistEntry {
134
+ ip: string;
135
+ reason?: string;
136
+ expiresAt?: Date;
137
+ createdAt: Date;
138
+ createdBy?: string;
139
+ }
140
+
141
+ export interface WhitelistEntry {
142
+ ip: string;
143
+ reason?: string;
144
+ createdAt: Date;
145
+ createdBy?: string;
146
+ }
147
+
148
+ export interface RateLimitStats {
149
+ totalRequests: number;
150
+ blockedRequests: number;
151
+ topIps: Array<{ ip: string; count: number }>;
152
+ topEndpoints: Array<{ endpoint: string; count: number }>;
153
+ }