webspresso 0.0.90 → 0.0.93
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/README.md +98 -2509
- package/bin/commands/admin-password.js +8 -2
- package/bin/commands/api.js +7 -1
- package/bin/commands/favicon-generate.js +6 -4
- package/bin/commands/mcp.js +162 -0
- package/bin/commands/new.js +7 -1
- package/bin/commands/page.js +7 -1
- package/bin/commands/seed.js +7 -1
- package/bin/commands/service.js +199 -0
- package/bin/commands/skill.js +7 -1
- package/bin/webspresso.js +4 -0
- package/core/auth/middleware.js +9 -0
- package/core/compileSchema.js +1 -5
- package/core/compression/compressible.js +63 -0
- package/core/compression/compression-stream.js +59 -0
- package/core/compression/index.js +25 -0
- package/core/compression/middleware.js +310 -0
- package/core/compression/negotiate.js +165 -0
- package/core/errors/base.js +31 -0
- package/core/errors/domain.js +193 -0
- package/core/errors/http.js +133 -0
- package/core/errors/index.js +77 -0
- package/core/errors/middleware.js +162 -0
- package/core/errors/normalize.js +171 -0
- package/core/kernel/app.js +2 -1
- package/core/kernel/index.js +5 -1
- package/core/orm/cache/fingerprint.js +23 -5
- package/core/orm/complexity.js +120 -0
- package/core/orm/eager-loader.js +54 -9
- package/core/orm/events.js +64 -14
- package/core/orm/index.js +83 -12
- package/core/orm/model.js +13 -2
- package/core/orm/query-builder.js +30 -4
- package/core/orm/repository.js +57 -25
- package/core/orm/transaction.js +39 -5
- package/core/orm/utils.js +6 -3
- package/core/queue/adapters/database.js +317 -0
- package/core/queue/adapters/memory.js +176 -0
- package/core/queue/adapters/redis.js +357 -0
- package/core/queue/index.js +21 -0
- package/core/queue/job.js +103 -0
- package/core/queue/manager.js +413 -0
- package/core/realtime/adapters/redis.js +431 -0
- package/core/realtime/adapters/socket-io.js +203 -0
- package/core/realtime/adapters/sse.js +210 -0
- package/core/realtime/adapters/websocket.js +337 -0
- package/core/realtime/auth-manager.js +140 -0
- package/core/realtime/core.js +359 -0
- package/core/realtime/emitter.js +98 -0
- package/core/realtime/errors.js +71 -0
- package/core/realtime/index.js +79 -0
- package/core/realtime/reconnect-manager.js +118 -0
- package/core/realtime/registry.js +243 -0
- package/core/shutdown/http-adapter.js +134 -0
- package/core/shutdown/index.js +12 -0
- package/core/shutdown/shutdown-manager.js +312 -0
- package/core/ssr/index.js +13 -0
- package/core/ssr/stream.js +174 -0
- package/core/validation/index.js +70 -0
- package/index.d.ts +924 -53
- package/index.js +110 -1
- package/package.json +30 -2
- package/plugins/admin-panel/api.js +37 -4
- package/plugins/admin-panel/app.js +57 -17
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +10 -2
- package/plugins/admin-panel/client/parts/04-field-renderers.js +102 -58
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +51 -27
- package/plugins/admin-panel/client/parts/07-model-list.js +9 -4
- package/plugins/admin-panel/client/parts/08-record-list.js +37 -26
- package/plugins/admin-panel/client/parts/09-record-form.js +168 -19
- package/plugins/admin-panel/core/admin-module.js +2 -2
- package/plugins/admin-panel/core/api-extensions.js +149 -0
- package/plugins/admin-panel/field-renderers/rich-text.js +3 -2
- package/plugins/admin-panel/index.js +155 -23
- package/plugins/admin-panel/modules/bulk-actions.js +9 -2
- package/plugins/admin-panel/modules/custom-pages.js +144 -45
- package/plugins/admin-panel/modules/dashboard.js +8 -1
- package/plugins/admin-panel/modules/menu.js +85 -0
- package/plugins/admin-panel/modules/user-management.js +5 -2
- package/plugins/admin-panel/vendor/mithril.min.js +1 -0
- package/plugins/admin-panel/vendor/quill.min.js +7 -0
- package/plugins/admin-panel/vendor/quill.snow.min.css +945 -0
- package/plugins/admin-panel/vendor/tailwind.min.js +65 -0
- package/plugins/basic-auth/index.js +293 -0
- package/plugins/data-exchange/export-xlsx.js +28 -2
- package/plugins/data-exchange/import.js +55 -16
- package/plugins/data-exchange/index.js +19 -0
- package/plugins/data-exchange/parse-table.js +14 -5
- package/plugins/data-exchange/record-selection.js +2 -0
- package/plugins/email/index.js +14 -1
- package/plugins/index.js +33 -0
- package/plugins/mcp/auth.js +104 -0
- package/plugins/mcp/discovery.js +88 -0
- package/plugins/mcp/index.js +208 -0
- package/plugins/mcp/prompts.js +149 -0
- package/plugins/mcp/providers/orm.js +325 -0
- package/plugins/mcp/providers/services.js +190 -0
- package/plugins/mcp/providers/system.js +87 -0
- package/plugins/mcp/server.js +472 -0
- package/plugins/mcp/transports/sse.js +139 -0
- package/plugins/mcp/transports/stdio.js +108 -0
- package/plugins/queue/index.js +108 -0
- package/plugins/realtime/index.js +150 -0
- package/plugins/recaptcha/index.js +11 -0
- package/plugins/redirect/index.js +9 -1
- package/plugins/rest-resources/index.js +280 -11
- package/plugins/upload/index.js +16 -0
- package/src/app-context.js +46 -6
- package/src/file-router.js +366 -130
- package/src/helpers.js +20 -2
- package/src/njk-frontmatter.js +24 -14
- package/src/plugin-manager.js +124 -4
- package/src/server.js +332 -54
- package/src/services/builtins/auth.js +273 -0
- package/src/services/builtins/exchange.js +229 -0
- package/src/services/builtins/mail.js +134 -0
- package/src/services/builtins/media.js +196 -0
- package/src/services/builtins/system.js +221 -0
- package/src/services/discovery.js +141 -0
- package/src/services/executor.js +292 -0
- package/src/services/index.js +70 -0
- package/src/services/memoize.js +256 -0
- package/src/services/registry.js +290 -0
- package/src/services/validator.js +230 -0
- package/templates/skills/webspresso-usage/REFERENCE-framework.md +6 -4
|
@@ -0,0 +1,310 @@
|
|
|
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
|
+
if (bufferedChunks.length === 1) {
|
|
266
|
+
if (typeof callback === 'function') {
|
|
267
|
+
compressStream.once('end', callback);
|
|
268
|
+
}
|
|
269
|
+
const singleBuf = bufferedChunks[0];
|
|
270
|
+
bufferedChunks = [];
|
|
271
|
+
compressStream.end(singleBuf);
|
|
272
|
+
return res;
|
|
273
|
+
}
|
|
274
|
+
for (const b of bufferedChunks) {
|
|
275
|
+
compressStream.write(b);
|
|
276
|
+
}
|
|
277
|
+
bufferedChunks = [];
|
|
278
|
+
} else {
|
|
279
|
+
// Output uncompressed
|
|
280
|
+
if (shouldCompress(Infinity) && bufferedLength < threshold) {
|
|
281
|
+
// Ensure Vary is set if content was compressible but just below threshold
|
|
282
|
+
appendVary(res, 'Accept-Encoding');
|
|
283
|
+
}
|
|
284
|
+
for (const b of bufferedChunks) {
|
|
285
|
+
origWrite(b);
|
|
286
|
+
}
|
|
287
|
+
bufferedChunks = [];
|
|
288
|
+
return origEnd(callback);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (isCompressing && compressStream) {
|
|
293
|
+
if (typeof callback === 'function') {
|
|
294
|
+
compressStream.once('end', callback);
|
|
295
|
+
}
|
|
296
|
+
compressStream.end();
|
|
297
|
+
return res;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return origEnd(callback);
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
next();
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
module.exports = {
|
|
308
|
+
createCompressionMiddleware,
|
|
309
|
+
appendVary,
|
|
310
|
+
};
|
|
@@ -0,0 +1,165 @@
|
|
|
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
|
+
const selectEncodingCache = new Map();
|
|
73
|
+
const MAX_ENCODING_CACHE_SIZE = 128;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Select the best compression encoding given client header and server supported list
|
|
77
|
+
* @param {string|undefined|null} acceptEncoding - The Accept-Encoding header value
|
|
78
|
+
* @param {string[]} [supportedEncodings] - Server supported encodings in preference order
|
|
79
|
+
* @returns {string|null} The selected encoding ('br', 'gzip', 'deflate', 'identity', or null)
|
|
80
|
+
*/
|
|
81
|
+
function selectEncoding(acceptEncoding, supportedEncodings) {
|
|
82
|
+
if (!acceptEncoding) {
|
|
83
|
+
return 'identity';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const supported = Array.isArray(supportedEncodings) && supportedEncodings.length > 0
|
|
87
|
+
? supportedEncodings.map(e => e.toLowerCase()).filter(e => e !== 'br' || supportsBrotli())
|
|
88
|
+
: getDefaultSupportedEncodings();
|
|
89
|
+
|
|
90
|
+
const cacheKey = `${acceptEncoding}|${supported.join(',')}`;
|
|
91
|
+
const cached = selectEncodingCache.get(cacheKey);
|
|
92
|
+
if (cached !== undefined) {
|
|
93
|
+
return cached;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const clientPreferences = parseAcceptEncoding(acceptEncoding);
|
|
97
|
+
if (clientPreferences.length === 0) {
|
|
98
|
+
if (selectEncodingCache.size < MAX_ENCODING_CACHE_SIZE) {
|
|
99
|
+
selectEncodingCache.set(cacheKey, 'identity');
|
|
100
|
+
}
|
|
101
|
+
return 'identity';
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Create lookup for explicit preferences
|
|
105
|
+
const prefMap = new Map();
|
|
106
|
+
let wildcardQ = null;
|
|
107
|
+
|
|
108
|
+
for (const item of clientPreferences) {
|
|
109
|
+
if (item.encoding === '*') {
|
|
110
|
+
wildcardQ = item.q;
|
|
111
|
+
} else {
|
|
112
|
+
prefMap.set(item.encoding, item.q);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// If identity is explicitly forbidden (q=0) and no other encoding matches, we must handle carefully
|
|
117
|
+
const identityQ = prefMap.has('identity') ? prefMap.get('identity') : (wildcardQ !== null ? wildcardQ : 1.0);
|
|
118
|
+
|
|
119
|
+
// Score candidate encodings
|
|
120
|
+
let bestEncoding = null;
|
|
121
|
+
let bestQ = 0;
|
|
122
|
+
let bestServerPriority = Infinity;
|
|
123
|
+
|
|
124
|
+
for (let priority = 0; priority < supported.length; priority++) {
|
|
125
|
+
const candidate = supported[priority];
|
|
126
|
+
let q = 0;
|
|
127
|
+
|
|
128
|
+
if (prefMap.has(candidate)) {
|
|
129
|
+
q = prefMap.get(candidate);
|
|
130
|
+
} else if (wildcardQ !== null) {
|
|
131
|
+
q = wildcardQ;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Must have positive q-value to be considered
|
|
135
|
+
if (q > 0) {
|
|
136
|
+
if (q > bestQ || (q === bestQ && priority < bestServerPriority)) {
|
|
137
|
+
bestQ = q;
|
|
138
|
+
bestEncoding = candidate;
|
|
139
|
+
bestServerPriority = priority;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let result = null;
|
|
145
|
+
if (bestEncoding && bestQ > 0) {
|
|
146
|
+
result = bestEncoding;
|
|
147
|
+
} else if (prefMap.has('identity')) {
|
|
148
|
+
result = prefMap.get('identity') > 0 ? 'identity' : null;
|
|
149
|
+
} else if (wildcardQ !== null) {
|
|
150
|
+
result = wildcardQ > 0 ? 'identity' : null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (selectEncodingCache.size < MAX_ENCODING_CACHE_SIZE) {
|
|
154
|
+
selectEncodingCache.set(cacheKey, result);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return result;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = {
|
|
161
|
+
supportsBrotli,
|
|
162
|
+
getDefaultSupportedEncodings,
|
|
163
|
+
parseAcceptEncoding,
|
|
164
|
+
selectEncoding,
|
|
165
|
+
};
|
|
@@ -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
|
+
};
|