webspresso 0.0.104 → 0.0.105
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/.agents/AGENTS.md +6 -0
- package/.agents/ARCHITECTURE.md +23 -0
- package/.agents/CACHE.md +147 -0
- package/.agents/CLI.md +1 -1
- package/.agents/EARLY_HINTS.md +120 -0
- package/.agents/HEALTH.md +118 -0
- package/.agents/LIMITS.md +109 -0
- package/.agents/ORM.md +155 -1
- package/.agents/PLUGINS.md +28 -0
- package/.agents/RESILIENCE.md +235 -0
- package/AGENTS.md +6 -0
- package/README.md +2 -2
- package/bin/commands/add-docker.js +6 -6
- package/bin/commands/dev.js +1 -1
- package/bin/commands/new.js +5 -5
- package/bin/commands/openapi.js +1 -1
- package/bin/commands/start.js +1 -1
- package/bin/utils/project.js +2 -2
- package/core/auth/hash.js +5 -4
- package/core/auth/jwt.js +5 -54
- package/core/auth/magic-link.js +4 -5
- package/core/auth/manager.js +3 -5
- package/core/auth/oauth.js +8 -17
- package/core/auth/totp.js +2 -1
- package/core/cache/byte-size.js +123 -0
- package/core/cache/index.js +21 -0
- package/core/cache/lru.js +477 -0
- package/core/cache/tagged-lru.js +346 -0
- package/core/commands/registry.js +6 -20
- package/core/early-hints/index.js +267 -0
- package/core/errors/domain.js +48 -0
- package/core/errors/http.js +31 -0
- package/core/errors/index.js +10 -0
- package/core/errors/normalize.js +88 -2
- package/core/events/discovery.js +6 -24
- package/core/events/dispatcher.js +2 -7
- package/core/health/index.js +41 -0
- package/core/health/middleware.js +213 -0
- package/core/health/normalize.js +140 -0
- package/core/health/probes.js +203 -0
- package/core/limits/index.js +34 -0
- package/core/limits/middleware.js +317 -0
- package/core/limits/normalize.js +222 -0
- package/core/orm/batch.js +70 -0
- package/core/orm/cache/fingerprint.js +2 -2
- package/core/orm/cache/memory-provider.js +27 -110
- package/core/orm/index.js +71 -8
- package/core/orm/json-fields.js +22 -8
- package/core/orm/model.js +10 -0
- package/core/orm/query-builder.js +63 -9
- package/core/orm/repository.js +318 -19
- package/core/orm/schema-helpers.js +8 -10
- package/core/orm/scopes.js +321 -1
- package/core/orm/transaction.js +10 -2
- package/core/orm/utils.js +4 -86
- package/core/queue/adapters/database.js +4 -8
- package/core/queue/manager.js +55 -41
- package/core/resilience/circuit-breaker.js +391 -0
- package/core/resilience/errors.js +63 -0
- package/core/resilience/index.js +53 -0
- package/core/resilience/registry.js +97 -0
- package/core/resilience/retry.js +203 -0
- package/core/schema-cache.js +9 -2
- package/core/shutdown/http-adapter.js +4 -1
- package/core/shutdown/shutdown-manager.js +120 -19
- package/core/utils/array.js +12 -0
- package/core/utils/base64url.js +8 -0
- package/core/utils/crypto.js +118 -0
- package/core/utils/ensure-leading-slash.js +6 -0
- package/core/utils/escape-regexp.js +7 -0
- package/core/utils/fetch.js +6 -5
- package/core/utils/fs.js +144 -0
- package/core/utils/hmac.js +6 -0
- package/core/utils/json.js +98 -0
- package/core/utils/object.js +106 -0
- package/core/utils/random.js +9 -0
- package/core/utils/regex.js +77 -0
- package/core/utils/sha256.js +6 -0
- package/core/utils/string.js +87 -0
- package/core/utils/strip-trailing-slash.js +6 -0
- package/core/utils/time.js +96 -0
- package/core/utils/timeout.js +348 -0
- package/core/utils/url.js +106 -0
- package/core/utils/walk.js +7 -0
- package/index.d.ts +616 -1
- package/index.js +148 -0
- package/package.json +1 -2
- package/plugins/admin-panel/core/api-extensions.js +2 -5
- package/plugins/admin-panel/index.js +1 -1
- package/plugins/admin-panel/modules/menu.js +1 -1
- package/plugins/api-key/crypto.js +3 -3
- package/plugins/api-key/scopes.js +5 -7
- package/plugins/audit-log/api-handlers.js +6 -15
- package/plugins/audit-log/parse.js +3 -3
- package/plugins/csrf/index.js +5 -9
- package/plugins/dashboard/app.js +2 -2
- package/plugins/dashboard/index.js +5 -5
- package/plugins/data-exchange/record-selection.js +2 -5
- package/plugins/email/api-handlers.js +5 -18
- package/plugins/email/auth-bridge.js +3 -2
- package/plugins/health-check.js +2 -1
- package/plugins/index.js +2 -0
- package/plugins/lazy.js +5 -6
- package/plugins/mcp/discovery.js +18 -17
- package/plugins/polar/src/api.js +2 -1
- package/plugins/polar/src/webhooks.js +5 -6
- package/plugins/rate-limit/index.js +3 -1
- package/plugins/redirect/index.js +9 -7
- package/plugins/rls/index.js +170 -0
- package/plugins/site-analytics/tracking.js +2 -2
- package/plugins/sitemap.js +3 -2
- package/plugins/swagger.js +4 -3
- package/plugins/upload/index.js +4 -3
- package/plugins/upload/orphan-pruner.js +4 -3
- package/plugins/webhooks/crypto.js +6 -11
- package/plugins/webhooks/dispatcher.js +8 -8
- package/plugins/webhooks/index.js +6 -0
- package/plugins/webhooks/matcher.js +7 -11
- package/plugins/webhooks/sender.js +8 -11
- package/src/app-context.js +41 -5
- package/src/dev/banner.js +6 -2
- package/src/file-router.js +1 -3
- package/src/helpers.js +159 -24
- package/src/modules/module-discovery.js +1 -9
- package/src/pages/page-loader.js +53 -1
- package/src/plugin-manager.js +90 -17
- package/src/server.js +136 -25
- package/src/services/discovery.js +19 -40
- package/src/services/executor.js +86 -25
- package/src/services/memoize.js +3 -25
- package/src/services/registry.js +6 -0
- package/src/template/precompile.js +8 -19
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso System & Component Health Probes
|
|
5
|
+
* Gathers system metrics (uptime, memory, event loop delay),
|
|
6
|
+
* tests database and queue availability, and runs user-defined health probes.
|
|
7
|
+
*
|
|
8
|
+
* @module core/health/probes
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { monitorEventLoopDelay } = require('perf_hooks');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Formats bytes to human-readable megabytes
|
|
15
|
+
* @param {number} bytes
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
function formatMb(bytes) {
|
|
19
|
+
if (typeof bytes !== 'number' || isNaN(bytes)) return '0 MB';
|
|
20
|
+
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Creates an event loop delay monitor
|
|
25
|
+
* @returns {{ getStats: () => Object, destroy: () => void }|null}
|
|
26
|
+
*/
|
|
27
|
+
function createEventLoopMonitor() {
|
|
28
|
+
try {
|
|
29
|
+
const histogram = monitorEventLoopDelay({ resolution: 20 });
|
|
30
|
+
histogram.enable();
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
getStats() {
|
|
34
|
+
const meanNs = histogram.mean || 0;
|
|
35
|
+
const maxNs = histogram.max || 0;
|
|
36
|
+
const p50Ns = histogram.percentile ? histogram.percentile(50) : 0;
|
|
37
|
+
const p99Ns = histogram.percentile ? histogram.percentile(99) : 0;
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
meanMs: Math.round((meanNs / 1e6) * 100) / 100,
|
|
41
|
+
maxMs: Math.round((maxNs / 1e6) * 100) / 100,
|
|
42
|
+
p50Ms: Math.round((p50Ns / 1e6) * 100) / 100,
|
|
43
|
+
p99Ms: Math.round((p99Ns / 1e6) * 100) / 100,
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
destroy() {
|
|
47
|
+
try {
|
|
48
|
+
histogram.disable();
|
|
49
|
+
} catch {}
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Collects runtime system health metrics
|
|
59
|
+
* @param {Object} [monitor] - Event loop monitor instance
|
|
60
|
+
* @returns {Object}
|
|
61
|
+
*/
|
|
62
|
+
function getSystemMetrics(monitor = null) {
|
|
63
|
+
const mem = process.memoryUsage();
|
|
64
|
+
const uptimeSeconds = Math.floor(process.uptime());
|
|
65
|
+
|
|
66
|
+
const metrics = {
|
|
67
|
+
uptimeSeconds,
|
|
68
|
+
memory: {
|
|
69
|
+
rss: formatMb(mem.rss),
|
|
70
|
+
heapUsed: formatMb(mem.heapUsed),
|
|
71
|
+
heapTotal: formatMb(mem.heapTotal),
|
|
72
|
+
external: formatMb(mem.external),
|
|
73
|
+
bytes: {
|
|
74
|
+
rss: mem.rss,
|
|
75
|
+
heapUsed: mem.heapUsed,
|
|
76
|
+
heapTotal: mem.heapTotal,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
pid: process.pid,
|
|
80
|
+
nodeVersion: process.version,
|
|
81
|
+
platform: process.platform,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (monitor && typeof monitor.getStats === 'function') {
|
|
85
|
+
metrics.eventLoopLag = monitor.getStats();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return metrics;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Safely probes database connectivity with timeout
|
|
93
|
+
* @param {Object} db - Webspresso database instance
|
|
94
|
+
* @param {number} [timeoutMs=3000] - Timeout in milliseconds
|
|
95
|
+
* @returns {Promise<{ status: 'up'|'down'|'skipped', latencyMs?: number, error?: string }>}
|
|
96
|
+
*/
|
|
97
|
+
async function probeDatabase(db, timeoutMs = 3000) {
|
|
98
|
+
if (!db) {
|
|
99
|
+
return { status: 'skipped', message: 'No database configured' };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const knex = db.knex || db;
|
|
103
|
+
if (!knex || typeof knex.raw !== 'function') {
|
|
104
|
+
return { status: 'up', message: 'Custom database without raw ping' };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const startTime = Date.now();
|
|
108
|
+
let timer = null;
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
112
|
+
timer = setTimeout(() => {
|
|
113
|
+
reject(new Error(`Database ping timed out after ${timeoutMs}ms`));
|
|
114
|
+
}, timeoutMs);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
await Promise.race([knex.raw('SELECT 1'), timeoutPromise]);
|
|
118
|
+
const latencyMs = Date.now() - startTime;
|
|
119
|
+
return { status: 'up', latencyMs };
|
|
120
|
+
} catch (err) {
|
|
121
|
+
return { status: 'down', error: err.message };
|
|
122
|
+
} finally {
|
|
123
|
+
if (timer) clearTimeout(timer);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Probes background worker queue readiness
|
|
129
|
+
* @param {Object} queue - Queue instance
|
|
130
|
+
* @returns {Promise<{ status: 'up'|'down'|'skipped', stats?: Object, error?: string }>}
|
|
131
|
+
*/
|
|
132
|
+
async function probeQueue(queue) {
|
|
133
|
+
if (!queue) {
|
|
134
|
+
return { status: 'skipped', message: 'No queue configured' };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
if (typeof queue.getStats === 'function') {
|
|
139
|
+
const stats = await queue.getStats();
|
|
140
|
+
return { status: 'up', stats };
|
|
141
|
+
}
|
|
142
|
+
if (typeof queue.isReady === 'boolean') {
|
|
143
|
+
return { status: queue.isReady ? 'up' : 'down' };
|
|
144
|
+
}
|
|
145
|
+
return { status: 'up' };
|
|
146
|
+
} catch (err) {
|
|
147
|
+
return { status: 'down', error: err.message };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Runs user-registered custom health checks concurrently
|
|
153
|
+
* @param {Record<string, Function>} checks - Map of named probe functions
|
|
154
|
+
* @param {Object} context - Server context
|
|
155
|
+
* @param {number} [timeoutMs=3000] - Timeout per probe
|
|
156
|
+
* @returns {Promise<{ isHealthy: boolean, results: Record<string, Object> }>}
|
|
157
|
+
*/
|
|
158
|
+
async function probeCustomChecks(checks = {}, context = {}, timeoutMs = 3000) {
|
|
159
|
+
const entries = Object.entries(checks);
|
|
160
|
+
if (entries.length === 0) {
|
|
161
|
+
return { isHealthy: true, results: {} };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const results = {};
|
|
165
|
+
let isHealthy = true;
|
|
166
|
+
|
|
167
|
+
const promises = entries.map(async ([name, checkFn]) => {
|
|
168
|
+
let timer = null;
|
|
169
|
+
const startTime = Date.now();
|
|
170
|
+
try {
|
|
171
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
172
|
+
timer = setTimeout(() => reject(new Error(`Health check "${name}" timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const res = await Promise.race([Promise.resolve(checkFn(context)), timeoutPromise]);
|
|
176
|
+
const latencyMs = Date.now() - startTime;
|
|
177
|
+
|
|
178
|
+
if (res === false || (res && typeof res === 'object' && res.status === 'down')) {
|
|
179
|
+
isHealthy = false;
|
|
180
|
+
results[name] = { status: 'down', latencyMs, ...(typeof res === 'object' ? res : {}) };
|
|
181
|
+
} else {
|
|
182
|
+
results[name] = { status: 'up', latencyMs, ...(typeof res === 'object' ? res : {}) };
|
|
183
|
+
}
|
|
184
|
+
} catch (err) {
|
|
185
|
+
isHealthy = false;
|
|
186
|
+
results[name] = { status: 'down', error: err.message };
|
|
187
|
+
} finally {
|
|
188
|
+
if (timer) clearTimeout(timer);
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
await Promise.all(promises);
|
|
193
|
+
return { isHealthy, results };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = {
|
|
197
|
+
formatMb,
|
|
198
|
+
createEventLoopMonitor,
|
|
199
|
+
getSystemMetrics,
|
|
200
|
+
probeDatabase,
|
|
201
|
+
probeQueue,
|
|
202
|
+
probeCustomChecks,
|
|
203
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso Payload Limits & DoS Protection Module
|
|
5
|
+
* Configurable body size limits for JSON, URL-encoded, raw, and text payloads,
|
|
6
|
+
* with route-level overrides, raw body capture, and seamless 413 PayloadTooLargeError handling.
|
|
7
|
+
*
|
|
8
|
+
* @module core/limits
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
DEFAULT_LIMITS,
|
|
13
|
+
parseByteSize,
|
|
14
|
+
normalizeLimits,
|
|
15
|
+
} = require('./normalize');
|
|
16
|
+
|
|
17
|
+
const {
|
|
18
|
+
matchesPath,
|
|
19
|
+
shouldSkipBodyParser,
|
|
20
|
+
shouldCaptureRawBody,
|
|
21
|
+
createLimitsMiddleware,
|
|
22
|
+
setupLimitsMiddleware,
|
|
23
|
+
} = require('./middleware');
|
|
24
|
+
|
|
25
|
+
module.exports = {
|
|
26
|
+
DEFAULT_LIMITS,
|
|
27
|
+
parseByteSize,
|
|
28
|
+
normalizeLimits,
|
|
29
|
+
matchesPath,
|
|
30
|
+
shouldSkipBodyParser,
|
|
31
|
+
shouldCaptureRawBody,
|
|
32
|
+
createLimitsMiddleware,
|
|
33
|
+
setupLimitsMiddleware,
|
|
34
|
+
};
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso Payload Limits Middleware
|
|
5
|
+
* Zero-dependency body size enforcement, DoS protection, and raw body capture.
|
|
6
|
+
*
|
|
7
|
+
* @module core/limits/middleware
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const express = require('express');
|
|
11
|
+
const { normalizeLimits } = require('./normalize');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Checks if a given URL pathname matches a path pattern (string, glob wildcard, or RegExp).
|
|
15
|
+
*
|
|
16
|
+
* @param {string|RegExp|Function} pattern
|
|
17
|
+
* @param {string} pathname
|
|
18
|
+
* @returns {boolean}
|
|
19
|
+
*/
|
|
20
|
+
function matchesPath(pattern, pathname) {
|
|
21
|
+
if (!pathname || !pattern) return false;
|
|
22
|
+
|
|
23
|
+
if (typeof pattern === 'function') {
|
|
24
|
+
try {
|
|
25
|
+
return Boolean(pattern(pathname));
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (pattern instanceof RegExp) {
|
|
32
|
+
return pattern.test(pathname);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (typeof pattern !== 'string') return false;
|
|
36
|
+
|
|
37
|
+
// Exact match
|
|
38
|
+
if (pattern === pathname) return true;
|
|
39
|
+
|
|
40
|
+
// Wildcard match (e.g. /api/webhooks/* or /api/webhooks/**)
|
|
41
|
+
if (pattern.endsWith('/**')) {
|
|
42
|
+
const base = pattern.slice(0, -3);
|
|
43
|
+
return pathname === base || pathname.startsWith(base.endsWith('/') ? base : `${base}/`);
|
|
44
|
+
}
|
|
45
|
+
if (pattern.endsWith('/*')) {
|
|
46
|
+
const base = pattern.slice(0, -2);
|
|
47
|
+
return pathname === base || pathname.startsWith(base.endsWith('/') ? base : `${base}/`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Parameterized express pattern: /api/users/:id/upload
|
|
51
|
+
if (pattern.includes(':')) {
|
|
52
|
+
const regexPattern = '^' + pattern.replace(/\/:[^/]+/g, '/[^/]+') + '$';
|
|
53
|
+
try {
|
|
54
|
+
return new RegExp(regexPattern).test(pathname);
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Determines whether body parsing should be skipped for the incoming request.
|
|
65
|
+
*
|
|
66
|
+
* @param {import('express').Request} req
|
|
67
|
+
* @param {Array<string|RegExp|Function>} excludeList
|
|
68
|
+
* @returns {boolean}
|
|
69
|
+
*/
|
|
70
|
+
function shouldSkipBodyParser(req, excludeList) {
|
|
71
|
+
if (!Array.isArray(excludeList) || excludeList.length === 0) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
const currentPath = req.path || '';
|
|
75
|
+
const originalUrl = req.originalUrl ? req.originalUrl.split('?')[0] : '';
|
|
76
|
+
|
|
77
|
+
return excludeList.some(
|
|
78
|
+
(pattern) => matchesPath(pattern, currentPath) || matchesPath(pattern, originalUrl)
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Determines whether raw body buffer should be captured into `req.rawBody`.
|
|
84
|
+
*
|
|
85
|
+
* @param {import('express').Request} req
|
|
86
|
+
* @param {boolean|Array<string|RegExp>|Function} captureSetting
|
|
87
|
+
* @returns {boolean}
|
|
88
|
+
*/
|
|
89
|
+
function shouldCaptureRawBody(req, captureSetting) {
|
|
90
|
+
if (!captureSetting) return false;
|
|
91
|
+
if (captureSetting === true) return true;
|
|
92
|
+
|
|
93
|
+
if (typeof captureSetting === 'function') {
|
|
94
|
+
try {
|
|
95
|
+
return Boolean(captureSetting(req));
|
|
96
|
+
} catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (Array.isArray(captureSetting)) {
|
|
102
|
+
const currentPath = req.path || '';
|
|
103
|
+
const originalUrl = req.originalUrl ? req.originalUrl.split('?')[0] : '';
|
|
104
|
+
return captureSetting.some(
|
|
105
|
+
(pattern) => matchesPath(pattern, currentPath) || matchesPath(pattern, originalUrl)
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Creates route-level limits middleware executing custom body parsers.
|
|
114
|
+
*
|
|
115
|
+
* @param {Object} [routeOptions]
|
|
116
|
+
* @returns {import('express').RequestHandler}
|
|
117
|
+
*/
|
|
118
|
+
function createLimitsMiddleware(routeOptions = {}) {
|
|
119
|
+
const limits = normalizeLimits(routeOptions);
|
|
120
|
+
const parsers = [];
|
|
121
|
+
|
|
122
|
+
function makeVerify(userVerify) {
|
|
123
|
+
return (req, res, buf, encoding) => {
|
|
124
|
+
if (typeof userVerify === 'function') {
|
|
125
|
+
userVerify(req, res, buf, encoding);
|
|
126
|
+
}
|
|
127
|
+
if (shouldCaptureRawBody(req, limits.captureRawBody)) {
|
|
128
|
+
req.rawBody = buf;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (limits.json.enabled) {
|
|
134
|
+
parsers.push(
|
|
135
|
+
express.json({
|
|
136
|
+
...limits.json.options,
|
|
137
|
+
limit: limits.json.limit,
|
|
138
|
+
strict: limits.json.strict,
|
|
139
|
+
verify: makeVerify(limits.json.verify),
|
|
140
|
+
})
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (limits.urlencoded.enabled) {
|
|
145
|
+
parsers.push(
|
|
146
|
+
express.urlencoded({
|
|
147
|
+
...limits.urlencoded.options,
|
|
148
|
+
limit: limits.urlencoded.limit,
|
|
149
|
+
extended: limits.urlencoded.extended,
|
|
150
|
+
parameterLimit: limits.urlencoded.parameterLimit,
|
|
151
|
+
verify: makeVerify(limits.urlencoded.verify),
|
|
152
|
+
})
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (limits.raw.enabled) {
|
|
157
|
+
parsers.push(
|
|
158
|
+
express.raw({
|
|
159
|
+
...limits.raw.options,
|
|
160
|
+
limit: limits.raw.limit,
|
|
161
|
+
type: limits.raw.type,
|
|
162
|
+
verify: makeVerify(limits.raw.verify),
|
|
163
|
+
})
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (limits.text.enabled) {
|
|
168
|
+
parsers.push(
|
|
169
|
+
express.text({
|
|
170
|
+
...limits.text.options,
|
|
171
|
+
limit: limits.text.limit,
|
|
172
|
+
type: limits.text.type,
|
|
173
|
+
verify: makeVerify(limits.text.verify),
|
|
174
|
+
})
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return function routeLimitsMiddleware(req, res, next) {
|
|
179
|
+
if (shouldSkipBodyParser(req, limits.exclude)) {
|
|
180
|
+
return next();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let idx = 0;
|
|
184
|
+
function runNextParser(err) {
|
|
185
|
+
if (err) return next(err);
|
|
186
|
+
if (idx >= parsers.length) return next();
|
|
187
|
+
const parser = parsers[idx++];
|
|
188
|
+
parser(req, res, runNextParser);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
runNextParser();
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Mounts payload limits and body parsing middleware onto an Express application.
|
|
197
|
+
*
|
|
198
|
+
* @param {import('express').Application} app
|
|
199
|
+
* @param {Object} [options]
|
|
200
|
+
* @returns {Object} Normalized limits configuration
|
|
201
|
+
*/
|
|
202
|
+
function setupLimitsMiddleware(app, options = {}) {
|
|
203
|
+
const limits = normalizeLimits(options);
|
|
204
|
+
|
|
205
|
+
// Expose limits on app instance for introspection
|
|
206
|
+
app.limits = limits;
|
|
207
|
+
if (typeof app.set === 'function') {
|
|
208
|
+
app.set('webspresso.limits', limits);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const allExcludes = [...limits.exclude];
|
|
212
|
+
|
|
213
|
+
// 1. Mount route-specific overrides (mounted first so they take precedence)
|
|
214
|
+
if (limits.routes && typeof limits.routes === 'object') {
|
|
215
|
+
for (const [routePattern, routeConfig] of Object.entries(limits.routes)) {
|
|
216
|
+
if (routeConfig === false) {
|
|
217
|
+
// Entirely exclude this route from global body parsing
|
|
218
|
+
allExcludes.push(routePattern);
|
|
219
|
+
} else if (routeConfig && typeof routeConfig === 'object') {
|
|
220
|
+
// Mount route-specific body parsers on the path
|
|
221
|
+
const routeMw = createLimitsMiddleware(routeConfig);
|
|
222
|
+
app.use(routePattern, routeMw);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 2. Setup Global Body Parsers with exclusion checking and raw body capture
|
|
228
|
+
function globalVerify(userVerify) {
|
|
229
|
+
return (req, res, buf, encoding) => {
|
|
230
|
+
if (typeof userVerify === 'function') {
|
|
231
|
+
userVerify(req, res, buf, encoding);
|
|
232
|
+
}
|
|
233
|
+
if (shouldCaptureRawBody(req, limits.captureRawBody)) {
|
|
234
|
+
req.rawBody = buf;
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Global JSON Parser
|
|
240
|
+
if (limits.json.enabled) {
|
|
241
|
+
const jsonParserFn = express.json({
|
|
242
|
+
...limits.json.options,
|
|
243
|
+
limit: limits.json.limit,
|
|
244
|
+
strict: limits.json.strict,
|
|
245
|
+
verify: globalVerify(limits.json.verify),
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
app.use(function jsonParser(req, res, next) {
|
|
249
|
+
if (req._body || shouldSkipBodyParser(req, allExcludes)) {
|
|
250
|
+
return next();
|
|
251
|
+
}
|
|
252
|
+
jsonParserFn(req, res, next);
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Global URL-Encoded Parser
|
|
257
|
+
if (limits.urlencoded.enabled) {
|
|
258
|
+
const urlencodedParserFn = express.urlencoded({
|
|
259
|
+
...limits.urlencoded.options,
|
|
260
|
+
limit: limits.urlencoded.limit,
|
|
261
|
+
extended: limits.urlencoded.extended,
|
|
262
|
+
parameterLimit: limits.urlencoded.parameterLimit,
|
|
263
|
+
verify: globalVerify(limits.urlencoded.verify),
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
app.use(function urlencodedParser(req, res, next) {
|
|
267
|
+
if (req._body || shouldSkipBodyParser(req, allExcludes)) {
|
|
268
|
+
return next();
|
|
269
|
+
}
|
|
270
|
+
urlencodedParserFn(req, res, next);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Global Raw Body Parser
|
|
275
|
+
if (limits.raw.enabled) {
|
|
276
|
+
const rawParser = express.raw({
|
|
277
|
+
...limits.raw.options,
|
|
278
|
+
limit: limits.raw.limit,
|
|
279
|
+
type: limits.raw.type,
|
|
280
|
+
verify: globalVerify(limits.raw.verify),
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
app.use((req, res, next) => {
|
|
284
|
+
if (req._body || shouldSkipBodyParser(req, allExcludes)) {
|
|
285
|
+
return next();
|
|
286
|
+
}
|
|
287
|
+
rawParser(req, res, next);
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Global Text Parser
|
|
292
|
+
if (limits.text.enabled) {
|
|
293
|
+
const textParser = express.text({
|
|
294
|
+
...limits.text.options,
|
|
295
|
+
limit: limits.text.limit,
|
|
296
|
+
type: limits.text.type,
|
|
297
|
+
verify: globalVerify(limits.text.verify),
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
app.use((req, res, next) => {
|
|
301
|
+
if (req._body || shouldSkipBodyParser(req, allExcludes)) {
|
|
302
|
+
return next();
|
|
303
|
+
}
|
|
304
|
+
textParser(req, res, next);
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return limits;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
module.exports = {
|
|
312
|
+
matchesPath,
|
|
313
|
+
shouldSkipBodyParser,
|
|
314
|
+
shouldCaptureRawBody,
|
|
315
|
+
createLimitsMiddleware,
|
|
316
|
+
setupLimitsMiddleware,
|
|
317
|
+
};
|