zero-http 0.2.5 → 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.
- package/README.md +1250 -283
- package/documentation/config/db.js +25 -0
- package/documentation/config/middleware.js +44 -0
- package/documentation/config/tls.js +12 -0
- package/documentation/controllers/cookies.js +34 -0
- package/documentation/controllers/tasks.js +108 -0
- package/documentation/full-server.js +25 -184
- package/documentation/models/Task.js +21 -0
- package/documentation/public/data/api.json +404 -24
- package/documentation/public/data/docs.json +1139 -0
- package/documentation/public/data/examples.json +80 -2
- package/documentation/public/data/options.json +23 -8
- package/documentation/public/index.html +138 -99
- package/documentation/public/scripts/app.js +1 -3
- package/documentation/public/scripts/custom-select.js +189 -0
- package/documentation/public/scripts/data-sections.js +233 -250
- package/documentation/public/scripts/playground.js +270 -0
- package/documentation/public/scripts/ui.js +4 -3
- package/documentation/public/styles.css +56 -5
- package/documentation/public/vendor/icons/compress.svg +17 -17
- package/documentation/public/vendor/icons/database.svg +21 -0
- package/documentation/public/vendor/icons/env.svg +21 -0
- package/documentation/public/vendor/icons/fetch.svg +11 -14
- package/documentation/public/vendor/icons/security.svg +15 -0
- package/documentation/public/vendor/icons/sse.svg +12 -13
- package/documentation/public/vendor/icons/static.svg +12 -26
- package/documentation/public/vendor/icons/stream.svg +7 -13
- package/documentation/public/vendor/icons/validate.svg +17 -0
- package/documentation/routes/api.js +41 -0
- package/documentation/routes/core.js +20 -0
- package/documentation/routes/playground.js +29 -0
- package/documentation/routes/realtime.js +49 -0
- package/documentation/routes/uploads.js +71 -0
- package/index.js +62 -1
- package/lib/app.js +200 -8
- package/lib/body/json.js +28 -5
- package/lib/body/multipart.js +29 -1
- package/lib/body/raw.js +1 -1
- package/lib/body/sendError.js +1 -0
- package/lib/body/text.js +1 -1
- package/lib/body/typeMatch.js +6 -2
- package/lib/body/urlencoded.js +5 -2
- package/lib/debug.js +345 -0
- package/lib/env/index.js +440 -0
- package/lib/errors.js +231 -0
- package/lib/http/request.js +219 -1
- package/lib/http/response.js +410 -6
- package/lib/middleware/compress.js +39 -6
- package/lib/middleware/cookieParser.js +237 -0
- package/lib/middleware/cors.js +13 -2
- package/lib/middleware/csrf.js +135 -0
- package/lib/middleware/errorHandler.js +90 -0
- package/lib/middleware/helmet.js +176 -0
- package/lib/middleware/index.js +7 -2
- package/lib/middleware/rateLimit.js +12 -1
- package/lib/middleware/requestId.js +54 -0
- package/lib/middleware/static.js +95 -11
- package/lib/middleware/timeout.js +72 -0
- package/lib/middleware/validator.js +257 -0
- package/lib/orm/adapters/json.js +215 -0
- package/lib/orm/adapters/memory.js +383 -0
- package/lib/orm/adapters/mongo.js +444 -0
- package/lib/orm/adapters/mysql.js +272 -0
- package/lib/orm/adapters/postgres.js +394 -0
- package/lib/orm/adapters/sql-base.js +142 -0
- package/lib/orm/adapters/sqlite.js +311 -0
- package/lib/orm/index.js +276 -0
- package/lib/orm/model.js +895 -0
- package/lib/orm/query.js +807 -0
- package/lib/orm/schema.js +172 -0
- package/lib/router/index.js +136 -47
- package/lib/sse/stream.js +15 -3
- package/lib/ws/connection.js +19 -3
- package/lib/ws/handshake.js +3 -0
- package/lib/ws/index.js +3 -1
- package/lib/ws/room.js +222 -0
- package/package.json +15 -5
- package/types/app.d.ts +120 -0
- package/types/env.d.ts +80 -0
- package/types/errors.d.ts +147 -0
- package/types/fetch.d.ts +43 -0
- package/types/index.d.ts +135 -0
- package/types/middleware.d.ts +292 -0
- package/types/orm.d.ts +610 -0
- package/types/request.d.ts +99 -0
- package/types/response.d.ts +142 -0
- package/types/router.d.ts +78 -0
- package/types/sse.d.ts +78 -0
- 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
|
|
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
|
|
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) {
|
|
42
|
-
if (strict && (typeof parsed !== 'object' || parsed === null
|
|
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
|
-
|
|
45
|
-
if (typeof parsed !== 'object') { req.body = null; return next(); }
|
|
68
|
+
_sanitize(parsed);
|
|
46
69
|
}
|
|
47
70
|
req.body = parsed;
|
|
48
71
|
} catch (err)
|
package/lib/body/multipart.js
CHANGED
|
@@ -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)
|
|
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
|
|
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
|
|