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/README.md +81 -1
- package/dist/index.d.ts +64 -35
- package/dist/index.js +221 -8
- package/dist/index.js.map +1 -1
- package/lib/index.ts +74 -10
- package/lib/internal/compression.ts +180 -0
- package/lib/internal/types.ts +10 -0
- package/lib/types.ts +14 -1
- package/lib/utils/auth.ts +1 -23
- package/lib/utils/cookieParser.ts +1 -11
- package/lib/utils/cors.ts +109 -0
- package/lib/utils/index.ts +3 -4
- package/lib/utils/render.ts +7 -0
- package/lib/utils/types.ts +51 -0
- package/package.json +4 -4
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 };
|
package/lib/utils/index.ts
CHANGED
|
@@ -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
|
|
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 };
|
package/lib/utils/render.ts
CHANGED
|
@@ -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.
|
|
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/
|
|
13
|
+
"url": "https://github.com/Cododev-Technology/cpeak.git"
|
|
14
14
|
},
|
|
15
15
|
"bugs": {
|
|
16
|
-
"url": "https://github.com/
|
|
16
|
+
"url": "https://github.com/Cododev-Technology/cpeak/issues"
|
|
17
17
|
},
|
|
18
|
-
"homepage": "https://github.com/
|
|
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",
|