cpeak 2.4.3 → 2.6.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 +265 -39
- package/dist/index.d.ts +75 -16
- package/dist/index.js +515 -169
- package/dist/index.js.map +1 -1
- package/lib/index.ts +262 -217
- package/lib/types.ts +12 -10
- package/lib/utils/auth.ts +170 -0
- package/lib/utils/cookieParser.ts +189 -0
- package/lib/utils/index.ts +16 -1
- package/lib/utils/parseJSON.ts +77 -24
- package/lib/utils/serveStatic.ts +16 -5
- package/lib/utils/swagger.ts +31 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4,6 +4,60 @@ import fs3 from "fs/promises";
|
|
|
4
4
|
import { createReadStream } from "fs";
|
|
5
5
|
import { pipeline } from "stream/promises";
|
|
6
6
|
|
|
7
|
+
// lib/utils/parseJSON.ts
|
|
8
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
9
|
+
function isJSON(contentType) {
|
|
10
|
+
if (!contentType) return false;
|
|
11
|
+
if (contentType === "application/json") return true;
|
|
12
|
+
return contentType.startsWith("application/json") || contentType.includes("+json");
|
|
13
|
+
}
|
|
14
|
+
var parseJSON = (options = {}) => {
|
|
15
|
+
const limit = options.limit || 1024 * 1024;
|
|
16
|
+
return (req, res, next) => {
|
|
17
|
+
if (!isJSON(req.headers["content-type"])) return next();
|
|
18
|
+
const chunks = [];
|
|
19
|
+
let bytesReceived = 0;
|
|
20
|
+
const onData = (chunk) => {
|
|
21
|
+
bytesReceived += chunk.length;
|
|
22
|
+
if (bytesReceived > limit) {
|
|
23
|
+
req.pause();
|
|
24
|
+
req.removeListener("data", onData);
|
|
25
|
+
req.removeListener("end", onEnd);
|
|
26
|
+
next(
|
|
27
|
+
frameworkError(
|
|
28
|
+
"JSON body too large",
|
|
29
|
+
onData,
|
|
30
|
+
"CPEAK_ERR_PAYLOAD_TOO_LARGE" /* PAYLOAD_TOO_LARGE */,
|
|
31
|
+
413
|
|
32
|
+
// HTTP 413 Payload Too Large
|
|
33
|
+
)
|
|
34
|
+
);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
chunks.push(chunk);
|
|
38
|
+
};
|
|
39
|
+
const onEnd = () => {
|
|
40
|
+
try {
|
|
41
|
+
const rawBody = chunks.length === 1 ? chunks[0].toString("utf-8") : Buffer2.concat(chunks).toString("utf-8");
|
|
42
|
+
req.body = rawBody ? JSON.parse(rawBody) : {};
|
|
43
|
+
next();
|
|
44
|
+
} catch (err) {
|
|
45
|
+
next(
|
|
46
|
+
frameworkError(
|
|
47
|
+
"Invalid JSON format",
|
|
48
|
+
onEnd,
|
|
49
|
+
"CPEAK_ERR_INVALID_JSON" /* INVALID_JSON */,
|
|
50
|
+
400
|
|
51
|
+
// HTTP 400 Bad Request
|
|
52
|
+
)
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
req.on("data", onData);
|
|
57
|
+
req.on("end", onEnd);
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
|
|
7
61
|
// lib/utils/serveStatic.ts
|
|
8
62
|
import fs from "fs";
|
|
9
63
|
import path from "path";
|
|
@@ -20,12 +74,17 @@ var MIME_TYPES = {
|
|
|
20
74
|
otf: "font/otf",
|
|
21
75
|
ttf: "font/ttf",
|
|
22
76
|
woff: "font/woff",
|
|
23
|
-
woff2: "font/woff2"
|
|
77
|
+
woff2: "font/woff2",
|
|
78
|
+
gif: "image/gif",
|
|
79
|
+
ico: "image/x-icon",
|
|
80
|
+
json: "application/json",
|
|
81
|
+
webmanifest: "application/manifest+json"
|
|
24
82
|
};
|
|
25
|
-
var serveStatic = (folderPath, newMimeTypes) => {
|
|
83
|
+
var serveStatic = (folderPath, newMimeTypes, options) => {
|
|
26
84
|
if (newMimeTypes) {
|
|
27
85
|
Object.assign(MIME_TYPES, newMimeTypes);
|
|
28
86
|
}
|
|
87
|
+
const prefix = options?.prefix ?? "";
|
|
29
88
|
function processFolder(folderPath2, parentFolder) {
|
|
30
89
|
const staticFiles = [];
|
|
31
90
|
const files = fs.readdirSync(folderPath2);
|
|
@@ -46,7 +105,7 @@ var serveStatic = (folderPath, newMimeTypes) => {
|
|
|
46
105
|
const filesMap2 = {};
|
|
47
106
|
for (const file of filesArray) {
|
|
48
107
|
const fileExtension = path.extname(file).slice(1);
|
|
49
|
-
filesMap2[file] = {
|
|
108
|
+
filesMap2[prefix + file] = {
|
|
50
109
|
path: folderPath + file,
|
|
51
110
|
mime: MIME_TYPES[fileExtension]
|
|
52
111
|
};
|
|
@@ -57,32 +116,15 @@ var serveStatic = (folderPath, newMimeTypes) => {
|
|
|
57
116
|
return function(req, res, next) {
|
|
58
117
|
const url = req.url;
|
|
59
118
|
if (typeof url !== "string") return next();
|
|
60
|
-
|
|
61
|
-
|
|
119
|
+
const pathname = url.split("?")[0];
|
|
120
|
+
if (Object.prototype.hasOwnProperty.call(filesMap, pathname)) {
|
|
121
|
+
const fileRoute = filesMap[pathname];
|
|
62
122
|
return res.sendFile(fileRoute.path, fileRoute.mime);
|
|
63
123
|
}
|
|
64
124
|
next();
|
|
65
125
|
};
|
|
66
126
|
};
|
|
67
127
|
|
|
68
|
-
// lib/utils/parseJSON.ts
|
|
69
|
-
var parseJSON = (req, res, next) => {
|
|
70
|
-
function isJSON(contentType = "") {
|
|
71
|
-
const [type] = contentType.split(";");
|
|
72
|
-
return type.trim().toLowerCase() === "application/json" || /\+json$/i.test(type.trim());
|
|
73
|
-
}
|
|
74
|
-
if (!isJSON(req.headers["content-type"])) return next();
|
|
75
|
-
let body = "";
|
|
76
|
-
req.on("data", (chunk) => {
|
|
77
|
-
body += chunk.toString("utf-8");
|
|
78
|
-
});
|
|
79
|
-
req.on("end", () => {
|
|
80
|
-
body = JSON.parse(body);
|
|
81
|
-
req.body = body;
|
|
82
|
-
return next();
|
|
83
|
-
});
|
|
84
|
-
};
|
|
85
|
-
|
|
86
128
|
// lib/utils/render.ts
|
|
87
129
|
import fs2 from "fs/promises";
|
|
88
130
|
function renderTemplate(templateStr, data) {
|
|
@@ -108,7 +150,6 @@ function renderTemplate(templateStr, data) {
|
|
|
108
150
|
return result.join("");
|
|
109
151
|
}
|
|
110
152
|
var render = () => {
|
|
111
|
-
console.log("render.ts loaded");
|
|
112
153
|
return function(req, res, next) {
|
|
113
154
|
res.render = async (path2, data, mime) => {
|
|
114
155
|
if (!mime) {
|
|
@@ -126,11 +167,270 @@ var render = () => {
|
|
|
126
167
|
};
|
|
127
168
|
};
|
|
128
169
|
|
|
170
|
+
// lib/utils/swagger.ts
|
|
171
|
+
var swagger = (spec, prefix = "/api-docs") => {
|
|
172
|
+
const initializerJs = `window.onload = function() {
|
|
173
|
+
SwaggerUIBundle({
|
|
174
|
+
url: "${prefix}/spec.json",
|
|
175
|
+
dom_id: '#swagger-ui',
|
|
176
|
+
presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
|
|
177
|
+
layout: "StandaloneLayout"
|
|
178
|
+
});
|
|
179
|
+
};`;
|
|
180
|
+
return (req, res, next) => {
|
|
181
|
+
if (req.url === prefix || req.url === `${prefix}/`) {
|
|
182
|
+
res.writeHead(302, { Location: `${prefix}/index.html` });
|
|
183
|
+
res.end();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (req.url === `${prefix}/spec.json`) {
|
|
187
|
+
return res.json(spec);
|
|
188
|
+
}
|
|
189
|
+
if (req.url === `${prefix}/swagger-initializer.js`) {
|
|
190
|
+
res.setHeader("Content-Type", "application/javascript");
|
|
191
|
+
res.end(initializerJs);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
next();
|
|
195
|
+
};
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
// lib/utils/auth.ts
|
|
199
|
+
import { randomBytes, pbkdf2, createHmac, timingSafeEqual } from "crypto";
|
|
200
|
+
import { promisify } from "util";
|
|
201
|
+
var pbkdf2Async = promisify(pbkdf2);
|
|
202
|
+
var DEFAULTS = {
|
|
203
|
+
iterations: 21e4,
|
|
204
|
+
keylen: 64,
|
|
205
|
+
digest: "sha512",
|
|
206
|
+
saltSize: 32,
|
|
207
|
+
hmacAlgorithm: "sha256",
|
|
208
|
+
tokenIdSize: 20,
|
|
209
|
+
tokenExpiry: 7 * 24 * 60 * 60 * 1e3
|
|
210
|
+
// 7 days in ms
|
|
211
|
+
};
|
|
212
|
+
async function hashPassword(password, options) {
|
|
213
|
+
const iterations = options?.iterations ?? DEFAULTS.iterations;
|
|
214
|
+
const keylen = options?.keylen ?? DEFAULTS.keylen;
|
|
215
|
+
const digest = options?.digest ?? DEFAULTS.digest;
|
|
216
|
+
const saltSize = options?.saltSize ?? DEFAULTS.saltSize;
|
|
217
|
+
const salt = randomBytes(saltSize);
|
|
218
|
+
const hash = await pbkdf2Async(password, salt, iterations, keylen, digest);
|
|
219
|
+
return `pbkdf2:${iterations}:${keylen}:${digest}:${salt.toString("hex")}:${hash.toString("hex")}`;
|
|
220
|
+
}
|
|
221
|
+
async function verifyPassword(password, stored) {
|
|
222
|
+
const withoutPrefix = stored.slice(stored.indexOf(":") + 1);
|
|
223
|
+
const parts = withoutPrefix.split(":");
|
|
224
|
+
if (parts.length !== 5) return false;
|
|
225
|
+
const [itersStr, keylenStr, digest, saltHex, hashHex] = parts;
|
|
226
|
+
const iterations = parseInt(itersStr, 10);
|
|
227
|
+
const keylen = parseInt(keylenStr, 10);
|
|
228
|
+
if (!digest || !saltHex || !hashHex || isNaN(iterations) || isNaN(keylen))
|
|
229
|
+
return false;
|
|
230
|
+
const salt = Buffer.from(saltHex, "hex");
|
|
231
|
+
const hash = await pbkdf2Async(password, salt, iterations, keylen, digest);
|
|
232
|
+
const storedHash = Buffer.from(hashHex, "hex");
|
|
233
|
+
if (storedHash.length !== hash.length) return false;
|
|
234
|
+
return timingSafeEqual(hash, storedHash);
|
|
235
|
+
}
|
|
236
|
+
function signToken(tokenId, secret, algorithm) {
|
|
237
|
+
const sig = createHmac(algorithm, secret).update(tokenId).digest("hex");
|
|
238
|
+
return `${tokenId}.${sig}`;
|
|
239
|
+
}
|
|
240
|
+
function extractTokenId(token, secret, algorithm) {
|
|
241
|
+
const dot = token.indexOf(".");
|
|
242
|
+
if (dot === -1) return null;
|
|
243
|
+
const tokenId = token.slice(0, dot);
|
|
244
|
+
const sig = token.slice(dot + 1);
|
|
245
|
+
const expected = createHmac(algorithm, secret).update(tokenId).digest("hex");
|
|
246
|
+
const expectedBuf = Buffer.from(expected, "hex");
|
|
247
|
+
const actualBuf = Buffer.from(sig, "hex");
|
|
248
|
+
if (expectedBuf.length !== actualBuf.length) return null;
|
|
249
|
+
if (!timingSafeEqual(expectedBuf, actualBuf)) return null;
|
|
250
|
+
return tokenId;
|
|
251
|
+
}
|
|
252
|
+
function auth(options) {
|
|
253
|
+
if (!options.secret || options.secret.length < 32) {
|
|
254
|
+
throw frameworkError(
|
|
255
|
+
"Secret must be at least 32 characters. HMAC security is only as strong as the key.",
|
|
256
|
+
auth,
|
|
257
|
+
"CPEAK_ERR_WEAK_SECRET" /* WEAK_SECRET */
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
const {
|
|
261
|
+
secret,
|
|
262
|
+
saveToken,
|
|
263
|
+
findToken,
|
|
264
|
+
revokeToken,
|
|
265
|
+
tokenExpiry = DEFAULTS.tokenExpiry,
|
|
266
|
+
hmacAlgorithm = DEFAULTS.hmacAlgorithm,
|
|
267
|
+
tokenIdSize = DEFAULTS.tokenIdSize
|
|
268
|
+
} = options;
|
|
269
|
+
const pbkdfOpts = {
|
|
270
|
+
iterations: options.iterations,
|
|
271
|
+
keylen: options.keylen,
|
|
272
|
+
digest: options.digest,
|
|
273
|
+
saltSize: options.saltSize
|
|
274
|
+
};
|
|
275
|
+
const _hashPassword = ({ password }) => hashPassword(password, pbkdfOpts);
|
|
276
|
+
const login = async ({
|
|
277
|
+
password,
|
|
278
|
+
hashedPassword,
|
|
279
|
+
userId
|
|
280
|
+
}) => {
|
|
281
|
+
const isMatch = await verifyPassword(password, hashedPassword);
|
|
282
|
+
if (!isMatch) return null;
|
|
283
|
+
const tokenId = randomBytes(tokenIdSize).toString("hex");
|
|
284
|
+
const token = signToken(tokenId, secret, hmacAlgorithm);
|
|
285
|
+
await saveToken(tokenId, userId, new Date(Date.now() + tokenExpiry));
|
|
286
|
+
return token;
|
|
287
|
+
};
|
|
288
|
+
const verifyToken = async (token) => {
|
|
289
|
+
if (!token) return null;
|
|
290
|
+
const tokenId = extractTokenId(token, secret, hmacAlgorithm);
|
|
291
|
+
if (!tokenId) return null;
|
|
292
|
+
const record = await findToken(tokenId);
|
|
293
|
+
if (!record) return null;
|
|
294
|
+
if (new Date(record.expiresAt) < /* @__PURE__ */ new Date()) return null;
|
|
295
|
+
return { userId: record.userId };
|
|
296
|
+
};
|
|
297
|
+
const logout = revokeToken ? async (token) => {
|
|
298
|
+
const tokenId = extractTokenId(token, secret, hmacAlgorithm);
|
|
299
|
+
if (!tokenId) return false;
|
|
300
|
+
await revokeToken(tokenId);
|
|
301
|
+
return true;
|
|
302
|
+
} : void 0;
|
|
303
|
+
return (req, _res, next) => {
|
|
304
|
+
req.hashPassword = _hashPassword;
|
|
305
|
+
req.login = login;
|
|
306
|
+
req.verifyToken = verifyToken;
|
|
307
|
+
if (logout) req.logout = logout;
|
|
308
|
+
next();
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// lib/utils/cookieParser.ts
|
|
313
|
+
import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
314
|
+
function sign(value, secret) {
|
|
315
|
+
const sig = createHmac2("sha256", secret).update(value).digest("base64url");
|
|
316
|
+
return `s:${value}.${sig}`;
|
|
317
|
+
}
|
|
318
|
+
function unsign(signed, secret) {
|
|
319
|
+
if (!signed.startsWith("s:")) return false;
|
|
320
|
+
const withoutPrefix = signed.slice(2);
|
|
321
|
+
const lastDot = withoutPrefix.lastIndexOf(".");
|
|
322
|
+
if (lastDot === -1) return false;
|
|
323
|
+
const value = withoutPrefix.slice(0, lastDot);
|
|
324
|
+
const sig = withoutPrefix.slice(lastDot + 1);
|
|
325
|
+
const expected = createHmac2("sha256", secret).update(value).digest("base64url");
|
|
326
|
+
const expectedBuf = Buffer.from(expected);
|
|
327
|
+
const actualBuf = Buffer.from(sig);
|
|
328
|
+
if (expectedBuf.length !== actualBuf.length) return false;
|
|
329
|
+
if (!timingSafeEqual2(expectedBuf, actualBuf)) return false;
|
|
330
|
+
return value;
|
|
331
|
+
}
|
|
332
|
+
function parseRawCookies(header) {
|
|
333
|
+
const cookies = /* @__PURE__ */ Object.create(null);
|
|
334
|
+
if (!header) return cookies;
|
|
335
|
+
const pairs = header.split(";");
|
|
336
|
+
for (let i = 0; i < pairs.length; i++) {
|
|
337
|
+
const pair = pairs[i];
|
|
338
|
+
const equalSignIndex = pair.indexOf("=");
|
|
339
|
+
if (equalSignIndex === -1) continue;
|
|
340
|
+
const key = pair.slice(0, equalSignIndex).trim();
|
|
341
|
+
if (!key || cookies[key] !== void 0) continue;
|
|
342
|
+
let val = pair.slice(equalSignIndex + 1).trim();
|
|
343
|
+
if (val.length > 1 && val[0] === '"' && val[val.length - 1] === '"') {
|
|
344
|
+
val = val.slice(1, -1);
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
cookies[key] = val.indexOf("%") !== -1 ? decodeURIComponent(val) : val;
|
|
348
|
+
} catch (e) {
|
|
349
|
+
cookies[key] = val;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
return cookies;
|
|
353
|
+
}
|
|
354
|
+
function buildSetCookieHeader(name, value, options) {
|
|
355
|
+
const parts = [`${name}=${encodeURIComponent(value)}`];
|
|
356
|
+
const path2 = options.path ?? "/";
|
|
357
|
+
parts.push(`Path=${path2}`);
|
|
358
|
+
if (options.domain) parts.push(`Domain=${options.domain}`);
|
|
359
|
+
if (options.maxAge !== void 0)
|
|
360
|
+
parts.push(`Max-Age=${Math.floor(options.maxAge / 1e3)}`);
|
|
361
|
+
if (options.expires) parts.push(`Expires=${options.expires.toUTCString()}`);
|
|
362
|
+
if (options.httpOnly) parts.push("HttpOnly");
|
|
363
|
+
if (options.secure) parts.push("Secure");
|
|
364
|
+
if (options.sameSite) parts.push(`SameSite=${options.sameSite}`);
|
|
365
|
+
return parts.join("; ");
|
|
366
|
+
}
|
|
367
|
+
function appendSetCookie(res, header) {
|
|
368
|
+
const existing = res.getHeader("Set-Cookie");
|
|
369
|
+
if (!existing) {
|
|
370
|
+
res.setHeader("Set-Cookie", [header]);
|
|
371
|
+
} else if (Array.isArray(existing)) {
|
|
372
|
+
res.setHeader("Set-Cookie", [...existing, header]);
|
|
373
|
+
} else {
|
|
374
|
+
res.setHeader("Set-Cookie", [String(existing), header]);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function cookieParser(options = {}) {
|
|
378
|
+
const { secret } = options;
|
|
379
|
+
if (secret !== void 0 && secret.length < 32) {
|
|
380
|
+
throw frameworkError(
|
|
381
|
+
"Secret must be at least 32 characters. HMAC security is only as strong as the key.",
|
|
382
|
+
cookieParser,
|
|
383
|
+
"CPEAK_ERR_WEAK_SECRET" /* WEAK_SECRET */
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
return (req, res, next) => {
|
|
387
|
+
const rawHeader = req.headers["cookie"] || "";
|
|
388
|
+
const raw = parseRawCookies(rawHeader);
|
|
389
|
+
const cookies = /* @__PURE__ */ Object.create(null);
|
|
390
|
+
const signedCookies = /* @__PURE__ */ Object.create(null);
|
|
391
|
+
for (const [key, val] of Object.entries(raw)) {
|
|
392
|
+
if (val.startsWith("s:") && secret) {
|
|
393
|
+
signedCookies[key] = unsign(val, secret);
|
|
394
|
+
} else {
|
|
395
|
+
cookies[key] = val;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
req.cookies = cookies;
|
|
399
|
+
req.signedCookies = signedCookies;
|
|
400
|
+
res.cookie = (name, value, options2 = {}) => {
|
|
401
|
+
let finalValue = value;
|
|
402
|
+
if (options2.signed) {
|
|
403
|
+
if (!secret)
|
|
404
|
+
throw new Error(
|
|
405
|
+
"cookieParser: secret is required to use signed cookies"
|
|
406
|
+
);
|
|
407
|
+
finalValue = sign(value, secret);
|
|
408
|
+
}
|
|
409
|
+
appendSetCookie(res, buildSetCookieHeader(name, finalValue, options2));
|
|
410
|
+
return res;
|
|
411
|
+
};
|
|
412
|
+
res.clearCookie = (name, options2 = {}) => {
|
|
413
|
+
appendSetCookie(
|
|
414
|
+
res,
|
|
415
|
+
buildSetCookieHeader(name, "", {
|
|
416
|
+
...options2,
|
|
417
|
+
maxAge: 0,
|
|
418
|
+
expires: /* @__PURE__ */ new Date(0)
|
|
419
|
+
})
|
|
420
|
+
);
|
|
421
|
+
return res;
|
|
422
|
+
};
|
|
423
|
+
next();
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
129
427
|
// lib/index.ts
|
|
130
|
-
function frameworkError(message, skipFn, code) {
|
|
428
|
+
function frameworkError(message, skipFn, code, status) {
|
|
131
429
|
const err = new Error(message);
|
|
132
430
|
Error.captureStackTrace(err, skipFn);
|
|
431
|
+
err.cpeak_err = true;
|
|
133
432
|
if (code) err.code = code;
|
|
433
|
+
if (status) err.status = status;
|
|
134
434
|
return err;
|
|
135
435
|
}
|
|
136
436
|
var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
|
|
@@ -138,196 +438,242 @@ var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
|
|
|
138
438
|
ErrorCode2["FILE_NOT_FOUND"] = "CPEAK_ERR_FILE_NOT_FOUND";
|
|
139
439
|
ErrorCode2["NOT_A_FILE"] = "CPEAK_ERR_NOT_A_FILE";
|
|
140
440
|
ErrorCode2["SEND_FILE_FAIL"] = "CPEAK_ERR_SEND_FILE_FAIL";
|
|
441
|
+
ErrorCode2["INVALID_JSON"] = "CPEAK_ERR_INVALID_JSON";
|
|
442
|
+
ErrorCode2["PAYLOAD_TOO_LARGE"] = "CPEAK_ERR_PAYLOAD_TOO_LARGE";
|
|
443
|
+
ErrorCode2["WEAK_SECRET"] = "CPEAK_ERR_WEAK_SECRET";
|
|
141
444
|
return ErrorCode2;
|
|
142
445
|
})(ErrorCode || {});
|
|
446
|
+
var CpeakIncomingMessage = class extends http.IncomingMessage {
|
|
447
|
+
// We define body and params here for better V8 optimization (not changing the shape of the object at runtime)
|
|
448
|
+
body = void 0;
|
|
449
|
+
params = {};
|
|
450
|
+
#query;
|
|
451
|
+
// Parse the URL parameters (like /users?key1=value1&key2=value2)
|
|
452
|
+
// We will call this query to be more familiar with other node.js frameworks.
|
|
453
|
+
// This is a getter method (accessed like a property)
|
|
454
|
+
get query() {
|
|
455
|
+
if (this.#query) return this.#query;
|
|
456
|
+
const url = this.url || "";
|
|
457
|
+
const qIndex = url.indexOf("?");
|
|
458
|
+
if (qIndex === -1) {
|
|
459
|
+
this.#query = {};
|
|
460
|
+
} else {
|
|
461
|
+
const searchParams = new URLSearchParams(url.substring(qIndex + 1));
|
|
462
|
+
this.#query = Object.fromEntries(searchParams.entries());
|
|
463
|
+
}
|
|
464
|
+
return this.#query;
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
var CpeakServerResponse = class extends http.ServerResponse {
|
|
468
|
+
// Send a file back to the client
|
|
469
|
+
async sendFile(path2, mime) {
|
|
470
|
+
if (!mime) {
|
|
471
|
+
throw frameworkError(
|
|
472
|
+
'MIME type is missing. Use res.sendFile(path, "mime-type").',
|
|
473
|
+
this.sendFile,
|
|
474
|
+
"CPEAK_ERR_MISSING_MIME" /* MISSING_MIME */
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
const stat = await fs3.stat(path2);
|
|
479
|
+
if (!stat.isFile()) {
|
|
480
|
+
throw frameworkError(
|
|
481
|
+
`Not a file: ${path2}`,
|
|
482
|
+
this.sendFile,
|
|
483
|
+
"CPEAK_ERR_NOT_A_FILE" /* NOT_A_FILE */
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
this.setHeader("Content-Type", mime);
|
|
487
|
+
this.setHeader("Content-Length", String(stat.size));
|
|
488
|
+
await pipeline(createReadStream(path2), this);
|
|
489
|
+
} catch (err) {
|
|
490
|
+
if (err?.code === "ENOENT") {
|
|
491
|
+
throw frameworkError(
|
|
492
|
+
`File not found: ${path2}`,
|
|
493
|
+
this.sendFile,
|
|
494
|
+
"CPEAK_ERR_FILE_NOT_FOUND" /* FILE_NOT_FOUND */
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
throw frameworkError(
|
|
498
|
+
`Failed to send file: ${path2}`,
|
|
499
|
+
this.sendFile,
|
|
500
|
+
"CPEAK_ERR_SEND_FILE_FAIL" /* SEND_FILE_FAIL */
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
// Set the status code of the response
|
|
505
|
+
status(code) {
|
|
506
|
+
this.statusCode = code;
|
|
507
|
+
return this;
|
|
508
|
+
}
|
|
509
|
+
// Set the Content-Disposition header to prompt the user to download a file
|
|
510
|
+
attachment(filename) {
|
|
511
|
+
const contentDisposition = filename ? `attachment; filename="${filename}"` : "attachment";
|
|
512
|
+
this.setHeader("Content-Disposition", contentDisposition);
|
|
513
|
+
return this;
|
|
514
|
+
}
|
|
515
|
+
// Redirects to a new URL
|
|
516
|
+
redirect(location) {
|
|
517
|
+
this.writeHead(302, { Location: location });
|
|
518
|
+
this.end();
|
|
519
|
+
}
|
|
520
|
+
// Send a json data back to the client (for small json data, less than the highWaterMark)
|
|
521
|
+
json(data) {
|
|
522
|
+
this.setHeader("Content-Type", "application/json");
|
|
523
|
+
this.end(JSON.stringify(data));
|
|
524
|
+
}
|
|
525
|
+
};
|
|
143
526
|
var Cpeak = class {
|
|
144
|
-
server;
|
|
145
|
-
routes;
|
|
146
|
-
middleware;
|
|
147
|
-
|
|
527
|
+
#server;
|
|
528
|
+
#routes;
|
|
529
|
+
#middleware;
|
|
530
|
+
#handleErr;
|
|
148
531
|
constructor() {
|
|
149
|
-
this
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
throw frameworkError(
|
|
165
|
-
`Not a file: ${path2}`,
|
|
166
|
-
res.sendFile,
|
|
167
|
-
"CPEAK_ERR_NOT_A_FILE" /* NOT_A_FILE */
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
res.setHeader("Content-Type", mime);
|
|
171
|
-
res.setHeader("Content-Length", String(stat.size));
|
|
172
|
-
await pipeline(createReadStream(path2), res);
|
|
173
|
-
} catch (err) {
|
|
174
|
-
if (err?.code === "ENOENT") {
|
|
175
|
-
throw frameworkError(
|
|
176
|
-
`File not found: ${path2}`,
|
|
177
|
-
res.sendFile,
|
|
178
|
-
"CPEAK_ERR_FILE_NOT_FOUND" /* FILE_NOT_FOUND */
|
|
179
|
-
);
|
|
532
|
+
this.#server = http.createServer({
|
|
533
|
+
IncomingMessage: CpeakIncomingMessage,
|
|
534
|
+
ServerResponse: CpeakServerResponse
|
|
535
|
+
});
|
|
536
|
+
this.#routes = {};
|
|
537
|
+
this.#middleware = [];
|
|
538
|
+
this.#server.on(
|
|
539
|
+
"request",
|
|
540
|
+
async (req, res) => {
|
|
541
|
+
const qIndex = req.url?.indexOf("?");
|
|
542
|
+
const urlWithoutQueries = qIndex === -1 ? req.url || "" : req.url?.substring(0, qIndex);
|
|
543
|
+
const dispatchError = (error) => {
|
|
544
|
+
if (res.headersSent) {
|
|
545
|
+
req.socket?.destroy();
|
|
546
|
+
return;
|
|
180
547
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
try {
|
|
209
|
-
const handlerResult = cb(req2, res2, (error) => {
|
|
210
|
-
res2.setHeader("Connection", "close");
|
|
211
|
-
this._handleErr?.(error, req2, res2);
|
|
212
|
-
});
|
|
213
|
-
if (handlerResult && typeof handlerResult.then === "function") {
|
|
214
|
-
handlerResult.catch((error) => {
|
|
215
|
-
res2.setHeader("Connection", "close");
|
|
216
|
-
this._handleErr?.(error, req2, res2);
|
|
217
|
-
});
|
|
548
|
+
res.setHeader("Connection", "close");
|
|
549
|
+
this.#handleErr?.(error, req, res);
|
|
550
|
+
};
|
|
551
|
+
const runHandler = async (req2, res2, middleware, cb, index) => {
|
|
552
|
+
if (index === middleware.length) {
|
|
553
|
+
try {
|
|
554
|
+
await cb(req2, res2, dispatchError);
|
|
555
|
+
} catch (error) {
|
|
556
|
+
dispatchError(error);
|
|
557
|
+
}
|
|
558
|
+
} else {
|
|
559
|
+
try {
|
|
560
|
+
await middleware[index](
|
|
561
|
+
req2,
|
|
562
|
+
res2,
|
|
563
|
+
// The next function
|
|
564
|
+
async (error) => {
|
|
565
|
+
if (error) {
|
|
566
|
+
return dispatchError(error);
|
|
567
|
+
}
|
|
568
|
+
await runHandler(req2, res2, middleware, cb, index + 1);
|
|
569
|
+
},
|
|
570
|
+
// Error handler for a route middleware
|
|
571
|
+
dispatchError
|
|
572
|
+
);
|
|
573
|
+
} catch (error) {
|
|
574
|
+
dispatchError(error);
|
|
218
575
|
}
|
|
219
|
-
return handlerResult;
|
|
220
|
-
} catch (error) {
|
|
221
|
-
res2.setHeader("Connection", "close");
|
|
222
|
-
this._handleErr?.(error, req2, res2);
|
|
223
576
|
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if (
|
|
232
|
-
|
|
233
|
-
|
|
577
|
+
};
|
|
578
|
+
const runMiddleware = async (req2, res2, middleware, index) => {
|
|
579
|
+
if (index === middleware.length) {
|
|
580
|
+
const routes = this.#routes[req2.method?.toLowerCase() || ""];
|
|
581
|
+
if (routes && typeof routes[Symbol.iterator] === "function")
|
|
582
|
+
for (const route of routes) {
|
|
583
|
+
const match = urlWithoutQueries?.match(route.regex);
|
|
584
|
+
if (match) {
|
|
585
|
+
const pathVariables = this.#extractPathVariables(
|
|
586
|
+
route.path,
|
|
587
|
+
match
|
|
588
|
+
);
|
|
589
|
+
req2.params = pathVariables;
|
|
590
|
+
return await runHandler(
|
|
591
|
+
req2,
|
|
592
|
+
res2,
|
|
593
|
+
route.middleware,
|
|
594
|
+
route.cb,
|
|
595
|
+
0
|
|
596
|
+
);
|
|
234
597
|
}
|
|
235
|
-
runHandler(req2, res2, middleware, cb, index + 1);
|
|
236
|
-
},
|
|
237
|
-
// Error handler for a route middleware
|
|
238
|
-
(error) => {
|
|
239
|
-
res2.setHeader("Connection", "close");
|
|
240
|
-
this._handleErr?.(error, req2, res2);
|
|
241
598
|
}
|
|
242
|
-
);
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
599
|
+
return res2.status(404).json({ error: `Cannot ${req2.method} ${urlWithoutQueries}` });
|
|
600
|
+
} else {
|
|
601
|
+
try {
|
|
602
|
+
await middleware[index](req2, res2, async (err) => {
|
|
603
|
+
if (err) {
|
|
604
|
+
return dispatchError(err);
|
|
605
|
+
}
|
|
606
|
+
await runMiddleware(req2, res2, middleware, index + 1);
|
|
247
607
|
});
|
|
608
|
+
} catch (error) {
|
|
609
|
+
dispatchError(error);
|
|
248
610
|
}
|
|
249
|
-
} catch (error) {
|
|
250
|
-
res2.setHeader("Connection", "close");
|
|
251
|
-
this._handleErr?.(error, req2, res2);
|
|
252
611
|
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
const routes = this.routes[req2.method?.toLowerCase() || ""];
|
|
258
|
-
if (routes && typeof routes[Symbol.iterator] === "function")
|
|
259
|
-
for (const route of routes) {
|
|
260
|
-
const match = urlWithoutParams?.match(route.regex);
|
|
261
|
-
if (match) {
|
|
262
|
-
const vars = this.#extractVars(route.path, match);
|
|
263
|
-
req2.vars = vars;
|
|
264
|
-
return runHandler(req2, res2, route.middleware, route.cb, 0);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
return res2.status(404).json({ error: `Cannot ${req2.method} ${urlWithoutParams}` });
|
|
268
|
-
} else {
|
|
269
|
-
middleware[index](req2, res2, () => {
|
|
270
|
-
runMiddleware(req2, res2, middleware, index + 1);
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
};
|
|
274
|
-
runMiddleware(req, res, this.middleware, 0);
|
|
275
|
-
});
|
|
612
|
+
};
|
|
613
|
+
await runMiddleware(req, res, this.#middleware, 0);
|
|
614
|
+
}
|
|
615
|
+
);
|
|
276
616
|
}
|
|
277
617
|
route(method, path2, ...args) {
|
|
278
|
-
if (!this
|
|
618
|
+
if (!this.#routes[method]) this.#routes[method] = [];
|
|
279
619
|
const cb = args.pop();
|
|
280
620
|
if (!cb || typeof cb !== "function") {
|
|
281
621
|
throw new Error("Route definition must include a handler");
|
|
282
622
|
}
|
|
283
623
|
const middleware = args.flat();
|
|
284
624
|
const regex = this.#pathToRegex(path2);
|
|
285
|
-
this
|
|
625
|
+
this.#routes[method].push({ path: path2, regex, middleware, cb });
|
|
286
626
|
}
|
|
287
627
|
beforeEach(cb) {
|
|
288
|
-
this
|
|
628
|
+
this.#middleware.push(cb);
|
|
289
629
|
}
|
|
290
630
|
handleErr(cb) {
|
|
291
|
-
this
|
|
631
|
+
this.#handleErr = cb;
|
|
292
632
|
}
|
|
293
633
|
listen(port, cb) {
|
|
294
|
-
return this
|
|
634
|
+
return this.#server.listen(port, cb);
|
|
635
|
+
}
|
|
636
|
+
address() {
|
|
637
|
+
return this.#server.address();
|
|
295
638
|
}
|
|
296
639
|
close(cb) {
|
|
297
|
-
this
|
|
640
|
+
this.#server.close(cb);
|
|
298
641
|
}
|
|
299
642
|
// ------------------------------
|
|
300
643
|
// PRIVATE METHODS:
|
|
301
644
|
// ------------------------------
|
|
302
645
|
#pathToRegex(path2) {
|
|
303
|
-
const
|
|
304
|
-
|
|
305
|
-
varNames.push(match.slice(1));
|
|
306
|
-
return "([^/]+)";
|
|
307
|
-
}) + "$";
|
|
308
|
-
const regex = new RegExp(regexString);
|
|
309
|
-
return regex;
|
|
646
|
+
const regexString = "^" + path2.replace(/:\w+/g, "([^/]+)").replace(/\*/g, ".*") + "$";
|
|
647
|
+
return new RegExp(regexString);
|
|
310
648
|
}
|
|
311
|
-
#
|
|
312
|
-
const
|
|
313
|
-
(
|
|
649
|
+
#extractPathVariables(path2, match) {
|
|
650
|
+
const paramNames = (path2.match(/:\w+/g) || []).map(
|
|
651
|
+
(param) => param.slice(1)
|
|
314
652
|
);
|
|
315
|
-
const
|
|
316
|
-
|
|
317
|
-
|
|
653
|
+
const params = {};
|
|
654
|
+
paramNames.forEach((name, index) => {
|
|
655
|
+
params[name] = match[index + 1];
|
|
318
656
|
});
|
|
319
|
-
return
|
|
657
|
+
return params;
|
|
320
658
|
}
|
|
321
659
|
};
|
|
322
660
|
function cpeak() {
|
|
323
661
|
return new Cpeak();
|
|
324
662
|
}
|
|
325
663
|
export {
|
|
664
|
+
Cpeak,
|
|
665
|
+
CpeakIncomingMessage,
|
|
666
|
+
CpeakServerResponse,
|
|
326
667
|
ErrorCode,
|
|
668
|
+
auth,
|
|
669
|
+
cookieParser,
|
|
327
670
|
cpeak as default,
|
|
328
671
|
frameworkError,
|
|
672
|
+
hashPassword,
|
|
329
673
|
parseJSON,
|
|
330
674
|
render,
|
|
331
|
-
serveStatic
|
|
675
|
+
serveStatic,
|
|
676
|
+
swagger,
|
|
677
|
+
verifyPassword
|
|
332
678
|
};
|
|
333
679
|
//# sourceMappingURL=index.js.map
|