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/README.md
CHANGED
|
@@ -28,6 +28,7 @@ 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)
|
|
@@ -37,6 +38,7 @@ This is an educational project that was started as part of the [Understanding No
|
|
|
37
38
|
- [cookieParser](#cookieparser)
|
|
38
39
|
- [swagger](#swagger)
|
|
39
40
|
- [auth](#auth)
|
|
41
|
+
- [cors](#cors)
|
|
40
42
|
- [Complete Example](#complete-example)
|
|
41
43
|
- [Versioning Notice](#versioning-notice)
|
|
42
44
|
|
|
@@ -206,6 +208,54 @@ If you want to redirect to a new URL, you can simply do:
|
|
|
206
208
|
res.redirect("https://whatever.com");
|
|
207
209
|
```
|
|
208
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
|
+
|
|
209
259
|
### Error Handling
|
|
210
260
|
|
|
211
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:
|
|
@@ -273,6 +323,7 @@ The list of utility functions as of now:
|
|
|
273
323
|
- cookieParser
|
|
274
324
|
- swagger
|
|
275
325
|
- auth
|
|
326
|
+
- cors
|
|
276
327
|
|
|
277
328
|
Including any one of them is done like this:
|
|
278
329
|
|
|
@@ -571,12 +622,34 @@ For complete working examples, see:
|
|
|
571
622
|
- [`examples/auth-localstorage.js`](examples/auth-localstorage.js) — token sent via the `Authorization` header (suited for SPAs and mobile clients)
|
|
572
623
|
- [`examples/auth-cookies.js`](examples/auth-cookies.js) — token stored in an `httpOnly` cookie (protects against XSS)
|
|
573
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
|
+
|
|
574
647
|
## Complete Example
|
|
575
648
|
|
|
576
649
|
Here you can see all the features that Cpeak offers (excluding the authentication features), in one small piece of code:
|
|
577
650
|
|
|
578
651
|
```javascript
|
|
579
|
-
import cpeak, { serveStatic, parseJSON, render, cookieParser } from "cpeak";
|
|
652
|
+
import cpeak, { serveStatic, parseJSON, render, cookieParser, cors } from "cpeak";
|
|
580
653
|
|
|
581
654
|
const server = cpeak();
|
|
582
655
|
|
|
@@ -594,6 +667,13 @@ server.beforeEach(parseJSON());
|
|
|
594
667
|
// For reading and setting cookies
|
|
595
668
|
server.beforeEach(cookieParser({ secret: "your-secret-key" }));
|
|
596
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
|
+
|
|
597
677
|
// Adding custom middleware functions
|
|
598
678
|
server.beforeEach((req, res, next) => {
|
|
599
679
|
req.custom = "This is some string";
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
import * as net from 'net';
|
|
2
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';
|
|
3
6
|
|
|
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;
|
|
@@ -16,7 +68,8 @@ interface CpeakResponse extends ServerResponse {
|
|
|
16
68
|
attachment: (filename?: string) => CpeakResponse;
|
|
17
69
|
cookie: (name: string, value: string, options?: any) => CpeakResponse;
|
|
18
70
|
redirect: (location: string) => void;
|
|
19
|
-
json: (data: any) => void
|
|
71
|
+
json: (data: any) => void | Promise<void>;
|
|
72
|
+
compress: (mime: string, body: Buffer | string | Readable, size?: number) => Promise<void>;
|
|
20
73
|
[key: string]: any;
|
|
21
74
|
}
|
|
22
75
|
type Next = (err?: any) => void;
|
|
@@ -44,44 +97,18 @@ declare const serveStatic: (folderPath: string, newMimeTypes?: StringMap, option
|
|
|
44
97
|
|
|
45
98
|
declare const render: () => (req: CpeakRequest, res: CpeakResponse, next: Next) => void;
|
|
46
99
|
|
|
47
|
-
declare const swagger: (spec: object, prefix?: string) => (req: CpeakRequest, res: CpeakResponse, next: Next) => void
|
|
100
|
+
declare const swagger: (spec: object, prefix?: string) => (req: CpeakRequest, res: CpeakResponse, next: Next) => void | Promise<void>;
|
|
48
101
|
|
|
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
102
|
declare function hashPassword(password: string, options?: PbkdfOptions): Promise<string>;
|
|
68
103
|
declare function verifyPassword(password: string, stored: string): Promise<boolean>;
|
|
69
104
|
declare function auth(options: AuthOptions): Middleware;
|
|
70
105
|
|
|
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
106
|
declare function cookieParser(options?: {
|
|
82
107
|
secret?: string;
|
|
83
108
|
}): (req: CpeakRequest, res: CpeakResponse, next: Next) => void;
|
|
84
109
|
|
|
110
|
+
declare const cors: (options?: CorsOptions) => (req: CpeakRequest, res: CpeakResponse, next: Next) => Promise<void>;
|
|
111
|
+
|
|
85
112
|
declare function frameworkError(message: string, skipFn: Function, code?: string, status?: number): Error & {
|
|
86
113
|
code?: string;
|
|
87
114
|
cpeak_err?: boolean;
|
|
@@ -93,7 +120,8 @@ declare enum ErrorCode {
|
|
|
93
120
|
SEND_FILE_FAIL = "CPEAK_ERR_SEND_FILE_FAIL",
|
|
94
121
|
INVALID_JSON = "CPEAK_ERR_INVALID_JSON",
|
|
95
122
|
PAYLOAD_TOO_LARGE = "CPEAK_ERR_PAYLOAD_TOO_LARGE",
|
|
96
|
-
WEAK_SECRET = "CPEAK_ERR_WEAK_SECRET"
|
|
123
|
+
WEAK_SECRET = "CPEAK_ERR_WEAK_SECRET",
|
|
124
|
+
COMPRESSION_NOT_ENABLED = "CPEAK_ERR_COMPRESSION_NOT_ENABLED"
|
|
97
125
|
}
|
|
98
126
|
declare class CpeakIncomingMessage extends http.IncomingMessage {
|
|
99
127
|
#private;
|
|
@@ -106,11 +134,12 @@ declare class CpeakServerResponse extends http.ServerResponse<CpeakIncomingMessa
|
|
|
106
134
|
status(code: number): this;
|
|
107
135
|
attachment(filename?: string): this;
|
|
108
136
|
redirect(location: string): void;
|
|
109
|
-
json(data: any): void
|
|
137
|
+
json(data: any): void | Promise<void>;
|
|
138
|
+
compress(mime: string, body: Buffer | string | Readable, size?: number): Promise<void>;
|
|
110
139
|
}
|
|
111
140
|
declare class Cpeak {
|
|
112
141
|
#private;
|
|
113
|
-
constructor();
|
|
142
|
+
constructor(options?: CpeakOptions);
|
|
114
143
|
route(method: string, path: string, ...args: (RouteMiddleware | Handler)[]): void;
|
|
115
144
|
beforeEach(cb: Middleware): void;
|
|
116
145
|
handleErr(cb: (err: unknown, req: CpeakRequest, res: CpeakResponse) => void): void;
|
|
@@ -119,6 +148,6 @@ declare class Cpeak {
|
|
|
119
148
|
close(cb?: (err?: Error) => void): void;
|
|
120
149
|
}
|
|
121
150
|
|
|
122
|
-
declare function cpeak(): Cpeak;
|
|
151
|
+
declare function cpeak(options?: CpeakOptions): Cpeak;
|
|
123
152
|
|
|
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 };
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -2,10 +2,120 @@
|
|
|
2
2
|
import http from "http";
|
|
3
3
|
import fs3 from "fs/promises";
|
|
4
4
|
import { createReadStream } from "fs";
|
|
5
|
+
import { pipeline as pipeline2 } from "stream/promises";
|
|
6
|
+
|
|
7
|
+
// lib/utils/compression.ts
|
|
8
|
+
import zlib from "zlib";
|
|
9
|
+
import { Readable } from "stream";
|
|
10
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
5
11
|
import { pipeline } from "stream/promises";
|
|
12
|
+
var COMPRESSIBLE_TYPE = /text|json|javascript|css|xml|svg/i;
|
|
13
|
+
var NO_TRANSFORM = /(?:^|,)\s*no-transform\s*(?:,|$)/i;
|
|
14
|
+
function pickEncoding(header) {
|
|
15
|
+
if (!header) return null;
|
|
16
|
+
const accepted = {};
|
|
17
|
+
let wildcard;
|
|
18
|
+
for (const part of header.split(",")) {
|
|
19
|
+
const [rawName, ...params] = part.trim().split(";");
|
|
20
|
+
const name = rawName.trim().toLowerCase();
|
|
21
|
+
if (!name) continue;
|
|
22
|
+
let q = 1;
|
|
23
|
+
for (const p of params) {
|
|
24
|
+
const m = p.trim().match(/^q=([\d.]+)$/i);
|
|
25
|
+
if (m) q = Number(m[1]);
|
|
26
|
+
}
|
|
27
|
+
if (Number.isNaN(q)) q = 0;
|
|
28
|
+
if (name === "*") wildcard = q;
|
|
29
|
+
else accepted[name] = q;
|
|
30
|
+
}
|
|
31
|
+
const tryPick = (enc) => {
|
|
32
|
+
const q = enc in accepted ? accepted[enc] : wildcard;
|
|
33
|
+
return q !== void 0 && q > 0;
|
|
34
|
+
};
|
|
35
|
+
if (tryPick("br")) return "br";
|
|
36
|
+
if (tryPick("gzip")) return "gzip";
|
|
37
|
+
if (tryPick("deflate")) return "deflate";
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
function appendVary(res, value) {
|
|
41
|
+
const existing = res.getHeader("Vary");
|
|
42
|
+
if (!existing) return res.setHeader("Vary", value);
|
|
43
|
+
const current = String(existing).split(",").map((s) => s.trim()).filter(Boolean);
|
|
44
|
+
if (current.includes("*") || current.some((v) => v.toLowerCase() === value.toLowerCase()))
|
|
45
|
+
return;
|
|
46
|
+
res.setHeader("Vary", [...current, value].join(", "));
|
|
47
|
+
}
|
|
48
|
+
function brotliOptsFor(config) {
|
|
49
|
+
const userBrotli = config.brotli || {};
|
|
50
|
+
return {
|
|
51
|
+
...userBrotli,
|
|
52
|
+
params: {
|
|
53
|
+
[zlib.constants.BROTLI_PARAM_QUALITY]: 4,
|
|
54
|
+
...userBrotli.params || {}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function createCompressorStream(encoding, config) {
|
|
59
|
+
if (encoding === "br")
|
|
60
|
+
return zlib.createBrotliCompress(brotliOptsFor(config));
|
|
61
|
+
if (encoding === "gzip") return zlib.createGzip(config.gzip);
|
|
62
|
+
return zlib.createDeflate(config.deflate);
|
|
63
|
+
}
|
|
64
|
+
function negotiate(res, mime, size, config) {
|
|
65
|
+
if (!COMPRESSIBLE_TYPE.test(mime)) return { encoding: null, eligible: false };
|
|
66
|
+
if (res.req?.method === "HEAD") return { encoding: null, eligible: false };
|
|
67
|
+
const cc = res.getHeader("Cache-Control");
|
|
68
|
+
if (cc && NO_TRANSFORM.test(String(cc)))
|
|
69
|
+
return { encoding: null, eligible: false };
|
|
70
|
+
const existing = res.getHeader("Content-Encoding");
|
|
71
|
+
if (existing && existing !== "identity")
|
|
72
|
+
return { encoding: null, eligible: false };
|
|
73
|
+
if (size < config.threshold) return { encoding: null, eligible: true };
|
|
74
|
+
const encoding = pickEncoding(
|
|
75
|
+
String(res.req?.headers["accept-encoding"] || "")
|
|
76
|
+
);
|
|
77
|
+
return { encoding, eligible: true };
|
|
78
|
+
}
|
|
79
|
+
function bodyAsReadable(body) {
|
|
80
|
+
if (Buffer2.isBuffer(body)) return Readable.from([body]);
|
|
81
|
+
if (typeof body === "string") return Readable.from([Buffer2.from(body)]);
|
|
82
|
+
return body;
|
|
83
|
+
}
|
|
84
|
+
function resolveCompressionOptions(input) {
|
|
85
|
+
const options = input === true ? {} : input;
|
|
86
|
+
return {
|
|
87
|
+
threshold: options.threshold ?? 1024,
|
|
88
|
+
brotli: options.brotli ?? {},
|
|
89
|
+
gzip: options.gzip ?? {},
|
|
90
|
+
deflate: options.deflate ?? {}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
async function compressAndSend(res, mime, body, config, size) {
|
|
94
|
+
res.setHeader("Content-Type", mime);
|
|
95
|
+
const knownSize = Buffer2.isBuffer(body) ? body.length : typeof body === "string" ? Buffer2.byteLength(body) : size ?? Infinity;
|
|
96
|
+
const { encoding, eligible } = negotiate(res, mime, knownSize, config);
|
|
97
|
+
if (!encoding) {
|
|
98
|
+
if (eligible) appendVary(res, "Accept-Encoding");
|
|
99
|
+
if (Buffer2.isBuffer(body) || typeof body === "string") {
|
|
100
|
+
res.setHeader("Content-Length", String(knownSize));
|
|
101
|
+
res.end(body);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (size !== void 0) res.setHeader("Content-Length", String(size));
|
|
105
|
+
await pipeline(body, res);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
res.setHeader("Content-Encoding", encoding);
|
|
109
|
+
appendVary(res, "Accept-Encoding");
|
|
110
|
+
await pipeline(
|
|
111
|
+
bodyAsReadable(body),
|
|
112
|
+
createCompressorStream(encoding, config),
|
|
113
|
+
res
|
|
114
|
+
);
|
|
115
|
+
}
|
|
6
116
|
|
|
7
117
|
// lib/utils/parseJSON.ts
|
|
8
|
-
import { Buffer as
|
|
118
|
+
import { Buffer as Buffer3 } from "buffer";
|
|
9
119
|
function isJSON(contentType) {
|
|
10
120
|
if (!contentType) return false;
|
|
11
121
|
if (contentType === "application/json") return true;
|
|
@@ -38,7 +148,7 @@ var parseJSON = (options = {}) => {
|
|
|
38
148
|
};
|
|
39
149
|
const onEnd = () => {
|
|
40
150
|
try {
|
|
41
|
-
const rawBody = chunks.length === 1 ? chunks[0].toString("utf-8") :
|
|
151
|
+
const rawBody = chunks.length === 1 ? chunks[0].toString("utf-8") : Buffer3.concat(chunks).toString("utf-8");
|
|
42
152
|
req.body = rawBody ? JSON.parse(rawBody) : {};
|
|
43
153
|
next();
|
|
44
154
|
} catch (err) {
|
|
@@ -160,6 +270,11 @@ var render = () => {
|
|
|
160
270
|
}
|
|
161
271
|
let fileStr = await fs2.readFile(path2, "utf-8");
|
|
162
272
|
const finalStr = renderTemplate(fileStr, data);
|
|
273
|
+
const config = res.socket?.server?._cpeakCompression;
|
|
274
|
+
if (config) {
|
|
275
|
+
await compressAndSend(res, mime, finalStr, config);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
163
278
|
res.setHeader("Content-Type", mime);
|
|
164
279
|
res.end(finalStr);
|
|
165
280
|
};
|
|
@@ -424,6 +539,69 @@ function cookieParser(options = {}) {
|
|
|
424
539
|
};
|
|
425
540
|
}
|
|
426
541
|
|
|
542
|
+
// lib/utils/cors.ts
|
|
543
|
+
function appendVary2(res, value) {
|
|
544
|
+
const existing = res.getHeader("Vary");
|
|
545
|
+
if (!existing) return res.setHeader("Vary", value);
|
|
546
|
+
const current = String(existing).split(",").map((s) => s.trim()).filter(Boolean);
|
|
547
|
+
if (current.includes("*") || current.includes(value)) return;
|
|
548
|
+
res.setHeader("Vary", [...current, value].join(", "));
|
|
549
|
+
}
|
|
550
|
+
async function isAllowed(origin, rule) {
|
|
551
|
+
if (rule === true || rule === "*") return true;
|
|
552
|
+
if (rule === false || !origin) return false;
|
|
553
|
+
if (typeof rule === "string") return rule === origin;
|
|
554
|
+
if (Array.isArray(rule)) return rule.includes(origin);
|
|
555
|
+
if (rule instanceof RegExp) return rule.test(origin);
|
|
556
|
+
if (typeof rule === "function") return await rule(origin);
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
var cors = (options = {}) => {
|
|
560
|
+
const {
|
|
561
|
+
origin = "*",
|
|
562
|
+
methods = "GET,HEAD,PUT,PATCH,POST,DELETE",
|
|
563
|
+
allowedHeaders,
|
|
564
|
+
exposedHeaders,
|
|
565
|
+
credentials = false,
|
|
566
|
+
maxAge = 86400,
|
|
567
|
+
preflightContinue = false,
|
|
568
|
+
optionsSuccessStatus = 204
|
|
569
|
+
} = options;
|
|
570
|
+
const methodsStr = Array.isArray(methods) ? methods.join(",") : methods;
|
|
571
|
+
const allowedHeadersStr = Array.isArray(allowedHeaders) ? allowedHeaders.join(",") : allowedHeaders;
|
|
572
|
+
const exposedHeadersStr = Array.isArray(exposedHeaders) ? exposedHeaders.join(",") : exposedHeaders;
|
|
573
|
+
return async (req, res, next) => {
|
|
574
|
+
const requestOrigin = req.headers.origin;
|
|
575
|
+
if (!requestOrigin) return next();
|
|
576
|
+
const allowed = await isAllowed(requestOrigin, origin);
|
|
577
|
+
if (!allowed) return next();
|
|
578
|
+
const allowOriginValue = origin === "*" && !credentials ? "*" : requestOrigin;
|
|
579
|
+
res.setHeader("Access-Control-Allow-Origin", allowOriginValue);
|
|
580
|
+
if (allowOriginValue !== "*") appendVary2(res, "Origin");
|
|
581
|
+
if (credentials) res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
582
|
+
if (exposedHeadersStr)
|
|
583
|
+
res.setHeader("Access-Control-Expose-Headers", exposedHeadersStr);
|
|
584
|
+
const isPreflight = req.method === "OPTIONS" && req.headers["access-control-request-method"] !== void 0;
|
|
585
|
+
if (!isPreflight) return next();
|
|
586
|
+
res.setHeader("Access-Control-Allow-Methods", methodsStr);
|
|
587
|
+
if (allowedHeadersStr) {
|
|
588
|
+
res.setHeader("Access-Control-Allow-Headers", allowedHeadersStr);
|
|
589
|
+
} else if (origin === "*") {
|
|
590
|
+
const requested = req.headers["access-control-request-headers"];
|
|
591
|
+
if (requested) res.setHeader("Access-Control-Allow-Headers", requested);
|
|
592
|
+
} else {
|
|
593
|
+
res.setHeader(
|
|
594
|
+
"Access-Control-Allow-Headers",
|
|
595
|
+
"Content-Type, Authorization"
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
res.setHeader("Access-Control-Max-Age", String(maxAge));
|
|
599
|
+
if (preflightContinue) return next();
|
|
600
|
+
res.statusCode = optionsSuccessStatus;
|
|
601
|
+
res.end();
|
|
602
|
+
};
|
|
603
|
+
};
|
|
604
|
+
|
|
427
605
|
// lib/index.ts
|
|
428
606
|
function frameworkError(message, skipFn, code, status) {
|
|
429
607
|
const err = new Error(message);
|
|
@@ -441,8 +619,12 @@ var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
|
|
|
441
619
|
ErrorCode2["INVALID_JSON"] = "CPEAK_ERR_INVALID_JSON";
|
|
442
620
|
ErrorCode2["PAYLOAD_TOO_LARGE"] = "CPEAK_ERR_PAYLOAD_TOO_LARGE";
|
|
443
621
|
ErrorCode2["WEAK_SECRET"] = "CPEAK_ERR_WEAK_SECRET";
|
|
622
|
+
ErrorCode2["COMPRESSION_NOT_ENABLED"] = "CPEAK_ERR_COMPRESSION_NOT_ENABLED";
|
|
444
623
|
return ErrorCode2;
|
|
445
624
|
})(ErrorCode || {});
|
|
625
|
+
function compressionConfigFor(res) {
|
|
626
|
+
return res.socket?.server?._cpeakCompression;
|
|
627
|
+
}
|
|
446
628
|
var CpeakIncomingMessage = class extends http.IncomingMessage {
|
|
447
629
|
// We define body and params here for better V8 optimization (not changing the shape of the object at runtime)
|
|
448
630
|
body = void 0;
|
|
@@ -483,9 +665,14 @@ var CpeakServerResponse = class extends http.ServerResponse {
|
|
|
483
665
|
"CPEAK_ERR_NOT_A_FILE" /* NOT_A_FILE */
|
|
484
666
|
);
|
|
485
667
|
}
|
|
668
|
+
const config = compressionConfigFor(this);
|
|
669
|
+
if (config) {
|
|
670
|
+
await compressAndSend(this, mime, createReadStream(path2), config, stat.size);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
486
673
|
this.setHeader("Content-Type", mime);
|
|
487
674
|
this.setHeader("Content-Length", String(stat.size));
|
|
488
|
-
await
|
|
675
|
+
await pipeline2(createReadStream(path2), this);
|
|
489
676
|
} catch (err) {
|
|
490
677
|
if (err?.code === "ENOENT") {
|
|
491
678
|
throw frameworkError(
|
|
@@ -517,10 +704,30 @@ var CpeakServerResponse = class extends http.ServerResponse {
|
|
|
517
704
|
this.writeHead(302, { Location: location });
|
|
518
705
|
this.end();
|
|
519
706
|
}
|
|
520
|
-
// Send a json data back to the client
|
|
707
|
+
// Send a json data back to the client. Sync hot path when compression is
|
|
708
|
+
// off — no Promise allocation, no microtask. Branches into compressAndSend
|
|
709
|
+
// (async) when compression was enabled at cpeak() construction.
|
|
521
710
|
json(data) {
|
|
711
|
+
const body = JSON.stringify(data);
|
|
712
|
+
const config = compressionConfigFor(this);
|
|
713
|
+
if (config) {
|
|
714
|
+
return compressAndSend(this, "application/json", body, config);
|
|
715
|
+
}
|
|
522
716
|
this.setHeader("Content-Type", "application/json");
|
|
523
|
-
this.end(
|
|
717
|
+
this.end(body);
|
|
718
|
+
}
|
|
719
|
+
// Explicit compression entry point. Throws if compression wasn't configured —
|
|
720
|
+
// the developer asked to compress but the framework was never told to.
|
|
721
|
+
compress(mime, body, size) {
|
|
722
|
+
const config = compressionConfigFor(this);
|
|
723
|
+
if (!config) {
|
|
724
|
+
throw frameworkError(
|
|
725
|
+
"compression is not enabled. Pass `compression` to cpeak({ compression: true | { ... } }) to use res.compress.",
|
|
726
|
+
this.compress,
|
|
727
|
+
"CPEAK_ERR_COMPRESSION_NOT_ENABLED" /* COMPRESSION_NOT_ENABLED */
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
return compressAndSend(this, mime, body, config, size);
|
|
524
731
|
}
|
|
525
732
|
};
|
|
526
733
|
var Cpeak = class {
|
|
@@ -528,13 +735,18 @@ var Cpeak = class {
|
|
|
528
735
|
#routes;
|
|
529
736
|
#middleware;
|
|
530
737
|
#handleErr;
|
|
531
|
-
constructor() {
|
|
738
|
+
constructor(options = {}) {
|
|
532
739
|
this.#server = http.createServer({
|
|
533
740
|
IncomingMessage: CpeakIncomingMessage,
|
|
534
741
|
ServerResponse: CpeakServerResponse
|
|
535
742
|
});
|
|
536
743
|
this.#routes = {};
|
|
537
744
|
this.#middleware = [];
|
|
745
|
+
if (options.compression) {
|
|
746
|
+
this.#server._cpeakCompression = resolveCompressionOptions(
|
|
747
|
+
options.compression
|
|
748
|
+
);
|
|
749
|
+
}
|
|
538
750
|
this.#server.on(
|
|
539
751
|
"request",
|
|
540
752
|
async (req, res) => {
|
|
@@ -657,8 +869,8 @@ var Cpeak = class {
|
|
|
657
869
|
return params;
|
|
658
870
|
}
|
|
659
871
|
};
|
|
660
|
-
function cpeak() {
|
|
661
|
-
return new Cpeak();
|
|
872
|
+
function cpeak(options) {
|
|
873
|
+
return new Cpeak(options);
|
|
662
874
|
}
|
|
663
875
|
export {
|
|
664
876
|
Cpeak,
|
|
@@ -667,6 +879,7 @@ export {
|
|
|
667
879
|
ErrorCode,
|
|
668
880
|
auth,
|
|
669
881
|
cookieParser,
|
|
882
|
+
cors,
|
|
670
883
|
cpeak as default,
|
|
671
884
|
frameworkError,
|
|
672
885
|
hashPassword,
|