@morojs/moro 1.8.7 → 1.8.9

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 (57) hide show
  1. package/dist/core/http/http-server.js +4 -0
  2. package/dist/core/http/http-server.js.map +1 -1
  3. package/dist/core/http/http2-server.js +4 -0
  4. package/dist/core/http/http2-server.js.map +1 -1
  5. package/dist/core/http/moro-engine-server.d.ts +1 -0
  6. package/dist/core/http/moro-engine-server.js +108 -35
  7. package/dist/core/http/moro-engine-server.js.map +1 -1
  8. package/dist/core/http/uws-http-server.d.ts +1 -0
  9. package/dist/core/http/uws-http-server.js +106 -20
  10. package/dist/core/http/uws-http-server.js.map +1 -1
  11. package/dist/core/middleware/built-in/cookie/core.d.ts +26 -1
  12. package/dist/core/middleware/built-in/cookie/core.js +70 -2
  13. package/dist/core/middleware/built-in/cookie/core.js.map +1 -1
  14. package/dist/core/middleware/built-in/cookie/hook.d.ts +3 -1
  15. package/dist/core/middleware/built-in/cookie/hook.js +17 -5
  16. package/dist/core/middleware/built-in/cookie/hook.js.map +1 -1
  17. package/dist/core/middleware/built-in/cookie/index.d.ts +1 -1
  18. package/dist/core/middleware/built-in/cookie/index.js +1 -1
  19. package/dist/core/middleware/built-in/cookie/index.js.map +1 -1
  20. package/dist/core/middleware/built-in/cookie/middleware.d.ts +1 -1
  21. package/dist/core/middleware/built-in/cookie/middleware.js +6 -4
  22. package/dist/core/middleware/built-in/cookie/middleware.js.map +1 -1
  23. package/dist/core/middleware/built-in/index.d.ts +4 -12
  24. package/dist/core/middleware/built-in/rate-limit/core.d.ts +24 -0
  25. package/dist/core/middleware/built-in/rate-limit/core.js +47 -0
  26. package/dist/core/middleware/built-in/rate-limit/core.js.map +1 -1
  27. package/dist/core/middleware/built-in/rate-limit/hook.d.ts +24 -9
  28. package/dist/core/middleware/built-in/rate-limit/hook.js +34 -12
  29. package/dist/core/middleware/built-in/rate-limit/hook.js.map +1 -1
  30. package/dist/core/middleware/built-in/rate-limit/middleware.js +9 -1
  31. package/dist/core/middleware/built-in/rate-limit/middleware.js.map +1 -1
  32. package/dist/core/middleware/built-in/session/core.d.ts +8 -0
  33. package/dist/core/middleware/built-in/session/core.js +29 -2
  34. package/dist/core/middleware/built-in/session/core.js.map +1 -1
  35. package/dist/core/middleware/built-in/session/hook.js +2 -3
  36. package/dist/core/middleware/built-in/session/hook.js.map +1 -1
  37. package/dist/core/middleware/built-in/session/middleware.js +2 -3
  38. package/dist/core/middleware/built-in/session/middleware.js.map +1 -1
  39. package/dist/core/middleware/built-in/static/core.d.ts +56 -0
  40. package/dist/core/middleware/built-in/static/core.js +214 -13
  41. package/dist/core/middleware/built-in/static/core.js.map +1 -1
  42. package/dist/core/middleware/built-in/static/middleware.d.ts +5 -0
  43. package/dist/core/middleware/built-in/static/middleware.js +5 -0
  44. package/dist/core/middleware/built-in/static/middleware.js.map +1 -1
  45. package/dist/core/middleware/built-in/upload/core.d.ts +17 -0
  46. package/dist/core/middleware/built-in/upload/core.js +30 -1
  47. package/dist/core/middleware/built-in/upload/core.js.map +1 -1
  48. package/dist/core/middleware/built-in/upload/middleware.js +12 -1
  49. package/dist/core/middleware/built-in/upload/middleware.js.map +1 -1
  50. package/dist/core/routing/index.d.ts +1 -0
  51. package/dist/core/routing/index.js.map +1 -1
  52. package/dist/core/routing/unified-router.js +60 -29
  53. package/dist/core/routing/unified-router.js.map +1 -1
  54. package/dist/index.d.ts +2 -6
  55. package/dist/types/http.d.ts +3 -0
  56. package/dist/types/http.js.map +1 -1
  57. package/package.json +1 -1
@@ -1,12 +1,18 @@
1
1
  import * as fs from 'fs/promises';
2
+ import { createReadStream } from 'fs';
2
3
  import * as path from 'path';
3
4
  import * as crypto from 'crypto';
4
5
  export class StaticCore {
5
6
  root;
7
+ prefix;
6
8
  maxAge;
7
9
  index;
8
10
  dotfiles;
9
11
  etag;
12
+ lastModified;
13
+ acceptRanges;
14
+ /** Files at or below this size are read in one go instead of streamed. */
15
+ static STREAM_THRESHOLD = 512 * 1024;
10
16
  mimeTypes = {
11
17
  '.html': 'text/html',
12
18
  '.css': 'text/css',
@@ -28,18 +34,26 @@ export class StaticCore {
28
34
  };
29
35
  constructor(options) {
30
36
  this.root = path.resolve(options.root);
37
+ this.prefix = StaticCore.normalizePrefix(options.prefix);
31
38
  this.maxAge = options.maxAge || 0;
32
39
  this.index = options.index || ['index.html', 'index.htm'];
33
40
  this.dotfiles = options.dotfiles || 'ignore';
34
41
  this.etag = options.etag !== false;
42
+ this.lastModified = options.lastModified !== false;
43
+ this.acceptRanges = options.acceptRanges !== false;
35
44
  }
36
45
  async handleRequest(req, res) {
37
46
  // Only handle GET and HEAD requests
38
47
  if (req.method !== 'GET' && req.method !== 'HEAD') {
39
48
  return false;
40
49
  }
50
+ // Strip the mount prefix; anything outside it is not ours to serve
51
+ const urlPath = this.stripPrefix(req.path);
52
+ if (urlPath === null) {
53
+ return false;
54
+ }
41
55
  try {
42
- let filePath = path.join(this.root, req.path);
56
+ let filePath = path.join(this.root, urlPath);
43
57
  // Security: prevent directory traversal. The trailing separator is required —
44
58
  // a bare prefix check would allow sibling dirs like `/app/static-backups` to
45
59
  // satisfy `startsWith('/app/static')` and escape the root.
@@ -105,34 +119,70 @@ export class StaticCore {
105
119
  const baseMimeType = this.mimeTypes[ext.toLowerCase()] || 'application/octet-stream';
106
120
  const contentType = this.addCharsetIfNeeded(baseMimeType);
107
121
  res.setHeader('Content-Type', contentType);
108
- res.setHeader('Content-Length', stats.size);
109
122
  // Cache headers
110
123
  if (this.maxAge > 0) {
111
124
  res.setHeader('Cache-Control', `public, max-age=${this.maxAge}`);
112
125
  }
113
- // ETag support
126
+ if (this.lastModified) {
127
+ res.setHeader('Last-Modified', stats.mtime.toUTCString());
128
+ }
129
+ if (this.acceptRanges) {
130
+ res.setHeader('Accept-Ranges', 'bytes');
131
+ }
132
+ let etag;
114
133
  if (this.etag) {
115
- const etag = crypto
134
+ etag = `"${crypto
116
135
  .createHash('md5')
117
136
  .update(`${stats.mtime.getTime()}-${stats.size}`)
118
- .digest('hex');
119
- res.setHeader('ETag', `"${etag}"`);
120
- // Handle conditional requests
121
- const ifNoneMatch = req.headers['if-none-match'];
122
- if (ifNoneMatch === `"${etag}"`) {
123
- res.statusCode = 304;
137
+ .digest('hex')}"`;
138
+ res.setHeader('ETag', etag);
139
+ }
140
+ // Conditional requests. Content-Length is deliberately not set for a 304 —
141
+ // the response carries no body.
142
+ if (this.isNotModified(req, stats.mtime, etag)) {
143
+ res.statusCode = 304;
144
+ res.end();
145
+ return true;
146
+ }
147
+ const range = this.acceptRanges ? this.resolveRange(req, stats, etag) : null;
148
+ if (range === 'unsatisfiable') {
149
+ res.setHeader('Content-Range', `bytes */${stats.size}`);
150
+ res.status(416).json({ success: false, error: 'Range not satisfiable' });
151
+ return true;
152
+ }
153
+ if (range) {
154
+ const chunkSize = range.end - range.start + 1;
155
+ res.statusCode = 206;
156
+ res.setHeader('Content-Range', `bytes ${range.start}-${range.end}/${stats.size}`);
157
+ res.setHeader('Content-Length', chunkSize);
158
+ if (req.method === 'HEAD') {
124
159
  res.end();
125
160
  return true;
126
161
  }
162
+ if (StaticCore.canStream(res)) {
163
+ await this.streamFile(filePath, res, range);
164
+ }
165
+ else {
166
+ res.end(await StaticCore.readSlice(filePath, range.start, chunkSize));
167
+ }
168
+ return true;
127
169
  }
170
+ res.setHeader('Content-Length', stats.size);
128
171
  // Handle HEAD requests
129
172
  if (req.method === 'HEAD') {
130
173
  res.end();
131
174
  return true;
132
175
  }
133
- // Send file
134
- const data = await fs.readFile(filePath);
135
- res.end(data);
176
+ // Small files take the one-read/one-write path — the common case for
177
+ // assets, and the cheapest. Larger ones stream, so serving a big file
178
+ // doesn't cost its full size in resident memory per request.
179
+ if (stats.size <= StaticCore.STREAM_THRESHOLD || !StaticCore.canStream(res)) {
180
+ const data = await fs.readFile(filePath);
181
+ res.end(data);
182
+ }
183
+ else {
184
+ await this.streamFile(filePath, res);
185
+ }
136
186
  return true;
137
187
  }
138
188
  catch {
@@ -140,6 +190,154 @@ export class StaticCore {
140
190
  return true;
141
191
  }
142
192
  }
193
+ /**
194
+ * Whether the client's cached copy is still current. `If-None-Match` wins
195
+ * over `If-Modified-Since` when both are present (RFC 9110 13.1.3).
196
+ */
197
+ isNotModified(req, mtime, etag) {
198
+ const ifNoneMatch = req.headers['if-none-match'];
199
+ if (etag && ifNoneMatch) {
200
+ if (ifNoneMatch === '*')
201
+ return true;
202
+ return ifNoneMatch
203
+ .split(',')
204
+ .some(candidate => candidate.trim().replace(/^W\//, '') === etag);
205
+ }
206
+ if (this.lastModified) {
207
+ const ifModifiedSince = req.headers['if-modified-since'];
208
+ if (ifModifiedSince) {
209
+ const since = Date.parse(ifModifiedSince);
210
+ // HTTP dates carry second precision, so compare at that resolution
211
+ if (!Number.isNaN(since)) {
212
+ return Math.floor(mtime.getTime() / 1000) <= Math.floor(since / 1000);
213
+ }
214
+ }
215
+ }
216
+ return false;
217
+ }
218
+ /**
219
+ * The byte range to serve, `'unsatisfiable'` for a 416, or null to send the
220
+ * whole entity — which is also the answer for a multi-range request and for
221
+ * an `If-Range` that no longer matches.
222
+ */
223
+ resolveRange(req, stats, etag) {
224
+ const rangeHeader = req.headers.range;
225
+ if (!rangeHeader)
226
+ return null;
227
+ const ifRange = req.headers['if-range'];
228
+ if (ifRange) {
229
+ const asDate = Date.parse(ifRange);
230
+ const matches = (etag !== undefined && ifRange.trim() === etag) ||
231
+ (!Number.isNaN(asDate) &&
232
+ Math.floor(stats.mtime.getTime() / 1000) <= Math.floor(asDate / 1000));
233
+ if (!matches)
234
+ return null;
235
+ }
236
+ const match = /^bytes=(\d*)-(\d*)$/.exec(rangeHeader.trim());
237
+ if (!match)
238
+ return null;
239
+ const size = stats.size;
240
+ const [, rawStart, rawEnd] = match;
241
+ let start;
242
+ let end;
243
+ if (rawStart === '') {
244
+ // Suffix form: the last N bytes
245
+ const suffix = parseInt(rawEnd, 10);
246
+ if (!suffix)
247
+ return 'unsatisfiable';
248
+ start = Math.max(size - suffix, 0);
249
+ end = size - 1;
250
+ }
251
+ else {
252
+ start = parseInt(rawStart, 10);
253
+ end = rawEnd === '' ? size - 1 : Math.min(parseInt(rawEnd, 10), size - 1);
254
+ }
255
+ if (Number.isNaN(start) || Number.isNaN(end) || start > end || start >= size) {
256
+ return 'unsatisfiable';
257
+ }
258
+ return { start, end };
259
+ }
260
+ /**
261
+ * Pipe a file (or a slice of one) to the response, closing it on abort.
262
+ * Resolves once the response is done — the caller must not return before
263
+ * then, or the router would see an untouched response and 404 the request.
264
+ */
265
+ streamFile(filePath, res, range) {
266
+ const stream = range
267
+ ? createReadStream(filePath, { start: range.start, end: range.end })
268
+ : createReadStream(filePath);
269
+ return new Promise(resolve => {
270
+ let settled = false;
271
+ const done = () => {
272
+ if (settled)
273
+ return;
274
+ settled = true;
275
+ resolve();
276
+ };
277
+ // A client that disconnects mid-download must not leak the descriptor
278
+ res.once?.('close', () => {
279
+ stream.destroy();
280
+ done();
281
+ });
282
+ stream.on('error', () => {
283
+ if (!res.headersSent) {
284
+ res.status(500).json({ success: false, error: 'Internal server error' });
285
+ }
286
+ else {
287
+ res.end();
288
+ }
289
+ done();
290
+ });
291
+ stream.on('end', done);
292
+ stream.pipe(res);
293
+ });
294
+ }
295
+ /** Read just the requested slice — the fallback where piping isn't available. */
296
+ static async readSlice(filePath, start, length) {
297
+ const handle = await fs.open(filePath, 'r');
298
+ try {
299
+ const buffer = Buffer.allocUnsafe(length);
300
+ const { bytesRead } = await handle.read(buffer, 0, length, start);
301
+ return bytesRead === length ? buffer : buffer.subarray(0, bytesRead);
302
+ }
303
+ finally {
304
+ await handle.close();
305
+ }
306
+ }
307
+ /** HTTP/2 responses are plain objects with no stream plumbing to pipe into. */
308
+ static canStream(res) {
309
+ const writable = res;
310
+ return typeof writable.write === 'function' && typeof writable.on === 'function';
311
+ }
312
+ /**
313
+ * `/assets/` -> `/assets`, `/` and `''` -> `''` (serve from the URL root).
314
+ * A prefix given without a leading slash still mounts where you'd expect.
315
+ */
316
+ static normalizePrefix(prefix) {
317
+ if (!prefix)
318
+ return '';
319
+ let normalized = prefix.trim();
320
+ if (!normalized.startsWith('/'))
321
+ normalized = `/${normalized}`;
322
+ while (normalized.length > 1 && normalized.endsWith('/')) {
323
+ normalized = normalized.slice(0, -1);
324
+ }
325
+ return normalized === '/' ? '' : normalized;
326
+ }
327
+ /**
328
+ * Request path relative to the mount point, or `null` when the request falls
329
+ * outside it. `/assets` itself maps to `/` so the directory index still
330
+ * resolves; `/assetsfoo` is a different route and is left alone.
331
+ */
332
+ stripPrefix(reqPath) {
333
+ if (!this.prefix)
334
+ return reqPath;
335
+ if (reqPath === this.prefix)
336
+ return '/';
337
+ if (reqPath.startsWith(`${this.prefix}/`))
338
+ return reqPath.slice(this.prefix.length);
339
+ return null;
340
+ }
143
341
  addCharsetIfNeeded(mimeType) {
144
342
  const textTypes = [
145
343
  'text/',
@@ -154,5 +352,8 @@ export class StaticCore {
154
352
  getRoot() {
155
353
  return this.root;
156
354
  }
355
+ getPrefix() {
356
+ return this.prefix;
357
+ }
157
358
  }
158
359
  //# sourceMappingURL=core.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"core.js","sourceRoot":"","sources":["../../../../../src/core/middleware/built-in/static/core.ts"],"names":[],"mappings":"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","sourcesContent":["// Static File Serving Core Logic\nimport { HttpRequest, HttpResponse } from '../../../../types/http.js';\nimport * as fs from 'fs/promises';\nimport * as path from 'path';\nimport * as crypto from 'crypto';\n\nexport interface StaticOptions {\n root: string;\n maxAge?: number;\n index?: string[];\n dotfiles?: 'allow' | 'deny' | 'ignore';\n etag?: boolean;\n}\n\nexport class StaticCore {\n private root: string;\n private maxAge: number;\n private index: string[];\n private dotfiles: 'allow' | 'deny' | 'ignore';\n private etag: boolean;\n\n private mimeTypes: Record<string, string> = {\n '.html': 'text/html',\n '.css': 'text/css',\n '.js': 'application/javascript',\n '.json': 'application/json',\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.gif': 'image/gif',\n '.svg': 'image/svg+xml',\n '.ico': 'image/x-icon',\n '.pdf': 'application/pdf',\n '.txt': 'text/plain',\n '.xml': 'application/xml',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n '.ttf': 'font/ttf',\n '.eot': 'application/vnd.ms-fontobject',\n };\n\n constructor(options: StaticOptions) {\n this.root = path.resolve(options.root);\n this.maxAge = options.maxAge || 0;\n this.index = options.index || ['index.html', 'index.htm'];\n this.dotfiles = options.dotfiles || 'ignore';\n this.etag = options.etag !== false;\n }\n\n async handleRequest(req: HttpRequest, res: HttpResponse): Promise<boolean> {\n // Only handle GET and HEAD requests\n if (req.method !== 'GET' && req.method !== 'HEAD') {\n return false;\n }\n\n try {\n let filePath = path.join(this.root, req.path);\n\n // Security: prevent directory traversal. The trailing separator is required —\n // a bare prefix check would allow sibling dirs like `/app/static-backups` to\n // satisfy `startsWith('/app/static')` and escape the root.\n const rootWithSep = this.root.endsWith(path.sep) ? this.root : this.root + path.sep;\n if (filePath !== this.root && !filePath.startsWith(rootWithSep)) {\n res.status(403).json({ success: false, error: 'Forbidden' });\n return true;\n }\n\n // Security: resolve symlinks and re-check path to prevent symlink-based traversal\n try {\n const realPath = await fs.realpath(filePath);\n if (realPath !== this.root && !realPath.startsWith(rootWithSep)) {\n res.status(403).json({ success: false, error: 'Forbidden' });\n return true;\n }\n filePath = realPath;\n } catch {\n // realpath fails if file doesn't exist — handled by stat below\n }\n\n // Handle dotfiles\n const basename = path.basename(filePath);\n if (basename.startsWith('.')) {\n if (this.dotfiles === 'deny') {\n res.status(403).json({ success: false, error: 'Forbidden' });\n return true;\n } else if (this.dotfiles === 'ignore') {\n return false;\n }\n }\n\n let stats;\n try {\n stats = await fs.stat(filePath);\n } catch {\n return false; // File not found, let other middleware handle\n }\n\n // Handle directories\n if (stats.isDirectory()) {\n let indexFound = false;\n\n for (const indexFile of this.index) {\n const indexPath = path.join(filePath, indexFile);\n try {\n const indexStats = await fs.stat(indexPath);\n if (indexStats.isFile()) {\n filePath = indexPath;\n stats = indexStats;\n indexFound = true;\n break;\n }\n } catch {\n // Continue to next index file\n }\n }\n\n if (!indexFound) {\n return false;\n }\n }\n\n // Get mime type and add charset for text files\n const ext = path.extname(filePath);\n const baseMimeType = this.mimeTypes[ext.toLowerCase()] || 'application/octet-stream';\n const contentType = this.addCharsetIfNeeded(baseMimeType);\n\n res.setHeader('Content-Type', contentType);\n res.setHeader('Content-Length', stats.size);\n\n // Cache headers\n if (this.maxAge > 0) {\n res.setHeader('Cache-Control', `public, max-age=${this.maxAge}`);\n }\n\n // ETag support\n if (this.etag) {\n const etag = crypto\n .createHash('md5')\n .update(`${stats.mtime.getTime()}-${stats.size}`)\n .digest('hex');\n res.setHeader('ETag', `\"${etag}\"`);\n\n // Handle conditional requests\n const ifNoneMatch = req.headers['if-none-match'];\n if (ifNoneMatch === `\"${etag}\"`) {\n res.statusCode = 304;\n res.end();\n return true;\n }\n }\n\n // Handle HEAD requests\n if (req.method === 'HEAD') {\n res.end();\n return true;\n }\n\n // Send file\n const data = await fs.readFile(filePath);\n res.end(data);\n return true;\n } catch {\n res.status(500).json({ success: false, error: 'Internal server error' });\n return true;\n }\n }\n\n private addCharsetIfNeeded(mimeType: string): string {\n const textTypes = [\n 'text/',\n 'application/json',\n 'application/javascript',\n 'application/xml',\n 'image/svg+xml',\n ];\n const needsCharset = textTypes.some(type => mimeType.startsWith(type));\n return needsCharset ? `${mimeType}; charset=utf-8` : mimeType;\n }\n\n getRoot(): string {\n return this.root;\n }\n}\n"]}
1
+ {"version":3,"file":"core.js","sourceRoot":"","sources":["../../../../../src/core/middleware/built-in/static/core.ts"],"names":[],"mappings":"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","sourcesContent":["// Static File Serving Core Logic\nimport { HttpRequest, HttpResponse } from '../../../../types/http.js';\nimport * as fs from 'fs/promises';\nimport { createReadStream } from 'fs';\nimport * as path from 'path';\nimport * as crypto from 'crypto';\n\nexport interface StaticOptions {\n root: string;\n /**\n * URL path the files are served under, e.g. `/assets`. Requests outside the\n * prefix fall through untouched; inside it, the prefix is stripped before the\n * path is resolved against `root` (`/assets/app.css` -> `<root>/app.css`).\n * Matching is exact and case-sensitive. Defaults to the URL root.\n */\n prefix?: string;\n maxAge?: number;\n index?: string[];\n dotfiles?: 'allow' | 'deny' | 'ignore';\n etag?: boolean;\n /**\n * Send `Last-Modified` and answer `If-Modified-Since` with a 304.\n * Default true.\n */\n lastModified?: boolean;\n /**\n * Advertise `Accept-Ranges: bytes` and serve `Range` requests as 206\n * responses — what media seeking and resumable downloads need. A request for\n * multiple ranges is answered with the whole entity. Default true.\n */\n acceptRanges?: boolean;\n}\n\nexport class StaticCore {\n private root: string;\n private prefix: string;\n private maxAge: number;\n private index: string[];\n private dotfiles: 'allow' | 'deny' | 'ignore';\n private etag: boolean;\n private lastModified: boolean;\n private acceptRanges: boolean;\n\n /** Files at or below this size are read in one go instead of streamed. */\n private static readonly STREAM_THRESHOLD = 512 * 1024;\n\n private mimeTypes: Record<string, string> = {\n '.html': 'text/html',\n '.css': 'text/css',\n '.js': 'application/javascript',\n '.json': 'application/json',\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.gif': 'image/gif',\n '.svg': 'image/svg+xml',\n '.ico': 'image/x-icon',\n '.pdf': 'application/pdf',\n '.txt': 'text/plain',\n '.xml': 'application/xml',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n '.ttf': 'font/ttf',\n '.eot': 'application/vnd.ms-fontobject',\n };\n\n constructor(options: StaticOptions) {\n this.root = path.resolve(options.root);\n this.prefix = StaticCore.normalizePrefix(options.prefix);\n this.maxAge = options.maxAge || 0;\n this.index = options.index || ['index.html', 'index.htm'];\n this.dotfiles = options.dotfiles || 'ignore';\n this.etag = options.etag !== false;\n this.lastModified = options.lastModified !== false;\n this.acceptRanges = options.acceptRanges !== false;\n }\n\n async handleRequest(req: HttpRequest, res: HttpResponse): Promise<boolean> {\n // Only handle GET and HEAD requests\n if (req.method !== 'GET' && req.method !== 'HEAD') {\n return false;\n }\n\n // Strip the mount prefix; anything outside it is not ours to serve\n const urlPath = this.stripPrefix(req.path);\n if (urlPath === null) {\n return false;\n }\n\n try {\n let filePath = path.join(this.root, urlPath);\n\n // Security: prevent directory traversal. The trailing separator is required —\n // a bare prefix check would allow sibling dirs like `/app/static-backups` to\n // satisfy `startsWith('/app/static')` and escape the root.\n const rootWithSep = this.root.endsWith(path.sep) ? this.root : this.root + path.sep;\n if (filePath !== this.root && !filePath.startsWith(rootWithSep)) {\n res.status(403).json({ success: false, error: 'Forbidden' });\n return true;\n }\n\n // Security: resolve symlinks and re-check path to prevent symlink-based traversal\n try {\n const realPath = await fs.realpath(filePath);\n if (realPath !== this.root && !realPath.startsWith(rootWithSep)) {\n res.status(403).json({ success: false, error: 'Forbidden' });\n return true;\n }\n filePath = realPath;\n } catch {\n // realpath fails if file doesn't exist — handled by stat below\n }\n\n // Handle dotfiles\n const basename = path.basename(filePath);\n if (basename.startsWith('.')) {\n if (this.dotfiles === 'deny') {\n res.status(403).json({ success: false, error: 'Forbidden' });\n return true;\n } else if (this.dotfiles === 'ignore') {\n return false;\n }\n }\n\n let stats;\n try {\n stats = await fs.stat(filePath);\n } catch {\n return false; // File not found, let other middleware handle\n }\n\n // Handle directories\n if (stats.isDirectory()) {\n let indexFound = false;\n\n for (const indexFile of this.index) {\n const indexPath = path.join(filePath, indexFile);\n try {\n const indexStats = await fs.stat(indexPath);\n if (indexStats.isFile()) {\n filePath = indexPath;\n stats = indexStats;\n indexFound = true;\n break;\n }\n } catch {\n // Continue to next index file\n }\n }\n\n if (!indexFound) {\n return false;\n }\n }\n\n // Get mime type and add charset for text files\n const ext = path.extname(filePath);\n const baseMimeType = this.mimeTypes[ext.toLowerCase()] || 'application/octet-stream';\n const contentType = this.addCharsetIfNeeded(baseMimeType);\n\n res.setHeader('Content-Type', contentType);\n\n // Cache headers\n if (this.maxAge > 0) {\n res.setHeader('Cache-Control', `public, max-age=${this.maxAge}`);\n }\n\n if (this.lastModified) {\n res.setHeader('Last-Modified', stats.mtime.toUTCString());\n }\n\n if (this.acceptRanges) {\n res.setHeader('Accept-Ranges', 'bytes');\n }\n\n let etag: string | undefined;\n if (this.etag) {\n etag = `\"${crypto\n .createHash('md5')\n .update(`${stats.mtime.getTime()}-${stats.size}`)\n .digest('hex')}\"`;\n res.setHeader('ETag', etag);\n }\n\n // Conditional requests. Content-Length is deliberately not set for a 304 —\n // the response carries no body.\n if (this.isNotModified(req, stats.mtime, etag)) {\n res.statusCode = 304;\n res.end();\n return true;\n }\n\n const range = this.acceptRanges ? this.resolveRange(req, stats, etag) : null;\n\n if (range === 'unsatisfiable') {\n res.setHeader('Content-Range', `bytes */${stats.size}`);\n res.status(416).json({ success: false, error: 'Range not satisfiable' });\n return true;\n }\n\n if (range) {\n const chunkSize = range.end - range.start + 1;\n res.statusCode = 206;\n res.setHeader('Content-Range', `bytes ${range.start}-${range.end}/${stats.size}`);\n res.setHeader('Content-Length', chunkSize);\n\n if (req.method === 'HEAD') {\n res.end();\n return true;\n }\n\n if (StaticCore.canStream(res)) {\n await this.streamFile(filePath, res, range);\n } else {\n res.end(await StaticCore.readSlice(filePath, range.start, chunkSize));\n }\n return true;\n }\n\n res.setHeader('Content-Length', stats.size);\n\n // Handle HEAD requests\n if (req.method === 'HEAD') {\n res.end();\n return true;\n }\n\n // Small files take the one-read/one-write path — the common case for\n // assets, and the cheapest. Larger ones stream, so serving a big file\n // doesn't cost its full size in resident memory per request.\n if (stats.size <= StaticCore.STREAM_THRESHOLD || !StaticCore.canStream(res)) {\n const data = await fs.readFile(filePath);\n res.end(data);\n } else {\n await this.streamFile(filePath, res);\n }\n return true;\n } catch {\n res.status(500).json({ success: false, error: 'Internal server error' });\n return true;\n }\n }\n\n /**\n * Whether the client's cached copy is still current. `If-None-Match` wins\n * over `If-Modified-Since` when both are present (RFC 9110 13.1.3).\n */\n private isNotModified(req: HttpRequest, mtime: Date, etag?: string): boolean {\n const ifNoneMatch = req.headers['if-none-match'];\n if (etag && ifNoneMatch) {\n if (ifNoneMatch === '*') return true;\n return ifNoneMatch\n .split(',')\n .some(candidate => candidate.trim().replace(/^W\\//, '') === etag);\n }\n\n if (this.lastModified) {\n const ifModifiedSince = req.headers['if-modified-since'];\n if (ifModifiedSince) {\n const since = Date.parse(ifModifiedSince);\n // HTTP dates carry second precision, so compare at that resolution\n if (!Number.isNaN(since)) {\n return Math.floor(mtime.getTime() / 1000) <= Math.floor(since / 1000);\n }\n }\n }\n\n return false;\n }\n\n /**\n * The byte range to serve, `'unsatisfiable'` for a 416, or null to send the\n * whole entity — which is also the answer for a multi-range request and for\n * an `If-Range` that no longer matches.\n */\n private resolveRange(\n req: HttpRequest,\n stats: { size: number; mtime: Date },\n etag?: string\n ): { start: number; end: number } | 'unsatisfiable' | null {\n const rangeHeader = req.headers.range;\n if (!rangeHeader) return null;\n\n const ifRange = req.headers['if-range'];\n if (ifRange) {\n const asDate = Date.parse(ifRange);\n const matches =\n (etag !== undefined && ifRange.trim() === etag) ||\n (!Number.isNaN(asDate) &&\n Math.floor(stats.mtime.getTime() / 1000) <= Math.floor(asDate / 1000));\n if (!matches) return null;\n }\n\n const match = /^bytes=(\\d*)-(\\d*)$/.exec(rangeHeader.trim());\n if (!match) return null;\n\n const size = stats.size;\n const [, rawStart, rawEnd] = match;\n let start: number;\n let end: number;\n\n if (rawStart === '') {\n // Suffix form: the last N bytes\n const suffix = parseInt(rawEnd as string, 10);\n if (!suffix) return 'unsatisfiable';\n start = Math.max(size - suffix, 0);\n end = size - 1;\n } else {\n start = parseInt(rawStart as string, 10);\n end = rawEnd === '' ? size - 1 : Math.min(parseInt(rawEnd as string, 10), size - 1);\n }\n\n if (Number.isNaN(start) || Number.isNaN(end) || start > end || start >= size) {\n return 'unsatisfiable';\n }\n\n return { start, end };\n }\n\n /**\n * Pipe a file (or a slice of one) to the response, closing it on abort.\n * Resolves once the response is done — the caller must not return before\n * then, or the router would see an untouched response and 404 the request.\n */\n private streamFile(\n filePath: string,\n res: HttpResponse,\n range?: { start: number; end: number }\n ): Promise<void> {\n const stream = range\n ? createReadStream(filePath, { start: range.start, end: range.end })\n : createReadStream(filePath);\n\n return new Promise<void>(resolve => {\n let settled = false;\n const done = () => {\n if (settled) return;\n settled = true;\n resolve();\n };\n\n // A client that disconnects mid-download must not leak the descriptor\n (res as any).once?.('close', () => {\n stream.destroy();\n done();\n });\n\n stream.on('error', () => {\n if (!res.headersSent) {\n res.status(500).json({ success: false, error: 'Internal server error' });\n } else {\n res.end();\n }\n done();\n });\n\n stream.on('end', done);\n stream.pipe(res as any);\n });\n }\n\n /** Read just the requested slice — the fallback where piping isn't available. */\n private static async readSlice(filePath: string, start: number, length: number): Promise<Buffer> {\n const handle = await fs.open(filePath, 'r');\n try {\n const buffer = Buffer.allocUnsafe(length);\n const { bytesRead } = await handle.read(buffer, 0, length, start);\n return bytesRead === length ? buffer : buffer.subarray(0, bytesRead);\n } finally {\n await handle.close();\n }\n }\n\n /** HTTP/2 responses are plain objects with no stream plumbing to pipe into. */\n private static canStream(res: HttpResponse): boolean {\n const writable = res as any;\n return typeof writable.write === 'function' && typeof writable.on === 'function';\n }\n\n /**\n * `/assets/` -> `/assets`, `/` and `''` -> `''` (serve from the URL root).\n * A prefix given without a leading slash still mounts where you'd expect.\n */\n private static normalizePrefix(prefix?: string): string {\n if (!prefix) return '';\n let normalized = prefix.trim();\n if (!normalized.startsWith('/')) normalized = `/${normalized}`;\n while (normalized.length > 1 && normalized.endsWith('/')) {\n normalized = normalized.slice(0, -1);\n }\n return normalized === '/' ? '' : normalized;\n }\n\n /**\n * Request path relative to the mount point, or `null` when the request falls\n * outside it. `/assets` itself maps to `/` so the directory index still\n * resolves; `/assetsfoo` is a different route and is left alone.\n */\n private stripPrefix(reqPath: string): string | null {\n if (!this.prefix) return reqPath;\n if (reqPath === this.prefix) return '/';\n if (reqPath.startsWith(`${this.prefix}/`)) return reqPath.slice(this.prefix.length);\n return null;\n }\n\n private addCharsetIfNeeded(mimeType: string): string {\n const textTypes = [\n 'text/',\n 'application/json',\n 'application/javascript',\n 'application/xml',\n 'image/svg+xml',\n ];\n const needsCharset = textTypes.some(type => mimeType.startsWith(type));\n return needsCharset ? `${mimeType}; charset=utf-8` : mimeType;\n }\n\n getRoot(): string {\n return this.root;\n }\n\n getPrefix(): string {\n return this.prefix;\n }\n}\n"]}
@@ -13,6 +13,11 @@ import { StaticOptions } from './core.js';
13
13
  * index: ['index.html'],
14
14
  * etag: true,
15
15
  * }));
16
+ *
17
+ * // Mounted under a URL prefix — ./public/app.css is served as /cdn/app.css.
18
+ * // Note the prefix goes in the options, not as app.use('/cdn', ...), which
19
+ * // only mounts createRouter() instances.
20
+ * app.use(staticFiles({ root: './public', prefix: '/cdn' }));
16
21
  * ```
17
22
  */
18
23
  export declare function createStaticMiddleware(options: StaticOptions): Middleware;
@@ -12,6 +12,11 @@ import { StaticCore } from './core.js';
12
12
  * index: ['index.html'],
13
13
  * etag: true,
14
14
  * }));
15
+ *
16
+ * // Mounted under a URL prefix — ./public/app.css is served as /cdn/app.css.
17
+ * // Note the prefix goes in the options, not as app.use('/cdn', ...), which
18
+ * // only mounts createRouter() instances.
19
+ * app.use(staticFiles({ root: './public', prefix: '/cdn' }));
15
20
  * ```
16
21
  */
17
22
  export function createStaticMiddleware(options) {
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../../../../src/core/middleware/built-in/static/middleware.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAiB,MAAM,WAAW,CAAC;AAEtD;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,sBAAsB,CAAC,OAAsB;IAC3D,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,OAAO,CAAC,CAAC;IAErC,OAAO,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QAC9B,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACnD,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,IAAI,EAAE,CAAC;QACT,CAAC;IACH,CAAC,CAAC;AACJ,CAAC","sourcesContent":["// Static File Serving Middleware\nimport { Middleware } from '../../../../types/http.js';\nimport { StaticCore, StaticOptions } from './core.js';\n\n/**\n * Create static file serving middleware\n *\n * @example\n * ```typescript\n * import { staticFiles } from '@morojs/moro';\n *\n * app.use(staticFiles({\n * root: './public',\n * maxAge: 3600, // 1 hour cache\n * index: ['index.html'],\n * etag: true,\n * }));\n * ```\n */\nexport function createStaticMiddleware(options: StaticOptions): Middleware {\n const core = new StaticCore(options);\n\n return async (req, res, next) => {\n const handled = await core.handleRequest(req, res);\n if (!handled) {\n next();\n }\n };\n}\n"]}
1
+ {"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../../../../src/core/middleware/built-in/static/middleware.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAiB,MAAM,WAAW,CAAC;AAEtD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,sBAAsB,CAAC,OAAsB;IAC3D,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,OAAO,CAAC,CAAC;IAErC,OAAO,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QAC9B,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,aAAa,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACnD,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,IAAI,EAAE,CAAC;QACT,CAAC;IACH,CAAC,CAAC;AACJ,CAAC","sourcesContent":["// Static File Serving Middleware\nimport { Middleware } from '../../../../types/http.js';\nimport { StaticCore, StaticOptions } from './core.js';\n\n/**\n * Create static file serving middleware\n *\n * @example\n * ```typescript\n * import { staticFiles } from '@morojs/moro';\n *\n * app.use(staticFiles({\n * root: './public',\n * maxAge: 3600, // 1 hour cache\n * index: ['index.html'],\n * etag: true,\n * }));\n *\n * // Mounted under a URL prefix — ./public/app.css is served as /cdn/app.css.\n * // Note the prefix goes in the options, not as app.use('/cdn', ...), which\n * // only mounts createRouter() instances.\n * app.use(staticFiles({ root: './public', prefix: '/cdn' }));\n * ```\n */\nexport function createStaticMiddleware(options: StaticOptions): Middleware {\n const core = new StaticCore(options);\n\n return async (req, res, next) => {\n const handled = await core.handleRequest(req, res);\n if (!handled) {\n next();\n }\n };\n}\n"]}
@@ -1,5 +1,10 @@
1
1
  import { HttpRequest } from '../../../../types/http.js';
2
2
  export interface UploadOptions {
3
+ /**
4
+ * Directory to write uploads to. Files are also kept in memory on
5
+ * `file.data`; set this when you want them on disk, and each file gains a
6
+ * `path`. Left unset, nothing is written.
7
+ */
3
8
  dest?: string;
4
9
  maxFileSize?: number;
5
10
  maxFiles?: number;
@@ -10,6 +15,10 @@ export interface UploadedFile {
10
15
  mimetype: string;
11
16
  data: Buffer;
12
17
  size: number;
18
+ /** Absolute path of the written file — present only when `dest` is set. */
19
+ path?: string;
20
+ /** Directory the file was written to. */
21
+ destination?: string;
13
22
  }
14
23
  export declare class UploadCore {
15
24
  private dest;
@@ -22,6 +31,14 @@ export declare class UploadCore {
22
31
  error?: string;
23
32
  };
24
33
  attachFiles(req: HttpRequest): void;
34
+ /**
35
+ * Write the uploaded files to `dest`, recording where each one landed.
36
+ * Stored names are randomized, so two uploads of the same filename can't
37
+ * overwrite one another and a crafted name can't pick its own destination.
38
+ * A no-op when no `dest` is configured.
39
+ */
40
+ persistFiles(files: Record<string, any>): Promise<void>;
41
+ getDest(): string | undefined;
25
42
  private formatSize;
26
43
  getMaxFileSize(): number;
27
44
  getMaxFiles(): number;
@@ -1,4 +1,6 @@
1
1
  // File Upload Core Logic
2
+ import * as crypto from 'crypto';
3
+ import * as fs from 'fs/promises';
2
4
  import * as path from 'path';
3
5
  // Pre-compiled regex — avoids recompilation per file
4
6
  // eslint-disable-next-line no-control-regex
@@ -9,7 +11,9 @@ export class UploadCore {
9
11
  maxFiles;
10
12
  allowedTypes;
11
13
  constructor(options = {}) {
12
- this.dest = options.dest || '/tmp';
14
+ // Undefined by default: writing every upload to /tmp behind the user's back
15
+ // would be a surprise, so files only hit disk when a dest is asked for.
16
+ this.dest = options.dest;
13
17
  this.maxFileSize = options.maxFileSize || 5 * 1024 * 1024; // 5MB default
14
18
  this.maxFiles = options.maxFiles || 10;
15
19
  this.allowedTypes = options.allowedTypes;
@@ -57,6 +61,31 @@ export class UploadCore {
57
61
  req.files = req.body.files;
58
62
  }
59
63
  }
64
+ /**
65
+ * Write the uploaded files to `dest`, recording where each one landed.
66
+ * Stored names are randomized, so two uploads of the same filename can't
67
+ * overwrite one another and a crafted name can't pick its own destination.
68
+ * A no-op when no `dest` is configured.
69
+ */
70
+ async persistFiles(files) {
71
+ if (!this.dest)
72
+ return;
73
+ const destination = path.resolve(this.dest);
74
+ await fs.mkdir(destination, { recursive: true });
75
+ for (const [, file] of Object.entries(files)) {
76
+ const uploaded = file;
77
+ if (!uploaded.data)
78
+ continue;
79
+ const safeName = path.basename(uploaded.filename || 'upload').replace(RE_CONTROL_CHARS, '') || 'upload';
80
+ const target = path.join(destination, `${crypto.randomBytes(8).toString('hex')}-${safeName}`);
81
+ await fs.writeFile(target, uploaded.data);
82
+ uploaded.path = target;
83
+ uploaded.destination = destination;
84
+ }
85
+ }
86
+ getDest() {
87
+ return this.dest;
88
+ }
60
89
  formatSize(bytes) {
61
90
  if (bytes >= 1024 * 1024) {
62
91
  return `${(bytes / (1024 * 1024)).toFixed(2)}MB`;
@@ -1 +1 @@
1
- {"version":3,"file":"core.js","sourceRoot":"","sources":["../../../../../src/core/middleware/built-in/upload/core.ts"],"names":[],"mappings":"AAAA,yBAAyB;AACzB,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAG7B,qDAAqD;AACrD,4CAA4C;AAC5C,MAAM,gBAAgB,GAAG,cAAc,CAAC;AAgBxC,MAAM,OAAO,UAAU;IACb,IAAI,CAAS;IACb,WAAW,CAAS;IACpB,QAAQ,CAAS;IACjB,YAAY,CAAwB;IAE5C,YAAY,UAAyB,EAAE;QACrC,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,MAAM,CAAC;QACnC,IAAI,CAAC,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,cAAc;QACzE,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC;QACvC,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,YAAY,CAAC;IAC3C,CAAC;IAED,cAAc,CAAC,KAA0B;QACvC,sBAAsB;QACtB,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC;QAC5C,IAAI,SAAS,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC9B,OAAO;gBACL,KAAK,EAAE,KAAK;gBACZ,KAAK,EAAE,2BAA2B,IAAI,CAAC,QAAQ,WAAW;aAC3D,CAAC;QACJ,CAAC;QAED,qBAAqB;QACrB,6DAA6D;QAC7D,KAAK,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACtD,MAAM,QAAQ,GAAG,IAAoB,CAAC;YAEtC,qBAAqB;YACrB,IAAI,QAAQ,CAAC,IAAI,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;gBACrC,OAAO;oBACL,KAAK,EAAE,KAAK;oBACZ,KAAK,EAAE,QAAQ,QAAQ,CAAC,QAAQ,0BAA0B,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC,WAAW;iBACvG,CAAC;YACJ,CAAC;YAED,qBAAqB;YACrB,IAAI,IAAI,CAAC,YAAY,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACxE,OAAO;oBACL,KAAK,EAAE,KAAK;oBACZ,KAAK,EAAE,aAAa,QAAQ,CAAC,QAAQ,eAAe;iBACrD,CAAC;YACJ,CAAC;QACH,CAAC;QAED,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IACzB,CAAC;IAED,WAAW,CAAC,GAAgB;QAC1B,IAAI,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;YAC/B,oEAAoE;YACpE,KAAK,MAAM,CAAC,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;gBACtD,MAAM,CAAC,GAAG,IAAoB,CAAC;gBAC/B,IAAI,CAAC,CAAC,QAAQ,EAAE,CAAC;oBACd,CAAS,CAAC,gBAAgB,GAAG,CAAC,CAAC,QAAQ,CAAC;oBACzC,CAAC,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,gBAAgB,EAAE,EAAE,CAAC,CAAC;gBACvE,CAAC;YACH,CAAC;YACD,GAAG,CAAC,KAAK,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC;QAC7B,CAAC;IACH,CAAC;IAEO,UAAU,CAAC,KAAa;QAC9B,IAAI,KAAK,IAAI,IAAI,GAAG,IAAI,EAAE,CAAC;YACzB,OAAO,GAAG,CAAC,KAAK,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;QACnD,CAAC;aAAM,IAAI,KAAK,IAAI,IAAI,EAAE,CAAC;YACzB,OAAO,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;QAC1C,CAAC;QACD,OAAO,GAAG,KAAK,GAAG,CAAC;IACrB,CAAC;IAED,cAAc;QACZ,OAAO,IAAI,CAAC,WAAW,CAAC;IAC1B,CAAC;IAED,WAAW;QACT,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;IAED,eAAe;QACb,OAAO,IAAI,CAAC,YAAY,CAAC;IAC3B,CAAC;CACF","sourcesContent":["// File Upload Core Logic\nimport * as path from 'path';\nimport { HttpRequest } from '../../../../types/http.js';\n\n// Pre-compiled regex — avoids recompilation per file\n// eslint-disable-next-line no-control-regex\nconst RE_CONTROL_CHARS = /[\\x00-\\x1f]/g;\n\nexport interface UploadOptions {\n dest?: string;\n maxFileSize?: number;\n maxFiles?: number;\n allowedTypes?: string[];\n}\n\nexport interface UploadedFile {\n filename: string;\n mimetype: string;\n data: Buffer;\n size: number;\n}\n\nexport class UploadCore {\n private dest: string;\n private maxFileSize: number;\n private maxFiles: number;\n private allowedTypes?: string[] | undefined;\n\n constructor(options: UploadOptions = {}) {\n this.dest = options.dest || '/tmp';\n this.maxFileSize = options.maxFileSize || 5 * 1024 * 1024; // 5MB default\n this.maxFiles = options.maxFiles || 10;\n this.allowedTypes = options.allowedTypes;\n }\n\n validateUpload(files: Record<string, any>): { valid: boolean; error?: string } {\n // Validate file count\n const fileCount = Object.keys(files).length;\n if (fileCount > this.maxFiles) {\n return {\n valid: false,\n error: `Too many files. Maximum ${this.maxFiles} allowed.`,\n };\n }\n\n // Validate each file\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n for (const [fieldName, file] of Object.entries(files)) {\n const fileData = file as UploadedFile;\n\n // Validate file size\n if (fileData.size > this.maxFileSize) {\n return {\n valid: false,\n error: `File ${fileData.filename} is too large. Maximum ${this.formatSize(this.maxFileSize)} allowed.`,\n };\n }\n\n // Validate file type\n if (this.allowedTypes && !this.allowedTypes.includes(fileData.mimetype)) {\n return {\n valid: false,\n error: `File type ${fileData.mimetype} not allowed.`,\n };\n }\n }\n\n return { valid: true };\n }\n\n attachFiles(req: HttpRequest): void {\n if (req.body && req.body.files) {\n // Sanitize filenames to prevent path traversal when writing to disk\n for (const [, file] of Object.entries(req.body.files)) {\n const f = file as UploadedFile;\n if (f.filename) {\n (f as any).originalFilename = f.filename;\n f.filename = path.basename(f.filename).replace(RE_CONTROL_CHARS, '');\n }\n }\n req.files = req.body.files;\n }\n }\n\n private formatSize(bytes: number): string {\n if (bytes >= 1024 * 1024) {\n return `${(bytes / (1024 * 1024)).toFixed(2)}MB`;\n } else if (bytes >= 1024) {\n return `${(bytes / 1024).toFixed(2)}KB`;\n }\n return `${bytes}B`;\n }\n\n getMaxFileSize(): number {\n return this.maxFileSize;\n }\n\n getMaxFiles(): number {\n return this.maxFiles;\n }\n\n getAllowedTypes(): string[] | undefined {\n return this.allowedTypes;\n }\n}\n"]}
1
+ {"version":3,"file":"core.js","sourceRoot":"","sources":["../../../../../src/core/middleware/built-in/upload/core.ts"],"names":[],"mappings":"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","sourcesContent":["// File Upload Core Logic\nimport * as crypto from 'crypto';\nimport * as fs from 'fs/promises';\nimport * as path from 'path';\nimport { HttpRequest } from '../../../../types/http.js';\n\n// Pre-compiled regex — avoids recompilation per file\n// eslint-disable-next-line no-control-regex\nconst RE_CONTROL_CHARS = /[\\x00-\\x1f]/g;\n\nexport interface UploadOptions {\n /**\n * Directory to write uploads to. Files are also kept in memory on\n * `file.data`; set this when you want them on disk, and each file gains a\n * `path`. Left unset, nothing is written.\n */\n dest?: string;\n maxFileSize?: number;\n maxFiles?: number;\n allowedTypes?: string[];\n}\n\nexport interface UploadedFile {\n filename: string;\n mimetype: string;\n data: Buffer;\n size: number;\n /** Absolute path of the written file — present only when `dest` is set. */\n path?: string;\n /** Directory the file was written to. */\n destination?: string;\n}\n\nexport class UploadCore {\n private dest: string | undefined;\n private maxFileSize: number;\n private maxFiles: number;\n private allowedTypes?: string[] | undefined;\n\n constructor(options: UploadOptions = {}) {\n // Undefined by default: writing every upload to /tmp behind the user's back\n // would be a surprise, so files only hit disk when a dest is asked for.\n this.dest = options.dest;\n this.maxFileSize = options.maxFileSize || 5 * 1024 * 1024; // 5MB default\n this.maxFiles = options.maxFiles || 10;\n this.allowedTypes = options.allowedTypes;\n }\n\n validateUpload(files: Record<string, any>): { valid: boolean; error?: string } {\n // Validate file count\n const fileCount = Object.keys(files).length;\n if (fileCount > this.maxFiles) {\n return {\n valid: false,\n error: `Too many files. Maximum ${this.maxFiles} allowed.`,\n };\n }\n\n // Validate each file\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n for (const [fieldName, file] of Object.entries(files)) {\n const fileData = file as UploadedFile;\n\n // Validate file size\n if (fileData.size > this.maxFileSize) {\n return {\n valid: false,\n error: `File ${fileData.filename} is too large. Maximum ${this.formatSize(this.maxFileSize)} allowed.`,\n };\n }\n\n // Validate file type\n if (this.allowedTypes && !this.allowedTypes.includes(fileData.mimetype)) {\n return {\n valid: false,\n error: `File type ${fileData.mimetype} not allowed.`,\n };\n }\n }\n\n return { valid: true };\n }\n\n attachFiles(req: HttpRequest): void {\n if (req.body && req.body.files) {\n // Sanitize filenames to prevent path traversal when writing to disk\n for (const [, file] of Object.entries(req.body.files)) {\n const f = file as UploadedFile;\n if (f.filename) {\n (f as any).originalFilename = f.filename;\n f.filename = path.basename(f.filename).replace(RE_CONTROL_CHARS, '');\n }\n }\n req.files = req.body.files;\n }\n }\n\n /**\n * Write the uploaded files to `dest`, recording where each one landed.\n * Stored names are randomized, so two uploads of the same filename can't\n * overwrite one another and a crafted name can't pick its own destination.\n * A no-op when no `dest` is configured.\n */\n async persistFiles(files: Record<string, any>): Promise<void> {\n if (!this.dest) return;\n\n const destination = path.resolve(this.dest);\n await fs.mkdir(destination, { recursive: true });\n\n for (const [, file] of Object.entries(files)) {\n const uploaded = file as UploadedFile;\n if (!uploaded.data) continue;\n\n const safeName =\n path.basename(uploaded.filename || 'upload').replace(RE_CONTROL_CHARS, '') || 'upload';\n const target = path.join(destination, `${crypto.randomBytes(8).toString('hex')}-${safeName}`);\n\n await fs.writeFile(target, uploaded.data);\n uploaded.path = target;\n uploaded.destination = destination;\n }\n }\n\n getDest(): string | undefined {\n return this.dest;\n }\n\n private formatSize(bytes: number): string {\n if (bytes >= 1024 * 1024) {\n return `${(bytes / (1024 * 1024)).toFixed(2)}MB`;\n } else if (bytes >= 1024) {\n return `${(bytes / 1024).toFixed(2)}KB`;\n }\n return `${bytes}B`;\n }\n\n getMaxFileSize(): number {\n return this.maxFileSize;\n }\n\n getMaxFiles(): number {\n return this.maxFiles;\n }\n\n getAllowedTypes(): string[] | undefined {\n return this.allowedTypes;\n }\n}\n"]}
@@ -16,7 +16,7 @@ import { UploadCore } from './core.js';
16
16
  */
17
17
  export function createUploadMiddleware(options = {}) {
18
18
  const core = new UploadCore(options);
19
- return (req, res, next) => {
19
+ return async (req, res, next) => {
20
20
  const contentType = req.headers['content-type'] || '';
21
21
  if (!contentType.includes('multipart/form-data')) {
22
22
  next();
@@ -34,6 +34,17 @@ export function createUploadMiddleware(options = {}) {
34
34
  });
35
35
  return;
36
36
  }
37
+ // Only touches disk when a dest was configured
38
+ try {
39
+ await core.persistFiles(req.files);
40
+ }
41
+ catch (error) {
42
+ res.status(500).json({
43
+ success: false,
44
+ error: `Failed to store uploaded files: ${error instanceof Error ? error.message : String(error)}`,
45
+ });
46
+ return;
47
+ }
37
48
  }
38
49
  next();
39
50
  };
@@ -1 +1 @@
1
- {"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../../../../src/core/middleware/built-in/upload/middleware.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAiB,MAAM,WAAW,CAAC;AAEtD;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,sBAAsB,CAAC,UAAyB,EAAE;IAChE,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,OAAO,CAAC,CAAC;IAErC,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACxB,MAAM,WAAW,GAAG,GAAG,CAAC,OAAO,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC;QAEtD,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,qBAAqB,CAAC,EAAE,CAAC;YACjD,IAAI,EAAE,CAAC;YACP,OAAO;QACT,CAAC;QAED,kDAAkD;QAClD,0CAA0C;QAC1C,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;QAEtB,IAAI,GAAG,CAAC,KAAK,EAAE,CAAC;YACd,MAAM,UAAU,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YAClD,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;gBACtB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;oBACnB,OAAO,EAAE,KAAK;oBACd,KAAK,EAAE,UAAU,CAAC,KAAK;iBACxB,CAAC,CAAC;gBACH,OAAO;YACT,CAAC;QACH,CAAC;QAED,IAAI,EAAE,CAAC;IACT,CAAC,CAAC;AACJ,CAAC","sourcesContent":["// File Upload Middleware\nimport { Middleware } from '../../../../types/http.js';\nimport { UploadCore, UploadOptions } from './core.js';\n\n/**\n * Create file upload middleware\n *\n * @example\n * ```typescript\n * import { upload } from '@morojs/moro';\n *\n * app.use(upload({\n * dest: './uploads',\n * maxFileSize: 5 * 1024 * 1024, // 5MB\n * maxFiles: 10,\n * allowedTypes: ['image/jpeg', 'image/png', 'image/gif'],\n * }));\n * ```\n */\nexport function createUploadMiddleware(options: UploadOptions = {}): Middleware {\n const core = new UploadCore(options);\n\n return (req, res, next) => {\n const contentType = req.headers['content-type'] || '';\n\n if (!contentType.includes('multipart/form-data')) {\n next();\n return;\n }\n\n // File upload handling is built into body parsing\n // This middleware validates after parsing\n core.attachFiles(req);\n\n if (req.files) {\n const validation = core.validateUpload(req.files);\n if (!validation.valid) {\n res.status(400).json({\n success: false,\n error: validation.error,\n });\n return;\n }\n }\n\n next();\n };\n}\n"]}
1
+ {"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../../../../src/core/middleware/built-in/upload/middleware.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAiB,MAAM,WAAW,CAAC;AAEtD;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,sBAAsB,CAAC,UAAyB,EAAE;IAChE,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,OAAO,CAAC,CAAC;IAErC,OAAO,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QAC9B,MAAM,WAAW,GAAG,GAAG,CAAC,OAAO,CAAC,cAAc,CAAC,IAAI,EAAE,CAAC;QAEtD,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,qBAAqB,CAAC,EAAE,CAAC;YACjD,IAAI,EAAE,CAAC;YACP,OAAO;QACT,CAAC;QAED,kDAAkD;QAClD,0CAA0C;QAC1C,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;QAEtB,IAAI,GAAG,CAAC,KAAK,EAAE,CAAC;YACd,MAAM,UAAU,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YAClD,IAAI,CAAC,UAAU,CAAC,KAAK,EAAE,CAAC;gBACtB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;oBACnB,OAAO,EAAE,KAAK;oBACd,KAAK,EAAE,UAAU,CAAC,KAAK;iBACxB,CAAC,CAAC;gBACH,OAAO;YACT,CAAC;YAED,+CAA+C;YAC/C,IAAI,CAAC;gBACH,MAAM,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YACrC,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;oBACnB,OAAO,EAAE,KAAK;oBACd,KAAK,EAAE,mCACL,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CACvD,EAAE;iBACH,CAAC,CAAC;gBACH,OAAO;YACT,CAAC;QACH,CAAC;QAED,IAAI,EAAE,CAAC;IACT,CAAC,CAAC;AACJ,CAAC","sourcesContent":["// File Upload Middleware\nimport { Middleware } from '../../../../types/http.js';\nimport { UploadCore, UploadOptions } from './core.js';\n\n/**\n * Create file upload middleware\n *\n * @example\n * ```typescript\n * import { upload } from '@morojs/moro';\n *\n * app.use(upload({\n * dest: './uploads',\n * maxFileSize: 5 * 1024 * 1024, // 5MB\n * maxFiles: 10,\n * allowedTypes: ['image/jpeg', 'image/png', 'image/gif'],\n * }));\n * ```\n */\nexport function createUploadMiddleware(options: UploadOptions = {}): Middleware {\n const core = new UploadCore(options);\n\n return async (req, res, next) => {\n const contentType = req.headers['content-type'] || '';\n\n if (!contentType.includes('multipart/form-data')) {\n next();\n return;\n }\n\n // File upload handling is built into body parsing\n // This middleware validates after parsing\n core.attachFiles(req);\n\n if (req.files) {\n const validation = core.validateUpload(req.files);\n if (!validation.valid) {\n res.status(400).json({\n success: false,\n error: validation.error,\n });\n return;\n }\n\n // Only touches disk when a dest was configured\n try {\n await core.persistFiles(req.files);\n } catch (error) {\n res.status(500).json({\n success: false,\n error: `Failed to store uploaded files: ${\n error instanceof Error ? error.message : String(error)\n }`,\n });\n return;\n }\n }\n\n next();\n };\n}\n"]}
@@ -18,6 +18,7 @@ export interface RateLimitConfig {
18
18
  requests: number;
19
19
  window: number;
20
20
  skipSuccessfulRequests?: boolean;
21
+ skipFailedRequests?: boolean;
21
22
  }
22
23
  export interface CacheConfig {
23
24
  ttl: number;