cpeak 2.5.0 → 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 +220 -2
- package/dist/index.d.ts +55 -12
- package/dist/index.js +434 -159
- package/dist/index.js.map +1 -1
- package/lib/index.ts +158 -151
- package/lib/types.ts +6 -4
- package/lib/utils/auth.ts +170 -0
- package/lib/utils/cookieParser.ts +189 -0
- package/lib/utils/index.ts +16 -1
- package/lib/utils/serveStatic.ts +16 -5
- package/lib/utils/swagger.ts +31 -0
- package/package.json +1 -1
- /package/lib/utils/{paseJSON.ts → parseJSON.ts} +0 -0
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,68 +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/paseJSON.ts
|
|
69
|
-
import { Buffer } from "buffer";
|
|
70
|
-
function isJSON(contentType) {
|
|
71
|
-
if (!contentType) return false;
|
|
72
|
-
if (contentType === "application/json") return true;
|
|
73
|
-
return contentType.startsWith("application/json") || contentType.includes("+json");
|
|
74
|
-
}
|
|
75
|
-
var parseJSON = (options = {}) => {
|
|
76
|
-
const limit = options.limit || 1024 * 1024;
|
|
77
|
-
return (req, res, next) => {
|
|
78
|
-
if (!isJSON(req.headers["content-type"])) return next();
|
|
79
|
-
const chunks = [];
|
|
80
|
-
let bytesReceived = 0;
|
|
81
|
-
const onData = (chunk) => {
|
|
82
|
-
bytesReceived += chunk.length;
|
|
83
|
-
if (bytesReceived > limit) {
|
|
84
|
-
req.pause();
|
|
85
|
-
req.removeListener("data", onData);
|
|
86
|
-
req.removeListener("end", onEnd);
|
|
87
|
-
next(
|
|
88
|
-
frameworkError(
|
|
89
|
-
"JSON body too large",
|
|
90
|
-
onData,
|
|
91
|
-
"CPEAK_ERR_PAYLOAD_TOO_LARGE" /* PAYLOAD_TOO_LARGE */,
|
|
92
|
-
413
|
|
93
|
-
// HTTP 413 Payload Too Large
|
|
94
|
-
)
|
|
95
|
-
);
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
chunks.push(chunk);
|
|
99
|
-
};
|
|
100
|
-
const onEnd = () => {
|
|
101
|
-
try {
|
|
102
|
-
const rawBody = chunks.length === 1 ? chunks[0].toString("utf-8") : Buffer.concat(chunks).toString("utf-8");
|
|
103
|
-
req.body = rawBody ? JSON.parse(rawBody) : {};
|
|
104
|
-
next();
|
|
105
|
-
} catch (err) {
|
|
106
|
-
next(
|
|
107
|
-
frameworkError(
|
|
108
|
-
"Invalid JSON format",
|
|
109
|
-
onEnd,
|
|
110
|
-
"CPEAK_ERR_INVALID_JSON" /* INVALID_JSON */,
|
|
111
|
-
400
|
|
112
|
-
// HTTP 400 Bad Request
|
|
113
|
-
)
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
};
|
|
117
|
-
req.on("data", onData);
|
|
118
|
-
req.on("end", onEnd);
|
|
119
|
-
};
|
|
120
|
-
};
|
|
121
|
-
|
|
122
128
|
// lib/utils/render.ts
|
|
123
129
|
import fs2 from "fs/promises";
|
|
124
130
|
function renderTemplate(templateStr, data) {
|
|
@@ -161,6 +167,263 @@ var render = () => {
|
|
|
161
167
|
};
|
|
162
168
|
};
|
|
163
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
|
+
|
|
164
427
|
// lib/index.ts
|
|
165
428
|
function frameworkError(message, skipFn, code, status) {
|
|
166
429
|
const err = new Error(message);
|
|
@@ -177,27 +440,28 @@ var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
|
|
|
177
440
|
ErrorCode2["SEND_FILE_FAIL"] = "CPEAK_ERR_SEND_FILE_FAIL";
|
|
178
441
|
ErrorCode2["INVALID_JSON"] = "CPEAK_ERR_INVALID_JSON";
|
|
179
442
|
ErrorCode2["PAYLOAD_TOO_LARGE"] = "CPEAK_ERR_PAYLOAD_TOO_LARGE";
|
|
443
|
+
ErrorCode2["WEAK_SECRET"] = "CPEAK_ERR_WEAK_SECRET";
|
|
180
444
|
return ErrorCode2;
|
|
181
445
|
})(ErrorCode || {});
|
|
182
446
|
var CpeakIncomingMessage = class extends http.IncomingMessage {
|
|
183
447
|
// We define body and params here for better V8 optimization (not changing the shape of the object at runtime)
|
|
184
448
|
body = void 0;
|
|
185
449
|
params = {};
|
|
186
|
-
|
|
450
|
+
#query;
|
|
187
451
|
// Parse the URL parameters (like /users?key1=value1&key2=value2)
|
|
188
452
|
// We will call this query to be more familiar with other node.js frameworks.
|
|
189
453
|
// This is a getter method (accessed like a property)
|
|
190
454
|
get query() {
|
|
191
|
-
if (this
|
|
455
|
+
if (this.#query) return this.#query;
|
|
192
456
|
const url = this.url || "";
|
|
193
457
|
const qIndex = url.indexOf("?");
|
|
194
458
|
if (qIndex === -1) {
|
|
195
|
-
this
|
|
459
|
+
this.#query = {};
|
|
196
460
|
} else {
|
|
197
461
|
const searchParams = new URLSearchParams(url.substring(qIndex + 1));
|
|
198
|
-
this
|
|
462
|
+
this.#query = Object.fromEntries(searchParams.entries());
|
|
199
463
|
}
|
|
200
|
-
return this
|
|
464
|
+
return this.#query;
|
|
201
465
|
}
|
|
202
466
|
};
|
|
203
467
|
var CpeakServerResponse = class extends http.ServerResponse {
|
|
@@ -242,11 +506,16 @@ var CpeakServerResponse = class extends http.ServerResponse {
|
|
|
242
506
|
this.statusCode = code;
|
|
243
507
|
return this;
|
|
244
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
|
+
}
|
|
245
515
|
// Redirects to a new URL
|
|
246
516
|
redirect(location) {
|
|
247
517
|
this.writeHead(302, { Location: location });
|
|
248
518
|
this.end();
|
|
249
|
-
return this;
|
|
250
519
|
}
|
|
251
520
|
// Send a json data back to the client (for small json data, less than the highWaterMark)
|
|
252
521
|
json(data) {
|
|
@@ -255,129 +524,127 @@ var CpeakServerResponse = class extends http.ServerResponse {
|
|
|
255
524
|
}
|
|
256
525
|
};
|
|
257
526
|
var Cpeak = class {
|
|
258
|
-
server;
|
|
259
|
-
routes;
|
|
260
|
-
middleware;
|
|
261
|
-
|
|
527
|
+
#server;
|
|
528
|
+
#routes;
|
|
529
|
+
#middleware;
|
|
530
|
+
#handleErr;
|
|
262
531
|
constructor() {
|
|
263
|
-
this
|
|
532
|
+
this.#server = http.createServer({
|
|
264
533
|
IncomingMessage: CpeakIncomingMessage,
|
|
265
534
|
ServerResponse: CpeakServerResponse
|
|
266
535
|
});
|
|
267
|
-
this
|
|
268
|
-
this
|
|
269
|
-
this
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
});
|
|
279
|
-
} catch (error) {
|
|
280
|
-
res2.setHeader("Connection", "close");
|
|
281
|
-
this._handleErr?.(error, req2, res2);
|
|
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;
|
|
282
547
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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);
|
|
575
|
+
}
|
|
305
576
|
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
+
);
|
|
597
|
+
}
|
|
327
598
|
}
|
|
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);
|
|
607
|
+
});
|
|
608
|
+
} catch (error) {
|
|
609
|
+
dispatchError(error);
|
|
328
610
|
}
|
|
329
|
-
return res2.status(404).json({ error: `Cannot ${req2.method} ${urlWithoutQueries}` });
|
|
330
|
-
} else {
|
|
331
|
-
try {
|
|
332
|
-
await middleware[index](req2, res2, async (err) => {
|
|
333
|
-
if (err) {
|
|
334
|
-
res2.setHeader("Connection", "close");
|
|
335
|
-
return this._handleErr?.(err, req2, res2);
|
|
336
|
-
}
|
|
337
|
-
await runMiddleware(req2, res2, middleware, index + 1);
|
|
338
|
-
});
|
|
339
|
-
} catch (error) {
|
|
340
|
-
res2.setHeader("Connection", "close");
|
|
341
|
-
this._handleErr?.(error, req2, res2);
|
|
342
611
|
}
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
612
|
+
};
|
|
613
|
+
await runMiddleware(req, res, this.#middleware, 0);
|
|
614
|
+
}
|
|
615
|
+
);
|
|
347
616
|
}
|
|
348
617
|
route(method, path2, ...args) {
|
|
349
|
-
if (!this
|
|
618
|
+
if (!this.#routes[method]) this.#routes[method] = [];
|
|
350
619
|
const cb = args.pop();
|
|
351
620
|
if (!cb || typeof cb !== "function") {
|
|
352
621
|
throw new Error("Route definition must include a handler");
|
|
353
622
|
}
|
|
354
623
|
const middleware = args.flat();
|
|
355
624
|
const regex = this.#pathToRegex(path2);
|
|
356
|
-
this
|
|
625
|
+
this.#routes[method].push({ path: path2, regex, middleware, cb });
|
|
357
626
|
}
|
|
358
627
|
beforeEach(cb) {
|
|
359
|
-
this
|
|
628
|
+
this.#middleware.push(cb);
|
|
360
629
|
}
|
|
361
630
|
handleErr(cb) {
|
|
362
|
-
this
|
|
631
|
+
this.#handleErr = cb;
|
|
363
632
|
}
|
|
364
633
|
listen(port, cb) {
|
|
365
|
-
return this
|
|
634
|
+
return this.#server.listen(port, cb);
|
|
635
|
+
}
|
|
636
|
+
address() {
|
|
637
|
+
return this.#server.address();
|
|
366
638
|
}
|
|
367
639
|
close(cb) {
|
|
368
|
-
this
|
|
640
|
+
this.#server.close(cb);
|
|
369
641
|
}
|
|
370
642
|
// ------------------------------
|
|
371
643
|
// PRIVATE METHODS:
|
|
372
644
|
// ------------------------------
|
|
373
645
|
#pathToRegex(path2) {
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
paramNames.push(match.slice(1));
|
|
377
|
-
return "([^/]+)";
|
|
378
|
-
}) + "$";
|
|
379
|
-
const regex = new RegExp(regexString);
|
|
380
|
-
return regex;
|
|
646
|
+
const regexString = "^" + path2.replace(/:\w+/g, "([^/]+)").replace(/\*/g, ".*") + "$";
|
|
647
|
+
return new RegExp(regexString);
|
|
381
648
|
}
|
|
382
649
|
#extractPathVariables(path2, match) {
|
|
383
650
|
const paramNames = (path2.match(/:\w+/g) || []).map(
|
|
@@ -394,11 +661,19 @@ function cpeak() {
|
|
|
394
661
|
return new Cpeak();
|
|
395
662
|
}
|
|
396
663
|
export {
|
|
664
|
+
Cpeak,
|
|
665
|
+
CpeakIncomingMessage,
|
|
666
|
+
CpeakServerResponse,
|
|
397
667
|
ErrorCode,
|
|
668
|
+
auth,
|
|
669
|
+
cookieParser,
|
|
398
670
|
cpeak as default,
|
|
399
671
|
frameworkError,
|
|
672
|
+
hashPassword,
|
|
400
673
|
parseJSON,
|
|
401
674
|
render,
|
|
402
|
-
serveStatic
|
|
675
|
+
serveStatic,
|
|
676
|
+
swagger,
|
|
677
|
+
verifyPassword
|
|
403
678
|
};
|
|
404
679
|
//# sourceMappingURL=index.js.map
|