dx-server 0.12.2 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +47 -55
  2. package/{cjs → lib}/body.d.ts +2 -3
  3. package/lib/body.js +10 -0
  4. package/{esm → lib}/bodyHelpers.d.ts +2 -4
  5. package/lib/bodyHelpers.js +89 -0
  6. package/{esm → lib}/dx.d.ts +5 -8
  7. package/lib/dx.js +127 -0
  8. package/{cjs → lib}/dxHelpers.d.ts +0 -4
  9. package/{esm → lib}/dxHelpers.js +0 -2
  10. package/{cjs → lib}/index.d.ts +0 -1
  11. package/{esm/index.d.ts → lib/index.js} +0 -1
  12. package/lib/logger.js +56 -0
  13. package/lib/router.d.ts +42 -0
  14. package/lib/router.js +43 -0
  15. package/lib/static.js +22 -0
  16. package/{cjs → lib}/staticHelpers.d.ts +0 -2
  17. package/lib/staticHelpers.js +186 -0
  18. package/{cjs → lib}/stream.d.ts +2 -7
  19. package/lib/stream.js +90 -0
  20. package/{esm → lib}/vendors/contentType.js +0 -1
  21. package/{cjs → lib}/vendors/etag.d.ts +0 -3
  22. package/lib/vendors/etag.js +104 -0
  23. package/{cjs → lib}/vendors/fresh.d.ts +2 -2
  24. package/lib/vendors/fresh.js +95 -0
  25. package/lib/vendors/mime.d.ts +1 -0
  26. package/lib/vendors/mime.js +35 -0
  27. package/{esm → lib}/vendors/mimeDb.js +0 -1
  28. package/{cjs → lib}/vendors/mimeScore.d.ts +1 -1
  29. package/lib/vendors/mimeScore.js +45 -0
  30. package/{cjs → lib}/vendors/onFinished.d.ts +1 -1
  31. package/lib/vendors/onFinished.js +231 -0
  32. package/lib/vendors/rangeParser.d.ts +20 -0
  33. package/lib/vendors/rangeParser.js +121 -0
  34. package/package.json +10 -19
  35. package/cjs/body.js +0 -14
  36. package/cjs/bodyHelpers.d.ts +0 -16
  37. package/cjs/bodyHelpers.js +0 -101
  38. package/cjs/connect.d.ts +0 -5
  39. package/cjs/connect.js +0 -44
  40. package/cjs/dx.d.ts +0 -46
  41. package/cjs/dx.js +0 -144
  42. package/cjs/dxHelpers.js +0 -123
  43. package/cjs/express.d.ts +0 -4
  44. package/cjs/express.js +0 -43
  45. package/cjs/helpers.js +0 -14
  46. package/cjs/index.js +0 -38
  47. package/cjs/logger.js +0 -61
  48. package/cjs/package.json +0 -3
  49. package/cjs/polyfillWithResolvers.d.ts +0 -1
  50. package/cjs/polyfillWithResolvers.js +0 -17
  51. package/cjs/router.js +0 -47
  52. package/cjs/static.js +0 -27
  53. package/cjs/staticHelpers.js +0 -195
  54. package/cjs/stream.js +0 -97
  55. package/cjs/vendors/contentType.js +0 -92
  56. package/cjs/vendors/etag.js +0 -136
  57. package/cjs/vendors/fresh.js +0 -102
  58. package/cjs/vendors/mime.d.ts +0 -1
  59. package/cjs/vendors/mime.js +0 -42
  60. package/cjs/vendors/mimeDb.js +0 -9417
  61. package/cjs/vendors/mimeScore.js +0 -50
  62. package/cjs/vendors/onFinished.js +0 -245
  63. package/cjs/vendors/rangeParser.d.ts +0 -10
  64. package/cjs/vendors/rangeParser.js +0 -126
  65. package/esm/body.d.ts +0 -8
  66. package/esm/body.js +0 -11
  67. package/esm/bodyHelpers.js +0 -90
  68. package/esm/connect.d.ts +0 -5
  69. package/esm/connect.js +0 -40
  70. package/esm/dx.js +0 -128
  71. package/esm/dxHelpers.d.ts +0 -49
  72. package/esm/express.d.ts +0 -4
  73. package/esm/express.js +0 -35
  74. package/esm/helpers.js +0 -3
  75. package/esm/index.js +0 -9
  76. package/esm/logger.d.ts +0 -3
  77. package/esm/logger.js +0 -57
  78. package/esm/polyfillWithResolvers.d.ts +0 -1
  79. package/esm/polyfillWithResolvers.js +0 -16
  80. package/esm/router.js +0 -44
  81. package/esm/static.d.ts +0 -5
  82. package/esm/static.js +0 -23
  83. package/esm/staticHelpers.d.ts +0 -18
  84. package/esm/staticHelpers.js +0 -188
  85. package/esm/stream.d.ts +0 -12
  86. package/esm/stream.js +0 -92
  87. package/esm/vendors/contentType.d.ts +0 -4
  88. package/esm/vendors/etag.d.ts +0 -10
  89. package/esm/vendors/etag.js +0 -105
  90. package/esm/vendors/fresh.d.ts +0 -23
  91. package/esm/vendors/fresh.js +0 -96
  92. package/esm/vendors/mime.d.ts +0 -1
  93. package/esm/vendors/mime.js +0 -35
  94. package/esm/vendors/mimeDb.d.ts +0 -9413
  95. package/esm/vendors/mimeScore.d.ts +0 -5
  96. package/esm/vendors/mimeScore.js +0 -46
  97. package/esm/vendors/onFinished.d.ts +0 -14
  98. package/esm/vendors/onFinished.js +0 -241
  99. package/esm/vendors/rangeParser.d.ts +0 -10
  100. package/esm/vendors/rangeParser.js +0 -122
  101. /package/{cjs → lib}/helpers.d.ts +0 -0
  102. /package/{esm/helpers.d.ts → lib/helpers.js} +0 -0
  103. /package/{cjs → lib}/logger.d.ts +0 -0
  104. /package/{cjs → lib}/static.d.ts +0 -0
  105. /package/{cjs → lib}/vendors/contentType.d.ts +0 -0
  106. /package/{cjs → lib}/vendors/mimeDb.d.ts +0 -0
package/lib/router.js ADDED
@@ -0,0 +1,43 @@
1
+ import { getReq } from './dx.js';
2
+ import { urlFromReq } from './bodyHelpers.js';
3
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods
4
+ const allMethods = [
5
+ 'get', 'head', 'post', 'put', 'delete', 'connect', 'options', 'trace', 'patch'
6
+ ];
7
+ function makeRouter(method, // undefined means any method
8
+ routes, { prefix = '' } = {}) {
9
+ const routeWithUrlPatterns = routes.map(([pattern, handler]) => [new URLPattern({ pathname: `${prefix}${pattern}` }), handler]);
10
+ return next => {
11
+ const req = getReq();
12
+ if (method !== undefined && req.method !== method.toUpperCase())
13
+ return next();
14
+ for (const [urlPattern, handler] of routeWithUrlPatterns) {
15
+ // '' matches nothing
16
+ // '/' matches both https://example.com and https://example.com/
17
+ // '/foo' matches https://example.com/foo but not https://example.com/foo/
18
+ // '/foo/' matches https://example.com/foo/ but not https://example.com/foo
19
+ // path can be '*' for OPTIONS request
20
+ // to test: curl -X OPTIONS --request-target '*' http://localhost:3000 -D -
21
+ // req.url === '*'
22
+ // new URL('*', 'https://example.com').pathname === '/*'
23
+ const matched = urlPattern.exec({ pathname: urlFromReq(req).pathname });
24
+ if (matched)
25
+ return handler({ matched, next });
26
+ }
27
+ return next();
28
+ };
29
+ }
30
+ export const router = {
31
+ method(method, ...params) {
32
+ return typeof params[0] === 'string'
33
+ ? makeRouter(method, [[params[0], params[1]]], params[2])
34
+ : makeRouter(method, Object.entries(params[0]), params[1]);
35
+ },
36
+ all(...params) {
37
+ return typeof params[0] === 'string'
38
+ ? makeRouter(undefined, [[params[0], params[1]]], params[2])
39
+ : makeRouter(undefined, Object.entries(params[0]), params[1]);
40
+ }
41
+ };
42
+ for (const method of allMethods)
43
+ router[method] = router.method.bind(router, method);
package/lib/static.js ADDED
@@ -0,0 +1,22 @@
1
+ import { getReq, getRes } from './dx.js';
2
+ import { sendFileTrusted } from './staticHelpers.js';
3
+ import { urlFromReq } from './bodyHelpers.js';
4
+ export function chainStatic(pattern, { getPathname, ...options }) {
5
+ const urlPattern = new URLPattern({ pathname: pattern });
6
+ return async (next) => {
7
+ const req = getReq();
8
+ if (req.method !== 'GET' && req.method !== 'HEAD')
9
+ return next();
10
+ const { pathname } = urlFromReq(req);
11
+ const matched = urlPattern.exec({ pathname });
12
+ if (!matched)
13
+ return next();
14
+ try {
15
+ await sendFileTrusted(req, getRes(), getPathname?.(matched)
16
+ ?? decodeURIComponent(pathname), options);
17
+ }
18
+ catch (e) {
19
+ return next(e); // if request's pathname matches pattern, but file is not found, next() will be called with error
20
+ }
21
+ };
22
+ }
@@ -1,6 +1,4 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
1
  import { IncomingMessage, ServerResponse } from 'node:http';
3
- import './polyfillWithResolvers.js';
4
2
  export interface SendFileOptions {
5
3
  allowDotfiles?: boolean;
6
4
  root?: string;
@@ -0,0 +1,186 @@
1
+ import path from 'node:path';
2
+ import { stat } from 'node:fs/promises';
3
+ import { entityTagPath, statTag } from './vendors/etag.js';
4
+ import { contentTypeForExtension } from './vendors/mime.js';
5
+ import { fresh, parseHttpDate, parseTokenList } from './vendors/fresh.js';
6
+ import { parseRange } from './vendors/rangeParser.js';
7
+ import { createReadStream } from 'node:fs';
8
+ import { onFinished } from './vendors/onFinished.js';
9
+ import { promisify } from 'node:util';
10
+ const BYTES_RANGE_REGEXP = /^ *bytes=/;
11
+ const UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
12
+ export async function sendFileTrusted(req, res, pathname, // plain path, not URI-encoded
13
+ { root, allowDotfiles, start = 0, end, disableAcceptRanges, disableLastModified, etag = 'strong', disableCacheControl, maxAge = 60 * 60 * 24 * 365 * 1000, // 1 year
14
+ immutable, } = {}) {
15
+ // null byte(s)
16
+ if (pathname.includes('\0'))
17
+ throw new Error('Forbidden');
18
+ let parts;
19
+ if (root) {
20
+ // normalize
21
+ pathname = path.normalize(`.${path.sep}${pathname}`);
22
+ // malicious path
23
+ if (UP_PATH_REGEXP.test(pathname))
24
+ throw new Error('Forbidden');
25
+ // explode path parts
26
+ parts = pathname.split(path.sep);
27
+ // join / normalize from optional root dir
28
+ pathname = path.normalize(path.join(root, pathname));
29
+ }
30
+ else {
31
+ // malicious path
32
+ if (UP_PATH_REGEXP.test(pathname))
33
+ throw new Error('Forbidden');
34
+ // explode path parts
35
+ parts = path.normalize(pathname).split(path.sep);
36
+ // join / normalize from optional root dir
37
+ pathname = path.resolve(pathname);
38
+ }
39
+ // dotfile handling
40
+ if (parts.some(part => part.length > 1 && part[0] === '.') && !allowDotfiles)
41
+ throw new Error('Forbidden: dotfiles are not allowed');
42
+ // pathEndsWithSep
43
+ if (pathname[pathname.length - 1] === path.sep)
44
+ throw new Error('Forbidden: directory access is not allowed');
45
+ const fileStat = await stat(pathname);
46
+ // not found, check extensions
47
+ // if (err.code === 'ENOENT' && !path.extname(pathname) && !pathEndsWithSep) throw err
48
+ // switch (err.code) {
49
+ // case 'ENAMETOOLONG':
50
+ // case 'ENOENT':
51
+ // case 'ENOTDIR':
52
+ // default:
53
+ // }
54
+ if (fileStat.isDirectory())
55
+ throw new Error('Forbidden: directory access is not allowed');
56
+ if (res.headersSent)
57
+ return;
58
+ //region set header fields
59
+ if (!disableAcceptRanges && !res.getHeader('Accept-Ranges'))
60
+ res.setHeader('Accept-Ranges', 'bytes');
61
+ if (!disableCacheControl && !res.getHeader('Cache-Control'))
62
+ res.setHeader('Cache-Control', [
63
+ `public, max-age=${Math.floor(maxAge / 1000)}`,
64
+ immutable && 'immutable',
65
+ ].filter(Boolean).join(', '));
66
+ if (!disableLastModified && !res.getHeader('Last-Modified'))
67
+ res.setHeader('Last-Modified', fileStat.mtime.toUTCString());
68
+ if (etag !== 'disabled' && !res.getHeader('ETag'))
69
+ res.setHeader('ETag', etag === 'weak' ? statTag(fileStat) : await entityTagPath(fileStat, pathname, false));
70
+ //endregion set header fields
71
+ // content-type
72
+ if (!res.getHeader('Content-Type'))
73
+ res.setHeader('Content-Type', contentTypeForExtension(path.extname(pathname).slice(1)) || 'application/octet-stream');
74
+ // conditional GET support
75
+ // isConditionalGET
76
+ if (req.headers['if-match'] || req.headers['if-unmodified-since'] || req.headers['if-none-match'] || req.headers['if-modified-since']) {
77
+ //region isPreconditionFailure
78
+ // if-match
79
+ const match = req.headers['if-match'];
80
+ if (match) {
81
+ const etag = res.getHeader('ETag');
82
+ if (!etag
83
+ || (match !== '*' && parseTokenList(match).every(match => match !== etag && match !== 'W/' + etag && 'W/' + match !== etag)))
84
+ throw new Error('Precondition Failed: request headers do not match the response');
85
+ }
86
+ // if-unmodified-since (ignore when using strong etag since mtime may be unreliable)
87
+ if (etag === 'weak') {
88
+ const unmodifiedSince = parseHttpDate(req.headers['if-unmodified-since']);
89
+ if (!isNaN(unmodifiedSince)) {
90
+ const lastModified = parseHttpDate(res.getHeader('Last-Modified'));
91
+ if (isNaN(lastModified) || lastModified > unmodifiedSince)
92
+ throw new Error('Precondition Failed: resource has been modified since the specified date');
93
+ }
94
+ }
95
+ //endregion isPreconditionFailure
96
+ // isCachable
97
+ if ((res.statusCode >= 200 && res.statusCode < 300 || res.statusCode === 304)
98
+ && fresh(req.headers, {
99
+ etag: res.getHeader('ETag'),
100
+ // Only use last-modified for freshness check when using weak Etag
101
+ 'last-modified': etag === 'weak' ? res.getHeader('Last-Modified') : undefined
102
+ })) {
103
+ // removeContentHeaderFields
104
+ res.removeHeader('Content-Encoding');
105
+ res.removeHeader('Content-Language');
106
+ res.removeHeader('Content-Length');
107
+ res.removeHeader('Content-Range');
108
+ res.removeHeader('Content-Type');
109
+ res.statusCode = 304;
110
+ return void await promisify(res.end.bind(res))();
111
+ }
112
+ }
113
+ // adjust len to start/end options
114
+ let len = fileStat.size;
115
+ len = Math.max(0, len - start);
116
+ if (end !== undefined) {
117
+ const bytes = end - start + 1;
118
+ if (len > bytes)
119
+ len = bytes;
120
+ }
121
+ // Range support
122
+ let ranges = req.headers.range;
123
+ if (!disableAcceptRanges && BYTES_RANGE_REGEXP.test(ranges ?? '')) {
124
+ // parse
125
+ let rangesNum = parseRange(len, ranges, { combine: true });
126
+ // If-Range support
127
+ if (!isRangeFresh(req, res))
128
+ rangesNum = -2;
129
+ // unsatisfiable
130
+ if (rangesNum === -1) {
131
+ // Content-Range
132
+ res.setHeader('Content-Range', contentRange('bytes', len));
133
+ // 416 Requested Range Not Satisfiable
134
+ throw new Error('Requested Range Not Satisfiable: requested range is not satisfiable');
135
+ // return this.error(416, {
136
+ // headers: {'Content-Range': res.getHeader('Content-Range')}
137
+ // })
138
+ }
139
+ // valid (syntactically invalid/multiple ranges are treated as a regular response)
140
+ if (rangesNum !== -2 && rangesNum.length === 1) {
141
+ // Content-Range
142
+ res.statusCode = 206;
143
+ res.setHeader('Content-Range', contentRange('bytes', len, rangesNum[0]));
144
+ // adjust for requested range
145
+ start += rangesNum[0].start;
146
+ len = rangesNum[0].end - rangesNum[0].start + 1;
147
+ }
148
+ }
149
+ // set read options
150
+ end = Math.max(start, start + len - 1);
151
+ // content-length
152
+ res.setHeader('Content-Length', len);
153
+ // HEAD support
154
+ if (req.method === 'HEAD')
155
+ return void await promisify(res.end.bind(res))();
156
+ // Weak ETag path: stream file
157
+ const stream = createReadStream(pathname, { start, end });
158
+ stream.pipe(res);
159
+ const defer = Promise.withResolvers();
160
+ onFinished(res, cleanup);
161
+ stream.on('error', err => {
162
+ cleanup();
163
+ defer.reject(err);
164
+ });
165
+ stream.on('end', defer.resolve);
166
+ function cleanup() {
167
+ stream.destroy();
168
+ }
169
+ return defer.promise;
170
+ }
171
+ function isRangeFresh(req, res) {
172
+ const ifRange = req.headers['if-range'];
173
+ if (!ifRange)
174
+ return true;
175
+ // if-range as etag
176
+ if (ifRange.indexOf('"') !== -1) {
177
+ const etag = res.getHeader('ETag');
178
+ return etag && ifRange.includes(etag);
179
+ }
180
+ // if-range as modified date
181
+ const lastModified = res.getHeader('Last-Modified');
182
+ return parseHttpDate(lastModified) <= parseHttpDate(ifRange);
183
+ }
184
+ function contentRange(type, size, range) {
185
+ return `${type} ${range ? range.start + '-' + range.end : '*'}/${size}`;
186
+ }
@@ -1,12 +1,7 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
- /// <reference types="node" resolution-mode="require"/>
3
- /// <reference types="node" resolution-mode="require"/>
4
- /// <reference types="node" resolution-mode="require"/>
5
1
  import type { IncomingMessage } from 'node:http';
6
2
  import type { Readable } from 'node:stream';
7
- import './polyfillWithResolvers.js';
8
- export declare function getContentStream(req: IncomingMessage, encoding: string, disableInflate?: boolean): IncomingMessage | import("zlib").Gunzip;
3
+ export declare function getContentStream(req: IncomingMessage, encoding: string, disableInflate?: boolean): IncomingMessage | import("node:zlib").Inflate | import("node:zlib").Gunzip | import("node:zlib").BrotliDecompress;
9
4
  export declare function readStream(stream: Readable, { length, limit }: {
10
5
  length?: number;
11
6
  limit?: number;
12
- }): Promise<Buffer>;
7
+ }): Promise<Buffer<ArrayBufferLike>>;
package/lib/stream.js ADDED
@@ -0,0 +1,90 @@
1
+ import { createBrotliDecompress, createGunzip, createInflate } from 'node:zlib';
2
+ // note: there might be multiple encodings applied to the stream
3
+ // we only support one encoding
4
+ export function getContentStream(req, encoding, disableInflate) {
5
+ if (disableInflate && encoding !== 'identity')
6
+ throw new Error(`content-encoding ${encoding} is not supported`);
7
+ switch (encoding) {
8
+ case 'deflate': {
9
+ const stream = createInflate();
10
+ req.pipe(stream);
11
+ return stream;
12
+ }
13
+ case 'gzip': {
14
+ const stream = createGunzip();
15
+ req.pipe(stream);
16
+ return stream;
17
+ }
18
+ case 'br': {
19
+ const stream = createBrotliDecompress();
20
+ req.pipe(stream);
21
+ return stream;
22
+ }
23
+ case 'identity':
24
+ return req;
25
+ default:
26
+ throw new Error(`unsupported content-encoding ${encoding}`);
27
+ }
28
+ }
29
+ // https://github.com/stream-utils/raw-body/blob/191e4b6506dcf77198eed01c8feb4b6817008342/index.js#L155
30
+ export async function readStream(stream, { length, limit }) {
31
+ let completed = false;
32
+ // check the length and limit options.
33
+ // note: we intentionally leave the stream paused,
34
+ // so users should handle the stream themselves.
35
+ if (limit !== undefined && length !== undefined && length > limit)
36
+ throw new Error('request entity too large');
37
+ let received = 0;
38
+ const buffers = [];
39
+ const defer = Promise.withResolvers();
40
+ // attach listeners
41
+ stream.on('aborted', onAborted);
42
+ stream.on('close', onClose);
43
+ stream.on('data', onData);
44
+ stream.on('end', onEnd);
45
+ stream.on('error', onError);
46
+ function done(err, result) {
47
+ if (completed)
48
+ return;
49
+ completed = true;
50
+ onClose();
51
+ if (err) {
52
+ stream.unpipe?.();
53
+ stream.pause?.();
54
+ defer.reject(err);
55
+ }
56
+ else
57
+ defer.resolve(result);
58
+ }
59
+ function onData(chunk) {
60
+ if (completed)
61
+ return;
62
+ received += chunk.length;
63
+ if (limit !== undefined && received > limit) {
64
+ done(new Error('request entity too large'));
65
+ }
66
+ else
67
+ buffers.push(chunk);
68
+ }
69
+ function onError(err) {
70
+ done(err);
71
+ }
72
+ function onEnd() {
73
+ if (length !== undefined && received !== length)
74
+ done(new Error('request size did not match content length'));
75
+ else
76
+ done(undefined, Buffer.concat(buffers));
77
+ }
78
+ function onAborted() {
79
+ done(new Error('request aborted'));
80
+ }
81
+ function onClose() {
82
+ buffers.splice(0, buffers.length);
83
+ stream.off('aborted', onAborted);
84
+ stream.off('data', onData);
85
+ stream.off('end', onEnd);
86
+ stream.off('error', onError);
87
+ stream.off('close', onClose);
88
+ }
89
+ return await defer.promise;
90
+ }
@@ -85,4 +85,3 @@ function formatContentType({ mediaType, parameters }) {
85
85
  ? Object.keys(parameters).sort().map(key => `; ${key}=${qstring(parameters[key])}`).join('')
86
86
  : ''}`;
87
87
  }
88
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29udGVudFR5cGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvdmVuZG9ycy9jb250ZW50VHlwZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFDQSxxR0FBcUc7QUFDckc7Ozs7OztHQU1HO0FBQ0gsTUFBTSxXQUFXLEdBQUcsNERBQTRELENBQUE7QUFDaEY7Ozs7Ozs7Ozs7Ozs7R0FhRztBQUNILE1BQU0sWUFBWSxHQUFHLGtLQUFrSyxDQUFBLENBQUMsdUNBQXVDO0FBQy9OLE1BQU0sV0FBVyxHQUFHLHVDQUF1QyxDQUFBLENBQUMsdUNBQXVDO0FBQ25HLE1BQU0sWUFBWSxHQUFHLCtCQUErQixDQUFBO0FBQ3BEOzs7OztHQUtHO0FBQ0gsTUFBTSxXQUFXLEdBQUcsNEJBQTRCLENBQUEsQ0FBQyx1Q0FBdUM7QUFFeEYsTUFBTSxVQUFVLGdCQUFnQixDQUFFLE1BQWM7SUFDL0MsSUFBSSxLQUFLLEdBQUcsTUFBTSxDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQTtJQUMvQixNQUFNLFNBQVMsR0FBRyxLQUFLLEtBQUssQ0FBQyxDQUFDO1FBQzdCLENBQUMsQ0FBQyxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUMsRUFBRSxLQUFLLENBQUMsQ0FBQyxJQUFJLEVBQUU7UUFDL0IsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxJQUFJLEVBQUUsQ0FBQTtJQUVoQixJQUFJLENBQUMsV0FBVyxDQUFDLElBQUksQ0FBQyxTQUFTLENBQUM7UUFBRSxNQUFNLElBQUksU0FBUyxDQUFDLHVCQUF1QixTQUFTLEVBQUUsQ0FBQyxDQUFBO0lBQ3pGLE1BQU0sVUFBVSxHQUEyQixNQUFNLENBQUMsTUFBTSxDQUFDLElBQUksQ0FBQyxDQUFBO0lBRTlELG1CQUFtQjtJQUNuQixJQUFJLEtBQUssS0FBSyxDQUFDLENBQUMsRUFBRSxDQUFDO1FBQ2xCLElBQUksR0FBRyxDQUFBO1FBQ1AsSUFBSSxLQUFLLENBQUE7UUFDVCxJQUFJLEtBQUssQ0FBQTtR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
@@ -1,6 +1,3 @@
1
- /// <reference types="node" resolution-mode="require"/>
2
- /// <reference types="node" resolution-mode="require"/>
3
- /// <reference types="node" resolution-mode="require"/>
4
1
  import type { IncomingMessage } from 'node:http';
5
2
  import { type Stats } from 'node:fs';
6
3
  export declare function entityTag(buf: Buffer, weak?: boolean): string;
@@ -0,0 +1,104 @@
1
+ // etag: https://github.com/jshttp/etag/blob/b9f0642256e63654287299d205bc6ced71b1a228/index.js#L39
2
+ import crypto, { createHash } from 'node:crypto';
3
+ import { createReadStream } from 'node:fs';
4
+ export function entityTag(buf, weak) {
5
+ // pre-computed empty
6
+ return buf.length
7
+ ? `${buf.length.toString(16)}-${crypto
8
+ .createHash('sha1')
9
+ .update(buf)
10
+ .digest('base64')
11
+ .substring(0, 27)}"`
12
+ : `${weak ? 'W/' : ''}"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"`;
13
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag#directives
14
+ // weak W/ vs strong eTag
15
+ // same weak eTag: 2 resources might be semantically equivalent, but not byte-for-byte identical
16
+ }
17
+ export async function entityTagPath(fileStat, filePath, weak) {
18
+ const hash = createHash('sha1');
19
+ const defer = Promise.withResolvers();
20
+ createReadStream(filePath).pipe(hash)
21
+ .on('finish', defer.resolve)
22
+ .on('error', defer.reject);
23
+ await defer.promise;
24
+ return `${fileStat.size.toString(16)}-${hash
25
+ .digest('base64')
26
+ .substring(0, 27)}`;
27
+ // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag#directives
28
+ // weak W/ vs strong eTag
29
+ // same weak eTag: 2 resources might be semantically equivalent, but not byte-for-byte identical
30
+ }
31
+ const CACHE_CONTROL_NO_CACHE_REGEXP = /(?:^|,)\s*?no-cache\s*?(?:,|$)/;
32
+ export function statTag(stat) {
33
+ const mtime = stat.mtime.getTime().toString(16);
34
+ const size = stat.size.toString(16);
35
+ return `"${size}-${mtime}"`;
36
+ }
37
+ // https://github.com/jshttp/fresh/blob/05254186fd7428915224db46144fc94293a7df7d/index.js#L33
38
+ export function isFreshETag(req, etag) {
39
+ const noneMatch = req.headers['if-none-match'];
40
+ if (!noneMatch)
41
+ return;
42
+ // Always return stale when Cache-Control: no-cache
43
+ // to support end-to-end reload requests
44
+ // https://tools.ietf.org/html/rfc2616#section-14.9.4
45
+ const cacheControl = req.headers['cache-control'];
46
+ if (cacheControl && CACHE_CONTROL_NO_CACHE_REGEXP.test(cacheControl))
47
+ return;
48
+ if (noneMatch && noneMatch !== '*') {
49
+ if (!etag)
50
+ return;
51
+ let etagStale = true;
52
+ for (const match of parseTokenList(noneMatch)) {
53
+ if (match === etag || match === `W/${etag}` || `W/${match}` === etag) {
54
+ etagStale = false;
55
+ break;
56
+ }
57
+ }
58
+ if (etagStale)
59
+ return;
60
+ }
61
+ return true;
62
+ }
63
+ export function isFreshModifiedSince(req, lastModified) {
64
+ const modifiedSince = req.headers['if-modified-since'];
65
+ if (!modifiedSince)
66
+ return;
67
+ // Always return stale when Cache-Control: no-cache
68
+ // to support end-to-end reload requests
69
+ // https://tools.ietf.org/html/rfc2616#section-14.9.4
70
+ const cacheControl = req.headers['cache-control'];
71
+ if (cacheControl && CACHE_CONTROL_NO_CACHE_REGEXP.test(cacheControl))
72
+ return;
73
+ if (modifiedSince && lastModified) {
74
+ const lastModifiedDate = Date.parse(lastModified);
75
+ const modifiedSinceDate = Date.parse(modifiedSince);
76
+ return !isNaN(lastModifiedDate) && !isNaN(modifiedSinceDate) && lastModifiedDate <= modifiedSinceDate;
77
+ }
78
+ return true;
79
+ }
80
+ function parseTokenList(str) {
81
+ let end = 0;
82
+ const list = [];
83
+ let start = 0;
84
+ // gather tokens
85
+ for (let i = 0, len = str.length; i < len; i++) {
86
+ switch (str.charCodeAt(i)) {
87
+ case 0x20: /* */
88
+ if (start === end) {
89
+ start = end = i + 1;
90
+ }
91
+ break;
92
+ case 0x2c: /* , */
93
+ list.push(str.substring(start, end));
94
+ start = end = i + 1;
95
+ break;
96
+ default:
97
+ end = i + 1;
98
+ break;
99
+ }
100
+ }
101
+ // final token
102
+ list.push(str.substring(start, end));
103
+ return list;
104
+ }
@@ -6,7 +6,7 @@
6
6
  * @return {Boolean}
7
7
  * @public
8
8
  */
9
- export declare function fresh(reqHeaders: any, resHeaders: any): boolean;
9
+ export declare function fresh(reqHeaders: Record<string, any>, resHeaders: Record<string, any>): boolean;
10
10
  /**
11
11
  * Parse an HTTP Date into a number.
12
12
  *
@@ -20,4 +20,4 @@ export declare function parseHttpDate(date: any): number;
20
20
  * @param {string} str
21
21
  * @private
22
22
  */
23
- export declare function parseTokenList(str: any): any[];
23
+ export declare function parseTokenList(str: string): string[];
@@ -0,0 +1,95 @@
1
+ const CACHE_CONTROL_NO_CACHE_REGEXP = /(?:^|,)\s*?no-cache\s*?(?:,|$)/;
2
+ /**
3
+ * Check freshness of the response using request and response headers.
4
+ *
5
+ * @param {Object} reqHeaders
6
+ * @param {Object} resHeaders
7
+ * @return {Boolean}
8
+ * @public
9
+ */
10
+ export function fresh(reqHeaders, resHeaders) {
11
+ // fields
12
+ const modifiedSince = reqHeaders['if-modified-since'];
13
+ const noneMatch = reqHeaders['if-none-match'];
14
+ // unconditional request
15
+ if (!modifiedSince && !noneMatch) {
16
+ return false;
17
+ }
18
+ // Always return stale when Cache-Control: no-cache
19
+ // to support end-to-end reload requests
20
+ // https://tools.ietf.org/html/rfc2616#section-14.9.4
21
+ const cacheControl = reqHeaders['cache-control'];
22
+ if (cacheControl && CACHE_CONTROL_NO_CACHE_REGEXP.test(cacheControl)) {
23
+ return false;
24
+ }
25
+ // if-none-match takes precedent over if-modified-since
26
+ if (noneMatch) {
27
+ if (noneMatch === '*') {
28
+ return true;
29
+ }
30
+ const etag = resHeaders.etag;
31
+ if (!etag) {
32
+ return false;
33
+ }
34
+ const matches = parseTokenList(noneMatch);
35
+ for (let i = 0; i < matches.length; i++) {
36
+ const match = matches[i];
37
+ if (match === etag || match === 'W/' + etag || 'W/' + match === etag) {
38
+ return true;
39
+ }
40
+ }
41
+ return false;
42
+ }
43
+ // if-modified-since
44
+ if (modifiedSince) {
45
+ const lastModified = resHeaders['last-modified'];
46
+ const modifiedStale = !lastModified || !(parseHttpDate(lastModified) <= parseHttpDate(modifiedSince));
47
+ if (modifiedStale) {
48
+ return false;
49
+ }
50
+ }
51
+ return true;
52
+ }
53
+ /**
54
+ * Parse an HTTP Date into a number.
55
+ *
56
+ * @param {string} date
57
+ * @private
58
+ */
59
+ export function parseHttpDate(date) {
60
+ const timestamp = date && Date.parse(date);
61
+ // istanbul ignore next: guard against date.js Date.parse patching
62
+ return typeof timestamp === 'number' ? timestamp : NaN;
63
+ }
64
+ /**
65
+ * Parse a HTTP token list.
66
+ *
67
+ * @param {string} str
68
+ * @private
69
+ */
70
+ export function parseTokenList(str) {
71
+ let end = 0;
72
+ const list = [];
73
+ let start = 0;
74
+ // gather tokens
75
+ let i = 0, len = str.length;
76
+ for (; i < len; i++) {
77
+ switch (str.charCodeAt(i)) {
78
+ case 0x20: /* */
79
+ if (start === end) {
80
+ start = end = i + 1;
81
+ }
82
+ break;
83
+ case 0x2c: /* , */
84
+ list.push(str.substring(start, end));
85
+ start = end = i + 1;
86
+ break;
87
+ default:
88
+ end = i + 1;
89
+ break;
90
+ }
91
+ }
92
+ // final token
93
+ list.push(str.substring(start, end));
94
+ return list;
95
+ }
@@ -0,0 +1 @@
1
+ export declare function contentTypeForExtension(extension: string): string | undefined;
@@ -0,0 +1,35 @@
1
+ import mimeDbRaw from './mimeDb.js';
2
+ import { mimeScore } from './mimeScore.js';
3
+ const mimeDb = mimeDbRaw;
4
+ const extensionToMime = Object.create(null);
5
+ for (const [type, { extensions = [] }] of Object.entries(mimeDb))
6
+ for (const extension of extensions)
7
+ extensionToMime[extension] = preferredType(extension, type, extensionToMime[extension]);
8
+ function preferredType(ext, type0, type1) {
9
+ const score0 = type0 ? mimeScore(type0, mimeDb[type0].source) : 0;
10
+ const score1 = type1 ? mimeScore(type1, mimeDb[type1].source) : 0;
11
+ return score0 > score1 ? type0 : type1;
12
+ }
13
+ export function contentTypeForExtension(extension) {
14
+ const mimeType = extensionToMime[extension.toLowerCase()];
15
+ if (!mimeType)
16
+ return;
17
+ if (!mimeType.includes('charset')) {
18
+ const charset = determineCharset(mimeType);
19
+ if (charset)
20
+ return mimeType + '; charset=' + charset.toLowerCase();
21
+ }
22
+ return mimeType;
23
+ }
24
+ const EXTRACT_TYPE_REGEXP = /^\s*([^;\s]*)(?:;|\s|$)/;
25
+ const TEXT_TYPE_REGEXP = /^text\//i;
26
+ function determineCharset(type) {
27
+ // _TODO: use media-typer
28
+ const match = EXTRACT_TYPE_REGEXP.exec(type);
29
+ const mime = match && mimeDb[match[1].toLowerCase()];
30
+ if (mime?.charset)
31
+ return mime.charset;
32
+ // default text/* to utf-8
33
+ if (match && TEXT_TYPE_REGEXP.test(match[1]))
34
+ return 'UTF-8';
35
+ }