zero-http 0.2.4 → 0.3.1

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 (89) hide show
  1. package/README.md +1250 -283
  2. package/documentation/config/db.js +25 -0
  3. package/documentation/config/middleware.js +44 -0
  4. package/documentation/config/tls.js +12 -0
  5. package/documentation/controllers/cookies.js +34 -0
  6. package/documentation/controllers/tasks.js +108 -0
  7. package/documentation/full-server.js +28 -177
  8. package/documentation/models/Task.js +21 -0
  9. package/documentation/public/data/api.json +404 -24
  10. package/documentation/public/data/docs.json +1139 -0
  11. package/documentation/public/data/examples.json +80 -2
  12. package/documentation/public/data/options.json +23 -8
  13. package/documentation/public/index.html +138 -99
  14. package/documentation/public/scripts/app.js +1 -3
  15. package/documentation/public/scripts/custom-select.js +189 -0
  16. package/documentation/public/scripts/data-sections.js +233 -250
  17. package/documentation/public/scripts/playground.js +270 -0
  18. package/documentation/public/scripts/ui.js +4 -3
  19. package/documentation/public/styles.css +56 -5
  20. package/documentation/public/vendor/icons/compress.svg +17 -17
  21. package/documentation/public/vendor/icons/database.svg +21 -0
  22. package/documentation/public/vendor/icons/env.svg +21 -0
  23. package/documentation/public/vendor/icons/fetch.svg +11 -14
  24. package/documentation/public/vendor/icons/security.svg +15 -0
  25. package/documentation/public/vendor/icons/sse.svg +12 -13
  26. package/documentation/public/vendor/icons/static.svg +12 -26
  27. package/documentation/public/vendor/icons/stream.svg +7 -13
  28. package/documentation/public/vendor/icons/validate.svg +17 -0
  29. package/documentation/routes/api.js +41 -0
  30. package/documentation/routes/core.js +20 -0
  31. package/documentation/routes/playground.js +29 -0
  32. package/documentation/routes/realtime.js +49 -0
  33. package/documentation/routes/uploads.js +71 -0
  34. package/index.js +62 -1
  35. package/lib/app.js +200 -8
  36. package/lib/body/json.js +28 -5
  37. package/lib/body/multipart.js +29 -1
  38. package/lib/body/raw.js +1 -1
  39. package/lib/body/sendError.js +1 -0
  40. package/lib/body/text.js +1 -1
  41. package/lib/body/typeMatch.js +6 -2
  42. package/lib/body/urlencoded.js +5 -2
  43. package/lib/debug.js +345 -0
  44. package/lib/env/index.js +440 -0
  45. package/lib/errors.js +231 -0
  46. package/lib/http/request.js +219 -1
  47. package/lib/http/response.js +410 -6
  48. package/lib/middleware/compress.js +39 -6
  49. package/lib/middleware/cookieParser.js +237 -0
  50. package/lib/middleware/cors.js +13 -2
  51. package/lib/middleware/csrf.js +135 -0
  52. package/lib/middleware/errorHandler.js +90 -0
  53. package/lib/middleware/helmet.js +176 -0
  54. package/lib/middleware/index.js +7 -2
  55. package/lib/middleware/rateLimit.js +12 -1
  56. package/lib/middleware/requestId.js +54 -0
  57. package/lib/middleware/static.js +95 -11
  58. package/lib/middleware/timeout.js +72 -0
  59. package/lib/middleware/validator.js +257 -0
  60. package/lib/orm/adapters/json.js +215 -0
  61. package/lib/orm/adapters/memory.js +383 -0
  62. package/lib/orm/adapters/mongo.js +444 -0
  63. package/lib/orm/adapters/mysql.js +272 -0
  64. package/lib/orm/adapters/postgres.js +394 -0
  65. package/lib/orm/adapters/sql-base.js +142 -0
  66. package/lib/orm/adapters/sqlite.js +311 -0
  67. package/lib/orm/index.js +276 -0
  68. package/lib/orm/model.js +895 -0
  69. package/lib/orm/query.js +807 -0
  70. package/lib/orm/schema.js +172 -0
  71. package/lib/router/index.js +136 -47
  72. package/lib/sse/stream.js +15 -3
  73. package/lib/ws/connection.js +19 -3
  74. package/lib/ws/handshake.js +3 -0
  75. package/lib/ws/index.js +3 -1
  76. package/lib/ws/room.js +222 -0
  77. package/package.json +15 -5
  78. package/types/app.d.ts +120 -0
  79. package/types/env.d.ts +80 -0
  80. package/types/errors.d.ts +147 -0
  81. package/types/fetch.d.ts +43 -0
  82. package/types/index.d.ts +135 -0
  83. package/types/middleware.d.ts +292 -0
  84. package/types/orm.d.ts +610 -0
  85. package/types/request.d.ts +99 -0
  86. package/types/response.d.ts +142 -0
  87. package/types/router.d.ts +78 -0
  88. package/types/sse.d.ts +78 -0
  89. package/types/websocket.d.ts +119 -0
@@ -0,0 +1,17 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" aria-hidden="true">
2
+ <defs>
3
+ <linearGradient id="g-val" x1="0" x2="1">
4
+ <stop offset="0" stop-color="#7b61ff"/>
5
+ <stop offset="1" stop-color="#3ec6ff"/>
6
+ </linearGradient>
7
+ </defs>
8
+ <rect width="64" height="64" rx="8" fill="transparent"/>
9
+ <!-- Rounded rectangle (form/clipboard) -->
10
+ <rect x="12" y="6" width="40" height="52" rx="6" fill="url(#g-val)" opacity="0.25"/>
11
+ <rect x="16" y="10" width="32" height="44" rx="4" fill="url(#g-val)" opacity="0.4"/>
12
+ <!-- Lines (form fields) -->
13
+ <line x1="22" y1="22" x2="36" y2="22" stroke="rgba(255,255,255,0.5)" stroke-width="2" stroke-linecap="round"/>
14
+ <line x1="22" y1="30" x2="34" y2="30" stroke="rgba(255,255,255,0.35)" stroke-width="2" stroke-linecap="round"/>
15
+ <!-- Large checkmark overlay -->
16
+ <path d="M22 38l8 8 14-18" fill="none" stroke="#ffffff" stroke-width="3.5" stroke-linecap="round" stroke-linejoin="round"/>
17
+ </svg>
@@ -0,0 +1,41 @@
1
+ const { Router, validate, fetch } = require('../..');
2
+ const proxyController = require('../controllers/proxy');
3
+
4
+ /**
5
+ * Mount the /api sub-router, validation demo, proxy, and debug routes.
6
+ */
7
+ function mountApiRoutes(app)
8
+ {
9
+ // --- Router Demo (sub-app) ---
10
+ const apiRouter = Router();
11
+ apiRouter.get('/health', (req, res) => res.json({ status: 'ok', uptime: process.uptime() }));
12
+ apiRouter.get('/info', (req, res) => res.json({
13
+ secure: req.secure,
14
+ protocol: req.protocol,
15
+ ip: req.ip,
16
+ method: req.method,
17
+ url: req.url,
18
+ }));
19
+ app.use('/api', apiRouter);
20
+
21
+ // --- Validation Demo ---
22
+ app.post('/demo/validate', validate({
23
+ body: {
24
+ name: { type: 'string', required: true, min: 1, max: 100 },
25
+ age: { type: 'number', min: 0, max: 150 },
26
+ },
27
+ }), (req, res) => res.json({ ok: true, data: req.body }));
28
+
29
+ // --- Proxy ---
30
+ const proxyFetch = (typeof globalThis !== 'undefined' && globalThis.fetch) || fetch;
31
+ app.get('/proxy', proxyController.proxy(proxyFetch));
32
+
33
+ // --- Route introspection ---
34
+ app.get('/debug/routes', (req, res) =>
35
+ {
36
+ res.set('Content-Type', 'application/json');
37
+ res.send(JSON.stringify(app.routes(), null, 2));
38
+ });
39
+ }
40
+
41
+ module.exports = mountApiRoutes;
@@ -0,0 +1,20 @@
1
+ const { raw } = require('../..');
2
+ const rootController = require('../controllers/root');
3
+ const headersController = require('../controllers/headers');
4
+ const echoController = require('../controllers/echo');
5
+
6
+ /**
7
+ * Mount core routes: root, headers, echo parsers.
8
+ */
9
+ function mountCoreRoutes(app)
10
+ {
11
+ app.get('/', rootController.getRoot);
12
+ app.get('/headers', headersController.getHeaders);
13
+ app.post('/echo-json', echoController.echoJson);
14
+ app.post('/echo', echoController.echo);
15
+ app.post('/echo-urlencoded', echoController.echoUrlencoded);
16
+ app.post('/echo-text', echoController.echoText);
17
+ app.post('/echo-raw', raw(), echoController.echoRaw);
18
+ }
19
+
20
+ module.exports = mountCoreRoutes;
@@ -0,0 +1,29 @@
1
+ const { initDatabase } = require('../config/db');
2
+ const tasksController = require('../controllers/tasks');
3
+ const cookiesController = require('../controllers/cookies');
4
+
5
+ /**
6
+ * Register the ORM database and mount all playground API routes onto the app.
7
+ * @param {import('../../lib/app').App} app
8
+ */
9
+ function mountPlaygroundRoutes(app)
10
+ {
11
+ // --- ORM setup ---
12
+ initDatabase();
13
+
14
+ // --- Task CRUD ---
15
+ app.get('/api/tasks', tasksController.list);
16
+ app.get('/api/tasks/stats', tasksController.stats);
17
+ app.post('/api/tasks', tasksController.create);
18
+ app.put('/api/tasks/:id', tasksController.update);
19
+ app.delete('/api/tasks/:id', tasksController.remove);
20
+ app.post('/api/tasks/:id/restore', tasksController.restore);
21
+ app.delete('/api/tasks', tasksController.removeAll);
22
+
23
+ // --- Cookies ---
24
+ app.get('/api/cookies', cookiesController.list);
25
+ app.post('/api/cookies', cookiesController.set);
26
+ app.delete('/api/cookies/:name', cookiesController.clear);
27
+ }
28
+
29
+ module.exports = mountPlaygroundRoutes;
@@ -0,0 +1,49 @@
1
+ const { WebSocketPool } = require('../..');
2
+
3
+ /**
4
+ * Mount WebSocket and SSE real-time routes.
5
+ */
6
+ function mountRealtimeRoutes(app)
7
+ {
8
+ // --- WebSocket Chat ---
9
+ const pool = new WebSocketPool();
10
+
11
+ app.ws('/ws/chat', { maxPayload: 64 * 1024, pingInterval: 25000 }, (ws, req) =>
12
+ {
13
+ ws.data.name = ws.query.name || 'anon';
14
+ pool.add(ws);
15
+ ws.send(JSON.stringify({ type: 'system', text: 'Welcome, ' + ws.data.name + '!' }));
16
+ pool.broadcast(JSON.stringify({ type: 'system', text: ws.data.name + ' joined' }), ws);
17
+
18
+ ws.on('message', (msg) =>
19
+ {
20
+ pool.broadcastJSON({ type: 'message', name: ws.data.name, text: String(msg) });
21
+ });
22
+
23
+ ws.on('close', () =>
24
+ {
25
+ pool.remove(ws);
26
+ pool.broadcastJSON({ type: 'system', text: ws.data.name + ' left' });
27
+ });
28
+ });
29
+
30
+ // --- Server-Sent Events ---
31
+ const sseClients = new Set();
32
+
33
+ app.get('/sse/events', (req, res) =>
34
+ {
35
+ const sse = res.sse({ retry: 5000, autoId: true, keepAlive: 30000 });
36
+ sseClients.add(sse);
37
+ sse.send({ type: 'connected', clients: sseClients.size });
38
+ sse.on('close', () => sseClients.delete(sse));
39
+ });
40
+
41
+ app.post('/sse/broadcast', (req, res) =>
42
+ {
43
+ const data = req.body || {};
44
+ for (const sse of sseClients) sse.event('broadcast', data);
45
+ res.json({ sent: sseClients.size });
46
+ });
47
+ }
48
+
49
+ module.exports = mountRealtimeRoutes;
@@ -0,0 +1,71 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const { multipart, static: serveStatic } = require('../..');
4
+ const uploadsController = require('../controllers/uploads');
5
+
6
+ /**
7
+ * Mount upload, trash, and file-listing routes.
8
+ * Also starts the automatic trash retention cleanup.
9
+ */
10
+ function mountUploadRoutes(app)
11
+ {
12
+ const uploadsDir = path.join(__dirname, '..', 'uploads');
13
+ uploadsController.ensureUploadsDir(uploadsDir);
14
+
15
+ // Serve uploaded files
16
+ app.use('/uploads', serveStatic(uploadsDir));
17
+
18
+ // Upload & CRUD
19
+ app.post('/upload', multipart({ maxFileSize: 5 * 1024 * 1024, dir: uploadsDir }), uploadsController.upload(uploadsDir));
20
+ app.delete('/uploads/:name', uploadsController.deleteUpload(uploadsDir));
21
+ app.delete('/uploads', uploadsController.deleteAllUploads(uploadsDir));
22
+ app.post('/uploads/:name/restore', uploadsController.restoreUpload(uploadsDir));
23
+
24
+ // Trash
25
+ app.get('/uploads-trash-list', uploadsController.listTrash(uploadsDir));
26
+ app.delete('/uploads-trash/:name', uploadsController.deleteTrashItem(uploadsDir));
27
+ app.delete('/uploads-trash', uploadsController.emptyTrash(uploadsDir));
28
+
29
+ // Listings
30
+ app.get('/uploads-list', uploadsController.listUploads(uploadsDir));
31
+ app.get('/uploads-all', uploadsController.listAll(uploadsDir));
32
+
33
+ // Trash retention
34
+ const RETENTION_MS = Number(process.env.TRASH_RETENTION_DAYS || 7) * 86400000;
35
+
36
+ function autoEmptyTrash()
37
+ {
38
+ try
39
+ {
40
+ const trash = path.join(uploadsDir, '.trash');
41
+ if (!fs.existsSync(trash)) return;
42
+ const now = Date.now();
43
+ const removed = [];
44
+ for (const f of fs.readdirSync(trash))
45
+ {
46
+ try
47
+ {
48
+ const p = path.join(trash, f);
49
+ const st = fs.statSync(p);
50
+ if (now - st.mtimeMs > RETENTION_MS)
51
+ {
52
+ fs.unlinkSync(p);
53
+ removed.push(f);
54
+ try { fs.unlinkSync(path.join(trash, '.thumbs', f + '-thumb.svg')); } catch (_) { }
55
+ }
56
+ } catch (_) { }
57
+ }
58
+ if (removed.length) console.log(`autoEmptyTrash: removed ${removed.length} file(s)`);
59
+ } catch (e) { console.error('autoEmptyTrash error:', e); }
60
+ }
61
+
62
+ autoEmptyTrash();
63
+ setInterval(autoEmptyTrash, 86400000).unref();
64
+
65
+ // Temp cleanup
66
+ const os = require('os');
67
+ const cleanupController = require('../controllers/cleanup');
68
+ app.post('/cleanup', cleanupController.cleanup(path.join(os.tmpdir(), 'zero-http-uploads')));
69
+ }
70
+
71
+ module.exports = mountUploadRoutes;
package/index.js CHANGED
@@ -12,8 +12,19 @@ const serveStatic = require('./lib/middleware/static');
12
12
  const rateLimit = require('./lib/middleware/rateLimit');
13
13
  const logger = require('./lib/middleware/logger');
14
14
  const compress = require('./lib/middleware/compress');
15
- const { WebSocketConnection } = require('./lib/ws');
15
+ const helmet = require('./lib/middleware/helmet');
16
+ const timeout = require('./lib/middleware/timeout');
17
+ const requestId = require('./lib/middleware/requestId');
18
+ const cookieParser = require('./lib/middleware/cookieParser');
19
+ const csrf = require('./lib/middleware/csrf');
20
+ const validate = require('./lib/middleware/validator');
21
+ const errorHandler = require('./lib/middleware/errorHandler');
22
+ const { WebSocketConnection, WebSocketPool } = require('./lib/ws');
16
23
  const { SSEStream } = require('./lib/sse');
24
+ const env = require('./lib/env');
25
+ const { Database, Model, TYPES, Query } = require('./lib/orm');
26
+ const errors = require('./lib/errors');
27
+ const debug = require('./lib/debug');
17
28
 
18
29
  module.exports = {
19
30
  /**
@@ -52,9 +63,59 @@ module.exports = {
52
63
  logger,
53
64
  /** @see module:compress */
54
65
  compress,
66
+ /** @see module:helmet */
67
+ helmet,
68
+ /** @see module:timeout */
69
+ timeout,
70
+ /** @see module:requestId */
71
+ requestId,
72
+ /** @see module:cookieParser */
73
+ cookieParser,
74
+ /** @see module:csrf */
75
+ csrf,
76
+ /** @see module:validator */
77
+ validate,
78
+ /** @see module:middleware/errorHandler */
79
+ errorHandler,
80
+ // env
81
+ /** @see module:env */
82
+ env,
83
+ // ORM
84
+ /** @see module:orm */
85
+ Database,
86
+ /** @see module:orm/model */
87
+ Model,
88
+ /** @see module:orm/schema */
89
+ TYPES,
90
+ /** @see module:orm/query */
91
+ Query,
92
+ // Error handling & debugging
93
+ /** @see module:errors */
94
+ HttpError: errors.HttpError,
95
+ BadRequestError: errors.BadRequestError,
96
+ UnauthorizedError: errors.UnauthorizedError,
97
+ ForbiddenError: errors.ForbiddenError,
98
+ NotFoundError: errors.NotFoundError,
99
+ MethodNotAllowedError: errors.MethodNotAllowedError,
100
+ ConflictError: errors.ConflictError,
101
+ GoneError: errors.GoneError,
102
+ PayloadTooLargeError: errors.PayloadTooLargeError,
103
+ UnprocessableEntityError: errors.UnprocessableEntityError,
104
+ ValidationError: errors.ValidationError,
105
+ TooManyRequestsError: errors.TooManyRequestsError,
106
+ InternalError: errors.InternalError,
107
+ NotImplementedError: errors.NotImplementedError,
108
+ BadGatewayError: errors.BadGatewayError,
109
+ ServiceUnavailableError: errors.ServiceUnavailableError,
110
+ createError: errors.createError,
111
+ isHttpError: errors.isHttpError,
112
+ /** @see module:debug */
113
+ debug,
55
114
  // classes (for advanced / direct usage)
56
115
  /** @see module:ws/connection */
57
116
  WebSocketConnection,
117
+ /** @see module:ws/room */
118
+ WebSocketPool,
58
119
  /** @see module:sse/stream */
59
120
  SSEStream,
60
121
  };
package/lib/app.js CHANGED
@@ -10,6 +10,7 @@ const https = require('https');
10
10
  const Router = require('./router');
11
11
  const { Request, Response } = require('./http');
12
12
  const { handleUpgrade } = require('./ws');
13
+ const log = require('./debug')('zero:app');
13
14
 
14
15
  class App
15
16
  {
@@ -33,10 +34,96 @@ class App
33
34
  /** @type {import('http').Server|import('https').Server|null} */
34
35
  this._server = null;
35
36
 
37
+ /**
38
+ * Application-level settings store.
39
+ * @type {Object<string, *>}
40
+ * @private
41
+ */
42
+ this._settings = {};
43
+
44
+ /**
45
+ * Application-level locals — persistent across the app lifecycle.
46
+ * Merged into every `req.locals` and `res.locals` on every request.
47
+ * @type {Object<string, *>}
48
+ */
49
+ this.locals = {};
50
+
51
+ /**
52
+ * Parameter pre-processing handlers.
53
+ * @type {Object<string, Function[]>}
54
+ * @private
55
+ */
56
+ this._paramHandlers = {};
57
+
36
58
  // Bind for use as `http.createServer(app.handler)`
37
59
  this.handler = (req, res) => this.handle(req, res);
38
60
  }
39
61
 
62
+ // -- Settings ------------------------------------
63
+
64
+ /**
65
+ * Set an application setting, or retrieve one when called with a single argument.
66
+ *
67
+ * When called with two arguments, sets the value and returns `this` for chaining.
68
+ * When called with one argument, returns the stored value.
69
+ *
70
+ * Common settings: `'trust proxy'`, `'env'`, `'json spaces'`, `'etag'`,
71
+ * `'view engine'`, `'views'`, `'case sensitive routing'`.
72
+ *
73
+ * @param {string} key - Setting name.
74
+ * @param {*} [val] - Setting value.
75
+ * @returns {*|App} The stored value (getter) or `this` (setter).
76
+ *
77
+ * @example
78
+ * app.set('trust proxy', true);
79
+ * app.set('json spaces', 2);
80
+ * app.set('env'); // => undefined (or previously set value)
81
+ */
82
+ set(key, val)
83
+ {
84
+ if (arguments.length === 1) return this._settings[key];
85
+ this._settings[key] = val;
86
+ return this;
87
+ }
88
+
89
+ /**
90
+ * Set a boolean setting to `true`.
91
+ *
92
+ * @param {string} key - Setting name.
93
+ * @returns {App} `this` for chaining.
94
+ *
95
+ * @example
96
+ * app.enable('trust proxy');
97
+ */
98
+ enable(key) { this._settings[key] = true; return this; }
99
+
100
+ /**
101
+ * Set a boolean setting to `false`.
102
+ *
103
+ * @param {string} key - Setting name.
104
+ * @returns {App} `this` for chaining.
105
+ *
106
+ * @example
107
+ * app.disable('etag');
108
+ */
109
+ disable(key) { this._settings[key] = false; return this; }
110
+
111
+ /**
112
+ * Check if a setting is truthy.
113
+ *
114
+ * @param {string} key - Setting name.
115
+ * @returns {boolean}
116
+ */
117
+ enabled(key) { return !!this._settings[key]; }
118
+
119
+ /**
120
+ * Check if a setting is falsy.
121
+ *
122
+ * @param {string} key - Setting name.
123
+ * @returns {boolean}
124
+ */
125
+ disabled(key) { return !this._settings[key]; }
126
+
40
127
  // -- Middleware -------------------------------------
41
128
 
42
129
  /**
@@ -93,6 +180,29 @@ class App
93
180
  this._errorHandler = fn;
94
181
  }
95
182
 
183
+ /**
184
+ * Register a parameter pre-processing handler.
185
+ * Runs before route handlers for any route containing a `:name` parameter.
186
+ *
187
+ * @param {string} name - Parameter name.
188
+ * @param {Function} fn - `(req, res, next, value) => void`.
189
+ * @returns {App} `this` for chaining.
190
+ *
191
+ * @example
192
+ * app.param('userId', async (req, res, next, id) => {
193
+ * req.locals.user = await db.users.findById(id);
194
+ * if (!req.locals.user) return res.status(404).json({ error: 'User not found' });
195
+ * next();
196
+ * });
197
+ */
198
+ param(name, fn)
199
+ {
200
+ if (!this._paramHandlers[name]) this._paramHandlers[name] = [];
201
+ this._paramHandlers[name].push(fn);
202
+ this.router._paramHandlers = this._paramHandlers;
203
+ return this;
204
+ }
205
+
96
206
  // -- Request Handling ------------------------------
97
207
 
98
208
  /**
@@ -108,11 +218,25 @@ class App
108
218
  const request = new Request(req);
109
219
  const response = new Response(res);
110
220
 
221
+ // Inject app reference into request and response
222
+ request.app = this;
223
+ response.app = this;
224
+ response._req = request;
225
+ request._res = response;
226
+
227
+ // Preserve original URL before any middleware rewrites
228
+ request.originalUrl = request.url;
229
+
230
+ // Merge app.locals into request/response locals via prototype chain (avoids copy per request)
231
+ request.locals = Object.create(this.locals);
232
+ response.locals = Object.create(this.locals);
233
+
111
234
  let idx = 0;
112
235
  const run = (err) =>
113
236
  {
114
237
  if (err)
115
238
  {
239
+ log.error('middleware error: %s', err.message || err);
116
240
  if (this._errorHandler) return this._errorHandler(err, request, response, run);
117
241
  response.status(500).json({ error: err.message || 'Internal Server Error' });
118
242
  return;
@@ -170,6 +294,7 @@ class App
170
294
  : http.createServer(this.handler);
171
295
 
172
296
  this._server = server;
297
+ log.info('starting %s server on port %d', isHTTPS ? 'HTTPS' : 'HTTP', port);
173
298
 
174
299
  // Always attach WebSocket upgrade handling so ws() works
175
300
  // regardless of registration order (before or after listen).
@@ -287,14 +412,81 @@ class App
287
412
  */
288
413
  route(method, path, ...fns) { const o = this._extractOpts(fns); this.router.add(method, path, fns, o); }
289
414
 
290
- /** @see App#route — shortcut for GET requests. */ get(path, ...fns) { this.route('GET', path, ...fns); }
291
- /** @see App#route — shortcut for POST requests. */ post(path, ...fns) { this.route('POST', path, ...fns); }
292
- /** @see App#route — shortcut for PUT requests. */ put(path, ...fns) { this.route('PUT', path, ...fns); }
293
- /** @see App#route — shortcut for DELETE requests. */ delete(path, ...fns) { this.route('DELETE', path, ...fns); }
294
- /** @see App#route — shortcut for PATCH requests. */ patch(path, ...fns) { this.route('PATCH', path, ...fns); }
295
- /** @see App#route — shortcut for OPTIONS requests.*/ options(path, ...fns) { this.route('OPTIONS', path, ...fns); }
296
- /** @see App#route — shortcut for HEAD requests. */ head(path, ...fns) { this.route('HEAD', path, ...fns); }
297
- /** @see App#route — matches every HTTP method. */ all(path, ...fns) { this.route('ALL', path, ...fns); }
415
+ /** @see App#route — shortcut for GET requests. */ get(path, ...fns) { if (arguments.length === 1 && typeof path === 'string' && fns.length === 0) return this.set(path); this.route('GET', path, ...fns); return this; }
416
+ /** @see App#route — shortcut for POST requests. */ post(path, ...fns) { this.route('POST', path, ...fns); return this; }
417
+ /** @see App#route — shortcut for PUT requests. */ put(path, ...fns) { this.route('PUT', path, ...fns); return this; }
418
+ /** @see App#route — shortcut for DELETE requests. */ delete(path, ...fns) { this.route('DELETE', path, ...fns); return this; }
419
+ /** @see App#route — shortcut for PATCH requests. */ patch(path, ...fns) { this.route('PATCH', path, ...fns); return this; }
420
+ /** @see App#route — shortcut for OPTIONS requests.*/ options(path, ...fns) { this.route('OPTIONS', path, ...fns); return this; }
421
+ /** @see App#route — shortcut for HEAD requests. */ head(path, ...fns) { this.route('HEAD', path, ...fns); return this; }
422
+ /** @see App#route — matches every HTTP method. */ all(path, ...fns) { this.route('ALL', path, ...fns); return this; }
423
+
424
+ /**
425
+ * Chainable route builder — register multiple methods on the same path.
426
+ *
427
+ * @param {string} path - Route pattern.
428
+ * @returns {object} Chain object with HTTP verb methods.
429
+ *
430
+ * @example
431
+ * app.chain('/users')
432
+ * .get((req, res) => res.json(users))
433
+ * .post((req, res) => res.json({ created: true }));
434
+ */
435
+ chain(path) { return this.router.route(path); }
436
+
437
+ /**
438
+ * Define a route group with shared middleware prefix.
439
+ * All routes registered inside the callback share the given path prefix
440
+ * and middleware stack.
441
+ *
442
+ * @param {string} prefix - URL prefix for the group.
443
+ * @param {...Function} middleware - Shared middleware, last argument is the callback.
444
+ * @returns {App} `this` for chaining.
445
+ *
446
+ * @example
447
+ * app.group('/api/v1', authMiddleware, (router) => {
448
+ * router.get('/users', listUsers);
449
+ * router.post('/users', createUser);
450
+ * router.get('/users/:id', getUser);
451
+ * });
452
+ */
453
+ group(prefix, ...args)
454
+ {
455
+ const cb = args.pop();
456
+ const middlewareStack = args;
457
+ const router = new Router();
458
+ cb(router);
459
+ if (middlewareStack.length > 0)
460
+ {
461
+ this.middlewares.push((req, res, next) =>
462
+ {
463
+ const urlPath = req.url.split('?')[0];
464
+ const cleanPrefix = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
465
+ if (urlPath === cleanPrefix || urlPath.startsWith(cleanPrefix + '/'))
466
+ {
467
+ let i = 0;
468
+ const runMw = () =>
469
+ {
470
+ if (i < middlewareStack.length)
471
+ {
472
+ const mw = middlewareStack[i++];
473
+ try
474
+ {
475
+ const result = mw(req, res, runMw);
476
+ if (result && typeof result.catch === 'function') result.catch(next);
477
+ }
478
+ catch (e) { next(e); }
479
+ }
480
+ else { next(); }
481
+ };
482
+ runMw();
483
+ }
484
+ else { next(); }
485
+ });
486
+ }
487
+ this.router.use(prefix, router);
488
+ return this;
489
+ }
298
490
  }
299
491
 
300
492
  module.exports = App;
package/lib/body/json.js CHANGED
@@ -7,6 +7,25 @@ const rawBuffer = require('./rawBuffer');
7
7
  const isTypeMatch = require('./typeMatch');
8
8
  const sendError = require('./sendError');
9
9
 
10
+ /** Recursively remove __proto__, constructor, and prototype keys to prevent prototype pollution. */
11
+ function _sanitize(obj)
12
+ {
13
+ if (!obj || typeof obj !== 'object') return;
14
+ const keys = Object.keys(obj);
15
+ for (let i = 0; i < keys.length; i++)
16
+ {
17
+ const k = keys[i];
18
+ if (k === '__proto__' || k === 'constructor' || k === 'prototype')
19
+ {
20
+ delete obj[k];
21
+ }
22
+ else if (typeof obj[k] === 'object' && obj[k] !== null)
23
+ {
24
+ _sanitize(obj[k]);
25
+ }
26
+ }
27
+ }
28
+
10
29
  /**
11
30
  * Create a JSON body-parsing middleware.
12
31
  *
@@ -21,7 +40,7 @@ const sendError = require('./sendError');
21
40
  function json(options = {})
22
41
  {
23
42
  const opts = options || {};
24
- const limit = opts.limit || null;
43
+ const limit = opts.limit !== undefined ? opts.limit : '1mb';
25
44
  const reviver = opts.reviver;
26
45
  const strict = (opts.hasOwnProperty('strict')) ? !!opts.strict : true;
27
46
  const typeOpt = opts.type || 'application/json';
@@ -38,11 +57,15 @@ function json(options = {})
38
57
  const txt = buf.toString('utf8');
39
58
  if (!txt) { req.body = null; return next(); }
40
59
  let parsed;
41
- try { parsed = JSON.parse(txt, reviver); } catch (e) { req.body = null; return next(); }
42
- if (strict && (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed) === false && Object.keys(parsed).length === 0 && !Array.isArray(parsed)))
60
+ try { parsed = JSON.parse(txt, reviver); } catch (e) { return sendError(res, 400, 'invalid JSON'); }
61
+ if (strict && (typeof parsed !== 'object' || parsed === null))
62
+ {
63
+ return sendError(res, 400, 'invalid JSON: root must be object or array');
64
+ }
65
+ // Prevent prototype pollution
66
+ if (parsed && typeof parsed === 'object')
43
67
  {
44
- // If strict, prefer objects/arrays; allow arrays but reject primitives
45
- if (typeof parsed !== 'object') { req.body = null; return next(); }
68
+ _sanitize(parsed);
46
69
  }
47
70
  req.body = parsed;
48
71
  } catch (err)
@@ -51,6 +51,27 @@ function parseHeaders(headerText)
51
51
  return obj;
52
52
  }
53
53
 
54
+ /**
55
+ * Sanitize a filename by stripping path traversal characters and
56
+ * null bytes. Keeps only the basename.
57
+ *
58
+ * @param {string} filename - Raw filename from the upload.
59
+ * @returns {string} Sanitized filename.
60
+ */
61
+ function sanitizeFilename(filename)
62
+ {
63
+ if (!filename) return '';
64
+ // Strip null bytes
65
+ let safe = filename.replace(/\0/g, '');
66
+ // Take only the basename (strip directory traversal)
67
+ safe = safe.replace(/^.*[/\\]/, '');
68
+ // Remove leading dots (prevent dotfile creation)
69
+ safe = safe.replace(/^\.+/, '');
70
+ // Replace potentially dangerous characters
71
+ safe = safe.replace(/[<>:"|?*]/g, '_');
72
+ return safe || 'unnamed';
73
+ }
74
+
54
75
  /**
55
76
  * Extract `name` and `filename` fields from a `Content-Disposition` header.
56
77
  *
@@ -66,7 +87,14 @@ function parseContentDisposition(cd)
66
87
  for (const p of parts)
67
88
  {
68
89
  const mm = /([^=]+)="?([^"]+)"?/.exec(p);
69
- if (mm) out[mm[1]] = mm[2];
90
+ if (mm)
91
+ {
92
+ const key = mm[1].trim();
93
+ let val = mm[2];
94
+ // Sanitize filename values
95
+ if (key === 'filename') val = sanitizeFilename(val);
96
+ out[key] = val;
97
+ }
70
98
  }
71
99
  return out;
72
100
  }
package/lib/body/raw.js CHANGED
@@ -19,7 +19,7 @@ const sendError = require('./sendError');
19
19
  function raw(options = {})
20
20
  {
21
21
  const opts = options || {};
22
- const limit = opts.limit || null;
22
+ const limit = opts.limit !== undefined ? opts.limit : '1mb';
23
23
  const typeOpt = opts.type || 'application/octet-stream';
24
24
  const requireSecure = !!opts.requireSecure;
25
25