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.
- package/.claude/settings.local.json +30 -0
- package/.github/CODEOWNERS +18 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +46 -0
- package/.github/dependabot.yml +59 -0
- package/.github/workflows/ci.yml +188 -0
- package/.github/workflows/release.yml +195 -0
- package/AUDIT.md +602 -0
- package/LICENSE +21 -0
- package/README.md +1102 -1
- package/dist/cli/index.cjs +2026 -2168
- package/dist/cli/index.cjs.map +1 -1
- package/dist/cli/index.js +2026 -2168
- package/dist/cli/index.js.map +1 -1
- package/dist/index.cjs +595 -616
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +114 -52
- package/dist/index.d.ts +114 -52
- package/dist/index.js +595 -616
- package/dist/index.js.map +1 -1
- package/docs/CLI-001_MULTI_DB_PLAN.md +546 -0
- package/docs/DATABASE_MULTI_ORM.md +399 -0
- package/docs/PHASE1_BREAKDOWN.md +346 -0
- package/docs/PROGRESS.md +550 -0
- package/docs/modules/ANALYTICS.md +226 -0
- package/docs/modules/API-VERSIONING.md +252 -0
- package/docs/modules/AUDIT.md +192 -0
- package/docs/modules/AUTH.md +431 -0
- package/docs/modules/CACHE.md +346 -0
- package/docs/modules/EMAIL.md +254 -0
- package/docs/modules/FEATURE-FLAG.md +291 -0
- package/docs/modules/I18N.md +294 -0
- package/docs/modules/MEDIA-PROCESSING.md +281 -0
- package/docs/modules/MFA.md +266 -0
- package/docs/modules/NOTIFICATION.md +311 -0
- package/docs/modules/OAUTH.md +237 -0
- package/docs/modules/PAYMENT.md +804 -0
- package/docs/modules/QUEUE.md +540 -0
- package/docs/modules/RATE-LIMIT.md +339 -0
- package/docs/modules/SEARCH.md +288 -0
- package/docs/modules/SECURITY.md +327 -0
- package/docs/modules/SESSION.md +382 -0
- package/docs/modules/SWAGGER.md +305 -0
- package/docs/modules/UPLOAD.md +296 -0
- package/docs/modules/USER.md +505 -0
- package/docs/modules/VALIDATION.md +294 -0
- package/docs/modules/WEBHOOK.md +270 -0
- package/docs/modules/WEBSOCKET.md +691 -0
- package/package.json +53 -38
- package/prisma/schema.prisma +395 -1
- package/src/cli/commands/add-module.ts +520 -87
- package/src/cli/commands/db.ts +3 -4
- package/src/cli/commands/docs.ts +256 -6
- package/src/cli/commands/generate.ts +12 -19
- package/src/cli/commands/init.ts +384 -214
- package/src/cli/index.ts +0 -4
- package/src/cli/templates/repository.ts +6 -1
- package/src/cli/templates/routes.ts +6 -21
- package/src/cli/utils/docs-generator.ts +6 -7
- package/src/cli/utils/env-manager.ts +717 -0
- package/src/cli/utils/field-parser.ts +16 -7
- package/src/cli/utils/interactive-prompt.ts +223 -0
- package/src/cli/utils/template-manager.ts +346 -0
- package/src/config/database.config.ts +183 -0
- package/src/config/env.ts +0 -10
- package/src/config/index.ts +0 -14
- package/src/core/server.ts +1 -1
- package/src/database/adapters/mongoose.adapter.ts +132 -0
- package/src/database/adapters/prisma.adapter.ts +118 -0
- package/src/database/connection.ts +190 -0
- package/src/database/interfaces/database.interface.ts +85 -0
- package/src/database/interfaces/index.ts +7 -0
- package/src/database/interfaces/repository.interface.ts +129 -0
- package/src/database/models/mongoose/index.ts +7 -0
- package/src/database/models/mongoose/payment.schema.ts +347 -0
- package/src/database/models/mongoose/user.schema.ts +154 -0
- package/src/database/prisma.ts +1 -4
- package/src/database/redis.ts +101 -0
- package/src/database/repositories/mongoose/index.ts +7 -0
- package/src/database/repositories/mongoose/payment.repository.ts +380 -0
- package/src/database/repositories/mongoose/user.repository.ts +255 -0
- package/src/database/seed.ts +6 -1
- package/src/index.ts +9 -20
- package/src/middleware/security.ts +2 -6
- package/src/modules/analytics/analytics.routes.ts +80 -0
- package/src/modules/analytics/analytics.service.ts +364 -0
- package/src/modules/analytics/index.ts +18 -0
- package/src/modules/analytics/types.ts +180 -0
- package/src/modules/api-versioning/index.ts +15 -0
- package/src/modules/api-versioning/types.ts +86 -0
- package/src/modules/api-versioning/versioning.middleware.ts +120 -0
- package/src/modules/api-versioning/versioning.routes.ts +54 -0
- package/src/modules/api-versioning/versioning.service.ts +189 -0
- package/src/modules/audit/audit.repository.ts +206 -0
- package/src/modules/audit/audit.service.ts +27 -59
- package/src/modules/auth/auth.controller.ts +2 -2
- package/src/modules/auth/auth.middleware.ts +3 -9
- package/src/modules/auth/auth.routes.ts +10 -107
- package/src/modules/auth/auth.service.ts +126 -23
- package/src/modules/auth/index.ts +3 -4
- package/src/modules/cache/cache.service.ts +367 -0
- package/src/modules/cache/index.ts +10 -0
- package/src/modules/cache/types.ts +44 -0
- package/src/modules/email/email.service.ts +3 -10
- package/src/modules/email/templates.ts +2 -8
- package/src/modules/feature-flag/feature-flag.repository.ts +303 -0
- package/src/modules/feature-flag/feature-flag.routes.ts +247 -0
- package/src/modules/feature-flag/feature-flag.service.ts +566 -0
- package/src/modules/feature-flag/index.ts +20 -0
- package/src/modules/feature-flag/types.ts +192 -0
- package/src/modules/i18n/i18n.middleware.ts +186 -0
- package/src/modules/i18n/i18n.routes.ts +191 -0
- package/src/modules/i18n/i18n.service.ts +456 -0
- package/src/modules/i18n/index.ts +18 -0
- package/src/modules/i18n/types.ts +118 -0
- package/src/modules/media-processing/index.ts +17 -0
- package/src/modules/media-processing/media-processing.routes.ts +111 -0
- package/src/modules/media-processing/media-processing.service.ts +245 -0
- package/src/modules/media-processing/types.ts +156 -0
- package/src/modules/mfa/index.ts +20 -0
- package/src/modules/mfa/mfa.repository.ts +206 -0
- package/src/modules/mfa/mfa.routes.ts +595 -0
- package/src/modules/mfa/mfa.service.ts +572 -0
- package/src/modules/mfa/totp.ts +150 -0
- package/src/modules/mfa/types.ts +57 -0
- package/src/modules/notification/index.ts +20 -0
- package/src/modules/notification/notification.repository.ts +356 -0
- package/src/modules/notification/notification.service.ts +483 -0
- package/src/modules/notification/types.ts +119 -0
- package/src/modules/oauth/index.ts +20 -0
- package/src/modules/oauth/oauth.repository.ts +219 -0
- package/src/modules/oauth/oauth.routes.ts +446 -0
- package/src/modules/oauth/oauth.service.ts +293 -0
- package/src/modules/oauth/providers/apple.provider.ts +250 -0
- package/src/modules/oauth/providers/facebook.provider.ts +181 -0
- package/src/modules/oauth/providers/github.provider.ts +248 -0
- package/src/modules/oauth/providers/google.provider.ts +189 -0
- package/src/modules/oauth/providers/twitter.provider.ts +214 -0
- package/src/modules/oauth/types.ts +94 -0
- package/src/modules/payment/index.ts +19 -0
- package/src/modules/payment/payment.repository.ts +733 -0
- package/src/modules/payment/payment.routes.ts +390 -0
- package/src/modules/payment/payment.service.ts +354 -0
- package/src/modules/payment/providers/mobile-money.provider.ts +274 -0
- package/src/modules/payment/providers/paypal.provider.ts +190 -0
- package/src/modules/payment/providers/stripe.provider.ts +215 -0
- package/src/modules/payment/types.ts +140 -0
- package/src/modules/queue/cron.ts +438 -0
- package/src/modules/queue/index.ts +87 -0
- package/src/modules/queue/queue.routes.ts +600 -0
- package/src/modules/queue/queue.service.ts +842 -0
- package/src/modules/queue/types.ts +222 -0
- package/src/modules/queue/workers.ts +366 -0
- package/src/modules/rate-limit/index.ts +59 -0
- package/src/modules/rate-limit/rate-limit.middleware.ts +134 -0
- package/src/modules/rate-limit/rate-limit.routes.ts +269 -0
- package/src/modules/rate-limit/rate-limit.service.ts +348 -0
- package/src/modules/rate-limit/stores/memory.store.ts +165 -0
- package/src/modules/rate-limit/stores/redis.store.ts +322 -0
- package/src/modules/rate-limit/types.ts +153 -0
- package/src/modules/search/adapters/elasticsearch.adapter.ts +326 -0
- package/src/modules/search/adapters/meilisearch.adapter.ts +261 -0
- package/src/modules/search/adapters/memory.adapter.ts +278 -0
- package/src/modules/search/index.ts +21 -0
- package/src/modules/search/search.service.ts +234 -0
- package/src/modules/search/types.ts +214 -0
- package/src/modules/security/index.ts +40 -0
- package/src/modules/security/sanitize.ts +223 -0
- package/src/modules/security/security-audit.service.ts +388 -0
- package/src/modules/security/security.middleware.ts +398 -0
- package/src/modules/session/index.ts +3 -0
- package/src/modules/session/session.repository.ts +159 -0
- package/src/modules/session/session.service.ts +340 -0
- package/src/modules/session/types.ts +38 -0
- package/src/modules/swagger/index.ts +7 -1
- package/src/modules/swagger/schema-builder.ts +16 -4
- package/src/modules/swagger/swagger.service.ts +9 -10
- package/src/modules/swagger/types.ts +0 -2
- package/src/modules/upload/index.ts +14 -0
- package/src/modules/upload/types.ts +83 -0
- package/src/modules/upload/upload.repository.ts +199 -0
- package/src/modules/upload/upload.routes.ts +311 -0
- package/src/modules/upload/upload.service.ts +448 -0
- package/src/modules/user/index.ts +3 -3
- package/src/modules/user/user.controller.ts +15 -9
- package/src/modules/user/user.repository.ts +237 -113
- package/src/modules/user/user.routes.ts +39 -164
- package/src/modules/user/user.service.ts +4 -3
- package/src/modules/validation/validator.ts +12 -17
- package/src/modules/webhook/index.ts +91 -0
- package/src/modules/webhook/retry.ts +196 -0
- package/src/modules/webhook/signature.ts +135 -0
- package/src/modules/webhook/types.ts +181 -0
- package/src/modules/webhook/webhook.repository.ts +358 -0
- package/src/modules/webhook/webhook.routes.ts +442 -0
- package/src/modules/webhook/webhook.service.ts +457 -0
- package/src/modules/websocket/features.ts +504 -0
- package/src/modules/websocket/index.ts +106 -0
- package/src/modules/websocket/middlewares.ts +298 -0
- package/src/modules/websocket/types.ts +181 -0
- package/src/modules/websocket/websocket.service.ts +692 -0
- package/src/utils/errors.ts +7 -0
- package/src/utils/pagination.ts +4 -1
- package/tests/helpers/db-check.ts +79 -0
- package/tests/integration/auth-redis.test.ts +94 -0
- package/tests/integration/cache-redis.test.ts +387 -0
- package/tests/integration/mongoose-repositories.test.ts +410 -0
- package/tests/integration/payment-prisma.test.ts +637 -0
- package/tests/integration/queue-bullmq.test.ts +417 -0
- package/tests/integration/user-prisma.test.ts +441 -0
- package/tests/integration/websocket-socketio.test.ts +552 -0
- package/tests/setup.ts +11 -9
- package/vitest.config.ts +3 -8
- package/npm-cache/_cacache/content-v2/sha512/1c/d0/03440d500a0487621aad1d6402978340698976602046db8e24fa03c01ee6c022c69b0582f969042d9442ee876ac35c038e960dd427d1e622fa24b8eb7dba +0 -0
- package/npm-cache/_cacache/content-v2/sha512/42/55/28b493ca491833e5aab0e9c3108d29ab3f36c248ca88f45d4630674fce9130959e56ae308797ac2b6328fa7f09a610b9550ed09cb971d039876d293fc69d +0 -0
- package/npm-cache/_cacache/content-v2/sha512/e0/12/f360dc9315ee5f17844a0c8c233ee6bf7c30837c4a02ea0d56c61c7f7ab21c0e958e50ed2c57c59f983c762b93056778c9009b2398ffc26def0183999b13 +0 -0
- package/npm-cache/_cacache/content-v2/sha512/ed/b0/fae1161902898f4c913c67d7f6cdf6be0665aec3b389b9c4f4f0a101ca1da59badf1b59c4e0030f5223023b8d63cfe501c46a32c20c895d4fb3f11ca2232 +0 -0
- package/npm-cache/_cacache/index-v5/58/94/c2cba79e0f16b4c10e95a87e32255741149e8222cc314a476aab67c39cc0 +0 -5
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
RateLimitConfig,
|
|
3
|
+
RateLimitStore,
|
|
4
|
+
RateLimitResult,
|
|
5
|
+
RateLimitInfo,
|
|
6
|
+
RateLimitRequest,
|
|
7
|
+
} from './types.js';
|
|
8
|
+
import { MemoryStore } from './stores/memory.store.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Rate Limiting Service
|
|
12
|
+
* Supports multiple algorithms: fixed-window, sliding-window, token-bucket
|
|
13
|
+
*/
|
|
14
|
+
export class RateLimitService {
|
|
15
|
+
private config: Required<RateLimitConfig>;
|
|
16
|
+
private store: RateLimitStore;
|
|
17
|
+
|
|
18
|
+
constructor(config: RateLimitConfig, store?: RateLimitStore) {
|
|
19
|
+
const defaultStore = new MemoryStore();
|
|
20
|
+
this.config = {
|
|
21
|
+
max: config.max,
|
|
22
|
+
windowMs: config.windowMs,
|
|
23
|
+
algorithm: config.algorithm || 'sliding-window',
|
|
24
|
+
whitelist: config.whitelist || [],
|
|
25
|
+
blacklist: config.blacklist || [],
|
|
26
|
+
keyGenerator: config.keyGenerator || ((req: RateLimitRequest): string => req.ip),
|
|
27
|
+
skip: config.skip || ((): boolean => false),
|
|
28
|
+
onLimitReached: config.onLimitReached || ((): void => {}),
|
|
29
|
+
store: (config.store || defaultStore) as RateLimitStore,
|
|
30
|
+
headers: config.headers !== false,
|
|
31
|
+
message: config.message || 'Too many requests',
|
|
32
|
+
statusCode: config.statusCode || 429,
|
|
33
|
+
customLimits: config.customLimits || {},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
this.store = (store || defaultStore) as RateLimitStore;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if a request should be rate limited
|
|
41
|
+
*/
|
|
42
|
+
async check(
|
|
43
|
+
key: string,
|
|
44
|
+
options?: { max?: number; windowMs?: number }
|
|
45
|
+
): Promise<RateLimitResult> {
|
|
46
|
+
const max = options?.max || this.config.max;
|
|
47
|
+
const windowMs = options?.windowMs || this.config.windowMs;
|
|
48
|
+
|
|
49
|
+
// Check whitelist
|
|
50
|
+
if (this.config.whitelist.includes(key)) {
|
|
51
|
+
return {
|
|
52
|
+
allowed: true,
|
|
53
|
+
limit: max,
|
|
54
|
+
remaining: max,
|
|
55
|
+
resetAt: Date.now() + windowMs,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Check blacklist
|
|
60
|
+
if (this.config.blacklist.includes(key)) {
|
|
61
|
+
return {
|
|
62
|
+
allowed: false,
|
|
63
|
+
limit: max,
|
|
64
|
+
remaining: 0,
|
|
65
|
+
resetAt: Date.now() + windowMs,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Apply rate limiting algorithm
|
|
70
|
+
switch (this.config.algorithm) {
|
|
71
|
+
case 'fixed-window':
|
|
72
|
+
return await this.fixedWindow(key, max, windowMs);
|
|
73
|
+
case 'sliding-window':
|
|
74
|
+
return await this.slidingWindow(key, max, windowMs);
|
|
75
|
+
case 'token-bucket':
|
|
76
|
+
return await this.tokenBucket(key, max, windowMs);
|
|
77
|
+
default:
|
|
78
|
+
return await this.slidingWindow(key, max, windowMs);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Fixed Window algorithm
|
|
84
|
+
* Simple but can allow bursts at window boundaries
|
|
85
|
+
*/
|
|
86
|
+
private async fixedWindow(key: string, max: number, windowMs: number): Promise<RateLimitResult> {
|
|
87
|
+
let entry = await this.store.get(key);
|
|
88
|
+
const now = Date.now();
|
|
89
|
+
|
|
90
|
+
// Create new window if none exists or if expired
|
|
91
|
+
if (!entry || !entry.resetAt || entry.resetAt <= now) {
|
|
92
|
+
entry = {
|
|
93
|
+
count: 1,
|
|
94
|
+
startTime: now,
|
|
95
|
+
resetAt: now + windowMs,
|
|
96
|
+
firstRequest: now,
|
|
97
|
+
lastRequest: now,
|
|
98
|
+
};
|
|
99
|
+
await this.store.set(key, entry);
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
allowed: true,
|
|
103
|
+
limit: max,
|
|
104
|
+
remaining: max - 1,
|
|
105
|
+
resetAt: entry.resetAt ?? now + windowMs,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Increment count
|
|
110
|
+
entry.count++;
|
|
111
|
+
entry.lastRequest = now;
|
|
112
|
+
await this.store.set(key, entry);
|
|
113
|
+
|
|
114
|
+
const allowed = entry.count <= max;
|
|
115
|
+
const resetAt = entry.resetAt ?? now + windowMs;
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
allowed,
|
|
119
|
+
limit: max,
|
|
120
|
+
remaining: Math.max(0, max - entry.count),
|
|
121
|
+
resetAt,
|
|
122
|
+
retryAfter: allowed ? undefined : Math.ceil((resetAt - now) / 1000),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Sliding Window algorithm
|
|
128
|
+
* More accurate, prevents bursts at boundaries
|
|
129
|
+
*/
|
|
130
|
+
private async slidingWindow(
|
|
131
|
+
key: string,
|
|
132
|
+
max: number,
|
|
133
|
+
windowMs: number
|
|
134
|
+
): Promise<RateLimitResult> {
|
|
135
|
+
let entry = await this.store.get(key);
|
|
136
|
+
const now = Date.now();
|
|
137
|
+
|
|
138
|
+
if (!entry) {
|
|
139
|
+
entry = {
|
|
140
|
+
count: 1,
|
|
141
|
+
startTime: now,
|
|
142
|
+
resetAt: now + windowMs,
|
|
143
|
+
firstRequest: now,
|
|
144
|
+
lastRequest: now,
|
|
145
|
+
};
|
|
146
|
+
await this.store.set(key, entry);
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
allowed: true,
|
|
150
|
+
limit: max,
|
|
151
|
+
remaining: max - 1,
|
|
152
|
+
resetAt: entry.resetAt ?? now + windowMs,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Calculate sliding window
|
|
157
|
+
const firstRequest = entry.firstRequest ?? entry.startTime;
|
|
158
|
+
const timeInWindow = now - firstRequest;
|
|
159
|
+
const windowProgress = timeInWindow / windowMs;
|
|
160
|
+
|
|
161
|
+
// If we're past the window, reset
|
|
162
|
+
if (windowProgress >= 1) {
|
|
163
|
+
entry = {
|
|
164
|
+
count: 1,
|
|
165
|
+
startTime: now,
|
|
166
|
+
resetAt: now + windowMs,
|
|
167
|
+
firstRequest: now,
|
|
168
|
+
lastRequest: now,
|
|
169
|
+
};
|
|
170
|
+
await this.store.set(key, entry);
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
allowed: true,
|
|
174
|
+
limit: max,
|
|
175
|
+
remaining: max - 1,
|
|
176
|
+
resetAt: entry.resetAt ?? now + windowMs,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Sliding window count
|
|
181
|
+
const weightedCount = entry.count * (1 - windowProgress);
|
|
182
|
+
const currentCount = Math.ceil(weightedCount) + 1;
|
|
183
|
+
|
|
184
|
+
entry.count = currentCount;
|
|
185
|
+
entry.lastRequest = now;
|
|
186
|
+
await this.store.set(key, entry);
|
|
187
|
+
|
|
188
|
+
const allowed = currentCount <= max;
|
|
189
|
+
const resetAt = entry.resetAt ?? now + windowMs;
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
allowed,
|
|
193
|
+
limit: max,
|
|
194
|
+
remaining: Math.max(0, max - currentCount),
|
|
195
|
+
resetAt,
|
|
196
|
+
retryAfter: allowed ? undefined : Math.ceil((resetAt - now) / 1000),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Token Bucket algorithm
|
|
202
|
+
* Allows small bursts but enforces average rate
|
|
203
|
+
*/
|
|
204
|
+
private async tokenBucket(key: string, max: number, windowMs: number): Promise<RateLimitResult> {
|
|
205
|
+
let entry = await this.store.get(key);
|
|
206
|
+
const now = Date.now();
|
|
207
|
+
|
|
208
|
+
if (!entry) {
|
|
209
|
+
entry = {
|
|
210
|
+
count: max - 1, // Start with full bucket minus 1
|
|
211
|
+
startTime: now,
|
|
212
|
+
resetAt: now + windowMs,
|
|
213
|
+
firstRequest: now,
|
|
214
|
+
lastRequest: now,
|
|
215
|
+
};
|
|
216
|
+
await this.store.set(key, entry);
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
allowed: true,
|
|
220
|
+
limit: max,
|
|
221
|
+
remaining: max - 1,
|
|
222
|
+
resetAt: entry.resetAt ?? now + windowMs,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Refill tokens based on time passed
|
|
227
|
+
const lastRequest = entry.lastRequest ?? entry.startTime;
|
|
228
|
+
const timePassed = now - lastRequest;
|
|
229
|
+
const refillRate = max / windowMs;
|
|
230
|
+
const tokensToAdd = Math.floor(timePassed * refillRate);
|
|
231
|
+
const tokens = Math.min(max, entry.count + tokensToAdd);
|
|
232
|
+
|
|
233
|
+
// Try to consume a token
|
|
234
|
+
if (tokens >= 1) {
|
|
235
|
+
entry.count = tokens - 1;
|
|
236
|
+
entry.lastRequest = now;
|
|
237
|
+
entry.resetAt = now + windowMs;
|
|
238
|
+
await this.store.set(key, entry);
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
allowed: true,
|
|
242
|
+
limit: max,
|
|
243
|
+
remaining: entry.count,
|
|
244
|
+
resetAt: entry.resetAt,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Not enough tokens
|
|
249
|
+
const timeUntilToken = Math.ceil((1 - tokens) / refillRate / 1000);
|
|
250
|
+
const resetAt = entry.resetAt ?? now + windowMs;
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
allowed: false,
|
|
254
|
+
limit: max,
|
|
255
|
+
remaining: 0,
|
|
256
|
+
resetAt,
|
|
257
|
+
retryAfter: timeUntilToken,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Reset rate limit for a specific key
|
|
263
|
+
*/
|
|
264
|
+
async reset(key: string): Promise<void> {
|
|
265
|
+
await this.store.reset(key);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Get current rate limit info for a key
|
|
270
|
+
*/
|
|
271
|
+
async getInfo(key: string): Promise<RateLimitInfo | null> {
|
|
272
|
+
const entry = await this.store.get(key);
|
|
273
|
+
if (!entry) return null;
|
|
274
|
+
|
|
275
|
+
const max = this.config.max;
|
|
276
|
+
const remaining = Math.max(0, max - entry.count);
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
limit: max,
|
|
280
|
+
remaining,
|
|
281
|
+
resetAt: entry.resetAt,
|
|
282
|
+
count: entry.count,
|
|
283
|
+
firstRequest: entry.firstRequest,
|
|
284
|
+
lastRequest: entry.lastRequest,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Add IP to whitelist
|
|
290
|
+
*/
|
|
291
|
+
addToWhitelist(ip: string): void {
|
|
292
|
+
if (!this.config.whitelist.includes(ip)) {
|
|
293
|
+
this.config.whitelist.push(ip);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Remove IP from whitelist
|
|
299
|
+
*/
|
|
300
|
+
removeFromWhitelist(ip: string): void {
|
|
301
|
+
const index = this.config.whitelist.indexOf(ip);
|
|
302
|
+
if (index > -1) {
|
|
303
|
+
this.config.whitelist.splice(index, 1);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Add IP to blacklist
|
|
309
|
+
*/
|
|
310
|
+
addToBlacklist(ip: string): void {
|
|
311
|
+
if (!this.config.blacklist.includes(ip)) {
|
|
312
|
+
this.config.blacklist.push(ip);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Remove IP from blacklist
|
|
318
|
+
*/
|
|
319
|
+
removeFromBlacklist(ip: string): void {
|
|
320
|
+
const index = this.config.blacklist.indexOf(ip);
|
|
321
|
+
if (index > -1) {
|
|
322
|
+
this.config.blacklist.splice(index, 1);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Get configuration
|
|
328
|
+
*/
|
|
329
|
+
getConfig(): RateLimitConfig {
|
|
330
|
+
return { ...this.config };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Clear all rate limit data
|
|
335
|
+
*/
|
|
336
|
+
async clear(): Promise<void> {
|
|
337
|
+
await this.store.clear();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Cleanup expired entries
|
|
342
|
+
*/
|
|
343
|
+
async cleanup(): Promise<void> {
|
|
344
|
+
if (this.store.cleanup) {
|
|
345
|
+
await this.store.cleanup();
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type { RateLimitStore, RateLimitEntry } from '../types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* In-memory rate limit store
|
|
5
|
+
* Good for single-instance deployments
|
|
6
|
+
*/
|
|
7
|
+
export class MemoryStore implements RateLimitStore {
|
|
8
|
+
private store = new Map<string, RateLimitEntry>();
|
|
9
|
+
private cleanupInterval: NodeJS.Timeout | null = null;
|
|
10
|
+
|
|
11
|
+
constructor(cleanupIntervalMs = 60000) {
|
|
12
|
+
// Periodic cleanup of expired entries
|
|
13
|
+
this.cleanupInterval = setInterval(() => void this.cleanup(), cleanupIntervalMs);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async get(key: string): Promise<RateLimitEntry | null> {
|
|
17
|
+
const entry = this.store.get(key);
|
|
18
|
+
if (!entry) return null;
|
|
19
|
+
|
|
20
|
+
// Check if entry has expired
|
|
21
|
+
const now = Date.now();
|
|
22
|
+
if (entry.startTime && now - entry.startTime > this.getTtl(key)) {
|
|
23
|
+
this.store.delete(key);
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return entry;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async set(key: string, entry: RateLimitEntry, _ttlMs: number): Promise<void> {
|
|
31
|
+
this.store.set(key, entry);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async increment(key: string, windowMs: number): Promise<RateLimitEntry> {
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
let entry = this.store.get(key);
|
|
37
|
+
|
|
38
|
+
if (!entry || now - entry.startTime >= windowMs) {
|
|
39
|
+
// Start new window
|
|
40
|
+
entry = {
|
|
41
|
+
count: 1,
|
|
42
|
+
startTime: now,
|
|
43
|
+
timestamps: [now],
|
|
44
|
+
};
|
|
45
|
+
} else {
|
|
46
|
+
// Increment existing window
|
|
47
|
+
entry.count++;
|
|
48
|
+
entry.timestamps = entry.timestamps || [];
|
|
49
|
+
entry.timestamps.push(now);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this.store.set(key, entry);
|
|
53
|
+
return entry;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async reset(key: string): Promise<void> {
|
|
57
|
+
this.store.delete(key);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async clear(): Promise<void> {
|
|
61
|
+
this.store.clear();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Sliding window increment
|
|
66
|
+
*/
|
|
67
|
+
async slidingWindowIncrement(key: string, windowMs: number): Promise<RateLimitEntry> {
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
let entry = this.store.get(key);
|
|
70
|
+
|
|
71
|
+
if (!entry) {
|
|
72
|
+
entry = {
|
|
73
|
+
count: 1,
|
|
74
|
+
startTime: now,
|
|
75
|
+
timestamps: [now],
|
|
76
|
+
};
|
|
77
|
+
} else {
|
|
78
|
+
// Remove expired timestamps
|
|
79
|
+
const windowStart = now - windowMs;
|
|
80
|
+
entry.timestamps = (entry.timestamps || []).filter((t) => t > windowStart);
|
|
81
|
+
entry.timestamps.push(now);
|
|
82
|
+
entry.count = entry.timestamps.length;
|
|
83
|
+
entry.startTime = entry.timestamps[0] || now;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
this.store.set(key, entry);
|
|
87
|
+
return entry;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Token bucket increment
|
|
92
|
+
*/
|
|
93
|
+
async tokenBucketIncrement(
|
|
94
|
+
key: string,
|
|
95
|
+
maxTokens: number,
|
|
96
|
+
refillRate: number,
|
|
97
|
+
refillIntervalMs: number
|
|
98
|
+
): Promise<RateLimitEntry> {
|
|
99
|
+
const now = Date.now();
|
|
100
|
+
let entry = this.store.get(key);
|
|
101
|
+
|
|
102
|
+
if (!entry) {
|
|
103
|
+
entry = {
|
|
104
|
+
count: 1,
|
|
105
|
+
startTime: now,
|
|
106
|
+
tokens: maxTokens - 1,
|
|
107
|
+
lastRefill: now,
|
|
108
|
+
};
|
|
109
|
+
} else {
|
|
110
|
+
// Calculate tokens to add based on time passed
|
|
111
|
+
const timePassed = now - (entry.lastRefill || now);
|
|
112
|
+
const tokensToAdd = Math.floor(timePassed / refillIntervalMs) * refillRate;
|
|
113
|
+
|
|
114
|
+
entry.tokens = Math.min(maxTokens, (entry.tokens || 0) + tokensToAdd);
|
|
115
|
+
entry.lastRefill = now;
|
|
116
|
+
|
|
117
|
+
if (entry.tokens > 0) {
|
|
118
|
+
entry.tokens--;
|
|
119
|
+
entry.count++;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this.store.set(key, entry);
|
|
124
|
+
return entry;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get all keys (for admin purposes)
|
|
129
|
+
*/
|
|
130
|
+
getKeys(): string[] {
|
|
131
|
+
return Array.from(this.store.keys());
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get store size
|
|
136
|
+
*/
|
|
137
|
+
getSize(): number {
|
|
138
|
+
return this.store.size;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Stop cleanup interval
|
|
143
|
+
*/
|
|
144
|
+
destroy(): void {
|
|
145
|
+
if (this.cleanupInterval) {
|
|
146
|
+
clearInterval(this.cleanupInterval);
|
|
147
|
+
this.cleanupInterval = null;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async cleanup(): Promise<void> {
|
|
152
|
+
const now = Date.now();
|
|
153
|
+
for (const [key, entry] of this.store.entries()) {
|
|
154
|
+
// Default TTL of 1 hour for cleanup
|
|
155
|
+
if (now - entry.startTime > 3600000) {
|
|
156
|
+
this.store.delete(key);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private getTtl(_key: string): number {
|
|
162
|
+
// Default TTL, can be customized per key if needed
|
|
163
|
+
return 3600000; // 1 hour
|
|
164
|
+
}
|
|
165
|
+
}
|