expediate 1.0.5 → 1.0.6
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/CHANGELOG.md +138 -0
- package/CONTRIBUTING.md +150 -0
- package/README.md +278 -779
- package/dist/apis.d.ts +372 -12
- package/dist/apis.d.ts.map +1 -1
- package/dist/apis.js +483 -65
- package/dist/apis.js.map +1 -1
- package/dist/cjs/index.js +2290 -807
- package/dist/git.d.ts +1 -1
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +5 -5
- package/dist/git.js.map +1 -1
- package/dist/http-objects.d.ts +26 -0
- package/dist/http-objects.d.ts.map +1 -0
- package/dist/http-objects.js +588 -0
- package/dist/http-objects.js.map +1 -0
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/jwt-auth.d.ts +11 -0
- package/dist/jwt-auth.d.ts.map +1 -1
- package/dist/jwt-auth.js +9 -9
- package/dist/jwt-auth.js.map +1 -1
- package/dist/middleware.js +2 -2
- package/dist/middleware.js.map +1 -1
- package/dist/mimetypes.json +882 -1
- package/dist/misc.d.ts +161 -25
- package/dist/misc.d.ts.map +1 -1
- package/dist/misc.js +228 -80
- package/dist/misc.js.map +1 -1
- package/dist/openapi.d.ts +156 -13
- package/dist/openapi.d.ts.map +1 -1
- package/dist/openapi.js +214 -71
- package/dist/openapi.js.map +1 -1
- package/dist/router-types.d.ts +760 -0
- package/dist/router-types.d.ts.map +1 -0
- package/dist/router-types.js +23 -0
- package/dist/router-types.js.map +1 -0
- package/dist/router.d.ts +7 -530
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +128 -375
- package/dist/router.js.map +1 -1
- package/dist/static.d.ts +2 -2
- package/dist/static.d.ts.map +1 -1
- package/dist/static.js +77 -22
- package/dist/static.js.map +1 -1
- package/docs/THREAT_MODEL.md +52 -0
- package/docs/api-builder-v2-design.md +644 -0
- package/docs/api-builder-v3-design.md +397 -0
- package/docs/api-builder.md +454 -0
- package/docs/benchmark.md +27 -0
- package/docs/body-parsing.md +223 -0
- package/docs/errors.md +359 -0
- package/docs/expediate.png +0 -0
- package/docs/git.md +139 -0
- package/docs/jwt-auth.md +251 -0
- package/docs/logo.svg +12 -0
- package/docs/middleware.md +264 -0
- package/docs/openapi.md +180 -0
- package/docs/router.md +356 -0
- package/docs/static.md +128 -0
- package/docs/wiki.json +123 -0
- package/package.json +30 -8
- package/dist/cjs/apis.js +0 -327
- package/dist/cjs/git.js +0 -293
- package/dist/cjs/jwt-auth.js +0 -532
- package/dist/cjs/middleware.js +0 -511
- package/dist/cjs/mimetypes.json +0 -1
- package/dist/cjs/misc.js +0 -787
- package/dist/cjs/openapi.js +0 -485
- package/dist/cjs/router.js +0 -898
- package/dist/cjs/static.js +0 -669
package/dist/cjs/misc.js
DELETED
|
@@ -1,787 +0,0 @@
|
|
|
1
|
-
/* Copyright 2021 Fabien Bavent
|
|
2
|
-
*
|
|
3
|
-
* Permission is hereby granted, free of charge, to any person obtaining a
|
|
4
|
-
* copy of this software and associated documentation files (the "Software"),
|
|
5
|
-
* to deal in the Software without restriction, including without limitation
|
|
6
|
-
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
7
|
-
* and/or sell copies of the Software, and to permit persons to whom the
|
|
8
|
-
* Software is furnished to do so, subject to the following conditions:
|
|
9
|
-
*
|
|
10
|
-
* The above copyright notice and this permission notice shall be included
|
|
11
|
-
* in all copies or substantial portions of the Software.
|
|
12
|
-
*
|
|
13
|
-
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
14
|
-
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
-
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
16
|
-
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
-
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
18
|
-
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
19
|
-
* DEALINGS IN THE SOFTWARE.
|
|
20
|
-
*/
|
|
21
|
-
'use strict';
|
|
22
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
-
exports.extractCharset = extractCharset;
|
|
24
|
-
exports.readReqBody = readReqBody;
|
|
25
|
-
exports.parseMultipartBody = parseMultipartBody;
|
|
26
|
-
exports.json = json;
|
|
27
|
-
exports.formData = formData;
|
|
28
|
-
exports.formEncoded = formEncoded;
|
|
29
|
-
exports.parseBody = parseBody;
|
|
30
|
-
exports.streamFormData = streamFormData;
|
|
31
|
-
exports.logger = logger;
|
|
32
|
-
exports.cors = cors;
|
|
33
|
-
const stream_1 = require("stream");
|
|
34
|
-
const zlib_1 = require("zlib");
|
|
35
|
-
// ---------------------------------------------------------------------------
|
|
36
|
-
// Decompression algorithm registry
|
|
37
|
-
// ---------------------------------------------------------------------------
|
|
38
|
-
/**
|
|
39
|
-
* Maps a supported `Content-Encoding` value to its corresponding `zlib`
|
|
40
|
-
* decompression function. Only `gzip` and `deflate` are supported.
|
|
41
|
-
*/
|
|
42
|
-
const DECOMPRESS_ALGO = {
|
|
43
|
-
gzip: zlib_1.default.gunzip,
|
|
44
|
-
deflate: zlib_1.default.inflate,
|
|
45
|
-
};
|
|
46
|
-
// ---------------------------------------------------------------------------
|
|
47
|
-
// Internal utilities
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
/**
|
|
50
|
-
* Parse a human-readable byte-size string into a number of bytes.
|
|
51
|
-
*
|
|
52
|
-
* Supported suffixes (case-insensitive): `b`, `kb`, `mb`, `gb`.
|
|
53
|
-
* The suffix is optional; a bare number is treated as bytes.
|
|
54
|
-
*
|
|
55
|
-
* @param value - The size string to parse (e.g. `'100kb'`, `'2.5mb'`).
|
|
56
|
-
* @returns The size in bytes, or `0` if the input cannot be parsed.
|
|
57
|
-
*
|
|
58
|
-
* @example
|
|
59
|
-
* ```ts
|
|
60
|
-
* readSize('100kb') // 102400
|
|
61
|
-
* readSize('2mb') // 2097152
|
|
62
|
-
* readSize('1024') // 1024
|
|
63
|
-
* readSize('bad') // 0
|
|
64
|
-
* ```
|
|
65
|
-
*/
|
|
66
|
-
function readSize(value) {
|
|
67
|
-
if (typeof value === 'number')
|
|
68
|
-
return value;
|
|
69
|
-
const fmt = /^(\d+(\.\d+)?)([kmg]?b?)?$/i.exec(value);
|
|
70
|
-
if (!fmt)
|
|
71
|
-
return 0;
|
|
72
|
-
const num = parseFloat(fmt[1] ?? '0');
|
|
73
|
-
const sfx = (fmt[3] ?? 'b').toLowerCase();
|
|
74
|
-
if (sfx[0] === 'k')
|
|
75
|
-
return num * 1024;
|
|
76
|
-
if (sfx[0] === 'm')
|
|
77
|
-
return num * 1024 * 1024;
|
|
78
|
-
if (sfx[0] === 'g')
|
|
79
|
-
return num * 1024 * 1024 * 1024;
|
|
80
|
-
return num;
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Split a `Buffer` on every occurrence of a `delimiter` buffer, returning an
|
|
84
|
-
* array of sub-buffers between delimiters (the delimiters themselves are not
|
|
85
|
-
* included in the output).
|
|
86
|
-
*
|
|
87
|
-
* Behaves like `String.prototype.split` but operates on raw binary data,
|
|
88
|
-
* making it safe for multipart bodies that may contain arbitrary byte
|
|
89
|
-
* sequences.
|
|
90
|
-
*
|
|
91
|
-
* @param buffer - The source buffer to split.
|
|
92
|
-
* @param delimiter - The byte sequence to split on.
|
|
93
|
-
* @returns An array of buffer slices; always contains at least one element.
|
|
94
|
-
*/
|
|
95
|
-
function splitBuffer(buffer, delimiter) {
|
|
96
|
-
const result = [];
|
|
97
|
-
let start = 0;
|
|
98
|
-
let index;
|
|
99
|
-
while ((index = buffer.indexOf(delimiter, start)) !== -1) {
|
|
100
|
-
result.push(buffer.slice(start, index));
|
|
101
|
-
start = index + delimiter.length;
|
|
102
|
-
}
|
|
103
|
-
result.push(buffer.slice(start));
|
|
104
|
-
return result;
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Extract the `charset` parameter from a `Content-Type` header value.
|
|
108
|
-
*
|
|
109
|
-
* Handles optional whitespace around the semicolon-separated parameters.
|
|
110
|
-
* Falls back to `'utf8'` when no `charset` parameter is found.
|
|
111
|
-
*
|
|
112
|
-
* @param contentType - The raw `Content-Type` header value
|
|
113
|
-
* (e.g. `'text/plain; charset=iso-8859-1'`).
|
|
114
|
-
* @returns A Node.js-compatible encoding name (e.g. `'utf8'`, `'iso-8859-1'`).
|
|
115
|
-
*/
|
|
116
|
-
function extractCharset(contentType) {
|
|
117
|
-
const param = contentType
|
|
118
|
-
.split(';')
|
|
119
|
-
.map((s) => s.replace(/^\s+|\s+$/g, ''))
|
|
120
|
-
.find((s) => s.startsWith('charset='));
|
|
121
|
-
return param ? param.substring('charset='.length) : 'utf8';
|
|
122
|
-
}
|
|
123
|
-
// ---------------------------------------------------------------------------
|
|
124
|
-
// Body collection
|
|
125
|
-
// ---------------------------------------------------------------------------
|
|
126
|
-
/**
|
|
127
|
-
* Collect the full request body into a single `Buffer`, enforce size limits,
|
|
128
|
-
* optionally decompress, and validate the `Content-Type` against an expected
|
|
129
|
-
* MIME type.
|
|
130
|
-
*
|
|
131
|
-
* When the body is successfully collected and validated, `callback` is invoked
|
|
132
|
-
* with the raw `Content-Type` header value and the (possibly decompressed)
|
|
133
|
-
* body buffer. In all error cases the appropriate HTTP error response is sent
|
|
134
|
-
* and `callback` is never called.
|
|
135
|
-
*
|
|
136
|
-
* Callers that need to pass control to the next middleware when there is no
|
|
137
|
-
* body should rely on the `next()` call that this function makes when
|
|
138
|
-
* `Content-Length` is `0` or absent and `Transfer-Encoding` is not `chunked`.
|
|
139
|
-
*
|
|
140
|
-
* @param req - The incoming request.
|
|
141
|
-
* @param res - The outgoing response.
|
|
142
|
-
* @param opts - Resolved body-parsing options.
|
|
143
|
-
* @param mimetype - Expected MIME type (e.g. `'application/json'`), or `null`
|
|
144
|
-
* to accept any content type.
|
|
145
|
-
* @param next - The next middleware callback; called when the body is
|
|
146
|
-
* empty or absent.
|
|
147
|
-
* @param callback - Invoked with `(contentType, body)` on success.
|
|
148
|
-
*/
|
|
149
|
-
function readBody(req, res, opts, mimetype, next, callback) {
|
|
150
|
-
const length = parseInt(req.headers['content-length'] ?? '0', 10);
|
|
151
|
-
// Detect chunked transfer encoding — no Content-Length is present in this case.
|
|
152
|
-
const isChunked = req.headers['transfer-encoding']
|
|
153
|
-
?.split(',').map((v) => v.trim()).some((v) => v.toLowerCase() === 'chunked') ?? false;
|
|
154
|
-
// No body declared and not chunked — skip to next middleware.
|
|
155
|
-
if (!isChunked && (!length || length === 0))
|
|
156
|
-
return next();
|
|
157
|
-
const maxLength = readSize(opts.limit) || 102_400;
|
|
158
|
-
// For requests with a known Content-Length we can reject upfront.
|
|
159
|
-
if (!isChunked && length > maxLength)
|
|
160
|
-
return void res.status(413).send('Content Too Large');
|
|
161
|
-
// Compression handling.
|
|
162
|
-
const encoding = req.headers['content-encoding'];
|
|
163
|
-
if (encoding && (opts.inflate === false || !DECOMPRESS_ALGO[encoding]))
|
|
164
|
-
return void res.status(415).send('Unsupported Media Type: Wrong Content-Encoding');
|
|
165
|
-
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
166
|
-
const decompress = (encoding ? DECOMPRESS_ALGO[encoding] : undefined) ?? ((d, c) => c(null, d));
|
|
167
|
-
// Content-Type validation.
|
|
168
|
-
const contentType = req.headers['content-type'] ?? '';
|
|
169
|
-
if (mimetype && contentType.split(';')[0].trim() !== mimetype)
|
|
170
|
-
return void res.status(415).send('Unsupported Media Type: Wrong Content-Type');
|
|
171
|
-
// Stream collection.
|
|
172
|
-
let data = Buffer.alloc(0);
|
|
173
|
-
req.on('data', (chunk) => {
|
|
174
|
-
if (data === null)
|
|
175
|
-
return; // already aborted
|
|
176
|
-
const next_ = Buffer.concat([data, chunk]);
|
|
177
|
-
if (next_.length > maxLength) {
|
|
178
|
-
data = null;
|
|
179
|
-
res.status(413).send('Content Too Large');
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
data = next_;
|
|
183
|
-
});
|
|
184
|
-
req.on('end', () => {
|
|
185
|
-
if (data === null)
|
|
186
|
-
return; // aborted during streaming
|
|
187
|
-
// zlib types require NonSharedBuffer; Buffer satisfies this at runtime.
|
|
188
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
189
|
-
decompress(data, (err, decompressed) => {
|
|
190
|
-
if (err)
|
|
191
|
-
return void res.status(500).send(err.message);
|
|
192
|
-
callback(contentType, decompressed);
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
// TODO
|
|
197
|
-
function readReqBody(req, opts, mimetype) {
|
|
198
|
-
return new Promise((resolve, reject) => {
|
|
199
|
-
const length = parseInt(req.headers['content-length'] ?? '0', 10);
|
|
200
|
-
// Detect chunked transfer encoding — no Content-Length is present in this case.
|
|
201
|
-
const isChunked = req.headers['transfer-encoding']
|
|
202
|
-
?.split(',').map((v) => v.trim()).some((v) => v.toLowerCase() === 'chunked') ?? false;
|
|
203
|
-
// No body declared and not chunked — resolve with null.
|
|
204
|
-
if (!isChunked && (!length || length === 0))
|
|
205
|
-
return resolve(null);
|
|
206
|
-
const maxLength = readSize(opts.limit) || 102_400;
|
|
207
|
-
// For requests with a known Content-Length we can reject upfront.
|
|
208
|
-
if (!isChunked && length > maxLength)
|
|
209
|
-
return reject({ status: 413, message: 'Content Too Large' });
|
|
210
|
-
// Compression handling.
|
|
211
|
-
const encoding = req.headers['content-encoding'];
|
|
212
|
-
if (encoding && (opts.inflate === false || !DECOMPRESS_ALGO[encoding]))
|
|
213
|
-
return reject({ status: 415, message: 'Unsupported Media Type: Wrong Content-Encoding' });
|
|
214
|
-
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
215
|
-
const decompress = (encoding ? DECOMPRESS_ALGO[encoding] : undefined) ?? ((d, c) => c(null, d));
|
|
216
|
-
// Content-Type validation.
|
|
217
|
-
const contentType = req.headers['content-type'] ?? '';
|
|
218
|
-
if (mimetype && contentType.split(';')[0].trim() !== mimetype)
|
|
219
|
-
return reject({ status: 415, message: 'Unsupported Media Type: Wrong Content-Type' });
|
|
220
|
-
// Stream collection.
|
|
221
|
-
let data = Buffer.alloc(0);
|
|
222
|
-
req.on('data', (chunk) => {
|
|
223
|
-
if (data === null)
|
|
224
|
-
return; // already aborted
|
|
225
|
-
const next_ = Buffer.concat([data, chunk]);
|
|
226
|
-
if (next_.length > maxLength) {
|
|
227
|
-
data = null;
|
|
228
|
-
reject({ status: 413, message: 'Content Too Large' });
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
data = next_;
|
|
232
|
-
});
|
|
233
|
-
req.on('end', () => {
|
|
234
|
-
if (data === null)
|
|
235
|
-
return; // aborted during streaming
|
|
236
|
-
// zlib types require NonSharedBuffer; Buffer satisfies this at runtime.
|
|
237
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
238
|
-
decompress(data, (err, decompressed) => {
|
|
239
|
-
if (err)
|
|
240
|
-
return reject({ status: 500, message: err.message });
|
|
241
|
-
resolve({ mimetype: contentType ?? '', content: decompressed });
|
|
242
|
-
});
|
|
243
|
-
});
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
|
-
// ---------------------------------------------------------------------------
|
|
247
|
-
// Body parsers
|
|
248
|
-
// ---------------------------------------------------------------------------
|
|
249
|
-
/**
|
|
250
|
-
* Parse a collected body buffer as plain text, decode it using the charset
|
|
251
|
-
* declared in `contentType`, and assign the result to `req.body`.
|
|
252
|
-
*
|
|
253
|
-
* On success, calls `next()`. On failure, sends a 500 Internal Server Error.
|
|
254
|
-
*
|
|
255
|
-
* @param req - The incoming request (mutated: `req.body` is set).
|
|
256
|
-
* @param res - The outgoing response.
|
|
257
|
-
* @param next - Called on successful parsing.
|
|
258
|
-
* @param contentType - The raw `Content-Type` header value.
|
|
259
|
-
* @param data - The raw body buffer.
|
|
260
|
-
*/
|
|
261
|
-
function readBodyAsPlainText(req, res, next, contentType, data) {
|
|
262
|
-
const charset = extractCharset(contentType);
|
|
263
|
-
try {
|
|
264
|
-
req.body = data.toString(charset);
|
|
265
|
-
next();
|
|
266
|
-
}
|
|
267
|
-
catch (ex) {
|
|
268
|
-
res.status(500).send(ex.message);
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
/**
|
|
272
|
-
* Parse a collected body buffer as JSON, decode it using the charset declared
|
|
273
|
-
* in `contentType`, apply an optional `reviver`, and assign the result to
|
|
274
|
-
* `req.body`.
|
|
275
|
-
*
|
|
276
|
-
* On success, calls `next()`. On failure (invalid JSON or unsupported charset),
|
|
277
|
-
* sends a 500 Internal Server Error.
|
|
278
|
-
*
|
|
279
|
-
* @param req - The incoming request (mutated: `req.body` is set).
|
|
280
|
-
* @param res - The outgoing response.
|
|
281
|
-
* @param next - Called on successful parsing.
|
|
282
|
-
* @param opts - Resolved options; `opts.reviver` is passed to `JSON.parse`.
|
|
283
|
-
* @param contentType - The raw `Content-Type` header value.
|
|
284
|
-
* @param data - The raw body buffer.
|
|
285
|
-
*/
|
|
286
|
-
function readBodyAsJson(req, res, next, opts, contentType, data) {
|
|
287
|
-
const charset = extractCharset(contentType);
|
|
288
|
-
try {
|
|
289
|
-
const parsed = JSON.parse(data.toString(charset), opts.reviver ?? undefined);
|
|
290
|
-
// FIX-10: strict mode — reject bare primitives (strings, numbers, booleans)
|
|
291
|
-
// at the top level; only objects and arrays are accepted.
|
|
292
|
-
if (opts.strict && (typeof parsed !== 'object' || parsed === null)) {
|
|
293
|
-
return void res.status(400).send('Bad Request: JSON body must be an object or array');
|
|
294
|
-
}
|
|
295
|
-
req.body = parsed;
|
|
296
|
-
next();
|
|
297
|
-
}
|
|
298
|
-
catch (ex) {
|
|
299
|
-
// Invalid JSON is a client error (400), not a server error.
|
|
300
|
-
res.status(400).send('Bad Request: ' + ex.message);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
/**
|
|
304
|
-
* Parse a raw `multipart/form-data` body buffer into an array of
|
|
305
|
-
* {@link FormPart} objects.
|
|
306
|
-
*
|
|
307
|
-
* This is the shared parsing kernel used by both the {@link formData}
|
|
308
|
-
* middleware and the `req.formData()` extension method on the request object.
|
|
309
|
-
*
|
|
310
|
-
* **Multipart wire format recap:**
|
|
311
|
-
* ```
|
|
312
|
-
* --boundary\r\n
|
|
313
|
-
* Header: value\r\n
|
|
314
|
-
* \r\n
|
|
315
|
-
* <binary content>
|
|
316
|
-
* --boundary\r\n
|
|
317
|
-
* ...
|
|
318
|
-
* --boundary--\r\n
|
|
319
|
-
* ```
|
|
320
|
-
* The boundary string in `Content-Type` does **not** include the leading `--`;
|
|
321
|
-
* actual part delimiters on the wire are `\r\n--boundary`.
|
|
322
|
-
*
|
|
323
|
-
* @param contentType - The raw `Content-Type` header value (must include
|
|
324
|
-
* `boundary=<value>`).
|
|
325
|
-
* @param data - The fully-collected raw body buffer.
|
|
326
|
-
* @returns An array of parsed {@link FormPart} objects.
|
|
327
|
-
* @throws `{ status: 400, message }` when the `boundary` parameter is absent.
|
|
328
|
-
*/
|
|
329
|
-
function parseMultipartBody(contentType, data) {
|
|
330
|
-
const boundary = contentType
|
|
331
|
-
.split(';')
|
|
332
|
-
.map((s) => s.replace(/^\s+|\s+$/g, ''))
|
|
333
|
-
.find((s) => s.startsWith('boundary='))
|
|
334
|
-
?.substring('boundary='.length);
|
|
335
|
-
if (!boundary)
|
|
336
|
-
throw { status: 400, message: 'Bad Request: missing multipart boundary' };
|
|
337
|
-
// Wire-level delimiter: each part (after the preamble) is preceded by
|
|
338
|
-
// \r\n--boundary. We split on this sequence so every resulting slice is
|
|
339
|
-
// the raw content of one part (headers + blank line + body), without any
|
|
340
|
-
// leading delimiter bytes.
|
|
341
|
-
const delimiter = Buffer.from(`\r\n--${boundary}`);
|
|
342
|
-
// Prepend \r\n so the very first part is also cleanly split.
|
|
343
|
-
const normalized = Buffer.concat([Buffer.from('\r\n'), data]);
|
|
344
|
-
const rawParts = splitBuffer(normalized, delimiter);
|
|
345
|
-
const parts = [];
|
|
346
|
-
for (const part of rawParts) {
|
|
347
|
-
// The closing delimiter ends with '--'; skip it.
|
|
348
|
-
if (part.toString('utf8', 0, 2) === '--')
|
|
349
|
-
continue;
|
|
350
|
-
// Each part begins with \r\n (from after the delimiter), then headers,
|
|
351
|
-
// then \r\n\r\n (blank line), then content.
|
|
352
|
-
// Skip the leading \r\n.
|
|
353
|
-
const partContent = part.slice(2);
|
|
354
|
-
const blankLine = Buffer.from('\r\n\r\n');
|
|
355
|
-
const blankIdx = partContent.indexOf(blankLine);
|
|
356
|
-
if (blankIdx === -1)
|
|
357
|
-
continue; // malformed part — skip
|
|
358
|
-
const headerSection = partContent.slice(0, blankIdx).toString('utf8');
|
|
359
|
-
const content = partContent.slice(blankIdx + blankLine.length);
|
|
360
|
-
const headers = {};
|
|
361
|
-
for (const line of headerSection.split('\r\n')) {
|
|
362
|
-
if (!line)
|
|
363
|
-
continue;
|
|
364
|
-
const colonIdx = line.indexOf(':');
|
|
365
|
-
if (colonIdx === -1)
|
|
366
|
-
continue;
|
|
367
|
-
const key = line.substring(0, colonIdx).replace(/^\s+|\s+$/g, '').toLowerCase();
|
|
368
|
-
const value = line.substring(colonIdx + 1).replace(/^\s+|\s+$/g, '');
|
|
369
|
-
headers[key] = value;
|
|
370
|
-
}
|
|
371
|
-
parts.push({ headers, content });
|
|
372
|
-
}
|
|
373
|
-
return parts;
|
|
374
|
-
}
|
|
375
|
-
/**
|
|
376
|
-
* Parse a collected body buffer as `multipart/form-data`, split it on the
|
|
377
|
-
* boundary declared in `contentType`, parse each part's headers, and assign
|
|
378
|
-
* an array of {@link FormPart} objects to `req.body`.
|
|
379
|
-
*
|
|
380
|
-
* On success, calls `next()`. On failure (missing boundary, malformed parts),
|
|
381
|
-
* sends the appropriate HTTP error.
|
|
382
|
-
*
|
|
383
|
-
* @param req - The incoming request (mutated: `req.body` is set).
|
|
384
|
-
* @param res - The outgoing response.
|
|
385
|
-
* @param next - Called on successful parsing.
|
|
386
|
-
* @param contentType - The raw `Content-Type` header value (must include
|
|
387
|
-
* `boundary=<value>`).
|
|
388
|
-
* @param data - The raw body buffer.
|
|
389
|
-
*/
|
|
390
|
-
function readBodyAsFormData(req, res, next, contentType, data) {
|
|
391
|
-
try {
|
|
392
|
-
req.body = parseMultipartBody(contentType, data);
|
|
393
|
-
next();
|
|
394
|
-
}
|
|
395
|
-
catch (ex) {
|
|
396
|
-
const status = ex.status ?? 500;
|
|
397
|
-
res.status(status).send(ex.message ?? String(ex));
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
/**
|
|
401
|
-
* Parse a collected body buffer as `application/x-www-form-urlencoded` and
|
|
402
|
-
* assign the result to `req.body`.
|
|
403
|
-
*
|
|
404
|
-
* Repeated keys (e.g. `tags=a&tags=b`) produce an array value:
|
|
405
|
-
* `{ tags: ['a', 'b'] }`. Single-occurrence keys produce a plain string.
|
|
406
|
-
*
|
|
407
|
-
* @param req - The incoming request (mutated: `req.body` is set).
|
|
408
|
-
* @param res - The outgoing response.
|
|
409
|
-
* @param next - Called on successful parsing.
|
|
410
|
-
* @param contentType - The raw `Content-Type` header value.
|
|
411
|
-
* @param data - The raw body buffer.
|
|
412
|
-
*/
|
|
413
|
-
function readBodyAsFormEncoded(req, res, next, contentType, data) {
|
|
414
|
-
const charset = extractCharset(contentType);
|
|
415
|
-
try {
|
|
416
|
-
const params = new URLSearchParams(data.toString(charset));
|
|
417
|
-
const result = {};
|
|
418
|
-
for (const [key, value] of params.entries()) {
|
|
419
|
-
const existing = result[key];
|
|
420
|
-
if (existing === undefined) {
|
|
421
|
-
result[key] = value;
|
|
422
|
-
}
|
|
423
|
-
else if (Array.isArray(existing)) {
|
|
424
|
-
existing.push(value);
|
|
425
|
-
}
|
|
426
|
-
else {
|
|
427
|
-
result[key] = [existing, value];
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
req.body = result;
|
|
431
|
-
next();
|
|
432
|
-
}
|
|
433
|
-
catch (ex) {
|
|
434
|
-
res.status(400).send('Bad Request: ' + ex.message);
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
/**
|
|
438
|
-
* Dispatch table mapping a MIME type to its body-parser function.
|
|
439
|
-
* Used by {@link parseBody} to select the appropriate parser at runtime.
|
|
440
|
-
*/
|
|
441
|
-
const BODY_READERS = {
|
|
442
|
-
'multipart/form-data': (req, res, next, _opts, ct, data) => readBodyAsFormData(req, res, next, ct, data),
|
|
443
|
-
'application/json': (req, res, next, opts, ct, data) => readBodyAsJson(req, res, next, opts, ct, data),
|
|
444
|
-
'application/x-www-form-urlencoded': (req, res, next, _opts, ct, data) => readBodyAsFormEncoded(req, res, next, ct, data),
|
|
445
|
-
'text/plain': (req, res, next, _opts, ct, data) => readBodyAsPlainText(req, res, next, ct, data),
|
|
446
|
-
};
|
|
447
|
-
// ---------------------------------------------------------------------------
|
|
448
|
-
// Public middleware factories
|
|
449
|
-
// ---------------------------------------------------------------------------
|
|
450
|
-
/**
|
|
451
|
-
* Middleware factory that parses a `application/json` request body and
|
|
452
|
-
* assigns the parsed value to `req.body`.
|
|
453
|
-
*
|
|
454
|
-
* Also attaches a `res.json(data)` helper to the response object so that
|
|
455
|
-
* handlers can send JSON responses conveniently:
|
|
456
|
-
* ```ts
|
|
457
|
-
* res.json({ ok: true });
|
|
458
|
-
* ```
|
|
459
|
-
*
|
|
460
|
-
* Behaviour:
|
|
461
|
-
* - Requests without a body (`Content-Length: 0` or absent) are passed
|
|
462
|
-
* through to `next()` without touching `req.body`.
|
|
463
|
-
* - Bodies larger than `opts.limit` receive **413 Content Too Large**.
|
|
464
|
-
* - Bodies with an unsupported `Content-Encoding` receive
|
|
465
|
-
* **415 Unsupported Media Type**.
|
|
466
|
-
* - Bodies whose `Content-Type` is not `application/json` receive
|
|
467
|
-
* **415 Unsupported Media Type**.
|
|
468
|
-
* - Parse errors receive **500 Internal Server Error**.
|
|
469
|
-
*
|
|
470
|
-
* @param opts - Optional configuration (see {@link BodyOptions}).
|
|
471
|
-
* @returns An Express-compatible middleware function.
|
|
472
|
-
*/
|
|
473
|
-
function json(opts) {
|
|
474
|
-
const resolved = {
|
|
475
|
-
inflate: true,
|
|
476
|
-
limit: '100kb',
|
|
477
|
-
reviver: null,
|
|
478
|
-
strict: true,
|
|
479
|
-
...opts,
|
|
480
|
-
};
|
|
481
|
-
return (req, res, next) => {
|
|
482
|
-
// Attach a JSON response helper so handlers can call res.json(data).
|
|
483
|
-
res.json = (data) => {
|
|
484
|
-
res.write(JSON.stringify(data));
|
|
485
|
-
res.end();
|
|
486
|
-
};
|
|
487
|
-
readBody(req, res, resolved, 'application/json', next, (contentType, body) => {
|
|
488
|
-
readBodyAsJson(req, res, next, resolved, contentType, body);
|
|
489
|
-
});
|
|
490
|
-
};
|
|
491
|
-
}
|
|
492
|
-
/**
|
|
493
|
-
* Middleware factory that parses a `multipart/form-data` request body and
|
|
494
|
-
* assigns an array of {@link FormPart} objects to `req.body`.
|
|
495
|
-
*
|
|
496
|
-
* Each element in `req.body` exposes:
|
|
497
|
-
* - `headers` — the part's MIME headers (e.g. `Content-Disposition`).
|
|
498
|
-
* - `content` — the raw binary content of the part as a `Buffer`.
|
|
499
|
-
*
|
|
500
|
-
* Behaviour:
|
|
501
|
-
* - Requests without a body are passed through to `next()`.
|
|
502
|
-
* - Bodies larger than `opts.limit` receive **413 Content Too Large**.
|
|
503
|
-
* - Bodies with a missing or malformed `boundary` parameter receive
|
|
504
|
-
* **400 Bad Request**.
|
|
505
|
-
* - Parse errors receive **500 Internal Server Error**.
|
|
506
|
-
*
|
|
507
|
-
* @param opts - Optional configuration (see {@link BodyOptions}).
|
|
508
|
-
* @returns An Express-compatible middleware function.
|
|
509
|
-
*/
|
|
510
|
-
function formData(opts) {
|
|
511
|
-
const resolved = {
|
|
512
|
-
inflate: true,
|
|
513
|
-
limit: '100kb',
|
|
514
|
-
reviver: null,
|
|
515
|
-
strict: true,
|
|
516
|
-
...opts,
|
|
517
|
-
};
|
|
518
|
-
return (req, res, next) => {
|
|
519
|
-
readBody(req, res, resolved, 'multipart/form-data', next, (contentType, body) => {
|
|
520
|
-
readBodyAsFormData(req, res, next, contentType, body);
|
|
521
|
-
});
|
|
522
|
-
};
|
|
523
|
-
}
|
|
524
|
-
/**
|
|
525
|
-
* Middleware factory that parses an `application/x-www-form-urlencoded`
|
|
526
|
-
* request body and assigns the decoded fields to `req.body`.
|
|
527
|
-
*
|
|
528
|
-
* Repeated keys (e.g. `tags=a&tags=b`) produce an array value:
|
|
529
|
-
* `{ tags: ['a', 'b'] }`. Single-occurrence keys produce a plain string.
|
|
530
|
-
*
|
|
531
|
-
* Behaviour:
|
|
532
|
-
* - Requests without a body are passed through to `next()`.
|
|
533
|
-
* - Bodies larger than `opts.limit` receive **413 Content Too Large**.
|
|
534
|
-
* - Bodies whose `Content-Type` is not `application/x-www-form-urlencoded`
|
|
535
|
-
* receive **415 Unsupported Media Type**.
|
|
536
|
-
* - Parse errors receive **400 Bad Request**.
|
|
537
|
-
*
|
|
538
|
-
* @param opts - Optional configuration (see {@link BodyOptions}).
|
|
539
|
-
* @returns An Express-compatible middleware function.
|
|
540
|
-
*
|
|
541
|
-
* @example
|
|
542
|
-
* ```ts
|
|
543
|
-
* app.post('/form', formEncoded(), (req, res) => {
|
|
544
|
-
* const { username, tags } = req.body as any;
|
|
545
|
-
* res.json({ username, tags }); // tags may be string | string[]
|
|
546
|
-
* });
|
|
547
|
-
* ```
|
|
548
|
-
*/
|
|
549
|
-
function formEncoded(opts) {
|
|
550
|
-
const resolved = {
|
|
551
|
-
inflate: true,
|
|
552
|
-
limit: '100kb',
|
|
553
|
-
reviver: null,
|
|
554
|
-
strict: true,
|
|
555
|
-
...opts,
|
|
556
|
-
};
|
|
557
|
-
return (req, res, next) => {
|
|
558
|
-
readBody(req, res, resolved, 'application/x-www-form-urlencoded', next, (contentType, body) => {
|
|
559
|
-
readBodyAsFormEncoded(req, res, next, contentType, body);
|
|
560
|
-
});
|
|
561
|
-
};
|
|
562
|
-
}
|
|
563
|
-
/**
|
|
564
|
-
* Middleware factory that auto-detects the `Content-Type` of the request body
|
|
565
|
-
* and parses it using the appropriate parser.
|
|
566
|
-
*
|
|
567
|
-
* Supported MIME types:
|
|
568
|
-
* - `application/json` → parsed as JSON; result is a JS value.
|
|
569
|
-
* - `multipart/form-data` → parsed as multipart; result is `FormPart[]`.
|
|
570
|
-
* - `application/x-www-form-urlencoded` → decoded as key/value pairs; result is
|
|
571
|
-
* `Record<string, string | string[]>`.
|
|
572
|
-
* - `text/plain` → decoded as text; result is a string.
|
|
573
|
-
*
|
|
574
|
-
* Requests with an unsupported MIME type receive **415 Unsupported Media Type**.
|
|
575
|
-
* All other error conditions behave identically to {@link json}, {@link formData},
|
|
576
|
-
* and {@link formEncoded}.
|
|
577
|
-
*
|
|
578
|
-
* @param opts - Optional configuration (see {@link BodyOptions}).
|
|
579
|
-
* @returns An Express-compatible middleware function.
|
|
580
|
-
*/
|
|
581
|
-
function parseBody(opts) {
|
|
582
|
-
const resolved = {
|
|
583
|
-
inflate: true,
|
|
584
|
-
limit: '100kb',
|
|
585
|
-
reviver: null,
|
|
586
|
-
strict: true,
|
|
587
|
-
...opts,
|
|
588
|
-
};
|
|
589
|
-
return (req, res, next) => {
|
|
590
|
-
readBody(req, res, resolved, null, next, (contentType, body) => {
|
|
591
|
-
const mimetype = contentType.split(';')[0].trim();
|
|
592
|
-
if (!BODY_READERS[mimetype])
|
|
593
|
-
return res.status(415).send('Unsupported Media Type');
|
|
594
|
-
BODY_READERS[mimetype](req, res, next, resolved, contentType, body);
|
|
595
|
-
});
|
|
596
|
-
};
|
|
597
|
-
}
|
|
598
|
-
/**
|
|
599
|
-
* Async generator that yields each part of a `multipart/form-data` request
|
|
600
|
-
* body as a {@link FormPartStream} object.
|
|
601
|
-
*
|
|
602
|
-
* Unlike {@link formData} (which must be installed as middleware before a
|
|
603
|
-
* handler), `streamFormData` can be called directly inside any handler and
|
|
604
|
-
* returns an async iterable of parts:
|
|
605
|
-
*
|
|
606
|
-
* ```ts
|
|
607
|
-
* app.post('/upload', async (req, res) => {
|
|
608
|
-
* for await (const part of streamFormData(req)) {
|
|
609
|
-
* const name = part.headers['content-disposition'];
|
|
610
|
-
* const chunks: Buffer[] = [];
|
|
611
|
-
* for await (const chunk of part.stream) chunks.push(chunk);
|
|
612
|
-
* const content = Buffer.concat(chunks);
|
|
613
|
-
* // ... process content
|
|
614
|
-
* }
|
|
615
|
-
* res.send('ok');
|
|
616
|
-
* });
|
|
617
|
-
* ```
|
|
618
|
-
*
|
|
619
|
-
* **Note:** the full request body is buffered before parts are yielded,
|
|
620
|
-
* because the multipart boundary must span the entire body. For very large
|
|
621
|
-
* uploads, prefer streaming directly from the raw request.
|
|
622
|
-
*
|
|
623
|
-
* @param req - The incoming request (must be a `multipart/form-data` request).
|
|
624
|
-
* @param opts - Optional configuration — only `limit` and `inflate` are used.
|
|
625
|
-
* @throws `{ status: 400, message }` when the `boundary` parameter is absent.
|
|
626
|
-
* @throws `{ status: 413, message }` when the body exceeds the size limit.
|
|
627
|
-
*/
|
|
628
|
-
async function* streamFormData(req, opts) {
|
|
629
|
-
const maxSize = (opts?.limit !== undefined ? readSize(opts.limit) : 0) || 102_400;
|
|
630
|
-
// Collect all chunks from the raw request stream.
|
|
631
|
-
const chunks = [];
|
|
632
|
-
let totalSize = 0;
|
|
633
|
-
for await (const chunk of req) {
|
|
634
|
-
totalSize += chunk.length;
|
|
635
|
-
if (totalSize > maxSize)
|
|
636
|
-
throw { status: 413, message: 'Content Too Large' };
|
|
637
|
-
chunks.push(chunk);
|
|
638
|
-
}
|
|
639
|
-
const body = Buffer.concat(chunks);
|
|
640
|
-
const contentType = req.headers['content-type'] ?? '';
|
|
641
|
-
const parts = parseMultipartBody(contentType, body);
|
|
642
|
-
for (const part of parts) {
|
|
643
|
-
// Expose each part's Buffer content as a Readable stream so callers can
|
|
644
|
-
// pipe, pipeline, or iterate it uniformly.
|
|
645
|
-
yield { headers: part.headers, stream: stream_1.Readable.from(part.content) };
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
// ---------------------------------------------------------------------------
|
|
649
|
-
// Logger middleware
|
|
650
|
-
// ---------------------------------------------------------------------------
|
|
651
|
-
/**
|
|
652
|
-
* ANSI terminal colour codes indexed by HTTP status class (1xx–5xx).
|
|
653
|
-
*
|
|
654
|
-
* Index 0 is unused (no HTTP 0xx class).
|
|
655
|
-
* - 1xx → yellow (`\x1b[33m`)
|
|
656
|
-
* - 2xx → green (`\x1b[32m`)
|
|
657
|
-
* - 3xx → yellow (`\x1b[33m`)
|
|
658
|
-
* - 4xx → red (`\x1b[31m`)
|
|
659
|
-
* - 5xx → bright red (`\x1b[91m`)
|
|
660
|
-
*/
|
|
661
|
-
const STATUS_COLORS = [
|
|
662
|
-
'\x1b[0m', // 0 — fallback / unknown
|
|
663
|
-
'\x1b[33m', // 1xx — informational (yellow)
|
|
664
|
-
'\x1b[32m', // 2xx — success (green)
|
|
665
|
-
'\x1b[33m', // 3xx — redirection (yellow)
|
|
666
|
-
'\x1b[31m', // 4xx — client error (red)
|
|
667
|
-
'\x1b[91m', // 5xx — server error (bright red)
|
|
668
|
-
];
|
|
669
|
-
/** ANSI reset escape sequence. */
|
|
670
|
-
const ANSI_RESET = '\x1b[0m';
|
|
671
|
-
/**
|
|
672
|
-
* Middleware factory that logs one line per completed HTTP request to the
|
|
673
|
-
* console (or a custom logger function).
|
|
674
|
-
*
|
|
675
|
-
* Each log line contains:
|
|
676
|
-
* - Timestamp (formatted with `Intl.DateTimeFormat`).
|
|
677
|
-
* - HTTP status code (ANSI-coloured by status class).
|
|
678
|
-
* - HTTP method and request path.
|
|
679
|
-
* - Client IP address (honours `X-Forwarded-For`).
|
|
680
|
-
* - User identity (from `opts.user` or `'-'`).
|
|
681
|
-
* - Elapsed time in milliseconds.
|
|
682
|
-
* - Response `Content-Length` (or `'-'` when absent).
|
|
683
|
-
*
|
|
684
|
-
* **Lost-request tracking:** when `opts.track` is `true`, a timer is started
|
|
685
|
-
* for each request. If the response is not finished within `opts.trackTimeout`
|
|
686
|
-
* milliseconds, a `LOST` warning line is emitted. This is useful in
|
|
687
|
-
* development to surface handlers that never call `res.end()`.
|
|
688
|
-
*
|
|
689
|
-
* @param opts - Optional configuration (see {@link LoggerOptions}).
|
|
690
|
-
* @returns An Express-compatible middleware function.
|
|
691
|
-
*
|
|
692
|
-
* @example
|
|
693
|
-
* ```ts
|
|
694
|
-
* app.use('/', logger({
|
|
695
|
-
* track: true,
|
|
696
|
-
* trackTimeout: 30_000,
|
|
697
|
-
* user: (req) => (req as any).authUser ?? '-',
|
|
698
|
-
* locale: 'en-US',
|
|
699
|
-
* logger: (msg) => process.stderr.write(msg + '\n'),
|
|
700
|
-
* }));
|
|
701
|
-
* ```
|
|
702
|
-
*/
|
|
703
|
-
function logger(opts) {
|
|
704
|
-
const options = opts ?? {};
|
|
705
|
-
const log = options.logger ?? console.log;
|
|
706
|
-
const formatter = new Intl.DateTimeFormat(options.locale ?? 'en-GB', options.dateFormat ?? {
|
|
707
|
-
month: 'short',
|
|
708
|
-
day: '2-digit',
|
|
709
|
-
hour: '2-digit',
|
|
710
|
-
minute: '2-digit',
|
|
711
|
-
});
|
|
712
|
-
return (req, res, next) => {
|
|
713
|
-
// Capture timestamp and path at request-arrival time so they are stable
|
|
714
|
-
// even if later middleware mutates req.path (e.g. prefix stripping).
|
|
715
|
-
const timestamp = formatter.format(new Date());
|
|
716
|
-
const requestPath = req.path ?? req.url ?? '/';
|
|
717
|
-
const ip = req.ip ?? '';
|
|
718
|
-
const user = options.user?.(req) ?? '-';
|
|
719
|
-
const receivedAt = Date.now();
|
|
720
|
-
// Lost-request tracking.
|
|
721
|
-
const tracker = options.track === true
|
|
722
|
-
? setTimeout(() => {
|
|
723
|
-
log(`${timestamp} LOST ${req.method} ${requestPath} ${ip} <${user}>`);
|
|
724
|
-
}, options.trackTimeout ?? 30_000)
|
|
725
|
-
: null;
|
|
726
|
-
res.on('finish', () => {
|
|
727
|
-
if (tracker !== null)
|
|
728
|
-
clearTimeout(tracker);
|
|
729
|
-
const host = req.headers.host;
|
|
730
|
-
const elapsed = Date.now() - receivedAt;
|
|
731
|
-
const statusClass = Math.floor(res.statusCode / 100);
|
|
732
|
-
const colour = STATUS_COLORS[statusClass] ?? STATUS_COLORS[0];
|
|
733
|
-
const statusStr = `${colour}${res.statusCode}${ANSI_RESET}`;
|
|
734
|
-
const contentLen = res.getHeader('content-length') ?? '-';
|
|
735
|
-
if (options.json === true)
|
|
736
|
-
log({
|
|
737
|
-
timestamp,
|
|
738
|
-
status: res.statusCode,
|
|
739
|
-
method: req.method,
|
|
740
|
-
path: requestPath,
|
|
741
|
-
ip,
|
|
742
|
-
user,
|
|
743
|
-
elapsed,
|
|
744
|
-
host,
|
|
745
|
-
length: contentLen,
|
|
746
|
-
});
|
|
747
|
-
else
|
|
748
|
-
log(`${timestamp} ${statusStr} ${req.method} ${requestPath} ${ip} <${user}> ${elapsed}ms (${contentLen})`);
|
|
749
|
-
});
|
|
750
|
-
next();
|
|
751
|
-
};
|
|
752
|
-
}
|
|
753
|
-
function cors(opts) {
|
|
754
|
-
const options = {
|
|
755
|
-
origin: opts?.origin || '*',
|
|
756
|
-
allowHeaders: opts?.allowHeaders || 'Accept, Content-Type, Authorization',
|
|
757
|
-
allowMethods: opts?.allowMethods || 'GET,HEAD,PUT,PATCH,POST,DELETE',
|
|
758
|
-
allowCredentials: opts?.allowCredentials,
|
|
759
|
-
maxAge: opts?.maxAge,
|
|
760
|
-
vary: opts?.vary,
|
|
761
|
-
optionsStatus: opts?.optionsStatus || 204,
|
|
762
|
-
preflight: opts?.preflight,
|
|
763
|
-
};
|
|
764
|
-
return (req, res, next) => {
|
|
765
|
-
if (options.preflight && !options.preflight(req)) {
|
|
766
|
-
res.status(req.method == 'OPTIONS' ? 403 : 400).end();
|
|
767
|
-
return;
|
|
768
|
-
}
|
|
769
|
-
if (req.headers.origin) {
|
|
770
|
-
res.setHeader('Access-Control-Allow-Origin', options.origin);
|
|
771
|
-
if (options.vary !== undefined)
|
|
772
|
-
res.setHeader('Vary', options.vary);
|
|
773
|
-
}
|
|
774
|
-
if (req.method == 'OPTIONS') {
|
|
775
|
-
res.setHeader('Access-Control-Allow-Headers', options.allowHeaders);
|
|
776
|
-
res.setHeader('Access-Control-Allow-Methods', options.allowMethods);
|
|
777
|
-
if (options.allowCredentials !== undefined)
|
|
778
|
-
res.setHeader('Access-Control-Allow-Credentials', options.allowCredentials ? 'true' : 'false');
|
|
779
|
-
if (options.maxAge !== undefined)
|
|
780
|
-
res.setHeader('Access-Control-Max-Age', options.maxAge.toFixed(0));
|
|
781
|
-
res.status(options.optionsStatus).end();
|
|
782
|
-
return;
|
|
783
|
-
}
|
|
784
|
-
next();
|
|
785
|
-
};
|
|
786
|
-
}
|
|
787
|
-
exports.default = { json, formData, formEncoded, parseBody, logger, cors, streamFormData };
|