@mojojs/core 1.26.18 → 2.0.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 (156) hide show
  1. package/README.md +1 -1
  2. package/lib/app.d.ts +31 -16
  3. package/lib/app.js +181 -126
  4. package/lib/body/params.js +0 -1
  5. package/lib/body.d.ts +13 -5
  6. package/lib/body.js +47 -17
  7. package/lib/cache.d.ts +1 -2
  8. package/lib/cache.js +12 -21
  9. package/lib/cgi.d.ts +3 -3
  10. package/lib/cgi.js +16 -8
  11. package/lib/cli/cgi.d.ts +1 -1
  12. package/lib/cli/cgi.js +1 -2
  13. package/lib/cli/create-full-app.d.ts +1 -1
  14. package/lib/cli/create-full-app.js +27 -27
  15. package/lib/cli/create-lite-app.d.ts +1 -1
  16. package/lib/cli/create-lite-app.js +2 -3
  17. package/lib/cli/create-plugin.d.ts +1 -1
  18. package/lib/cli/create-plugin.js +25 -23
  19. package/lib/cli/eval.d.ts +1 -1
  20. package/lib/cli/eval.js +12 -5
  21. package/lib/cli/get.d.ts +1 -1
  22. package/lib/cli/get.js +30 -26
  23. package/lib/cli/repl.d.ts +1 -1
  24. package/lib/cli/repl.js +0 -1
  25. package/lib/cli/routes.d.ts +1 -1
  26. package/lib/cli/routes.js +10 -7
  27. package/lib/cli/server.d.ts +1 -1
  28. package/lib/cli/server.js +41 -24
  29. package/lib/cli/version.js +1 -2
  30. package/lib/cli.d.ts +1 -1
  31. package/lib/cli.js +25 -19
  32. package/lib/context.d.ts +33 -10
  33. package/lib/context.js +137 -32
  34. package/lib/core.d.ts +14 -13
  35. package/lib/core.js +12 -12
  36. package/lib/headers.js +5 -4
  37. package/lib/hooks.d.ts +1 -1
  38. package/lib/hooks.js +1 -5
  39. package/lib/logger.d.ts +2 -2
  40. package/lib/logger.js +28 -16
  41. package/lib/mime.js +10 -13
  42. package/lib/plugins/default-conditions.d.ts +1 -1
  43. package/lib/plugins/default-conditions.js +0 -1
  44. package/lib/plugins/default-helpers.d.ts +1 -1
  45. package/lib/plugins/default-helpers.js +28 -11
  46. package/lib/plugins/json-config.d.ts +1 -1
  47. package/lib/plugins/json-config.js +0 -1
  48. package/lib/plugins/mount.d.ts +1 -1
  49. package/lib/plugins/mount.js +0 -1
  50. package/lib/plugins/tmpl-engine.d.ts +1 -1
  51. package/lib/plugins/tmpl-engine.js +2 -5
  52. package/lib/plugins/yaml-config.d.ts +1 -1
  53. package/lib/plugins/yaml-config.js +1 -2
  54. package/lib/renderer.d.ts +1 -1
  55. package/lib/renderer.js +27 -30
  56. package/lib/router/pattern.d.ts +1 -1
  57. package/lib/router/pattern.js +25 -14
  58. package/lib/router/plan.d.ts +1 -1
  59. package/lib/router/plan.js +12 -15
  60. package/lib/router/route.d.ts +10 -3
  61. package/lib/router/route.js +44 -38
  62. package/lib/router.d.ts +4 -4
  63. package/lib/router.js +25 -26
  64. package/lib/server/cookie.d.ts +1 -1
  65. package/lib/server/cookie.js +3 -2
  66. package/lib/server/request.d.ts +3 -3
  67. package/lib/server/request.js +44 -17
  68. package/lib/server/response.d.ts +3 -3
  69. package/lib/server/response.js +20 -20
  70. package/lib/server.d.ts +7 -2
  71. package/lib/server.js +70 -20
  72. package/lib/session.d.ts +8 -3
  73. package/lib/session.js +47 -29
  74. package/lib/sse.d.ts +10 -0
  75. package/lib/sse.js +62 -0
  76. package/lib/static.d.ts +1 -1
  77. package/lib/static.js +27 -22
  78. package/lib/types.d.ts +13 -5
  79. package/lib/types.js +0 -1
  80. package/lib/user-agent/cookie-jar.d.ts +2 -2
  81. package/lib/user-agent/cookie-jar.js +6 -9
  82. package/lib/user-agent/mock.d.ts +4 -4
  83. package/lib/user-agent/mock.js +6 -7
  84. package/lib/user-agent/response.d.ts +12 -1
  85. package/lib/user-agent/response.js +36 -2
  86. package/lib/user-agent/test.d.ts +75 -5
  87. package/lib/user-agent/test.js +147 -13
  88. package/lib/user-agent/transport/http.d.ts +2 -2
  89. package/lib/user-agent/transport/http.js +7 -10
  90. package/lib/user-agent/transport/https.d.ts +2 -2
  91. package/lib/user-agent/transport/https.js +2 -6
  92. package/lib/user-agent/transport/ws.d.ts +2 -2
  93. package/lib/user-agent/transport/ws.js +2 -3
  94. package/lib/user-agent.d.ts +21 -18
  95. package/lib/user-agent.js +70 -49
  96. package/lib/util.d.ts +13 -0
  97. package/lib/util.js +35 -11
  98. package/lib/validator/result.d.ts +1 -1
  99. package/lib/validator/result.js +8 -1
  100. package/lib/validator.d.ts +2 -2
  101. package/lib/validator.js +3 -6
  102. package/lib/websocket.d.ts +2 -2
  103. package/lib/websocket.js +9 -1
  104. package/package.json +22 -29
  105. package/lib/app.js.map +0 -1
  106. package/lib/body/params.js.map +0 -1
  107. package/lib/body.js.map +0 -1
  108. package/lib/cache.js.map +0 -1
  109. package/lib/cgi.js.map +0 -1
  110. package/lib/cli/cgi.js.map +0 -1
  111. package/lib/cli/create-full-app.js.map +0 -1
  112. package/lib/cli/create-lite-app.js.map +0 -1
  113. package/lib/cli/create-plugin.js.map +0 -1
  114. package/lib/cli/eval.js.map +0 -1
  115. package/lib/cli/get.js.map +0 -1
  116. package/lib/cli/repl.js.map +0 -1
  117. package/lib/cli/routes.js.map +0 -1
  118. package/lib/cli/server.js.map +0 -1
  119. package/lib/cli/version.js.map +0 -1
  120. package/lib/cli.js.map +0 -1
  121. package/lib/context.js.map +0 -1
  122. package/lib/core.js.map +0 -1
  123. package/lib/headers.js.map +0 -1
  124. package/lib/hooks.js.map +0 -1
  125. package/lib/logger.js.map +0 -1
  126. package/lib/mime.js.map +0 -1
  127. package/lib/plugins/default-conditions.js.map +0 -1
  128. package/lib/plugins/default-helpers.js.map +0 -1
  129. package/lib/plugins/json-config.js.map +0 -1
  130. package/lib/plugins/mount.js.map +0 -1
  131. package/lib/plugins/tmpl-engine.js.map +0 -1
  132. package/lib/plugins/yaml-config.js.map +0 -1
  133. package/lib/renderer.js.map +0 -1
  134. package/lib/router/pattern.js.map +0 -1
  135. package/lib/router/plan.js.map +0 -1
  136. package/lib/router/route.js.map +0 -1
  137. package/lib/router.js.map +0 -1
  138. package/lib/server/cookie.js.map +0 -1
  139. package/lib/server/request.js.map +0 -1
  140. package/lib/server/response.js.map +0 -1
  141. package/lib/server.js.map +0 -1
  142. package/lib/session.js.map +0 -1
  143. package/lib/static.js.map +0 -1
  144. package/lib/types.js.map +0 -1
  145. package/lib/user-agent/cookie-jar.js.map +0 -1
  146. package/lib/user-agent/mock.js.map +0 -1
  147. package/lib/user-agent/response.js.map +0 -1
  148. package/lib/user-agent/test.js.map +0 -1
  149. package/lib/user-agent/transport/http.js.map +0 -1
  150. package/lib/user-agent/transport/https.js.map +0 -1
  151. package/lib/user-agent/transport/ws.js.map +0 -1
  152. package/lib/user-agent.js.map +0 -1
  153. package/lib/util.js.map +0 -1
  154. package/lib/validator/result.js.map +0 -1
  155. package/lib/validator.js.map +0 -1
  156. package/lib/websocket.js.map +0 -1
package/lib/server.js CHANGED
@@ -2,11 +2,11 @@ import cluster from 'node:cluster';
2
2
  import http from 'node:http';
3
3
  import https from 'node:https';
4
4
  import os from 'node:os';
5
- import { Stream } from 'node:stream';
5
+ import { pipeline, Stream } from 'node:stream';
6
6
  import { URL } from 'node:url';
7
- import { ServerRequest } from './server/request.js';
8
- import { ServerResponse } from './server/response.js';
9
- import { WebSocket } from './websocket.js';
7
+ import { ServerRequest } from "./server/request.js";
8
+ import { ServerResponse } from "./server/response.js";
9
+ import { WebSocket } from "./websocket.js";
10
10
  import Path from '@mojojs/path';
11
11
  import { termEscape } from '@mojojs/util';
12
12
  import { WebSocketServer } from 'ws';
@@ -22,16 +22,52 @@ const devKey = certs.child('development.key').toString();
22
22
  * HTTP and WebSocket server class.
23
23
  */
24
24
  export class Server {
25
+ /**
26
+ * Application this server handles.
27
+ */
28
+ app;
29
+ /**
30
+ * Limit the amount of time the parser will wait to receive the complete HTTP headers, defaults to `60000`
31
+ * (60 seconds).
32
+ */
33
+ headersTimeout;
34
+ /**
35
+ * Limit the amount of time of inactivity a server needs to wait for additional incoming data, after it has finished
36
+ * writing the last response, before a socket will be destroyed, defaults to `5000` (5 seconds).
37
+ */
38
+ keepAliveTimeout;
39
+ /**
40
+ * Maximum number of requests socket can handle before closing keep alive connection, defaults to `0`.
41
+ */
42
+ maxRequestsPerSocket;
43
+ /**
44
+ * Maximum WebSocket message size in bytes, defaults to `262144` (256 KiB). Connections exceeding the limit are closed
45
+ * with status `1009`.
46
+ */
47
+ maxWebSocketSize;
48
+ /**
49
+ * Limit the amount of time for receiving the entire request from the client, defaults to `300000` (300 seconds).
50
+ */
51
+ requestTimeout;
52
+ /**
53
+ * Reverse proxy mode.
54
+ */
55
+ reverseProxy;
56
+ /**
57
+ * Server URLs.
58
+ */
59
+ urls = [];
60
+ _cluster;
61
+ _listen;
62
+ _servers = [];
63
+ _quiet;
64
+ _workers;
25
65
  constructor(app, options = {}) {
26
- /**
27
- * Server URLs.
28
- */
29
- this.urls = [];
30
- this._servers = [];
31
66
  this.app = app;
32
67
  this.headersTimeout = options.headersTimeout;
33
68
  this.keepAliveTimeout = options.keepAliveTimeout;
34
69
  this.maxRequestsPerSocket = options.maxRequestsPerSocket;
70
+ this.maxWebSocketSize = options.maxWebSocketSize ?? 256 * 1024;
35
71
  this.requestTimeout = options.requestTimeout;
36
72
  this.reverseProxy = options.reverseProxy ?? false;
37
73
  this._cluster = options.cluster ?? false;
@@ -72,6 +108,7 @@ export class Server {
72
108
  cluster.fork();
73
109
  }
74
110
  else {
111
+ await app.warmup();
75
112
  for (const location of this._listen) {
76
113
  await this._createServer(location);
77
114
  }
@@ -100,8 +137,7 @@ export class Server {
100
137
  options.key = await new Path(searchParams.get('key') ?? devKey).readFile();
101
138
  isHttps = true;
102
139
  }
103
- await this.app.warmup();
104
- const wss = new WebSocketServer({ noServer: true });
140
+ const wss = new WebSocketServer({ maxPayload: this.maxWebSocketSize, noServer: true });
105
141
  const server = (isHttps ? https : http).createServer(options, this._handleRequest.bind(this));
106
142
  this._servers.push(server);
107
143
  if (this.maxRequestsPerSocket !== undefined)
@@ -118,9 +154,9 @@ export class Server {
118
154
  const stderr = process.stderr;
119
155
  socket.on('data', (chunk) => stderr.write(termEscape(`-- Server <<< Client\n${chunk}`)));
120
156
  const write = socket.write;
121
- socket.write = (chunk, cb) => {
157
+ socket.write = (chunk, ...args) => {
122
158
  stderr.write(termEscape(`-- Server >>> Client\n${chunk}`));
123
- return write.apply(socket, [chunk, cb]);
159
+ return write.apply(socket, [chunk, ...args]);
124
160
  };
125
161
  });
126
162
  }
@@ -159,7 +195,7 @@ export class Server {
159
195
  const ctx = app.newContext(this._prepareRequest(req, socket, false), new ServerResponse((res, body) => {
160
196
  sendResponse(res, body, raw);
161
197
  }), { name: 'server', req, res: raw });
162
- raw.on('finish', () => ctx.emit('finish'));
198
+ raw.on('close', () => ctx.emit('finish'));
163
199
  app.handleRequest(ctx).catch(error => ctx.exception(error));
164
200
  }
165
201
  _handleUpgrade(wss, req, socket, head) {
@@ -202,11 +238,18 @@ function sendHeaders(res, raw) {
202
238
  const statusCode = res.statusCode;
203
239
  const statusMessage = res.statusMessage;
204
240
  const headers = res.headers.toArray();
205
- if (statusMessage === null) {
206
- raw.writeHead(statusCode, headers);
241
+ try {
242
+ if (statusMessage === null) {
243
+ raw.writeHead(statusCode, headers);
244
+ }
245
+ else {
246
+ raw.writeHead(statusCode, statusMessage, headers);
247
+ }
207
248
  }
208
- else {
209
- raw.writeHead(statusCode, statusMessage, headers);
249
+ catch (error) {
250
+ // The response counts as sent already, so close the connection instead of letting the client wait forever
251
+ raw.destroy();
252
+ throw error;
210
253
  }
211
254
  }
212
255
  function sendResponse(res, body, raw) {
@@ -217,7 +260,15 @@ function sendResponse(res, body, raw) {
217
260
  }
218
261
  else if (body instanceof Stream) {
219
262
  sendHeaders(res, raw);
220
- body.pipe(raw);
263
+ // Both streams need to be cleaned up, and only aborted connections are expected
264
+ pipeline(body, raw, error => {
265
+ if (error === undefined ||
266
+ error === null ||
267
+ error.code === 'ERR_STREAM_PREMATURE_CLOSE') {
268
+ return;
269
+ }
270
+ res._ctx?.deref()?.log.error(`Response stream failed: ${error.stack}`);
271
+ });
221
272
  }
222
273
  else {
223
274
  sendHeaders(res, raw);
@@ -227,4 +278,3 @@ function sendResponse(res, body, raw) {
227
278
  function urlToSocketPath(url) {
228
279
  return url.host + url.pathname;
229
280
  }
230
- //# sourceMappingURL=server.js.map
package/lib/session.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import type { App } from './app.js';
2
- import type { Context } from './context.js';
3
- import type { SessionData } from './types.js';
1
+ import type { App } from './app.ts';
2
+ import type { Context } from './context.ts';
3
+ import type { SessionData } from './types.ts';
4
4
  /**
5
5
  * Session manager class.
6
6
  */
@@ -23,6 +23,11 @@ export declare class Session {
23
23
  * Set the `HttpOnly` value on all session cookies.
24
24
  */
25
25
  httpOnly: boolean;
26
+ /**
27
+ * Set the `Partitioned` value on all session cookies, to be used in accordance with the CHIPS amendment to RFC 6265.
28
+ * Note that this property is EXPERIMENTAL and might change without warning!
29
+ */
30
+ partitioned: boolean;
26
31
  /**
27
32
  * Set the `SameSite` value on all session cookies, defaults to `lax`.
28
33
  */
package/lib/session.js CHANGED
@@ -2,37 +2,55 @@ import crypto from 'node:crypto';
2
2
  import { promisify } from 'node:util';
3
3
  const scrypt = promisify(crypto.scrypt);
4
4
  const randomBytes = promisify(crypto.randomBytes);
5
+ // Deriving keys with scrypt is slow on purpose, so it is only done once per secret
6
+ const keys = new Map();
7
+ function deriveKey(secret) {
8
+ let key = keys.get(secret);
9
+ if (key === undefined) {
10
+ key = scrypt(secret, 'salt', 32);
11
+ keys.set(secret, key);
12
+ // Do not cache failures
13
+ key.catch(() => keys.delete(secret));
14
+ }
15
+ return key;
16
+ }
5
17
  /**
6
18
  * Session manager class.
7
19
  */
8
20
  export class Session {
21
+ /**
22
+ * Name for session cookies, defaults to `mojo`.
23
+ */
24
+ cookieName = 'mojo';
25
+ /**
26
+ * Path for session cookies, defaults to `/`.
27
+ */
28
+ cookiePath = '/';
29
+ /**
30
+ * Default time for sessions to expire in seconds from now, defaults to `3600`. The expiration timeout gets refreshed
31
+ * for every request. Setting the value to `0` will allow sessions to persist until the browser window is closed,
32
+ * this can have security implications though.
33
+ */
34
+ expiration = 3600;
35
+ /**
36
+ * Set the `HttpOnly` value on all session cookies.
37
+ */
38
+ httpOnly = true;
39
+ /**
40
+ * Set the `Partitioned` value on all session cookies, to be used in accordance with the CHIPS amendment to RFC 6265.
41
+ * Note that this property is EXPERIMENTAL and might change without warning!
42
+ */
43
+ partitioned = false;
44
+ /**
45
+ * Set the `SameSite` value on all session cookies, defaults to `lax`.
46
+ */
47
+ sameSite = 'lax';
48
+ /**
49
+ * Set the secure flag on all session cookies, so that browsers send them only over HTTPS connections.
50
+ */
51
+ secure = false;
52
+ _app;
9
53
  constructor(app) {
10
- /**
11
- * Name for session cookies, defaults to `mojo`.
12
- */
13
- this.cookieName = 'mojo';
14
- /**
15
- * Path for session cookies, defaults to `/`.
16
- */
17
- this.cookiePath = '/';
18
- /**
19
- * Default time for sessions to expire in seconds from now, defaults to `3600`. The expiration timeout gets refreshed
20
- * for every request. Setting the value to `0` will allow sessions to persist until the browser window is closed,
21
- * this can have security implications though.
22
- */
23
- this.expiration = 3600;
24
- /**
25
- * Set the `HttpOnly` value on all session cookies.
26
- */
27
- this.httpOnly = true;
28
- /**
29
- * Set the `SameSite` value on all session cookies, defaults to `lax`.
30
- */
31
- this.sameSite = 'lax';
32
- /**
33
- * Set the secure flag on all session cookies, so that browsers send them only over HTTPS connections.
34
- */
35
- this.secure = false;
36
54
  this._app = new WeakRef(app);
37
55
  }
38
56
  /**
@@ -47,7 +65,7 @@ export class Session {
47
65
  const authTag = Buffer.from(match[3], 'base64');
48
66
  for (const secret of secrets) {
49
67
  try {
50
- const key = await scrypt(secret, 'salt', 32);
68
+ const key = await deriveKey(secret);
51
69
  const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv, { authTagLength: 16 });
52
70
  decipher.setAuthTag(authTag);
53
71
  const decrypted = decipher.update(value, 'base64', 'utf8');
@@ -63,7 +81,7 @@ export class Session {
63
81
  * Encrypt cookie.
64
82
  */
65
83
  static async encrypt(secret, value) {
66
- const key = await scrypt(secret, 'salt', 32);
84
+ const key = await deriveKey(secret);
67
85
  const iv = await randomBytes(12);
68
86
  const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
69
87
  const encrypted = cipher.update(value, 'utf8', 'base64') + cipher.final('base64');
@@ -111,10 +129,10 @@ export class Session {
111
129
  ctx.res.setCookie(this.cookieName, encrypted, {
112
130
  expires: expires === undefined ? undefined : new Date(expires * 1000),
113
131
  httpOnly: this.httpOnly,
132
+ partitioned: this.partitioned,
114
133
  path: this.cookiePath,
115
134
  sameSite: this.sameSite,
116
135
  secure: this.secure
117
136
  });
118
137
  }
119
138
  }
120
- //# sourceMappingURL=session.js.map
package/lib/sse.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { SSEEvent } from './types.ts';
2
+ /**
3
+ * Build Server-Sent Event. Note that this function is EXPERIMENTAL and might change without warning!
4
+ */
5
+ export declare function buildEvent(event: SSEEvent): Buffer;
6
+ /**
7
+ * Parse Server-Sent Event, returns the event (or `null` if no complete event was found) and the remaining buffer. Note
8
+ * that this function is EXPERIMENTAL and might change without warning!
9
+ */
10
+ export declare function parseEvent(buffer: string): [SSEEvent | null, string];
package/lib/sse.js ADDED
@@ -0,0 +1,62 @@
1
+ const SPLIT_RE = /\r\n|(?<!\r)\n|\r(?!\n)/;
2
+ const EVENT_RE = /^([\s\S]*?)(?:\r\n|(?<!\r)\n|\r(?!\n)){2}/;
3
+ /**
4
+ * Build Server-Sent Event. Note that this function is EXPERIMENTAL and might change without warning!
5
+ */
6
+ export function buildEvent(event) {
7
+ const data = splitLines(event.text);
8
+ const comment = splitLines(event.comment);
9
+ const parts = [];
10
+ if (comment.length > 0) {
11
+ for (const line of comment)
12
+ parts.push(`: ${line}`);
13
+ }
14
+ else {
15
+ if (event.type !== undefined)
16
+ parts.push(`event: ${event.type}`);
17
+ for (const line of data)
18
+ parts.push(`data: ${line}`);
19
+ if (event.id !== undefined && event.id !== null)
20
+ parts.push(`id: ${event.id}`);
21
+ }
22
+ return Buffer.from([...parts, '', ''].join('\r\n'));
23
+ }
24
+ /**
25
+ * Parse Server-Sent Event, returns the event (or `null` if no complete event was found) and the remaining buffer. Note
26
+ * that this function is EXPERIMENTAL and might change without warning!
27
+ */
28
+ export function parseEvent(buffer) {
29
+ const event = { id: null, type: 'message', text: '' };
30
+ for (let match = buffer.match(EVENT_RE); match !== null; match = buffer.match(EVENT_RE)) {
31
+ buffer = buffer.slice(match[0].length);
32
+ // Skip comments
33
+ const lines = match[1];
34
+ if (/^\s*:/.test(lines) === true)
35
+ continue;
36
+ let first = true;
37
+ for (const line of lines.split(SPLIT_RE)) {
38
+ let field;
39
+ if ((field = line.match(/^event(?::\s*(\S.*))?$/)) !== null) {
40
+ event.type = field[1] ?? 'message';
41
+ }
42
+ else if ((field = line.match(/^data(?::\s*(.*))?$/)) !== null) {
43
+ event.text += (first === true ? '' : '\n') + (field[1] ?? '');
44
+ first = false;
45
+ }
46
+ else if ((field = line.match(/^id(?::\s*(.*))?$/)) !== null) {
47
+ event.id = field[1] ?? null;
48
+ }
49
+ }
50
+ return [event, buffer];
51
+ }
52
+ return [null, buffer];
53
+ }
54
+ // Split like Perl, without trailing empty lines
55
+ function splitLines(value) {
56
+ if (value === undefined)
57
+ return [];
58
+ const lines = value.split(SPLIT_RE);
59
+ while (lines.length > 0 && lines[lines.length - 1] === '')
60
+ lines.pop();
61
+ return lines;
62
+ }
package/lib/static.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { MojoContext } from './types.js';
1
+ import type { MojoContext } from './types.ts';
2
2
  import Path from '@mojojs/path';
3
3
  type AssetIndex = Record<string, string>;
4
4
  /**
package/lib/static.js CHANGED
@@ -1,23 +1,21 @@
1
- import crypto from 'node:crypto';
2
1
  import Path from '@mojojs/path';
3
2
  /**
4
3
  * Static file server class.
5
4
  */
6
5
  export class Static {
7
- constructor() {
8
- /**
9
- * Subdirectory used for all static assets.
10
- */
11
- this.assetDir = 'assets';
12
- /**
13
- * Prefix to use for all static files.
14
- */
15
- this.prefix = '/static';
16
- /**
17
- * Directories to serve static files from, first one has the highest precedence.
18
- */
19
- this.publicPaths = [Path.currentFile().sibling('..', 'vendor', 'public').toString()];
20
- }
6
+ /**
7
+ * Subdirectory used for all static assets.
8
+ */
9
+ assetDir = 'assets';
10
+ /**
11
+ * Prefix to use for all static files.
12
+ */
13
+ prefix = '/static';
14
+ /**
15
+ * Directories to serve static files from, first one has the highest precedence.
16
+ */
17
+ publicPaths = [Path.currentFile().sibling('..', 'vendor', 'public').toString()];
18
+ _assets;
21
19
  /**
22
20
  * Get static asset path.
23
21
  */
@@ -79,9 +77,10 @@ export class Static {
79
77
  // If-None-Match
80
78
  const ifNoneMatch = req.get('If-None-Match');
81
79
  if (etag !== undefined && ifNoneMatch !== null) {
82
- const etags = ifNoneMatch.split(/,\s+/).map(value => value.replaceAll('"', ''));
80
+ // Weak comparison, because proxies like nginx turn ETags into weak ones when compressing responses
81
+ const etags = ifNoneMatch.split(/\s*,\s*/).map(value => value.replace(/^W\//, '').replaceAll('"', ''));
83
82
  for (const match of etags) {
84
- if (match === etag)
83
+ if (match === etag || match === '*')
85
84
  return true;
86
85
  }
87
86
  }
@@ -91,7 +90,8 @@ export class Static {
91
90
  const epoch = Date.parse(ifModifiedSince);
92
91
  if (isNaN(epoch))
93
92
  return false;
94
- if (epoch > options.lastModified.getTime())
93
+ // HTTP dates have no milliseconds
94
+ if (epoch >= Math.floor(options.lastModified.getTime() / 1000) * 1000)
95
95
  return true;
96
96
  }
97
97
  return false;
@@ -113,7 +113,8 @@ export class Static {
113
113
  const { res } = ctx;
114
114
  res.set('Accept-Ranges', 'bytes');
115
115
  const lastModified = stat.mtime;
116
- const etag = crypto.createHash('md5').update(lastModified.getTime().toString()).digest('hex');
116
+ // Size too, because reproducible builds often give all files the same modification time
117
+ const etag = `${lastModified.getTime()}-${length}`;
117
118
  if (this.isFresh(ctx, { etag, lastModified })) {
118
119
  await res.status(304).send();
119
120
  return;
@@ -131,10 +132,15 @@ export class Static {
131
132
  if (bytes === null)
132
133
  return await this._rangeNotSatisfiable(ctx);
133
134
  let start = parseInt(bytes[1]);
135
+ let end = parseInt(bytes[2]);
136
+ // Suffix range for the last bytes of the file
137
+ if (isNaN(start) && isNaN(end) === false) {
138
+ start = Math.max(length - end, 0);
139
+ end = length - 1;
140
+ }
134
141
  if (isNaN(start))
135
142
  start = 0;
136
- let end = parseInt(bytes[2]);
137
- if (isNaN(end))
143
+ if (isNaN(end) || end >= length)
138
144
  end = length - 1;
139
145
  if (start > end)
140
146
  return await this._rangeNotSatisfiable(ctx);
@@ -175,4 +181,3 @@ export class Static {
175
181
  await ctx.res.status(416).send();
176
182
  }
177
183
  }
178
- //# sourceMappingURL=static.js.map
package/lib/types.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import type { App } from './app.js';
2
- import type { Context } from './context.js';
3
- import type { Route } from './router/route.js';
4
- import type { SafeString } from './util.js';
5
- import type { ValidatorResult } from './validator/result.js';
1
+ import type { App } from './app.ts';
2
+ import type { Context } from './context.ts';
3
+ import type { Route } from './router/route.ts';
4
+ import type { SafeString } from './util.ts';
5
+ import type { ValidatorResult } from './validator/result.ts';
6
6
  import type { Agent } from 'node:http';
7
7
  import type { Readable, Stream } from 'node:stream';
8
8
  import type { URL } from 'node:url';
@@ -123,6 +123,7 @@ export interface CookieOptions {
123
123
  expires?: Date;
124
124
  httpOnly?: boolean;
125
125
  maxAge?: number;
126
+ partitioned?: boolean;
126
127
  path?: string;
127
128
  sameSite?: 'lax' | 'strict' | 'none';
128
129
  secure?: boolean;
@@ -155,6 +156,7 @@ export interface ServerOptions {
155
156
  keepAliveTimeout?: number;
156
157
  listen?: string[];
157
158
  maxRequestsPerSocket?: number;
159
+ maxWebSocketSize?: number;
158
160
  quiet?: boolean;
159
161
  requestTimeout?: number;
160
162
  reverseProxy?: boolean;
@@ -170,6 +172,12 @@ export interface ServerRequestOptions {
170
172
  reverseProxy: boolean;
171
173
  url?: string;
172
174
  }
175
+ export interface SSEEvent {
176
+ comment?: string;
177
+ id?: string | number | null;
178
+ text?: string;
179
+ type?: string;
180
+ }
173
181
  export type ServerResponseBody = string | Buffer | Stream | undefined;
174
182
  export interface UploadOptions {
175
183
  limits?: {
package/lib/types.js CHANGED
@@ -1,2 +1 @@
1
1
  export {};
2
- //# sourceMappingURL=types.js.map
@@ -1,5 +1,5 @@
1
1
  import { type URL } from 'node:url';
2
- import tough from 'tough-cookie';
2
+ import { CookieJar as ToughCookieJar } from 'tough-cookie';
3
3
  /**
4
4
  * Cookie jar class.
5
5
  */
@@ -7,7 +7,7 @@ export declare class CookieJar {
7
7
  /**
8
8
  * Cookie storage.
9
9
  */
10
- storage: tough.CookieJar;
10
+ storage: ToughCookieJar;
11
11
  /**
12
12
  * Load cookies from storage.
13
13
  */
@@ -1,15 +1,13 @@
1
1
  import { format } from 'node:url';
2
- import tough from 'tough-cookie';
2
+ import { Cookie, CookieJar as ToughCookieJar } from 'tough-cookie';
3
3
  /**
4
4
  * Cookie jar class.
5
5
  */
6
6
  export class CookieJar {
7
- constructor() {
8
- /**
9
- * Cookie storage.
10
- */
11
- this.storage = new tough.CookieJar();
12
- }
7
+ /**
8
+ * Cookie storage.
9
+ */
10
+ storage = new ToughCookieJar();
13
11
  /**
14
12
  * Load cookies from storage.
15
13
  */
@@ -24,7 +22,7 @@ export class CookieJar {
24
22
  */
25
23
  async storeCookies(url, headers) {
26
24
  const cookieURL = this._cookieURL(url);
27
- for (const cookie of headers.map(value => tough.Cookie.parse(value))) {
25
+ for (const cookie of headers.map(value => Cookie.parse(value))) {
28
26
  if (cookie !== undefined)
29
27
  await this.storage.setCookie(cookie, cookieURL);
30
28
  }
@@ -33,4 +31,3 @@ export class CookieJar {
33
31
  return format(currentURL, { auth: false, fragment: false, search: false });
34
32
  }
35
33
  }
36
- //# sourceMappingURL=cookie-jar.js.map
@@ -1,7 +1,7 @@
1
- import type { App } from '../app.js';
2
- import type { ServerOptions, UserAgentOptions } from '../types.js';
3
- import { Server } from '../server.js';
4
- import { UserAgent } from '../user-agent.js';
1
+ import type { App } from '../app.ts';
2
+ import type { ServerOptions, UserAgentOptions } from '../types.ts';
3
+ import { Server } from '../server.ts';
4
+ import { UserAgent } from '../user-agent.ts';
5
5
  /**
6
6
  * Mock user-agent class.
7
7
  */
@@ -1,15 +1,15 @@
1
- import { Server } from '../server.js';
2
- import { UserAgent } from '../user-agent.js';
1
+ import { Server } from "../server.js";
2
+ import { UserAgent } from "../user-agent.js";
3
3
  /**
4
4
  * Mock user-agent class.
5
5
  */
6
6
  export class MockUserAgent extends UserAgent {
7
+ /**
8
+ * Server to use for mock requests.
9
+ */
10
+ server = undefined;
7
11
  constructor(options) {
8
12
  super(options);
9
- /**
10
- * Server to use for mock requests.
11
- */
12
- this.server = undefined;
13
13
  this.server = undefined;
14
14
  }
15
15
  async [Symbol.asyncDispose]() {
@@ -41,4 +41,3 @@ export class MockUserAgent extends UserAgent {
41
41
  return await this.server.stop();
42
42
  }
43
43
  }
44
- //# sourceMappingURL=mock.js.map
@@ -1,5 +1,6 @@
1
+ import type { SSEEvent } from '../types.ts';
1
2
  import type { Readable } from 'node:stream';
2
- import { Body } from '../body.js';
3
+ import { Body } from '../body.ts';
3
4
  interface UserAgentResponseOptions {
4
5
  body: Readable;
5
6
  headers: string[];
@@ -44,6 +45,16 @@ export declare class UserAgentResponse extends Body {
44
45
  * Check if response has a `2xx` response status code.
45
46
  */
46
47
  get isSuccess(): boolean;
48
+ /**
49
+ * Get async iterator for Server-Sent Events from response body. Note that this method is EXPERIMENTAL and might
50
+ * change without warning!
51
+ * @example
52
+ * // Iterate over events
53
+ * for await (const {id, type, text} of res.sse()) {
54
+ * console.log(`${type}: ${text}`);
55
+ * }
56
+ */
57
+ sse(): AsyncIterableIterator<SSEEvent>;
47
58
  /**
48
59
  * Get `Content-Type` header value.
49
60
  */