@donkeylabs/server 0.3.0 → 0.4.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.
- package/LICENSE +1 -1
- package/docs/api-client.md +7 -7
- package/docs/cache.md +1 -74
- package/docs/core-services.md +4 -116
- package/docs/cron.md +1 -1
- package/docs/errors.md +2 -2
- package/docs/events.md +3 -98
- package/docs/handlers.md +13 -48
- package/docs/logger.md +3 -58
- package/docs/middleware.md +2 -2
- package/docs/plugins.md +13 -64
- package/docs/project-structure.md +4 -142
- package/docs/rate-limiter.md +4 -136
- package/docs/router.md +6 -14
- package/docs/sse.md +1 -99
- package/docs/sveltekit-adapter.md +420 -0
- package/package.json +8 -11
- package/registry.d.ts +15 -14
- package/src/core/cache.ts +0 -75
- package/src/core/cron.ts +3 -96
- package/src/core/errors.ts +78 -11
- package/src/core/events.ts +1 -47
- package/src/core/index.ts +0 -4
- package/src/core/jobs.ts +0 -112
- package/src/core/logger.ts +12 -79
- package/src/core/rate-limiter.ts +29 -108
- package/src/core/sse.ts +1 -84
- package/src/core.ts +13 -104
- package/src/generator/index.ts +566 -0
- package/src/generator/zod-to-ts.ts +114 -0
- package/src/handlers.ts +14 -110
- package/src/index.ts +30 -24
- package/src/middleware.ts +2 -5
- package/src/registry.ts +4 -0
- package/src/router.ts +47 -1
- package/src/server.ts +618 -332
- package/README.md +0 -254
- package/cli/commands/dev.ts +0 -134
- package/cli/commands/generate.ts +0 -605
- package/cli/commands/init.ts +0 -205
- package/cli/commands/interactive.ts +0 -417
- package/cli/commands/plugin.ts +0 -192
- package/cli/commands/route.ts +0 -195
- package/cli/donkeylabs +0 -2
- package/cli/index.ts +0 -114
- package/docs/svelte-frontend.md +0 -324
- package/docs/testing.md +0 -438
- package/mcp/donkeylabs-mcp +0 -3238
- package/mcp/server.ts +0 -3238
package/docs/middleware.md
CHANGED
|
@@ -5,7 +5,7 @@ Request/response middleware for cross-cutting concerns like authentication, rate
|
|
|
5
5
|
## Quick Start
|
|
6
6
|
|
|
7
7
|
```ts
|
|
8
|
-
import { createMiddleware } from "
|
|
8
|
+
import { createMiddleware } from "./middleware";
|
|
9
9
|
|
|
10
10
|
// Create middleware with config
|
|
11
11
|
const authMiddleware = createMiddleware<{ required: boolean }>(
|
|
@@ -66,7 +66,7 @@ function createMiddleware<TConfig = void>(
|
|
|
66
66
|
|
|
67
67
|
```ts
|
|
68
68
|
// middleware/auth.ts
|
|
69
|
-
import { createMiddleware } from "
|
|
69
|
+
import { createMiddleware } from "../middleware";
|
|
70
70
|
|
|
71
71
|
interface AuthConfig {
|
|
72
72
|
required?: boolean;
|
package/docs/plugins.md
CHANGED
|
@@ -22,7 +22,7 @@ The simplest plugin exports a service that becomes available to all route handle
|
|
|
22
22
|
|
|
23
23
|
```ts
|
|
24
24
|
// plugins/greeter/index.ts
|
|
25
|
-
import { createPlugin } from "
|
|
25
|
+
import { createPlugin } from "../../core";
|
|
26
26
|
|
|
27
27
|
export const greeterPlugin = createPlugin.define({
|
|
28
28
|
name: "greeter",
|
|
@@ -88,7 +88,7 @@ This creates `plugins/users/schema.ts` with Kysely types.
|
|
|
88
88
|
|
|
89
89
|
```ts
|
|
90
90
|
// plugins/users/index.ts
|
|
91
|
-
import { createPlugin } from "
|
|
91
|
+
import { createPlugin } from "../../core";
|
|
92
92
|
import type { DB as UsersSchema } from "./schema";
|
|
93
93
|
|
|
94
94
|
export const usersPlugin = createPlugin
|
|
@@ -131,7 +131,7 @@ Plugins can accept configuration at registration time using the factory pattern:
|
|
|
131
131
|
|
|
132
132
|
```ts
|
|
133
133
|
// plugins/email/index.ts
|
|
134
|
-
import { createPlugin } from "
|
|
134
|
+
import { createPlugin } from "../../core";
|
|
135
135
|
|
|
136
136
|
export interface EmailConfig {
|
|
137
137
|
apiKey: string;
|
|
@@ -193,7 +193,7 @@ Plugins can depend on other plugins. Dependencies are resolved automatically in
|
|
|
193
193
|
|
|
194
194
|
```ts
|
|
195
195
|
// plugins/notifications/index.ts
|
|
196
|
-
import { createPlugin } from "
|
|
196
|
+
import { createPlugin } from "../../core";
|
|
197
197
|
|
|
198
198
|
export const notificationsPlugin = createPlugin.define({
|
|
199
199
|
name: "notifications",
|
|
@@ -242,8 +242,9 @@ Plugins can define custom request handlers for specialized processing:
|
|
|
242
242
|
|
|
243
243
|
```ts
|
|
244
244
|
// plugins/api/index.ts
|
|
245
|
-
import { createPlugin
|
|
246
|
-
import
|
|
245
|
+
import { createPlugin } from "../../core";
|
|
246
|
+
import { createHandler } from "../../handlers";
|
|
247
|
+
import type { ServerContext } from "../../router";
|
|
247
248
|
|
|
248
249
|
// Define handler signature
|
|
249
250
|
type XMLHandler = (xmlBody: string, ctx: ServerContext) => Promise<string>;
|
|
@@ -369,16 +370,12 @@ The `PluginContext` passed to the service function provides:
|
|
|
369
370
|
|
|
370
371
|
```ts
|
|
371
372
|
interface PluginContext<Deps, Schema, Config> {
|
|
372
|
-
// Convenience getters (auto-namespaced for this plugin)
|
|
373
|
-
logger: Logger; // Auto-namespaced with { plugin: "pluginName" }
|
|
374
|
-
cache: NamespacedCache; // Auto-prefixed with "pluginName:"
|
|
375
|
-
|
|
376
373
|
// Core services (always available)
|
|
377
374
|
core: {
|
|
378
375
|
db: Kysely<any>;
|
|
379
376
|
config: Record<string, any>;
|
|
380
|
-
logger: Logger;
|
|
381
|
-
cache: Cache;
|
|
377
|
+
logger: Logger;
|
|
378
|
+
cache: Cache;
|
|
382
379
|
events: Events;
|
|
383
380
|
cron: Cron;
|
|
384
381
|
jobs: Jobs;
|
|
@@ -397,42 +394,6 @@ interface PluginContext<Deps, Schema, Config> {
|
|
|
397
394
|
}
|
|
398
395
|
```
|
|
399
396
|
|
|
400
|
-
**Example using convenience getters:**
|
|
401
|
-
|
|
402
|
-
```ts
|
|
403
|
-
export const paymentsPlugin = createPlugin.define({
|
|
404
|
-
name: "payments",
|
|
405
|
-
service: async (ctx) => {
|
|
406
|
-
// ctx.logger is auto-namespaced with { plugin: "payments" }
|
|
407
|
-
const logger = ctx.logger;
|
|
408
|
-
|
|
409
|
-
// ctx.cache is auto-prefixed with "payments:"
|
|
410
|
-
const cache = ctx.cache;
|
|
411
|
-
|
|
412
|
-
logger.info("Payment plugin initialized");
|
|
413
|
-
// Output: [14:30:45.123 CST][payments][INFO] Payment plugin initialized
|
|
414
|
-
|
|
415
|
-
return {
|
|
416
|
-
async processPayment(orderId: string) {
|
|
417
|
-
// Check cache (key becomes "payments:order:123")
|
|
418
|
-
const cached = await cache.get(`order:${orderId}`);
|
|
419
|
-
if (cached) {
|
|
420
|
-
logger.debug("Cache hit", { orderId });
|
|
421
|
-
return cached;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
logger.info("Processing payment", { orderId });
|
|
425
|
-
// Output: [14:30:45.456 CST][payments][INFO] Processing payment orderId=123
|
|
426
|
-
|
|
427
|
-
const result = await chargeCard(orderId);
|
|
428
|
-
await cache.set(`order:${orderId}`, result, 60000);
|
|
429
|
-
return result;
|
|
430
|
-
},
|
|
431
|
-
};
|
|
432
|
-
},
|
|
433
|
-
});
|
|
434
|
-
```
|
|
435
|
-
|
|
436
397
|
**Example using all context features:**
|
|
437
398
|
|
|
438
399
|
```ts
|
|
@@ -443,13 +404,9 @@ export const analyticsPlugin = createPlugin
|
|
|
443
404
|
name: "analytics",
|
|
444
405
|
dependencies: ["users"] as const,
|
|
445
406
|
service: async (ctx) => {
|
|
446
|
-
// Use plugin-scoped logger and cache
|
|
447
|
-
const logger = ctx.logger;
|
|
448
|
-
const cache = ctx.cache;
|
|
449
|
-
|
|
450
407
|
// Schedule daily report
|
|
451
408
|
ctx.core.cron.schedule("0 0 * * *", async () => {
|
|
452
|
-
logger.info("Running daily analytics");
|
|
409
|
+
ctx.core.logger.info("Running daily analytics");
|
|
453
410
|
// ...
|
|
454
411
|
});
|
|
455
412
|
|
|
@@ -462,16 +419,8 @@ export const analyticsPlugin = createPlugin
|
|
|
462
419
|
});
|
|
463
420
|
|
|
464
421
|
return {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
const cacheKey = `event:${event}:${JSON.stringify(data)}`;
|
|
468
|
-
if (await cache.has(cacheKey)) {
|
|
469
|
-
logger.debug("Duplicate event ignored", { event });
|
|
470
|
-
return;
|
|
471
|
-
}
|
|
472
|
-
await cache.set(cacheKey, true, 5000);
|
|
473
|
-
|
|
474
|
-
await ctx.core.events.emit(`analytics.${event}`, {
|
|
422
|
+
track(event: string, data: any) {
|
|
423
|
+
ctx.core.events.emit(`analytics.${event}`, {
|
|
475
424
|
trackingId: ctx.config.trackingId,
|
|
476
425
|
...data,
|
|
477
426
|
});
|
|
@@ -495,7 +444,7 @@ import {
|
|
|
495
444
|
InferMiddleware,
|
|
496
445
|
InferDependencies,
|
|
497
446
|
InferConfig,
|
|
498
|
-
} from "
|
|
447
|
+
} from "./core";
|
|
499
448
|
|
|
500
449
|
import { usersPlugin } from "./plugins/users";
|
|
501
450
|
|
|
@@ -94,7 +94,7 @@ plugins/<name>/
|
|
|
94
94
|
|
|
95
95
|
```ts
|
|
96
96
|
// plugins/<name>/index.ts
|
|
97
|
-
import { createPlugin } from "
|
|
97
|
+
import { createPlugin } from "../../core";
|
|
98
98
|
import type { DB } from "./schema"; // If using database
|
|
99
99
|
|
|
100
100
|
// Configuration type (if plugin is configurable)
|
|
@@ -144,7 +144,7 @@ routes/
|
|
|
144
144
|
|
|
145
145
|
```ts
|
|
146
146
|
// routes/users.ts
|
|
147
|
-
import { createRouter } from "
|
|
147
|
+
import { createRouter } from "../router";
|
|
148
148
|
import { z } from "zod";
|
|
149
149
|
|
|
150
150
|
export const usersRouter = createRouter("users")
|
|
@@ -173,144 +173,6 @@ export const usersRouter = createRouter("users")
|
|
|
173
173
|
|
|
174
174
|
---
|
|
175
175
|
|
|
176
|
-
## Advanced Route Organization
|
|
177
|
-
|
|
178
|
-
For larger projects, the starter template provides a more structured route organization:
|
|
179
|
-
|
|
180
|
-
```
|
|
181
|
-
routes/<namespace>/<route>/
|
|
182
|
-
├── index.ts # Route definition
|
|
183
|
-
├── schema.ts # Zod schemas (Input, Output)
|
|
184
|
-
├── models/
|
|
185
|
-
│ └── model.ts # Handler class
|
|
186
|
-
└── tests/
|
|
187
|
-
├── unit.test.ts # Unit tests
|
|
188
|
-
└── integ.test.ts # Integration tests
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
### Handler Class Pattern
|
|
192
|
-
|
|
193
|
-
Instead of inline handlers, use the `Handler<T>` interface with model classes:
|
|
194
|
-
|
|
195
|
-
```ts
|
|
196
|
-
// routes/health/ping/schema.ts
|
|
197
|
-
import { z } from "zod";
|
|
198
|
-
|
|
199
|
-
export const Input = z.object({ message: z.string() });
|
|
200
|
-
export const Output = z.object({ response: z.string() });
|
|
201
|
-
|
|
202
|
-
export type Input = z.infer<typeof Input>;
|
|
203
|
-
export type Output = z.infer<typeof Output>;
|
|
204
|
-
```
|
|
205
|
-
|
|
206
|
-
```ts
|
|
207
|
-
// routes/health/ping/models/model.ts
|
|
208
|
-
import type { Handler } from "@donkeylabs/server";
|
|
209
|
-
import type { Health } from "$server/routes"; // Generated types
|
|
210
|
-
import type { AppContext } from "$server/context";
|
|
211
|
-
|
|
212
|
-
export class PingModel implements Handler<Health.Ping> {
|
|
213
|
-
private ctx: AppContext;
|
|
214
|
-
|
|
215
|
-
constructor(ctx: AppContext) {
|
|
216
|
-
this.ctx = ctx;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
handle(input: Health.Ping.Input): Health.Ping.Output {
|
|
220
|
-
return { response: "pong" };
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
```ts
|
|
226
|
-
// routes/health/ping/index.ts
|
|
227
|
-
import { createRoute } from "@donkeylabs/server";
|
|
228
|
-
import { Input, Output } from "./schema";
|
|
229
|
-
import { PingModel } from "./models/model";
|
|
230
|
-
|
|
231
|
-
export const pingRoute = createRoute.typed({
|
|
232
|
-
input: Input,
|
|
233
|
-
output: Output,
|
|
234
|
-
handle: async (input, ctx) => new PingModel(ctx).handle(input),
|
|
235
|
-
});
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
This pattern separates concerns and makes testing easier.
|
|
239
|
-
|
|
240
|
-
---
|
|
241
|
-
|
|
242
|
-
## Path Aliases
|
|
243
|
-
|
|
244
|
-
Configure tsconfig.json path aliases for cleaner imports:
|
|
245
|
-
|
|
246
|
-
```json
|
|
247
|
-
{
|
|
248
|
-
"compilerOptions": {
|
|
249
|
-
"paths": {
|
|
250
|
-
"$server/*": [".@donkeylabs/server/*"],
|
|
251
|
-
"$api": [".@donkeylabs/server/client"]
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
**Usage:**
|
|
258
|
-
|
|
259
|
-
```ts
|
|
260
|
-
// Instead of relative paths
|
|
261
|
-
import type { AppContext } from "../../../.@donkeylabs/server/context";
|
|
262
|
-
|
|
263
|
-
// Use path aliases
|
|
264
|
-
import type { AppContext } from "$server/context";
|
|
265
|
-
import type { Health } from "$server/routes";
|
|
266
|
-
import { api } from "$api";
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
---
|
|
270
|
-
|
|
271
|
-
## Generated Files
|
|
272
|
-
|
|
273
|
-
The framework generates type definition files in `.@donkeylabs/server/`:
|
|
274
|
-
|
|
275
|
-
| File | Purpose |
|
|
276
|
-
|------|---------|
|
|
277
|
-
| `registry.d.ts` | Plugin handlers and middleware types |
|
|
278
|
-
| `context.d.ts` | Merged `AppContext` with all plugin schemas |
|
|
279
|
-
| `routes.d.ts` | Route type definitions for `Handler<T>` |
|
|
280
|
-
| `client.ts` | Type-safe API client |
|
|
281
|
-
|
|
282
|
-
**Important:**
|
|
283
|
-
- These files are auto-generated by `donkeylabs generate`
|
|
284
|
-
- Add `.@donkeylabs/` to `.gitignore`
|
|
285
|
-
- Regenerate after any plugin or route changes
|
|
286
|
-
- Never edit these files manually
|
|
287
|
-
|
|
288
|
-
---
|
|
289
|
-
|
|
290
|
-
## AppContext vs ServerContext
|
|
291
|
-
|
|
292
|
-
The framework provides two context types:
|
|
293
|
-
|
|
294
|
-
| Type | Source | Use When |
|
|
295
|
-
|------|--------|----------|
|
|
296
|
-
| `ServerContext` | Library export | Generic library code |
|
|
297
|
-
| `AppContext` | Generated | Application code with full typing |
|
|
298
|
-
|
|
299
|
-
```ts
|
|
300
|
-
// ServerContext - library type, uses PluginRegistry
|
|
301
|
-
import type { ServerContext } from "@donkeylabs/server";
|
|
302
|
-
|
|
303
|
-
// AppContext - generated, includes merged schemas
|
|
304
|
-
import type { AppContext } from "$server/context";
|
|
305
|
-
|
|
306
|
-
// In your handlers/models, prefer AppContext for full typing
|
|
307
|
-
export class MyModel {
|
|
308
|
-
constructor(private ctx: AppContext) {}
|
|
309
|
-
}
|
|
310
|
-
```
|
|
311
|
-
|
|
312
|
-
---
|
|
313
|
-
|
|
314
176
|
## Do's and Don'ts
|
|
315
177
|
|
|
316
178
|
### DO: Use the Plugin System
|
|
@@ -554,7 +416,7 @@ export async function down(db: Kysely<any>): Promise<void> {
|
|
|
554
416
|
|
|
555
417
|
```ts
|
|
556
418
|
// test/plugins/auth.test.ts
|
|
557
|
-
import { createTestHarness } from "
|
|
419
|
+
import { createTestHarness } from "../../harness";
|
|
558
420
|
import { authPlugin } from "../../plugins/auth";
|
|
559
421
|
|
|
560
422
|
describe("Auth Plugin", () => {
|
|
@@ -580,7 +442,7 @@ describe("Auth Plugin", () => {
|
|
|
580
442
|
|
|
581
443
|
```ts
|
|
582
444
|
// test/integration.test.ts
|
|
583
|
-
import { createTestHarness } from "
|
|
445
|
+
import { createTestHarness } from "../harness";
|
|
584
446
|
import { authPlugin } from "../plugins/auth";
|
|
585
447
|
import { ordersPlugin } from "../plugins/orders";
|
|
586
448
|
|
package/docs/rate-limiter.md
CHANGED
|
@@ -24,16 +24,8 @@ if (!result.allowed) {
|
|
|
24
24
|
|
|
25
25
|
```ts
|
|
26
26
|
interface RateLimiter {
|
|
27
|
-
// Manual rate limiting
|
|
28
27
|
check(key: string, limit: number, windowMs: number): Promise<RateLimitResult>;
|
|
29
28
|
reset(key: string): Promise<void>;
|
|
30
|
-
|
|
31
|
-
// Declarative rules (auto-enforced)
|
|
32
|
-
registerRule(pattern: string, rule: RateLimitRule): void;
|
|
33
|
-
registerRules(rules: Record<string, RateLimitRule>): void;
|
|
34
|
-
matchRoute(route: string): RateLimitRule | undefined;
|
|
35
|
-
checkRoute(route: string, ctx: { ip: string; user?: any }): Promise<RateLimitResult | null>;
|
|
36
|
-
listRules(): Array<{ pattern: string; limit: number; window: string | number }>;
|
|
37
29
|
}
|
|
38
30
|
|
|
39
31
|
interface RateLimitResult {
|
|
@@ -43,14 +35,6 @@ interface RateLimitResult {
|
|
|
43
35
|
resetAt: Date; // When window resets
|
|
44
36
|
retryAfter?: number; // Seconds until retry (if blocked)
|
|
45
37
|
}
|
|
46
|
-
|
|
47
|
-
interface RateLimitRule {
|
|
48
|
-
limit: number;
|
|
49
|
-
window: string | number; // "1m", "10s", or milliseconds
|
|
50
|
-
keyFn?: (ctx: { ip: string; route: string; user?: any }) => string;
|
|
51
|
-
skip?: (ctx: { ip: string; route: string; user?: any }) => boolean;
|
|
52
|
-
message?: string;
|
|
53
|
-
}
|
|
54
38
|
```
|
|
55
39
|
|
|
56
40
|
### Methods
|
|
@@ -59,11 +43,6 @@ interface RateLimitRule {
|
|
|
59
43
|
|--------|-------------|
|
|
60
44
|
| `check(key, limit, windowMs)` | Check and increment counter for key |
|
|
61
45
|
| `reset(key)` | Reset counter for key |
|
|
62
|
-
| `registerRule(pattern, rule)` | Register declarative rate limit rule |
|
|
63
|
-
| `registerRules(rules)` | Register multiple rules at once |
|
|
64
|
-
| `matchRoute(route)` | Get rule matching a route |
|
|
65
|
-
| `checkRoute(route, ctx)` | Check rate limit for route (auto-uses registered rules) |
|
|
66
|
-
| `listRules()` | List all registered rules |
|
|
67
46
|
|
|
68
47
|
---
|
|
69
48
|
|
|
@@ -87,117 +66,6 @@ router.route("protected").typed({
|
|
|
87
66
|
|
|
88
67
|
---
|
|
89
68
|
|
|
90
|
-
## Declarative Rate Limits
|
|
91
|
-
|
|
92
|
-
Register rate limit rules declaratively and let the framework enforce them automatically.
|
|
93
|
-
|
|
94
|
-
### Server-Level Registration
|
|
95
|
-
|
|
96
|
-
```ts
|
|
97
|
-
const server = new AppServer({ db });
|
|
98
|
-
|
|
99
|
-
// Register individual rules
|
|
100
|
-
server.registerRateLimit("auth.login", { limit: 5, window: "15m" });
|
|
101
|
-
server.registerRateLimit("api.*", { limit: 100, window: "1m" });
|
|
102
|
-
server.registerRateLimit("billing.*", { limit: 10, window: "1h" });
|
|
103
|
-
|
|
104
|
-
// Register multiple at once
|
|
105
|
-
server.registerRateLimits({
|
|
106
|
-
"uploads.create": { limit: 10, window: "1h" },
|
|
107
|
-
"exports.*": { limit: 5, window: "1h" },
|
|
108
|
-
});
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
### Pattern Matching
|
|
112
|
-
|
|
113
|
-
Rules support glob patterns:
|
|
114
|
-
|
|
115
|
-
```ts
|
|
116
|
-
// Exact match
|
|
117
|
-
server.registerRateLimit("auth.login", { limit: 5, window: "15m" });
|
|
118
|
-
|
|
119
|
-
// Wildcard - matches api.users.list, api.posts.create, etc.
|
|
120
|
-
server.registerRateLimit("api.*", { limit: 100, window: "1m" });
|
|
121
|
-
|
|
122
|
-
// Exact matches take precedence over patterns
|
|
123
|
-
server.registerRateLimit("api.search", { limit: 20, window: "1m" }); // More restrictive for search
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
### Skip Function
|
|
127
|
-
|
|
128
|
-
Bypass rate limits for certain requests:
|
|
129
|
-
|
|
130
|
-
```ts
|
|
131
|
-
server.registerRateLimit("api.*", {
|
|
132
|
-
limit: 100,
|
|
133
|
-
window: "1m",
|
|
134
|
-
skip: (ctx) => {
|
|
135
|
-
// Skip for admins
|
|
136
|
-
return ctx.user?.role === "admin";
|
|
137
|
-
},
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
server.registerRateLimit("webhooks.*", {
|
|
141
|
-
limit: 1000,
|
|
142
|
-
window: "1m",
|
|
143
|
-
skip: (ctx) => {
|
|
144
|
-
// Skip for internal IPs
|
|
145
|
-
return ctx.ip.startsWith("10.0.");
|
|
146
|
-
},
|
|
147
|
-
});
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
### Custom Key Function
|
|
151
|
-
|
|
152
|
-
Control how rate limits are grouped:
|
|
153
|
-
|
|
154
|
-
```ts
|
|
155
|
-
server.registerRateLimit("api.*", {
|
|
156
|
-
limit: 100,
|
|
157
|
-
window: "1m",
|
|
158
|
-
keyFn: (ctx) => {
|
|
159
|
-
// Rate limit per user instead of per IP
|
|
160
|
-
return ctx.user?.id ? `user:${ctx.user.id}` : `ip:${ctx.ip}`;
|
|
161
|
-
},
|
|
162
|
-
});
|
|
163
|
-
```
|
|
164
|
-
|
|
165
|
-
### Auto-Enforcement
|
|
166
|
-
|
|
167
|
-
Registered rules are automatically checked before route handlers. If a request exceeds the limit, a 429 response is returned:
|
|
168
|
-
|
|
169
|
-
```json
|
|
170
|
-
{
|
|
171
|
-
"error": "TOO_MANY_REQUESTS",
|
|
172
|
-
"retryAfter": 45
|
|
173
|
-
}
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
With headers:
|
|
177
|
-
```
|
|
178
|
-
Retry-After: 45
|
|
179
|
-
X-RateLimit-Limit: 100
|
|
180
|
-
X-RateLimit-Remaining: 0
|
|
181
|
-
X-RateLimit-Reset: 2024-01-15T12:35:00.000Z
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
### Introspection
|
|
185
|
-
|
|
186
|
-
```ts
|
|
187
|
-
// Check which rule applies to a route
|
|
188
|
-
const rule = ctx.core.rateLimiter.matchRoute("api.users.list");
|
|
189
|
-
// { limit: 100, window: "1m", ... }
|
|
190
|
-
|
|
191
|
-
// List all registered rules
|
|
192
|
-
const rules = ctx.core.rateLimiter.listRules();
|
|
193
|
-
// [
|
|
194
|
-
// { pattern: "auth.login", limit: 5, window: "15m" },
|
|
195
|
-
// { pattern: "api.*", limit: 100, window: "1m" },
|
|
196
|
-
// ]
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
---
|
|
200
|
-
|
|
201
69
|
## Usage Examples
|
|
202
70
|
|
|
203
71
|
### Basic Rate Limiting
|
|
@@ -379,7 +247,7 @@ router.middleware
|
|
|
379
247
|
Convert duration strings to milliseconds:
|
|
380
248
|
|
|
381
249
|
```ts
|
|
382
|
-
import { parseDuration } from "
|
|
250
|
+
import { parseDuration } from "./core/rate-limiter";
|
|
383
251
|
|
|
384
252
|
parseDuration("100ms"); // 100
|
|
385
253
|
parseDuration("30s"); // 30000
|
|
@@ -393,7 +261,7 @@ parseDuration("1d"); // 86400000
|
|
|
393
261
|
Build consistent rate limit keys:
|
|
394
262
|
|
|
395
263
|
```ts
|
|
396
|
-
import { createRateLimitKey } from "
|
|
264
|
+
import { createRateLimitKey } from "./core/rate-limiter";
|
|
397
265
|
|
|
398
266
|
const key = createRateLimitKey("api.users.list", "192.168.1.1");
|
|
399
267
|
// "ratelimit:api.users.list:192.168.1.1"
|
|
@@ -404,7 +272,7 @@ const key = createRateLimitKey("api.users.list", "192.168.1.1");
|
|
|
404
272
|
Manual IP extraction if needed:
|
|
405
273
|
|
|
406
274
|
```ts
|
|
407
|
-
import { extractClientIP } from "
|
|
275
|
+
import { extractClientIP } from "./core/rate-limiter";
|
|
408
276
|
|
|
409
277
|
const ip = extractClientIP(req, socketAddr);
|
|
410
278
|
```
|
|
@@ -555,7 +423,7 @@ interface RateLimitAdapter {
|
|
|
555
423
|
### Redis Adapter Example
|
|
556
424
|
|
|
557
425
|
```ts
|
|
558
|
-
import { createRateLimiter, type RateLimitAdapter } from "
|
|
426
|
+
import { createRateLimiter, type RateLimitAdapter } from "./core/rate-limiter";
|
|
559
427
|
import Redis from "ioredis";
|
|
560
428
|
|
|
561
429
|
class RedisRateLimitAdapter implements RateLimitAdapter {
|
package/docs/router.md
CHANGED
|
@@ -5,7 +5,7 @@ Fluent API for defining type-safe routes with handler selection and middleware c
|
|
|
5
5
|
## Quick Start
|
|
6
6
|
|
|
7
7
|
```ts
|
|
8
|
-
import { createRouter } from "
|
|
8
|
+
import { createRouter } from "./router";
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
|
|
11
11
|
const router = createRouter("api")
|
|
@@ -166,18 +166,10 @@ router.route("example").typed({
|
|
|
166
166
|
// Plugin services
|
|
167
167
|
const data = await ctx.plugins.myPlugin.getData();
|
|
168
168
|
|
|
169
|
-
// Core services
|
|
169
|
+
// Core services
|
|
170
170
|
ctx.core.logger.info("Processing request", { input });
|
|
171
171
|
const cached = await ctx.core.cache.get("key");
|
|
172
172
|
|
|
173
|
-
// Error factories
|
|
174
|
-
if (!users.length) {
|
|
175
|
-
throw ctx.errors.NotFound("No users found");
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Application config
|
|
179
|
-
console.log(ctx.config.env); // "development" | "production"
|
|
180
|
-
|
|
181
173
|
// Request info
|
|
182
174
|
console.log(ctx.ip); // Client IP
|
|
183
175
|
console.log(ctx.requestId); // Unique request ID
|
|
@@ -475,18 +467,18 @@ router.route("stream").raw({
|
|
|
475
467
|
Register routes with the server:
|
|
476
468
|
|
|
477
469
|
```ts
|
|
478
|
-
import { AppServer } from "
|
|
470
|
+
import { AppServer } from "./server";
|
|
479
471
|
import { userRouter } from "./routes/users";
|
|
480
472
|
import { orderRouter } from "./routes/orders";
|
|
481
473
|
|
|
482
474
|
const server = new AppServer({ db, port: 3000 });
|
|
483
475
|
|
|
484
476
|
// Register single router
|
|
485
|
-
server.
|
|
477
|
+
server.use(userRouter);
|
|
486
478
|
|
|
487
479
|
// Register multiple routers
|
|
488
|
-
server.
|
|
489
|
-
server.
|
|
480
|
+
server.use(userRouter);
|
|
481
|
+
server.use(orderRouter);
|
|
490
482
|
|
|
491
483
|
await server.start();
|
|
492
484
|
```
|