webspresso 0.0.90 → 0.0.91

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 (94) hide show
  1. package/README.md +358 -8
  2. package/bin/commands/service.js +193 -0
  3. package/bin/webspresso.js +2 -0
  4. package/core/auth/middleware.js +9 -0
  5. package/core/compression/compressible.js +51 -0
  6. package/core/compression/compression-stream.js +59 -0
  7. package/core/compression/index.js +25 -0
  8. package/core/compression/middleware.js +301 -0
  9. package/core/compression/negotiate.js +153 -0
  10. package/core/errors/base.js +31 -0
  11. package/core/errors/domain.js +193 -0
  12. package/core/errors/http.js +133 -0
  13. package/core/errors/index.js +77 -0
  14. package/core/errors/middleware.js +162 -0
  15. package/core/errors/normalize.js +171 -0
  16. package/core/orm/complexity.js +120 -0
  17. package/core/orm/events.js +64 -14
  18. package/core/orm/index.js +45 -10
  19. package/core/orm/model.js +2 -0
  20. package/core/orm/query-builder.js +30 -4
  21. package/core/orm/repository.js +57 -25
  22. package/core/orm/transaction.js +39 -5
  23. package/core/orm/utils.js +6 -3
  24. package/core/queue/adapters/database.js +284 -0
  25. package/core/queue/adapters/memory.js +155 -0
  26. package/core/queue/adapters/redis.js +336 -0
  27. package/core/queue/index.js +21 -0
  28. package/core/queue/job.js +103 -0
  29. package/core/queue/manager.js +387 -0
  30. package/core/realtime/adapters/redis.js +431 -0
  31. package/core/realtime/adapters/socket-io.js +203 -0
  32. package/core/realtime/adapters/sse.js +210 -0
  33. package/core/realtime/adapters/websocket.js +337 -0
  34. package/core/realtime/auth-manager.js +140 -0
  35. package/core/realtime/core.js +359 -0
  36. package/core/realtime/emitter.js +98 -0
  37. package/core/realtime/errors.js +71 -0
  38. package/core/realtime/index.js +79 -0
  39. package/core/realtime/reconnect-manager.js +118 -0
  40. package/core/realtime/registry.js +243 -0
  41. package/core/shutdown/http-adapter.js +134 -0
  42. package/core/shutdown/index.js +12 -0
  43. package/core/shutdown/shutdown-manager.js +312 -0
  44. package/core/ssr/index.js +13 -0
  45. package/core/ssr/stream.js +174 -0
  46. package/index.d.ts +829 -52
  47. package/index.js +103 -0
  48. package/package.json +22 -2
  49. package/plugins/admin-panel/api.js +37 -4
  50. package/plugins/admin-panel/app.js +57 -17
  51. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +4 -1
  52. package/plugins/admin-panel/client/parts/04-field-renderers.js +102 -58
  53. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +37 -23
  54. package/plugins/admin-panel/client/parts/08-record-list.js +17 -24
  55. package/plugins/admin-panel/client/parts/09-record-form.js +168 -19
  56. package/plugins/admin-panel/core/admin-module.js +2 -2
  57. package/plugins/admin-panel/core/api-extensions.js +149 -0
  58. package/plugins/admin-panel/field-renderers/rich-text.js +3 -2
  59. package/plugins/admin-panel/index.js +90 -17
  60. package/plugins/admin-panel/modules/custom-pages.js +144 -45
  61. package/plugins/admin-panel/modules/menu.js +85 -0
  62. package/plugins/admin-panel/modules/user-management.js +5 -2
  63. package/plugins/admin-panel/vendor/mithril.min.js +1 -0
  64. package/plugins/admin-panel/vendor/quill.min.js +7 -0
  65. package/plugins/admin-panel/vendor/quill.snow.min.css +945 -0
  66. package/plugins/admin-panel/vendor/tailwind.min.js +65 -0
  67. package/plugins/basic-auth/index.js +293 -0
  68. package/plugins/data-exchange/export-xlsx.js +5 -1
  69. package/plugins/data-exchange/import.js +16 -2
  70. package/plugins/data-exchange/index.js +14 -0
  71. package/plugins/email/index.js +14 -1
  72. package/plugins/index.js +29 -0
  73. package/plugins/queue/index.js +108 -0
  74. package/plugins/realtime/index.js +150 -0
  75. package/plugins/recaptcha/index.js +11 -0
  76. package/plugins/redirect/index.js +9 -1
  77. package/plugins/rest-resources/index.js +280 -11
  78. package/plugins/upload/index.js +16 -0
  79. package/src/app-context.js +46 -6
  80. package/src/file-router.js +196 -21
  81. package/src/helpers.js +20 -2
  82. package/src/plugin-manager.js +124 -4
  83. package/src/server.js +304 -44
  84. package/src/services/builtins/auth.js +273 -0
  85. package/src/services/builtins/exchange.js +229 -0
  86. package/src/services/builtins/mail.js +134 -0
  87. package/src/services/builtins/media.js +196 -0
  88. package/src/services/builtins/system.js +221 -0
  89. package/src/services/discovery.js +140 -0
  90. package/src/services/executor.js +292 -0
  91. package/src/services/index.js +70 -0
  92. package/src/services/memoize.js +256 -0
  93. package/src/services/registry.js +288 -0
  94. package/src/services/validator.js +281 -0
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Framework Domain Exceptions
3
+ * Includes Validation, Configuration, Plugin, Router, Security, and Request errors.
4
+ * @module core/errors/domain
5
+ */
6
+
7
+ const { WebspressoError } = require('./base');
8
+ const { HttpError, NotFoundError } = require('./http');
9
+
10
+ /**
11
+ * Validation Error
12
+ * Represents schema or business rule validation failures.
13
+ * Maps to HTTP 422 Unprocessable Entity by default.
14
+ */
15
+ class ValidationError extends HttpError {
16
+ /**
17
+ * @param {string} [message='Validation failed']
18
+ * @param {Object} [options]
19
+ * @param {Record<string, string[]|string>} [options.fields={}] - Field-specific validation error messages
20
+ * @param {number} [options.status=422]
21
+ * @param {string} [options.code='VALIDATION_ERROR']
22
+ * @param {unknown} [options.details]
23
+ * @param {unknown} [options.cause]
24
+ */
25
+ constructor(message = 'Validation failed', options = {}) {
26
+ super(options.status || 422, message, {
27
+ code: options.code || 'VALIDATION_ERROR',
28
+ expose: true,
29
+ ...options,
30
+ });
31
+
32
+ this.fields = options.fields || {};
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Configuration Error
38
+ * Represents framework, server, or application bootstrap misconfiguration.
39
+ */
40
+ class ConfigurationError extends WebspressoError {
41
+ /**
42
+ * @param {string} message
43
+ * @param {Object} [options]
44
+ */
45
+ constructor(message, options = {}) {
46
+ super(message, {
47
+ code: options.code || 'CONFIGURATION_ERROR',
48
+ status: 500,
49
+ ...options,
50
+ });
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Plugin Error
56
+ * Represents plugin lifecycle, registration, or dependency failure.
57
+ */
58
+ class PluginError extends WebspressoError {
59
+ /**
60
+ * @param {string} message
61
+ * @param {Object} [options]
62
+ * @param {string} [options.plugin] - Name of the plugin that threw the error
63
+ */
64
+ constructor(message, options = {}) {
65
+ super(message, {
66
+ code: options.code || 'PLUGIN_ERROR',
67
+ status: 500,
68
+ ...options,
69
+ });
70
+
71
+ this.plugin = options.plugin || null;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Security Error
77
+ * Represents suspicious requests, invalid signatures, CSRF/cookie tampering.
78
+ * Maps to HTTP 400 Bad Request by default, with internal details masked.
79
+ */
80
+ class SecurityError extends HttpError {
81
+ /**
82
+ * @param {string} [message='Security violation']
83
+ * @param {Object} [options]
84
+ */
85
+ constructor(message = 'Security violation', options = {}) {
86
+ super(options.status || 400, message, {
87
+ code: options.code || 'SECURITY_ERROR',
88
+ expose: false,
89
+ ...options,
90
+ });
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Base Request Transport Error
96
+ */
97
+ class RequestError extends HttpError {
98
+ /**
99
+ * @param {number} status
100
+ * @param {string} message
101
+ * @param {Object} [options]
102
+ */
103
+ constructor(status = 400, message = 'Request Error', options = {}) {
104
+ super(status, message, options);
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Request Aborted Error
110
+ * Thrown when client connection is terminated/aborted before response completion.
111
+ */
112
+ class RequestAbortedError extends RequestError {
113
+ /**
114
+ * @param {string} [message='Request aborted by client']
115
+ * @param {Object} [options]
116
+ */
117
+ constructor(message = 'Request aborted by client', options = {}) {
118
+ super(options.status || 499, message, {
119
+ code: options.code || 'REQUEST_ABORTED',
120
+ expose: false,
121
+ ...options,
122
+ });
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Base Router Error
128
+ */
129
+ class RouterError extends WebspressoError {
130
+ /**
131
+ * @param {string} message
132
+ * @param {Object} [options]
133
+ */
134
+ constructor(message, options = {}) {
135
+ super(message, options);
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Route Not Found Error
141
+ * Represents a URL that cannot be resolved to any page or route handler.
142
+ * Maps to HTTP 404.
143
+ */
144
+ class RouteNotFoundError extends NotFoundError {
145
+ /**
146
+ * @param {string} [pathOrMessage='Route not found']
147
+ * @param {Object} [options]
148
+ */
149
+ constructor(pathOrMessage = 'Route not found', options = {}) {
150
+ const msg = typeof pathOrMessage === 'string' && pathOrMessage.startsWith('/')
151
+ ? `Route not found: ${pathOrMessage}`
152
+ : pathOrMessage;
153
+
154
+ super(msg, {
155
+ code: options.code || 'ROUTE_NOT_FOUND',
156
+ ...options,
157
+ });
158
+
159
+ if (typeof pathOrMessage === 'string' && pathOrMessage.startsWith('/')) {
160
+ this.path = pathOrMessage;
161
+ }
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Route Generation Error
167
+ * Developer error when generating a URL for a named route with missing or invalid parameters.
168
+ */
169
+ class RouteGenerationError extends RouterError {
170
+ /**
171
+ * @param {string} message
172
+ * @param {Object} [options]
173
+ */
174
+ constructor(message, options = {}) {
175
+ super(message, {
176
+ code: options.code || 'ROUTE_GENERATION_ERROR',
177
+ status: 500,
178
+ ...options,
179
+ });
180
+ }
181
+ }
182
+
183
+ module.exports = {
184
+ ValidationError,
185
+ ConfigurationError,
186
+ PluginError,
187
+ SecurityError,
188
+ RequestError,
189
+ RequestAbortedError,
190
+ RouterError,
191
+ RouteNotFoundError,
192
+ RouteGenerationError,
193
+ };
@@ -0,0 +1,133 @@
1
+ /**
2
+ * HTTP Exception Hierarchy
3
+ * @module core/errors/http
4
+ */
5
+
6
+ const { WebspressoError } = require('./base');
7
+
8
+ /**
9
+ * Base HTTP Error
10
+ */
11
+ class HttpError extends WebspressoError {
12
+ /**
13
+ * @param {number} status - HTTP status code
14
+ * @param {string} [message] - Error message
15
+ * @param {Object} [options]
16
+ * @param {string} [options.code] - Machine-readable error code
17
+ * @param {unknown} [options.details] - Additional contextual details
18
+ * @param {unknown} [options.cause] - Underlying root cause error
19
+ * @param {Record<string, string>} [options.headers] - Response headers to attach
20
+ * @param {boolean} [options.expose] - Whether to expose message to clients in production
21
+ */
22
+ constructor(status = 500, message = 'Internal Server Error', options = {}) {
23
+ super(message, options);
24
+
25
+ this.status = typeof status === 'number' ? status : 500;
26
+ this.headers = options.headers || {};
27
+ this.expose = options.expose !== undefined ? Boolean(options.expose) : this.status < 500;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * 400 Bad Request
33
+ */
34
+ class BadRequestError extends HttpError {
35
+ constructor(message = 'Bad Request', options = {}) {
36
+ super(400, message, { code: 'BAD_REQUEST', ...options });
37
+ }
38
+ }
39
+
40
+ /**
41
+ * 401 Unauthorized
42
+ */
43
+ class UnauthorizedError extends HttpError {
44
+ constructor(message = 'Unauthorized', options = {}) {
45
+ super(401, message, { code: 'UNAUTHORIZED', ...options });
46
+ }
47
+ }
48
+
49
+ /**
50
+ * 403 Forbidden
51
+ */
52
+ class ForbiddenError extends HttpError {
53
+ constructor(message = 'Forbidden', options = {}) {
54
+ super(403, message, { code: 'FORBIDDEN', ...options });
55
+ }
56
+ }
57
+
58
+ /**
59
+ * 404 Not Found
60
+ */
61
+ class NotFoundError extends HttpError {
62
+ constructor(message = 'Not Found', options = {}) {
63
+ super(404, message, { code: 'NOT_FOUND', ...options });
64
+ }
65
+ }
66
+
67
+ /**
68
+ * 405 Method Not Allowed
69
+ */
70
+ class MethodNotAllowedError extends HttpError {
71
+ constructor(message = 'Method Not Allowed', options = {}) {
72
+ super(405, message, { code: 'METHOD_NOT_ALLOWED', ...options });
73
+ }
74
+ }
75
+
76
+ /**
77
+ * 409 Conflict
78
+ */
79
+ class ConflictError extends HttpError {
80
+ constructor(message = 'Conflict', options = {}) {
81
+ super(409, message, { code: 'CONFLICT', ...options });
82
+ }
83
+ }
84
+
85
+ /**
86
+ * 413 Payload Too Large
87
+ */
88
+ class PayloadTooLargeError extends HttpError {
89
+ constructor(message = 'Payload Too Large', options = {}) {
90
+ super(413, message, { code: 'PAYLOAD_TOO_LARGE', ...options });
91
+ }
92
+ }
93
+
94
+ /**
95
+ * 415 Unsupported Media Type
96
+ */
97
+ class UnsupportedMediaTypeError extends HttpError {
98
+ constructor(message = 'Unsupported Media Type', options = {}) {
99
+ super(415, message, { code: 'UNSUPPORTED_MEDIA_TYPE', ...options });
100
+ }
101
+ }
102
+
103
+ /**
104
+ * 422 Unprocessable Entity
105
+ */
106
+ class UnprocessableEntityError extends HttpError {
107
+ constructor(message = 'Unprocessable Entity', options = {}) {
108
+ super(422, message, { code: 'UNPROCESSABLE_ENTITY', ...options });
109
+ }
110
+ }
111
+
112
+ /**
113
+ * 429 Too Many Requests
114
+ */
115
+ class TooManyRequestsError extends HttpError {
116
+ constructor(message = 'Too Many Requests', options = {}) {
117
+ super(429, message, { code: 'TOO_MANY_REQUESTS', ...options });
118
+ }
119
+ }
120
+
121
+ module.exports = {
122
+ HttpError,
123
+ BadRequestError,
124
+ UnauthorizedError,
125
+ ForbiddenError,
126
+ NotFoundError,
127
+ MethodNotAllowedError,
128
+ ConflictError,
129
+ PayloadTooLargeError,
130
+ UnsupportedMediaTypeError,
131
+ UnprocessableEntityError,
132
+ TooManyRequestsError,
133
+ };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Webspresso Exceptions & Error Handling Module
3
+ * @module core/errors
4
+ */
5
+
6
+ const { WebspressoError } = require('./base');
7
+ const {
8
+ HttpError,
9
+ BadRequestError,
10
+ UnauthorizedError,
11
+ ForbiddenError,
12
+ NotFoundError,
13
+ MethodNotAllowedError,
14
+ ConflictError,
15
+ PayloadTooLargeError,
16
+ UnsupportedMediaTypeError,
17
+ UnprocessableEntityError,
18
+ TooManyRequestsError,
19
+ } = require('./http');
20
+ const {
21
+ ValidationError,
22
+ ConfigurationError,
23
+ PluginError,
24
+ SecurityError,
25
+ RequestError,
26
+ RequestAbortedError,
27
+ RouterError,
28
+ RouteNotFoundError,
29
+ RouteGenerationError,
30
+ } = require('./domain');
31
+ const {
32
+ getStatusTitle,
33
+ normalizeError,
34
+ toErrorResponseObject,
35
+ } = require('./normalize');
36
+ const {
37
+ preferJsonErrorResponse,
38
+ createCentralErrorHandler,
39
+ renderDefaultErrorHtml,
40
+ } = require('./middleware');
41
+
42
+ module.exports = {
43
+ // Base
44
+ WebspressoError,
45
+
46
+ // HTTP Exceptions
47
+ HttpError,
48
+ BadRequestError,
49
+ UnauthorizedError,
50
+ ForbiddenError,
51
+ NotFoundError,
52
+ MethodNotAllowedError,
53
+ ConflictError,
54
+ PayloadTooLargeError,
55
+ UnsupportedMediaTypeError,
56
+ UnprocessableEntityError,
57
+ TooManyRequestsError,
58
+
59
+ // Domain Exceptions
60
+ ValidationError,
61
+ ConfigurationError,
62
+ PluginError,
63
+ SecurityError,
64
+ RequestError,
65
+ RequestAbortedError,
66
+ RouterError,
67
+ RouteNotFoundError,
68
+ RouteGenerationError,
69
+
70
+ // Utilities & Middleware
71
+ getStatusTitle,
72
+ normalizeError,
73
+ toErrorResponseObject,
74
+ preferJsonErrorResponse,
75
+ createCentralErrorHandler,
76
+ renderDefaultErrorHtml,
77
+ };
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Central Error Boundary Middleware
3
+ * @module core/errors/middleware
4
+ */
5
+
6
+ const { normalizeError, toErrorResponseObject } = require('./normalize');
7
+ const { RequestAbortedError } = require('./domain');
8
+
9
+ /**
10
+ * Check if the request prefers a JSON error response
11
+ * @param {import('express').Request} req
12
+ * @returns {boolean}
13
+ */
14
+ function preferJsonErrorResponse(req) {
15
+ if (req.path.startsWith('/api/') || req.path.startsWith('/_admin/api/')) {
16
+ return true;
17
+ }
18
+ const accept = req.headers['accept'] || '';
19
+ if (accept.includes('application/json') && !accept.includes('text/html')) {
20
+ return true;
21
+ }
22
+ return req.xhr || req.headers['x-requested-with'] === 'XMLHttpRequest';
23
+ }
24
+
25
+ /**
26
+ * Default HTML error renderer fallback
27
+ * @param {import('./http').HttpError|Error} err
28
+ * @param {boolean} isDev
29
+ * @returns {string}
30
+ */
31
+ function renderDefaultErrorHtml(err, isDev) {
32
+ const status = err.status || 500;
33
+ const message = (err.expose !== false || isDev) ? (err.message || 'Internal Server Error') : 'Internal Server Error';
34
+ const stackHtml = isDev && err.stack
35
+ ? `<pre style="background:#1e293b;color:#f87171;padding:16px;border-radius:8px;overflow:auto;font-size:13px;line-height:1.5;">${escapeHtml(err.stack)}</pre>`
36
+ : '';
37
+
38
+ return `<!DOCTYPE html>
39
+ <html lang="en">
40
+ <head>
41
+ <meta charset="UTF-8">
42
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
43
+ <title>${status} - ${escapeHtml(message)}</title>
44
+ <style>
45
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f172a; color: #f8fafc; margin: 0; padding: 40px 20px; display: flex; justify-content: center; }
46
+ .card { background: #1e293b; padding: 32px; border-radius: 12px; max-width: 650px; width: 100%; border: 1px solid #334155; }
47
+ h1 { margin-top: 0; color: #ef4444; font-size: 24px; }
48
+ p { color: #cbd5e1; line-height: 1.6; }
49
+ a { color: #38bdf8; text-decoration: none; }
50
+ </style>
51
+ </head>
52
+ <body>
53
+ <div class="card">
54
+ <h1>Error ${status}</h1>
55
+ <p>${escapeHtml(message)}</p>
56
+ ${stackHtml}
57
+ <p><a href="/">← Back to Home</a></p>
58
+ </div>
59
+ </body>
60
+ </html>`;
61
+ }
62
+
63
+ function escapeHtml(str) {
64
+ return String(str || '')
65
+ .replace(/&/g, '&amp;')
66
+ .replace(/</g, '&lt;')
67
+ .replace(/>/g, '&gt;')
68
+ .replace(/"/g, '&quot;')
69
+ .replace(/'/g, '&#39;');
70
+ }
71
+
72
+ /**
73
+ * Create Webspresso Central Error Handler Middleware
74
+ * @param {Object} [options]
75
+ * @param {boolean} [options.isDev=false]
76
+ * @param {Function} [options.getCustomHandler]
77
+ * @param {Function} [options.renderHtmlError]
78
+ * @returns {import('express').ErrorRequestHandler}
79
+ */
80
+ function createCentralErrorHandler(options = {}) {
81
+ const isDev = Boolean(options.isDev);
82
+ const getCustomHandler = typeof options.getCustomHandler === 'function' ? options.getCustomHandler : () => null;
83
+ const renderHtmlError = typeof options.renderHtmlError === 'function' ? options.renderHtmlError : null;
84
+
85
+ return async function centralErrorHandler(err, req, res, next) {
86
+ if (res.headersSent) {
87
+ return next(err);
88
+ }
89
+
90
+ // Handle Client Request Aborted
91
+ if (err instanceof RequestAbortedError || req.aborted || (req.socket && req.socket.destroyed)) {
92
+ // Do not attempt to write to closed socket
93
+ return;
94
+ }
95
+
96
+ // Normalize error
97
+ const normalized = normalizeError(err, isDev);
98
+ const status = normalized.status || 500;
99
+
100
+ // Attach custom headers from HttpError if present
101
+ if (normalized.headers && typeof normalized.headers === 'object') {
102
+ for (const [key, value] of Object.entries(normalized.headers)) {
103
+ res.setHeader(key, value);
104
+ }
105
+ }
106
+
107
+ // Check for custom error handler registered via app.setErrorHandler(...)
108
+ const customHandler = getCustomHandler();
109
+ if (typeof customHandler === 'function') {
110
+ try {
111
+ const handled = await customHandler(err, req, res, next);
112
+ if (res.headersSent || handled === true) {
113
+ return;
114
+ }
115
+ } catch (customErr) {
116
+ // Prevent infinite error loops by logging custom handler error and continuing with fallback
117
+ if (typeof console !== 'undefined' && console.error) {
118
+ console.error('[webspresso] Error in custom error handler:', customErr);
119
+ }
120
+ }
121
+ }
122
+
123
+ // Log errors based on severity
124
+ if (status >= 500) {
125
+ if (typeof console !== 'undefined' && console.error) {
126
+ console.error('[webspresso] Server Error:', err);
127
+ }
128
+ } else if (isDev && status >= 400) {
129
+ if (typeof console !== 'undefined' && console.warn) {
130
+ console.warn(`[webspresso] HTTP ${status}:`, normalized.message);
131
+ }
132
+ }
133
+
134
+ res.status(status);
135
+
136
+ // Format response (JSON vs HTML)
137
+ if (preferJsonErrorResponse(req)) {
138
+ return res.json(toErrorResponseObject(normalized, isDev));
139
+ }
140
+
141
+ if (renderHtmlError) {
142
+ try {
143
+ const html = await renderHtmlError(normalized, req, res);
144
+ if (html && !res.headersSent) {
145
+ return res.send(html);
146
+ }
147
+ } catch (renderErr) {
148
+ if (typeof console !== 'undefined' && console.error) {
149
+ console.error('[webspresso] Error rendering HTML error page:', renderErr);
150
+ }
151
+ }
152
+ }
153
+
154
+ return res.send(renderDefaultErrorHtml(normalized, isDev));
155
+ };
156
+ }
157
+
158
+ module.exports = {
159
+ preferJsonErrorResponse,
160
+ createCentralErrorHandler,
161
+ renderDefaultErrorHtml,
162
+ };