cpeak 2.6.0 → 2.8.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 +195 -67
- package/dist/index.d.ts +73 -42
- package/dist/index.js +503 -118
- package/dist/index.js.map +1 -1
- package/lib/index.ts +177 -123
- package/lib/internal/compression.ts +180 -0
- package/lib/internal/errors.ts +35 -0
- package/lib/internal/mimeTypes.ts +22 -0
- package/lib/internal/router.ts +259 -0
- package/lib/internal/types.ts +10 -0
- package/lib/types.ts +31 -20
- package/lib/utils/auth.ts +1 -23
- package/lib/utils/cookieParser.ts +1 -11
- package/lib/utils/cors.ts +109 -0
- package/lib/utils/index.ts +3 -4
- package/lib/utils/render.ts +18 -6
- package/lib/utils/serveStatic.ts +29 -28
- package/lib/utils/types.ts +51 -0
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -2,10 +2,318 @@
|
|
|
2
2
|
import http from "http";
|
|
3
3
|
import fs3 from "fs/promises";
|
|
4
4
|
import { createReadStream } from "fs";
|
|
5
|
+
import { pipeline as pipeline2 } from "stream/promises";
|
|
6
|
+
|
|
7
|
+
// lib/internal/compression.ts
|
|
8
|
+
import zlib from "zlib";
|
|
9
|
+
import { Readable } from "stream";
|
|
10
|
+
import { Buffer as Buffer2 } from "buffer";
|
|
5
11
|
import { pipeline } from "stream/promises";
|
|
12
|
+
var COMPRESSIBLE_TYPE = /text|json|javascript|css|xml|svg/i;
|
|
13
|
+
var NO_TRANSFORM = /(?:^|,)\s*no-transform\s*(?:,|$)/i;
|
|
14
|
+
function pickEncoding(header) {
|
|
15
|
+
if (!header) return null;
|
|
16
|
+
const accepted = {};
|
|
17
|
+
let wildcard;
|
|
18
|
+
for (const part of header.split(",")) {
|
|
19
|
+
const [rawName, ...params] = part.trim().split(";");
|
|
20
|
+
const name = rawName.trim().toLowerCase();
|
|
21
|
+
if (!name) continue;
|
|
22
|
+
let q = 1;
|
|
23
|
+
for (const p of params) {
|
|
24
|
+
const m = p.trim().match(/^q=([\d.]+)$/i);
|
|
25
|
+
if (m) q = Number(m[1]);
|
|
26
|
+
}
|
|
27
|
+
if (Number.isNaN(q)) q = 0;
|
|
28
|
+
if (name === "*") wildcard = q;
|
|
29
|
+
else accepted[name] = q;
|
|
30
|
+
}
|
|
31
|
+
const tryPick = (enc) => {
|
|
32
|
+
const q = enc in accepted ? accepted[enc] : wildcard;
|
|
33
|
+
return q !== void 0 && q > 0;
|
|
34
|
+
};
|
|
35
|
+
if (tryPick("br")) return "br";
|
|
36
|
+
if (tryPick("gzip")) return "gzip";
|
|
37
|
+
if (tryPick("deflate")) return "deflate";
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
function appendVary(res, value) {
|
|
41
|
+
const existing = res.getHeader("Vary");
|
|
42
|
+
if (!existing) return res.setHeader("Vary", value);
|
|
43
|
+
const current = String(existing).split(",").map((s) => s.trim()).filter(Boolean);
|
|
44
|
+
if (current.includes("*") || current.some((v) => v.toLowerCase() === value.toLowerCase()))
|
|
45
|
+
return;
|
|
46
|
+
res.setHeader("Vary", [...current, value].join(", "));
|
|
47
|
+
}
|
|
48
|
+
function brotliOptsFor(config) {
|
|
49
|
+
const userBrotli = config.brotli || {};
|
|
50
|
+
return {
|
|
51
|
+
...userBrotli,
|
|
52
|
+
params: {
|
|
53
|
+
[zlib.constants.BROTLI_PARAM_QUALITY]: 4,
|
|
54
|
+
...userBrotli.params || {}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function createCompressorStream(encoding, config) {
|
|
59
|
+
if (encoding === "br")
|
|
60
|
+
return zlib.createBrotliCompress(brotliOptsFor(config));
|
|
61
|
+
if (encoding === "gzip") return zlib.createGzip(config.gzip);
|
|
62
|
+
return zlib.createDeflate(config.deflate);
|
|
63
|
+
}
|
|
64
|
+
function negotiate(res, mime, size, config) {
|
|
65
|
+
if (!COMPRESSIBLE_TYPE.test(mime)) return { encoding: null, eligible: false };
|
|
66
|
+
if (res.req?.method === "HEAD") return { encoding: null, eligible: false };
|
|
67
|
+
const cc = res.getHeader("Cache-Control");
|
|
68
|
+
if (cc && NO_TRANSFORM.test(String(cc)))
|
|
69
|
+
return { encoding: null, eligible: false };
|
|
70
|
+
const existing = res.getHeader("Content-Encoding");
|
|
71
|
+
if (existing && existing !== "identity")
|
|
72
|
+
return { encoding: null, eligible: false };
|
|
73
|
+
if (size < config.threshold) return { encoding: null, eligible: true };
|
|
74
|
+
const encoding = pickEncoding(
|
|
75
|
+
String(res.req?.headers["accept-encoding"] || "")
|
|
76
|
+
);
|
|
77
|
+
return { encoding, eligible: true };
|
|
78
|
+
}
|
|
79
|
+
function bodyAsReadable(body) {
|
|
80
|
+
if (Buffer2.isBuffer(body)) return Readable.from([body]);
|
|
81
|
+
if (typeof body === "string") return Readable.from([Buffer2.from(body)]);
|
|
82
|
+
return body;
|
|
83
|
+
}
|
|
84
|
+
function resolveCompressionOptions(input) {
|
|
85
|
+
const options = input === true ? {} : input;
|
|
86
|
+
return {
|
|
87
|
+
threshold: options.threshold ?? 1024,
|
|
88
|
+
brotli: options.brotli ?? {},
|
|
89
|
+
gzip: options.gzip ?? {},
|
|
90
|
+
deflate: options.deflate ?? {}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
async function compressAndSend(res, mime, body, config, size) {
|
|
94
|
+
res.setHeader("Content-Type", mime);
|
|
95
|
+
const knownSize = Buffer2.isBuffer(body) ? body.length : typeof body === "string" ? Buffer2.byteLength(body) : size ?? Infinity;
|
|
96
|
+
const { encoding, eligible } = negotiate(res, mime, knownSize, config);
|
|
97
|
+
if (!encoding) {
|
|
98
|
+
if (eligible) appendVary(res, "Accept-Encoding");
|
|
99
|
+
if (Buffer2.isBuffer(body) || typeof body === "string") {
|
|
100
|
+
res.setHeader("Content-Length", String(knownSize));
|
|
101
|
+
res.end(body);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (size !== void 0) res.setHeader("Content-Length", String(size));
|
|
105
|
+
await pipeline(body, res);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
res.setHeader("Content-Encoding", encoding);
|
|
109
|
+
appendVary(res, "Accept-Encoding");
|
|
110
|
+
await pipeline(
|
|
111
|
+
bodyAsReadable(body),
|
|
112
|
+
createCompressorStream(encoding, config),
|
|
113
|
+
res
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// lib/internal/mimeTypes.ts
|
|
118
|
+
var MIME_TYPES = {
|
|
119
|
+
html: "text/html",
|
|
120
|
+
css: "text/css",
|
|
121
|
+
js: "application/javascript",
|
|
122
|
+
jpg: "image/jpeg",
|
|
123
|
+
jpeg: "image/jpeg",
|
|
124
|
+
png: "image/png",
|
|
125
|
+
svg: "image/svg+xml",
|
|
126
|
+
txt: "text/plain",
|
|
127
|
+
eot: "application/vnd.ms-fontobject",
|
|
128
|
+
otf: "font/otf",
|
|
129
|
+
ttf: "font/ttf",
|
|
130
|
+
woff: "font/woff",
|
|
131
|
+
woff2: "font/woff2",
|
|
132
|
+
gif: "image/gif",
|
|
133
|
+
ico: "image/x-icon",
|
|
134
|
+
json: "application/json",
|
|
135
|
+
webmanifest: "application/manifest+json"
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// lib/internal/errors.ts
|
|
139
|
+
function frameworkError(message, skipFn, code, status) {
|
|
140
|
+
const err = new Error(message);
|
|
141
|
+
Error.captureStackTrace(err, skipFn);
|
|
142
|
+
err.cpeak_err = true;
|
|
143
|
+
if (code) err.code = code;
|
|
144
|
+
if (status) err.status = status;
|
|
145
|
+
return err;
|
|
146
|
+
}
|
|
147
|
+
var ErrorCode = /* @__PURE__ */ ((ErrorCode2) => {
|
|
148
|
+
ErrorCode2["MISSING_MIME"] = "CPEAK_ERR_MISSING_MIME";
|
|
149
|
+
ErrorCode2["FILE_NOT_FOUND"] = "CPEAK_ERR_FILE_NOT_FOUND";
|
|
150
|
+
ErrorCode2["NOT_A_FILE"] = "CPEAK_ERR_NOT_A_FILE";
|
|
151
|
+
ErrorCode2["SEND_FILE_FAIL"] = "CPEAK_ERR_SEND_FILE_FAIL";
|
|
152
|
+
ErrorCode2["INVALID_JSON"] = "CPEAK_ERR_INVALID_JSON";
|
|
153
|
+
ErrorCode2["PAYLOAD_TOO_LARGE"] = "CPEAK_ERR_PAYLOAD_TOO_LARGE";
|
|
154
|
+
ErrorCode2["WEAK_SECRET"] = "CPEAK_ERR_WEAK_SECRET";
|
|
155
|
+
ErrorCode2["COMPRESSION_NOT_ENABLED"] = "CPEAK_ERR_COMPRESSION_NOT_ENABLED";
|
|
156
|
+
ErrorCode2["DUPLICATE_ROUTE"] = "CPEAK_ERR_DUPLICATE_ROUTE";
|
|
157
|
+
ErrorCode2["INVALID_ROUTE"] = "CPEAK_ERR_INVALID_ROUTE";
|
|
158
|
+
return ErrorCode2;
|
|
159
|
+
})(ErrorCode || {});
|
|
160
|
+
|
|
161
|
+
// lib/internal/router.ts
|
|
162
|
+
function createNode() {
|
|
163
|
+
return { staticChildren: /* @__PURE__ */ new Map() };
|
|
164
|
+
}
|
|
165
|
+
var Router = class {
|
|
166
|
+
#treesByMethod = /* @__PURE__ */ new Map();
|
|
167
|
+
add(method, path2, middleware, handler) {
|
|
168
|
+
const methodKey = method.toLowerCase();
|
|
169
|
+
let root = this.#treesByMethod.get(methodKey);
|
|
170
|
+
if (!root) {
|
|
171
|
+
root = createNode();
|
|
172
|
+
this.#treesByMethod.set(methodKey, root);
|
|
173
|
+
}
|
|
174
|
+
const segments = splitPath(path2);
|
|
175
|
+
const paramNames = [];
|
|
176
|
+
let currentNode = root;
|
|
177
|
+
for (let i = 0; i < segments.length; i++) {
|
|
178
|
+
const segment = segments[i];
|
|
179
|
+
const isLastSegment = i === segments.length - 1;
|
|
180
|
+
if (segment.length > 1 && segment.startsWith("*")) {
|
|
181
|
+
throw frameworkError(
|
|
182
|
+
`Invalid route "${path2}": named wildcards (e.g. "*name") are not supported. Use a plain "*" at the end of the path.`,
|
|
183
|
+
this.add,
|
|
184
|
+
"CPEAK_ERR_INVALID_ROUTE" /* INVALID_ROUTE */
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
if (segment === "*") {
|
|
188
|
+
if (!isLastSegment) {
|
|
189
|
+
throw frameworkError(
|
|
190
|
+
`Invalid route "${path2}": "*" is only allowed as the final path segment.`,
|
|
191
|
+
this.add,
|
|
192
|
+
"CPEAK_ERR_INVALID_ROUTE" /* INVALID_ROUTE */
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
if (currentNode.wildcardChild) {
|
|
196
|
+
throw frameworkError(
|
|
197
|
+
`Duplicate route: ${method.toUpperCase()} ${path2}`,
|
|
198
|
+
this.add,
|
|
199
|
+
"CPEAK_ERR_DUPLICATE_ROUTE" /* DUPLICATE_ROUTE */
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
currentNode.wildcardChild = { handler, middleware, paramNames };
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (segment.startsWith(":")) {
|
|
206
|
+
const paramName = segment.slice(1);
|
|
207
|
+
if (!paramName) {
|
|
208
|
+
throw frameworkError(
|
|
209
|
+
`Invalid route "${path2}": empty parameter name.`,
|
|
210
|
+
this.add,
|
|
211
|
+
"CPEAK_ERR_INVALID_ROUTE" /* INVALID_ROUTE */
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
paramNames.push(paramName);
|
|
215
|
+
if (!currentNode.paramChild) {
|
|
216
|
+
currentNode.paramChild = createNode();
|
|
217
|
+
}
|
|
218
|
+
currentNode = currentNode.paramChild;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
let staticChild = currentNode.staticChildren.get(segment);
|
|
222
|
+
if (!staticChild) {
|
|
223
|
+
staticChild = createNode();
|
|
224
|
+
currentNode.staticChildren.set(segment, staticChild);
|
|
225
|
+
}
|
|
226
|
+
currentNode = staticChild;
|
|
227
|
+
}
|
|
228
|
+
if (currentNode.handler) {
|
|
229
|
+
throw frameworkError(
|
|
230
|
+
`Duplicate route: ${method.toUpperCase()} ${path2}`,
|
|
231
|
+
this.add,
|
|
232
|
+
"CPEAK_ERR_DUPLICATE_ROUTE" /* DUPLICATE_ROUTE */
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
currentNode.handler = handler;
|
|
236
|
+
currentNode.middleware = middleware;
|
|
237
|
+
currentNode.paramNames = paramNames;
|
|
238
|
+
}
|
|
239
|
+
find(method, path2) {
|
|
240
|
+
const root = this.#treesByMethod.get(method.toLowerCase());
|
|
241
|
+
if (!root) return null;
|
|
242
|
+
const segments = splitPath(path2);
|
|
243
|
+
return matchSegments(root, segments, 0, []);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
function matchSegments(node, segments, segmentIndex, capturedValues) {
|
|
247
|
+
if (segmentIndex === segments.length) {
|
|
248
|
+
if (node.handler) {
|
|
249
|
+
return {
|
|
250
|
+
middleware: node.middleware,
|
|
251
|
+
handler: node.handler,
|
|
252
|
+
params: zipParams(node.paramNames, capturedValues)
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
if (node.wildcardChild) {
|
|
256
|
+
return {
|
|
257
|
+
middleware: node.wildcardChild.middleware,
|
|
258
|
+
handler: node.wildcardChild.handler,
|
|
259
|
+
params: zipParams(node.wildcardChild.paramNames, capturedValues)
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
const segment = segments[segmentIndex];
|
|
265
|
+
const staticChild = node.staticChildren.get(segment);
|
|
266
|
+
if (staticChild) {
|
|
267
|
+
const foundMatch = matchSegments(
|
|
268
|
+
staticChild,
|
|
269
|
+
segments,
|
|
270
|
+
segmentIndex + 1,
|
|
271
|
+
capturedValues
|
|
272
|
+
);
|
|
273
|
+
if (foundMatch) return foundMatch;
|
|
274
|
+
}
|
|
275
|
+
if (node.paramChild) {
|
|
276
|
+
capturedValues.push(safeDecode(segment));
|
|
277
|
+
const foundMatch = matchSegments(
|
|
278
|
+
node.paramChild,
|
|
279
|
+
segments,
|
|
280
|
+
segmentIndex + 1,
|
|
281
|
+
capturedValues
|
|
282
|
+
);
|
|
283
|
+
if (foundMatch) return foundMatch;
|
|
284
|
+
capturedValues.pop();
|
|
285
|
+
}
|
|
286
|
+
if (node.wildcardChild) {
|
|
287
|
+
return {
|
|
288
|
+
middleware: node.wildcardChild.middleware,
|
|
289
|
+
handler: node.wildcardChild.handler,
|
|
290
|
+
params: zipParams(node.wildcardChild.paramNames, capturedValues)
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
function zipParams(names, values) {
|
|
296
|
+
const params = {};
|
|
297
|
+
for (let i = 0; i < names.length; i++) {
|
|
298
|
+
params[names[i]] = values[i];
|
|
299
|
+
}
|
|
300
|
+
return params;
|
|
301
|
+
}
|
|
302
|
+
function safeDecode(segment) {
|
|
303
|
+
try {
|
|
304
|
+
return decodeURIComponent(segment);
|
|
305
|
+
} catch {
|
|
306
|
+
return segment;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
function splitPath(path2) {
|
|
310
|
+
if (path2 === "" || path2 === "/") return [];
|
|
311
|
+
const withoutLeadingSlash = path2.startsWith("/") ? path2.slice(1) : path2;
|
|
312
|
+
return withoutLeadingSlash.split("/");
|
|
313
|
+
}
|
|
6
314
|
|
|
7
315
|
// lib/utils/parseJSON.ts
|
|
8
|
-
import { Buffer as
|
|
316
|
+
import { Buffer as Buffer3 } from "buffer";
|
|
9
317
|
function isJSON(contentType) {
|
|
10
318
|
if (!contentType) return false;
|
|
11
319
|
if (contentType === "application/json") return true;
|
|
@@ -38,7 +346,7 @@ var parseJSON = (options = {}) => {
|
|
|
38
346
|
};
|
|
39
347
|
const onEnd = () => {
|
|
40
348
|
try {
|
|
41
|
-
const rawBody = chunks.length === 1 ? chunks[0].toString("utf-8") :
|
|
349
|
+
const rawBody = chunks.length === 1 ? chunks[0].toString("utf-8") : Buffer3.concat(chunks).toString("utf-8");
|
|
42
350
|
req.body = rawBody ? JSON.parse(rawBody) : {};
|
|
43
351
|
next();
|
|
44
352
|
} catch (err) {
|
|
@@ -61,30 +369,25 @@ var parseJSON = (options = {}) => {
|
|
|
61
369
|
// lib/utils/serveStatic.ts
|
|
62
370
|
import fs from "fs";
|
|
63
371
|
import path from "path";
|
|
64
|
-
var
|
|
65
|
-
html: "text/html",
|
|
66
|
-
css: "text/css",
|
|
67
|
-
js: "application/javascript",
|
|
68
|
-
jpg: "image/jpeg",
|
|
69
|
-
jpeg: "image/jpeg",
|
|
70
|
-
png: "image/png",
|
|
71
|
-
svg: "image/svg+xml",
|
|
72
|
-
txt: "text/plain",
|
|
73
|
-
eot: "application/vnd.ms-fontobject",
|
|
74
|
-
otf: "font/otf",
|
|
75
|
-
ttf: "font/ttf",
|
|
76
|
-
woff: "font/woff",
|
|
77
|
-
woff2: "font/woff2",
|
|
78
|
-
gif: "image/gif",
|
|
79
|
-
ico: "image/x-icon",
|
|
80
|
-
json: "application/json",
|
|
81
|
-
webmanifest: "application/manifest+json"
|
|
82
|
-
};
|
|
83
|
-
var serveStatic = (folderPath, newMimeTypes, options) => {
|
|
84
|
-
if (newMimeTypes) {
|
|
85
|
-
Object.assign(MIME_TYPES, newMimeTypes);
|
|
86
|
-
}
|
|
372
|
+
var serveStatic = (folderPath, options) => {
|
|
87
373
|
const prefix = options?.prefix ?? "";
|
|
374
|
+
const live = options?.live ?? false;
|
|
375
|
+
if (live) {
|
|
376
|
+
const resolvedFolder = path.resolve(folderPath);
|
|
377
|
+
return async function(req, res, next) {
|
|
378
|
+
const url = req.url;
|
|
379
|
+
if (typeof url !== "string") return next();
|
|
380
|
+
const pathname = url.split("?")[0];
|
|
381
|
+
const unprefixed = prefix ? pathname.slice(prefix.length) : pathname;
|
|
382
|
+
const filePath = path.join(resolvedFolder, unprefixed);
|
|
383
|
+
const fileExtension = path.extname(filePath).slice(1);
|
|
384
|
+
const mime = MIME_TYPES[fileExtension];
|
|
385
|
+
if (!mime || !filePath.startsWith(resolvedFolder)) return next();
|
|
386
|
+
const stat = await fs.promises.stat(filePath).catch(() => null);
|
|
387
|
+
if (stat?.isFile()) return res.sendFile(filePath, mime);
|
|
388
|
+
next();
|
|
389
|
+
};
|
|
390
|
+
}
|
|
88
391
|
function processFolder(folderPath2, parentFolder) {
|
|
89
392
|
const staticFiles = [];
|
|
90
393
|
const files = fs.readdirSync(folderPath2);
|
|
@@ -153,13 +456,22 @@ var render = () => {
|
|
|
153
456
|
return function(req, res, next) {
|
|
154
457
|
res.render = async (path2, data, mime) => {
|
|
155
458
|
if (!mime) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
)
|
|
459
|
+
const dotIndex = path2.lastIndexOf(".");
|
|
460
|
+
const fileExtension = dotIndex >= 0 ? path2.slice(dotIndex + 1) : "";
|
|
461
|
+
mime = MIME_TYPES[fileExtension];
|
|
462
|
+
if (!mime) {
|
|
463
|
+
throw frameworkError(
|
|
464
|
+
`MIME type is missing for "${path2}". Pass it as the third argument or register the extension via cpeak({ mimeTypes: { ${fileExtension || "ext"}: "..." } }).`,
|
|
465
|
+
res.render
|
|
466
|
+
);
|
|
467
|
+
}
|
|
160
468
|
}
|
|
161
469
|
let fileStr = await fs2.readFile(path2, "utf-8");
|
|
162
470
|
const finalStr = renderTemplate(fileStr, data);
|
|
471
|
+
if (res._compression) {
|
|
472
|
+
await compressAndSend(res, mime, finalStr, res._compression);
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
163
475
|
res.setHeader("Content-Type", mime);
|
|
164
476
|
res.end(finalStr);
|
|
165
477
|
};
|
|
@@ -424,25 +736,70 @@ function cookieParser(options = {}) {
|
|
|
424
736
|
};
|
|
425
737
|
}
|
|
426
738
|
|
|
427
|
-
// lib/
|
|
428
|
-
function
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
if (
|
|
433
|
-
|
|
434
|
-
return err;
|
|
739
|
+
// lib/utils/cors.ts
|
|
740
|
+
function appendVary2(res, value) {
|
|
741
|
+
const existing = res.getHeader("Vary");
|
|
742
|
+
if (!existing) return res.setHeader("Vary", value);
|
|
743
|
+
const current = String(existing).split(",").map((s) => s.trim()).filter(Boolean);
|
|
744
|
+
if (current.includes("*") || current.includes(value)) return;
|
|
745
|
+
res.setHeader("Vary", [...current, value].join(", "));
|
|
435
746
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
747
|
+
async function isAllowed(origin, rule) {
|
|
748
|
+
if (rule === true || rule === "*") return true;
|
|
749
|
+
if (rule === false || !origin) return false;
|
|
750
|
+
if (typeof rule === "string") return rule === origin;
|
|
751
|
+
if (Array.isArray(rule)) return rule.includes(origin);
|
|
752
|
+
if (rule instanceof RegExp) return rule.test(origin);
|
|
753
|
+
if (typeof rule === "function") return await rule(origin);
|
|
754
|
+
return false;
|
|
755
|
+
}
|
|
756
|
+
var cors = (options = {}) => {
|
|
757
|
+
const {
|
|
758
|
+
origin = "*",
|
|
759
|
+
methods = "GET,HEAD,PUT,PATCH,POST,DELETE",
|
|
760
|
+
allowedHeaders,
|
|
761
|
+
exposedHeaders,
|
|
762
|
+
credentials = false,
|
|
763
|
+
maxAge = 86400,
|
|
764
|
+
preflightContinue = false,
|
|
765
|
+
optionsSuccessStatus = 204
|
|
766
|
+
} = options;
|
|
767
|
+
const methodsStr = Array.isArray(methods) ? methods.join(",") : methods;
|
|
768
|
+
const allowedHeadersStr = Array.isArray(allowedHeaders) ? allowedHeaders.join(",") : allowedHeaders;
|
|
769
|
+
const exposedHeadersStr = Array.isArray(exposedHeaders) ? exposedHeaders.join(",") : exposedHeaders;
|
|
770
|
+
return async (req, res, next) => {
|
|
771
|
+
const requestOrigin = req.headers.origin;
|
|
772
|
+
if (!requestOrigin) return next();
|
|
773
|
+
const allowed = await isAllowed(requestOrigin, origin);
|
|
774
|
+
if (!allowed) return next();
|
|
775
|
+
const allowOriginValue = origin === "*" && !credentials ? "*" : requestOrigin;
|
|
776
|
+
res.setHeader("Access-Control-Allow-Origin", allowOriginValue);
|
|
777
|
+
if (allowOriginValue !== "*") appendVary2(res, "Origin");
|
|
778
|
+
if (credentials) res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
779
|
+
if (exposedHeadersStr)
|
|
780
|
+
res.setHeader("Access-Control-Expose-Headers", exposedHeadersStr);
|
|
781
|
+
const isPreflight = req.method === "OPTIONS" && req.headers["access-control-request-method"] !== void 0;
|
|
782
|
+
if (!isPreflight) return next();
|
|
783
|
+
res.setHeader("Access-Control-Allow-Methods", methodsStr);
|
|
784
|
+
if (allowedHeadersStr) {
|
|
785
|
+
res.setHeader("Access-Control-Allow-Headers", allowedHeadersStr);
|
|
786
|
+
} else if (origin === "*") {
|
|
787
|
+
const requested = req.headers["access-control-request-headers"];
|
|
788
|
+
if (requested) res.setHeader("Access-Control-Allow-Headers", requested);
|
|
789
|
+
} else {
|
|
790
|
+
res.setHeader(
|
|
791
|
+
"Access-Control-Allow-Headers",
|
|
792
|
+
"Content-Type, Authorization"
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
res.setHeader("Access-Control-Max-Age", String(maxAge));
|
|
796
|
+
if (preflightContinue) return next();
|
|
797
|
+
res.statusCode = optionsSuccessStatus;
|
|
798
|
+
res.end();
|
|
799
|
+
};
|
|
800
|
+
};
|
|
801
|
+
|
|
802
|
+
// lib/index.ts
|
|
446
803
|
var CpeakIncomingMessage = class extends http.IncomingMessage {
|
|
447
804
|
// We define body and params here for better V8 optimization (not changing the shape of the object at runtime)
|
|
448
805
|
body = void 0;
|
|
@@ -465,14 +822,21 @@ var CpeakIncomingMessage = class extends http.IncomingMessage {
|
|
|
465
822
|
}
|
|
466
823
|
};
|
|
467
824
|
var CpeakServerResponse = class extends http.ServerResponse {
|
|
825
|
+
// Set per-request from the Cpeak instance. Undefined when compression isn't enabled.
|
|
826
|
+
_compression;
|
|
468
827
|
// Send a file back to the client
|
|
469
828
|
async sendFile(path2, mime) {
|
|
470
829
|
if (!mime) {
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
830
|
+
const dotIndex = path2.lastIndexOf(".");
|
|
831
|
+
const fileExtension = dotIndex >= 0 ? path2.slice(dotIndex + 1) : "";
|
|
832
|
+
mime = MIME_TYPES[fileExtension];
|
|
833
|
+
if (!mime) {
|
|
834
|
+
throw frameworkError(
|
|
835
|
+
`MIME type is missing for "${path2}". Pass it as the second argument or register the extension via cpeak({ mimeTypes: { ${fileExtension || "ext"}: "..." } }).`,
|
|
836
|
+
this.sendFile,
|
|
837
|
+
"CPEAK_ERR_MISSING_MIME" /* MISSING_MIME */
|
|
838
|
+
);
|
|
839
|
+
}
|
|
476
840
|
}
|
|
477
841
|
try {
|
|
478
842
|
const stat = await fs3.stat(path2);
|
|
@@ -483,9 +847,19 @@ var CpeakServerResponse = class extends http.ServerResponse {
|
|
|
483
847
|
"CPEAK_ERR_NOT_A_FILE" /* NOT_A_FILE */
|
|
484
848
|
);
|
|
485
849
|
}
|
|
850
|
+
if (this._compression) {
|
|
851
|
+
await compressAndSend(
|
|
852
|
+
this,
|
|
853
|
+
mime,
|
|
854
|
+
createReadStream(path2),
|
|
855
|
+
this._compression,
|
|
856
|
+
stat.size
|
|
857
|
+
);
|
|
858
|
+
return;
|
|
859
|
+
}
|
|
486
860
|
this.setHeader("Content-Type", mime);
|
|
487
861
|
this.setHeader("Content-Length", String(stat.size));
|
|
488
|
-
await
|
|
862
|
+
await pipeline2(createReadStream(path2), this);
|
|
489
863
|
} catch (err) {
|
|
490
864
|
if (err?.code === "ENOENT") {
|
|
491
865
|
throw frameworkError(
|
|
@@ -517,59 +891,91 @@ var CpeakServerResponse = class extends http.ServerResponse {
|
|
|
517
891
|
this.writeHead(302, { Location: location });
|
|
518
892
|
this.end();
|
|
519
893
|
}
|
|
520
|
-
// Send a json data back to the client
|
|
894
|
+
// Send a json data back to the client.
|
|
895
|
+
// This is only good for bodies that their size is less than the highWaterMark value.
|
|
521
896
|
json(data) {
|
|
897
|
+
const body = JSON.stringify(data);
|
|
898
|
+
if (this._compression) {
|
|
899
|
+
return compressAndSend(this, "application/json", body, this._compression);
|
|
900
|
+
}
|
|
522
901
|
this.setHeader("Content-Type", "application/json");
|
|
523
|
-
this.end(
|
|
902
|
+
this.end(body);
|
|
903
|
+
return Promise.resolve();
|
|
904
|
+
}
|
|
905
|
+
// Explicit compression entry point. A developer can use this in any custom handler to compress arbitrary responses
|
|
906
|
+
compress(mime, body, size) {
|
|
907
|
+
if (!this._compression) {
|
|
908
|
+
throw frameworkError(
|
|
909
|
+
"compression is not enabled. Pass `compression` to cpeak({ compression: true | { ... } }) to use res.compress.",
|
|
910
|
+
this.compress,
|
|
911
|
+
"CPEAK_ERR_COMPRESSION_NOT_ENABLED" /* COMPRESSION_NOT_ENABLED */
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
return compressAndSend(this, mime, body, this._compression, size);
|
|
524
915
|
}
|
|
525
916
|
};
|
|
526
917
|
var Cpeak = class {
|
|
527
918
|
#server;
|
|
528
|
-
#
|
|
919
|
+
#router;
|
|
529
920
|
#middleware;
|
|
530
921
|
#handleErr;
|
|
531
|
-
|
|
922
|
+
#compression;
|
|
923
|
+
constructor(options = {}) {
|
|
532
924
|
this.#server = http.createServer({
|
|
533
925
|
IncomingMessage: CpeakIncomingMessage,
|
|
534
926
|
ServerResponse: CpeakServerResponse
|
|
535
927
|
});
|
|
536
|
-
this.#
|
|
928
|
+
this.#router = new Router();
|
|
537
929
|
this.#middleware = [];
|
|
930
|
+
if (options.compression) {
|
|
931
|
+
this.#compression = resolveCompressionOptions(options.compression);
|
|
932
|
+
}
|
|
933
|
+
if (options.mimeTypes) Object.assign(MIME_TYPES, options.mimeTypes);
|
|
538
934
|
this.#server.on(
|
|
539
935
|
"request",
|
|
540
936
|
async (req, res) => {
|
|
937
|
+
res._compression = this.#compression;
|
|
541
938
|
const qIndex = req.url?.indexOf("?");
|
|
542
939
|
const urlWithoutQueries = qIndex === -1 ? req.url || "" : req.url?.substring(0, qIndex);
|
|
543
|
-
const dispatchError = (error) => {
|
|
940
|
+
const dispatchError = async (error) => {
|
|
544
941
|
if (res.headersSent) {
|
|
545
942
|
req.socket?.destroy();
|
|
546
943
|
return;
|
|
547
944
|
}
|
|
548
945
|
res.setHeader("Connection", "close");
|
|
549
|
-
|
|
946
|
+
try {
|
|
947
|
+
await this.#handleErr?.(error, req, res);
|
|
948
|
+
} catch (handlerFailure) {
|
|
949
|
+
console.error(
|
|
950
|
+
"[cpeak] handleErr failed while processing:",
|
|
951
|
+
error,
|
|
952
|
+
"\nReason:",
|
|
953
|
+
handlerFailure
|
|
954
|
+
);
|
|
955
|
+
if (!res.headersSent) {
|
|
956
|
+
try {
|
|
957
|
+
res.statusCode = 500;
|
|
958
|
+
res.end();
|
|
959
|
+
} catch {
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
550
963
|
};
|
|
551
964
|
const runHandler = async (req2, res2, middleware, cb, index) => {
|
|
552
965
|
if (index === middleware.length) {
|
|
553
966
|
try {
|
|
554
|
-
await cb(req2, res2
|
|
967
|
+
await cb(req2, res2);
|
|
555
968
|
} catch (error) {
|
|
556
969
|
dispatchError(error);
|
|
557
970
|
}
|
|
558
971
|
} else {
|
|
559
972
|
try {
|
|
560
|
-
await middleware[index](
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
);
|
|
973
|
+
await middleware[index](req2, res2, async (error) => {
|
|
974
|
+
if (error) {
|
|
975
|
+
return dispatchError(error);
|
|
976
|
+
}
|
|
977
|
+
await runHandler(req2, res2, middleware, cb, index + 1);
|
|
978
|
+
});
|
|
573
979
|
} catch (error) {
|
|
574
980
|
dispatchError(error);
|
|
575
981
|
}
|
|
@@ -577,25 +983,18 @@ var Cpeak = class {
|
|
|
577
983
|
};
|
|
578
984
|
const runMiddleware = async (req2, res2, middleware, index) => {
|
|
579
985
|
if (index === middleware.length) {
|
|
580
|
-
const
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
res2,
|
|
593
|
-
route.middleware,
|
|
594
|
-
route.cb,
|
|
595
|
-
0
|
|
596
|
-
);
|
|
597
|
-
}
|
|
598
|
-
}
|
|
986
|
+
const method = req2.method?.toLowerCase() || "";
|
|
987
|
+
const found = this.#router.find(method, urlWithoutQueries || "");
|
|
988
|
+
if (found) {
|
|
989
|
+
req2.params = found.params;
|
|
990
|
+
return await runHandler(
|
|
991
|
+
req2,
|
|
992
|
+
res2,
|
|
993
|
+
found.middleware,
|
|
994
|
+
found.handler,
|
|
995
|
+
0
|
|
996
|
+
);
|
|
997
|
+
}
|
|
599
998
|
return res2.status(404).json({ error: `Cannot ${req2.method} ${urlWithoutQueries}` });
|
|
600
999
|
} else {
|
|
601
1000
|
try {
|
|
@@ -615,14 +1014,12 @@ var Cpeak = class {
|
|
|
615
1014
|
);
|
|
616
1015
|
}
|
|
617
1016
|
route(method, path2, ...args) {
|
|
618
|
-
if (!this.#routes[method]) this.#routes[method] = [];
|
|
619
1017
|
const cb = args.pop();
|
|
620
1018
|
if (!cb || typeof cb !== "function") {
|
|
621
1019
|
throw new Error("Route definition must include a handler");
|
|
622
1020
|
}
|
|
623
1021
|
const middleware = args.flat();
|
|
624
|
-
|
|
625
|
-
this.#routes[method].push({ path: path2, regex, middleware, cb });
|
|
1022
|
+
this.#router.add(method, path2, middleware, cb);
|
|
626
1023
|
}
|
|
627
1024
|
beforeEach(cb) {
|
|
628
1025
|
this.#middleware.push(cb);
|
|
@@ -630,35 +1027,22 @@ var Cpeak = class {
|
|
|
630
1027
|
handleErr(cb) {
|
|
631
1028
|
this.#handleErr = cb;
|
|
632
1029
|
}
|
|
633
|
-
listen(
|
|
634
|
-
return this.#server.listen(
|
|
1030
|
+
listen(...args) {
|
|
1031
|
+
return this.#server.listen(...args);
|
|
635
1032
|
}
|
|
636
1033
|
address() {
|
|
637
1034
|
return this.#server.address();
|
|
638
1035
|
}
|
|
639
1036
|
close(cb) {
|
|
640
|
-
this.#server.close(cb);
|
|
641
|
-
}
|
|
642
|
-
//
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
#pathToRegex(path2) {
|
|
646
|
-
const regexString = "^" + path2.replace(/:\w+/g, "([^/]+)").replace(/\*/g, ".*") + "$";
|
|
647
|
-
return new RegExp(regexString);
|
|
648
|
-
}
|
|
649
|
-
#extractPathVariables(path2, match) {
|
|
650
|
-
const paramNames = (path2.match(/:\w+/g) || []).map(
|
|
651
|
-
(param) => param.slice(1)
|
|
652
|
-
);
|
|
653
|
-
const params = {};
|
|
654
|
-
paramNames.forEach((name, index) => {
|
|
655
|
-
params[name] = match[index + 1];
|
|
656
|
-
});
|
|
657
|
-
return params;
|
|
1037
|
+
return this.#server.close(cb);
|
|
1038
|
+
}
|
|
1039
|
+
// A getter for developers who want to access the underlying http server instance for advanced use cases that aren't covered by Cpeak
|
|
1040
|
+
get server() {
|
|
1041
|
+
return this.#server;
|
|
658
1042
|
}
|
|
659
1043
|
};
|
|
660
|
-
function cpeak() {
|
|
661
|
-
return new Cpeak();
|
|
1044
|
+
function cpeak(options) {
|
|
1045
|
+
return new Cpeak(options);
|
|
662
1046
|
}
|
|
663
1047
|
export {
|
|
664
1048
|
Cpeak,
|
|
@@ -667,6 +1051,7 @@ export {
|
|
|
667
1051
|
ErrorCode,
|
|
668
1052
|
auth,
|
|
669
1053
|
cookieParser,
|
|
1054
|
+
cors,
|
|
670
1055
|
cpeak as default,
|
|
671
1056
|
frameworkError,
|
|
672
1057
|
hashPassword,
|