webspresso 0.0.89 → 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 (102) hide show
  1. package/README.md +450 -53
  2. package/bin/commands/service.js +193 -0
  3. package/bin/webspresso.js +2 -0
  4. package/core/auth/manager.js +36 -26
  5. package/core/auth/middleware.js +21 -2
  6. package/core/compression/compressible.js +51 -0
  7. package/core/compression/compression-stream.js +59 -0
  8. package/core/compression/index.js +25 -0
  9. package/core/compression/middleware.js +301 -0
  10. package/core/compression/negotiate.js +153 -0
  11. package/core/errors/base.js +31 -0
  12. package/core/errors/domain.js +193 -0
  13. package/core/errors/http.js +133 -0
  14. package/core/errors/index.js +77 -0
  15. package/core/errors/middleware.js +162 -0
  16. package/core/errors/normalize.js +171 -0
  17. package/core/orm/complexity.js +120 -0
  18. package/core/orm/events.js +64 -14
  19. package/core/orm/index.js +45 -10
  20. package/core/orm/model.js +10 -2
  21. package/core/orm/query-builder.js +30 -4
  22. package/core/orm/repository.js +57 -25
  23. package/core/orm/scopes.js +2 -1
  24. package/core/orm/transaction.js +39 -5
  25. package/core/orm/utils.js +6 -3
  26. package/core/queue/adapters/database.js +284 -0
  27. package/core/queue/adapters/memory.js +155 -0
  28. package/core/queue/adapters/redis.js +336 -0
  29. package/core/queue/index.js +21 -0
  30. package/core/queue/job.js +103 -0
  31. package/core/queue/manager.js +387 -0
  32. package/core/realtime/adapters/redis.js +431 -0
  33. package/core/realtime/adapters/socket-io.js +203 -0
  34. package/core/realtime/adapters/sse.js +210 -0
  35. package/core/realtime/adapters/websocket.js +337 -0
  36. package/core/realtime/auth-manager.js +140 -0
  37. package/core/realtime/core.js +359 -0
  38. package/core/realtime/emitter.js +98 -0
  39. package/core/realtime/errors.js +71 -0
  40. package/core/realtime/index.js +79 -0
  41. package/core/realtime/reconnect-manager.js +118 -0
  42. package/core/realtime/registry.js +243 -0
  43. package/core/shutdown/http-adapter.js +134 -0
  44. package/core/shutdown/index.js +12 -0
  45. package/core/shutdown/shutdown-manager.js +312 -0
  46. package/core/ssr/index.js +13 -0
  47. package/core/ssr/stream.js +174 -0
  48. package/index.d.ts +829 -52
  49. package/index.js +103 -0
  50. package/package.json +22 -2
  51. package/plugins/admin-panel/api.js +37 -4
  52. package/plugins/admin-panel/app.js +67 -17
  53. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +4 -1
  54. package/plugins/admin-panel/client/parts/03-pagination-intro.js +25 -18
  55. package/plugins/admin-panel/client/parts/04-field-renderers.js +102 -58
  56. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +37 -23
  57. package/plugins/admin-panel/client/parts/08-record-list.js +27 -34
  58. package/plugins/admin-panel/client/parts/09-record-form.js +168 -19
  59. package/plugins/admin-panel/core/admin-module.js +2 -2
  60. package/plugins/admin-panel/core/api-extensions.js +149 -0
  61. package/plugins/admin-panel/field-renderers/rich-text.js +3 -2
  62. package/plugins/admin-panel/index.js +131 -18
  63. package/plugins/admin-panel/modules/admin-users.js +770 -0
  64. package/plugins/admin-panel/modules/custom-pages.js +144 -45
  65. package/plugins/admin-panel/modules/menu.js +85 -0
  66. package/plugins/admin-panel/modules/user-management.js +5 -2
  67. package/plugins/admin-panel/vendor/mithril.min.js +1 -0
  68. package/plugins/admin-panel/vendor/quill.min.js +7 -0
  69. package/plugins/admin-panel/vendor/quill.snow.min.css +945 -0
  70. package/plugins/admin-panel/vendor/tailwind.min.js +65 -0
  71. package/plugins/basic-auth/index.js +293 -0
  72. package/plugins/content/client/inline-edit.js +41 -17
  73. package/plugins/cors.js +12 -8
  74. package/plugins/csrf/index.js +8 -8
  75. package/plugins/data-exchange/export-xlsx.js +5 -1
  76. package/plugins/data-exchange/import.js +16 -2
  77. package/plugins/data-exchange/index.js +14 -0
  78. package/plugins/email/index.js +14 -1
  79. package/plugins/index.js +29 -0
  80. package/plugins/queue/index.js +108 -0
  81. package/plugins/realtime/index.js +150 -0
  82. package/plugins/recaptcha/index.js +11 -0
  83. package/plugins/redirect/index.js +9 -1
  84. package/plugins/rest-resources/index.js +280 -11
  85. package/plugins/upload/index.js +16 -0
  86. package/src/app-context.js +46 -6
  87. package/src/file-router.js +220 -28
  88. package/src/helpers.js +24 -5
  89. package/src/plugin-manager.js +134 -7
  90. package/src/server.js +470 -44
  91. package/src/services/builtins/auth.js +273 -0
  92. package/src/services/builtins/exchange.js +229 -0
  93. package/src/services/builtins/mail.js +134 -0
  94. package/src/services/builtins/media.js +196 -0
  95. package/src/services/builtins/system.js +221 -0
  96. package/src/services/discovery.js +140 -0
  97. package/src/services/executor.js +292 -0
  98. package/src/services/index.js +70 -0
  99. package/src/services/memoize.js +256 -0
  100. package/src/services/registry.js +288 -0
  101. package/src/services/validator.js +281 -0
  102. package/templates/skills/webspresso-usage/REFERENCE-framework.md +2 -2
@@ -0,0 +1,301 @@
1
+ /**
2
+ * HTTP Compression Middleware
3
+ * Provides threshold-based streaming response compression for Webspresso applications.
4
+ * @module core/compression/middleware
5
+ */
6
+
7
+ const { selectEncoding, getDefaultSupportedEncodings } = require('./negotiate');
8
+ const { isCompressible } = require('./compressible');
9
+ const { createCompressionStream } = require('./compression-stream');
10
+
11
+ /**
12
+ * Safely append a field to the HTTP Vary header
13
+ * @param {import('http').ServerResponse} res
14
+ * @param {string} field
15
+ */
16
+ function appendVary(res, field) {
17
+ const current = res.getHeader('Vary');
18
+ if (!current) {
19
+ res.setHeader('Vary', field);
20
+ return;
21
+ }
22
+
23
+ const currentStr = Array.isArray(current) ? current.join(', ') : String(current);
24
+ const parts = currentStr.split(',').map(s => s.trim().toLowerCase());
25
+
26
+ if (parts.includes('*') || parts.includes(field.toLowerCase())) {
27
+ return;
28
+ }
29
+
30
+ res.setHeader('Vary', `${currentStr}, ${field}`);
31
+ }
32
+
33
+ /**
34
+ * Create Webspresso HTTP Response Compression Middleware
35
+ * @param {Object} [options]
36
+ * @param {number} [options.threshold=1024] - Minimum response size in bytes to apply compression
37
+ * @param {number} [options.level=6] - Default compression level
38
+ * @param {string[]} [options.encodings] - Supported encodings in preference order
39
+ * @param {function(import('http').IncomingMessage, import('http').ServerResponse): boolean} [options.filter] - Custom filter function
40
+ * @param {Object} [options.brotli] - Custom Brotli options
41
+ * @param {Object} [options.gzip] - Custom Gzip options
42
+ * @param {Object} [options.deflate] - Custom Deflate options
43
+ * @returns {import('express').RequestHandler}
44
+ */
45
+ function createCompressionMiddleware(options = {}) {
46
+ const threshold = typeof options.threshold === 'number' && options.threshold >= 0 ? options.threshold : 1024;
47
+ const supportedEncodings = Array.isArray(options.encodings) && options.encodings.length > 0
48
+ ? options.encodings
49
+ : getDefaultSupportedEncodings();
50
+
51
+ const customFilter = typeof options.filter === 'function' ? options.filter : null;
52
+
53
+ return function compressionMiddleware(req, res, next) {
54
+ // Expose opt-out API on response object
55
+ res._noCompression = false;
56
+ res.compress = function(enable = true) {
57
+ res._noCompression = !enable;
58
+ return res;
59
+ };
60
+
61
+ // Skip compression for HEAD requests
62
+ if (req.method === 'HEAD') {
63
+ return next();
64
+ }
65
+
66
+ const acceptEncoding = req.headers['accept-encoding'];
67
+ let selectedEncoding = null;
68
+ let checkedEncoding = false;
69
+
70
+ function getSelectedEncoding() {
71
+ if (!checkedEncoding) {
72
+ checkedEncoding = true;
73
+ selectedEncoding = selectEncoding(acceptEncoding, supportedEncodings);
74
+ }
75
+ return selectedEncoding;
76
+ }
77
+
78
+ // Original methods to wrap
79
+ const origWrite = res.write.bind(res);
80
+ const origEnd = res.end.bind(res);
81
+ const origWriteHead = res.writeHead.bind(res);
82
+
83
+ let compressStream = null;
84
+ let isCompressing = false;
85
+ let headersSent = false;
86
+ let bufferedChunks = [];
87
+ let bufferedLength = 0;
88
+ let decided = false;
89
+
90
+ /**
91
+ * Check if response should be compressed
92
+ * @param {number} [expectedLength]
93
+ * @returns {boolean}
94
+ */
95
+ function shouldCompress(expectedLength) {
96
+ if (res._noCompression || res.locals?.compress === false || res.shouldCompress === false) {
97
+ return false;
98
+ }
99
+
100
+ // Check status codes that must not have a body
101
+ const statusCode = res.statusCode || 200;
102
+ if (statusCode === 204 || statusCode === 304 || statusCode === 205 || statusCode < 200) {
103
+ return false;
104
+ }
105
+
106
+ // If response already has a Content-Encoding, do not double-compress
107
+ if (res.getHeader('Content-Encoding')) {
108
+ return false;
109
+ }
110
+
111
+ // Check Content-Type compressibility
112
+ const contentType = res.getHeader('Content-Type');
113
+ if (!contentType || !isCompressible(contentType)) {
114
+ return false;
115
+ }
116
+
117
+ // Check threshold if length is known
118
+ if (typeof expectedLength === 'number' && expectedLength < threshold) {
119
+ return false;
120
+ }
121
+
122
+ // Custom filter hook
123
+ if (customFilter && !customFilter(req, res)) {
124
+ return false;
125
+ }
126
+
127
+ // Check if client supports any available encoding
128
+ const encoding = getSelectedEncoding();
129
+ if (!encoding || encoding === 'identity') {
130
+ return false;
131
+ }
132
+
133
+ return true;
134
+ }
135
+
136
+ /**
137
+ * Initialize compression stream and headers
138
+ */
139
+ function startCompression() {
140
+ const encoding = getSelectedEncoding();
141
+ compressStream = createCompressionStream(encoding, options);
142
+ if (!compressStream) {
143
+ isCompressing = false;
144
+ return;
145
+ }
146
+
147
+ isCompressing = true;
148
+ appendVary(res, 'Accept-Encoding');
149
+ res.setHeader('Content-Encoding', encoding);
150
+ res.removeHeader('Content-Length');
151
+
152
+ // Pipe compression stream chunks directly into underlying socket/response
153
+ compressStream.on('data', (chunk) => {
154
+ origWrite(chunk);
155
+ });
156
+
157
+ compressStream.on('end', () => {
158
+ origEnd();
159
+ });
160
+
161
+ compressStream.on('error', (err) => {
162
+ origEnd();
163
+ });
164
+ }
165
+
166
+ // Wrap writeHead
167
+ res.writeHead = function(statusCode, ...args) {
168
+ if (res.headersSent) return res;
169
+
170
+ if (typeof args[0] === 'object' && args[0] !== null) {
171
+ for (const [k, v] of Object.entries(args[0])) {
172
+ res.setHeader(k, v);
173
+ }
174
+ } else if (typeof args[1] === 'object' && args[1] !== null) {
175
+ for (const [k, v] of Object.entries(args[1])) {
176
+ res.setHeader(k, v);
177
+ }
178
+ }
179
+
180
+ if (isCompressing) {
181
+ const encoding = getSelectedEncoding();
182
+ if (encoding) {
183
+ res.setHeader('Content-Encoding', encoding);
184
+ }
185
+ res.removeHeader('Content-Length');
186
+ appendVary(res, 'Accept-Encoding');
187
+ }
188
+
189
+ return origWriteHead.call(res, statusCode, ...args);
190
+ };
191
+
192
+ // Wrap write
193
+ res.write = function(chunk, encoding, callback) {
194
+ if (typeof encoding === 'function') {
195
+ callback = encoding;
196
+ encoding = undefined;
197
+ }
198
+
199
+ if (!chunk) {
200
+ return true;
201
+ }
202
+
203
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
204
+
205
+ if (!decided) {
206
+ bufferedChunks.push(buf);
207
+ bufferedLength += buf.length;
208
+
209
+ // If buffered size exceeds threshold, decide to start compression immediately
210
+ if (bufferedLength >= threshold) {
211
+ decided = true;
212
+ if (shouldCompress()) {
213
+ startCompression();
214
+ // Flush buffered chunks through compressor
215
+ for (const b of bufferedChunks) {
216
+ compressStream.write(b);
217
+ }
218
+ bufferedChunks = [];
219
+ } else {
220
+ // Send buffered chunks uncompressed
221
+ for (const b of bufferedChunks) {
222
+ origWrite(b);
223
+ }
224
+ bufferedChunks = [];
225
+ }
226
+ }
227
+ if (typeof callback === 'function') callback();
228
+ return true;
229
+ }
230
+
231
+ if (isCompressing && compressStream) {
232
+ return compressStream.write(buf, callback);
233
+ }
234
+
235
+ return origWrite(buf, callback);
236
+ };
237
+
238
+ // Wrap end
239
+ res.end = function(chunk, encoding, callback) {
240
+ if (typeof chunk === 'function') {
241
+ callback = chunk;
242
+ chunk = null;
243
+ } else if (typeof encoding === 'function') {
244
+ callback = encoding;
245
+ encoding = undefined;
246
+ }
247
+
248
+ if (chunk) {
249
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding);
250
+ if (!decided) {
251
+ bufferedChunks.push(buf);
252
+ bufferedLength += buf.length;
253
+ } else if (isCompressing && compressStream) {
254
+ compressStream.write(buf);
255
+ } else {
256
+ origWrite(buf);
257
+ }
258
+ }
259
+
260
+ if (!decided) {
261
+ decided = true;
262
+ // Check if total length satisfies threshold and compressibility
263
+ if (shouldCompress(bufferedLength)) {
264
+ startCompression();
265
+ for (const b of bufferedChunks) {
266
+ compressStream.write(b);
267
+ }
268
+ bufferedChunks = [];
269
+ } else {
270
+ // Output uncompressed
271
+ if (shouldCompress(Infinity) && bufferedLength < threshold) {
272
+ // Ensure Vary is set if content was compressible but just below threshold
273
+ appendVary(res, 'Accept-Encoding');
274
+ }
275
+ for (const b of bufferedChunks) {
276
+ origWrite(b);
277
+ }
278
+ bufferedChunks = [];
279
+ return origEnd(callback);
280
+ }
281
+ }
282
+
283
+ if (isCompressing && compressStream) {
284
+ if (typeof callback === 'function') {
285
+ compressStream.once('end', callback);
286
+ }
287
+ compressStream.end();
288
+ return res;
289
+ }
290
+
291
+ return origEnd(callback);
292
+ };
293
+
294
+ next();
295
+ };
296
+ }
297
+
298
+ module.exports = {
299
+ createCompressionMiddleware,
300
+ appendVary,
301
+ };
@@ -0,0 +1,153 @@
1
+ /**
2
+ * HTTP Compression Negotiation Utilities
3
+ * Parses Accept-Encoding headers with quality (q-value) support, wildcards, and algorithm selection.
4
+ * @module core/compression/negotiate
5
+ */
6
+
7
+ const zlib = require('zlib');
8
+
9
+ /**
10
+ * Check if Brotli compression is supported by the runtime
11
+ * @returns {boolean}
12
+ */
13
+ function supportsBrotli() {
14
+ return typeof zlib.createBrotliCompress === 'function';
15
+ }
16
+
17
+ /**
18
+ * Available server-side encodings in preference order
19
+ * @param {boolean} [includeBrotli=true]
20
+ * @returns {string[]}
21
+ */
22
+ function getDefaultSupportedEncodings(includeBrotli = true) {
23
+ const encodings = [];
24
+ if (includeBrotli && supportsBrotli()) {
25
+ encodings.push('br');
26
+ }
27
+ encodings.push('gzip', 'deflate');
28
+ return encodings;
29
+ }
30
+
31
+ /**
32
+ * Parse an Accept-Encoding header into structured entries with q-values
33
+ * @param {string|undefined|null} header
34
+ * @returns {Array<{ encoding: string, q: number, index: number }>}
35
+ */
36
+ function parseAcceptEncoding(header) {
37
+ if (!header || typeof header !== 'string') {
38
+ return [];
39
+ }
40
+
41
+ const raw = header.trim();
42
+ if (!raw) return [];
43
+
44
+ const parts = raw.split(',');
45
+ const results = [];
46
+
47
+ for (let i = 0; i < parts.length; i++) {
48
+ const part = parts[i].trim();
49
+ if (!part) continue;
50
+
51
+ const [encPart, ...params] = part.split(';');
52
+ const encoding = encPart.trim().toLowerCase();
53
+ if (!encoding) continue;
54
+
55
+ let q = 1.0;
56
+ for (const param of params) {
57
+ const trimmed = param.trim();
58
+ if (trimmed.startsWith('q=')) {
59
+ const val = parseFloat(trimmed.slice(2));
60
+ if (!isNaN(val)) {
61
+ q = Math.max(0, Math.min(1, val));
62
+ }
63
+ }
64
+ }
65
+
66
+ results.push({ encoding, q, index: i });
67
+ }
68
+
69
+ return results;
70
+ }
71
+
72
+ /**
73
+ * Select the best compression encoding given client header and server supported list
74
+ * @param {string|undefined|null} acceptEncoding - The Accept-Encoding header value
75
+ * @param {string[]} [supportedEncodings] - Server supported encodings in preference order
76
+ * @returns {string|null} The selected encoding ('br', 'gzip', 'deflate', 'identity', or null)
77
+ */
78
+ function selectEncoding(acceptEncoding, supportedEncodings) {
79
+ const supported = Array.isArray(supportedEncodings) && supportedEncodings.length > 0
80
+ ? supportedEncodings.map(e => e.toLowerCase()).filter(e => e !== 'br' || supportsBrotli())
81
+ : getDefaultSupportedEncodings();
82
+
83
+ if (!acceptEncoding) {
84
+ return 'identity';
85
+ }
86
+
87
+ const clientPreferences = parseAcceptEncoding(acceptEncoding);
88
+ if (clientPreferences.length === 0) {
89
+ return 'identity';
90
+ }
91
+
92
+ // Create lookup for explicit preferences
93
+ const prefMap = new Map();
94
+ let wildcardQ = null;
95
+
96
+ for (const item of clientPreferences) {
97
+ if (item.encoding === '*') {
98
+ wildcardQ = item.q;
99
+ } else {
100
+ prefMap.set(item.encoding, item.q);
101
+ }
102
+ }
103
+
104
+ // If identity is explicitly forbidden (q=0) and no other encoding matches, we must handle carefully
105
+ const identityQ = prefMap.has('identity') ? prefMap.get('identity') : (wildcardQ !== null ? wildcardQ : 1.0);
106
+
107
+ // Score candidate encodings
108
+ let bestEncoding = null;
109
+ let bestQ = 0;
110
+ let bestServerPriority = Infinity;
111
+
112
+ for (let priority = 0; priority < supported.length; priority++) {
113
+ const candidate = supported[priority];
114
+ let q = 0;
115
+
116
+ if (prefMap.has(candidate)) {
117
+ q = prefMap.get(candidate);
118
+ } else if (wildcardQ !== null) {
119
+ q = wildcardQ;
120
+ }
121
+
122
+ // Must have positive q-value to be considered
123
+ if (q > 0) {
124
+ if (q > bestQ || (q === bestQ && priority < bestServerPriority)) {
125
+ bestQ = q;
126
+ bestEncoding = candidate;
127
+ bestServerPriority = priority;
128
+ }
129
+ }
130
+ }
131
+
132
+ if (bestEncoding && bestQ > 0) {
133
+ return bestEncoding;
134
+ }
135
+
136
+ // If no compressed encoding was chosen:
137
+ // If identity or wildcard was explicitly specified, check their q values
138
+ if (prefMap.has('identity')) {
139
+ return prefMap.get('identity') > 0 ? 'identity' : null;
140
+ }
141
+ if (wildcardQ !== null) {
142
+ return wildcardQ > 0 ? 'identity' : null;
143
+ }
144
+
145
+ return null;
146
+ }
147
+
148
+ module.exports = {
149
+ supportsBrotli,
150
+ getDefaultSupportedEncodings,
151
+ parseAcceptEncoding,
152
+ selectEncoding,
153
+ };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Base Webspresso Error Class
3
+ * @module core/errors/base
4
+ */
5
+
6
+ class WebspressoError extends Error {
7
+ /**
8
+ * @param {string} message
9
+ * @param {Object} [options]
10
+ * @param {string} [options.code] - Machine-readable error code
11
+ * @param {unknown} [options.details] - Additional contextual details
12
+ * @param {unknown} [options.cause] - Underlying root cause error
13
+ * @param {number} [options.status] - Optional HTTP status code mapping
14
+ */
15
+ constructor(message, options = {}) {
16
+ super(message, {
17
+ cause: options.cause,
18
+ });
19
+
20
+ this.name = this.constructor.name;
21
+ this.code = options.code;
22
+ this.details = options.details;
23
+ if (typeof options.status === 'number') {
24
+ this.status = options.status;
25
+ }
26
+ }
27
+ }
28
+
29
+ module.exports = {
30
+ WebspressoError,
31
+ };
@@ -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
+ };