@donkeylabs/server 0.1.0 → 0.1.2

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.
@@ -0,0 +1,525 @@
1
+ # Rate Limiter Service
2
+
3
+ Request throttling with sliding window algorithm and automatic IP detection from proxy headers.
4
+
5
+ ## Quick Start
6
+
7
+ ```ts
8
+ // Check rate limit
9
+ const result = await ctx.core.rateLimiter.check(`api:${ctx.ip}`, 100, 60000);
10
+
11
+ if (!result.allowed) {
12
+ return new Response("Too Many Requests", {
13
+ status: 429,
14
+ headers: { "Retry-After": String(result.retryAfter) },
15
+ });
16
+ }
17
+ ```
18
+
19
+ ---
20
+
21
+ ## API Reference
22
+
23
+ ### Interface
24
+
25
+ ```ts
26
+ interface RateLimiter {
27
+ check(key: string, limit: number, windowMs: number): Promise<RateLimitResult>;
28
+ reset(key: string): Promise<void>;
29
+ }
30
+
31
+ interface RateLimitResult {
32
+ allowed: boolean; // Whether request is allowed
33
+ remaining: number; // Requests remaining in window
34
+ limit: number; // Total limit
35
+ resetAt: Date; // When window resets
36
+ retryAfter?: number; // Seconds until retry (if blocked)
37
+ }
38
+ ```
39
+
40
+ ### Methods
41
+
42
+ | Method | Description |
43
+ |--------|-------------|
44
+ | `check(key, limit, windowMs)` | Check and increment counter for key |
45
+ | `reset(key)` | Reset counter for key |
46
+
47
+ ---
48
+
49
+ ## IP Extraction
50
+
51
+ The framework automatically extracts client IP and provides it as `ctx.ip`. Headers are checked in priority order:
52
+
53
+ 1. `CF-Connecting-IP` (Cloudflare)
54
+ 2. `True-Client-IP` (Akamai, Cloudflare Enterprise)
55
+ 3. `X-Real-IP` (Nginx)
56
+ 4. `X-Forwarded-For` (first IP in chain)
57
+ 5. Socket address (direct connection)
58
+
59
+ ```ts
60
+ router.route("protected").typed({
61
+ handle: async (input, ctx) => {
62
+ console.log("Client IP:", ctx.ip); // Automatically extracted
63
+ },
64
+ });
65
+ ```
66
+
67
+ ---
68
+
69
+ ## Usage Examples
70
+
71
+ ### Basic Rate Limiting
72
+
73
+ ```ts
74
+ router.route("api").typed({
75
+ handle: async (input, ctx) => {
76
+ // 100 requests per minute per IP
77
+ const result = await ctx.core.rateLimiter.check(
78
+ `api:${ctx.ip}`,
79
+ 100,
80
+ 60000
81
+ );
82
+
83
+ if (!result.allowed) {
84
+ return Response.json(
85
+ { error: "Rate limit exceeded", retryAfter: result.retryAfter },
86
+ {
87
+ status: 429,
88
+ headers: {
89
+ "Retry-After": String(result.retryAfter),
90
+ "X-RateLimit-Limit": String(result.limit),
91
+ "X-RateLimit-Remaining": "0",
92
+ "X-RateLimit-Reset": result.resetAt.toISOString(),
93
+ },
94
+ }
95
+ );
96
+ }
97
+
98
+ // Process request...
99
+ return { data: "success" };
100
+ },
101
+ });
102
+ ```
103
+
104
+ ### Per-User Rate Limiting
105
+
106
+ ```ts
107
+ router.route("user-action").typed({
108
+ handle: async (input, ctx) => {
109
+ // Rate limit by user, not IP (for authenticated routes)
110
+ const key = `user:${ctx.user.id}:action`;
111
+ const result = await ctx.core.rateLimiter.check(key, 10, 60000);
112
+
113
+ if (!result.allowed) {
114
+ throw new Error(`Rate limited. Try again in ${result.retryAfter}s`);
115
+ }
116
+
117
+ return performAction(input);
118
+ },
119
+ });
120
+ ```
121
+
122
+ ### Per-Route Rate Limiting
123
+
124
+ ```ts
125
+ // Different limits for different endpoints
126
+ const RATE_LIMITS: Record<string, { limit: number; windowMs: number }> = {
127
+ login: { limit: 5, windowMs: 60000 }, // 5/min
128
+ search: { limit: 30, windowMs: 60000 }, // 30/min
129
+ upload: { limit: 10, windowMs: 3600000 }, // 10/hour
130
+ default: { limit: 100, windowMs: 60000 }, // 100/min
131
+ };
132
+
133
+ async function checkRouteLimit(route: string, ip: string, ctx: ServerContext) {
134
+ const config = RATE_LIMITS[route] || RATE_LIMITS.default;
135
+ return ctx.core.rateLimiter.check(`${route}:${ip}`, config.limit, config.windowMs);
136
+ }
137
+ ```
138
+
139
+ ### Tiered Rate Limits
140
+
141
+ ```ts
142
+ // Different limits based on subscription tier
143
+ router.route("api").typed({
144
+ handle: async (input, ctx) => {
145
+ const tier = ctx.user?.tier || "free";
146
+
147
+ const limits: Record<string, { limit: number; window: number }> = {
148
+ free: { limit: 100, window: 3600000 }, // 100/hour
149
+ pro: { limit: 1000, window: 3600000 }, // 1000/hour
150
+ enterprise: { limit: 10000, window: 3600000 }, // 10000/hour
151
+ };
152
+
153
+ const { limit, window } = limits[tier];
154
+ const key = `api:${ctx.user?.id || ctx.ip}:${tier}`;
155
+
156
+ const result = await ctx.core.rateLimiter.check(key, limit, window);
157
+
158
+ if (!result.allowed) {
159
+ return Response.json({
160
+ error: "Rate limit exceeded",
161
+ tier,
162
+ limit,
163
+ upgrade: tier === "free" ? "Upgrade to Pro for higher limits" : undefined,
164
+ }, { status: 429 });
165
+ }
166
+
167
+ return processRequest(input);
168
+ },
169
+ });
170
+ ```
171
+
172
+ ---
173
+
174
+ ## Rate Limit Middleware
175
+
176
+ Create reusable rate limit middleware:
177
+
178
+ ```ts
179
+ // middleware/rateLimit.ts
180
+ import { createMiddleware } from "../middleware";
181
+ import { parseDuration } from "../core/rate-limiter";
182
+
183
+ interface RateLimitConfig {
184
+ limit: number;
185
+ window: string; // "1m", "1h", etc.
186
+ keyPrefix?: string;
187
+ keyFn?: (ctx: ServerContext) => string;
188
+ }
189
+
190
+ export const rateLimitMiddleware = createMiddleware<RateLimitConfig>(
191
+ async (req, ctx, next, config) => {
192
+ const windowMs = parseDuration(config.window);
193
+ const keyBase = config.keyFn?.(ctx) ?? ctx.ip;
194
+ const key = config.keyPrefix
195
+ ? `${config.keyPrefix}:${keyBase}`
196
+ : `ratelimit:${keyBase}`;
197
+
198
+ const result = await ctx.core.rateLimiter.check(key, config.limit, windowMs);
199
+
200
+ // Add rate limit headers to response
201
+ const response = result.allowed
202
+ ? await next()
203
+ : Response.json({ error: "Too Many Requests" }, { status: 429 });
204
+
205
+ response.headers.set("X-RateLimit-Limit", String(result.limit));
206
+ response.headers.set("X-RateLimit-Remaining", String(result.remaining));
207
+ response.headers.set("X-RateLimit-Reset", result.resetAt.toISOString());
208
+
209
+ if (!result.allowed) {
210
+ response.headers.set("Retry-After", String(result.retryAfter));
211
+ }
212
+
213
+ return response;
214
+ }
215
+ );
216
+
217
+ // Register in plugin
218
+ export const rateLimitPlugin = createPlugin.define({
219
+ name: "rateLimit",
220
+ middleware: {
221
+ rateLimit: rateLimitMiddleware,
222
+ },
223
+ service: async () => ({}),
224
+ });
225
+ ```
226
+
227
+ **Usage:**
228
+
229
+ ```ts
230
+ router.middleware
231
+ .rateLimit({ limit: 100, window: "1m" })
232
+ .route("api")
233
+ .typed({ handle: ... });
234
+
235
+ router.middleware
236
+ .rateLimit({ limit: 5, window: "1h", keyPrefix: "login" })
237
+ .route("login")
238
+ .typed({ handle: ... });
239
+ ```
240
+
241
+ ---
242
+
243
+ ## Helper Functions
244
+
245
+ ### parseDuration
246
+
247
+ Convert duration strings to milliseconds:
248
+
249
+ ```ts
250
+ import { parseDuration } from "./core/rate-limiter";
251
+
252
+ parseDuration("100ms"); // 100
253
+ parseDuration("30s"); // 30000
254
+ parseDuration("5m"); // 300000
255
+ parseDuration("2h"); // 7200000
256
+ parseDuration("1d"); // 86400000
257
+ ```
258
+
259
+ ### createRateLimitKey
260
+
261
+ Build consistent rate limit keys:
262
+
263
+ ```ts
264
+ import { createRateLimitKey } from "./core/rate-limiter";
265
+
266
+ const key = createRateLimitKey("api.users.list", "192.168.1.1");
267
+ // "ratelimit:api.users.list:192.168.1.1"
268
+ ```
269
+
270
+ ### extractClientIP
271
+
272
+ Manual IP extraction if needed:
273
+
274
+ ```ts
275
+ import { extractClientIP } from "./core/rate-limiter";
276
+
277
+ const ip = extractClientIP(req, socketAddr);
278
+ ```
279
+
280
+ ---
281
+
282
+ ## Real-World Examples
283
+
284
+ ### API Rate Limiting with Response Headers
285
+
286
+ ```ts
287
+ async function withRateLimit(
288
+ ctx: ServerContext,
289
+ key: string,
290
+ limit: number,
291
+ windowMs: number,
292
+ handler: () => Promise<Response>
293
+ ): Promise<Response> {
294
+ const result = await ctx.core.rateLimiter.check(key, limit, windowMs);
295
+
296
+ const headers = {
297
+ "X-RateLimit-Limit": String(result.limit),
298
+ "X-RateLimit-Remaining": String(result.remaining),
299
+ "X-RateLimit-Reset": String(Math.floor(result.resetAt.getTime() / 1000)),
300
+ };
301
+
302
+ if (!result.allowed) {
303
+ return Response.json(
304
+ {
305
+ error: "rate_limit_exceeded",
306
+ message: `Rate limit exceeded. Retry in ${result.retryAfter} seconds.`,
307
+ },
308
+ {
309
+ status: 429,
310
+ headers: {
311
+ ...headers,
312
+ "Retry-After": String(result.retryAfter),
313
+ },
314
+ }
315
+ );
316
+ }
317
+
318
+ const response = await handler();
319
+
320
+ // Add headers to successful response
321
+ for (const [key, value] of Object.entries(headers)) {
322
+ response.headers.set(key, value);
323
+ }
324
+
325
+ return response;
326
+ }
327
+ ```
328
+
329
+ ### Login Brute Force Protection
330
+
331
+ ```ts
332
+ router.route("login").typed({
333
+ handle: async (input, ctx) => {
334
+ // Strict limit on login attempts
335
+ const result = await ctx.core.rateLimiter.check(
336
+ `login:${ctx.ip}`,
337
+ 5, // 5 attempts
338
+ 300000 // per 5 minutes
339
+ );
340
+
341
+ if (!result.allowed) {
342
+ ctx.core.logger.warn("Login rate limited", {
343
+ ip: ctx.ip,
344
+ email: input.email,
345
+ });
346
+
347
+ return Response.json({
348
+ error: "Too many login attempts",
349
+ retryAfter: result.retryAfter,
350
+ }, { status: 429 });
351
+ }
352
+
353
+ const user = await authenticate(input.email, input.password);
354
+
355
+ if (!user) {
356
+ // Failed attempt still counts
357
+ return Response.json({ error: "Invalid credentials" }, { status: 401 });
358
+ }
359
+
360
+ // Success - optionally reset limit
361
+ await ctx.core.rateLimiter.reset(`login:${ctx.ip}`);
362
+
363
+ return { token: generateToken(user) };
364
+ },
365
+ });
366
+ ```
367
+
368
+ ### Cost-Based Rate Limiting
369
+
370
+ ```ts
371
+ // Different operations have different costs
372
+ const OPERATION_COSTS: Record<string, number> = {
373
+ "query.simple": 1,
374
+ "query.complex": 5,
375
+ "mutation.create": 2,
376
+ "mutation.bulkCreate": 10,
377
+ "export.csv": 20,
378
+ "export.pdf": 50,
379
+ };
380
+
381
+ router.route("graphql").typed({
382
+ handle: async (input, ctx) => {
383
+ const operationType = analyzeQuery(input.query);
384
+ const cost = OPERATION_COSTS[operationType] || 1;
385
+
386
+ // 1000 cost units per hour
387
+ const key = `graphql:${ctx.user.id}`;
388
+
389
+ // Check if we have enough budget
390
+ const current = await ctx.core.cache.get<number>(`${key}:cost`) || 0;
391
+
392
+ if (current + cost > 1000) {
393
+ return Response.json({
394
+ error: "Rate limit exceeded",
395
+ cost,
396
+ used: current,
397
+ limit: 1000,
398
+ }, { status: 429 });
399
+ }
400
+
401
+ // Increment cost
402
+ await ctx.core.cache.set(`${key}:cost`, current + cost, 3600000);
403
+
404
+ return executeQuery(input.query);
405
+ },
406
+ });
407
+ ```
408
+
409
+ ---
410
+
411
+ ## Custom Adapters
412
+
413
+ Implement `RateLimitAdapter` for custom backends:
414
+
415
+ ```ts
416
+ interface RateLimitAdapter {
417
+ increment(key: string, windowMs: number): Promise<{ count: number; resetAt: Date }>;
418
+ get(key: string): Promise<{ count: number; resetAt: Date } | null>;
419
+ reset(key: string): Promise<void>;
420
+ }
421
+ ```
422
+
423
+ ### Redis Adapter Example
424
+
425
+ ```ts
426
+ import { createRateLimiter, type RateLimitAdapter } from "./core/rate-limiter";
427
+ import Redis from "ioredis";
428
+
429
+ class RedisRateLimitAdapter implements RateLimitAdapter {
430
+ constructor(private redis: Redis) {}
431
+
432
+ async increment(key: string, windowMs: number): Promise<{ count: number; resetAt: Date }> {
433
+ const now = Date.now();
434
+ const windowKey = `${key}:${Math.floor(now / windowMs)}`;
435
+
436
+ const count = await this.redis.incr(windowKey);
437
+
438
+ if (count === 1) {
439
+ // Set expiry on first request in window
440
+ await this.redis.pexpire(windowKey, windowMs);
441
+ }
442
+
443
+ const resetAt = new Date(Math.ceil(now / windowMs) * windowMs);
444
+
445
+ return { count, resetAt };
446
+ }
447
+
448
+ async get(key: string): Promise<{ count: number; resetAt: Date } | null> {
449
+ // Implementation for getting current state
450
+ }
451
+
452
+ async reset(key: string): Promise<void> {
453
+ const keys = await this.redis.keys(`${key}:*`);
454
+ if (keys.length > 0) {
455
+ await this.redis.del(...keys);
456
+ }
457
+ }
458
+ }
459
+
460
+ const rateLimiter = createRateLimiter({
461
+ adapter: new RedisRateLimitAdapter(new Redis()),
462
+ });
463
+ ```
464
+
465
+ ---
466
+
467
+ ## Best Practices
468
+
469
+ ### 1. Use Appropriate Key Granularity
470
+
471
+ ```ts
472
+ // Per IP for anonymous
473
+ `public:${ctx.ip}`
474
+
475
+ // Per user for authenticated
476
+ `user:${ctx.user.id}`
477
+
478
+ // Per user per endpoint
479
+ `user:${ctx.user.id}:${endpoint}`
480
+
481
+ // Per organization for team limits
482
+ `org:${ctx.user.orgId}`
483
+ ```
484
+
485
+ ### 2. Set Reasonable Limits
486
+
487
+ ```ts
488
+ // Consider normal usage patterns
489
+ const limits = {
490
+ // Login: low limit, short window (brute force protection)
491
+ login: { limit: 5, window: "5m" },
492
+
493
+ // Search: moderate limit (expensive operations)
494
+ search: { limit: 30, window: "1m" },
495
+
496
+ // Read API: generous limit
497
+ read: { limit: 1000, window: "1h" },
498
+
499
+ // Write API: moderate limit
500
+ write: { limit: 100, window: "1h" },
501
+ };
502
+ ```
503
+
504
+ ### 3. Return Helpful Headers
505
+
506
+ ```ts
507
+ // Always include rate limit info in responses
508
+ response.headers.set("X-RateLimit-Limit", limit);
509
+ response.headers.set("X-RateLimit-Remaining", remaining);
510
+ response.headers.set("X-RateLimit-Reset", resetTimestamp);
511
+ response.headers.set("Retry-After", seconds); // Only when blocked
512
+ ```
513
+
514
+ ### 4. Log Rate Limit Events
515
+
516
+ ```ts
517
+ if (!result.allowed) {
518
+ ctx.core.logger.warn("Rate limit exceeded", {
519
+ key,
520
+ ip: ctx.ip,
521
+ userId: ctx.user?.id,
522
+ endpoint: req.url,
523
+ });
524
+ }
525
+ ```