skyguard-js 1.2.1 → 1.2.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.
Files changed (39) hide show
  1. package/README.md +9 -683
  2. package/dist/app.d.ts +14 -9
  3. package/dist/app.js +27 -24
  4. package/dist/http/context.d.ts +115 -0
  5. package/dist/http/context.js +147 -0
  6. package/dist/http/httpAdapter.d.ts +4 -4
  7. package/dist/http/index.d.ts +1 -0
  8. package/dist/http/index.js +3 -1
  9. package/dist/http/nodeNativeHttp.d.ts +4 -4
  10. package/dist/http/nodeNativeHttp.js +11 -4
  11. package/dist/http/request.d.ts +4 -0
  12. package/dist/http/request.js +8 -0
  13. package/dist/http/response.d.ts +21 -2
  14. package/dist/http/response.js +30 -2
  15. package/dist/index.d.ts +2 -2
  16. package/dist/index.js +2 -2
  17. package/dist/middlewares/cors.d.ts +10 -4
  18. package/dist/middlewares/cors.js +35 -18
  19. package/dist/middlewares/csrf.js +33 -33
  20. package/dist/middlewares/index.d.ts +1 -1
  21. package/dist/middlewares/rateLimiter.d.ts +58 -6
  22. package/dist/middlewares/rateLimiter.js +149 -40
  23. package/dist/middlewares/session.js +4 -4
  24. package/dist/routing/routeResolveFunc.d.ts +10 -0
  25. package/dist/routing/routeResolveFunc.js +21 -0
  26. package/dist/routing/router.d.ts +16 -10
  27. package/dist/routing/router.js +32 -25
  28. package/dist/routing/routerGroup.d.ts +10 -5
  29. package/dist/routing/routerGroup.js +11 -10
  30. package/dist/storage/storage.d.ts +3 -3
  31. package/dist/storage/storage.js +7 -7
  32. package/dist/storage/types.d.ts +5 -5
  33. package/dist/storage/uploader.d.ts +9 -9
  34. package/dist/storage/uploader.js +62 -62
  35. package/dist/types/index.d.ts +11 -10
  36. package/dist/validators/validationSchema.js +8 -8
  37. package/package.json +2 -2
  38. package/dist/helpers/http.d.ts +0 -95
  39. package/dist/helpers/http.js +0 -112
package/README.md CHANGED
@@ -31,7 +31,7 @@ Skyguard.js currently delivers a solid core that includes **routing**, **type-sa
31
31
  - HTTP routing by method (GET, POST, PUT, PATCH, DELETE)
32
32
  - Route groups with prefixes
33
33
  - Global, group, and route-level middlewares
34
- - Request / Response abstractions
34
+ - Unified `Context` abstraction (`ctx.req` + response helpers)
35
35
  - Declarative data validation
36
36
  - Support for template motors (handlebars, pugs, ejs, etc.)
37
37
  - Built-in HTTP exceptions
@@ -73,700 +73,22 @@ npm install skyguard-js
73
73
  ## 🏁 Quick Start
74
74
 
75
75
  ```ts
76
- import { createApp, Response } from "skyguard-js";
76
+ import { createApp } from "skyguard-js";
77
77
 
78
78
  const app = createApp();
79
79
 
80
- const PORT = 3000;
80
+ app.get("/", ctx => ctx.json({ status: "ok" }));
81
81
 
82
- app.get("/health", () => {
83
- return Response.json({ status: "ok" });
84
- });
85
-
86
- app.run(PORT, () => {
87
- console.log(`Server running in port: http://localhost:${PORT}`);
88
- });
89
- ```
90
-
91
- ---
92
-
93
- ## 🛣️ Routing
94
-
95
- Routes are registered using HTTP methods on the `app` instance.
96
-
97
- ```ts
98
- app.get("/posts/{id}", (request: Request) => {
99
- return Response.json(request.params);
100
- });
101
-
102
- app.post("/posts", (request: Request) => {
103
- return Response.json(request.data);
104
- });
105
- ```
106
-
107
- Internally, the framework maps HTTP methods to route layers using an optimized routing table.
108
-
109
- ---
110
-
111
- ## 🧩 Route Groups
112
-
113
- Route groups allow you to organize endpoints under a shared prefix.
114
-
115
- ```ts
116
- app.group("/api", api => {
117
- api.get("/users", () => res.json({ message: "Users" }));
118
- api.get("/products", () => res.json({ message: "Products" }));
119
- });
120
- ```
121
-
122
- ---
123
-
124
- ## 🛠️ Middlewares
125
-
126
- Middlewares can be registered **globally**, **per group**, or **per route**.
127
-
128
- ```ts
129
- import { Request, Response, json, RouteHandler } from "skyguard-js";
130
-
131
- const authMiddleware = async (
132
- request: Request,
133
- next: RouteHandler,
134
- ): Promise<Response> => {
135
- if (request.headers["authorization"] !== "secret") {
136
- return json({ message: "Unauthorized" }).setStatus(401);
137
- }
138
-
139
- return next(request);
140
- };
141
-
142
- // Global middleware
143
- app.middlewares(authMiddleware);
144
-
145
- // Group middleware
146
- app.group("/admin", admin => {
147
- admin.middlewares(authMiddleware);
148
- admin.get("/dashboard", () => json({ ok: true }));
149
- });
150
-
151
- // Route-level middleware
152
- app.get("/secure", () => json({ secure: true }), [authMiddleware]);
82
+ app.run();
153
83
  ```
154
84
 
155
85
  ---
156
86
 
157
- ## 🌐 CORS Middleware
158
-
159
- To enable CORS, use the built-in `cors` middleware.
160
-
161
- ```ts
162
- import { cors, HttpMethods } from "skyguard-js";
163
-
164
- app.middlewares(
165
- cors({
166
- origin: ["http://localhost:3000", "https://myapp.com"],
167
- methods: [HttpMethods.get, HttpMethods.post],
168
- allowedHeaders: ["Content-Type", "Authorization"],
169
- credentials: true,
170
- }),
171
- );
172
- ```
173
-
174
- ---
175
-
176
- ## 🛡️ CSRF Middleware
177
-
178
- Use the built-in `csrf` middleware to protect endpoints against CSRF attacks.
179
-
180
- ```ts
181
- import { csrf, json } from "skyguard-js";
182
-
183
- app.middlewares(
184
- csrf({
185
- cookieName: "XSRF-TOKEN",
186
- headerNames: ["x-csrf-token"],
187
- }),
188
- );
189
-
190
- app.post("/transfer", () => {
191
- return json({ ok: true });
192
- });
193
- ```
194
-
195
- The middleware follows a hardened **double-submit cookie** strategy:
196
-
197
- - It issues a CSRF cookie when missing (including first GET/HEAD/OPTIONS and failed protected requests).
198
- - For state-changing requests (POST/PUT/PATCH/DELETE), it validates the token from header/body against the cookie value.
199
- - It validates `Origin`/`Referer` for protected requests (and requires `Referer` on HTTPS when `Origin` is missing).
200
- - It rejects duplicated CSRF header values to avoid ambiguous token parsing.
201
-
202
- ### Example: CSRF token in HTML templates (Express Handlebars)
203
-
204
- When you render server-side HTML, you can pass the CSRF token to your template and include it as a hidden field in forms.
205
-
206
- ```ts
207
- import { createApp, csrf, render, json } from "skyguard-js";
208
- import { engine } from "express-handlebars";
209
- import { join } from "node:path";
210
-
211
- const app = createApp();
212
-
213
- app.views(__dirname, "views");
214
- app.engineTemplates(
215
- "hbs",
216
- engine({
217
- extname: "hbs",
218
- layoutsDir: join(__dirname, "views"),
219
- defaultLayout: "main",
220
- }),
221
- );
222
-
223
- app.middlewares(
224
- csrf({
225
- cookieName: "XSRF-TOKEN",
226
- headerNames: ["x-csrf-token"],
227
- }),
228
- );
229
-
230
- app.get("/transfer", request => {
231
- return render("transfer", {
232
- csrfToken: request.cookies["XSRF-TOKEN"],
233
- });
234
- });
235
-
236
- app.post("/transfer", request => {
237
- // If middleware passes, token is valid
238
- return json({ ok: true, amount: request.body.amount });
239
- });
240
- ```
241
-
242
- `views/transfer.hbs`:
243
-
244
- ```hbs
245
- <form action="/transfer" method="POST">
246
- <input type="hidden" name="csrf" value="{{csrfToken}}" />
247
- <input type="number" name="amount" />
248
- <button type="submit">Send</button>
249
- </form>
250
- ```
251
-
252
- For `fetch`/AJAX requests, send the same token in headers:
253
-
254
- ```html
255
- <script>
256
- const csrfToken = "{{csrfToken}}";
257
-
258
- async function sendTransfer() {
259
- await fetch("/transfer", {
260
- method: "POST",
261
- headers: {
262
- "Content-Type": "application/json",
263
- "x-csrf-token": csrfToken,
264
- },
265
- body: JSON.stringify({ amount: 150 }),
266
- });
267
- }
268
- </script>
269
- ```
270
-
271
- ---
272
-
273
- ## 🚦 Rate Limit Middleware
274
-
275
- You can limit requests with the built-in `rateLimit` middleware.
276
-
277
- ```ts
278
- import { rateLimit, Response } from "skyguard-js";
279
-
280
- const apiRateLimit = rateLimit({
281
- windowMs: 60_000, // 1 minute
282
- max: 100,
283
- message: "Too many requests from this IP",
284
- });
285
-
286
- app.get(
287
- "/api/users",
288
- () => {
289
- return Response.json([{ id: 1 }]);
290
- },
291
- [apiRateLimit],
292
- );
293
- ```
294
-
295
- ---
296
-
297
- ## 📌 Static Files
298
-
299
- To serve static files, use the application's `staticFiles` method with the directory path. The name of the folder will determine the initial route prefix.
300
-
301
- ```ts
302
- import { join } from "node:path";
303
-
304
- app.staticFiles(join(__dirname, "..", "static"));
305
-
306
- // Route http://localhost:3000/static/style.css will serve the file located at ./static/style.css
307
- ```
308
-
309
- ---
310
-
311
- ## ⛔ Data Validation
312
-
313
- To validate the data in the body of client requests, the framework provides the creation of validation schemes and a middleware function to validate the body of HTTP requests, used as follows:
314
-
315
- ```ts
316
- import { v, schema, validateRequest, json } from "skyguard-js";
317
-
318
- // Created Schema
319
- const userSchema = schema({
320
- body: {
321
- name: v.string({ maxLength: 60 }),
322
- email: v.email(),
323
- age: v.number({ min: 18 }),
324
- active: v.boolean().default(false),
325
- birthdate: v.date({ max: new Date() }),
326
- },
327
- });
328
-
329
- app.post(
330
- "/test",
331
- (request: Request) => {
332
- const data = request.body;
333
- return json(data).setStatusCode(201);
334
- },
335
- [validateRequest(userSchema)],
336
- );
337
- ```
338
-
339
- To type the request body, an interface is used and the .getData() method is used, which allows returning the typed bodym. By default each property you define in the schema is required, to define it optional you use the `.optional()` or `.default(value)` function
340
-
341
- Validation is:
342
-
343
- - Fail-fast per field
344
- - Fully typed
345
- - Reusable
346
- - Decoupled from transport layer
347
-
348
- ---
349
-
350
- ## 🚨 Exceptions & Error Handling
351
-
352
- The framework provides a set of built-in HTTP exceptions that can be thrown from route handlers or middleware. When an exception is thrown, the framework detects it and sends an appropriate HTTP response with the status code and message you specified in the class.
353
-
354
- ```ts
355
- import { NotFoundError, InternalServerError, json } from "skyguard-js";
356
-
357
- const listResources = ["1", "2", "3"];
358
-
359
- app.get("/resource/{id}", (request: Request) => {
360
- const resource = request.params["id"];
361
-
362
- if (!listResources.includes(resource)) {
363
- throw new NotFoundError("Resource not found");
364
- }
365
-
366
- return json(resource);
367
- });
368
-
369
- app.get("/divide", (request: Request) => {
370
- try {
371
- const { a, b } = request.query;
372
- const result = Number(a) / Number(b);
373
-
374
- return json({ result });
375
- } catch (error) {
376
- throw new InternalServerError(
377
- "An error occurred while processing your request",
378
- );
379
- }
380
- });
381
- ```
382
-
383
- ---
384
-
385
- ## 🧱 Sessions
386
-
387
- To handle sessions, you must use the framework’s built-in middleware. Depending on where you want to store them (in memory, in files, or in a database), you need to use the corresponding storage class.
388
-
389
- ```ts
390
- import { sessions, FileSessionStorage, json } from "skyguard-js";
391
-
392
- app.middlewares(
393
- sessions(FileSessionStorage, {
394
- name: "connect.sid",
395
- rolling: true,
396
- saveUninitialized: false,
397
- cookie: {
398
- maxAge: 60 * 60 * 24,
399
- httpOnly: true,
400
- sameSite: "Lax",
401
- secure: false,
402
- path: "/",
403
- },
404
- }),
405
- );
406
-
407
- app.post("/login", (request: Request) => {
408
- const { username, password } = request.data;
409
-
410
- if (username === "admin" && password === "secret") {
411
- request.session.set("user", {
412
- id: 1,
413
- username: "admin",
414
- role: "admin",
415
- });
416
-
417
- return json({ message: "Logged in" });
418
- }
419
-
420
- throw new UnauthorizedError("Invalid credentials");
421
- });
422
-
423
- app.get("/me", (request: Request) => {
424
- const user = request.session.get("user");
425
-
426
- if (!user) throw new UnauthorizedError("Not authenticated");
427
- return json({ user });
428
- });
429
- ```
430
-
431
- For **database-backed sessions**, configure `DatabaseSessionStorage` once with an adapter that maps to your DB client/ORM. This keeps the framework **DB-engine agnostic** (MySQL, MariaDB, SQLite, PostgreSQL, SQL Server, Oracle, etc.).
432
-
433
- ```ts
434
- import {
435
- sessions,
436
- DatabaseSessionStorage,
437
- type SessionDatabaseAdapter,
438
- } from "skyguard-js";
439
-
440
- const sessionAdapter: SessionDatabaseAdapter = {
441
- async findById(id) {
442
- // query row by id and return: { data: parsedJson, expiresAt: unixMs }
443
- return null;
444
- },
445
- async upsert(id, payload) {
446
- // insert/update row depending on your DB driver
447
- },
448
- async deleteById(id) {
449
- // delete row by id
450
- },
451
- async deleteExpired(now) {
452
- // delete rows where expiresAt <= now
453
- },
454
- };
455
-
456
- DatabaseSessionStorage.configure(sessionAdapter);
457
-
458
- app.middlewares(sessions(DatabaseSessionStorage));
459
- ```
460
-
461
- ### Concrete DB adapter examples
462
-
463
- > Suggested table shape (portable across engines):
464
- >
465
- > - `id` (string/varchar, primary key)
466
- > - `data` (JSON/TEXT containing serialized object)
467
- > - `expires_at` (bigint/timestamp in unix milliseconds)
468
-
469
- To keep the code cleaner, you should create a separate file where you can configure the database sessions, such as `src/sessions/config.ts`
470
-
471
- #### Prisma (MySQL / PostgreSQL / SQLite / SQL Server / CockroachDB)
472
-
473
- ```ts
474
- import { PrismaClient } from "@prisma/client";
475
- import {
476
- DatabaseSessionStorage,
477
- type SessionDatabaseAdapter,
478
- } from "skyguard-js";
479
-
480
- const prisma = new PrismaClient();
481
-
482
- // model Session {
483
- // id String @id
484
- // data String
485
- // expiresAt BigInt @map("expires_at")
486
- // @@map("sessions")
487
- // }
488
-
489
- const adapter: SessionDatabaseAdapter = {
490
- async findById(id) {
491
- const row = await prisma.session.findUnique({ where: { id } });
492
- if (!row) return null;
493
- return { data: JSON.parse(row.data), expiresAt: Number(row.expiresAt) };
494
- },
495
- async upsert(id, payload) {
496
- await prisma.session.upsert({
497
- where: { id },
498
- update: {
499
- data: JSON.stringify(payload.data),
500
- expiresAt: BigInt(payload.expiresAt),
501
- },
502
- create: {
503
- id,
504
- data: JSON.stringify(payload.data),
505
- expiresAt: BigInt(payload.expiresAt),
506
- },
507
- });
508
- },
509
- async deleteById(id) {
510
- await prisma.session.deleteMany({ where: { id } });
511
- },
512
- async deleteExpired(now) {
513
- await prisma.session.deleteMany({
514
- where: { expiresAt: { lte: BigInt(now) } },
515
- });
516
- },
517
- };
518
-
519
- DatabaseSessionStorage.configure(adapter);
520
- ```
521
-
522
- #### TypeORM (MySQL / MariaDB / PostgreSQL / SQLite / MSSQL / Oracle)
523
-
524
- ```ts
525
- import {
526
- DataSource,
527
- Entity,
528
- Column,
529
- PrimaryColumn,
530
- LessThanOrEqual,
531
- } from "typeorm";
532
- import {
533
- DatabaseSessionStorage,
534
- type SessionDatabaseAdapter,
535
- } from "skyguard-js";
536
-
537
- @Entity({ name: "sessions" })
538
- class SessionEntity {
539
- @PrimaryColumn({ type: "varchar", length: 64 })
540
- id!: string;
541
-
542
- @Column({ type: "text" })
543
- data!: string;
544
-
545
- @Column({ name: "expires_at", type: "bigint" })
546
- expiresAt!: string;
547
- }
548
-
549
- const ds = new DataSource({ /* your db config */ entities: [SessionEntity] });
550
- await ds.initialize();
551
- const repo = ds.getRepository(SessionEntity);
552
-
553
- const adapter: SessionDatabaseAdapter = {
554
- async findById(id) {
555
- const row = await repo.findOneBy({ id });
556
- if (!row) return null;
557
- return { data: JSON.parse(row.data), expiresAt: Number(row.expiresAt) };
558
- },
559
- async upsert(id, payload) {
560
- await repo.save({
561
- id,
562
- data: JSON.stringify(payload.data),
563
- expiresAt: String(payload.expiresAt),
564
- });
565
- },
566
- async deleteById(id) {
567
- await repo.delete({ id });
568
- },
569
- async deleteExpired(now) {
570
- await repo.delete({ expiresAt: LessThanOrEqual(String(now)) });
571
- },
572
- };
573
-
574
- DatabaseSessionStorage.configure(adapter);
575
- ```
576
-
577
- #### mysql2 (MySQL)
578
-
579
- ```ts
580
- import mysql from "mysql2/promise";
581
- import {
582
- DatabaseSessionStorage,
583
- type SessionDatabaseAdapter,
584
- } from "skyguard-js";
585
-
586
- const pool = mysql.createPool({ uri: process.env.DATABASE_URL });
587
-
588
- const adapter: SessionDatabaseAdapter = {
589
- async findById(id) {
590
- const [rows] = await pool.query<any[]>(
591
- "SELECT data, expires_at FROM sessions WHERE id = ? LIMIT 1",
592
- [id],
593
- );
594
- const row = rows[0];
595
- if (!row) return null;
596
- return { data: JSON.parse(row.data), expiresAt: Number(row.expires_at) };
597
- },
598
- async upsert(id, payload) {
599
- await pool.query(
600
- `INSERT INTO sessions (id, data, expires_at) VALUES (?, ?, ?)
601
- ON DUPLICATE KEY UPDATE data = VALUES(data), expires_at = VALUES(expires_at)`,
602
- [id, JSON.stringify(payload.data), payload.expiresAt],
603
- );
604
- },
605
- async deleteById(id) {
606
- await pool.query("DELETE FROM sessions WHERE id = ?", [id]);
607
- },
608
- async deleteExpired(now) {
609
- await pool.query("DELETE FROM sessions WHERE expires_at <= ?", [now]);
610
- },
611
- };
612
-
613
- DatabaseSessionStorage.configure(adapter);
614
- ```
615
-
616
- #### sqlite3 (SQLite)
617
-
618
- ```ts
619
- import sqlite3 from "sqlite3";
620
- import { open } from "sqlite";
621
- import { type SessionDatabaseAdapter } from "skyguard-js";
622
-
623
- const db = await open({ filename: "./sessions.db", driver: sqlite3.Database });
624
-
625
- const adapter: SessionDatabaseAdapter = {
626
- async findById(id) {
627
- const row = await db.get<{ data: string; expires_at: number }>(
628
- "SELECT data, expires_at FROM sessions WHERE id = ? LIMIT 1",
629
- [id],
630
- );
631
- if (!row) return null;
632
- return { data: JSON.parse(row.data), expiresAt: Number(row.expires_at) };
633
- },
634
- async upsert(id, payload) {
635
- await db.run(
636
- `INSERT INTO sessions (id, data, expires_at) VALUES (?, ?, ?)
637
- ON CONFLICT(id) DO UPDATE SET data = excluded.data, expires_at = excluded.expires_at`,
638
- [id, JSON.stringify(payload.data), payload.expiresAt],
639
- );
640
- },
641
- async deleteById(id) {
642
- await db.run("DELETE FROM sessions WHERE id = ?", [id]);
643
- },
644
- async deleteExpired(now) {
645
- await db.run("DELETE FROM sessions WHERE expires_at <= ?", [now]);
646
- },
647
- };
648
-
649
- DatabaseSessionStorage.configure(adapter);
650
- ```
651
-
652
- ---
653
-
654
- ## 🛡️ Security
655
-
656
- The framework includes some password hashing and JWT token generation functions, and also includes JWT authentication middleware.
657
-
658
- ```ts
659
- import { Hasher, JWT, json } from "skyguard-js";
660
-
661
- app.post("/register", async (request: Request) => {
662
- const { username, password } = request.data;
663
- const hashedPassword = await Hasher.hash(password);
664
-
665
- // Save username and hashedPassword to database
666
- // ...
667
-
668
- return json({ message: "User registered" });
669
- });
670
-
671
- app.post("/login", async (request: Request) => {
672
- const { username, password } = request.data;
673
-
674
- // Retrieve user from database by username
675
- // ...
676
-
677
- const isValid = await Hasher.verify(password, user.hashedPassword);
678
-
679
- if (!isValid) {
680
- throw new UnauthorizedError("Invalid credentials");
681
- }
682
-
683
- const token = JWT.create({ sub: "123" }, "secret-key", {
684
- algorithm: "HS256",
685
- expiresIn: "1h",
686
- });
687
-
688
- return json({ token });
689
- });
690
- ```
691
-
692
- ---
693
-
694
- ## 📂 File Uploads
695
-
696
- To handle file uploads, use the built-in `createUploader` function to create an uploader middleware with the desired storage configuration.
697
-
698
- ```ts
699
- import { createUploader, StorageType, json } from "skyguard-js";
700
-
701
- const uploader = createUploader({
702
- storageType: StorageType.DISK,
703
- storageOptions: {
704
- disk: {
705
- destination: "./uploads",
706
- },
707
- },
708
- });
709
-
710
- app.post(
711
- "/upload",
712
- (request: Request) => {
713
- return json({
714
- message: "File uploaded successfully",
715
- file: request.file,
716
- });
717
- },
718
- [uploader.single("file")],
719
- );
720
- ```
721
-
722
- Depending on the `Storage Type` you have selected, the storage options will contain two properties: `disk` and `memory`
723
-
724
- ---
725
-
726
- ## 📄 Views & Template Engine
727
-
728
- To render views, you must first set up the template engine using the `engineTemplates` method of the `app`, set the view path with the `views` method of the `app`, and then you can use the `render` method within your handlers to render the views with the data you want to pass.
729
-
730
- ```ts
731
- import { engine } from "express-handlebars";
732
- import ejs from "ejs";
733
- import { join } from "node:path";
734
- import { render } from "skyguard-js";
735
-
736
- app.views(__dirname, "views");
737
-
738
- // Config for Express Handlebars
739
- app.engineTemplates(
740
- "hbs",
741
- engine({
742
- extname: "hbs",
743
- layoutsDir: join(__dirname, "views"),
744
- defaultLayout: "main",
745
- }),
746
- );
747
-
748
- // Config for EJS
749
- app.engineTemplates("ejs", (templatePath, data) => {
750
- return ejs.renderFile(templatePath, data);
751
- });
752
-
753
- app.get("/home", () => {
754
- return render("index", {
755
- title: "Home Page",
756
- message: "Welcome to the home page!",
757
- });
758
- });
759
- ```
760
-
761
- Currently, it works with third-party template engines such as **Express Handlebars**, **Pug**, and **EJS**, but the idea is to implement its own template engine in the future.
762
-
763
- ---
764
-
765
87
  ## 🔮 Roadmap (Tentative)
766
88
 
767
89
  - Middleware system (✅)
768
90
  - Template engines supported (✅)
769
- - Request / Response abstraction (✅)
91
+ - Context abstraction (✅)
770
92
  - Data validation (✅)
771
93
  - Error handling improvements (✅)
772
94
  - Sessions & cookies (✅)
@@ -775,3 +97,7 @@ Currently, it works with third-party template engines such as **Express Handleba
775
97
  - Database & ORM integration
776
98
  - Authentication & authorization
777
99
  - WebSockets
100
+
101
+ ## License
102
+
103
+ MIT