cpeak 2.5.0 → 2.7.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
@@ -28,12 +28,17 @@ This is an educational project that was started as part of the [Understanding No
28
28
  - [URL Variables & Parameters](#url-variables--parameters)
29
29
  - [Sending Files](#sending-files)
30
30
  - [Redirecting](#redirecting)
31
+ - [Compression](#compression)
31
32
  - [Error Handling](#error-handling)
32
33
  - [Listening](#listening)
33
34
  - [Util Functions](#util-functions)
34
35
  - [serveStatic](#servestatic)
35
36
  - [parseJSON](#parsejson)
36
37
  - [render](#render)
38
+ - [cookieParser](#cookieparser)
39
+ - [swagger](#swagger)
40
+ - [auth](#auth)
41
+ - [cors](#cors)
37
42
  - [Complete Example](#complete-example)
38
43
  - [Versioning Notice](#versioning-notice)
39
44
 
@@ -203,6 +208,54 @@ If you want to redirect to a new URL, you can simply do:
203
208
  res.redirect("https://whatever.com");
204
209
  ```
205
210
 
211
+ ### Compression
212
+
213
+ You can enable HTTP response compression at construction time. Once enabled, `serveStatic`, `res.json()` and `res.sendFile()` will compress eligible responses automatically, and you also get a `res.compress()` method on the response for custom payloads.
214
+
215
+ Fire it up with the defaults like this:
216
+
217
+ ```javascript
218
+ const server = cpeak({ compression: true });
219
+ ```
220
+
221
+ Or pass options to tune the behavior:
222
+
223
+ ```javascript
224
+ const server = cpeak({
225
+ compression: {
226
+ threshold: 1024, // bytes — responses smaller than this are sent uncompressed. Default: 1024
227
+ brotli: {}, // node:zlib BrotliOptions
228
+ gzip: {}, // node:zlib ZlibOptions
229
+ deflate: {} // node:zlib ZlibOptions
230
+ }
231
+ });
232
+ ```
233
+
234
+ For arbitrary payloads, like a `Buffer`, `string`, or `Readable` stream, use `res.compress`:
235
+
236
+ ```javascript
237
+ server.route("get", "/report", async (req, res) => {
238
+ const csv = await buildCsvReport();
239
+ await res.compress("text/csv", csv);
240
+ });
241
+ ```
242
+
243
+ When you're streaming, you can pass a known size as the third argument. Cpeak will use it to decide eligibility against `threshold`, and to set `Content-Length` if the body ends up being sent uncompressed:
244
+
245
+ ```javascript
246
+ import { Readable } from "node:stream";
247
+
248
+ server.route("get", "/proxy/feed", async (req, res) => {
249
+ const upstream = await fetch("https://example.com/feed.xml");
250
+ const size = Number(upstream.headers.get("content-length"));
251
+ await res.compress("application/xml", Readable.fromWeb(upstream.body), size);
252
+ });
253
+ ```
254
+
255
+ You must first enable compression at construction time to use `res.compress`.
256
+
257
+ One thing to keep in mind: when compression is enabled, `res.json()` returns a `Promise` because the work runs through async streams. You don't have to await it, but you can if you want to know when the response has been fully flushed.
258
+
206
259
  ### Error Handling
207
260
 
208
261
  If anywhere in your route functions or route middleware functions you want to return an error, you can just throw the error and let the automatic error handler catch it:
@@ -267,6 +320,10 @@ The list of utility functions as of now:
267
320
  - serveStatic
268
321
  - parseJSON
269
322
  - render
323
+ - cookieParser
324
+ - swagger
325
+ - auth
326
+ - cors
270
327
 
271
328
  Including any one of them is done like this:
272
329
 
@@ -296,7 +353,11 @@ If you have file types in your public folder that are not one of the following,
296
353
  jpeg: "image/jpeg",
297
354
  png: "image/png",
298
355
  svg: "image/svg+xml",
356
+ gif: "image/gif",
357
+ ico: "image/x-icon",
299
358
  txt: "text/plain",
359
+ json: "application/json",
360
+ webmanifest: "application/manifest+json",
300
361
  eot: "application/vnd.ms-fontobject",
301
362
  otf: "font/otf",
302
363
  ttf: "font/ttf",
@@ -304,6 +365,16 @@ If you have file types in your public folder that are not one of the following,
304
365
  woff2: "font/woff2"
305
366
  ```
306
367
 
368
+ 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`:
369
+
370
+ ```javascript
371
+ server.beforeEach(
372
+ serveStatic("./public", null, { prefix: "/static" })
373
+ );
374
+ ```
375
+
376
+ 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.
377
+
307
378
  #### parseJSON
308
379
 
309
380
  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 +437,219 @@ You can then inject the variables into your file in {{ variable_name }} like thi
366
437
  </html>
367
438
  ```
368
439
 
440
+ #### cookieParser
441
+
442
+ With this middleware function, you can easily read and set cookies in your route and middleware functions. Fire it up like this:
443
+
444
+ ```javascript
445
+ server.beforeEach(cookieParser());
446
+ ```
447
+
448
+ If you need to use signed cookies, pass a secret:
449
+
450
+ ```javascript
451
+ server.beforeEach(cookieParser({ secret: "your-secret-key" }));
452
+ ```
453
+
454
+ 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.
455
+
456
+ Read incoming cookies like this:
457
+
458
+ ```javascript
459
+ server.route("get", "/dashboard", (req, res) => {
460
+ // Regular cookies
461
+ const theme = req.cookies.theme;
462
+
463
+ // Signed cookies — returns false if the signature is invalid or the value was tampered with
464
+ const userId = req.signedCookies.userId;
465
+
466
+ res.status(200).json({ theme, userId });
467
+ });
468
+ ```
469
+
470
+ Set cookies on the response like this:
471
+
472
+ ```javascript
473
+ server.route("post", "/login", (req, res) => {
474
+ // A plain cookie
475
+ res.cookie("theme", "dark", { httpOnly: true, maxAge: 7 * 24 * 60 * 60 * 1000 });
476
+
477
+ // A signed cookie
478
+ res.cookie("userId", "abc123", { signed: true, httpOnly: true, secure: true });
479
+
480
+ res.status(200).json({ message: "Logged in" });
481
+ });
482
+ ```
483
+
484
+ Clear a cookie like this:
485
+
486
+ ```javascript
487
+ res.clearCookie("userId");
488
+ ```
489
+
490
+ The full list of cookie options you can pass as the third argument to `res.cookie()`:
491
+
492
+ - `signed` — sign the cookie value with HMAC using the secret you provided to `cookieParser`
493
+ - `httpOnly` — prevents client-side JavaScript from accessing the cookie
494
+ - `secure` — instructs the browser to send the cookie only over HTTPS
495
+ - `sameSite` — controls cross-site cookie behavior; accepts `"strict"`, `"lax"`, or `"none"`
496
+ - `maxAge` — cookie lifetime in milliseconds
497
+ - `expires` — a specific expiration `Date` for the cookie
498
+ - `path` — path the cookie is valid for (defaults to `"/"`)
499
+ - `domain` — domain the cookie is valid for
500
+
501
+ #### swagger
502
+
503
+ 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).
504
+
505
+ Start by installing the dependencies:
506
+
507
+ ```bash
508
+ npm install swagger-ui-dist yamljs
509
+ ```
510
+
511
+ Then fire it up like this:
512
+
513
+ ```javascript
514
+ import cpeak, { swagger, serveStatic } from "cpeak";
515
+ import YAML from "yamljs";
516
+ import swaggerUiDist from "swagger-ui-dist";
517
+ import path from "node:path";
518
+
519
+ const server = cpeak();
520
+
521
+ const swaggerDocument = YAML.load(
522
+ path.join(path.resolve(), "./src/swagger.yml")
523
+ );
524
+
525
+ server.beforeEach(swagger(swaggerDocument));
526
+ server.beforeEach(
527
+ serveStatic(swaggerUiDist.getAbsoluteFSPath(), undefined, {
528
+ prefix: "/api-docs",
529
+ })
530
+ );
531
+ ```
532
+
533
+ 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.
534
+
535
+ 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`:
536
+
537
+ ```javascript
538
+ server.beforeEach(swagger(swaggerDocument, "/docs"));
539
+ server.beforeEach(
540
+ serveStatic(swaggerUiDist.getAbsoluteFSPath(), undefined, {
541
+ prefix: "/docs",
542
+ })
543
+ );
544
+ ```
545
+
546
+ #### auth
547
+
548
+ 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.
549
+
550
+ Fire it up like this:
551
+
552
+ ```javascript
553
+ import cpeak, { parseJSON, cookieParser, auth } from "cpeak";
554
+
555
+ const app = cpeak();
556
+
557
+ app.beforeEach(parseJSON());
558
+ app.beforeEach(cookieParser());
559
+
560
+ app.beforeEach(
561
+ auth({
562
+ // Required
563
+ secret: "your-secret-min-32-chars-long!!!", // used to sign token IDs with HMAC
564
+ saveToken: async (tokenId, userId, expiresAt) => { /* store in your DB */ },
565
+ findToken: async (tokenId) => { /* return { userId, expiresAt } or null */ },
566
+
567
+ // Enables req.logout()
568
+ revokeToken: async (tokenId) => { /* delete from your DB */ },
569
+
570
+ // Optional PBKDF2 tuning (defaults shown):
571
+ iterations: 210_000, // higher = slower brute-force
572
+ keylen: 64, // derived key length in bytes
573
+ digest: "sha512",
574
+ saltSize: 32,
575
+
576
+ // Optional token signing tuning (defaults shown):
577
+ hmacAlgorithm: "sha256",
578
+ tokenIdSize: 20,
579
+ tokenExpiry: 7 * 24 * 60 * 60 * 1000, // 7 days in ms
580
+ })
581
+ );
582
+ ```
583
+
584
+ Once set up, the following methods are available on `req` inside your routes and middleware:
585
+
586
+ | Method | Description |
587
+ |--------|-------------|
588
+ | `req.hashPassword({ password })` | Hashes a password with PBKDF2. Store the result; never store plaintext. |
589
+ | `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. |
590
+ | `req.verifyToken(token)` | Validates a token's HMAC signature and expiry. Returns `{ userId }` or `null`. |
591
+ | `req.logout(token)` | Revokes the token via your `revokeToken` callback. Only available when `revokeToken` is provided. |
592
+
593
+ Here are the two most common middleware patterns you'll want to set up:
594
+
595
+ ```javascript
596
+ // Throws 401 if the request has no valid token. Use on protected routes.
597
+ const requireAuth = async (req, res, next) => {
598
+ const token = req.headers["authorization"];
599
+ if (!token) throw { status: 401, message: "Unauthorized." };
600
+
601
+ const result = await req.verifyToken(token);
602
+ if (!result) throw { status: 401, message: "Unauthorized." };
603
+
604
+ req.user = { id: result.userId };
605
+ next();
606
+ };
607
+
608
+ // Silently sets req.user when a valid token is present, but lets the request through either way.
609
+ // Useful for routes accessible by both authenticated and unauthenticated users.
610
+ const optionalAuth = async (req, _res, next) => {
611
+ const token = req.headers["authorization"];
612
+ if (token) {
613
+ const result = await req.verifyToken(token);
614
+ if (result) req.user = { id: result.userId };
615
+ }
616
+ next();
617
+ };
618
+ ```
619
+
620
+ For complete working examples, see:
621
+
622
+ - [`examples/auth-localstorage.js`](examples/auth-localstorage.js) — token sent via the `Authorization` header (suited for SPAs and mobile clients)
623
+ - [`examples/auth-cookies.js`](examples/auth-cookies.js) — token stored in an `httpOnly` cookie (protects against XSS)
624
+
625
+ #### cors
626
+ The CORS middleware allows you to enable Cross-Origin Resource Sharing in your application.
627
+
628
+ ```javascript
629
+ server.beforeEach(cors({
630
+ origin: "http://localhost:3000", // string, string[], RegExp, boolean, or async (origin) => boolean. Default: "*" (all origins)
631
+ methods: "GET,POST,PUT,DELETE", // allowed HTTP methods. Default: "GET,HEAD,PUT,PATCH,POST,DELETE"
632
+ allowedHeaders: "Content-Type", // headers the browser may send. Default: echoes request headers for origin:"*", else "Content-Type, Authorization"
633
+ exposedHeaders: "X-Request-Id", // response headers the browser may read. Default: none
634
+ credentials: true, // adds Access-Control-Allow-Credentials: true. Default: false
635
+ maxAge: 3600, // seconds to cache preflight result in the browser. Default: 86400
636
+ preflightContinue: false, // pass OPTIONS preflight to next middleware instead of auto-responding. Default: false
637
+ optionsSuccessStatus: 204 // status code for successful preflight responses. Default: 204
638
+ }));
639
+ ```
640
+
641
+ Or if you don't care and want to allow everything with the default settings, just do:
642
+
643
+ ```javascript
644
+ server.beforeEach(cors());
645
+ ```
646
+
369
647
  ## Complete Example
370
648
 
371
- Here you can see all the features that Cpeak offers, in one small piece of code:
649
+ Here you can see all the features that Cpeak offers (excluding the authentication features), in one small piece of code:
372
650
 
373
651
  ```javascript
374
- import cpeak, { serveStatic, parseJSON, render } from "cpeak";
652
+ import cpeak, { serveStatic, parseJSON, render, cookieParser, cors } from "cpeak";
375
653
 
376
654
  const server = cpeak();
377
655
 
@@ -386,6 +664,16 @@ server.beforeEach(render());
386
664
  // For parsing JSON bodies
387
665
  server.beforeEach(parseJSON());
388
666
 
667
+ // For reading and setting cookies
668
+ server.beforeEach(cookieParser({ secret: "your-secret-key" }));
669
+
670
+ // For enabling CORS
671
+ server.beforeEach(cors({
672
+ origin: "http://localhost:3000",
673
+ credentials: true,
674
+ methods: "GET,POST,PUT,DELETE"
675
+ }));
676
+
389
677
  // Adding custom middleware functions
390
678
  server.beforeEach((req, res, next) => {
391
679
  req.custom = "This is some string";
@@ -437,6 +725,16 @@ server.route("get", "/api/document/:title", testRouteMiddleware, (req, res) => {
437
725
  res.status(200).json({ message: "This is a test response" });
438
726
  });
439
727
 
728
+ // Reading and setting cookies
729
+ server.route("post", "/login", (req, res) => {
730
+ // Reads are available via req.cookies and req.signedCookies
731
+ const sessionId = req.signedCookies.sessionId;
732
+
733
+ // Set a signed session cookie
734
+ res.cookie("sessionId", "abc123", { signed: true, httpOnly: true, secure: true });
735
+ res.status(200).json({ message: "Logged in" });
736
+ });
737
+
440
738
  // Sending a file response
441
739
  server.route("get", "/file", (req, res) => {
442
740
  // Make sure to specify a correct path and MIME type...
package/dist/index.d.ts CHANGED
@@ -1,18 +1,75 @@
1
+ import * as net from 'net';
1
2
  import http, { IncomingMessage, ServerResponse } from 'node:http';
3
+ import { Readable } from 'node:stream';
4
+ import { Buffer } from 'node:buffer';
5
+ import * as node_zlib from 'node:zlib';
2
6
 
3
- type Cpeak$1 = ReturnType<typeof cpeak>;
7
+ interface PbkdfOptions {
8
+ iterations?: number;
9
+ keylen?: number;
10
+ digest?: string;
11
+ saltSize?: number;
12
+ }
13
+ interface AuthOptions extends PbkdfOptions {
14
+ secret: string;
15
+ saveToken: (tokenId: string, userId: string, expiresAt: Date) => Promise<void>;
16
+ findToken: (tokenId: string) => Promise<{
17
+ userId: string;
18
+ expiresAt: Date;
19
+ } | null>;
20
+ tokenExpiry?: number;
21
+ hmacAlgorithm?: string;
22
+ tokenIdSize?: number;
23
+ revokeToken?: (tokenId: string) => Promise<void>;
24
+ }
25
+ interface CookieOptions {
26
+ signed?: boolean;
27
+ httpOnly?: boolean;
28
+ secure?: boolean;
29
+ sameSite?: "strict" | "lax" | "none";
30
+ maxAge?: number;
31
+ expires?: Date;
32
+ path?: string;
33
+ domain?: string;
34
+ }
35
+ type OriginInput = string | string[] | RegExp | boolean | ((origin: string | undefined) => boolean | Promise<boolean>);
36
+ interface CorsOptions {
37
+ origin?: OriginInput;
38
+ methods?: string | string[];
39
+ allowedHeaders?: string | string[];
40
+ exposedHeaders?: string | string[];
41
+ credentials?: boolean;
42
+ maxAge?: number;
43
+ preflightContinue?: boolean;
44
+ optionsSuccessStatus?: number;
45
+ }
46
+ interface CompressionOptions {
47
+ threshold?: number;
48
+ brotli?: node_zlib.BrotliOptions;
49
+ gzip?: node_zlib.ZlibOptions;
50
+ deflate?: node_zlib.ZlibOptions;
51
+ }
52
+
53
+ interface CpeakOptions {
54
+ compression?: boolean | CompressionOptions;
55
+ }
4
56
  type StringMap = Record<string, string>;
5
57
  interface CpeakRequest<ReqBody = any, ReqQueries = any> extends IncomingMessage {
6
58
  params: StringMap;
7
59
  query: ReqQueries;
8
60
  body?: ReqBody;
61
+ cookies?: StringMap;
62
+ signedCookies?: Record<string, string | false>;
9
63
  [key: string]: any;
10
64
  }
11
65
  interface CpeakResponse extends ServerResponse {
12
66
  sendFile: (path: string, mime: string) => Promise<void>;
13
67
  status: (code: number) => CpeakResponse;
14
- redirect: (location: string) => CpeakResponse;
15
- json: (data: any) => void;
68
+ attachment: (filename?: string) => CpeakResponse;
69
+ cookie: (name: string, value: string, options?: any) => CpeakResponse;
70
+ redirect: (location: string) => void;
71
+ json: (data: any) => void | Promise<void>;
72
+ compress: (mime: string, body: Buffer | string | Readable, size?: number) => Promise<void>;
16
73
  [key: string]: any;
17
74
  }
18
75
  type Next = (err?: any) => void;
@@ -30,14 +87,28 @@ interface RoutesMap {
30
87
  [method: string]: Route[];
31
88
  }
32
89
 
33
- declare const serveStatic: (folderPath: string, newMimeTypes?: StringMap) => (req: CpeakRequest, res: CpeakResponse, next: Next) => void | Promise<void>;
34
-
35
90
  declare const parseJSON: (options?: {
36
91
  limit?: number;
37
92
  }) => (req: CpeakRequest, res: CpeakResponse, next: Next) => void;
38
93
 
94
+ declare const serveStatic: (folderPath: string, newMimeTypes?: StringMap, options?: {
95
+ prefix?: string;
96
+ }) => (req: CpeakRequest, res: CpeakResponse, next: Next) => void | Promise<void>;
97
+
39
98
  declare const render: () => (req: CpeakRequest, res: CpeakResponse, next: Next) => void;
40
99
 
100
+ declare const swagger: (spec: object, prefix?: string) => (req: CpeakRequest, res: CpeakResponse, next: Next) => void | Promise<void>;
101
+
102
+ declare function hashPassword(password: string, options?: PbkdfOptions): Promise<string>;
103
+ declare function verifyPassword(password: string, stored: string): Promise<boolean>;
104
+ declare function auth(options: AuthOptions): Middleware;
105
+
106
+ declare function cookieParser(options?: {
107
+ secret?: string;
108
+ }): (req: CpeakRequest, res: CpeakResponse, next: Next) => void;
109
+
110
+ declare const cors: (options?: CorsOptions) => (req: CpeakRequest, res: CpeakResponse, next: Next) => Promise<void>;
111
+
41
112
  declare function frameworkError(message: string, skipFn: Function, code?: string, status?: number): Error & {
42
113
  code?: string;
43
114
  cpeak_err?: boolean;
@@ -48,34 +119,35 @@ declare enum ErrorCode {
48
119
  NOT_A_FILE = "CPEAK_ERR_NOT_A_FILE",
49
120
  SEND_FILE_FAIL = "CPEAK_ERR_SEND_FILE_FAIL",
50
121
  INVALID_JSON = "CPEAK_ERR_INVALID_JSON",
51
- PAYLOAD_TOO_LARGE = "CPEAK_ERR_PAYLOAD_TOO_LARGE"
122
+ PAYLOAD_TOO_LARGE = "CPEAK_ERR_PAYLOAD_TOO_LARGE",
123
+ WEAK_SECRET = "CPEAK_ERR_WEAK_SECRET",
124
+ COMPRESSION_NOT_ENABLED = "CPEAK_ERR_COMPRESSION_NOT_ENABLED"
52
125
  }
53
126
  declare class CpeakIncomingMessage extends http.IncomingMessage {
127
+ #private;
54
128
  body: any;
55
129
  params: StringMap;
56
- private _query?;
57
130
  get query(): StringMap;
58
131
  }
59
132
  declare class CpeakServerResponse extends http.ServerResponse<CpeakIncomingMessage> {
60
133
  sendFile(path: string, mime: string): Promise<void>;
61
134
  status(code: number): this;
62
- redirect(location: string): this;
63
- json(data: any): void;
135
+ attachment(filename?: string): this;
136
+ redirect(location: string): void;
137
+ json(data: any): void | Promise<void>;
138
+ compress(mime: string, body: Buffer | string | Readable, size?: number): Promise<void>;
64
139
  }
65
140
  declare class Cpeak {
66
141
  #private;
67
- private server;
68
- private routes;
69
- private middleware;
70
- private _handleErr?;
71
- constructor();
142
+ constructor(options?: CpeakOptions);
72
143
  route(method: string, path: string, ...args: (RouteMiddleware | Handler)[]): void;
73
144
  beforeEach(cb: Middleware): void;
74
145
  handleErr(cb: (err: unknown, req: CpeakRequest, res: CpeakResponse) => void): void;
75
146
  listen(port: number, cb?: () => void): http.Server<typeof CpeakIncomingMessage, typeof CpeakServerResponse>;
147
+ address(): string | net.AddressInfo | null;
76
148
  close(cb?: (err?: Error) => void): void;
77
149
  }
78
150
 
79
- declare function cpeak(): Cpeak;
151
+ declare function cpeak(options?: CpeakOptions): Cpeak;
80
152
 
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 };
153
+ export { type AuthOptions, type CompressionOptions, type CookieOptions, type CorsOptions, Cpeak, CpeakIncomingMessage, type CpeakOptions, type CpeakRequest, type CpeakResponse, CpeakServerResponse, ErrorCode, type HandleErr, type Handler, type Middleware, type Next, type PbkdfOptions, type RouteMiddleware, type RoutesMap, auth, cookieParser, cors, cpeak as default, frameworkError, hashPassword, parseJSON, render, serveStatic, swagger, verifyPassword };