@coderbuzz/ken 0.1.5 → 0.2.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/README.md +683 -56
- package/dist/README.md +683 -56
- package/dist/{deno-LZU5JBGL.js → deno-DV3633IH.js} +8 -2
- package/dist/deno-DV3633IH.js.map +1 -0
- package/dist/index.d.ts +15 -17
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/package.json +1 -1
- package/package.json +1 -1
- package/dist/deno-LZU5JBGL.js.map +0 -1
package/README.md
CHANGED
|
@@ -25,8 +25,8 @@ built-in WebSocket support.
|
|
|
25
25
|
the box.
|
|
26
26
|
- **Schema Validation**: Validate request params, query, headers, cookies, and
|
|
27
27
|
body with inline schemas.
|
|
28
|
-
- **Built-in Middleware**: JWT, CORS, sessions, compression, rate
|
|
29
|
-
secure headers, and more.
|
|
28
|
+
- **Built-in Middleware**: JWT, JWK/JWKS, CORS, sessions, compression, rate
|
|
29
|
+
limiting, secure headers, and more.
|
|
30
30
|
- **WebSocket Support**: Real-time connections with pub/sub, ping/pong, and
|
|
31
31
|
typed upgrade data.
|
|
32
32
|
- **Performance-Driven**: Minimal overhead engineered for high throughput and
|
|
@@ -49,12 +49,11 @@ npm install @coderbuzz/ken
|
|
|
49
49
|
import { AppServer } from "npm:@coderbuzz/ken";
|
|
50
50
|
```
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
```
|
|
52
|
+
> **Node.js**: The package ships as ESM. Your project must have
|
|
53
|
+
> `"type": "module"` in `package.json`, or use the `.mjs` extension. Node.js 18+
|
|
54
|
+
> required. If you write your app in TypeScript, use a runtime that supports it
|
|
55
|
+
> natively (Bun, Deno) or a transpiler such as [tsx](https://tsx.is) / `tsc` for
|
|
56
|
+
> Node.js.
|
|
58
57
|
|
|
59
58
|
---
|
|
60
59
|
|
|
@@ -73,14 +72,38 @@ console.log(`Listening on ${hostname}:${port}`);
|
|
|
73
72
|
|
|
74
73
|
---
|
|
75
74
|
|
|
75
|
+
## App vs AppServer
|
|
76
|
+
|
|
77
|
+
| Class | Purpose |
|
|
78
|
+
| ----------- | ----------------------------------------------------------------------------- |
|
|
79
|
+
| `App` | Pure router — no server lifecycle. Used for sub-apps and modular composition. |
|
|
80
|
+
| `AppServer` | `App` + `run()` / `stop()`. The entry point for a server process. |
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
import { App, AppServer } from "@coderbuzz/ken";
|
|
84
|
+
|
|
85
|
+
// Root server
|
|
86
|
+
const app = new AppServer({ port: 3000, hostname: "0.0.0.0" });
|
|
87
|
+
|
|
88
|
+
// Sub-app (pure router)
|
|
89
|
+
const api = new App();
|
|
90
|
+
api.get("/users", handler);
|
|
91
|
+
app.use("/api/v1", api);
|
|
92
|
+
|
|
93
|
+
const { hostname, port } = await app.run();
|
|
94
|
+
await app.stop(); // graceful shutdown
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
76
99
|
## Routing
|
|
77
100
|
|
|
78
101
|
### Static Routes
|
|
79
102
|
|
|
80
103
|
```ts
|
|
81
|
-
app.get("/", "Hello Ken!");
|
|
104
|
+
app.get("/", "Hello Ken!"); // string → text/plain
|
|
82
105
|
app.get("/health", "OK");
|
|
83
|
-
app.get("/version", { version: "1.0.0" }); //
|
|
106
|
+
app.get("/version", { version: "1.0.0" }); // object → JSON-serialized
|
|
84
107
|
```
|
|
85
108
|
|
|
86
109
|
### Dynamic Params
|
|
@@ -109,17 +132,37 @@ app.get("/files/*", (ctx) => new Response(`File: ${ctx.params["*"]}`));
|
|
|
109
132
|
### HTTP Methods
|
|
110
133
|
|
|
111
134
|
```ts
|
|
135
|
+
app.get("/items", handler);
|
|
112
136
|
app.post("/items", handler);
|
|
113
137
|
app.put("/items/:id", handler);
|
|
114
138
|
app.patch("/items/:id", handler);
|
|
115
139
|
app.delete("/items/:id", handler);
|
|
140
|
+
app.head("/items", handler);
|
|
141
|
+
app.options("/items", handler);
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Route Introspection
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
// Returns RouteInfo[] — { method, path }[]
|
|
148
|
+
const routes = app.getRoutes();
|
|
149
|
+
|
|
150
|
+
// Print a colored table to console
|
|
151
|
+
app.printRoutes();
|
|
152
|
+
// ┌──────────┬────────────────────┐
|
|
153
|
+
// │ Method │ Path │
|
|
154
|
+
// ├──────────┼────────────────────┤
|
|
155
|
+
// │ GET │ / │
|
|
156
|
+
// │ POST │ /users │
|
|
157
|
+
// │ WS │ /chat │
|
|
158
|
+
// └──────────┴────────────────────┘
|
|
116
159
|
```
|
|
117
160
|
|
|
118
161
|
---
|
|
119
162
|
|
|
120
163
|
## Schema Validation
|
|
121
164
|
|
|
122
|
-
Ken validates request data inline via the schema object (
|
|
165
|
+
Ken validates request data inline via the schema object (second argument before
|
|
123
166
|
the handler). Use
|
|
124
167
|
[`@coderbuzz/kyo`](https://www.npmjs.com/package/@coderbuzz/kyo) for schema
|
|
125
168
|
builders.
|
|
@@ -128,6 +171,7 @@ builders.
|
|
|
128
171
|
import {
|
|
129
172
|
boolean,
|
|
130
173
|
coerce,
|
|
174
|
+
date,
|
|
131
175
|
number,
|
|
132
176
|
object,
|
|
133
177
|
optional,
|
|
@@ -218,6 +262,21 @@ app.post("/api/submit", {
|
|
|
218
262
|
});
|
|
219
263
|
```
|
|
220
264
|
|
|
265
|
+
### Date Validation
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
app.post("/api/register", {
|
|
269
|
+
json: object({
|
|
270
|
+
born: coerce(
|
|
271
|
+
date({ min: new Date("1900-01-01"), max: new Date("2025-12-31") }),
|
|
272
|
+
),
|
|
273
|
+
}),
|
|
274
|
+
}, async (ctx) => {
|
|
275
|
+
const { born } = await ctx.json;
|
|
276
|
+
return Response.json({ born: born.toISOString() });
|
|
277
|
+
});
|
|
278
|
+
```
|
|
279
|
+
|
|
221
280
|
---
|
|
222
281
|
|
|
223
282
|
## Middleware & State
|
|
@@ -268,11 +327,15 @@ app.post("/admin/action", {
|
|
|
268
327
|
if ((ctx.state as any).auth.role !== "admin") {
|
|
269
328
|
throw new Response("Forbidden", { status: 403 });
|
|
270
329
|
}
|
|
330
|
+
// void return — not included in ctx.state type
|
|
271
331
|
},
|
|
272
332
|
},
|
|
273
333
|
}, () => Response.json({ message: "Action performed" }));
|
|
274
334
|
```
|
|
275
335
|
|
|
336
|
+
> **Note:** Middleware with `void` return types are excluded from `ctx.state`.
|
|
337
|
+
> Only middleware that returns a value contributes to the state type.
|
|
338
|
+
|
|
276
339
|
### `onFinish` Callback
|
|
277
340
|
|
|
278
341
|
```ts
|
|
@@ -294,7 +357,8 @@ app.get("/with-logging", {
|
|
|
294
357
|
|
|
295
358
|
### `define()` — Scoped Middleware
|
|
296
359
|
|
|
297
|
-
Apply middleware to a group of routes with full type inference
|
|
360
|
+
Apply middleware to a group of routes with full type inference. Routes inside
|
|
361
|
+
the callback automatically inherit the state type:
|
|
298
362
|
|
|
299
363
|
```ts
|
|
300
364
|
app.define(
|
|
@@ -316,16 +380,33 @@ app.define(
|
|
|
316
380
|
);
|
|
317
381
|
```
|
|
318
382
|
|
|
383
|
+
`define()` can be nested for layered composition:
|
|
384
|
+
|
|
385
|
+
```ts
|
|
386
|
+
app.define({ requestId: () => crypto.randomUUID() }, (app) => {
|
|
387
|
+
app.define({ timestamp: () => Date.now() }, (app) => {
|
|
388
|
+
app.get("/meta", (ctx) =>
|
|
389
|
+
Response.json({
|
|
390
|
+
id: ctx.state.requestId,
|
|
391
|
+
ts: ctx.state.timestamp,
|
|
392
|
+
}));
|
|
393
|
+
});
|
|
394
|
+
});
|
|
395
|
+
```
|
|
396
|
+
|
|
319
397
|
### `apply()` — Global Middleware
|
|
320
398
|
|
|
321
399
|
```ts
|
|
322
|
-
// Side-effect middleware (logging, metrics)
|
|
400
|
+
// Side-effect middleware (logging, metrics) — no state produced
|
|
323
401
|
app.apply("/*", (ctx) => {
|
|
324
402
|
console.log(ctx.method, ctx.url);
|
|
325
403
|
});
|
|
326
404
|
|
|
327
|
-
// State-producing middleware
|
|
405
|
+
// State-producing middleware — added to ctx.state for all matching routes
|
|
328
406
|
app.apply("/*", { auth: (ctx) => verifyAuth(ctx) });
|
|
407
|
+
|
|
408
|
+
// Scoped to a prefix
|
|
409
|
+
app.apply("/api/*", { apiVersion: () => "v1" });
|
|
329
410
|
```
|
|
330
411
|
|
|
331
412
|
### `use()` — Mount Sub-Apps
|
|
@@ -335,7 +416,10 @@ const api = new App();
|
|
|
335
416
|
api.get("/users", handler);
|
|
336
417
|
api.get("/posts", handler);
|
|
337
418
|
|
|
338
|
-
app.use("/api/v1", api);
|
|
419
|
+
app.use("/api/v1", api); // mounts at /api/v1/users and /api/v1/posts
|
|
420
|
+
|
|
421
|
+
// Without prefix — routes merged at root
|
|
422
|
+
app.use(api);
|
|
339
423
|
```
|
|
340
424
|
|
|
341
425
|
---
|
|
@@ -363,14 +447,24 @@ app.get("/login", (ctx) => {
|
|
|
363
447
|
});
|
|
364
448
|
return new Response("Logged in");
|
|
365
449
|
});
|
|
450
|
+
|
|
451
|
+
// Multiple cookies
|
|
452
|
+
app.get("/setup", (ctx) => {
|
|
453
|
+
ctx.setCookie("theme", "dark");
|
|
454
|
+
ctx.setCookie("lang", "en", { path: "/", maxAge: 86400 });
|
|
455
|
+
return new Response("OK");
|
|
456
|
+
});
|
|
366
457
|
```
|
|
367
458
|
|
|
459
|
+
All cookie options: `path`, `domain`, `maxAge`, `expires`, `httpOnly`, `secure`,
|
|
460
|
+
`sameSite` (`'Strict' | 'Lax' | 'None'`).
|
|
461
|
+
|
|
368
462
|
---
|
|
369
463
|
|
|
370
464
|
## Error Handling
|
|
371
465
|
|
|
372
466
|
```ts
|
|
373
|
-
//
|
|
467
|
+
// App-level error handler — fallback for all routes
|
|
374
468
|
app.onError((error, ctx) => {
|
|
375
469
|
console.error(ctx.method, ctx.url, error);
|
|
376
470
|
return Response.json(
|
|
@@ -386,37 +480,134 @@ app.notFound((ctx) => {
|
|
|
386
480
|
return Response.json({ error: "Not Found", path: ctx.url }, { status: 404 });
|
|
387
481
|
});
|
|
388
482
|
|
|
483
|
+
// Route-level onError — takes priority over app-level
|
|
484
|
+
app.get("/validate", {
|
|
485
|
+
onError: (error, ctx) => {
|
|
486
|
+
return Response.json({ custom: true, message: String(error) }, {
|
|
487
|
+
status: 422,
|
|
488
|
+
});
|
|
489
|
+
},
|
|
490
|
+
}, () => {
|
|
491
|
+
throw new Error("validation failed");
|
|
492
|
+
});
|
|
493
|
+
|
|
389
494
|
// Throw a Response to short-circuit with a specific status
|
|
390
|
-
app.get("/secret", (
|
|
495
|
+
app.get("/secret", () => {
|
|
391
496
|
throw new Response("Forbidden", { status: 403 });
|
|
392
497
|
});
|
|
393
498
|
```
|
|
394
499
|
|
|
500
|
+
### Error Propagation Rules
|
|
501
|
+
|
|
502
|
+
1. **Route-level `onError`** takes priority over app-level.
|
|
503
|
+
2. **Thrown `Response`** instances short-circuit the chain and are returned
|
|
504
|
+
as-is (the `onError` handler receives the `Response` as the error value).
|
|
505
|
+
3. Sub-app `onError` is inherited by routes mounted via `app.use()`.
|
|
506
|
+
4. Middleware errors are caught by the route's `onError` (or app-level fallback)
|
|
507
|
+
— they behave the same as handler errors.
|
|
508
|
+
|
|
509
|
+
### Sub-App Error Scoping
|
|
510
|
+
|
|
511
|
+
```ts
|
|
512
|
+
const subApp = new App();
|
|
513
|
+
subApp.onError((error) => Response.json({ appError: true }, { status: 400 }));
|
|
514
|
+
subApp.get("/fail", () => {
|
|
515
|
+
throw new Error("fail");
|
|
516
|
+
});
|
|
517
|
+
// Route-level overrides sub-app error handler
|
|
518
|
+
subApp.get("/custom", {
|
|
519
|
+
onError: () => Response.json({ routeError: true }, { status: 418 }),
|
|
520
|
+
}, () => {
|
|
521
|
+
throw new Error("custom");
|
|
522
|
+
});
|
|
523
|
+
app.use("/sub", subApp);
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### Custom Not Found Handlers
|
|
527
|
+
|
|
528
|
+
```ts
|
|
529
|
+
// Sub-app scoped 404 (only matches /nf-api/* paths)
|
|
530
|
+
const apiApp = new App();
|
|
531
|
+
apiApp.get("/items", () => Response.json([{ id: 1 }]));
|
|
532
|
+
apiApp.notFound((ctx) =>
|
|
533
|
+
Response.json({ error: "API not found", path: ctx.url }, { status: 404 })
|
|
534
|
+
);
|
|
535
|
+
app.use("/nf-api", apiApp);
|
|
536
|
+
|
|
537
|
+
// define()-scoped notFound (inherits middleware state)
|
|
538
|
+
app.define({ reqUser: () => "user123" }, (app) => {
|
|
539
|
+
app.get("/dashboard", (ctx) => Response.json({ user: ctx.state.reqUser }));
|
|
540
|
+
app.notFound((ctx) =>
|
|
541
|
+
Response.json({ error: "Not Found", user: ctx.state.reqUser }, {
|
|
542
|
+
status: 404,
|
|
543
|
+
})
|
|
544
|
+
);
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
// Global fallback
|
|
548
|
+
app.notFound((ctx) =>
|
|
549
|
+
Response.json({ error: "Global not found", method: ctx.method }, {
|
|
550
|
+
status: 404,
|
|
551
|
+
})
|
|
552
|
+
);
|
|
553
|
+
```
|
|
554
|
+
|
|
395
555
|
---
|
|
396
556
|
|
|
397
557
|
## Built-in Middleware
|
|
398
558
|
|
|
559
|
+
All middleware are used via the `state` key in the schema object (per-route) or
|
|
560
|
+
via `define()` / `apply()` (scoped). They return typed values into `ctx.state`.
|
|
561
|
+
|
|
399
562
|
### CORS
|
|
400
563
|
|
|
564
|
+
`cors()` returns an `App` instance. Mount it with `app.use()`:
|
|
565
|
+
|
|
401
566
|
```ts
|
|
402
567
|
import { cors } from "@coderbuzz/ken";
|
|
403
568
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
569
|
+
// CORS for specific routes — define routes inside the cors app
|
|
570
|
+
const apiCors = cors({ origin: "https://example.com", credentials: true });
|
|
571
|
+
apiCors.get("/data", () => Response.json({ data: 1 }));
|
|
572
|
+
app.use("/api", apiCors);
|
|
573
|
+
|
|
574
|
+
// CORS with wildcard (all origins)
|
|
575
|
+
const publicCors = cors({ origin: "*" });
|
|
576
|
+
publicCors.get("/", () => Response.json({ public: true }));
|
|
577
|
+
app.use("/public", publicCors);
|
|
578
|
+
|
|
579
|
+
// CORS with dynamic origin resolver
|
|
580
|
+
const dynamicCors = cors({
|
|
581
|
+
origin: (requestOrigin, ctx) => {
|
|
582
|
+
const allowed = ["https://app.example.com", "https://admin.example.com"];
|
|
583
|
+
return allowed.includes(requestOrigin) ? requestOrigin : allowed[0];
|
|
584
|
+
},
|
|
585
|
+
allowMethods: ["GET", "POST"],
|
|
586
|
+
allowHeaders: ["Content-Type", "Authorization"],
|
|
587
|
+
exposeHeaders: ["X-Request-Id"],
|
|
588
|
+
maxAge: 86400,
|
|
589
|
+
credentials: true,
|
|
590
|
+
});
|
|
407
591
|
```
|
|
408
592
|
|
|
409
593
|
### JWT
|
|
410
594
|
|
|
411
595
|
```ts
|
|
412
|
-
import { jwt, signJwt } from "@coderbuzz/ken";
|
|
596
|
+
import { decodeJwt, jwt, signJwt, verifyJwt } from "@coderbuzz/ken";
|
|
413
597
|
|
|
414
598
|
// Sign a token
|
|
415
599
|
app.get("/token", async () => {
|
|
416
|
-
const token = await signJwt(
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
600
|
+
const token = await signJwt(
|
|
601
|
+
{
|
|
602
|
+
sub: "user123",
|
|
603
|
+
iss: "my-app",
|
|
604
|
+
aud: "my-api",
|
|
605
|
+
exp: Math.floor(Date.now() / 1000) + 3600,
|
|
606
|
+
iat: Math.floor(Date.now() / 1000),
|
|
607
|
+
},
|
|
608
|
+
"secret",
|
|
609
|
+
"HS256", // default — also supports HS384, HS512
|
|
610
|
+
);
|
|
420
611
|
return Response.json({ token });
|
|
421
612
|
});
|
|
422
613
|
|
|
@@ -424,6 +615,46 @@ app.get("/token", async () => {
|
|
|
424
615
|
app.get("/secure", {
|
|
425
616
|
state: { auth: jwt({ secret: "secret" }) },
|
|
426
617
|
}, (ctx) => Response.json({ payload: ctx.state.auth }));
|
|
618
|
+
|
|
619
|
+
// With claims validation
|
|
620
|
+
app.get("/strict", {
|
|
621
|
+
state: {
|
|
622
|
+
auth: jwt({
|
|
623
|
+
secret: "secret",
|
|
624
|
+
issuer: "my-app",
|
|
625
|
+
audience: "my-api",
|
|
626
|
+
clockTolerance: 5, // seconds
|
|
627
|
+
}),
|
|
628
|
+
},
|
|
629
|
+
}, (ctx) => Response.json({ sub: ctx.state.auth.sub }));
|
|
630
|
+
```
|
|
631
|
+
|
|
632
|
+
### JWK / JWKS (RSA, ECDSA)
|
|
633
|
+
|
|
634
|
+
```ts
|
|
635
|
+
import { jwk } from "@coderbuzz/ken";
|
|
636
|
+
|
|
637
|
+
// Verify tokens against a JWKS endpoint (e.g., Auth0, Cognito)
|
|
638
|
+
app.get("/secure", {
|
|
639
|
+
state: {
|
|
640
|
+
auth: jwk({
|
|
641
|
+
jwksUrl: "https://your-auth-provider/.well-known/jwks.json",
|
|
642
|
+
issuer: "https://your-auth-provider/",
|
|
643
|
+
audience: "your-api-identifier",
|
|
644
|
+
cacheTtl: 600_000, // 10 minutes
|
|
645
|
+
}),
|
|
646
|
+
},
|
|
647
|
+
}, (ctx) => Response.json({ payload: ctx.state.auth }));
|
|
648
|
+
|
|
649
|
+
// With pre-loaded keys (no HTTP fetch)
|
|
650
|
+
app.get("/secure2", {
|
|
651
|
+
state: {
|
|
652
|
+
auth: jwk({
|
|
653
|
+
keys: [myRsaJwk],
|
|
654
|
+
issuer: "https://issuer.example.com",
|
|
655
|
+
}),
|
|
656
|
+
},
|
|
657
|
+
}, (ctx) => Response.json({ payload: ctx.state.auth }));
|
|
427
658
|
```
|
|
428
659
|
|
|
429
660
|
### Session
|
|
@@ -431,6 +662,7 @@ app.get("/secure", {
|
|
|
431
662
|
```ts
|
|
432
663
|
import { session } from "@coderbuzz/ken";
|
|
433
664
|
|
|
665
|
+
// Sync — in-memory lookup
|
|
434
666
|
const userSession = session({
|
|
435
667
|
cookieName: "_sid",
|
|
436
668
|
validate: (cookieValue) => {
|
|
@@ -438,6 +670,23 @@ const userSession = session({
|
|
|
438
670
|
if (!user?.active) throw new Response("Unauthorized", { status: 401 });
|
|
439
671
|
return user;
|
|
440
672
|
},
|
|
673
|
+
// Optional: custom unauthorized response
|
|
674
|
+
onUnauthorized: (ctx) => new Response("Please log in", { status: 401 }),
|
|
675
|
+
});
|
|
676
|
+
|
|
677
|
+
// Async — database lookup or encrypted cookie
|
|
678
|
+
const encryptedSession = session({
|
|
679
|
+
cookieName: "_ak",
|
|
680
|
+
validate: async (cookieValue, ctx) => {
|
|
681
|
+
const apiKey = await decryptString(cookieValue, secretKey);
|
|
682
|
+
if (!MEDIA_OWNERS[apiKey]?.active) {
|
|
683
|
+
throw new Response(null, {
|
|
684
|
+
status: 302,
|
|
685
|
+
headers: { Location: "/auth?redirect=" + ctx.url },
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
return apiKey;
|
|
689
|
+
},
|
|
441
690
|
});
|
|
442
691
|
|
|
443
692
|
app.get("/dashboard", {
|
|
@@ -450,9 +699,23 @@ app.get("/dashboard", {
|
|
|
450
699
|
```ts
|
|
451
700
|
import { basicAuth } from "@coderbuzz/ken";
|
|
452
701
|
|
|
702
|
+
// Static credentials
|
|
453
703
|
app.get("/admin", {
|
|
454
704
|
state: { auth: basicAuth({ username: "admin", password: "secret" }) },
|
|
455
705
|
}, (ctx) => Response.json({ user: ctx.state.auth.username }));
|
|
706
|
+
|
|
707
|
+
// Custom verification function
|
|
708
|
+
app.get("/custom-admin", {
|
|
709
|
+
state: {
|
|
710
|
+
auth: basicAuth({
|
|
711
|
+
verifyUser: async (username, password, ctx) => {
|
|
712
|
+
return username === "admin" &&
|
|
713
|
+
password === await getHashedPassword(username);
|
|
714
|
+
},
|
|
715
|
+
realm: "Admin Area",
|
|
716
|
+
}),
|
|
717
|
+
},
|
|
718
|
+
}, (ctx) => Response.json({ user: ctx.state.auth.username }));
|
|
456
719
|
```
|
|
457
720
|
|
|
458
721
|
### Bearer Auth
|
|
@@ -460,17 +723,48 @@ app.get("/admin", {
|
|
|
460
723
|
```ts
|
|
461
724
|
import { bearerAuth } from "@coderbuzz/ken";
|
|
462
725
|
|
|
726
|
+
// Single token
|
|
463
727
|
app.get("/api/resource", {
|
|
464
728
|
state: { auth: bearerAuth({ token: "my-token" }) },
|
|
465
729
|
}, (ctx) => Response.json({ token: ctx.state.auth.token }));
|
|
730
|
+
|
|
731
|
+
// Multiple valid tokens
|
|
732
|
+
app.get("/api/multi", {
|
|
733
|
+
state: { auth: bearerAuth({ token: ["token-a", "token-b"] }) },
|
|
734
|
+
}, (ctx) => Response.json({ token: ctx.state.auth.token }));
|
|
735
|
+
|
|
736
|
+
// Custom verification
|
|
737
|
+
app.get("/api/dynamic", {
|
|
738
|
+
state: {
|
|
739
|
+
auth: bearerAuth({
|
|
740
|
+
verifyToken: async (token, ctx) => {
|
|
741
|
+
return await verifyInDatabase(token);
|
|
742
|
+
},
|
|
743
|
+
}),
|
|
744
|
+
},
|
|
745
|
+
}, (ctx) => Response.json({ token: ctx.state.auth.token }));
|
|
466
746
|
```
|
|
467
747
|
|
|
468
748
|
### Logger
|
|
469
749
|
|
|
750
|
+
`logger()` returns an `App` instance. Mount it with `app.use()`:
|
|
751
|
+
|
|
470
752
|
```ts
|
|
471
753
|
import { logger } from "@coderbuzz/ken";
|
|
472
754
|
|
|
755
|
+
// Global logger (all routes)
|
|
473
756
|
app.use(logger());
|
|
757
|
+
|
|
758
|
+
// Custom log function
|
|
759
|
+
app.use(logger({ logFn: (msg) => myLogService.info(msg) }));
|
|
760
|
+
|
|
761
|
+
// Custom format
|
|
762
|
+
app.use(logger({
|
|
763
|
+
format: ({ method, url, status, duration }) =>
|
|
764
|
+
`[${
|
|
765
|
+
new Date().toISOString()
|
|
766
|
+
}] ${method} ${url} → ${status} (${duration}ms)`,
|
|
767
|
+
}));
|
|
474
768
|
```
|
|
475
769
|
|
|
476
770
|
### Request ID
|
|
@@ -481,6 +775,18 @@ import { requestId } from "@coderbuzz/ken";
|
|
|
481
775
|
app.get("/request", {
|
|
482
776
|
state: { reqId: requestId() },
|
|
483
777
|
}, (ctx) => Response.json({ id: ctx.state.reqId }));
|
|
778
|
+
// Response header: X-Request-Id: <uuid>
|
|
779
|
+
|
|
780
|
+
// Custom options
|
|
781
|
+
app.get("/request", {
|
|
782
|
+
state: {
|
|
783
|
+
reqId: requestId({
|
|
784
|
+
requestHeaderName: "X-Correlation-Id", // reuse incoming ID if present
|
|
785
|
+
headerName: "X-Request-Id",
|
|
786
|
+
generator: () => `req-${Date.now()}`,
|
|
787
|
+
}),
|
|
788
|
+
},
|
|
789
|
+
}, (ctx) => Response.json({ id: ctx.state.reqId }));
|
|
484
790
|
```
|
|
485
791
|
|
|
486
792
|
### Secure Headers
|
|
@@ -488,11 +794,29 @@ app.get("/request", {
|
|
|
488
794
|
```ts
|
|
489
795
|
import { secureHeaders } from "@coderbuzz/ken";
|
|
490
796
|
|
|
797
|
+
// Use sensible defaults (Helmet-inspired)
|
|
798
|
+
app.define({ sec: secureHeaders() }, (app) => {
|
|
799
|
+
app.get("/", () => new Response("secure"));
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
// Customize individual headers
|
|
491
803
|
app.get("/page", {
|
|
492
|
-
state: {
|
|
804
|
+
state: {
|
|
805
|
+
sec: secureHeaders({
|
|
806
|
+
xFrameOptions: "DENY",
|
|
807
|
+
contentSecurityPolicy: "default-src 'self'",
|
|
808
|
+
crossOriginOpenerPolicy: false, // disable a header
|
|
809
|
+
}),
|
|
810
|
+
},
|
|
493
811
|
}, () => new Response("secure"));
|
|
494
812
|
```
|
|
495
813
|
|
|
814
|
+
Default headers set: `Cross-Origin-Opener-Policy`,
|
|
815
|
+
`Cross-Origin-Resource-Policy`, `Origin-Agent-Cluster`, `Referrer-Policy`,
|
|
816
|
+
`Strict-Transport-Security`, `X-Content-Type-Options`, `X-DNS-Prefetch-Control`,
|
|
817
|
+
`X-Download-Options`, `X-Frame-Options`, `X-Permitted-Cross-Domain-Policies`,
|
|
818
|
+
`X-XSS-Protection`. Also removes `X-Powered-By`.
|
|
819
|
+
|
|
496
820
|
### Compression
|
|
497
821
|
|
|
498
822
|
```ts
|
|
@@ -500,20 +824,44 @@ import { compress } from "@coderbuzz/ken";
|
|
|
500
824
|
|
|
501
825
|
app.get("/data", {
|
|
502
826
|
state: { encoding: compress() },
|
|
503
|
-
}, (ctx) =>
|
|
827
|
+
}, (ctx) => {
|
|
828
|
+
// ctx.state.encoding.encoding is 'gzip' | 'deflate' | 'br' | null
|
|
829
|
+
return Response.json({ data: largePayload });
|
|
830
|
+
});
|
|
504
831
|
```
|
|
505
832
|
|
|
833
|
+
Note: Ken negotiates the encoding preference; actual body compression is handled
|
|
834
|
+
by the runtime (Bun, Deno) or a reverse proxy.
|
|
835
|
+
|
|
506
836
|
### Cache
|
|
507
837
|
|
|
508
838
|
```ts
|
|
509
839
|
import { cache } from "@coderbuzz/ken";
|
|
510
840
|
|
|
841
|
+
// Cache for 1 hour (CDN-friendly)
|
|
511
842
|
app.get("/static", {
|
|
512
843
|
state: { caching: cache({ maxAge: 3600, public: true }) },
|
|
513
844
|
}, () => new Response("cached content"));
|
|
845
|
+
|
|
846
|
+
// No caching
|
|
847
|
+
app.get("/dynamic", {
|
|
848
|
+
state: { caching: cache({ noStore: true }) },
|
|
849
|
+
}, () => Response.json({ time: Date.now() }));
|
|
850
|
+
|
|
851
|
+
// Advanced CDN directives
|
|
852
|
+
app.get("/cdn", {
|
|
853
|
+
state: {
|
|
854
|
+
caching: cache({
|
|
855
|
+
maxAge: 60,
|
|
856
|
+
sMaxAge: 3600,
|
|
857
|
+
staleWhileRevalidate: 300,
|
|
858
|
+
vary: "Accept-Language",
|
|
859
|
+
}),
|
|
860
|
+
},
|
|
861
|
+
}, () => new Response("cdn content"));
|
|
514
862
|
```
|
|
515
863
|
|
|
516
|
-
###
|
|
864
|
+
### Body Limit
|
|
517
865
|
|
|
518
866
|
```ts
|
|
519
867
|
import { bodyLimit } from "@coderbuzz/ken";
|
|
@@ -533,7 +881,13 @@ import { timeout } from "@coderbuzz/ken";
|
|
|
533
881
|
|
|
534
882
|
app.get("/slow", {
|
|
535
883
|
state: { timeoutSig: timeout({ duration: 5000 }) },
|
|
536
|
-
}, () =>
|
|
884
|
+
}, async (ctx) => {
|
|
885
|
+
// Pass the AbortSignal to downstream fetch calls
|
|
886
|
+
const data = await fetch("https://api.example.com/data", {
|
|
887
|
+
signal: ctx.state.timeoutSig.signal,
|
|
888
|
+
});
|
|
889
|
+
return Response.json(await data.json());
|
|
890
|
+
});
|
|
537
891
|
```
|
|
538
892
|
|
|
539
893
|
### Timing
|
|
@@ -544,6 +898,15 @@ import { timing } from "@coderbuzz/ken";
|
|
|
544
898
|
app.get("/timed", {
|
|
545
899
|
state: { perf: timing() },
|
|
546
900
|
}, () => new Response("timed"));
|
|
901
|
+
// Response header: Server-Timing: total;dur=2.50
|
|
902
|
+
|
|
903
|
+
// Custom metric name and description
|
|
904
|
+
app.get("/timed-custom", {
|
|
905
|
+
state: {
|
|
906
|
+
perf: timing({ name: "app", description: "Application processing" }),
|
|
907
|
+
},
|
|
908
|
+
}, () => new Response("timed"));
|
|
909
|
+
// Response header: Server-Timing: app;desc="Application processing";dur=2.50
|
|
547
910
|
```
|
|
548
911
|
|
|
549
912
|
### IP Restriction
|
|
@@ -551,9 +914,15 @@ app.get("/timed", {
|
|
|
551
914
|
```ts
|
|
552
915
|
import { ipRestriction } from "@coderbuzz/ken";
|
|
553
916
|
|
|
917
|
+
// Allow list
|
|
554
918
|
app.get("/internal", {
|
|
555
919
|
state: { ipCheck: ipRestriction({ allowList: ["127.0.0.1", "::1"] }) },
|
|
556
920
|
}, () => new Response("allowed"));
|
|
921
|
+
|
|
922
|
+
// Deny list
|
|
923
|
+
app.get("/public", {
|
|
924
|
+
state: { ipCheck: ipRestriction({ denyList: ["10.0.0.1"] }) },
|
|
925
|
+
}, () => new Response("public content"));
|
|
557
926
|
```
|
|
558
927
|
|
|
559
928
|
### CSRF Protection
|
|
@@ -561,11 +930,22 @@ app.get("/internal", {
|
|
|
561
930
|
```ts
|
|
562
931
|
import { csrf } from "@coderbuzz/ken";
|
|
563
932
|
|
|
933
|
+
// Same-origin only (auto-detect)
|
|
934
|
+
app.post("/form", {
|
|
935
|
+
state: { protection: csrf() },
|
|
936
|
+
}, () => Response.json({ success: true }));
|
|
937
|
+
|
|
938
|
+
// Specific origins
|
|
564
939
|
app.post("/form", {
|
|
565
940
|
state: { protection: csrf({ origin: ["https://example.com"] }) },
|
|
566
941
|
}, () => Response.json({ success: true }));
|
|
567
942
|
```
|
|
568
943
|
|
|
944
|
+
Note: CSRF only applies to unsafe methods (POST, PUT, PATCH, DELETE) with
|
|
945
|
+
form-compatible content types. JSON API requests
|
|
946
|
+
(`Content-Type:
|
|
947
|
+
application/json`) are automatically skipped.
|
|
948
|
+
|
|
569
949
|
### ETag
|
|
570
950
|
|
|
571
951
|
```ts
|
|
@@ -619,6 +999,15 @@ app.ws("/chat", {
|
|
|
619
999
|
close(peer, code, reason) {
|
|
620
1000
|
console.log("disconnected", code, reason);
|
|
621
1001
|
},
|
|
1002
|
+
error(peer, error) {
|
|
1003
|
+
console.error("ws error", error);
|
|
1004
|
+
},
|
|
1005
|
+
ping(peer, data) {
|
|
1006
|
+
console.log("ping received");
|
|
1007
|
+
},
|
|
1008
|
+
pong(peer, data) {
|
|
1009
|
+
console.log("pong received — peer alive");
|
|
1010
|
+
},
|
|
622
1011
|
});
|
|
623
1012
|
```
|
|
624
1013
|
|
|
@@ -630,7 +1019,7 @@ app.ws<{ userId: string }>("/auth", {
|
|
|
630
1019
|
const url = new URL(req.url);
|
|
631
1020
|
const userId = url.searchParams.get("userId");
|
|
632
1021
|
if (!userId) return new Response("Unauthorized", { status: 401 });
|
|
633
|
-
return { userId };
|
|
1022
|
+
return { userId }; // becomes peer.data
|
|
634
1023
|
},
|
|
635
1024
|
open(peer) {
|
|
636
1025
|
peer.send(`Hello ${peer.data.userId}`);
|
|
@@ -650,10 +1039,11 @@ app.ws("/chat", {
|
|
|
650
1039
|
peer.publish("chat", "someone joined");
|
|
651
1040
|
},
|
|
652
1041
|
message(peer, message) {
|
|
653
|
-
peer.publish("chat", message);
|
|
654
|
-
peer.send(`you: ${message}`);
|
|
1042
|
+
peer.publish("chat", message); // broadcast to all except sender
|
|
1043
|
+
peer.send(`you: ${message}`); // echo to sender
|
|
655
1044
|
},
|
|
656
1045
|
close(peer) {
|
|
1046
|
+
peer.unsubscribe("chat");
|
|
657
1047
|
peer.publish("chat", "someone left");
|
|
658
1048
|
},
|
|
659
1049
|
});
|
|
@@ -661,6 +1051,9 @@ app.ws("/chat", {
|
|
|
661
1051
|
|
|
662
1052
|
### WsTopicHub (App-Level Pub/Sub)
|
|
663
1053
|
|
|
1054
|
+
Use `WsTopicHub` for pub/sub across all runtimes, including cross-topic
|
|
1055
|
+
broadcasts from HTTP routes or background tasks:
|
|
1056
|
+
|
|
664
1057
|
```ts
|
|
665
1058
|
import { WsTopicHub } from "@coderbuzz/ken";
|
|
666
1059
|
|
|
@@ -674,7 +1067,7 @@ app.ws("/notifications", {
|
|
|
674
1067
|
hub.unsubscribeAll(peer);
|
|
675
1068
|
},
|
|
676
1069
|
message(peer, _msg) {
|
|
677
|
-
hub.markAlive(peer);
|
|
1070
|
+
hub.markAlive(peer); // heartbeat acknowledgment
|
|
678
1071
|
},
|
|
679
1072
|
});
|
|
680
1073
|
|
|
@@ -689,14 +1082,114 @@ app.post("/broadcast", async (ctx) => {
|
|
|
689
1082
|
### Ping/Pong (Heartbeat)
|
|
690
1083
|
|
|
691
1084
|
```ts
|
|
692
|
-
app.ws(
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
1085
|
+
app.ws(
|
|
1086
|
+
"/live",
|
|
1087
|
+
{
|
|
1088
|
+
pong(peer) {
|
|
1089
|
+
console.log(`${peer.remoteAddress} is alive`);
|
|
1090
|
+
},
|
|
1091
|
+
message(peer, msg) {
|
|
1092
|
+
peer.send(msg);
|
|
1093
|
+
},
|
|
698
1094
|
},
|
|
699
|
-
|
|
1095
|
+
{ pingInterval: 30, pongTimeout: 10 }, // WsOptions
|
|
1096
|
+
);
|
|
1097
|
+
```
|
|
1098
|
+
|
|
1099
|
+
### WsOptions
|
|
1100
|
+
|
|
1101
|
+
| Option | Type | Default | Description |
|
|
1102
|
+
| ------------------- | --------- | ------------ | ------------------------------------------ |
|
|
1103
|
+
| `maxPayloadLength` | `number` | `16_777_216` | Max message size in bytes (16 MB) |
|
|
1104
|
+
| `backpressureLimit` | `number` | `16_777_216` | Max send buffer size (16 MB) |
|
|
1105
|
+
| `pingInterval` | `number` | `30` | Seconds between server ping frames |
|
|
1106
|
+
| `pongTimeout` | `number` | `10` | Seconds to wait for pong before closing |
|
|
1107
|
+
| `perMessageDeflate` | `boolean` | `false` | Enable per-message compression |
|
|
1108
|
+
| `idleTimeout` | `number` | `120` | Seconds before idle connections are closed |
|
|
1109
|
+
|
|
1110
|
+
---
|
|
1111
|
+
|
|
1112
|
+
## WebSocket Client (WSClient)
|
|
1113
|
+
|
|
1114
|
+
Ken includes a built-in fault-tolerant WebSocket client with the Ken Binary
|
|
1115
|
+
WebSocket Protocol (KBWP) — 80–93% bandwidth reduction over JSON for protocol
|
|
1116
|
+
messages.
|
|
1117
|
+
|
|
1118
|
+
```ts
|
|
1119
|
+
import { WSClient } from "@coderbuzz/ken";
|
|
1120
|
+
|
|
1121
|
+
const client = new WSClient("wss://api.example.com/ws", {
|
|
1122
|
+
token: "my-jwt", // auth token
|
|
1123
|
+
authMode: "message", // 'message' (binary, default) or 'query' (?token=)
|
|
1124
|
+
heartbeatInterval: 30_000, // ms between pings
|
|
1125
|
+
requestTimeout: 10_000, // sendWait() timeout
|
|
1126
|
+
maxRetries: Infinity, // reconnect attempts
|
|
1127
|
+
onConnect: () => console.log("Connected"),
|
|
1128
|
+
onDisconnect: (code, reason) => console.log("Disconnected", code),
|
|
1129
|
+
onReconnectAttempt: (attempt, delay) =>
|
|
1130
|
+
console.log(`Retry #${attempt} in ${delay}ms`),
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
await client.connect();
|
|
1134
|
+
|
|
1135
|
+
// Fire-and-forget
|
|
1136
|
+
client.send("hello");
|
|
1137
|
+
|
|
1138
|
+
// Request-response (binary REQUEST frame + correlation ID)
|
|
1139
|
+
const result = await client.sendWait({ type: "getData", id: 42 });
|
|
1140
|
+
|
|
1141
|
+
// Pub/sub
|
|
1142
|
+
client.subscribe("events", (data) => console.log("Event:", data));
|
|
1143
|
+
client.publish("events", { action: "click" });
|
|
1144
|
+
client.unsubscribe("events");
|
|
1145
|
+
|
|
1146
|
+
await client.close();
|
|
1147
|
+
```
|
|
1148
|
+
|
|
1149
|
+
### wsClientProtocol — Server-Side Counterpart
|
|
1150
|
+
|
|
1151
|
+
```ts
|
|
1152
|
+
import { wsClientProtocol } from "@coderbuzz/ken";
|
|
1153
|
+
|
|
1154
|
+
// Without auth
|
|
1155
|
+
app.use(
|
|
1156
|
+
"/chat",
|
|
1157
|
+
wsClientProtocol({
|
|
1158
|
+
pingInterval: 20,
|
|
1159
|
+
idleTimeout: 60,
|
|
1160
|
+
upgrade(req) {
|
|
1161
|
+
const name = new URL(req.url).searchParams.get("name") ?? "anon";
|
|
1162
|
+
return { username: name };
|
|
1163
|
+
},
|
|
1164
|
+
open(peer) {
|
|
1165
|
+
peer.subscribe("chat");
|
|
1166
|
+
},
|
|
1167
|
+
message(peer, msg) {
|
|
1168
|
+
peer.send(`echo: ${msg}`);
|
|
1169
|
+
},
|
|
1170
|
+
close(peer) {
|
|
1171
|
+
peer.unsubscribe("chat");
|
|
1172
|
+
},
|
|
1173
|
+
}),
|
|
1174
|
+
);
|
|
1175
|
+
|
|
1176
|
+
// With post-connect binary auth (secure)
|
|
1177
|
+
app.use(
|
|
1178
|
+
"/ws",
|
|
1179
|
+
wsClientProtocol<{ userId: string }>({
|
|
1180
|
+
authenticate: async (token) => {
|
|
1181
|
+
const user = await verifyJwt(token, "secret");
|
|
1182
|
+
return user ? { userId: String(user.sub) } : null;
|
|
1183
|
+
},
|
|
1184
|
+
tokenParam: "token", // also accept ?token= query param
|
|
1185
|
+
open(peer) {
|
|
1186
|
+
console.log("authed:", peer.data.userId);
|
|
1187
|
+
},
|
|
1188
|
+
message(peer, msg) {
|
|
1189
|
+
peer.send(msg);
|
|
1190
|
+
},
|
|
1191
|
+
}),
|
|
1192
|
+
);
|
|
700
1193
|
```
|
|
701
1194
|
|
|
702
1195
|
---
|
|
@@ -722,54 +1215,154 @@ app.get("/stream", () => {
|
|
|
722
1215
|
|
|
723
1216
|
```ts
|
|
724
1217
|
import {
|
|
1218
|
+
getMimeType,
|
|
725
1219
|
listDirectory,
|
|
726
1220
|
receiveFiles,
|
|
727
1221
|
saveFile,
|
|
728
1222
|
sendFile,
|
|
729
1223
|
} from "@coderbuzz/ken";
|
|
1224
|
+
```
|
|
1225
|
+
|
|
1226
|
+
### Serve a File
|
|
730
1227
|
|
|
731
|
-
|
|
1228
|
+
```ts
|
|
1229
|
+
// Basic
|
|
732
1230
|
app.get("/download", (ctx) => sendFile(ctx, "./assets/file.pdf"));
|
|
733
1231
|
|
|
734
|
-
//
|
|
1232
|
+
// With full options (enables ETag, Range, conditional requests)
|
|
1233
|
+
app.get(
|
|
1234
|
+
"/download/:name",
|
|
1235
|
+
(ctx) =>
|
|
1236
|
+
sendFile(ctx, `./uploads/${ctx.params.name}`, {
|
|
1237
|
+
download: true, // trigger browser download
|
|
1238
|
+
// download: "custom-name.pdf", // download with custom filename
|
|
1239
|
+
cacheControl: "public, max-age=3600",
|
|
1240
|
+
reqHeaders: ctx.headers, // enables ETag/Range/If-Modified-Since
|
|
1241
|
+
}),
|
|
1242
|
+
);
|
|
1243
|
+
```
|
|
1244
|
+
|
|
1245
|
+
`sendFile` supports ETag / If-None-Match (→ 304), Last-Modified /
|
|
1246
|
+
If-Modified-Since (→ 304), and Range requests (→ 206 Partial Content). On Bun,
|
|
1247
|
+
uses `Bun.file()` for zero-copy sendfile.
|
|
1248
|
+
|
|
1249
|
+
### List Directory
|
|
1250
|
+
|
|
1251
|
+
```ts
|
|
735
1252
|
app.get("/files", (ctx) => listDirectory(ctx, "./uploads"));
|
|
736
1253
|
|
|
737
|
-
//
|
|
1254
|
+
// With options
|
|
1255
|
+
app.get("/files", async (ctx) => {
|
|
1256
|
+
return listDirectory(ctx, "./uploads", {
|
|
1257
|
+
recursive: true,
|
|
1258
|
+
maxDepth: 3,
|
|
1259
|
+
stats: true, // include size and modifiedAt (default: true)
|
|
1260
|
+
filter: (entry) => !entry.name.startsWith("."),
|
|
1261
|
+
});
|
|
1262
|
+
});
|
|
1263
|
+
```
|
|
1264
|
+
|
|
1265
|
+
Each entry: `{ name, path, isDirectory, size, modifiedAt }`.
|
|
1266
|
+
|
|
1267
|
+
### Receive Uploaded Files
|
|
1268
|
+
|
|
1269
|
+
```ts
|
|
738
1270
|
app.post("/upload", async (ctx) => {
|
|
739
|
-
const files = await receiveFiles(ctx
|
|
1271
|
+
const files = await receiveFiles(ctx, {
|
|
1272
|
+
maxFileSize: 5_000_000,
|
|
1273
|
+
maxFiles: 10,
|
|
1274
|
+
allowedTypes: ["image/png", "image/jpeg"],
|
|
1275
|
+
fields: ["avatar", "banner"],
|
|
1276
|
+
});
|
|
740
1277
|
for (const file of files) {
|
|
1278
|
+
// file: { fieldName, fileName, type, size, data: ArrayBuffer }
|
|
741
1279
|
await saveFile(file, "./uploads");
|
|
742
1280
|
}
|
|
743
1281
|
return Response.json({ count: files.length });
|
|
744
1282
|
});
|
|
745
1283
|
```
|
|
746
1284
|
|
|
1285
|
+
### MIME Type Detection
|
|
1286
|
+
|
|
1287
|
+
```ts
|
|
1288
|
+
getMimeType("photo.jpg"); // 'image/jpeg'
|
|
1289
|
+
getMimeType(".css"); // 'text/css; charset=utf-8'
|
|
1290
|
+
getMimeType("data.bin"); // 'application/octet-stream'
|
|
1291
|
+
```
|
|
1292
|
+
|
|
747
1293
|
---
|
|
748
1294
|
|
|
749
1295
|
## Utilities
|
|
750
1296
|
|
|
1297
|
+
### Encryption (AES-GCM)
|
|
1298
|
+
|
|
751
1299
|
```ts
|
|
752
1300
|
import {
|
|
753
|
-
compressString,
|
|
754
|
-
decompressString,
|
|
755
1301
|
decryptString,
|
|
756
1302
|
encryptString,
|
|
757
1303
|
generateSecretKey,
|
|
758
|
-
getPathname,
|
|
759
|
-
memoize,
|
|
760
1304
|
} from "@coderbuzz/ken";
|
|
761
1305
|
|
|
762
|
-
//
|
|
763
|
-
const key =
|
|
764
|
-
|
|
765
|
-
|
|
1306
|
+
// Generate a random 256-bit key (sync)
|
|
1307
|
+
const key = generateSecretKey(); // base64-encoded string
|
|
1308
|
+
|
|
1309
|
+
// Encrypt / decrypt
|
|
1310
|
+
const encrypted = await encryptString("hello world", key);
|
|
1311
|
+
const original = await decryptString(encrypted, key);
|
|
1312
|
+
```
|
|
1313
|
+
|
|
1314
|
+
Uses AES-256-GCM via Web Crypto API with LRU key caching. Output is
|
|
1315
|
+
base64-encoded `IV (12 bytes) + ciphertext`.
|
|
766
1316
|
|
|
767
|
-
|
|
768
|
-
|
|
1317
|
+
### Compression
|
|
1318
|
+
|
|
1319
|
+
```ts
|
|
1320
|
+
import { compressString, decompressString } from "@coderbuzz/ken";
|
|
1321
|
+
|
|
1322
|
+
// Default: gzip (standard Web API, compatible with all runtimes)
|
|
1323
|
+
const compressed = await compressString("large text payload...");
|
|
769
1324
|
const original = await decompressString(compressed);
|
|
770
1325
|
|
|
771
|
-
//
|
|
772
|
-
const
|
|
1326
|
+
// Custom encoding and quality level
|
|
1327
|
+
const gz = await compressString("text", { encoding: "gzip", level: 9 });
|
|
1328
|
+
const plain = await decompressString(gz, { encoding: "gzip" });
|
|
1329
|
+
```
|
|
1330
|
+
|
|
1331
|
+
Supported encodings: `'gzip'` (default), `'deflate'`, `'deflate-raw'`.
|
|
1332
|
+
gzip/deflate levels: 0–9.
|
|
1333
|
+
|
|
1334
|
+
### Memoization
|
|
1335
|
+
|
|
1336
|
+
```ts
|
|
1337
|
+
import { memoize } from "@coderbuzz/ken";
|
|
1338
|
+
|
|
1339
|
+
// Sync — auto-detected
|
|
1340
|
+
const expensive = memoize((id: string) => computeResult(id));
|
|
1341
|
+
expensive("abc"); // computes
|
|
1342
|
+
expensive("abc"); // cached
|
|
1343
|
+
|
|
1344
|
+
// Async — auto-detected, with thundering herd protection
|
|
1345
|
+
const fetchUser = memoize(
|
|
1346
|
+
async (id: string) => db.users.findById(id),
|
|
1347
|
+
{ ttl: 30_000, maxSize: 500 },
|
|
1348
|
+
);
|
|
1349
|
+
|
|
1350
|
+
// Multi-arg with custom key resolver
|
|
1351
|
+
const multi = memoize(
|
|
1352
|
+
(a: number, b: number) => a + b,
|
|
1353
|
+
{ key: (a, b) => `${a}:${b}` },
|
|
1354
|
+
);
|
|
1355
|
+
```
|
|
1356
|
+
|
|
1357
|
+
Options: `maxSize` (default: 256), `ttl` (ms, default: 0 = no expiry), `key`
|
|
1358
|
+
(custom key resolver).
|
|
1359
|
+
|
|
1360
|
+
### URL Utilities
|
|
1361
|
+
|
|
1362
|
+
```ts
|
|
1363
|
+
import { getPathname } from "@coderbuzz/ken";
|
|
1364
|
+
|
|
1365
|
+
getPathname("https://example.com/api/v1?q=1"); // '/api/v1'
|
|
773
1366
|
```
|
|
774
1367
|
|
|
775
1368
|
---
|
|
@@ -784,6 +1377,16 @@ if (isDeno) console.log("Running on Deno");
|
|
|
784
1377
|
if (isNode) console.log("Running on Node.js");
|
|
785
1378
|
```
|
|
786
1379
|
|
|
1380
|
+
### Node.js with uWebSockets.js
|
|
1381
|
+
|
|
1382
|
+
For maximum performance on Node.js, install `uWebSockets.js` and set the `UWS`
|
|
1383
|
+
environment variable:
|
|
1384
|
+
|
|
1385
|
+
```sh
|
|
1386
|
+
npm install uWebSockets.js
|
|
1387
|
+
UWS=1 node --import tsx/esm server.ts
|
|
1388
|
+
```
|
|
1389
|
+
|
|
787
1390
|
---
|
|
788
1391
|
|
|
789
1392
|
## Remote Info
|
|
@@ -795,6 +1398,30 @@ app.get("/ip", (ctx) => {
|
|
|
795
1398
|
});
|
|
796
1399
|
```
|
|
797
1400
|
|
|
1401
|
+
Ken checks proxy headers in order: `CF-Connecting-IP`, `X-Real-IP`,
|
|
1402
|
+
`X-Forwarded-For`, `True-Client-IP` — then falls back to the raw socket address.
|
|
1403
|
+
|
|
1404
|
+
---
|
|
1405
|
+
|
|
1406
|
+
## Context API Reference
|
|
1407
|
+
|
|
1408
|
+
| Property | Type | Description |
|
|
1409
|
+
| ----------------------------------- | ----------------------------------- | ---------------------------------- |
|
|
1410
|
+
| `ctx.url` | `string` | Full request URL |
|
|
1411
|
+
| `ctx.method` | `string` | HTTP method (uppercase) |
|
|
1412
|
+
| `ctx.params` | `Record<string, string>` (or typed) | Route path parameters |
|
|
1413
|
+
| `ctx.query` | `Record<string, string>` (or typed) | URL query string parameters |
|
|
1414
|
+
| `ctx.headers` | `Record<string, string>` (or typed) | Request headers (lowercase keys) |
|
|
1415
|
+
| `ctx.cookies` | `Record<string, string>` (or typed) | Request cookies |
|
|
1416
|
+
| `ctx.json` | `Promise<any>` (or typed) | Parsed JSON body |
|
|
1417
|
+
| `ctx.text` | `Promise<string>` (or typed) | Raw text body |
|
|
1418
|
+
| `ctx.form` | `Promise<FormData>` (or typed) | Parsed form data body |
|
|
1419
|
+
| `ctx.body` | `any` | Raw body stream (runtime-specific) |
|
|
1420
|
+
| `ctx.state` | typed | State produced by middleware |
|
|
1421
|
+
| `ctx.remoteInfo` | `{ address: string; port: number }` | Client IP and port |
|
|
1422
|
+
| `ctx.setCookie(name, value, opts?)` | `void` | Set a response cookie |
|
|
1423
|
+
| `ctx.onFinish(cb)` | `void` | Register post-response callback |
|
|
1424
|
+
|
|
798
1425
|
---
|
|
799
1426
|
|
|
800
1427
|
## License
|