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/dist/index.js CHANGED
@@ -2,71 +2,120 @@
2
2
  import http from "http";
3
3
  import fs3 from "fs/promises";
4
4
  import { createReadStream } from "fs";
5
- import { pipeline } from "stream/promises";
5
+ import { pipeline as pipeline2 } from "stream/promises";
6
6
 
7
- // lib/utils/serveStatic.ts
8
- import fs from "fs";
9
- import path from "path";
10
- var MIME_TYPES = {
11
- html: "text/html",
12
- css: "text/css",
13
- js: "application/javascript",
14
- jpg: "image/jpeg",
15
- jpeg: "image/jpeg",
16
- png: "image/png",
17
- svg: "image/svg+xml",
18
- txt: "text/plain",
19
- eot: "application/vnd.ms-fontobject",
20
- otf: "font/otf",
21
- ttf: "font/ttf",
22
- woff: "font/woff",
23
- woff2: "font/woff2"
24
- };
25
- var serveStatic = (folderPath, newMimeTypes) => {
26
- if (newMimeTypes) {
27
- Object.assign(MIME_TYPES, newMimeTypes);
28
- }
29
- function processFolder(folderPath2, parentFolder) {
30
- const staticFiles = [];
31
- const files = fs.readdirSync(folderPath2);
32
- for (const file of files) {
33
- const fullPath = path.join(folderPath2, file);
34
- if (fs.statSync(fullPath).isDirectory()) {
35
- const subfolderFiles = processFolder(fullPath, parentFolder);
36
- staticFiles.push(...subfolderFiles);
37
- } else {
38
- const relativePath = path.relative(parentFolder, fullPath);
39
- const fileExtension = path.extname(file).slice(1);
40
- if (MIME_TYPES[fileExtension]) staticFiles.push("/" + relativePath);
41
- }
7
+ // lib/utils/compression.ts
8
+ import zlib from "zlib";
9
+ import { Readable } from "stream";
10
+ import { Buffer as Buffer2 } from "buffer";
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]);
42
26
  }
43
- return staticFiles;
27
+ if (Number.isNaN(q)) q = 0;
28
+ if (name === "*") wildcard = q;
29
+ else accepted[name] = q;
44
30
  }
45
- const filesArrayToFilesMap = (filesArray) => {
46
- const filesMap2 = {};
47
- for (const file of filesArray) {
48
- const fileExtension = path.extname(file).slice(1);
49
- filesMap2[file] = {
50
- path: folderPath + file,
51
- mime: MIME_TYPES[fileExtension]
52
- };
53
- }
54
- return filesMap2;
31
+ const tryPick = (enc) => {
32
+ const q = enc in accepted ? accepted[enc] : wildcard;
33
+ return q !== void 0 && q > 0;
55
34
  };
56
- const filesMap = filesArrayToFilesMap(processFolder(folderPath, folderPath));
57
- return function(req, res, next) {
58
- const url = req.url;
59
- if (typeof url !== "string") return next();
60
- if (Object.prototype.hasOwnProperty.call(filesMap, url)) {
61
- const fileRoute = filesMap[url];
62
- return res.sendFile(fileRoute.path, fileRoute.mime);
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 || {}
63
55
  }
64
- next();
65
56
  };
66
- };
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
+ }
67
116
 
68
- // lib/utils/paseJSON.ts
69
- import { Buffer } from "buffer";
117
+ // lib/utils/parseJSON.ts
118
+ import { Buffer as Buffer3 } from "buffer";
70
119
  function isJSON(contentType) {
71
120
  if (!contentType) return false;
72
121
  if (contentType === "application/json") return true;
@@ -99,7 +148,7 @@ var parseJSON = (options = {}) => {
99
148
  };
100
149
  const onEnd = () => {
101
150
  try {
102
- const rawBody = chunks.length === 1 ? chunks[0].toString("utf-8") : Buffer.concat(chunks).toString("utf-8");
151
+ const rawBody = chunks.length === 1 ? chunks[0].toString("utf-8") : Buffer3.concat(chunks).toString("utf-8");
103
152
  req.body = rawBody ? JSON.parse(rawBody) : {};
104
153
  next();
105
154
  } catch (err) {
@@ -119,6 +168,73 @@ var parseJSON = (options = {}) => {
119
168
  };
120
169
  };
121
170
 
171
+ // lib/utils/serveStatic.ts
172
+ import fs from "fs";
173
+ import path from "path";
174
+ var MIME_TYPES = {
175
+ html: "text/html",
176
+ css: "text/css",
177
+ js: "application/javascript",
178
+ jpg: "image/jpeg",
179
+ jpeg: "image/jpeg",
180
+ png: "image/png",
181
+ svg: "image/svg+xml",
182
+ txt: "text/plain",
183
+ eot: "application/vnd.ms-fontobject",
184
+ otf: "font/otf",
185
+ ttf: "font/ttf",
186
+ woff: "font/woff",
187
+ woff2: "font/woff2",
188
+ gif: "image/gif",
189
+ ico: "image/x-icon",
190
+ json: "application/json",
191
+ webmanifest: "application/manifest+json"
192
+ };
193
+ var serveStatic = (folderPath, newMimeTypes, options) => {
194
+ if (newMimeTypes) {
195
+ Object.assign(MIME_TYPES, newMimeTypes);
196
+ }
197
+ const prefix = options?.prefix ?? "";
198
+ function processFolder(folderPath2, parentFolder) {
199
+ const staticFiles = [];
200
+ const files = fs.readdirSync(folderPath2);
201
+ for (const file of files) {
202
+ const fullPath = path.join(folderPath2, file);
203
+ if (fs.statSync(fullPath).isDirectory()) {
204
+ const subfolderFiles = processFolder(fullPath, parentFolder);
205
+ staticFiles.push(...subfolderFiles);
206
+ } else {
207
+ const relativePath = path.relative(parentFolder, fullPath);
208
+ const fileExtension = path.extname(file).slice(1);
209
+ if (MIME_TYPES[fileExtension]) staticFiles.push("/" + relativePath);
210
+ }
211
+ }
212
+ return staticFiles;
213
+ }
214
+ const filesArrayToFilesMap = (filesArray) => {
215
+ const filesMap2 = {};
216
+ for (const file of filesArray) {
217
+ const fileExtension = path.extname(file).slice(1);
218
+ filesMap2[prefix + file] = {
219
+ path: folderPath + file,
220
+ mime: MIME_TYPES[fileExtension]
221
+ };
222
+ }
223
+ return filesMap2;
224
+ };
225
+ const filesMap = filesArrayToFilesMap(processFolder(folderPath, folderPath));
226
+ return function(req, res, next) {
227
+ const url = req.url;
228
+ if (typeof url !== "string") return next();
229
+ const pathname = url.split("?")[0];
230
+ if (Object.prototype.hasOwnProperty.call(filesMap, pathname)) {
231
+ const fileRoute = filesMap[pathname];
232
+ return res.sendFile(fileRoute.path, fileRoute.mime);
233
+ }
234
+ next();
235
+ };
236
+ };
237
+
122
238
  // lib/utils/render.ts
123
239
  import fs2 from "fs/promises";
124
240
  function renderTemplate(templateStr, data) {
@@ -154,6 +270,11 @@ var render = () => {
154
270
  }
155
271
  let fileStr = await fs2.readFile(path2, "utf-8");
156
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
+ }
157
278
  res.setHeader("Content-Type", mime);
158
279
  res.end(finalStr);
159
280
  };
@@ -161,6 +282,326 @@ var render = () => {
161
282
  };
162
283
  };
163
284
 
285
+ // lib/utils/swagger.ts
286
+ var swagger = (spec, prefix = "/api-docs") => {
287
+ const initializerJs = `window.onload = function() {
288
+ SwaggerUIBundle({
289
+ url: "${prefix}/spec.json",
290
+ dom_id: '#swagger-ui',
291
+ presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
292
+ layout: "StandaloneLayout"
293
+ });
294
+ };`;
295
+ return (req, res, next) => {
296
+ if (req.url === prefix || req.url === `${prefix}/`) {
297
+ res.writeHead(302, { Location: `${prefix}/index.html` });
298
+ res.end();
299
+ return;
300
+ }
301
+ if (req.url === `${prefix}/spec.json`) {
302
+ return res.json(spec);
303
+ }
304
+ if (req.url === `${prefix}/swagger-initializer.js`) {
305
+ res.setHeader("Content-Type", "application/javascript");
306
+ res.end(initializerJs);
307
+ return;
308
+ }
309
+ next();
310
+ };
311
+ };
312
+
313
+ // lib/utils/auth.ts
314
+ import { randomBytes, pbkdf2, createHmac, timingSafeEqual } from "crypto";
315
+ import { promisify } from "util";
316
+ var pbkdf2Async = promisify(pbkdf2);
317
+ var DEFAULTS = {
318
+ iterations: 21e4,
319
+ keylen: 64,
320
+ digest: "sha512",
321
+ saltSize: 32,
322
+ hmacAlgorithm: "sha256",
323
+ tokenIdSize: 20,
324
+ tokenExpiry: 7 * 24 * 60 * 60 * 1e3
325
+ // 7 days in ms
326
+ };
327
+ async function hashPassword(password, options) {
328
+ const iterations = options?.iterations ?? DEFAULTS.iterations;
329
+ const keylen = options?.keylen ?? DEFAULTS.keylen;
330
+ const digest = options?.digest ?? DEFAULTS.digest;
331
+ const saltSize = options?.saltSize ?? DEFAULTS.saltSize;
332
+ const salt = randomBytes(saltSize);
333
+ const hash = await pbkdf2Async(password, salt, iterations, keylen, digest);
334
+ return `pbkdf2:${iterations}:${keylen}:${digest}:${salt.toString("hex")}:${hash.toString("hex")}`;
335
+ }
336
+ async function verifyPassword(password, stored) {
337
+ const withoutPrefix = stored.slice(stored.indexOf(":") + 1);
338
+ const parts = withoutPrefix.split(":");
339
+ if (parts.length !== 5) return false;
340
+ const [itersStr, keylenStr, digest, saltHex, hashHex] = parts;
341
+ const iterations = parseInt(itersStr, 10);
342
+ const keylen = parseInt(keylenStr, 10);
343
+ if (!digest || !saltHex || !hashHex || isNaN(iterations) || isNaN(keylen))
344
+ return false;
345
+ const salt = Buffer.from(saltHex, "hex");
346
+ const hash = await pbkdf2Async(password, salt, iterations, keylen, digest);
347
+ const storedHash = Buffer.from(hashHex, "hex");
348
+ if (storedHash.length !== hash.length) return false;
349
+ return timingSafeEqual(hash, storedHash);
350
+ }
351
+ function signToken(tokenId, secret, algorithm) {
352
+ const sig = createHmac(algorithm, secret).update(tokenId).digest("hex");
353
+ return `${tokenId}.${sig}`;
354
+ }
355
+ function extractTokenId(token, secret, algorithm) {
356
+ const dot = token.indexOf(".");
357
+ if (dot === -1) return null;
358
+ const tokenId = token.slice(0, dot);
359
+ const sig = token.slice(dot + 1);
360
+ const expected = createHmac(algorithm, secret).update(tokenId).digest("hex");
361
+ const expectedBuf = Buffer.from(expected, "hex");
362
+ const actualBuf = Buffer.from(sig, "hex");
363
+ if (expectedBuf.length !== actualBuf.length) return null;
364
+ if (!timingSafeEqual(expectedBuf, actualBuf)) return null;
365
+ return tokenId;
366
+ }
367
+ function auth(options) {
368
+ if (!options.secret || options.secret.length < 32) {
369
+ throw frameworkError(
370
+ "Secret must be at least 32 characters. HMAC security is only as strong as the key.",
371
+ auth,
372
+ "CPEAK_ERR_WEAK_SECRET" /* WEAK_SECRET */
373
+ );
374
+ }
375
+ const {
376
+ secret,
377
+ saveToken,
378
+ findToken,
379
+ revokeToken,
380
+ tokenExpiry = DEFAULTS.tokenExpiry,
381
+ hmacAlgorithm = DEFAULTS.hmacAlgorithm,
382
+ tokenIdSize = DEFAULTS.tokenIdSize
383
+ } = options;
384
+ const pbkdfOpts = {
385
+ iterations: options.iterations,
386
+ keylen: options.keylen,
387
+ digest: options.digest,
388
+ saltSize: options.saltSize
389
+ };
390
+ const _hashPassword = ({ password }) => hashPassword(password, pbkdfOpts);
391
+ const login = async ({
392
+ password,
393
+ hashedPassword,
394
+ userId
395
+ }) => {
396
+ const isMatch = await verifyPassword(password, hashedPassword);
397
+ if (!isMatch) return null;
398
+ const tokenId = randomBytes(tokenIdSize).toString("hex");
399
+ const token = signToken(tokenId, secret, hmacAlgorithm);
400
+ await saveToken(tokenId, userId, new Date(Date.now() + tokenExpiry));
401
+ return token;
402
+ };
403
+ const verifyToken = async (token) => {
404
+ if (!token) return null;
405
+ const tokenId = extractTokenId(token, secret, hmacAlgorithm);
406
+ if (!tokenId) return null;
407
+ const record = await findToken(tokenId);
408
+ if (!record) return null;
409
+ if (new Date(record.expiresAt) < /* @__PURE__ */ new Date()) return null;
410
+ return { userId: record.userId };
411
+ };
412
+ const logout = revokeToken ? async (token) => {
413
+ const tokenId = extractTokenId(token, secret, hmacAlgorithm);
414
+ if (!tokenId) return false;
415
+ await revokeToken(tokenId);
416
+ return true;
417
+ } : void 0;
418
+ return (req, _res, next) => {
419
+ req.hashPassword = _hashPassword;
420
+ req.login = login;
421
+ req.verifyToken = verifyToken;
422
+ if (logout) req.logout = logout;
423
+ next();
424
+ };
425
+ }
426
+
427
+ // lib/utils/cookieParser.ts
428
+ import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
429
+ function sign(value, secret) {
430
+ const sig = createHmac2("sha256", secret).update(value).digest("base64url");
431
+ return `s:${value}.${sig}`;
432
+ }
433
+ function unsign(signed, secret) {
434
+ if (!signed.startsWith("s:")) return false;
435
+ const withoutPrefix = signed.slice(2);
436
+ const lastDot = withoutPrefix.lastIndexOf(".");
437
+ if (lastDot === -1) return false;
438
+ const value = withoutPrefix.slice(0, lastDot);
439
+ const sig = withoutPrefix.slice(lastDot + 1);
440
+ const expected = createHmac2("sha256", secret).update(value).digest("base64url");
441
+ const expectedBuf = Buffer.from(expected);
442
+ const actualBuf = Buffer.from(sig);
443
+ if (expectedBuf.length !== actualBuf.length) return false;
444
+ if (!timingSafeEqual2(expectedBuf, actualBuf)) return false;
445
+ return value;
446
+ }
447
+ function parseRawCookies(header) {
448
+ const cookies = /* @__PURE__ */ Object.create(null);
449
+ if (!header) return cookies;
450
+ const pairs = header.split(";");
451
+ for (let i = 0; i < pairs.length; i++) {
452
+ const pair = pairs[i];
453
+ const equalSignIndex = pair.indexOf("=");
454
+ if (equalSignIndex === -1) continue;
455
+ const key = pair.slice(0, equalSignIndex).trim();
456
+ if (!key || cookies[key] !== void 0) continue;
457
+ let val = pair.slice(equalSignIndex + 1).trim();
458
+ if (val.length > 1 && val[0] === '"' && val[val.length - 1] === '"') {
459
+ val = val.slice(1, -1);
460
+ }
461
+ try {
462
+ cookies[key] = val.indexOf("%") !== -1 ? decodeURIComponent(val) : val;
463
+ } catch (e) {
464
+ cookies[key] = val;
465
+ }
466
+ }
467
+ return cookies;
468
+ }
469
+ function buildSetCookieHeader(name, value, options) {
470
+ const parts = [`${name}=${encodeURIComponent(value)}`];
471
+ const path2 = options.path ?? "/";
472
+ parts.push(`Path=${path2}`);
473
+ if (options.domain) parts.push(`Domain=${options.domain}`);
474
+ if (options.maxAge !== void 0)
475
+ parts.push(`Max-Age=${Math.floor(options.maxAge / 1e3)}`);
476
+ if (options.expires) parts.push(`Expires=${options.expires.toUTCString()}`);
477
+ if (options.httpOnly) parts.push("HttpOnly");
478
+ if (options.secure) parts.push("Secure");
479
+ if (options.sameSite) parts.push(`SameSite=${options.sameSite}`);
480
+ return parts.join("; ");
481
+ }
482
+ function appendSetCookie(res, header) {
483
+ const existing = res.getHeader("Set-Cookie");
484
+ if (!existing) {
485
+ res.setHeader("Set-Cookie", [header]);
486
+ } else if (Array.isArray(existing)) {
487
+ res.setHeader("Set-Cookie", [...existing, header]);
488
+ } else {
489
+ res.setHeader("Set-Cookie", [String(existing), header]);
490
+ }
491
+ }
492
+ function cookieParser(options = {}) {
493
+ const { secret } = options;
494
+ if (secret !== void 0 && secret.length < 32) {
495
+ throw frameworkError(
496
+ "Secret must be at least 32 characters. HMAC security is only as strong as the key.",
497
+ cookieParser,
498
+ "CPEAK_ERR_WEAK_SECRET" /* WEAK_SECRET */
499
+ );
500
+ }
501
+ return (req, res, next) => {
502
+ const rawHeader = req.headers["cookie"] || "";
503
+ const raw = parseRawCookies(rawHeader);
504
+ const cookies = /* @__PURE__ */ Object.create(null);
505
+ const signedCookies = /* @__PURE__ */ Object.create(null);
506
+ for (const [key, val] of Object.entries(raw)) {
507
+ if (val.startsWith("s:") && secret) {
508
+ signedCookies[key] = unsign(val, secret);
509
+ } else {
510
+ cookies[key] = val;
511
+ }
512
+ }
513
+ req.cookies = cookies;
514
+ req.signedCookies = signedCookies;
515
+ res.cookie = (name, value, options2 = {}) => {
516
+ let finalValue = value;
517
+ if (options2.signed) {
518
+ if (!secret)
519
+ throw new Error(
520
+ "cookieParser: secret is required to use signed cookies"
521
+ );
522
+ finalValue = sign(value, secret);
523
+ }
524
+ appendSetCookie(res, buildSetCookieHeader(name, finalValue, options2));
525
+ return res;
526
+ };
527
+ res.clearCookie = (name, options2 = {}) => {
528
+ appendSetCookie(
529
+ res,
530
+ buildSetCookieHeader(name, "", {
531
+ ...options2,
532
+ maxAge: 0,
533
+ expires: /* @__PURE__ */ new Date(0)
534
+ })
535
+ );
536
+ return res;
537
+ };
538
+ next();
539
+ };
540
+ }
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
+
164
605
  // lib/index.ts
165
606
  function frameworkError(message, skipFn, code, status) {
166
607
  const err = new Error(message);
@@ -177,27 +618,32 @@ var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
177
618
  ErrorCode2["SEND_FILE_FAIL"] = "CPEAK_ERR_SEND_FILE_FAIL";
178
619
  ErrorCode2["INVALID_JSON"] = "CPEAK_ERR_INVALID_JSON";
179
620
  ErrorCode2["PAYLOAD_TOO_LARGE"] = "CPEAK_ERR_PAYLOAD_TOO_LARGE";
621
+ ErrorCode2["WEAK_SECRET"] = "CPEAK_ERR_WEAK_SECRET";
622
+ ErrorCode2["COMPRESSION_NOT_ENABLED"] = "CPEAK_ERR_COMPRESSION_NOT_ENABLED";
180
623
  return ErrorCode2;
181
624
  })(ErrorCode || {});
625
+ function compressionConfigFor(res) {
626
+ return res.socket?.server?._cpeakCompression;
627
+ }
182
628
  var CpeakIncomingMessage = class extends http.IncomingMessage {
183
629
  // We define body and params here for better V8 optimization (not changing the shape of the object at runtime)
184
630
  body = void 0;
185
631
  params = {};
186
- _query;
632
+ #query;
187
633
  // Parse the URL parameters (like /users?key1=value1&key2=value2)
188
634
  // We will call this query to be more familiar with other node.js frameworks.
189
635
  // This is a getter method (accessed like a property)
190
636
  get query() {
191
- if (this._query) return this._query;
637
+ if (this.#query) return this.#query;
192
638
  const url = this.url || "";
193
639
  const qIndex = url.indexOf("?");
194
640
  if (qIndex === -1) {
195
- this._query = {};
641
+ this.#query = {};
196
642
  } else {
197
643
  const searchParams = new URLSearchParams(url.substring(qIndex + 1));
198
- this._query = Object.fromEntries(searchParams.entries());
644
+ this.#query = Object.fromEntries(searchParams.entries());
199
645
  }
200
- return this._query;
646
+ return this.#query;
201
647
  }
202
648
  };
203
649
  var CpeakServerResponse = class extends http.ServerResponse {
@@ -219,9 +665,14 @@ var CpeakServerResponse = class extends http.ServerResponse {
219
665
  "CPEAK_ERR_NOT_A_FILE" /* NOT_A_FILE */
220
666
  );
221
667
  }
668
+ const config = compressionConfigFor(this);
669
+ if (config) {
670
+ await compressAndSend(this, mime, createReadStream(path2), config, stat.size);
671
+ return;
672
+ }
222
673
  this.setHeader("Content-Type", mime);
223
674
  this.setHeader("Content-Length", String(stat.size));
224
- await pipeline(createReadStream(path2), this);
675
+ await pipeline2(createReadStream(path2), this);
225
676
  } catch (err) {
226
677
  if (err?.code === "ENOENT") {
227
678
  throw frameworkError(
@@ -242,142 +693,170 @@ var CpeakServerResponse = class extends http.ServerResponse {
242
693
  this.statusCode = code;
243
694
  return this;
244
695
  }
696
+ // Set the Content-Disposition header to prompt the user to download a file
697
+ attachment(filename) {
698
+ const contentDisposition = filename ? `attachment; filename="${filename}"` : "attachment";
699
+ this.setHeader("Content-Disposition", contentDisposition);
700
+ return this;
701
+ }
245
702
  // Redirects to a new URL
246
703
  redirect(location) {
247
704
  this.writeHead(302, { Location: location });
248
705
  this.end();
249
- return this;
250
706
  }
251
- // Send a json data back to the client (for small json data, less than the highWaterMark)
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.
252
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
+ }
253
716
  this.setHeader("Content-Type", "application/json");
254
- this.end(JSON.stringify(data));
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);
255
731
  }
256
732
  };
257
733
  var Cpeak = class {
258
- server;
259
- routes;
260
- middleware;
261
- _handleErr;
262
- constructor() {
263
- this.server = http.createServer({
734
+ #server;
735
+ #routes;
736
+ #middleware;
737
+ #handleErr;
738
+ constructor(options = {}) {
739
+ this.#server = http.createServer({
264
740
  IncomingMessage: CpeakIncomingMessage,
265
741
  ServerResponse: CpeakServerResponse
266
742
  });
267
- this.routes = {};
268
- this.middleware = [];
269
- this.server.on("request", async (req, res) => {
270
- const qIndex = req.url?.indexOf("?");
271
- const urlWithoutQueries = qIndex === -1 ? req.url || "" : req.url?.substring(0, qIndex);
272
- const runHandler = async (req2, res2, middleware, cb, index) => {
273
- if (index === middleware.length) {
274
- try {
275
- await cb(req2, res2, (error) => {
276
- res2.setHeader("Connection", "close");
277
- this._handleErr?.(error, req2, res2);
278
- });
279
- } catch (error) {
280
- res2.setHeader("Connection", "close");
281
- this._handleErr?.(error, req2, res2);
743
+ this.#routes = {};
744
+ this.#middleware = [];
745
+ if (options.compression) {
746
+ this.#server._cpeakCompression = resolveCompressionOptions(
747
+ options.compression
748
+ );
749
+ }
750
+ this.#server.on(
751
+ "request",
752
+ async (req, res) => {
753
+ const qIndex = req.url?.indexOf("?");
754
+ const urlWithoutQueries = qIndex === -1 ? req.url || "" : req.url?.substring(0, qIndex);
755
+ const dispatchError = (error) => {
756
+ if (res.headersSent) {
757
+ req.socket?.destroy();
758
+ return;
282
759
  }
283
- } else {
284
- try {
285
- await middleware[index](
286
- req2,
287
- res2,
288
- // The next function
289
- async (error) => {
290
- if (error) {
291
- res2.setHeader("Connection", "close");
292
- return this._handleErr?.(error, req2, res2);
293
- }
294
- await runHandler(req2, res2, middleware, cb, index + 1);
295
- },
296
- // Error handler for a route middleware
297
- (error) => {
298
- res2.setHeader("Connection", "close");
299
- this._handleErr?.(error, req2, res2);
300
- }
301
- );
302
- } catch (error) {
303
- res2.setHeader("Connection", "close");
304
- this._handleErr?.(error, req2, res2);
760
+ res.setHeader("Connection", "close");
761
+ this.#handleErr?.(error, req, res);
762
+ };
763
+ const runHandler = async (req2, res2, middleware, cb, index) => {
764
+ if (index === middleware.length) {
765
+ try {
766
+ await cb(req2, res2, dispatchError);
767
+ } catch (error) {
768
+ dispatchError(error);
769
+ }
770
+ } else {
771
+ try {
772
+ await middleware[index](
773
+ req2,
774
+ res2,
775
+ // The next function
776
+ async (error) => {
777
+ if (error) {
778
+ return dispatchError(error);
779
+ }
780
+ await runHandler(req2, res2, middleware, cb, index + 1);
781
+ },
782
+ // Error handler for a route middleware
783
+ dispatchError
784
+ );
785
+ } catch (error) {
786
+ dispatchError(error);
787
+ }
305
788
  }
306
- }
307
- };
308
- const runMiddleware = async (req2, res2, middleware, index) => {
309
- if (index === middleware.length) {
310
- const routes = this.routes[req2.method?.toLowerCase() || ""];
311
- if (routes && typeof routes[Symbol.iterator] === "function")
312
- for (const route of routes) {
313
- const match = urlWithoutQueries?.match(route.regex);
314
- if (match) {
315
- const pathVariables = this.#extractPathVariables(
316
- route.path,
317
- match
318
- );
319
- req2.params = pathVariables;
320
- return await runHandler(
321
- req2,
322
- res2,
323
- route.middleware,
324
- route.cb,
325
- 0
326
- );
789
+ };
790
+ const runMiddleware = async (req2, res2, middleware, index) => {
791
+ if (index === middleware.length) {
792
+ const routes = this.#routes[req2.method?.toLowerCase() || ""];
793
+ if (routes && typeof routes[Symbol.iterator] === "function")
794
+ for (const route of routes) {
795
+ const match = urlWithoutQueries?.match(route.regex);
796
+ if (match) {
797
+ const pathVariables = this.#extractPathVariables(
798
+ route.path,
799
+ match
800
+ );
801
+ req2.params = pathVariables;
802
+ return await runHandler(
803
+ req2,
804
+ res2,
805
+ route.middleware,
806
+ route.cb,
807
+ 0
808
+ );
809
+ }
327
810
  }
811
+ return res2.status(404).json({ error: `Cannot ${req2.method} ${urlWithoutQueries}` });
812
+ } else {
813
+ try {
814
+ await middleware[index](req2, res2, async (err) => {
815
+ if (err) {
816
+ return dispatchError(err);
817
+ }
818
+ await runMiddleware(req2, res2, middleware, index + 1);
819
+ });
820
+ } catch (error) {
821
+ dispatchError(error);
328
822
  }
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
823
  }
343
- }
344
- };
345
- await runMiddleware(req, res, this.middleware, 0);
346
- });
824
+ };
825
+ await runMiddleware(req, res, this.#middleware, 0);
826
+ }
827
+ );
347
828
  }
348
829
  route(method, path2, ...args) {
349
- if (!this.routes[method]) this.routes[method] = [];
830
+ if (!this.#routes[method]) this.#routes[method] = [];
350
831
  const cb = args.pop();
351
832
  if (!cb || typeof cb !== "function") {
352
833
  throw new Error("Route definition must include a handler");
353
834
  }
354
835
  const middleware = args.flat();
355
836
  const regex = this.#pathToRegex(path2);
356
- this.routes[method].push({ path: path2, regex, middleware, cb });
837
+ this.#routes[method].push({ path: path2, regex, middleware, cb });
357
838
  }
358
839
  beforeEach(cb) {
359
- this.middleware.push(cb);
840
+ this.#middleware.push(cb);
360
841
  }
361
842
  handleErr(cb) {
362
- this._handleErr = cb;
843
+ this.#handleErr = cb;
363
844
  }
364
845
  listen(port, cb) {
365
- return this.server.listen(port, cb);
846
+ return this.#server.listen(port, cb);
847
+ }
848
+ address() {
849
+ return this.#server.address();
366
850
  }
367
851
  close(cb) {
368
- this.server.close(cb);
852
+ this.#server.close(cb);
369
853
  }
370
854
  // ------------------------------
371
855
  // PRIVATE METHODS:
372
856
  // ------------------------------
373
857
  #pathToRegex(path2) {
374
- const paramNames = [];
375
- const regexString = "^" + path2.replace(/:\w+/g, (match, offset) => {
376
- paramNames.push(match.slice(1));
377
- return "([^/]+)";
378
- }) + "$";
379
- const regex = new RegExp(regexString);
380
- return regex;
858
+ const regexString = "^" + path2.replace(/:\w+/g, "([^/]+)").replace(/\*/g, ".*") + "$";
859
+ return new RegExp(regexString);
381
860
  }
382
861
  #extractPathVariables(path2, match) {
383
862
  const paramNames = (path2.match(/:\w+/g) || []).map(
@@ -390,15 +869,24 @@ var Cpeak = class {
390
869
  return params;
391
870
  }
392
871
  };
393
- function cpeak() {
394
- return new Cpeak();
872
+ function cpeak(options) {
873
+ return new Cpeak(options);
395
874
  }
396
875
  export {
876
+ Cpeak,
877
+ CpeakIncomingMessage,
878
+ CpeakServerResponse,
397
879
  ErrorCode,
880
+ auth,
881
+ cookieParser,
882
+ cors,
398
883
  cpeak as default,
399
884
  frameworkError,
885
+ hashPassword,
400
886
  parseJSON,
401
887
  render,
402
- serveStatic
888
+ serveStatic,
889
+ swagger,
890
+ verifyPassword
403
891
  };
404
892
  //# sourceMappingURL=index.js.map