expediate 1.0.4 → 1.0.5
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/LICENSE +16 -16
- package/README.md +417 -30
- package/dist/apis.d.ts +138 -21
- package/dist/apis.d.ts.map +1 -1
- package/dist/apis.js +172 -79
- package/dist/apis.js.map +1 -1
- package/dist/cjs/apis.js +327 -0
- package/dist/cjs/git.js +293 -0
- package/dist/cjs/index.js +2583 -0
- package/dist/cjs/jwt-auth.js +532 -0
- package/dist/cjs/middleware.js +511 -0
- package/dist/cjs/mimetypes.json +1 -0
- package/dist/cjs/misc.js +787 -0
- package/dist/cjs/openapi.js +485 -0
- package/dist/cjs/package.json +1 -0
- package/dist/cjs/router.js +898 -0
- package/dist/cjs/static.js +669 -0
- package/dist/git.d.ts +71 -8
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +127 -72
- package/dist/git.js.map +1 -1
- package/dist/index.d.ts +17 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -24
- package/dist/index.js.map +1 -1
- package/dist/jwt-auth.d.ts +147 -57
- package/dist/jwt-auth.d.ts.map +1 -1
- package/dist/jwt-auth.js +445 -205
- package/dist/jwt-auth.js.map +1 -1
- package/dist/middleware.d.ts +476 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +647 -0
- package/dist/middleware.js.map +1 -0
- package/dist/mimetypes.json +1 -1
- package/dist/misc.d.ts +112 -5
- package/dist/misc.d.ts.map +1 -1
- package/dist/misc.js +235 -102
- package/dist/misc.js.map +1 -1
- package/dist/openapi.d.ts +290 -0
- package/dist/openapi.d.ts.map +1 -0
- package/dist/openapi.js +481 -0
- package/dist/openapi.js.map +1 -0
- package/dist/router.d.ts +405 -46
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +658 -153
- package/dist/router.js.map +1 -1
- package/dist/static.d.ts +1 -1
- package/dist/static.d.ts.map +1 -1
- package/dist/static.js +88 -84
- package/dist/static.js.map +1 -1
- package/package.json +21 -4
- package/.npmignore +0 -16
|
@@ -0,0 +1,511 @@
|
|
|
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.compress = compress;
|
|
24
|
+
exports.requestId = requestId;
|
|
25
|
+
exports.rateLimit = rateLimit;
|
|
26
|
+
exports.cacheControl = cacheControl;
|
|
27
|
+
exports.csrf = csrf;
|
|
28
|
+
exports.securityHeaders = securityHeaders;
|
|
29
|
+
const crypto = require("crypto");
|
|
30
|
+
const zlib = require("zlib");
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Internal helpers
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
/**
|
|
35
|
+
* Convert a string or Buffer value to a `Buffer`.
|
|
36
|
+
*
|
|
37
|
+
* @param chunk - The data to convert.
|
|
38
|
+
* @param encoding - Character encoding to use when `chunk` is a string.
|
|
39
|
+
* @returns A `Buffer` containing the bytes of `chunk`.
|
|
40
|
+
*/
|
|
41
|
+
function toBuffer(chunk, encoding = 'utf8') {
|
|
42
|
+
return Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Response compression middleware.
|
|
46
|
+
*
|
|
47
|
+
* Reads the `Accept-Encoding` request header and transparently compresses the
|
|
48
|
+
* response body using Brotli, gzip, or deflate (tested in that preference
|
|
49
|
+
* order). Responses whose total body is smaller than `threshold` bytes are
|
|
50
|
+
* sent uncompressed to avoid wasting CPU on tiny payloads.
|
|
51
|
+
*
|
|
52
|
+
* The middleware monkey-patches `res.write` and `res.end` so that all
|
|
53
|
+
* downstream code (including `res.json`, `res.send`, and manual writes)
|
|
54
|
+
* passes through the compressor without any changes to handler code.
|
|
55
|
+
*
|
|
56
|
+
* The `Content-Encoding` and `Vary: Accept-Encoding` headers are set
|
|
57
|
+
* automatically. `Content-Length` is removed because the compressed length
|
|
58
|
+
* cannot be known in advance.
|
|
59
|
+
*
|
|
60
|
+
* @param opts - Optional compression settings.
|
|
61
|
+
* @returns An Express-compatible middleware function.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* const app = createRouter();
|
|
66
|
+
* app.use(compress()); // at the top — must come first
|
|
67
|
+
* app.get('/api/data', (_req, res) => res.json(bigPayload));
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
function compress(opts) {
|
|
71
|
+
const threshold = opts?.threshold ?? 1024;
|
|
72
|
+
const brEnabled = opts?.br !== false;
|
|
73
|
+
const brotliQuality = opts?.brotliQuality ?? 4;
|
|
74
|
+
const gzipLevel = opts?.gzipLevel ?? zlib.constants.Z_DEFAULT_COMPRESSION;
|
|
75
|
+
return (req, res, next) => {
|
|
76
|
+
// User-supplied filter.
|
|
77
|
+
if (opts?.filter && !opts.filter(req, res))
|
|
78
|
+
return next();
|
|
79
|
+
const ae = req.headers['accept-encoding'] ?? '';
|
|
80
|
+
// Negotiate encoding: Brotli > gzip > deflate.
|
|
81
|
+
let compressor = null;
|
|
82
|
+
let encoding = '';
|
|
83
|
+
if (brEnabled && /\bbr\b/.test(ae)) {
|
|
84
|
+
compressor = zlib.createBrotliCompress({
|
|
85
|
+
params: { [zlib.constants.BROTLI_PARAM_QUALITY]: brotliQuality },
|
|
86
|
+
});
|
|
87
|
+
encoding = 'br';
|
|
88
|
+
}
|
|
89
|
+
else if (/\bgzip\b/.test(ae)) {
|
|
90
|
+
compressor = zlib.createGzip({ level: gzipLevel });
|
|
91
|
+
encoding = 'gzip';
|
|
92
|
+
}
|
|
93
|
+
else if (/\bdeflate\b/.test(ae)) {
|
|
94
|
+
compressor = zlib.createDeflate({ level: gzipLevel });
|
|
95
|
+
encoding = 'deflate';
|
|
96
|
+
}
|
|
97
|
+
if (!compressor)
|
|
98
|
+
return next();
|
|
99
|
+
const comp = compressor;
|
|
100
|
+
const rawRes = res;
|
|
101
|
+
// Capture original write / end before we override them.
|
|
102
|
+
const origWrite = rawRes.write.bind(rawRes);
|
|
103
|
+
const origEnd = rawRes.end.bind(rawRes);
|
|
104
|
+
// Route compressor output back to the raw socket.
|
|
105
|
+
comp.on('data', (chunk) => { origWrite(chunk); });
|
|
106
|
+
comp.on('end', () => { origEnd(); });
|
|
107
|
+
comp.on('error', (e) => {
|
|
108
|
+
console.error('[compress] stream error:', e.message);
|
|
109
|
+
if (!rawRes.writableEnded)
|
|
110
|
+
origEnd();
|
|
111
|
+
});
|
|
112
|
+
// Buffer incoming data so we can decide compress vs bypass at end().
|
|
113
|
+
const pending = [];
|
|
114
|
+
let totalBytes = 0;
|
|
115
|
+
let decided = false; // true once we committed to compress or bypass
|
|
116
|
+
let doCompress = false;
|
|
117
|
+
/**
|
|
118
|
+
* Commit to compression: flush all buffered pending data to the compressor
|
|
119
|
+
* and mark `decided = true, doCompress = true`.
|
|
120
|
+
*
|
|
121
|
+
* @param andEnd - When `true`, also call `comp.end()` to flush and close
|
|
122
|
+
* the compressor after writing pending data.
|
|
123
|
+
*/
|
|
124
|
+
const startCompressing = (andEnd) => {
|
|
125
|
+
decided = true;
|
|
126
|
+
doCompress = true;
|
|
127
|
+
res.setHeader('Content-Encoding', encoding);
|
|
128
|
+
res.setHeader('Vary', 'Accept-Encoding');
|
|
129
|
+
res.removeHeader('Content-Length');
|
|
130
|
+
for (const buf of pending)
|
|
131
|
+
comp.write(buf);
|
|
132
|
+
if (andEnd)
|
|
133
|
+
comp.end();
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Commit to bypassing compression (total bytes below threshold).
|
|
137
|
+
* Undoes the tentative headers and writes buffered data directly.
|
|
138
|
+
*/
|
|
139
|
+
const skipCompression = () => {
|
|
140
|
+
decided = true;
|
|
141
|
+
doCompress = false;
|
|
142
|
+
comp.destroy();
|
|
143
|
+
if (totalBytes > 0)
|
|
144
|
+
res.setHeader('Content-Length', totalBytes);
|
|
145
|
+
for (const buf of pending)
|
|
146
|
+
origWrite(buf);
|
|
147
|
+
};
|
|
148
|
+
// Override res.write.
|
|
149
|
+
res.write = (chunk, encOrCb, _cb) => {
|
|
150
|
+
const enc = typeof encOrCb === 'string' ? encOrCb : 'utf8';
|
|
151
|
+
const buf = toBuffer(chunk, enc);
|
|
152
|
+
if (decided) {
|
|
153
|
+
// Already committed — write directly to the right destination.
|
|
154
|
+
return doCompress ? comp.write(buf) : origWrite(buf);
|
|
155
|
+
}
|
|
156
|
+
pending.push(buf);
|
|
157
|
+
totalBytes += buf.length;
|
|
158
|
+
// Crossed threshold mid-stream: start compression now.
|
|
159
|
+
if (totalBytes >= threshold)
|
|
160
|
+
startCompressing(false);
|
|
161
|
+
return true;
|
|
162
|
+
};
|
|
163
|
+
// Override res.end.
|
|
164
|
+
res.end = (chunk, encOrCb, _cb) => {
|
|
165
|
+
if (typeof chunk === 'function')
|
|
166
|
+
chunk = undefined;
|
|
167
|
+
if (typeof encOrCb === 'function')
|
|
168
|
+
encOrCb = undefined;
|
|
169
|
+
if (chunk != null) {
|
|
170
|
+
const enc = typeof encOrCb === 'string' ? encOrCb : 'utf8';
|
|
171
|
+
const buf = toBuffer(chunk, enc);
|
|
172
|
+
if (decided) {
|
|
173
|
+
if (doCompress)
|
|
174
|
+
comp.write(buf);
|
|
175
|
+
else
|
|
176
|
+
origWrite(buf);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
pending.push(buf);
|
|
180
|
+
totalBytes += buf.length;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (!decided) {
|
|
184
|
+
if (totalBytes >= threshold)
|
|
185
|
+
startCompressing(true);
|
|
186
|
+
else {
|
|
187
|
+
skipCompression();
|
|
188
|
+
origEnd();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else if (doCompress) {
|
|
192
|
+
comp.end();
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
origEnd();
|
|
196
|
+
}
|
|
197
|
+
return rawRes;
|
|
198
|
+
};
|
|
199
|
+
next();
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Request ID middleware.
|
|
204
|
+
*
|
|
205
|
+
* Attaches a unique identifier to every request as `req.id` and echoes it
|
|
206
|
+
* back in the response header (default: `X-Request-ID`). The ID is taken
|
|
207
|
+
* from the incoming header when `allowFromHeader` is `true` (the default);
|
|
208
|
+
* otherwise a new UUID is generated with `crypto.randomUUID()`.
|
|
209
|
+
*
|
|
210
|
+
* Use `req.id` in log statements and error responses to correlate distributed
|
|
211
|
+
* traces back to a single originating request.
|
|
212
|
+
*
|
|
213
|
+
* @param opts - Optional configuration.
|
|
214
|
+
* @returns An Express-compatible middleware function.
|
|
215
|
+
*
|
|
216
|
+
* @example
|
|
217
|
+
* ```ts
|
|
218
|
+
* app.use(requestId());
|
|
219
|
+
* app.use(logger()); // logger can now include req.id in output
|
|
220
|
+
* app.get('/health', (req, res) => res.json({ id: req.id, status: 'ok' }));
|
|
221
|
+
* ```
|
|
222
|
+
*/
|
|
223
|
+
function requestId(opts) {
|
|
224
|
+
const header = (opts?.header ?? 'x-request-id').toLowerCase();
|
|
225
|
+
const allowFromHeader = opts?.allowFromHeader !== false;
|
|
226
|
+
const generator = opts?.generator ?? (() => crypto.randomUUID());
|
|
227
|
+
return (req, res, next) => {
|
|
228
|
+
const incoming = req.headers[header];
|
|
229
|
+
const id = (allowFromHeader && incoming) ? incoming : generator();
|
|
230
|
+
req.id = id;
|
|
231
|
+
res.setHeader(header, id);
|
|
232
|
+
next();
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* In-memory sliding-window rate limiting middleware.
|
|
237
|
+
*
|
|
238
|
+
* Uses a `Map<key, timestamp[]>` to track request timestamps per client.
|
|
239
|
+
* On each request the list is pruned to the current window, then checked
|
|
240
|
+
* against `max`. No external dependencies are required.
|
|
241
|
+
*
|
|
242
|
+
* **Caveats:**
|
|
243
|
+
* - State is held in memory and is lost on process restart.
|
|
244
|
+
* - Not suitable for multi-process deployments without a shared store.
|
|
245
|
+
*
|
|
246
|
+
* @param opts - Rate-limit configuration. `windowMs` and `max` are required.
|
|
247
|
+
* @returns An Express-compatible middleware function.
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* ```ts
|
|
251
|
+
* // Max 100 requests per minute per IP:
|
|
252
|
+
* app.use(rateLimit({ windowMs: 60_000, max: 100 }));
|
|
253
|
+
*
|
|
254
|
+
* // Stricter limit on a specific route:
|
|
255
|
+
* app.post('/auth/login', rateLimit({ windowMs: 60_000, max: 5 }), loginHandler);
|
|
256
|
+
* ```
|
|
257
|
+
*/
|
|
258
|
+
function rateLimit(opts) {
|
|
259
|
+
const windowMs = opts.windowMs;
|
|
260
|
+
const max = opts.max;
|
|
261
|
+
const keyBy = opts.keyBy ?? ((req) => req.ip ?? '');
|
|
262
|
+
const message = opts.message ?? 'Too Many Requests';
|
|
263
|
+
const statusCode = opts.statusCode ?? 429;
|
|
264
|
+
const sendHeaders = opts.headers !== false;
|
|
265
|
+
// Map<clientKey, requestTimestamps[]>
|
|
266
|
+
const store = new Map();
|
|
267
|
+
return (req, res, next) => {
|
|
268
|
+
const key = keyBy(req);
|
|
269
|
+
const now = Date.now();
|
|
270
|
+
const windowStart = now - windowMs;
|
|
271
|
+
// Prune expired timestamps for this key.
|
|
272
|
+
const timestamps = (store.get(key) ?? []).filter((t) => t > windowStart);
|
|
273
|
+
timestamps.push(now);
|
|
274
|
+
store.set(key, timestamps);
|
|
275
|
+
const count = timestamps.length;
|
|
276
|
+
const remaining = Math.max(0, max - count);
|
|
277
|
+
// Reset = when the oldest request in the current window will fall out.
|
|
278
|
+
const resetAt = timestamps.length > 0
|
|
279
|
+
? Math.ceil((timestamps[0] + windowMs) / 1000)
|
|
280
|
+
: Math.ceil((now + windowMs) / 1000);
|
|
281
|
+
if (sendHeaders) {
|
|
282
|
+
res.setHeader('X-RateLimit-Limit', String(max));
|
|
283
|
+
res.setHeader('X-RateLimit-Remaining', String(remaining));
|
|
284
|
+
res.setHeader('X-RateLimit-Reset', String(resetAt));
|
|
285
|
+
}
|
|
286
|
+
if (count > max) {
|
|
287
|
+
res.setHeader('Retry-After', String(Math.ceil(windowMs / 1000)));
|
|
288
|
+
res.statusCode = statusCode;
|
|
289
|
+
res.end(message);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
next();
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Response caching header middleware.
|
|
297
|
+
*
|
|
298
|
+
* Sets `Cache-Control`, and optionally `Expires` and `Vary` response headers
|
|
299
|
+
* based on the supplied options. All directives are optional — only the
|
|
300
|
+
* ones you specify are included in the header value.
|
|
301
|
+
*
|
|
302
|
+
* Mount at the router level for a global default, or on individual routes to
|
|
303
|
+
* apply per-route caching policies.
|
|
304
|
+
*
|
|
305
|
+
* @param opts - Cache policy options.
|
|
306
|
+
* @returns An Express-compatible middleware function.
|
|
307
|
+
*
|
|
308
|
+
* @example
|
|
309
|
+
* ```ts
|
|
310
|
+
* // Global: 5-minute browser cache, CDN-agnostic
|
|
311
|
+
* app.use(cacheControl({ maxAge: 300, public: true }));
|
|
312
|
+
*
|
|
313
|
+
* // Per-route: content-addressed asset, cache forever
|
|
314
|
+
* app.get('/static/app.:hash.js', cacheControl({ maxAge: 31_536_000, immutable: true }), serveStatic('public'));
|
|
315
|
+
*
|
|
316
|
+
* // No caching at all
|
|
317
|
+
* app.use('/api', cacheControl({ noStore: true }));
|
|
318
|
+
* ```
|
|
319
|
+
*/
|
|
320
|
+
function cacheControl(opts = {}) {
|
|
321
|
+
// Pre-compute the Cache-Control value — it is the same for every response.
|
|
322
|
+
const directives = [];
|
|
323
|
+
if (opts.private)
|
|
324
|
+
directives.push('private');
|
|
325
|
+
if (opts.public)
|
|
326
|
+
directives.push('public');
|
|
327
|
+
if (opts.noStore)
|
|
328
|
+
directives.push('no-store');
|
|
329
|
+
if (opts.noCache)
|
|
330
|
+
directives.push('no-cache');
|
|
331
|
+
if (opts.mustRevalidate)
|
|
332
|
+
directives.push('must-revalidate');
|
|
333
|
+
if (opts.immutable)
|
|
334
|
+
directives.push('immutable');
|
|
335
|
+
if (opts.maxAge != null)
|
|
336
|
+
directives.push(`max-age=${opts.maxAge}`);
|
|
337
|
+
if (opts.sMaxAge != null)
|
|
338
|
+
directives.push(`s-maxage=${opts.sMaxAge}`);
|
|
339
|
+
const cacheControlValue = directives.join(', ');
|
|
340
|
+
const varyValue = Array.isArray(opts.vary)
|
|
341
|
+
? opts.vary.join(', ')
|
|
342
|
+
: (opts.vary ?? null);
|
|
343
|
+
return (_req, res, next) => {
|
|
344
|
+
if (cacheControlValue)
|
|
345
|
+
res.setHeader('Cache-Control', cacheControlValue);
|
|
346
|
+
if (opts.maxAge != null) {
|
|
347
|
+
const expires = new Date(Date.now() + opts.maxAge * 1000);
|
|
348
|
+
res.setHeader('Expires', expires.toUTCString());
|
|
349
|
+
}
|
|
350
|
+
if (varyValue)
|
|
351
|
+
res.setHeader('Vary', varyValue);
|
|
352
|
+
next();
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
/** HTTP methods that do not mutate server state and require no CSRF check. */
|
|
356
|
+
const SAFE_CSRF_METHODS = new Set(['GET', 'HEAD', 'OPTIONS', 'TRACE']);
|
|
357
|
+
/**
|
|
358
|
+
* CSRF protection middleware (double-submit cookie pattern).
|
|
359
|
+
*
|
|
360
|
+
* On every request the middleware:
|
|
361
|
+
* 1. Reads (or generates) a random 256-bit hex token stored in a cookie.
|
|
362
|
+
* 2. Attaches `req.csrfToken()` so handlers can embed the token in forms /
|
|
363
|
+
* single-page applications.
|
|
364
|
+
* 3. For state-mutating requests (POST / PUT / PATCH / DELETE) it validates
|
|
365
|
+
* that the `X-CSRF-Token` header (or `_csrf` body field) matches the
|
|
366
|
+
* cookie value, responding **403** on mismatch.
|
|
367
|
+
*
|
|
368
|
+
* The cookie is **not** `HttpOnly` so that browser JavaScript can read the
|
|
369
|
+
* value and include it in subsequent AJAX request headers.
|
|
370
|
+
*
|
|
371
|
+
* @param opts - Optional configuration.
|
|
372
|
+
* @returns An Express-compatible middleware function.
|
|
373
|
+
*
|
|
374
|
+
* @example
|
|
375
|
+
* ```ts
|
|
376
|
+
* app.use(csrf());
|
|
377
|
+
* app.get('/form', (req, res) =>
|
|
378
|
+
* res.send(`<input type="hidden" name="_csrf" value="${req.csrfToken!()}">`));
|
|
379
|
+
* app.post('/submit', (req, res) => res.send('ok')); // validated automatically
|
|
380
|
+
* ```
|
|
381
|
+
*/
|
|
382
|
+
function csrf(opts) {
|
|
383
|
+
const cookieName = opts?.cookieName ?? '_csrf';
|
|
384
|
+
const headerName = (opts?.headerName ?? 'x-csrf-token').toLowerCase();
|
|
385
|
+
const fieldName = opts?.fieldName ?? '_csrf';
|
|
386
|
+
const secure = opts?.secure ?? false;
|
|
387
|
+
const sameSite = opts?.sameSite ?? 'Strict';
|
|
388
|
+
return (req, res, next) => {
|
|
389
|
+
// Generate or reuse existing token from the incoming cookie.
|
|
390
|
+
const existing = req.cookies?.[cookieName];
|
|
391
|
+
const token = (typeof existing === 'string' && existing.length > 0)
|
|
392
|
+
? existing
|
|
393
|
+
: crypto.randomBytes(32).toString('hex');
|
|
394
|
+
// Refresh the cookie when we generated a new token.
|
|
395
|
+
if (!existing) {
|
|
396
|
+
let cookieStr = `${cookieName}=${token}; Path=/; SameSite=${sameSite}`;
|
|
397
|
+
if (secure)
|
|
398
|
+
cookieStr += '; Secure';
|
|
399
|
+
// Append without clobbering other Set-Cookie headers.
|
|
400
|
+
const prev = res.getHeader('Set-Cookie');
|
|
401
|
+
if (prev == null)
|
|
402
|
+
res.setHeader('Set-Cookie', cookieStr);
|
|
403
|
+
else if (Array.isArray(prev))
|
|
404
|
+
res.setHeader('Set-Cookie', [...prev, cookieStr]);
|
|
405
|
+
else
|
|
406
|
+
res.setHeader('Set-Cookie', [prev, cookieStr]);
|
|
407
|
+
}
|
|
408
|
+
// Expose helper so handlers can embed the token in responses.
|
|
409
|
+
req.csrfToken = () => token;
|
|
410
|
+
// Safe methods skip validation entirely.
|
|
411
|
+
if (SAFE_CSRF_METHODS.has(req.method ?? ''))
|
|
412
|
+
return next();
|
|
413
|
+
// Validate: prefer header, fall back to parsed body field.
|
|
414
|
+
const submitted = req.headers[headerName] ??
|
|
415
|
+
req.body?.[fieldName] ??
|
|
416
|
+
'';
|
|
417
|
+
if (!submitted || submitted !== token) {
|
|
418
|
+
res.statusCode = 403;
|
|
419
|
+
res.end('Forbidden: invalid CSRF token');
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
next();
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Security-hardening response headers middleware.
|
|
427
|
+
*
|
|
428
|
+
* Sets a sensible baseline of HTTP response headers that reduce the attack
|
|
429
|
+
* surface for common web vulnerabilities (clickjacking, MIME sniffing, XSS,
|
|
430
|
+
* etc.). Every header can be individually disabled or overridden.
|
|
431
|
+
*
|
|
432
|
+
* Mount once at the top of the router so all responses are covered:
|
|
433
|
+
*
|
|
434
|
+
* ```ts
|
|
435
|
+
* app.use(securityHeaders());
|
|
436
|
+
* ```
|
|
437
|
+
*
|
|
438
|
+
* **Headers set by default:**
|
|
439
|
+
*
|
|
440
|
+
* | Header | Default value |
|
|
441
|
+
* |---------------------------|--------------------------------------------|
|
|
442
|
+
* | Strict-Transport-Security | max-age=15552000; includeSubDomains |
|
|
443
|
+
* | X-Frame-Options | SAMEORIGIN |
|
|
444
|
+
* | X-Content-Type-Options | nosniff |
|
|
445
|
+
* | Referrer-Policy | strict-origin-when-cross-origin |
|
|
446
|
+
* | Permissions-Policy | geolocation=(), microphone=(), camera=() |
|
|
447
|
+
* | X-XSS-Protection | 0 |
|
|
448
|
+
*
|
|
449
|
+
* @param opts - Per-header overrides. Omit to use all defaults.
|
|
450
|
+
* @returns An Express-compatible middleware function.
|
|
451
|
+
*
|
|
452
|
+
* @example
|
|
453
|
+
* ```ts
|
|
454
|
+
* // Disable HSTS on a plain-HTTP development server:
|
|
455
|
+
* app.use(securityHeaders({ hsts: false }));
|
|
456
|
+
*
|
|
457
|
+
* // Deny all framing (not just same-origin):
|
|
458
|
+
* app.use(securityHeaders({ frameOptions: 'DENY' }));
|
|
459
|
+
*
|
|
460
|
+
* // Custom Permissions-Policy:
|
|
461
|
+
* app.use(securityHeaders({
|
|
462
|
+
* permissionsPolicy: 'geolocation=(), payment=(self)',
|
|
463
|
+
* }));
|
|
464
|
+
* ```
|
|
465
|
+
*/
|
|
466
|
+
function securityHeaders(opts) {
|
|
467
|
+
// Pre-compute all header values once at middleware-creation time.
|
|
468
|
+
const headers = [];
|
|
469
|
+
// Strict-Transport-Security
|
|
470
|
+
const hsts = opts?.hsts;
|
|
471
|
+
if (hsts !== false) {
|
|
472
|
+
const h = typeof hsts === 'object' ? hsts : {};
|
|
473
|
+
const maxAge = h.maxAge ?? 15_552_000;
|
|
474
|
+
const subdomain = h.includeSubDomains !== false;
|
|
475
|
+
let value = `max-age=${maxAge}`;
|
|
476
|
+
if (subdomain)
|
|
477
|
+
value += '; includeSubDomains';
|
|
478
|
+
if (h.preload)
|
|
479
|
+
value += '; preload';
|
|
480
|
+
headers.push(['Strict-Transport-Security', value]);
|
|
481
|
+
}
|
|
482
|
+
// X-Frame-Options
|
|
483
|
+
const fo = opts?.frameOptions;
|
|
484
|
+
if (fo !== false) {
|
|
485
|
+
headers.push(['X-Frame-Options', fo ?? 'SAMEORIGIN']);
|
|
486
|
+
}
|
|
487
|
+
// X-Content-Type-Options
|
|
488
|
+
if (opts?.contentTypeOptions !== false) {
|
|
489
|
+
headers.push(['X-Content-Type-Options', 'nosniff']);
|
|
490
|
+
}
|
|
491
|
+
// Referrer-Policy
|
|
492
|
+
const rp = opts?.referrerPolicy;
|
|
493
|
+
if (rp !== false) {
|
|
494
|
+
headers.push(['Referrer-Policy', typeof rp === 'string' ? rp : 'strict-origin-when-cross-origin']);
|
|
495
|
+
}
|
|
496
|
+
// Permissions-Policy
|
|
497
|
+
const pp = opts?.permissionsPolicy;
|
|
498
|
+
if (pp !== false) {
|
|
499
|
+
headers.push(['Permissions-Policy', typeof pp === 'string' ? pp : 'geolocation=(), microphone=(), camera=()']);
|
|
500
|
+
}
|
|
501
|
+
// X-XSS-Protection
|
|
502
|
+
const xxp = opts?.xssProtection;
|
|
503
|
+
if (xxp !== false) {
|
|
504
|
+
headers.push(['X-XSS-Protection', typeof xxp === 'string' ? xxp : '0']);
|
|
505
|
+
}
|
|
506
|
+
return (_req, res, next) => {
|
|
507
|
+
for (const [name, value] of headers)
|
|
508
|
+
res.setHeader(name, value);
|
|
509
|
+
next();
|
|
510
|
+
};
|
|
511
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{ "application/andrew-inset": ["ez"], "application/applixware": ["aw"], "application/atom+xml": ["atom"], "application/atomcat+xml": ["atomcat"], "application/atomsvc+xml": ["atomsvc"], "application/bdoc": ["bdoc"], "application/ccxml+xml": ["ccxml"], "application/cdmi-capability": ["cdmia"], "application/cdmi-container": ["cdmic"], "application/cdmi-domain": ["cdmid"], "application/cdmi-object": ["cdmio"], "application/cdmi-queue": ["cdmiq"], "application/cu-seeme": ["cu"], "application/dash+xml": ["mpd"], "application/davmount+xml": ["davmount"], "application/docbook+xml": ["dbk"], "application/dssc+der": ["dssc"], "application/dssc+xml": ["xdssc"], "application/ecmascript": ["ecma"], "application/emma+xml": ["emma"], "application/epub+zip": ["epub"], "application/exi": ["exi"], "application/font-tdpfr": ["pfr"], "application/font-woff": [], "application/font-woff2": [], "application/geo+json": ["geojson"], "application/gml+xml": ["gml"], "application/gpx+xml": ["gpx"], "application/gxf": ["gxf"], "application/gzip": ["gz"], "application/hyperstudio": ["stk"], "application/inkml+xml": ["ink", "inkml"], "application/ipfix": ["ipfix"], "application/java-archive": ["jar", "war", "ear"], "application/java-serialized-object": ["ser"], "application/java-vm": ["class"], "application/javascript": ["js", "mjs"], "application/json": ["json", "map"], "application/json5": ["json5"], "application/jsonml+json": ["jsonml"], "application/ld+json": ["jsonld"], "application/lost+xml": ["lostxml"], "application/mac-binhex40": ["hqx"], "application/mac-compactpro": ["cpt"], "application/mads+xml": ["mads"], "application/manifest+json": ["webmanifest"], "application/marc": ["mrc"], "application/marcxml+xml": ["mrcx"], "application/mathematica": ["ma", "nb", "mb"], "application/mathml+xml": ["mathml"], "application/mbox": ["mbox"], "application/mediaservercontrol+xml": ["mscml"], "application/metalink+xml": ["metalink"], "application/metalink4+xml": ["meta4"], "application/mets+xml": ["mets"], "application/mods+xml": ["mods"], "application/mp21": ["m21", "mp21"], "application/mp4": ["mp4s", "m4p"], "application/msword": ["doc", "dot"], "application/mxf": ["mxf"], "application/octet-stream": ["bin", "dms", "lrf", "mar", "so", "dist", "distz", "pkg", "bpk", "dump", "elc", "deploy", "exe", "dll", "deb", "dmg", "iso", "img", "msi", "msp", "msm", "buffer"], "application/oda": ["oda"], "application/oebps-package+xml": ["opf"], "application/ogg": ["ogx"], "application/omdoc+xml": ["omdoc"], "application/onenote": ["onetoc", "onetoc2", "onetmp", "onepkg"], "application/oxps": ["oxps"], "application/patch-ops-error+xml": ["xer"], "application/pdf": ["pdf"], "application/pgp-encrypted": ["pgp"], "application/pgp-signature": ["asc", "sig"], "application/pics-rules": ["prf"], "application/pkcs10": ["p10"], "application/pkcs7-mime": ["p7m", "p7c"], "application/pkcs7-signature": ["p7s"], "application/pkcs8": ["p8"], "application/pkix-attr-cert": ["ac"], "application/pkix-cert": ["cer"], "application/pkix-crl": ["crl"], "application/pkix-pkipath": ["pkipath"], "application/pkixcmp": ["pki"], "application/pls+xml": ["pls"], "application/postscript": ["ai", "eps", "ps"], "application/prs.cww": ["cww"], "application/pskc+xml": ["pskcxml"], "application/raml+yaml": ["raml"], "application/rdf+xml": ["rdf"], "application/reginfo+xml": ["rif"], "application/relax-ng-compact-syntax": ["rnc"], "application/resource-lists+xml": ["rl"], "application/resource-lists-diff+xml": ["rld"], "application/rls-services+xml": ["rs"], "application/rpki-ghostbusters": ["gbr"], "application/rpki-manifest": ["mft"], "application/rpki-roa": ["roa"], "application/rsd+xml": ["rsd"], "application/rss+xml": ["rss"], "application/rtf": ["rtf"], "application/sbml+xml": ["sbml"], "application/scvp-cv-request": ["scq"], "application/scvp-cv-response": ["scs"], "application/scvp-vp-request": ["spq"], "application/scvp-vp-response": ["spp"], "application/sdp": ["sdp"], "application/set-payment-initiation": ["setpay"], "application/set-registration-initiation": ["setreg"], "application/shf+xml": ["shf"], "application/smil+xml": ["smi", "smil"], "application/sparql-query": ["rq"], "application/sparql-results+xml": ["srx"], "application/srgs": ["gram"], "application/srgs+xml": ["grxml"], "application/sru+xml": ["sru"], "application/ssdl+xml": ["ssdl"], "application/ssml+xml": ["ssml"], "application/tei+xml": ["tei", "teicorpus"], "application/thraud+xml": ["tfi"], "application/timestamped-data": ["tsd"], "application/vnd.3gpp.pic-bw-large": ["plb"], "application/vnd.3gpp.pic-bw-small": ["psb"], "application/vnd.3gpp.pic-bw-var": ["pvb"], "application/vnd.3gpp2.tcap": ["tcap"], "application/vnd.3m.post-it-notes": ["pwn"], "application/vnd.accpac.simply.aso": ["aso"], "application/vnd.accpac.simply.imp": ["imp"], "application/vnd.acucobol": ["acu"], "application/vnd.acucorp": ["atc", "acutc"], "application/vnd.adobe.air-application-installer-package+zip": ["air"], "application/vnd.adobe.formscentral.fcdt": ["fcdt"], "application/vnd.adobe.fxp": ["fxp", "fxpl"], "application/vnd.adobe.xdp+xml": ["xdp"], "application/vnd.adobe.xfdf": ["xfdf"], "application/vnd.ahead.space": ["ahead"], "application/vnd.airzip.filesecure.azf": ["azf"], "application/vnd.airzip.filesecure.azs": ["azs"], "application/vnd.amazon.ebook": ["azw"], "application/vnd.americandynamics.acc": ["acc"], "application/vnd.amiga.ami": ["ami"], "application/vnd.android.package-archive": ["apk"], "application/vnd.anser-web-certificate-issue-initiation": ["cii"], "application/vnd.anser-web-funds-transfer-initiation": ["fti"], "application/vnd.antix.game-component": ["atx"], "application/vnd.apple.installer+xml": ["mpkg"], "application/vnd.apple.mpegurl": ["m3u8"], "application/vnd.apple.pkpass": ["pkpass"], "application/vnd.aristanetworks.swi": ["swi"], "application/vnd.astraea-software.iota": ["iota"], "application/vnd.audiograph": ["aep"], "application/vnd.blueice.multipass": ["mpm"], "application/vnd.bmi": ["bmi"], "application/vnd.businessobjects": ["rep"], "application/vnd.chemdraw+xml": ["cdxml"], "application/vnd.chipnuts.karaoke-mmd": ["mmd"], "application/vnd.cinderella": ["cdy"], "application/vnd.claymore": ["cla"], "application/vnd.cloanto.rp9": ["rp9"], "application/vnd.clonk.c4group": ["c4g", "c4d", "c4f", "c4p", "c4u"], "application/vnd.cluetrust.cartomobile-config": ["c11amc"], "application/vnd.cluetrust.cartomobile-config-pkg": ["c11amz"], "application/vnd.commonspace": ["csp"], "application/vnd.contact.cmsg": ["cdbcmsg"], "application/vnd.cosmocaller": ["cmc"], "application/vnd.crick.clicker": ["clkx"], "application/vnd.crick.clicker.keyboard": ["clkk"], "application/vnd.crick.clicker.palette": ["clkp"], "application/vnd.crick.clicker.template": ["clkt"], "application/vnd.crick.clicker.wordbank": ["clkw"], "application/vnd.criticaltools.wbs+xml": ["wbs"], "application/vnd.ctc-posml": ["pml"], "application/vnd.cups-ppd": ["ppd"], "application/vnd.curl.car": ["car"], "application/vnd.curl.pcurl": ["pcurl"], "application/vnd.dart": ["dart"], "application/vnd.data-vision.rdz": ["rdz"], "application/vnd.dece.data": ["uvf", "uvvf", "uvd", "uvvd"], "application/vnd.dece.ttml+xml": ["uvt", "uvvt"], "application/vnd.dece.unspecified": ["uvx", "uvvx"], "application/vnd.dece.zip": ["uvz", "uvvz"], "application/vnd.denovo.fcselayout-link": ["fe_launch"], "application/vnd.dna": ["dna"], "application/vnd.dolby.mlp": ["mlp"], "application/vnd.dpgraph": ["dpg"], "application/vnd.dreamfactory": ["dfac"], "application/vnd.ds-keypoint": ["kpxx"], "application/vnd.dvb.ait": ["ait"], "application/vnd.dvb.service": ["svc"], "application/vnd.dynageo": ["geo"], "application/vnd.ecowin.chart": ["mag"], "application/vnd.enliven": ["nml"], "application/vnd.epson.esf": ["esf"], "application/vnd.epson.msf": ["msf"], "application/vnd.epson.quickanime": ["qam"], "application/vnd.epson.salt": ["slt"], "application/vnd.epson.ssf": ["ssf"], "application/vnd.eszigno3+xml": ["es3", "et3"], "application/vnd.ezpix-album": ["ez2"], "application/vnd.ezpix-package": ["ez3"], "application/vnd.fdf": ["fdf"], "application/vnd.fdsn.mseed": ["mseed"], "application/vnd.fdsn.seed": ["seed", "dataless"], "application/vnd.flographit": ["gph"], "application/vnd.fluxtime.clip": ["ftc"], "application/vnd.framemaker": ["fm", "frame", "maker", "book"], "application/vnd.frogans.fnc": ["fnc"], "application/vnd.frogans.ltf": ["ltf"], "application/vnd.fsc.weblaunch": ["fsc"], "application/vnd.fujitsu.oasys": ["oas"], "application/vnd.fujitsu.oasys2": ["oa2"], "application/vnd.fujitsu.oasys3": ["oa3"], "application/vnd.fujitsu.oasysgp": ["fg5"], "application/vnd.fujitsu.oasysprs": ["bh2"], "application/vnd.fujixerox.ddd": ["ddd"], "application/vnd.fujixerox.docuworks": ["xdw"], "application/vnd.fujixerox.docuworks.binder": ["xbd"], "application/vnd.fuzzysheet": ["fzs"], "application/vnd.genomatix.tuxedo": ["txd"], "application/vnd.geogebra.file": ["ggb"], "application/vnd.geogebra.tool": ["ggt"], "application/vnd.geometry-explorer": ["gex", "gre"], "application/vnd.geonext": ["gxt"], "application/vnd.geoplan": ["g2w"], "application/vnd.geospace": ["g3w"], "application/vnd.gmx": ["gmx"], "application/vnd.google-apps.document": ["gdoc"], "application/vnd.google-apps.presentation": ["gslides"], "application/vnd.google-apps.spreadsheet": ["gsheet"], "application/vnd.google-earth.kml+xml": ["kml"], "application/vnd.google-earth.kmz": ["kmz"], "application/vnd.grafeq": ["gqf", "gqs"], "application/vnd.groove-account": ["gac"], "application/vnd.groove-help": ["ghf"], "application/vnd.groove-identity-message": ["gim"], "application/vnd.groove-injector": ["grv"], "application/vnd.groove-tool-message": ["gtm"], "application/vnd.groove-tool-template": ["tpl"], "application/vnd.groove-vcard": ["vcg"], "application/vnd.hal+xml": ["hal"], "application/vnd.handheld-entertainment+xml": ["zmm"], "application/vnd.hbci": ["hbci"], "application/vnd.hhe.lesson-player": ["les"], "application/vnd.hp-hpgl": ["hpgl"], "application/vnd.hp-hpid": ["hpid"], "application/vnd.hp-hps": ["hps"], "application/vnd.hp-jlyt": ["jlt"], "application/vnd.hp-pcl": ["pcl"], "application/vnd.hp-pclxl": ["pclxl"], "application/vnd.hydrostatix.sof-data": ["sfd-hdstx"], "application/vnd.ibm.minipay": ["mpy"], "application/vnd.ibm.modcap": ["afp", "listafp", "list3820"], "application/vnd.ibm.rights-management": ["irm"], "application/vnd.ibm.secure-container": ["sc"], "application/vnd.iccprofile": ["icc", "icm"], "application/vnd.igloader": ["igl"], "application/vnd.immervision-ivp": ["ivp"], "application/vnd.immervision-ivu": ["ivu"], "application/vnd.insors.igm": ["igm"], "application/vnd.intercon.formnet": ["xpw", "xpx"], "application/vnd.intergeo": ["i2g"], "application/vnd.intu.qbo": ["qbo"], "application/vnd.intu.qfx": ["qfx"], "application/vnd.ipunplugged.rcprofile": ["rcprofile"], "application/vnd.irepository.package+xml": ["irp"], "application/vnd.is-xpr": ["xpr"], "application/vnd.isac.fcs": ["fcs"], "application/vnd.jam": ["jam"], "application/vnd.jcp.javame.midlet-rms": ["rms"], "application/vnd.jisp": ["jisp"], "application/vnd.joost.joda-archive": ["joda"], "application/vnd.kahootz": ["ktz", "ktr"], "application/vnd.kde.karbon": ["karbon"], "application/vnd.kde.kchart": ["chrt"], "application/vnd.kde.kformula": ["kfo"], "application/vnd.kde.kivio": ["flw"], "application/vnd.kde.kontour": ["kon"], "application/vnd.kde.kpresenter": ["kpr", "kpt"], "application/vnd.kde.kspread": ["ksp"], "application/vnd.kde.kword": ["kwd", "kwt"], "application/vnd.kenameaapp": ["htke"], "application/vnd.kidspiration": ["kia"], "application/vnd.kinar": ["kne", "knp"], "application/vnd.koan": ["skp", "skd", "skt", "skm"], "application/vnd.kodak-descriptor": ["sse"], "application/vnd.las.las+xml": ["lasxml"], "application/vnd.llamagraphics.life-balance.desktop": ["lbd"], "application/vnd.llamagraphics.life-balance.exchange+xml": ["lbe"], "application/vnd.lotus-1-2-3": ["123"], "application/vnd.lotus-approach": ["apr"], "application/vnd.lotus-freelance": ["pre"], "application/vnd.lotus-notes": ["nsf"], "application/vnd.lotus-organizer": ["org"], "application/vnd.lotus-screencam": ["scm"], "application/vnd.lotus-wordpro": ["lwp"], "application/vnd.macports.portpkg": ["portpkg"], "application/vnd.mcd": ["mcd"], "application/vnd.medcalcdata": ["mc1"], "application/vnd.mediastation.cdkey": ["cdkey"], "application/vnd.mfer": ["mwf"], "application/vnd.mfmp": ["mfm"], "application/vnd.micrografx.flo": ["flo"], "application/vnd.micrografx.igx": ["igx"], "application/vnd.mif": ["mif"], "application/vnd.mobius.daf": ["daf"], "application/vnd.mobius.dis": ["dis"], "application/vnd.mobius.mbk": ["mbk"], "application/vnd.mobius.mqy": ["mqy"], "application/vnd.mobius.msl": ["msl"], "application/vnd.mobius.plc": ["plc"], "application/vnd.mobius.txf": ["txf"], "application/vnd.mophun.application": ["mpn"], "application/vnd.mophun.certificate": ["mpc"], "application/vnd.mozilla.xul+xml": ["xul"], "application/vnd.ms-artgalry": ["cil"], "application/vnd.ms-cab-compressed": ["cab"], "application/vnd.ms-excel": ["xls", "xlm", "xla", "xlc", "xlt", "xlw"], "application/vnd.ms-excel.addin.macroenabled.12": ["xlam"], "application/vnd.ms-excel.sheet.binary.macroenabled.12": ["xlsb"], "application/vnd.ms-excel.sheet.macroenabled.12": ["xlsm"], "application/vnd.ms-excel.template.macroenabled.12": ["xltm"], "application/vnd.ms-fontobject": ["eot"], "application/vnd.ms-htmlhelp": ["chm"], "application/vnd.ms-ims": ["ims"], "application/vnd.ms-lrm": ["lrm"], "application/vnd.ms-officetheme": ["thmx"], "application/vnd.ms-outlook": ["msg"], "application/vnd.ms-pki.seccat": ["cat"], "application/vnd.ms-pki.stl": ["stl"], "application/vnd.ms-powerpoint": ["ppt", "pps", "pot"], "application/vnd.ms-powerpoint.addin.macroenabled.12": ["ppam"], "application/vnd.ms-powerpoint.presentation.macroenabled.12": ["pptm"], "application/vnd.ms-powerpoint.slide.macroenabled.12": ["sldm"], "application/vnd.ms-powerpoint.slideshow.macroenabled.12": ["ppsm"], "application/vnd.ms-powerpoint.template.macroenabled.12": ["potm"], "application/vnd.ms-project": ["mpp", "mpt"], "application/vnd.ms-word.document.macroenabled.12": ["docm"], "application/vnd.ms-word.template.macroenabled.12": ["dotm"], "application/vnd.ms-works": ["wps", "wks", "wcm", "wdb"], "application/vnd.ms-wpl": ["wpl"], "application/vnd.ms-xpsdocument": ["xps"], "application/vnd.mseq": ["mseq"], "application/vnd.musician": ["mus"], "application/vnd.muvee.style": ["msty"], "application/vnd.mynfc": ["taglet"], "application/vnd.neurolanguage.nlu": ["nlu"], "application/vnd.nitf": ["ntf", "nitf"], "application/vnd.noblenet-directory": ["nnd"], "application/vnd.noblenet-sealer": ["nns"], "application/vnd.noblenet-web": ["nnw"], "application/vnd.nokia.n-gage.data": ["ngdat"], "application/vnd.nokia.n-gage.symbian.install": ["n-gage"], "application/vnd.nokia.radio-preset": ["rpst"], "application/vnd.nokia.radio-presets": ["rpss"], "application/vnd.novadigm.edm": ["edm"], "application/vnd.novadigm.edx": ["edx"], "application/vnd.novadigm.ext": ["ext"], "application/vnd.oasis.opendocument.chart": ["odc"], "application/vnd.oasis.opendocument.chart-template": ["otc"], "application/vnd.oasis.opendocument.database": ["odb"], "application/vnd.oasis.opendocument.formula": ["odf"], "application/vnd.oasis.opendocument.formula-template": ["odft"], "application/vnd.oasis.opendocument.graphics": ["odg"], "application/vnd.oasis.opendocument.graphics-template": ["otg"], "application/vnd.oasis.opendocument.image": ["odi"], "application/vnd.oasis.opendocument.image-template": ["oti"], "application/vnd.oasis.opendocument.presentation": ["odp"], "application/vnd.oasis.opendocument.presentation-template": ["otp"], "application/vnd.oasis.opendocument.spreadsheet": ["ods"], "application/vnd.oasis.opendocument.spreadsheet-template": ["ots"], "application/vnd.oasis.opendocument.text": ["odt"], "application/vnd.oasis.opendocument.text-master": ["odm"], "application/vnd.oasis.opendocument.text-template": ["ott"], "application/vnd.oasis.opendocument.text-web": ["oth"], "application/vnd.olpc-sugar": ["xo"], "application/vnd.oma.dd2+xml": ["dd2"], "application/vnd.openofficeorg.extension": ["oxt"], "application/vnd.openxmlformats-officedocument.presentationml.presentation": ["pptx"], "application/vnd.openxmlformats-officedocument.presentationml.slide": ["sldx"], "application/vnd.openxmlformats-officedocument.presentationml.slideshow": ["ppsx"], "application/vnd.openxmlformats-officedocument.presentationml.template": ["potx"], "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": ["xlsx"], "application/vnd.openxmlformats-officedocument.spreadsheetml.template": ["xltx"], "application/vnd.openxmlformats-officedocument.wordprocessingml.document": ["docx"], "application/vnd.openxmlformats-officedocument.wordprocessingml.template": ["dotx"], "application/vnd.osgeo.mapguide.package": ["mgp"], "application/vnd.osgi.dp": ["dp"], "application/vnd.osgi.subsystem": ["esa"], "application/vnd.palm": ["pdb", "pqa", "oprc"], "application/vnd.pawaafile": ["paw"], "application/vnd.pg.format": ["str"], "application/vnd.pg.osasli": ["ei6"], "application/vnd.picsel": ["efif"], "application/vnd.pmi.widget": ["wg"], "application/vnd.pocketlearn": ["plf"], "application/vnd.powerbuilder6": ["pbd"], "application/vnd.previewsystems.box": ["box"], "application/vnd.proteus.magazine": ["mgz"], "application/vnd.publishare-delta-tree": ["qps"], "application/vnd.pvi.ptid1": ["ptid"], "application/vnd.quark.quarkxpress": ["qxd", "qxt", "qwd", "qwt", "qxl", "qxb"], "application/vnd.realvnc.bed": ["bed"], "application/vnd.recordare.musicxml": ["mxl"], "application/vnd.recordare.musicxml+xml": ["musicxml"], "application/vnd.rig.cryptonote": ["cryptonote"], "application/vnd.rim.cod": ["cod"], "application/vnd.rn-realmedia": ["rm"], "application/vnd.rn-realmedia-vbr": ["rmvb"], "application/vnd.route66.link66+xml": ["link66"], "application/vnd.sailingtracker.track": ["st"], "application/vnd.seemail": ["see"], "application/vnd.sema": ["sema"], "application/vnd.semd": ["semd"], "application/vnd.semf": ["semf"], "application/vnd.shana.informed.formdata": ["ifm"], "application/vnd.shana.informed.formtemplate": ["itp"], "application/vnd.shana.informed.interchange": ["iif"], "application/vnd.shana.informed.package": ["ipk"], "application/vnd.simtech-mindmapper": ["twd", "twds"], "application/vnd.smaf": ["mmf"], "application/vnd.smart.teacher": ["teacher"], "application/vnd.solent.sdkm+xml": ["sdkm", "sdkd"], "application/vnd.spotfire.dxp": ["dxp"], "application/vnd.spotfire.sfs": ["sfs"], "application/vnd.stardivision.calc": ["sdc"], "application/vnd.stardivision.draw": ["sda"], "application/vnd.stardivision.impress": ["sdd"], "application/vnd.stardivision.math": ["smf"], "application/vnd.stardivision.writer": ["sdw", "vor"], "application/vnd.stardivision.writer-global": ["sgl"], "application/vnd.stepmania.package": ["smzip"], "application/vnd.stepmania.stepchart": ["sm"], "application/vnd.sun.wadl+xml": ["wadl"], "application/vnd.sun.xml.calc": ["sxc"], "application/vnd.sun.xml.calc.template": ["stc"], "application/vnd.sun.xml.draw": ["sxd"], "application/vnd.sun.xml.draw.template": ["std"], "application/vnd.sun.xml.impress": ["sxi"], "application/vnd.sun.xml.impress.template": ["sti"], "application/vnd.sun.xml.math": ["sxm"], "application/vnd.sun.xml.writer": ["sxw"], "application/vnd.sun.xml.writer.global": ["sxg"], "application/vnd.sun.xml.writer.template": ["stw"], "application/vnd.sus-calendar": ["sus", "susp"], "application/vnd.svd": ["svd"], "application/vnd.symbian.install": ["sis", "sisx"], "application/vnd.syncml+xml": ["xsm"], "application/vnd.syncml.dm+wbxml": ["bdm"], "application/vnd.syncml.dm+xml": ["xdm"], "application/vnd.tao.intent-module-archive": ["tao"], "application/vnd.tcpdump.pcap": ["pcap", "cap", "dmp"], "application/vnd.tmobile-livetv": ["tmo"], "application/vnd.trid.tpt": ["tpt"], "application/vnd.triscape.mxs": ["mxs"], "application/vnd.trueapp": ["tra"], "application/vnd.ufdl": ["ufd", "ufdl"], "application/vnd.uiq.theme": ["utz"], "application/vnd.umajin": ["umj"], "application/vnd.unity": ["unityweb"], "application/vnd.uoml+xml": ["uoml"], "application/vnd.vcx": ["vcx"], "application/vnd.visio": ["vsd", "vst", "vss", "vsw"], "application/vnd.visionary": ["vis"], "application/vnd.vsf": ["vsf"], "application/vnd.wap.wbxml": ["wbxml"], "application/vnd.wap.wmlc": ["wmlc"], "application/vnd.wap.wmlscriptc": ["wmlsc"], "application/vnd.webturbo": ["wtb"], "application/vnd.wolfram.player": ["nbp"], "application/vnd.wordperfect": ["wpd"], "application/vnd.wqd": ["wqd"], "application/vnd.wt.stf": ["stf"], "application/vnd.xara": ["xar"], "application/vnd.xfdl": ["xfdl"], "application/vnd.yamaha.hv-dic": ["hvd"], "application/vnd.yamaha.hv-script": ["hvs"], "application/vnd.yamaha.hv-voice": ["hvp"], "application/vnd.yamaha.openscoreformat": ["osf"], "application/vnd.yamaha.openscoreformat.osfpvg+xml": ["osfpvg"], "application/vnd.yamaha.smaf-audio": ["saf"], "application/vnd.yamaha.smaf-phrase": ["spf"], "application/vnd.yellowriver-custom-menu": ["cmp"], "application/vnd.zul": ["zir", "zirz"], "application/vnd.zzazz.deck+xml": ["zaz"], "application/voicexml+xml": ["vxml"], "application/wasm": ["wasm"], "application/widget": ["wgt"], "application/winhlp": ["hlp"], "application/wsdl+xml": ["wsdl"], "application/wspolicy+xml": ["wspolicy"], "application/x-7z-compressed": ["7z"], "application/x-abiword": ["abw"], "application/x-ace-compressed": ["ace"], "application/x-apple-diskimage": [], "application/x-arj": ["arj"], "application/x-authorware-bin": ["aab", "x32", "u32", "vox"], "application/x-authorware-map": ["aam"], "application/x-authorware-seg": ["aas"], "application/x-bcpio": ["bcpio"], "application/x-bdoc": [], "application/x-bittorrent": ["torrent"], "application/x-blorb": ["blb", "blorb"], "application/x-bzip": ["bz"], "application/x-bzip2": ["bz2", "boz"], "application/x-cbr": ["cbr", "cba", "cbt", "cbz", "cb7"], "application/x-cdlink": ["vcd"], "application/x-cfs-compressed": ["cfs"], "application/x-chat": ["chat"], "application/x-chess-pgn": ["pgn"], "application/x-chrome-extension": ["crx"], "application/x-cocoa": ["cco"], "application/x-conference": ["nsc"], "application/x-cpio": ["cpio"], "application/x-csh": ["csh"], "application/x-debian-package": ["udeb"], "application/x-dgc-compressed": ["dgc"], "application/x-director": ["dir", "dcr", "dxr", "cst", "cct", "cxt", "w3d", "fgd", "swa"], "application/x-doom": ["wad"], "application/x-dtbncx+xml": ["ncx"], "application/x-dtbook+xml": ["dtb"], "application/x-dtbresource+xml": ["res"], "application/x-dvi": ["dvi"], "application/x-envoy": ["evy"], "application/x-eva": ["eva"], "application/x-font-bdf": ["bdf"], "application/x-font-ghostscript": ["gsf"], "application/x-font-linux-psf": ["psf"], "application/x-font-pcf": ["pcf"], "application/x-font-snf": ["snf"], "application/x-font-type1": ["pfa", "pfb", "pfm", "afm"], "application/x-freearc": ["arc"], "application/x-futuresplash": ["spl"], "application/x-gca-compressed": ["gca"], "application/x-glulx": ["ulx"], "application/x-gnumeric": ["gnumeric"], "application/x-gramps-xml": ["gramps"], "application/x-gtar": ["gtar"], "application/x-hdf": ["hdf"], "application/x-httpd-php": ["php"], "application/x-install-instructions": ["install"], "application/x-iso9660-image": [], "application/x-java-archive-diff": ["jardiff"], "application/x-java-jnlp-file": ["jnlp"], "application/x-latex": ["latex"], "application/x-lua-bytecode": ["luac"], "application/x-lzh-compressed": ["lzh", "lha"], "application/x-makeself": ["run"], "application/x-mie": ["mie"], "application/x-mobipocket-ebook": ["prc", "mobi"], "application/x-ms-application": ["application"], "application/x-ms-shortcut": ["lnk"], "application/x-ms-wmd": ["wmd"], "application/x-ms-wmz": ["wmz"], "application/x-ms-xbap": ["xbap"], "application/x-msaccess": ["mdb"], "application/x-msbinder": ["obd"], "application/x-mscardfile": ["crd"], "application/x-msclip": ["clp"], "application/x-msdos-program": [], "application/x-msdownload": ["com", "bat"], "application/x-msmediaview": ["mvb", "m13", "m14"], "application/x-msmetafile": ["wmf", "emf", "emz"], "application/x-msmoney": ["mny"], "application/x-mspublisher": ["pub"], "application/x-msschedule": ["scd"], "application/x-msterminal": ["trm"], "application/x-mswrite": ["wri"], "application/x-netcdf": ["nc", "cdf"], "application/x-ns-proxy-autoconfig": ["pac"], "application/x-nzb": ["nzb"], "application/x-perl": ["pl", "pm"], "application/x-pilot": [], "application/x-pkcs12": ["p12", "pfx"], "application/x-pkcs7-certificates": ["p7b", "spc"], "application/x-pkcs7-certreqresp": ["p7r"], "application/x-rar-compressed": ["rar"], "application/x-redhat-package-manager": ["rpm"], "application/x-research-info-systems": ["ris"], "application/x-sea": ["sea"], "application/x-sh": ["sh"], "application/x-shar": ["shar"], "application/x-shockwave-flash": ["swf"], "application/x-silverlight-app": ["xap"], "application/x-sql": ["sql"], "application/x-stuffit": ["sit"], "application/x-stuffitx": ["sitx"], "application/x-subrip": ["srt"], "application/x-sv4cpio": ["sv4cpio"], "application/x-sv4crc": ["sv4crc"], "application/x-t3vm-image": ["t3"], "application/x-tads": ["gam"], "application/x-tar": ["tar"], "application/x-tcl": ["tcl", "tk"], "application/x-tex": ["tex"], "application/x-tex-tfm": ["tfm"], "application/x-texinfo": ["texinfo", "texi"], "application/x-tgif": ["obj"], "application/x-ustar": ["ustar"], "application/x-virtualbox-hdd": ["hdd"], "application/x-virtualbox-ova": ["ova"], "application/x-virtualbox-ovf": ["ovf"], "application/x-virtualbox-vbox": ["vbox"], "application/x-virtualbox-vbox-extpack": ["vbox-extpack"], "application/x-virtualbox-vdi": ["vdi"], "application/x-virtualbox-vhd": ["vhd"], "application/x-virtualbox-vmdk": ["vmdk"], "application/x-wais-source": ["src"], "application/x-web-app-manifest+json": ["webapp"], "application/x-x509-ca-cert": ["der", "crt", "pem"], "application/x-xfig": ["fig"], "application/x-xliff+xml": ["xlf"], "application/x-xpinstall": ["xpi"], "application/x-xz": ["xz"], "application/x-zmachine": ["z1", "z2", "z3", "z4", "z5", "z6", "z7", "z8"], "application/xaml+xml": ["xaml"], "application/xcap-diff+xml": ["xdf"], "application/xenc+xml": ["xenc"], "application/xhtml+xml": ["xhtml", "xht"], "application/xml": ["xml", "xsl", "xsd", "rng"], "application/xml-dtd": ["dtd"], "application/xop+xml": ["xop"], "application/xproc+xml": ["xpl"], "application/xslt+xml": ["xslt"], "application/xspf+xml": ["xspf"], "application/xv+xml": ["mxml", "xhvml", "xvml", "xvm"], "application/yang": ["yang"], "application/yin+xml": ["yin"], "application/zip": ["zip"], "audio/3gpp": [], "audio/adpcm": ["adp"], "audio/basic": ["au", "snd"], "audio/midi": ["mid", "midi", "kar", "rmi"], "audio/mp3": [], "audio/mp4": ["m4a", "mp4a"], "audio/mpeg": ["mpga", "mp2", "mp2a", "mp3", "m2a", "m3a"], "audio/ogg": ["oga", "ogg", "spx"], "audio/s3m": ["s3m"], "audio/silk": ["sil"], "audio/vnd.dece.audio": ["uva", "uvva"], "audio/vnd.digital-winds": ["eol"], "audio/vnd.dra": ["dra"], "audio/vnd.dts": ["dts"], "audio/vnd.dts.hd": ["dtshd"], "audio/vnd.lucent.voice": ["lvp"], "audio/vnd.ms-playready.media.pya": ["pya"], "audio/vnd.nuera.ecelp4800": ["ecelp4800"], "audio/vnd.nuera.ecelp7470": ["ecelp7470"], "audio/vnd.nuera.ecelp9600": ["ecelp9600"], "audio/vnd.rip": ["rip"], "audio/wav": ["wav"], "audio/wave": [], "audio/webm": ["weba"], "audio/x-aac": ["aac"], "audio/x-aiff": ["aif", "aiff", "aifc"], "audio/x-caf": ["caf"], "audio/x-flac": ["flac"], "audio/x-m4a": [], "audio/x-matroska": ["mka"], "audio/x-mpegurl": ["m3u"], "audio/x-ms-wax": ["wax"], "audio/x-ms-wma": ["wma"], "audio/x-pn-realaudio": ["ram", "ra"], "audio/x-pn-realaudio-plugin": ["rmp"], "audio/x-realaudio": [], "audio/x-wav": [], "audio/xm": ["xm"], "chemical/x-cdx": ["cdx"], "chemical/x-cif": ["cif"], "chemical/x-cmdf": ["cmdf"], "chemical/x-cml": ["cml"], "chemical/x-csml": ["csml"], "chemical/x-xyz": ["xyz"], "font/collection": ["ttc"], "font/otf": ["otf"], "font/ttf": ["ttf"], "font/woff": ["woff"], "font/woff2": ["woff2"], "image/apng": ["apng"], "image/bmp": ["bmp"], "image/cgm": ["cgm"], "image/g3fax": ["g3"], "image/gif": ["gif"], "image/ief": ["ief"], "image/jp2": ["jp2", "jpg2"], "image/jpeg": ["jpeg", "jpg", "jpe"], "image/jpm": ["jpm"], "image/jpx": ["jpx", "jpf"], "image/ktx": ["ktx"], "image/png": ["png"], "image/prs.btif": ["btif"], "image/sgi": ["sgi"], "image/svg+xml": ["svg", "svgz"], "image/tiff": ["tiff", "tif"], "image/vnd.adobe.photoshop": ["psd"], "image/vnd.dece.graphic": ["uvi", "uvvi", "uvg", "uvvg"], "image/vnd.djvu": ["djvu", "djv"], "image/vnd.dvb.subtitle": [], "image/vnd.dwg": ["dwg"], "image/vnd.dxf": ["dxf"], "image/vnd.fastbidsheet": ["fbs"], "image/vnd.fpx": ["fpx"], "image/vnd.fst": ["fst"], "image/vnd.fujixerox.edmics-mmr": ["mmr"], "image/vnd.fujixerox.edmics-rlc": ["rlc"], "image/vnd.ms-modi": ["mdi"], "image/vnd.ms-photo": ["wdp"], "image/vnd.net-fpx": ["npx"], "image/vnd.wap.wbmp": ["wbmp"], "image/vnd.xiff": ["xif"], "image/webp": ["webp"], "image/x-3ds": ["3ds"], "image/x-cmu-raster": ["ras"], "image/x-cmx": ["cmx"], "image/x-freehand": ["fh", "fhc", "fh4", "fh5", "fh7"], "image/x-icon": ["ico"], "image/x-jng": ["jng"], "image/x-mrsid-image": ["sid"], "image/x-ms-bmp": [], "image/x-pcx": ["pcx"], "image/x-pict": ["pic", "pct"], "image/x-portable-anymap": ["pnm"], "image/x-portable-bitmap": ["pbm"], "image/x-portable-graymap": ["pgm"], "image/x-portable-pixmap": ["ppm"], "image/x-rgb": ["rgb"], "image/x-tga": ["tga"], "image/x-xbitmap": ["xbm"], "image/x-xpixmap": ["xpm"], "image/x-xwindowdump": ["xwd"], "message/rfc822": ["eml", "mime"], "model/gltf+json": ["gltf"], "model/gltf-binary": ["glb"], "model/iges": ["igs", "iges"], "model/mesh": ["msh", "mesh", "silo"], "model/vnd.collada+xml": ["dae"], "model/vnd.dwf": ["dwf"], "model/vnd.gdl": ["gdl"], "model/vnd.gtw": ["gtw"], "model/vnd.mts": ["mts"], "model/vnd.vtu": ["vtu"], "model/vrml": ["wrl", "vrml"], "model/x3d+binary": ["x3db", "x3dbz"], "model/x3d+vrml": ["x3dv", "x3dvz"], "model/x3d+xml": ["x3d", "x3dz"], "text/cache-manifest": ["appcache", "manifest"], "text/calendar": ["ics", "ifb"], "text/coffeescript": ["coffee", "litcoffee"], "text/css": ["css"], "text/csv": ["csv"], "text/hjson": ["hjson"], "text/html": ["html", "htm", "shtml"], "text/jade": ["jade"], "text/jsx": ["jsx"], "text/less": ["less"], "text/markdown": ["markdown", "md"], "text/mathml": ["mml"], "text/n3": ["n3"], "text/plain": ["txt", "text", "conf", "def", "list", "log", "in", "ini"], "text/prs.lines.tag": ["dsc"], "text/richtext": ["rtx"], "text/rtf": [], "text/sgml": ["sgml", "sgm"], "text/slim": ["slim", "slm"], "text/stylus": ["stylus", "styl"], "text/tab-separated-values": ["tsv"], "text/troff": ["t", "tr", "roff", "man", "me", "ms"], "text/turtle": ["ttl"], "text/uri-list": ["uri", "uris", "urls"], "text/vcard": ["vcard"], "text/vnd.curl": ["curl"], "text/vnd.curl.dcurl": ["dcurl"], "text/vnd.curl.mcurl": ["mcurl"], "text/vnd.curl.scurl": ["scurl"], "text/vnd.dvb.subtitle": ["sub"], "text/vnd.fly": ["fly"], "text/vnd.fmi.flexstor": ["flx"], "text/vnd.graphviz": ["gv"], "text/vnd.in3d.3dml": ["3dml"], "text/vnd.in3d.spot": ["spot"], "text/vnd.sun.j2me.app-descriptor": ["jad"], "text/vnd.wap.wml": ["wml"], "text/vnd.wap.wmlscript": ["wmls"], "text/vtt": ["vtt"], "text/x-asm": ["s", "asm"], "text/x-c": ["c", "cc", "cxx", "cpp", "h", "hh", "dic"], "text/x-component": ["htc"], "text/x-fortran": ["f", "for", "f77", "f90"], "text/x-handlebars-template": ["hbs"], "text/x-java-source": ["java"], "text/x-lua": ["lua"], "text/x-markdown": ["mkd"], "text/x-nfo": ["nfo"], "text/x-opml": ["opml"], "text/x-org": [], "text/x-pascal": ["p", "pas"], "text/x-processing": ["pde"], "text/x-sass": ["sass"], "text/x-scss": ["scss"], "text/x-setext": ["etx"], "text/x-sfv": ["sfv"], "text/x-suse-ymp": ["ymp"], "text/x-uuencode": ["uu"], "text/x-vcalendar": ["vcs"], "text/x-vcard": ["vcf"], "text/xml": [], "text/yaml": ["yaml", "yml"], "video/3gpp": ["3gp", "3gpp"], "video/3gpp2": ["3g2"], "video/h261": ["h261"], "video/h263": ["h263"], "video/h264": ["h264"], "video/jpeg": ["jpgv"], "video/jpm": ["jpgm"], "video/mj2": ["mj2", "mjp2"], "video/mp2t": ["ts"], "video/mp4": ["mp4", "mp4v", "mpg4"], "video/mpeg": ["mpeg", "mpg", "mpe", "m1v", "m2v"], "video/ogg": ["ogv"], "video/quicktime": ["qt", "mov"], "video/vnd.dece.hd": ["uvh", "uvvh"], "video/vnd.dece.mobile": ["uvm", "uvvm"], "video/vnd.dece.pd": ["uvp", "uvvp"], "video/vnd.dece.sd": ["uvs", "uvvs"], "video/vnd.dece.video": ["uvv", "uvvv"], "video/vnd.dvb.file": ["dvb"], "video/vnd.fvt": ["fvt"], "video/vnd.mpegurl": ["mxu", "m4u"], "video/vnd.ms-playready.media.pyv": ["pyv"], "video/vnd.uvvu.mp4": ["uvu", "uvvu"], "video/vnd.vivo": ["viv"], "video/webm": ["webm"], "video/x-f4v": ["f4v"], "video/x-fli": ["fli"], "video/x-flv": ["flv"], "video/x-m4v": ["m4v"], "video/x-matroska": ["mkv", "mk3d", "mks"], "video/x-mng": ["mng"], "video/x-ms-asf": ["asf", "asx"], "video/x-ms-vob": ["vob"], "video/x-ms-wm": ["wm"], "video/x-ms-wmv": ["wmv"], "video/x-ms-wmx": ["wmx"], "video/x-ms-wvx": ["wvx"], "video/x-msvideo": ["avi"], "video/x-sgi-movie": ["movie"], "video/x-smv": ["smv"], "x-conference/x-cooltalk": ["ice"] }
|