cpeak 2.6.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/lib/types.ts CHANGED
@@ -1,7 +1,15 @@
1
1
  import { IncomingMessage, ServerResponse } from "node:http";
2
+ import type { Readable } from "node:stream";
3
+ import type { Buffer } from "node:buffer";
4
+ import type { CompressionOptions } from "./internal/types";
2
5
 
3
6
  export type { Cpeak } from "./index";
4
7
 
8
+ // For constructor options passed to `cpeak()`
9
+ export interface CpeakOptions {
10
+ compression?: boolean | CompressionOptions;
11
+ }
12
+
5
13
  // Extending Node.js's Request and Response objects to add our custom properties
6
14
  export type StringMap = Record<string, string>;
7
15
 
@@ -23,7 +31,12 @@ export interface CpeakResponse extends ServerResponse {
23
31
  attachment: (filename?: string) => CpeakResponse;
24
32
  cookie: (name: string, value: string, options?: any) => CpeakResponse;
25
33
  redirect: (location: string) => void;
26
- json: (data: any) => void;
34
+ json: (data: any) => void | Promise<void>; // sync when compression is off, async when enabled
35
+ compress: (
36
+ mime: string,
37
+ body: Buffer | string | Readable,
38
+ size?: number
39
+ ) => Promise<void>;
27
40
  [key: string]: any; // allow developers to add their onw extensions (e.g. res.test)
28
41
  }
29
42
 
package/lib/utils/auth.ts CHANGED
@@ -2,6 +2,7 @@ import { randomBytes, pbkdf2, createHmac, timingSafeEqual } from "node:crypto";
2
2
  import { promisify } from "node:util";
3
3
  import type { Middleware } from "../types";
4
4
  import { frameworkError, ErrorCode } from "../index";
5
+ import type { AuthOptions, PbkdfOptions } from "./types";
5
6
 
6
7
  const pbkdf2Async = promisify(pbkdf2);
7
8
 
@@ -15,29 +16,6 @@ const DEFAULTS = {
15
16
  tokenExpiry: 7 * 24 * 60 * 60 * 1000 // 7 days in ms
16
17
  } as const;
17
18
 
18
- export interface PbkdfOptions {
19
- iterations?: number;
20
- keylen?: number;
21
- digest?: string;
22
- saltSize?: number;
23
- }
24
-
25
- export interface AuthOptions extends PbkdfOptions {
26
- secret: string;
27
- saveToken: (
28
- tokenId: string,
29
- userId: string,
30
- expiresAt: Date
31
- ) => Promise<void>;
32
- findToken: (
33
- tokenId: string
34
- ) => Promise<{ userId: string; expiresAt: Date } | null>;
35
- tokenExpiry?: number;
36
- hmacAlgorithm?: string;
37
- tokenIdSize?: number;
38
- revokeToken?: (tokenId: string) => Promise<void>;
39
- }
40
-
41
19
  export async function hashPassword(
42
20
  password: string,
43
21
  options?: PbkdfOptions
@@ -1,17 +1,7 @@
1
1
  import { createHmac, timingSafeEqual } from "node:crypto";
2
2
  import type { CpeakRequest, CpeakResponse, Next } from "../types";
3
3
  import { frameworkError, ErrorCode } from "../index";
4
-
5
- export interface CookieOptions {
6
- signed?: boolean;
7
- httpOnly?: boolean;
8
- secure?: boolean;
9
- sameSite?: "strict" | "lax" | "none";
10
- maxAge?: number; // ms
11
- expires?: Date;
12
- path?: string;
13
- domain?: string;
14
- }
4
+ import type { CookieOptions } from "./types";
15
5
 
16
6
  // This will sign the cookie value with HMAC with the secret.
17
7
  // Ideal for data like user IDs or session IDs, where you want to ensure the integrity of the cookie value without encryption.
@@ -0,0 +1,109 @@
1
+ import type { CpeakRequest, CpeakResponse, Next } from "../types";
2
+ import type { CorsOptions, OriginInput } from "./types";
3
+
4
+ // Append a value to an existing header without overwriting prior entries
5
+ // (e.g. compression already sets `Vary: Accept-Encoding`).
6
+ function appendVary(res: CpeakResponse, value: string) {
7
+ const existing = res.getHeader("Vary");
8
+ if (!existing) return res.setHeader("Vary", value);
9
+ const current = String(existing)
10
+ .split(",")
11
+ .map((s) => s.trim())
12
+ .filter(Boolean);
13
+ if (current.includes("*") || current.includes(value)) return;
14
+ res.setHeader("Vary", [...current, value].join(", "));
15
+ }
16
+
17
+ // Determine if the given origin is allowed based on the rule.
18
+ // Examples of what developers can specify for the rule:
19
+ // - `true` or `*`: allow all origins
20
+ // - `false`: disallow all origins
21
+ // - `"https://example.com"`: allow only this origin
22
+ // - `["https://example.com", "https://foo.com"]`: allow these origins
23
+ // - `/\.example\.com$/`: allow origins that match this regex
24
+ // - `(origin) => origin === "https://example.com"`: custom function to determine if the origin is allowed
25
+ async function isAllowed(
26
+ origin: string | undefined,
27
+ rule: OriginInput
28
+ ): Promise<boolean> {
29
+ if (rule === true || rule === "*") return true;
30
+ if (rule === false || !origin) return false;
31
+ if (typeof rule === "string") return rule === origin;
32
+ if (Array.isArray(rule)) return rule.includes(origin);
33
+ if (rule instanceof RegExp) return rule.test(origin);
34
+ if (typeof rule === "function") return await rule(origin);
35
+ return false;
36
+ }
37
+
38
+ const cors = (options: CorsOptions = {}) => {
39
+ const {
40
+ origin = "*",
41
+ methods = "GET,HEAD,PUT,PATCH,POST,DELETE",
42
+ allowedHeaders,
43
+ exposedHeaders,
44
+ credentials = false,
45
+ maxAge = 86400,
46
+ preflightContinue = false,
47
+ optionsSuccessStatus = 204
48
+ } = options;
49
+
50
+ const methodsStr = Array.isArray(methods) ? methods.join(",") : methods;
51
+ const allowedHeadersStr = Array.isArray(allowedHeaders)
52
+ ? allowedHeaders.join(",")
53
+ : allowedHeaders;
54
+ const exposedHeadersStr = Array.isArray(exposedHeaders)
55
+ ? exposedHeaders.join(",")
56
+ : exposedHeaders;
57
+
58
+ return async (req: CpeakRequest, res: CpeakResponse, next: Next) => {
59
+ const requestOrigin = req.headers.origin;
60
+
61
+ // Not a CORS request, nothing to do.
62
+ if (!requestOrigin) return next();
63
+
64
+ const allowed = await isAllowed(requestOrigin, origin);
65
+ if (!allowed) return next();
66
+
67
+ // We cannot combine Access-Control-Allow-Origin: * with
68
+ // Access-Control-Allow-Credentials: true. Browsers will flat-out reject it.
69
+ // Instead we'll reflect the origin.
70
+ const allowOriginValue =
71
+ origin === "*" && !credentials ? "*" : requestOrigin;
72
+ res.setHeader("Access-Control-Allow-Origin", allowOriginValue);
73
+ if (allowOriginValue !== "*") appendVary(res, "Origin");
74
+
75
+ if (credentials) res.setHeader("Access-Control-Allow-Credentials", "true");
76
+ if (exposedHeadersStr)
77
+ res.setHeader("Access-Control-Expose-Headers", exposedHeadersStr);
78
+
79
+ const isPreflight =
80
+ req.method === "OPTIONS" &&
81
+ req.headers["access-control-request-method"] !== undefined;
82
+
83
+ if (!isPreflight) return next();
84
+
85
+ res.setHeader("Access-Control-Allow-Methods", methodsStr);
86
+
87
+ if (allowedHeadersStr) {
88
+ res.setHeader("Access-Control-Allow-Headers", allowedHeadersStr);
89
+ } else if (origin === "*") {
90
+ // If origin is *, just act like an echo chamber for requested headers. Give back whatever the browser asks for.
91
+ const requested = req.headers["access-control-request-headers"];
92
+ if (requested) res.setHeader("Access-Control-Allow-Headers", requested);
93
+ } else {
94
+ res.setHeader(
95
+ "Access-Control-Allow-Headers",
96
+ "Content-Type, Authorization"
97
+ );
98
+ }
99
+
100
+ res.setHeader("Access-Control-Max-Age", String(maxAge));
101
+
102
+ if (preflightContinue) return next();
103
+
104
+ res.statusCode = optionsSuccessStatus;
105
+ res.end();
106
+ };
107
+ };
108
+
109
+ export { cors };
@@ -3,9 +3,8 @@ import { serveStatic } from "./serveStatic";
3
3
  import { render } from "./render";
4
4
  import { swagger } from "./swagger";
5
5
  import { auth, hashPassword, verifyPassword } from "./auth";
6
- import type { AuthOptions, PbkdfOptions } from "./auth";
7
6
  import { cookieParser } from "./cookieParser";
8
- import type { CookieOptions } from "./cookieParser";
7
+ import { cors } from "./cors";
9
8
 
10
9
  export {
11
10
  serveStatic,
@@ -15,6 +14,6 @@ export {
15
14
  auth,
16
15
  hashPassword,
17
16
  verifyPassword,
18
- cookieParser
17
+ cookieParser,
18
+ cors
19
19
  };
20
- export type { AuthOptions, PbkdfOptions, CookieOptions };
@@ -1,5 +1,6 @@
1
1
  import fs from "node:fs/promises";
2
2
  import { frameworkError } from "../";
3
+ import { compressAndSend } from "../internal/compression";
3
4
  import type { CpeakRequest, CpeakResponse, Next } from "../types";
4
5
 
5
6
  function renderTemplate(
@@ -70,6 +71,12 @@ const render = () => {
70
71
 
71
72
  let fileStr = await fs.readFile(path, "utf-8");
72
73
  const finalStr = renderTemplate(fileStr, data);
74
+
75
+ if (res._compression) {
76
+ await compressAndSend(res, mime, finalStr, res._compression);
77
+ return;
78
+ }
79
+
73
80
  res.setHeader("Content-Type", mime);
74
81
  res.end(finalStr);
75
82
  };
@@ -0,0 +1,51 @@
1
+ export interface PbkdfOptions {
2
+ iterations?: number;
3
+ keylen?: number;
4
+ digest?: string;
5
+ saltSize?: number;
6
+ }
7
+
8
+ export interface AuthOptions extends PbkdfOptions {
9
+ secret: string;
10
+ saveToken: (
11
+ tokenId: string,
12
+ userId: string,
13
+ expiresAt: Date
14
+ ) => Promise<void>;
15
+ findToken: (
16
+ tokenId: string
17
+ ) => Promise<{ userId: string; expiresAt: Date } | null>;
18
+ tokenExpiry?: number;
19
+ hmacAlgorithm?: string;
20
+ tokenIdSize?: number;
21
+ revokeToken?: (tokenId: string) => Promise<void>;
22
+ }
23
+
24
+ export interface CookieOptions {
25
+ signed?: boolean;
26
+ httpOnly?: boolean;
27
+ secure?: boolean;
28
+ sameSite?: "strict" | "lax" | "none";
29
+ maxAge?: number; // ms
30
+ expires?: Date;
31
+ path?: string;
32
+ domain?: string;
33
+ }
34
+
35
+ export type OriginInput =
36
+ | string
37
+ | string[]
38
+ | RegExp
39
+ | boolean
40
+ | ((origin: string | undefined) => boolean | Promise<boolean>);
41
+
42
+ export interface CorsOptions {
43
+ origin?: OriginInput;
44
+ methods?: string | string[];
45
+ allowedHeaders?: string | string[];
46
+ exposedHeaders?: string | string[];
47
+ credentials?: boolean;
48
+ maxAge?: number;
49
+ preflightContinue?: boolean;
50
+ optionsSuccessStatus?: number;
51
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cpeak",
3
- "version": "2.6.0",
3
+ "version": "2.7.0",
4
4
  "description": "A minimal and fast Node.js HTTP framework.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -10,12 +10,12 @@
10
10
  },
11
11
  "repository": {
12
12
  "type": "git",
13
- "url": "https://github.com/agile8118/cpeak.git"
13
+ "url": "https://github.com/Cododev-Technology/cpeak.git"
14
14
  },
15
15
  "bugs": {
16
- "url": "https://github.com/agile8118/cpeak/issues"
16
+ "url": "https://github.com/Cododev-Technology/cpeak/issues"
17
17
  },
18
- "homepage": "https://github.com/agile8118/cpeak#readme",
18
+ "homepage": "https://github.com/Cododev-Technology/cpeak#readme",
19
19
  "main": "./dist/index.js",
20
20
  "module": "./dist/index.js",
21
21
  "types": "./dist/index.d.ts",