webspresso 0.0.103 → 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 +4 -3
- package/bin/commands/new.js +5 -5
- package/bin/commands/openapi.js +1 -1
- package/bin/commands/start.js +1 -1
- package/bin/utils/orm-map-html.js +2 -8
- package/bin/utils/project.js +2 -2
- package/core/auth/hash.js +5 -4
- package/core/auth/jwt.js +6 -57
- package/core/auth/magic-link.js +4 -5
- package/core/auth/manager.js +3 -5
- package/core/auth/oauth.js +45 -30
- package/core/auth/totp.js +5 -16
- package/core/auth/user-agent.js +1 -21
- 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/content/renderer.js +1 -13
- 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/middleware.js +2 -8
- package/core/errors/normalize.js +88 -2
- package/core/events/discovery.js +6 -24
- package/core/events/dispatcher.js +2 -7
- package/core/forms/form-builder.js +1 -9
- 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 +76 -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/query-filters.js +393 -0
- 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 +132 -31
- package/core/utils/array.js +12 -0
- package/core/utils/base64url.js +8 -0
- package/core/utils/crypto.js +191 -0
- package/core/utils/ensure-leading-slash.js +6 -0
- package/core/utils/escape-html.js +7 -0
- package/core/utils/escape-regexp.js +7 -0
- package/core/utils/fetch.js +121 -0
- package/core/utils/fs.js +144 -0
- package/core/utils/get-client-ip.js +6 -0
- package/core/utils/hmac.js +6 -0
- package/core/utils/html.js +72 -0
- package/core/utils/ip.js +86 -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/safe-compare.js +3 -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 +627 -2
- package/index.js +158 -0
- package/package.json +1 -2
- package/plugins/admin-panel/auth.js +15 -7
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +82 -10
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +4 -2
- package/plugins/admin-panel/client/parts/08-record-list.js +3 -2
- package/plugins/admin-panel/client/parts/10-export-registry.js +6 -0
- package/plugins/admin-panel/core/api-extensions.js +3 -80
- package/plugins/admin-panel/index.js +1 -1
- package/plugins/admin-panel/lib/login-rate-limiter.js +15 -13
- package/plugins/admin-panel/modules/menu.js +1 -1
- package/plugins/api-key/crypto.js +3 -26
- package/plugins/api-key/scopes.js +5 -7
- package/plugins/audit-log/api-handlers.js +8 -7
- package/plugins/audit-log/middleware.js +11 -2
- package/plugins/audit-log/parse.js +3 -3
- package/plugins/basic-auth/index.js +2 -26
- package/plugins/cors.js +2 -2
- package/plugins/csrf/index.js +7 -23
- 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/file-manager/admin-component.js +3 -1
- package/plugins/file-manager/core.js +34 -4
- package/plugins/health-check.js +2 -1
- package/plugins/index.js +2 -0
- package/plugins/lazy.js +5 -6
- package/plugins/mcp/auth.js +1 -13
- package/plugins/mcp/discovery.js +18 -17
- package/plugins/mcp/index.js +23 -1
- package/plugins/mcp/transports/sse.js +10 -1
- package/plugins/orm-cache-admin/admin-component.js +3 -2
- package/plugins/polar/src/api.js +11 -3
- package/plugins/polar/src/webhooks.js +7 -13
- package/plugins/rate-limit/index.js +3 -1
- package/plugins/recaptcha/helpers.js +1 -7
- package/plugins/recaptcha/verify.js +37 -23
- package/plugins/redirect/index.js +57 -7
- package/plugins/rest-resources/index.js +1 -207
- package/plugins/rls/index.js +170 -0
- package/plugins/schema-explorer/admin-component.js +4 -2
- package/plugins/schema-explorer.js +40 -29
- package/plugins/seo-checker/index.js +5 -2
- package/plugins/site-analytics/tracking.js +21 -18
- package/plugins/sitemap.js +21 -17
- package/plugins/swagger.js +13 -17
- package/plugins/upload/index.js +4 -3
- package/plugins/upload/orphan-pruner.js +4 -3
- package/plugins/webhooks/crypto.js +6 -28
- package/plugins/webhooks/dispatcher.js +14 -9
- package/plugins/webhooks/index.js +6 -0
- package/plugins/webhooks/matcher.js +28 -17
- package/plugins/webhooks/sender.js +40 -13
- package/src/app-context.js +41 -5
- package/src/dev/banner.js +6 -2
- package/src/dev-reload/index.js +47 -6
- package/src/file-router.js +1 -3
- package/src/helpers.js +180 -33
- 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 +137 -26
- 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
package/core/errors/domain.js
CHANGED
|
@@ -180,6 +180,51 @@ class RouteGenerationError extends RouterError {
|
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Circuit Breaker Open Error
|
|
185
|
+
* Thrown when an invocation is rejected because the circuit is in the OPEN state.
|
|
186
|
+
* Maps to HTTP 503 Service Unavailable.
|
|
187
|
+
*/
|
|
188
|
+
class CircuitBreakerOpenError extends HttpError {
|
|
189
|
+
/**
|
|
190
|
+
* @param {string} [breakerName='default']
|
|
191
|
+
* @param {number} [resetTimeoutMs=0]
|
|
192
|
+
* @param {Object} [options]
|
|
193
|
+
*/
|
|
194
|
+
constructor(breakerName = 'default', resetTimeoutMs = 0, options = {}) {
|
|
195
|
+
super(503, `Circuit breaker '${breakerName}' is OPEN and failing fast`, {
|
|
196
|
+
code: 'CIRCUIT_BREAKER_OPEN',
|
|
197
|
+
expose: true,
|
|
198
|
+
...options,
|
|
199
|
+
});
|
|
200
|
+
this.breakerName = breakerName;
|
|
201
|
+
this.resetTimeoutMs = resetTimeoutMs;
|
|
202
|
+
this.state = 'OPEN';
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Circuit Breaker Timeout Error
|
|
208
|
+
* Thrown when execution inside a circuit breaker exceeds timeout.
|
|
209
|
+
* Maps to HTTP 504 Gateway Timeout.
|
|
210
|
+
*/
|
|
211
|
+
class CircuitBreakerTimeoutError extends HttpError {
|
|
212
|
+
/**
|
|
213
|
+
* @param {string} [breakerName='default']
|
|
214
|
+
* @param {number} [timeoutMs=0]
|
|
215
|
+
* @param {Object} [options]
|
|
216
|
+
*/
|
|
217
|
+
constructor(breakerName = 'default', timeoutMs = 0, options = {}) {
|
|
218
|
+
super(504, `Execution in circuit breaker '${breakerName}' timed out after ${timeoutMs}ms`, {
|
|
219
|
+
code: 'CIRCUIT_BREAKER_TIMEOUT',
|
|
220
|
+
expose: true,
|
|
221
|
+
...options,
|
|
222
|
+
});
|
|
223
|
+
this.breakerName = breakerName;
|
|
224
|
+
this.timeoutMs = timeoutMs;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
183
228
|
module.exports = {
|
|
184
229
|
ValidationError,
|
|
185
230
|
ConfigurationError,
|
|
@@ -190,4 +235,7 @@ module.exports = {
|
|
|
190
235
|
RouterError,
|
|
191
236
|
RouteNotFoundError,
|
|
192
237
|
RouteGenerationError,
|
|
238
|
+
CircuitBreakerOpenError,
|
|
239
|
+
CircuitBreakerTimeoutError,
|
|
193
240
|
};
|
|
241
|
+
|
package/core/errors/http.js
CHANGED
|
@@ -109,6 +109,15 @@ class UnprocessableEntityError extends HttpError {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* 408 Request Timeout
|
|
114
|
+
*/
|
|
115
|
+
class RequestTimeoutError extends HttpError {
|
|
116
|
+
constructor(message = 'Request Timeout', options = {}) {
|
|
117
|
+
super(408, message, { code: 'REQUEST_TIMEOUT', ...options });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
112
121
|
/**
|
|
113
122
|
* 429 Too Many Requests
|
|
114
123
|
*/
|
|
@@ -118,6 +127,24 @@ class TooManyRequestsError extends HttpError {
|
|
|
118
127
|
}
|
|
119
128
|
}
|
|
120
129
|
|
|
130
|
+
/**
|
|
131
|
+
* 503 Service Unavailable
|
|
132
|
+
*/
|
|
133
|
+
class ServiceUnavailableError extends HttpError {
|
|
134
|
+
constructor(message = 'Service Unavailable', options = {}) {
|
|
135
|
+
super(503, message, { code: 'SERVICE_UNAVAILABLE', ...options });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 504 Gateway Timeout
|
|
141
|
+
*/
|
|
142
|
+
class GatewayTimeoutError extends HttpError {
|
|
143
|
+
constructor(message = 'Gateway Timeout', options = {}) {
|
|
144
|
+
super(504, message, { code: 'GATEWAY_TIMEOUT', ...options });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
121
148
|
module.exports = {
|
|
122
149
|
HttpError,
|
|
123
150
|
BadRequestError,
|
|
@@ -129,5 +156,9 @@ module.exports = {
|
|
|
129
156
|
PayloadTooLargeError,
|
|
130
157
|
UnsupportedMediaTypeError,
|
|
131
158
|
UnprocessableEntityError,
|
|
159
|
+
RequestTimeoutError,
|
|
132
160
|
TooManyRequestsError,
|
|
161
|
+
ServiceUnavailableError,
|
|
162
|
+
GatewayTimeoutError,
|
|
133
163
|
};
|
|
164
|
+
|
package/core/errors/index.js
CHANGED
|
@@ -15,7 +15,10 @@ const {
|
|
|
15
15
|
PayloadTooLargeError,
|
|
16
16
|
UnsupportedMediaTypeError,
|
|
17
17
|
UnprocessableEntityError,
|
|
18
|
+
RequestTimeoutError,
|
|
18
19
|
TooManyRequestsError,
|
|
20
|
+
ServiceUnavailableError,
|
|
21
|
+
GatewayTimeoutError,
|
|
19
22
|
} = require('./http');
|
|
20
23
|
const {
|
|
21
24
|
ValidationError,
|
|
@@ -27,6 +30,8 @@ const {
|
|
|
27
30
|
RouterError,
|
|
28
31
|
RouteNotFoundError,
|
|
29
32
|
RouteGenerationError,
|
|
33
|
+
CircuitBreakerOpenError,
|
|
34
|
+
CircuitBreakerTimeoutError,
|
|
30
35
|
} = require('./domain');
|
|
31
36
|
const {
|
|
32
37
|
getStatusTitle,
|
|
@@ -54,7 +59,10 @@ module.exports = {
|
|
|
54
59
|
PayloadTooLargeError,
|
|
55
60
|
UnsupportedMediaTypeError,
|
|
56
61
|
UnprocessableEntityError,
|
|
62
|
+
RequestTimeoutError,
|
|
57
63
|
TooManyRequestsError,
|
|
64
|
+
ServiceUnavailableError,
|
|
65
|
+
GatewayTimeoutError,
|
|
58
66
|
|
|
59
67
|
// Domain Exceptions
|
|
60
68
|
ValidationError,
|
|
@@ -66,6 +74,8 @@ module.exports = {
|
|
|
66
74
|
RouterError,
|
|
67
75
|
RouteNotFoundError,
|
|
68
76
|
RouteGenerationError,
|
|
77
|
+
CircuitBreakerOpenError,
|
|
78
|
+
CircuitBreakerTimeoutError,
|
|
69
79
|
|
|
70
80
|
// Utilities & Middleware
|
|
71
81
|
getStatusTitle,
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
const { normalizeError, toErrorResponseObject } = require('./normalize');
|
|
7
7
|
const { RequestAbortedError } = require('./domain');
|
|
8
|
+
const { escapeHtml } = require('../utils/html');
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Check if the request prefers a JSON error response
|
|
@@ -60,14 +61,7 @@ function renderDefaultErrorHtml(err, isDev) {
|
|
|
60
61
|
</html>`;
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
return String(str || '')
|
|
65
|
-
.replace(/&/g, '&')
|
|
66
|
-
.replace(/</g, '<')
|
|
67
|
-
.replace(/>/g, '>')
|
|
68
|
-
.replace(/"/g, '"')
|
|
69
|
-
.replace(/'/g, ''');
|
|
70
|
-
}
|
|
64
|
+
|
|
71
65
|
|
|
72
66
|
/**
|
|
73
67
|
* Create Webspresso Central Error Handler Middleware
|
package/core/errors/normalize.js
CHANGED
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const { WebspressoError } = require('./base');
|
|
8
|
-
const {
|
|
8
|
+
const {
|
|
9
|
+
HttpError,
|
|
10
|
+
BadRequestError,
|
|
11
|
+
PayloadTooLargeError,
|
|
12
|
+
} = require('./http');
|
|
9
13
|
|
|
10
14
|
const HTTP_TITLES = {
|
|
11
15
|
400: 'Bad Request',
|
|
@@ -62,6 +66,60 @@ function normalizeError(err, isDev = false) {
|
|
|
62
66
|
const isClientError = status < 500;
|
|
63
67
|
const expose = typeof err.expose === 'boolean' ? err.expose : isClientError;
|
|
64
68
|
|
|
69
|
+
// Body parser / payload limit error mapping (HTTP 413)
|
|
70
|
+
if (status === 413 || err.type === 'entity.too.large' || err.type === 'parameters.too.many') {
|
|
71
|
+
const code = err.code || (err.type === 'parameters.too.many' ? 'PARAMETERS_TOO_MANY' : 'PAYLOAD_TOO_LARGE');
|
|
72
|
+
const details = err.details || (
|
|
73
|
+
(err.limit !== undefined || err.length !== undefined || err.expected !== undefined)
|
|
74
|
+
? { limit: err.limit, length: err.length, expected: err.expected }
|
|
75
|
+
: (err.type ? { type: err.type } : undefined)
|
|
76
|
+
);
|
|
77
|
+
const payloadErr = new PayloadTooLargeError(
|
|
78
|
+
(expose || isDev) ? (err.message || getStatusTitle(413)) : getStatusTitle(413),
|
|
79
|
+
{
|
|
80
|
+
code,
|
|
81
|
+
details,
|
|
82
|
+
cause: err,
|
|
83
|
+
headers: err.headers,
|
|
84
|
+
expose,
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
if (err.stack) payloadErr.stack = err.stack;
|
|
89
|
+
if (err.route) payloadErr.route = err.route;
|
|
90
|
+
if (err.method) payloadErr.method = err.method;
|
|
91
|
+
if (err.source) payloadErr.source = err.source;
|
|
92
|
+
if (err.module) payloadErr.module = err.module;
|
|
93
|
+
if (err.phase) payloadErr.phase = err.phase;
|
|
94
|
+
if (err.requestId) payloadErr.requestId = err.requestId;
|
|
95
|
+
|
|
96
|
+
return payloadErr;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Body parser JSON syntax error (HTTP 400)
|
|
100
|
+
if (err.type === 'entity.parse.failed') {
|
|
101
|
+
const badReqErr = new BadRequestError(
|
|
102
|
+
(expose || isDev) ? (err.message || 'Invalid JSON payload') : 'Invalid JSON payload',
|
|
103
|
+
{
|
|
104
|
+
code: err.code || 'INVALID_JSON_PAYLOAD',
|
|
105
|
+
details: err.details || { type: err.type },
|
|
106
|
+
cause: err,
|
|
107
|
+
headers: err.headers,
|
|
108
|
+
expose: true,
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
if (err.stack) badReqErr.stack = err.stack;
|
|
113
|
+
if (err.route) badReqErr.route = err.route;
|
|
114
|
+
if (err.method) badReqErr.method = err.method;
|
|
115
|
+
if (err.source) badReqErr.source = err.source;
|
|
116
|
+
if (err.module) badReqErr.module = err.module;
|
|
117
|
+
if (err.phase) badReqErr.phase = err.phase;
|
|
118
|
+
if (err.requestId) badReqErr.requestId = err.requestId;
|
|
119
|
+
|
|
120
|
+
return badReqErr;
|
|
121
|
+
}
|
|
122
|
+
|
|
65
123
|
const httpErr = new HttpError(
|
|
66
124
|
status,
|
|
67
125
|
(expose || isDev) ? (err.message || getStatusTitle(status)) : getStatusTitle(status),
|
|
@@ -105,13 +163,41 @@ function normalizeError(err, isDev = false) {
|
|
|
105
163
|
? err.status
|
|
106
164
|
: (typeof err.statusCode === 'number' ? err.statusCode : 500);
|
|
107
165
|
|
|
166
|
+
const isClientError = status < 500;
|
|
167
|
+
const expose = typeof err.expose === 'boolean' ? err.expose : isClientError;
|
|
168
|
+
|
|
169
|
+
if (status === 413 || err.type === 'entity.too.large' || err.type === 'parameters.too.many') {
|
|
170
|
+
const code = err.code || (err.type === 'parameters.too.many' ? 'PARAMETERS_TOO_MANY' : 'PAYLOAD_TOO_LARGE');
|
|
171
|
+
const details = err.details || (
|
|
172
|
+
(err.limit !== undefined || err.length !== undefined || err.expected !== undefined)
|
|
173
|
+
? { limit: err.limit, length: err.length, expected: err.expected }
|
|
174
|
+
: (err.type ? { type: err.type } : undefined)
|
|
175
|
+
);
|
|
176
|
+
const payloadErr = new PayloadTooLargeError(
|
|
177
|
+
(expose || isDev) ? (err.message || getStatusTitle(413)) : getStatusTitle(413),
|
|
178
|
+
{
|
|
179
|
+
code,
|
|
180
|
+
details,
|
|
181
|
+
headers: err.headers,
|
|
182
|
+
expose,
|
|
183
|
+
}
|
|
184
|
+
);
|
|
185
|
+
if (err.route) payloadErr.route = err.route;
|
|
186
|
+
if (err.method) payloadErr.method = err.method;
|
|
187
|
+
if (err.source) payloadErr.source = err.source;
|
|
188
|
+
if (err.module) payloadErr.module = err.module;
|
|
189
|
+
if (err.phase) payloadErr.phase = err.phase;
|
|
190
|
+
if (err.requestId) payloadErr.requestId = err.requestId;
|
|
191
|
+
return payloadErr;
|
|
192
|
+
}
|
|
193
|
+
|
|
108
194
|
const msg = typeof err.message === 'string' ? err.message : getStatusTitle(status);
|
|
109
195
|
|
|
110
196
|
const httpErr = new HttpError(status, (status < 500 || isDev) ? msg : getStatusTitle(status), {
|
|
111
197
|
code: err.code,
|
|
112
198
|
details: err.details,
|
|
113
199
|
headers: err.headers,
|
|
114
|
-
expose
|
|
200
|
+
expose,
|
|
115
201
|
});
|
|
116
202
|
|
|
117
203
|
if (err.route) httpErr.route = err.route;
|
package/core/events/discovery.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const path = require('path');
|
|
9
|
+
const { walkDirectory } = require('../utils/fs');
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Define a typed event listener
|
|
@@ -44,29 +45,6 @@ function filePathToEventName(relPath) {
|
|
|
44
45
|
return parts.join('.');
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
/**
|
|
48
|
-
* Recursively find all .js / .cjs files in a directory
|
|
49
|
-
* @param {string} dir
|
|
50
|
-
* @returns {string[]}
|
|
51
|
-
*/
|
|
52
|
-
function walkDirectory(dir) {
|
|
53
|
-
const files = [];
|
|
54
|
-
if (!fs.existsSync(dir)) return files;
|
|
55
|
-
|
|
56
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
57
|
-
for (const entry of entries) {
|
|
58
|
-
const fullPath = path.join(dir, entry.name);
|
|
59
|
-
if (entry.isDirectory()) {
|
|
60
|
-
files.push(...walkDirectory(fullPath));
|
|
61
|
-
} else if (entry.isFile() && (entry.name.endsWith('.js') || entry.name.endsWith('.cjs'))) {
|
|
62
|
-
if (!entry.name.startsWith('_') && !entry.name.endsWith('.test.js')) {
|
|
63
|
-
files.push(fullPath);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return files;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
48
|
/**
|
|
71
49
|
* Load and register listeners from a directory into an EventDispatcher
|
|
72
50
|
* @param {string} dirPath - Absolute or relative directory path
|
|
@@ -84,7 +62,11 @@ function loadListeners(dirPath, dispatcher, options = {}) {
|
|
|
84
62
|
return [];
|
|
85
63
|
}
|
|
86
64
|
|
|
87
|
-
const filePaths = walkDirectory(resolvedDir
|
|
65
|
+
const filePaths = walkDirectory(resolvedDir, {
|
|
66
|
+
extensions: ['.js', '.cjs'],
|
|
67
|
+
ignorePrefixes: ['_', '.'],
|
|
68
|
+
ignoreTests: true,
|
|
69
|
+
});
|
|
88
70
|
const loaded = [];
|
|
89
71
|
|
|
90
72
|
for (const filePath of filePaths) {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const EventEmitter = require('events');
|
|
9
|
+
const { globToRegExp } = require('../utils/regex');
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Convert a glob/wildcard pattern (e.g. 'order.*', 'order.**', '*') into a RegExp
|
|
@@ -21,13 +22,7 @@ function patternToRegExp(pattern) {
|
|
|
21
22
|
const cached = patternRegexCache.get(pattern);
|
|
22
23
|
if (cached !== undefined) return cached;
|
|
23
24
|
|
|
24
|
-
const
|
|
25
|
-
.replace(/[.+?^${}()|[\]\\]/g, '\\$&') // Escape regex special chars except *
|
|
26
|
-
.replace(/\*\*/g, '___DOUBLE_STAR___')
|
|
27
|
-
.replace(/\*/g, '[^.]+')
|
|
28
|
-
.replace(/___DOUBLE_STAR___/g, '.*');
|
|
29
|
-
|
|
30
|
-
const reg = new RegExp(`^${escaped}$`);
|
|
25
|
+
const reg = globToRegExp(pattern, { separator: '.' });
|
|
31
26
|
if (patternRegexCache.size < 500) {
|
|
32
27
|
patternRegexCache.set(pattern, reg);
|
|
33
28
|
}
|
|
@@ -4,15 +4,7 @@
|
|
|
4
4
|
* @module core/forms/form-builder
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
if (str === null || str === undefined) return '';
|
|
9
|
-
return String(str)
|
|
10
|
-
.replace(/&/g, '&')
|
|
11
|
-
.replace(/</g, '<')
|
|
12
|
-
.replace(/>/g, '>')
|
|
13
|
-
.replace(/"/g, '"')
|
|
14
|
-
.replace(/'/g, ''');
|
|
15
|
-
}
|
|
7
|
+
const { escapeHtml } = require('../utils/html');
|
|
16
8
|
|
|
17
9
|
/**
|
|
18
10
|
* Render a single form field with label, input, and validation error message
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso Health & Readiness Probes Module
|
|
5
|
+
* Kubernetes, Docker & Cloud-ready liveness (/healthz), readiness (/readyz),
|
|
6
|
+
* and diagnostics endpoints with database pings, queue stats, and graceful draining integration.
|
|
7
|
+
*
|
|
8
|
+
* @module core/health
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
DEFAULT_HEALTH_CONFIG,
|
|
13
|
+
normalizeHealthConfig,
|
|
14
|
+
} = require('./normalize');
|
|
15
|
+
|
|
16
|
+
const {
|
|
17
|
+
formatMb,
|
|
18
|
+
createEventLoopMonitor,
|
|
19
|
+
getSystemMetrics,
|
|
20
|
+
probeDatabase,
|
|
21
|
+
probeQueue,
|
|
22
|
+
probeCustomChecks,
|
|
23
|
+
} = require('./probes');
|
|
24
|
+
|
|
25
|
+
const {
|
|
26
|
+
createHealthManager,
|
|
27
|
+
setupHealthMiddleware,
|
|
28
|
+
} = require('./middleware');
|
|
29
|
+
|
|
30
|
+
module.exports = {
|
|
31
|
+
DEFAULT_HEALTH_CONFIG,
|
|
32
|
+
normalizeHealthConfig,
|
|
33
|
+
formatMb,
|
|
34
|
+
createEventLoopMonitor,
|
|
35
|
+
getSystemMetrics,
|
|
36
|
+
probeDatabase,
|
|
37
|
+
probeQueue,
|
|
38
|
+
probeCustomChecks,
|
|
39
|
+
createHealthManager,
|
|
40
|
+
setupHealthMiddleware,
|
|
41
|
+
};
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso Health Check Middleware & Coordinator
|
|
5
|
+
* Exposes /healthz (liveness) and /readyz (readiness) endpoints conforming to cloud & Kubernetes standards.
|
|
6
|
+
*
|
|
7
|
+
* @module core/health/middleware
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const { normalizeHealthConfig } = require('./normalize');
|
|
11
|
+
const {
|
|
12
|
+
createEventLoopMonitor,
|
|
13
|
+
getSystemMetrics,
|
|
14
|
+
probeDatabase,
|
|
15
|
+
probeQueue,
|
|
16
|
+
probeCustomChecks,
|
|
17
|
+
} = require('./probes');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Creates a health manager instance coordinating probes and runtime metrics.
|
|
21
|
+
*
|
|
22
|
+
* @param {Object} config - Normalized health configuration
|
|
23
|
+
* @param {Object} [context={}] - Server context (db, queue, shutdownManager)
|
|
24
|
+
* @returns {Object} HealthManager
|
|
25
|
+
*/
|
|
26
|
+
function createHealthManager(config, context = {}) {
|
|
27
|
+
const eventLoopMonitor = config.eventLoopLag ? createEventLoopMonitor() : null;
|
|
28
|
+
|
|
29
|
+
async function getLiveness() {
|
|
30
|
+
const metrics = getSystemMetrics(eventLoopMonitor);
|
|
31
|
+
let status = 'ok';
|
|
32
|
+
|
|
33
|
+
if (config.eventLoopLag && metrics.eventLoopLag && config.eventLoopThresholdMs) {
|
|
34
|
+
if (metrics.eventLoopLag.meanMs > config.eventLoopThresholdMs) {
|
|
35
|
+
status = 'degraded';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
status,
|
|
41
|
+
timestamp: new Date().toISOString(),
|
|
42
|
+
...metrics,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function getReadiness() {
|
|
47
|
+
const shutdownManager = context.shutdownManager || context.app?.shutdownManager;
|
|
48
|
+
|
|
49
|
+
// 1. Check if server is currently draining connections during shutdown
|
|
50
|
+
if (shutdownManager && (shutdownManager.isShuttingDown || shutdownManager.isClosed)) {
|
|
51
|
+
return {
|
|
52
|
+
ready: false,
|
|
53
|
+
status: 'draining',
|
|
54
|
+
message: 'Server is currently shutting down and draining active connections',
|
|
55
|
+
timestamp: new Date().toISOString(),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let isReady = true;
|
|
60
|
+
const checks = {};
|
|
61
|
+
|
|
62
|
+
// 2. Check Database connectivity
|
|
63
|
+
if (config.db && context.db) {
|
|
64
|
+
const dbResult = await probeDatabase(context.db, config.timeoutMs);
|
|
65
|
+
checks.database = dbResult;
|
|
66
|
+
if (dbResult.status === 'down') {
|
|
67
|
+
isReady = false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 3. Check Queue readiness
|
|
72
|
+
if (config.queue && context.queue) {
|
|
73
|
+
const queueResult = await probeQueue(context.queue);
|
|
74
|
+
checks.queue = queueResult;
|
|
75
|
+
if (queueResult.status === 'down') {
|
|
76
|
+
isReady = false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 4. Run user-defined custom checks
|
|
81
|
+
if (config.checks && Object.keys(config.checks).length > 0) {
|
|
82
|
+
const customResults = await probeCustomChecks(config.checks, context, config.timeoutMs);
|
|
83
|
+
checks.custom = customResults.results;
|
|
84
|
+
if (!customResults.isHealthy) {
|
|
85
|
+
isReady = false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
ready: isReady,
|
|
91
|
+
status: isReady ? 'ready' : 'unhealthy',
|
|
92
|
+
timestamp: new Date().toISOString(),
|
|
93
|
+
checks,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function getStatus() {
|
|
98
|
+
const [liveness, readiness] = await Promise.all([getLiveness(), getReadiness()]);
|
|
99
|
+
return {
|
|
100
|
+
status: readiness.ready ? liveness.status : 'unhealthy',
|
|
101
|
+
liveness,
|
|
102
|
+
readiness,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function destroy() {
|
|
107
|
+
if (eventLoopMonitor) {
|
|
108
|
+
eventLoopMonitor.destroy();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
config,
|
|
114
|
+
getLiveness,
|
|
115
|
+
getReadiness,
|
|
116
|
+
getStatus,
|
|
117
|
+
destroy,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Mounts standard health check endpoints onto the Express app
|
|
123
|
+
*
|
|
124
|
+
* @param {import('express').Application} app
|
|
125
|
+
* @param {Object} [options] - Health check options or createApp options
|
|
126
|
+
* @param {Object} [context={}] - Server context (db, queue, shutdownManager)
|
|
127
|
+
* @returns {Object|null} Active HealthManager or null if disabled
|
|
128
|
+
*/
|
|
129
|
+
function setupHealthMiddleware(app, options = {}, context = {}) {
|
|
130
|
+
const config = normalizeHealthConfig(options);
|
|
131
|
+
if (!config.enabled) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const serverContext = {
|
|
136
|
+
...context,
|
|
137
|
+
app,
|
|
138
|
+
db: context.db || app.db || (app.get ? app.get('db') : null),
|
|
139
|
+
queue: context.queue || app.queue || (app.get ? app.get('queue') : null),
|
|
140
|
+
shutdownManager: context.shutdownManager || app.shutdownManager,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const healthManager = createHealthManager(config, serverContext);
|
|
144
|
+
|
|
145
|
+
// Helper to attach configured headers (Cache-Control: no-store, etc.)
|
|
146
|
+
function applyHealthHeaders(res) {
|
|
147
|
+
if (config.headers && typeof config.headers === 'object') {
|
|
148
|
+
for (const [key, val] of Object.entries(config.headers)) {
|
|
149
|
+
res.setHeader(key, val);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 1. Mount Liveness Endpoint (/healthz)
|
|
155
|
+
if (config.path) {
|
|
156
|
+
app.get(config.path, async (req, res) => {
|
|
157
|
+
try {
|
|
158
|
+
applyHealthHeaders(res);
|
|
159
|
+
const liveness = await healthManager.getLiveness();
|
|
160
|
+
const statusCode = liveness.status === 'ok' ? 200 : 200; // Liveness passes if process responds
|
|
161
|
+
return res.status(statusCode).json(liveness);
|
|
162
|
+
} catch (err) {
|
|
163
|
+
return res.status(500).json({ status: 'error', error: err.message });
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// 2. Mount Readiness Endpoint (/readyz)
|
|
169
|
+
if (config.readyPath && config.readyPath !== config.path) {
|
|
170
|
+
app.get(config.readyPath, async (req, res) => {
|
|
171
|
+
try {
|
|
172
|
+
applyHealthHeaders(res);
|
|
173
|
+
const readiness = await healthManager.getReadiness();
|
|
174
|
+
const statusCode = readiness.ready ? 200 : 503;
|
|
175
|
+
return res.status(statusCode).json(readiness);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
return res.status(503).json({ ready: false, status: 'error', error: err.message });
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 3. Mount Combined Diagnostic Endpoint (/health)
|
|
183
|
+
if (config.combinedPath && config.combinedPath !== config.path && config.combinedPath !== config.readyPath) {
|
|
184
|
+
app.get(config.combinedPath, async (req, res) => {
|
|
185
|
+
try {
|
|
186
|
+
applyHealthHeaders(res);
|
|
187
|
+
const status = await healthManager.getStatus();
|
|
188
|
+
const statusCode = status.readiness.ready ? 200 : 503;
|
|
189
|
+
return res.status(statusCode).json(status);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
return res.status(503).json({ status: 'error', error: err.message });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Decorate app for programmatic inspection
|
|
197
|
+
app.healthManager = healthManager;
|
|
198
|
+
app.getHealthStatus = () => healthManager.getStatus();
|
|
199
|
+
|
|
200
|
+
// Attach cleanup to shutdownManager
|
|
201
|
+
if (serverContext.shutdownManager && typeof serverContext.shutdownManager.onShutdown === 'function') {
|
|
202
|
+
serverContext.shutdownManager.onShutdown(() => {
|
|
203
|
+
healthManager.destroy();
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return healthManager;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = {
|
|
211
|
+
createHealthManager,
|
|
212
|
+
setupHealthMiddleware,
|
|
213
|
+
};
|