cpeak 2.5.0 → 2.6.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 CHANGED
@@ -34,6 +34,9 @@ This is an educational project that was started as part of the [Understanding No
34
34
  - [serveStatic](#servestatic)
35
35
  - [parseJSON](#parsejson)
36
36
  - [render](#render)
37
+ - [cookieParser](#cookieparser)
38
+ - [swagger](#swagger)
39
+ - [auth](#auth)
37
40
  - [Complete Example](#complete-example)
38
41
  - [Versioning Notice](#versioning-notice)
39
42
 
@@ -267,6 +270,9 @@ The list of utility functions as of now:
267
270
  - serveStatic
268
271
  - parseJSON
269
272
  - render
273
+ - cookieParser
274
+ - swagger
275
+ - auth
270
276
 
271
277
  Including any one of them is done like this:
272
278
 
@@ -296,7 +302,11 @@ If you have file types in your public folder that are not one of the following,
296
302
  jpeg: "image/jpeg",
297
303
  png: "image/png",
298
304
  svg: "image/svg+xml",
305
+ gif: "image/gif",
306
+ ico: "image/x-icon",
299
307
  txt: "text/plain",
308
+ json: "application/json",
309
+ webmanifest: "application/manifest+json",
300
310
  eot: "application/vnd.ms-fontobject",
301
311
  otf: "font/otf",
302
312
  ttf: "font/ttf",
@@ -304,6 +314,16 @@ If you have file types in your public folder that are not one of the following,
304
314
  woff2: "font/woff2"
305
315
  ```
306
316
 
317
+ You can also serve your static files under a URL prefix by passing a third argument with a `prefix` option. This is useful when you want all static assets to live under a specific path like `/static`:
318
+
319
+ ```javascript
320
+ server.beforeEach(
321
+ serveStatic("./public", null, { prefix: "/static" })
322
+ );
323
+ ```
324
+
325
+ With this setup, a file at `./public/app.js` would be served at `/static/app.js` instead of `/app.js`. Pass `null` as the second argument if you don’t need any custom MIME types.
326
+
307
327
  #### parseJSON
308
328
 
309
329
  With this middleware function, you can easily read and send JSON in HTTP message bodies in route and middleware functions. Fire it up like this:
@@ -366,12 +386,197 @@ You can then inject the variables into your file in {{ variable_name }} like thi
366
386
  </html>
367
387
  ```
368
388
 
389
+ #### cookieParser
390
+
391
+ With this middleware function, you can easily read and set cookies in your route and middleware functions. Fire it up like this:
392
+
393
+ ```javascript
394
+ server.beforeEach(cookieParser());
395
+ ```
396
+
397
+ If you need to use signed cookies, pass a secret:
398
+
399
+ ```javascript
400
+ server.beforeEach(cookieParser({ secret: "your-secret-key" }));
401
+ ```
402
+
403
+ Signed cookies use HMAC to verify integrity. The original value stays readable by the client, but any tampering with it will be detected on the server side. This makes them a solid choice for session identifiers or user IDs where you want to prevent impersonation without hiding the value itself.
404
+
405
+ Read incoming cookies like this:
406
+
407
+ ```javascript
408
+ server.route("get", "/dashboard", (req, res) => {
409
+ // Regular cookies
410
+ const theme = req.cookies.theme;
411
+
412
+ // Signed cookies — returns false if the signature is invalid or the value was tampered with
413
+ const userId = req.signedCookies.userId;
414
+
415
+ res.status(200).json({ theme, userId });
416
+ });
417
+ ```
418
+
419
+ Set cookies on the response like this:
420
+
421
+ ```javascript
422
+ server.route("post", "/login", (req, res) => {
423
+ // A plain cookie
424
+ res.cookie("theme", "dark", { httpOnly: true, maxAge: 7 * 24 * 60 * 60 * 1000 });
425
+
426
+ // A signed cookie
427
+ res.cookie("userId", "abc123", { signed: true, httpOnly: true, secure: true });
428
+
429
+ res.status(200).json({ message: "Logged in" });
430
+ });
431
+ ```
432
+
433
+ Clear a cookie like this:
434
+
435
+ ```javascript
436
+ res.clearCookie("userId");
437
+ ```
438
+
439
+ The full list of cookie options you can pass as the third argument to `res.cookie()`:
440
+
441
+ - `signed` — sign the cookie value with HMAC using the secret you provided to `cookieParser`
442
+ - `httpOnly` — prevents client-side JavaScript from accessing the cookie
443
+ - `secure` — instructs the browser to send the cookie only over HTTPS
444
+ - `sameSite` — controls cross-site cookie behavior; accepts `"strict"`, `"lax"`, or `"none"`
445
+ - `maxAge` — cookie lifetime in milliseconds
446
+ - `expires` — a specific expiration `Date` for the cookie
447
+ - `path` — path the cookie is valid for (defaults to `"/"`)
448
+ - `domain` — domain the cookie is valid for
449
+
450
+ #### swagger
451
+
452
+ With this middleware function, you can serve an interactive Swagger UI for your API documentation. It works alongside the `serveStatic` utility and two npm packages: `swagger-ui-dist` (the Swagger UI static assets) and `yamljs` (to load your YAML spec file).
453
+
454
+ Start by installing the dependencies:
455
+
456
+ ```bash
457
+ npm install swagger-ui-dist yamljs
458
+ ```
459
+
460
+ Then fire it up like this:
461
+
462
+ ```javascript
463
+ import cpeak, { swagger, serveStatic } from "cpeak";
464
+ import YAML from "yamljs";
465
+ import swaggerUiDist from "swagger-ui-dist";
466
+ import path from "node:path";
467
+
468
+ const server = cpeak();
469
+
470
+ const swaggerDocument = YAML.load(
471
+ path.join(path.resolve(), "./src/swagger.yml")
472
+ );
473
+
474
+ server.beforeEach(swagger(swaggerDocument));
475
+ server.beforeEach(
476
+ serveStatic(swaggerUiDist.getAbsoluteFSPath(), undefined, {
477
+ prefix: "/api-docs",
478
+ })
479
+ );
480
+ ```
481
+
482
+ Once set up, your Swagger UI will be available at `/api-docs`. The `swagger` middleware handles serving your spec at `/api-docs/spec.json` and wiring up the Swagger UI initializer, while `serveStatic` serves all the Swagger UI static assets under the same prefix.
483
+
484
+ If you want to serve the docs under a different path, pass it as the second argument to `swagger` and match the prefix in `serveStatic`:
485
+
486
+ ```javascript
487
+ server.beforeEach(swagger(swaggerDocument, "/docs"));
488
+ server.beforeEach(
489
+ serveStatic(swaggerUiDist.getAbsoluteFSPath(), undefined, {
490
+ prefix: "/docs",
491
+ })
492
+ );
493
+ ```
494
+
495
+ #### auth
496
+
497
+ With this middleware you can add a full-fledged authentication system to your application with emails, username and password authentication, with features such as Forgot Password, Update Password and so forth. We have no external dependencies, with timing-safe comparisons throughout. It attaches helper methods directly to `req` so your route handlers stay clean.
498
+
499
+ Fire it up like this:
500
+
501
+ ```javascript
502
+ import cpeak, { parseJSON, cookieParser, auth } from "cpeak";
503
+
504
+ const app = cpeak();
505
+
506
+ app.beforeEach(parseJSON());
507
+ app.beforeEach(cookieParser());
508
+
509
+ app.beforeEach(
510
+ auth({
511
+ // Required
512
+ secret: "your-secret-min-32-chars-long!!!", // used to sign token IDs with HMAC
513
+ saveToken: async (tokenId, userId, expiresAt) => { /* store in your DB */ },
514
+ findToken: async (tokenId) => { /* return { userId, expiresAt } or null */ },
515
+
516
+ // Enables req.logout()
517
+ revokeToken: async (tokenId) => { /* delete from your DB */ },
518
+
519
+ // Optional PBKDF2 tuning (defaults shown):
520
+ iterations: 210_000, // higher = slower brute-force
521
+ keylen: 64, // derived key length in bytes
522
+ digest: "sha512",
523
+ saltSize: 32,
524
+
525
+ // Optional token signing tuning (defaults shown):
526
+ hmacAlgorithm: "sha256",
527
+ tokenIdSize: 20,
528
+ tokenExpiry: 7 * 24 * 60 * 60 * 1000, // 7 days in ms
529
+ })
530
+ );
531
+ ```
532
+
533
+ Once set up, the following methods are available on `req` inside your routes and middleware:
534
+
535
+ | Method | Description |
536
+ |--------|-------------|
537
+ | `req.hashPassword({ password })` | Hashes a password with PBKDF2. Store the result; never store plaintext. |
538
+ | `req.login({ password, hashedPassword, userId })` | Verifies the password and if correct, creates a signed token. Returns the token string to send to the client, or `null` on wrong password. |
539
+ | `req.verifyToken(token)` | Validates a token's HMAC signature and expiry. Returns `{ userId }` or `null`. |
540
+ | `req.logout(token)` | Revokes the token via your `revokeToken` callback. Only available when `revokeToken` is provided. |
541
+
542
+ Here are the two most common middleware patterns you'll want to set up:
543
+
544
+ ```javascript
545
+ // Throws 401 if the request has no valid token. Use on protected routes.
546
+ const requireAuth = async (req, res, next) => {
547
+ const token = req.headers["authorization"];
548
+ if (!token) throw { status: 401, message: "Unauthorized." };
549
+
550
+ const result = await req.verifyToken(token);
551
+ if (!result) throw { status: 401, message: "Unauthorized." };
552
+
553
+ req.user = { id: result.userId };
554
+ next();
555
+ };
556
+
557
+ // Silently sets req.user when a valid token is present, but lets the request through either way.
558
+ // Useful for routes accessible by both authenticated and unauthenticated users.
559
+ const optionalAuth = async (req, _res, next) => {
560
+ const token = req.headers["authorization"];
561
+ if (token) {
562
+ const result = await req.verifyToken(token);
563
+ if (result) req.user = { id: result.userId };
564
+ }
565
+ next();
566
+ };
567
+ ```
568
+
569
+ For complete working examples, see:
570
+
571
+ - [`examples/auth-localstorage.js`](examples/auth-localstorage.js) — token sent via the `Authorization` header (suited for SPAs and mobile clients)
572
+ - [`examples/auth-cookies.js`](examples/auth-cookies.js) — token stored in an `httpOnly` cookie (protects against XSS)
573
+
369
574
  ## Complete Example
370
575
 
371
- Here you can see all the features that Cpeak offers, in one small piece of code:
576
+ Here you can see all the features that Cpeak offers (excluding the authentication features), in one small piece of code:
372
577
 
373
578
  ```javascript
374
- import cpeak, { serveStatic, parseJSON, render } from "cpeak";
579
+ import cpeak, { serveStatic, parseJSON, render, cookieParser } from "cpeak";
375
580
 
376
581
  const server = cpeak();
377
582
 
@@ -386,6 +591,9 @@ server.beforeEach(render());
386
591
  // For parsing JSON bodies
387
592
  server.beforeEach(parseJSON());
388
593
 
594
+ // For reading and setting cookies
595
+ server.beforeEach(cookieParser({ secret: "your-secret-key" }));
596
+
389
597
  // Adding custom middleware functions
390
598
  server.beforeEach((req, res, next) => {
391
599
  req.custom = "This is some string";
@@ -437,6 +645,16 @@ server.route("get", "/api/document/:title", testRouteMiddleware, (req, res) => {
437
645
  res.status(200).json({ message: "This is a test response" });
438
646
  });
439
647
 
648
+ // Reading and setting cookies
649
+ server.route("post", "/login", (req, res) => {
650
+ // Reads are available via req.cookies and req.signedCookies
651
+ const sessionId = req.signedCookies.sessionId;
652
+
653
+ // Set a signed session cookie
654
+ res.cookie("sessionId", "abc123", { signed: true, httpOnly: true, secure: true });
655
+ res.status(200).json({ message: "Logged in" });
656
+ });
657
+
440
658
  // Sending a file response
441
659
  server.route("get", "/file", (req, res) => {
442
660
  // Make sure to specify a correct path and MIME type...
package/dist/index.d.ts CHANGED
@@ -1,17 +1,21 @@
1
+ import * as net from 'net';
1
2
  import http, { IncomingMessage, ServerResponse } from 'node:http';
2
3
 
3
- type Cpeak$1 = ReturnType<typeof cpeak>;
4
4
  type StringMap = Record<string, string>;
5
5
  interface CpeakRequest<ReqBody = any, ReqQueries = any> extends IncomingMessage {
6
6
  params: StringMap;
7
7
  query: ReqQueries;
8
8
  body?: ReqBody;
9
+ cookies?: StringMap;
10
+ signedCookies?: Record<string, string | false>;
9
11
  [key: string]: any;
10
12
  }
11
13
  interface CpeakResponse extends ServerResponse {
12
14
  sendFile: (path: string, mime: string) => Promise<void>;
13
15
  status: (code: number) => CpeakResponse;
14
- redirect: (location: string) => CpeakResponse;
16
+ attachment: (filename?: string) => CpeakResponse;
17
+ cookie: (name: string, value: string, options?: any) => CpeakResponse;
18
+ redirect: (location: string) => void;
15
19
  json: (data: any) => void;
16
20
  [key: string]: any;
17
21
  }
@@ -30,14 +34,54 @@ interface RoutesMap {
30
34
  [method: string]: Route[];
31
35
  }
32
36
 
33
- declare const serveStatic: (folderPath: string, newMimeTypes?: StringMap) => (req: CpeakRequest, res: CpeakResponse, next: Next) => void | Promise<void>;
34
-
35
37
  declare const parseJSON: (options?: {
36
38
  limit?: number;
37
39
  }) => (req: CpeakRequest, res: CpeakResponse, next: Next) => void;
38
40
 
41
+ declare const serveStatic: (folderPath: string, newMimeTypes?: StringMap, options?: {
42
+ prefix?: string;
43
+ }) => (req: CpeakRequest, res: CpeakResponse, next: Next) => void | Promise<void>;
44
+
39
45
  declare const render: () => (req: CpeakRequest, res: CpeakResponse, next: Next) => void;
40
46
 
47
+ declare const swagger: (spec: object, prefix?: string) => (req: CpeakRequest, res: CpeakResponse, next: Next) => void;
48
+
49
+ interface PbkdfOptions {
50
+ iterations?: number;
51
+ keylen?: number;
52
+ digest?: string;
53
+ saltSize?: number;
54
+ }
55
+ interface AuthOptions extends PbkdfOptions {
56
+ secret: string;
57
+ saveToken: (tokenId: string, userId: string, expiresAt: Date) => Promise<void>;
58
+ findToken: (tokenId: string) => Promise<{
59
+ userId: string;
60
+ expiresAt: Date;
61
+ } | null>;
62
+ tokenExpiry?: number;
63
+ hmacAlgorithm?: string;
64
+ tokenIdSize?: number;
65
+ revokeToken?: (tokenId: string) => Promise<void>;
66
+ }
67
+ declare function hashPassword(password: string, options?: PbkdfOptions): Promise<string>;
68
+ declare function verifyPassword(password: string, stored: string): Promise<boolean>;
69
+ declare function auth(options: AuthOptions): Middleware;
70
+
71
+ interface CookieOptions {
72
+ signed?: boolean;
73
+ httpOnly?: boolean;
74
+ secure?: boolean;
75
+ sameSite?: "strict" | "lax" | "none";
76
+ maxAge?: number;
77
+ expires?: Date;
78
+ path?: string;
79
+ domain?: string;
80
+ }
81
+ declare function cookieParser(options?: {
82
+ secret?: string;
83
+ }): (req: CpeakRequest, res: CpeakResponse, next: Next) => void;
84
+
41
85
  declare function frameworkError(message: string, skipFn: Function, code?: string, status?: number): Error & {
42
86
  code?: string;
43
87
  cpeak_err?: boolean;
@@ -48,34 +92,33 @@ declare enum ErrorCode {
48
92
  NOT_A_FILE = "CPEAK_ERR_NOT_A_FILE",
49
93
  SEND_FILE_FAIL = "CPEAK_ERR_SEND_FILE_FAIL",
50
94
  INVALID_JSON = "CPEAK_ERR_INVALID_JSON",
51
- PAYLOAD_TOO_LARGE = "CPEAK_ERR_PAYLOAD_TOO_LARGE"
95
+ PAYLOAD_TOO_LARGE = "CPEAK_ERR_PAYLOAD_TOO_LARGE",
96
+ WEAK_SECRET = "CPEAK_ERR_WEAK_SECRET"
52
97
  }
53
98
  declare class CpeakIncomingMessage extends http.IncomingMessage {
99
+ #private;
54
100
  body: any;
55
101
  params: StringMap;
56
- private _query?;
57
102
  get query(): StringMap;
58
103
  }
59
104
  declare class CpeakServerResponse extends http.ServerResponse<CpeakIncomingMessage> {
60
105
  sendFile(path: string, mime: string): Promise<void>;
61
106
  status(code: number): this;
62
- redirect(location: string): this;
107
+ attachment(filename?: string): this;
108
+ redirect(location: string): void;
63
109
  json(data: any): void;
64
110
  }
65
111
  declare class Cpeak {
66
112
  #private;
67
- private server;
68
- private routes;
69
- private middleware;
70
- private _handleErr?;
71
113
  constructor();
72
114
  route(method: string, path: string, ...args: (RouteMiddleware | Handler)[]): void;
73
115
  beforeEach(cb: Middleware): void;
74
116
  handleErr(cb: (err: unknown, req: CpeakRequest, res: CpeakResponse) => void): void;
75
117
  listen(port: number, cb?: () => void): http.Server<typeof CpeakIncomingMessage, typeof CpeakServerResponse>;
118
+ address(): string | net.AddressInfo | null;
76
119
  close(cb?: (err?: Error) => void): void;
77
120
  }
78
121
 
79
122
  declare function cpeak(): Cpeak;
80
123
 
81
- export { type Cpeak$1 as Cpeak, type CpeakRequest, type CpeakResponse, ErrorCode, type HandleErr, type Handler, type Middleware, type Next, type RouteMiddleware, type RoutesMap, cpeak as default, frameworkError, parseJSON, render, serveStatic };
124
+ export { type AuthOptions, type CookieOptions, Cpeak, CpeakIncomingMessage, type CpeakRequest, type CpeakResponse, CpeakServerResponse, ErrorCode, type HandleErr, type Handler, type Middleware, type Next, type PbkdfOptions, type RouteMiddleware, type RoutesMap, auth, cookieParser, cpeak as default, frameworkError, hashPassword, parseJSON, render, serveStatic, swagger, verifyPassword };