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 +300 -2
- package/dist/index.d.ts +88 -16
- package/dist/index.js +653 -165
- package/dist/index.js.map +1 -1
- package/lib/index.ts +229 -158
- package/lib/internal/compression.ts +180 -0
- package/lib/internal/types.ts +10 -0
- package/lib/types.ts +20 -5
- package/lib/utils/auth.ts +148 -0
- package/lib/utils/cookieParser.ts +179 -0
- package/lib/utils/cors.ts +109 -0
- package/lib/utils/index.ts +15 -1
- package/lib/utils/render.ts +7 -0
- package/lib/utils/serveStatic.ts +16 -5
- package/lib/utils/swagger.ts +31 -0
- package/lib/utils/types.ts +51 -0
- package/package.json +4 -4
- /package/lib/utils/{paseJSON.ts → parseJSON.ts} +0 -0
package/lib/index.ts
CHANGED
|
@@ -2,9 +2,17 @@ import http from "node:http";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import { createReadStream } from "node:fs";
|
|
4
4
|
import { pipeline } from "node:stream/promises";
|
|
5
|
+
import type { Readable } from "node:stream";
|
|
6
|
+
import type { Buffer } from "node:buffer";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
resolveCompressionOptions,
|
|
10
|
+
compressAndSend
|
|
11
|
+
} from "./internal/compression";
|
|
5
12
|
|
|
6
13
|
import type {
|
|
7
14
|
StringMap,
|
|
15
|
+
CpeakOptions,
|
|
8
16
|
CpeakRequest,
|
|
9
17
|
CpeakResponse,
|
|
10
18
|
Middleware,
|
|
@@ -13,6 +21,8 @@ import type {
|
|
|
13
21
|
RoutesMap
|
|
14
22
|
} from "./types";
|
|
15
23
|
|
|
24
|
+
import type { ResolvedCompressionConfig } from "./internal/types";
|
|
25
|
+
|
|
16
26
|
// A utility function to create an error with a custom stack trace
|
|
17
27
|
export function frameworkError(
|
|
18
28
|
message: string,
|
|
@@ -40,38 +50,43 @@ export enum ErrorCode {
|
|
|
40
50
|
NOT_A_FILE = "CPEAK_ERR_NOT_A_FILE",
|
|
41
51
|
SEND_FILE_FAIL = "CPEAK_ERR_SEND_FILE_FAIL",
|
|
42
52
|
INVALID_JSON = "CPEAK_ERR_INVALID_JSON",
|
|
43
|
-
PAYLOAD_TOO_LARGE = "CPEAK_ERR_PAYLOAD_TOO_LARGE"
|
|
53
|
+
PAYLOAD_TOO_LARGE = "CPEAK_ERR_PAYLOAD_TOO_LARGE",
|
|
54
|
+
WEAK_SECRET = "CPEAK_ERR_WEAK_SECRET",
|
|
55
|
+
COMPRESSION_NOT_ENABLED = "CPEAK_ERR_COMPRESSION_NOT_ENABLED"
|
|
44
56
|
}
|
|
45
57
|
|
|
46
|
-
class CpeakIncomingMessage extends http.IncomingMessage {
|
|
58
|
+
export class CpeakIncomingMessage extends http.IncomingMessage {
|
|
47
59
|
// We define body and params here for better V8 optimization (not changing the shape of the object at runtime)
|
|
48
60
|
public body: any = undefined;
|
|
49
61
|
public params: StringMap = {};
|
|
50
62
|
|
|
51
|
-
|
|
63
|
+
#query?: StringMap;
|
|
52
64
|
|
|
53
65
|
// Parse the URL parameters (like /users?key1=value1&key2=value2)
|
|
54
66
|
// We will call this query to be more familiar with other node.js frameworks.
|
|
55
67
|
// This is a getter method (accessed like a property)
|
|
56
68
|
get query(): StringMap {
|
|
57
69
|
// This way if a developer writes req.query multiple times, we don't parse it multiple times
|
|
58
|
-
if (this
|
|
70
|
+
if (this.#query) return this.#query;
|
|
59
71
|
|
|
60
72
|
const url = this.url || "";
|
|
61
73
|
const qIndex = url.indexOf("?");
|
|
62
74
|
|
|
63
75
|
if (qIndex === -1) {
|
|
64
|
-
this
|
|
76
|
+
this.#query = {};
|
|
65
77
|
} else {
|
|
66
78
|
const searchParams = new URLSearchParams(url.substring(qIndex + 1));
|
|
67
|
-
this
|
|
79
|
+
this.#query = Object.fromEntries(searchParams.entries());
|
|
68
80
|
}
|
|
69
81
|
|
|
70
|
-
return this
|
|
82
|
+
return this.#query;
|
|
71
83
|
}
|
|
72
84
|
}
|
|
73
85
|
|
|
74
|
-
class CpeakServerResponse extends http.ServerResponse<CpeakIncomingMessage> {
|
|
86
|
+
export class CpeakServerResponse extends http.ServerResponse<CpeakIncomingMessage> {
|
|
87
|
+
// Set per-request from the Cpeak instance. Undefined when compression isn't enabled.
|
|
88
|
+
_compression?: ResolvedCompressionConfig;
|
|
89
|
+
|
|
75
90
|
// Send a file back to the client
|
|
76
91
|
async sendFile(path: string, mime: string) {
|
|
77
92
|
if (!mime) {
|
|
@@ -92,6 +107,17 @@ class CpeakServerResponse extends http.ServerResponse<CpeakIncomingMessage> {
|
|
|
92
107
|
);
|
|
93
108
|
}
|
|
94
109
|
|
|
110
|
+
if (this._compression) {
|
|
111
|
+
await compressAndSend(
|
|
112
|
+
this,
|
|
113
|
+
mime,
|
|
114
|
+
createReadStream(path),
|
|
115
|
+
this._compression,
|
|
116
|
+
stat.size
|
|
117
|
+
);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
95
121
|
this.setHeader("Content-Type", mime);
|
|
96
122
|
this.setHeader("Content-Length", String(stat.size));
|
|
97
123
|
|
|
@@ -120,157 +146,188 @@ class CpeakServerResponse extends http.ServerResponse<CpeakIncomingMessage> {
|
|
|
120
146
|
return this;
|
|
121
147
|
}
|
|
122
148
|
|
|
149
|
+
// Set the Content-Disposition header to prompt the user to download a file
|
|
150
|
+
attachment(filename?: string) {
|
|
151
|
+
const contentDisposition = filename
|
|
152
|
+
? `attachment; filename="${filename}"`
|
|
153
|
+
: "attachment";
|
|
154
|
+
this.setHeader("Content-Disposition", contentDisposition);
|
|
155
|
+
return this;
|
|
156
|
+
}
|
|
157
|
+
|
|
123
158
|
// Redirects to a new URL
|
|
124
159
|
redirect(location: string) {
|
|
125
160
|
this.writeHead(302, { Location: location });
|
|
126
161
|
this.end();
|
|
127
|
-
return this;
|
|
128
162
|
}
|
|
129
163
|
|
|
130
|
-
// Send a json data back to the client
|
|
131
|
-
|
|
132
|
-
|
|
164
|
+
// Send a json data back to the client.
|
|
165
|
+
// This is only good for bodies that their size is less than the highWaterMark value.
|
|
166
|
+
// Branches into compressAndSend (async) when compression was enabled at cpeak() construction.
|
|
167
|
+
json(data: any): void | Promise<void> {
|
|
168
|
+
const body = JSON.stringify(data);
|
|
169
|
+
if (this._compression) {
|
|
170
|
+
return compressAndSend(this, "application/json", body, this._compression);
|
|
171
|
+
}
|
|
133
172
|
this.setHeader("Content-Type", "application/json");
|
|
134
|
-
this.end(
|
|
173
|
+
this.end(body);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Explicit compression entry point. A developer can use this in any custom handler to compress arbitrary responses
|
|
177
|
+
compress(
|
|
178
|
+
mime: string,
|
|
179
|
+
body: Buffer | string | Readable,
|
|
180
|
+
size?: number
|
|
181
|
+
): Promise<void> {
|
|
182
|
+
if (!this._compression) {
|
|
183
|
+
throw frameworkError(
|
|
184
|
+
"compression is not enabled. Pass `compression` to cpeak({ compression: true | { ... } }) to use res.compress.",
|
|
185
|
+
this.compress,
|
|
186
|
+
ErrorCode.COMPRESSION_NOT_ENABLED
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
return compressAndSend(this, mime, body, this._compression, size);
|
|
135
190
|
}
|
|
136
191
|
}
|
|
137
192
|
|
|
138
|
-
class Cpeak {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
req: CpeakRequest,
|
|
148
|
-
res: CpeakResponse
|
|
149
|
-
) => void;
|
|
150
|
-
|
|
151
|
-
constructor() {
|
|
152
|
-
this.server = http.createServer({
|
|
193
|
+
export class Cpeak {
|
|
194
|
+
#server: http.Server<typeof CpeakIncomingMessage, typeof CpeakServerResponse>;
|
|
195
|
+
#routes: RoutesMap;
|
|
196
|
+
#middleware: Middleware[];
|
|
197
|
+
#handleErr?: (err: unknown, req: CpeakRequest, res: CpeakResponse) => void;
|
|
198
|
+
#compression?: ResolvedCompressionConfig;
|
|
199
|
+
|
|
200
|
+
constructor(options: CpeakOptions = {}) {
|
|
201
|
+
this.#server = http.createServer({
|
|
153
202
|
IncomingMessage: CpeakIncomingMessage,
|
|
154
203
|
ServerResponse: CpeakServerResponse
|
|
155
204
|
});
|
|
156
|
-
this
|
|
157
|
-
this
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
await cb(req, res, (error) => {
|
|
179
|
-
res.setHeader("Connection", "close");
|
|
180
|
-
this._handleErr?.(error, req, res);
|
|
181
|
-
});
|
|
182
|
-
} catch (error) {
|
|
183
|
-
res.setHeader("Connection", "close");
|
|
184
|
-
this._handleErr?.(error, req, res);
|
|
205
|
+
this.#routes = {};
|
|
206
|
+
this.#middleware = [];
|
|
207
|
+
|
|
208
|
+
// Resolve compression options once at app startup.
|
|
209
|
+
if (options.compression) {
|
|
210
|
+
this.#compression = resolveCompressionOptions(options.compression);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
this.#server.on(
|
|
214
|
+
"request",
|
|
215
|
+
async (req: CpeakRequest, res: CpeakResponse) => {
|
|
216
|
+
res._compression = this.#compression;
|
|
217
|
+
|
|
218
|
+
// Get the url without the URL parameters (query strings)
|
|
219
|
+
const qIndex = req.url?.indexOf("?");
|
|
220
|
+
const urlWithoutQueries =
|
|
221
|
+
qIndex === -1 ? req.url || "" : req.url?.substring(0, qIndex);
|
|
222
|
+
|
|
223
|
+
const dispatchError = (error: unknown) => {
|
|
224
|
+
if (res.headersSent) {
|
|
225
|
+
req.socket?.destroy();
|
|
226
|
+
return;
|
|
185
227
|
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
|
|
228
|
+
res.setHeader("Connection", "close");
|
|
229
|
+
this.#handleErr?.(error, req, res);
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// Run all the specific middleware functions for that router only and then run the handler
|
|
233
|
+
const runHandler = async (
|
|
234
|
+
req: CpeakRequest,
|
|
235
|
+
res: CpeakResponse,
|
|
236
|
+
middleware: RouteMiddleware[],
|
|
237
|
+
cb: Handler,
|
|
238
|
+
index: number
|
|
239
|
+
) => {
|
|
240
|
+
// Our exit point...
|
|
241
|
+
if (index === middleware.length) {
|
|
242
|
+
// Call the route handler with the modified req and res objects.
|
|
243
|
+
// Also handle the promise errors by passing them to the handleErr to save developers from having to manually wrap every handler in try catch.
|
|
244
|
+
try {
|
|
245
|
+
await cb(req, res, dispatchError);
|
|
246
|
+
} catch (error) {
|
|
247
|
+
dispatchError(error);
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
// Handle the promise errors by passing them to the handleErr to save developers from having to manually wrap every handler middleware in try catch.
|
|
251
|
+
try {
|
|
252
|
+
await middleware[index](
|
|
253
|
+
req,
|
|
254
|
+
res,
|
|
255
|
+
// The next function
|
|
256
|
+
async (error) => {
|
|
257
|
+
// this function only accepts an error argument to be more compatible with NPM modules that are built for express
|
|
258
|
+
if (error) {
|
|
259
|
+
return dispatchError(error);
|
|
260
|
+
}
|
|
261
|
+
await runHandler(req, res, middleware, cb, index + 1);
|
|
262
|
+
},
|
|
263
|
+
// Error handler for a route middleware
|
|
264
|
+
dispatchError
|
|
265
|
+
);
|
|
266
|
+
} catch (error) {
|
|
267
|
+
dispatchError(error);
|
|
268
|
+
}
|
|
210
269
|
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// Run all the middleware functions (beforeEach functions) before we run the corresponding route
|
|
273
|
+
const runMiddleware = async (
|
|
274
|
+
req: CpeakRequest,
|
|
275
|
+
res: CpeakResponse,
|
|
276
|
+
middleware: Middleware[],
|
|
277
|
+
index: number
|
|
278
|
+
) => {
|
|
279
|
+
// Our exit point...
|
|
280
|
+
if (index === middleware.length) {
|
|
281
|
+
const routes = this.#routes[req.method?.toLowerCase() || ""];
|
|
282
|
+
if (routes && typeof routes[Symbol.iterator] === "function")
|
|
283
|
+
for (const route of routes) {
|
|
284
|
+
const match = urlWithoutQueries?.match(route.regex);
|
|
285
|
+
|
|
286
|
+
if (match) {
|
|
287
|
+
// Parse the URL path variables from the matched route (like /users/:id)
|
|
288
|
+
const pathVariables = this.#extractPathVariables(
|
|
289
|
+
route.path,
|
|
290
|
+
match
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
// We will call this params to be more familiar with other node.js frameworks.
|
|
294
|
+
req.params = pathVariables;
|
|
295
|
+
|
|
296
|
+
return await runHandler(
|
|
297
|
+
req,
|
|
298
|
+
res,
|
|
299
|
+
route.middleware,
|
|
300
|
+
route.cb,
|
|
301
|
+
0
|
|
302
|
+
);
|
|
303
|
+
}
|
|
245
304
|
}
|
|
246
|
-
}
|
|
247
305
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
})
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
this._handleErr?.(error, req, res);
|
|
306
|
+
// If the requested route dose not exist, return 404
|
|
307
|
+
return res
|
|
308
|
+
.status(404)
|
|
309
|
+
.json({ error: `Cannot ${req.method} ${urlWithoutQueries}` });
|
|
310
|
+
} else {
|
|
311
|
+
try {
|
|
312
|
+
await middleware[index](req, res, async (err?: unknown) => {
|
|
313
|
+
if (err) {
|
|
314
|
+
return dispatchError(err);
|
|
315
|
+
}
|
|
316
|
+
await runMiddleware(req, res, middleware, index + 1);
|
|
317
|
+
});
|
|
318
|
+
} catch (error) {
|
|
319
|
+
dispatchError(error);
|
|
320
|
+
}
|
|
264
321
|
}
|
|
265
|
-
}
|
|
266
|
-
};
|
|
322
|
+
};
|
|
267
323
|
|
|
268
|
-
|
|
269
|
-
|
|
324
|
+
await runMiddleware(req, res, this.#middleware, 0);
|
|
325
|
+
}
|
|
326
|
+
);
|
|
270
327
|
}
|
|
271
328
|
|
|
272
329
|
route(method: string, path: string, ...args: (RouteMiddleware | Handler)[]) {
|
|
273
|
-
if (!this
|
|
330
|
+
if (!this.#routes[method]) this.#routes[method] = [];
|
|
274
331
|
|
|
275
332
|
// The last argument should always be our handler
|
|
276
333
|
const cb = args.pop() as Handler;
|
|
@@ -283,40 +340,37 @@ class Cpeak {
|
|
|
283
340
|
const middleware = args.flat() as RouteMiddleware[];
|
|
284
341
|
|
|
285
342
|
const regex = this.#pathToRegex(path);
|
|
286
|
-
this
|
|
343
|
+
this.#routes[method].push({ path, regex, middleware, cb });
|
|
287
344
|
}
|
|
288
345
|
|
|
289
346
|
beforeEach(cb: Middleware) {
|
|
290
|
-
this
|
|
347
|
+
this.#middleware.push(cb);
|
|
291
348
|
}
|
|
292
349
|
|
|
293
350
|
handleErr(cb: (err: unknown, req: CpeakRequest, res: CpeakResponse) => void) {
|
|
294
|
-
this
|
|
351
|
+
this.#handleErr = cb;
|
|
295
352
|
}
|
|
296
353
|
|
|
297
354
|
listen(port: number, cb?: () => void) {
|
|
298
|
-
return this
|
|
355
|
+
return this.#server.listen(port, cb);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
address() {
|
|
359
|
+
return this.#server.address();
|
|
299
360
|
}
|
|
300
361
|
|
|
301
362
|
close(cb?: (err?: Error) => void) {
|
|
302
|
-
this
|
|
363
|
+
this.#server.close(cb);
|
|
303
364
|
}
|
|
304
365
|
|
|
305
366
|
// ------------------------------
|
|
306
367
|
// PRIVATE METHODS:
|
|
307
368
|
// ------------------------------
|
|
308
369
|
#pathToRegex(path: string) {
|
|
309
|
-
const paramNames: string[] = [];
|
|
310
370
|
const regexString =
|
|
311
|
-
"^" +
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
return "([^/]+)";
|
|
315
|
-
}) +
|
|
316
|
-
"$";
|
|
317
|
-
|
|
318
|
-
const regex = new RegExp(regexString);
|
|
319
|
-
return regex;
|
|
371
|
+
"^" + path.replace(/:\w+/g, "([^/]+)").replace(/\*/g, ".*") + "$";
|
|
372
|
+
|
|
373
|
+
return new RegExp(regexString);
|
|
320
374
|
}
|
|
321
375
|
|
|
322
376
|
#extractPathVariables(path: string, match: RegExpMatchArray) {
|
|
@@ -333,12 +387,29 @@ class Cpeak {
|
|
|
333
387
|
}
|
|
334
388
|
|
|
335
389
|
// Util functions
|
|
336
|
-
export {
|
|
337
|
-
|
|
338
|
-
|
|
390
|
+
export {
|
|
391
|
+
serveStatic,
|
|
392
|
+
parseJSON,
|
|
393
|
+
render,
|
|
394
|
+
swagger,
|
|
395
|
+
auth,
|
|
396
|
+
hashPassword,
|
|
397
|
+
verifyPassword,
|
|
398
|
+
cookieParser,
|
|
399
|
+
cors
|
|
400
|
+
} from "./utils";
|
|
401
|
+
|
|
402
|
+
export type {
|
|
403
|
+
AuthOptions,
|
|
404
|
+
PbkdfOptions,
|
|
405
|
+
CookieOptions,
|
|
406
|
+
CorsOptions
|
|
407
|
+
} from "./utils/types";
|
|
408
|
+
|
|
409
|
+
export type { CompressionOptions } from "./internal/types";
|
|
339
410
|
|
|
340
411
|
export type {
|
|
341
|
-
|
|
412
|
+
CpeakOptions,
|
|
342
413
|
CpeakRequest,
|
|
343
414
|
CpeakResponse,
|
|
344
415
|
Next,
|
|
@@ -349,6 +420,6 @@ export type {
|
|
|
349
420
|
RoutesMap
|
|
350
421
|
} from "./types";
|
|
351
422
|
|
|
352
|
-
export default function cpeak() {
|
|
353
|
-
return new Cpeak();
|
|
423
|
+
export default function cpeak(options?: CpeakOptions): Cpeak {
|
|
424
|
+
return new Cpeak(options);
|
|
354
425
|
}
|