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,59 @@
1
+ /**
2
+ * Compression Stream Factory
3
+ * Instantiates native node:zlib transform streams (brotli, gzip, deflate).
4
+ * @module core/compression/compression-stream
5
+ */
6
+
7
+ const zlib = require('zlib');
8
+ const { supportsBrotli } = require('./negotiate');
9
+
10
+ /**
11
+ * Create a zlib compression transform stream based on encoding
12
+ * @param {'br'|'gzip'|'deflate'} encoding
13
+ * @param {Object} [options]
14
+ * @param {number} [options.level=6] - Default compression level (0-9 for gzip/deflate, 0-11 for brotli)
15
+ * @param {Object} [options.brotli] - Custom options for zlib.createBrotliCompress
16
+ * @param {Object} [options.gzip] - Custom options for zlib.createGzip
17
+ * @param {Object} [options.deflate] - Custom options for zlib.createDeflate
18
+ * @returns {import('stream').Transform|null}
19
+ */
20
+ function createCompressionStream(encoding, options = {}) {
21
+ const enc = (encoding || '').toLowerCase();
22
+ const level = typeof options.level === 'number' ? options.level : 6;
23
+
24
+ switch (enc) {
25
+ case 'br': {
26
+ if (!supportsBrotli()) {
27
+ return null;
28
+ }
29
+ const brotliOpts = { ...(options.brotli || {}) };
30
+ if (!brotliOpts.params) {
31
+ brotliOpts.params = {};
32
+ }
33
+ if (brotliOpts.params[zlib.constants.BROTLI_PARAM_QUALITY] === undefined) {
34
+ // Brotli quality 4 is standard for dynamic HTTP compression (fast & high ratio)
35
+ brotliOpts.params[zlib.constants.BROTLI_PARAM_QUALITY] = typeof options.brotli?.quality === 'number'
36
+ ? options.brotli.quality
37
+ : (options.level !== undefined ? options.level : 4);
38
+ }
39
+ return zlib.createBrotliCompress(brotliOpts);
40
+ }
41
+
42
+ case 'gzip': {
43
+ const gzipOpts = { level, ...(options.gzip || {}) };
44
+ return zlib.createGzip(gzipOpts);
45
+ }
46
+
47
+ case 'deflate': {
48
+ const deflateOpts = { level, ...(options.deflate || {}) };
49
+ return zlib.createDeflate(deflateOpts);
50
+ }
51
+
52
+ default:
53
+ return null;
54
+ }
55
+ }
56
+
57
+ module.exports = {
58
+ createCompressionStream,
59
+ };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Webspresso HTTP Response Compression Module
3
+ * @module core/compression
4
+ */
5
+
6
+ const {
7
+ supportsBrotli,
8
+ getDefaultSupportedEncodings,
9
+ parseAcceptEncoding,
10
+ selectEncoding,
11
+ } = require('./negotiate');
12
+ const { isCompressible } = require('./compressible');
13
+ const { createCompressionStream } = require('./compression-stream');
14
+ const { createCompressionMiddleware, appendVary } = require('./middleware');
15
+
16
+ module.exports = {
17
+ supportsBrotli,
18
+ getDefaultSupportedEncodings,
19
+ parseAcceptEncoding,
20
+ selectEncoding,
21
+ isCompressible,
22
+ createCompressionStream,
23
+ createCompressionMiddleware,
24
+ appendVary,
25
+ };
@@ -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
+ };