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,193 @@
1
+ /**
2
+ * Webspresso CLI: Service Command
3
+ * Run and test services interactively without starting the HTTP server
4
+ * @module bin/commands/service
5
+ */
6
+
7
+ const path = require('path');
8
+ const inquirer = require('inquirer');
9
+ const { createServiceRegistry } = require('../../src/services');
10
+ const { resolveDbConfigIfExists, createDbInstance } = require('../utils/db');
11
+
12
+ /**
13
+ * Parse input string into an object
14
+ * @param {string} inputStr
15
+ * @returns {*}
16
+ */
17
+ function parseInputPayload(inputStr) {
18
+ if (!inputStr || typeof inputStr !== 'string') {
19
+ return {};
20
+ }
21
+
22
+ const trimmed = inputStr.trim();
23
+ if (!trimmed) return {};
24
+
25
+ // Try JSON first
26
+ if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
27
+ try {
28
+ return JSON.parse(trimmed);
29
+ } catch (e) {
30
+ throw new Error(`Invalid JSON input: ${e.message}`);
31
+ }
32
+ }
33
+
34
+ // Try key=value pairs (e.g. "id=42 name=Alice" or "id=42&name=Alice")
35
+ const result = {};
36
+ const pairs = trimmed.includes('&') ? trimmed.split('&') : trimmed.split(/\s+/);
37
+
38
+ for (const pair of pairs) {
39
+ if (!pair.includes('=')) continue;
40
+ const [rawKey, ...valParts] = pair.split('=');
41
+ const key = rawKey.trim();
42
+ const val = valParts.join('=').trim();
43
+
44
+ // Auto coerce simple types
45
+ if (val === 'true') result[key] = true;
46
+ else if (val === 'false') result[key] = false;
47
+ else if (val === 'null') result[key] = null;
48
+ else if (!Number.isNaN(Number(val)) && val !== '') result[key] = Number(val);
49
+ else result[key] = val;
50
+ }
51
+
52
+ return Object.keys(result).length > 0 ? result : trimmed;
53
+ }
54
+
55
+ function registerCommand(program) {
56
+ program
57
+ .command('service [name]')
58
+ .alias('service:run')
59
+ .alias('service:call')
60
+ .description('Run and test services interactively without starting the HTTP server')
61
+ .option('-i, --input <payload>', 'Input payload as JSON string or key=val pairs')
62
+ .option('-d, --dir <path>', 'Path to services directory (default: ./services)')
63
+ .option('-c, --config <path>', 'Path to database config file (optional)')
64
+ .option('-E, --env <environment>', 'Environment (development, production)', 'development')
65
+ .action(async (nameArg, options) => {
66
+ let db = null;
67
+ try {
68
+ const servicesDir = options.dir
69
+ ? path.resolve(process.cwd(), options.dir)
70
+ : path.resolve(process.cwd(), 'services');
71
+
72
+ const registry = createServiceRegistry({
73
+ servicesDir,
74
+ isDev: true,
75
+ });
76
+
77
+ const serviceNames = registry.list();
78
+
79
+ if (serviceNames.length === 0) {
80
+ console.log(`\n⚠️ No services found in "${path.relative(process.cwd(), servicesDir) || '.'}".`);
81
+ console.log(' Create your first service file in services/user/get.js\n');
82
+ return;
83
+ }
84
+
85
+ let serviceName = nameArg;
86
+
87
+ // Interactive Service Selection if not provided
88
+ if (!serviceName) {
89
+ console.log(`\n⚡ Discovered ${serviceNames.length} service(s) in ${path.relative(process.cwd(), servicesDir) || 'services/'}:\n`);
90
+
91
+ const answer = await inquirer.prompt([
92
+ {
93
+ type: 'list',
94
+ name: 'selectedService',
95
+ message: 'Select a service to execute:',
96
+ choices: serviceNames.map((name) => {
97
+ const def = registry.get(name);
98
+ const hasSchema = !!def?.schema;
99
+ return {
100
+ name: `${name.padEnd(25)} ${hasSchema ? '📋 (has schema)' : ''}`,
101
+ value: name,
102
+ };
103
+ }),
104
+ },
105
+ ]);
106
+
107
+ serviceName = answer.selectedService;
108
+ }
109
+
110
+ if (!registry.has(serviceName)) {
111
+ console.error(`\n❌ Error: Service "${serviceName}" not found.`);
112
+ console.log(` Available services: ${serviceNames.join(', ')}\n`);
113
+ process.exit(1);
114
+ }
115
+
116
+ const serviceDef = registry.get(serviceName);
117
+
118
+ // Input gathering
119
+ let input = {};
120
+ if (options.input) {
121
+ input = parseInputPayload(options.input);
122
+ } else {
123
+ console.log(`\n⚙️ Target: \x1b[36m${serviceName}\x1b[0m (${serviceDef.filePath ? path.relative(process.cwd(), serviceDef.filePath) : 'in-memory'})`);
124
+
125
+ const inputAnswer = await inquirer.prompt([
126
+ {
127
+ type: 'input',
128
+ name: 'rawInput',
129
+ message: 'Enter input (JSON or key=val pairs, leave empty for {}):',
130
+ default: '{}',
131
+ },
132
+ ]);
133
+
134
+ input = parseInputPayload(inputAnswer.rawInput);
135
+ }
136
+
137
+ // Initialize DB if config exists
138
+ const dbConfigResult = resolveDbConfigIfExists(options.config);
139
+ if (dbConfigResult) {
140
+ try {
141
+ db = await createDbInstance(dbConfigResult.config, options.env);
142
+ } catch (dbErr) {
143
+ console.warn(`\n⚠️ Could not connect to database: ${dbErr.message}`);
144
+ }
145
+ }
146
+
147
+ const ctx = {
148
+ db,
149
+ user: { id: 1, role: 'admin', email: 'cli-admin@webspresso.local' },
150
+ logger: console,
151
+ };
152
+
153
+ console.log(`\n🚀 Executing \x1b[36m${serviceName}\x1b[0m with input:`, JSON.stringify(input));
154
+ const startTime = Date.now();
155
+
156
+ try {
157
+ const result = await registry.call(serviceName, input, ctx);
158
+ const duration = Date.now() - startTime;
159
+
160
+ console.log(`\n✅ \x1b[32mExecution succeeded (${duration}ms)\x1b[0m\n`);
161
+ console.log(JSON.stringify(result, null, 2));
162
+ console.log('');
163
+ } catch (execErr) {
164
+ const duration = Date.now() - startTime;
165
+ console.error(`\n❌ \x1b[31mExecution failed (${duration}ms): ${execErr.message}\x1b[0m\n`);
166
+
167
+ if (execErr.fields) {
168
+ console.error('Validation Details:');
169
+ for (const [f, msg] of Object.entries(execErr.fields)) {
170
+ console.error(` - ${f}: ${msg}`);
171
+ }
172
+ console.error('');
173
+ } else if (execErr.stack) {
174
+ console.error(execErr.stack);
175
+ console.error('');
176
+ }
177
+ process.exitCode = 1;
178
+ }
179
+ } catch (err) {
180
+ console.error(`\n❌ CLI Error: ${err.message}\n`);
181
+ process.exitCode = 1;
182
+ } finally {
183
+ if (db && typeof db.destroy === 'function') {
184
+ await db.destroy();
185
+ }
186
+ }
187
+ });
188
+ }
189
+
190
+ module.exports = {
191
+ registerCommand,
192
+ parseInputPayload,
193
+ };
package/bin/webspresso.js CHANGED
@@ -33,6 +33,7 @@ const { registerCommand: registerDoctor } = require('./commands/doctor');
33
33
  const { registerCommand: registerSkill } = require('./commands/skill');
34
34
  const { registerCommand: registerUpgrade } = require('./commands/upgrade');
35
35
  const { registerCommand: registerOrmMap } = require('./commands/orm-map');
36
+ const { registerCommand: registerService } = require('./commands/service');
36
37
 
37
38
  registerNew(program);
38
39
  registerPage(program);
@@ -54,6 +55,7 @@ registerDoctor(program);
54
55
  registerSkill(program);
55
56
  registerUpgrade(program);
56
57
  registerOrmMap(program);
58
+ registerService(program);
57
59
 
58
60
  // Parse arguments
59
61
  program.parse();
@@ -38,7 +38,7 @@ const DEFAULT_CONFIG = {
38
38
  saveUninitialized: false,
39
39
  cookie: {
40
40
  httpOnly: true,
41
- secure: process.env.NODE_ENV === 'production',
41
+ secure: process.env.NODE_ENV === 'production' ? true : 'auto',
42
42
  sameSite: 'lax',
43
43
  maxAge: 24 * 60 * 60 * 1000, // 1 day
44
44
  },
@@ -272,9 +272,11 @@ class AuthManager {
272
272
  throw new AuthenticationError('Invalid user object', 'INVALID_USER');
273
273
  }
274
274
 
275
- // Set session
276
- req.session.userId = user.id;
277
- req.session.loggedInAt = Date.now();
275
+ // Set session if active
276
+ if (req.session) {
277
+ req.session.userId = user.id;
278
+ req.session.loggedInAt = Date.now();
279
+ }
278
280
 
279
281
  // Set req.user
280
282
  req.user = user;
@@ -284,19 +286,21 @@ class AuthManager {
284
286
  await self.createRememberToken(user.id, res);
285
287
  }
286
288
 
287
- // Regenerate session ID for security
288
- return new Promise((resolve, reject) => {
289
- req.session.regenerate((err) => {
290
- if (err) {
291
- reject(new AuthenticationError('Session regeneration failed', 'SESSION_ERROR'));
292
- } else {
293
- // Restore userId after regenerate
294
- req.session.userId = user.id;
295
- req.session.loggedInAt = Date.now();
296
- resolve();
297
- }
289
+ // Regenerate session ID for security if supported
290
+ if (req.session && typeof req.session.regenerate === 'function') {
291
+ return new Promise((resolve, reject) => {
292
+ req.session.regenerate((err) => {
293
+ if (err) {
294
+ reject(new AuthenticationError('Session regeneration failed', 'SESSION_ERROR'));
295
+ } else {
296
+ // Restore userId after regenerate
297
+ req.session.userId = user.id;
298
+ req.session.loggedInAt = Date.now();
299
+ resolve();
300
+ }
301
+ });
298
302
  });
299
- });
303
+ }
300
304
  },
301
305
 
302
306
  /**
@@ -320,21 +324,25 @@ class AuthManager {
320
324
  }
321
325
 
322
326
  // Clear cookie
323
- res.clearCookie(self.config.rememberMe.cookieName);
327
+ if (typeof res?.clearCookie === 'function') {
328
+ res.clearCookie(self.config.rememberMe.cookieName);
329
+ }
324
330
  }
325
331
 
326
332
  // Clear user
327
333
  req.user = null;
328
334
 
329
- // Destroy session
330
- return new Promise((resolve) => {
331
- req.session.destroy((err) => {
332
- if (err) {
333
- console.error('Session destroy error:', err);
334
- }
335
- resolve();
335
+ // Destroy session if supported
336
+ if (req.session && typeof req.session.destroy === 'function') {
337
+ return new Promise((resolve) => {
338
+ req.session.destroy((err) => {
339
+ if (err) {
340
+ console.error('Session destroy error:', err);
341
+ }
342
+ resolve();
343
+ });
336
344
  });
337
- });
345
+ }
338
346
  },
339
347
 
340
348
  /**
@@ -715,7 +723,9 @@ class AuthManager {
715
723
  const payload = {
716
724
  id: user.id,
717
725
  type: 'access',
718
- [this.identifierField]: user[this.identifierField],
726
+ ...(this.identifierField && user[this.identifierField] ? { [this.identifierField]: user[this.identifierField] } : {}),
727
+ ...(user.email && { email: user.email }),
728
+ ...(user.role && { role: user.role }),
719
729
  ...options.payload,
720
730
  };
721
731
  return this.createJwt(payload, options);
@@ -15,11 +15,30 @@ const cookieParser = require('cookie-parser');
15
15
  function createAuthMiddleware(authManager) {
16
16
  const config = authManager.config;
17
17
 
18
+ const sessionConfig = authManager.getSessionConfig();
19
+
20
+ if (!sessionConfig.store && process.env.NODE_ENV === 'production') {
21
+ const warnMsg = '[webspresso:auth] Warning: Default MemoryStore is being used in production. It is not designed for production environments and will leak memory under high load. Please configure a persistent session store (e.g. Knex or Redis).';
22
+ if (typeof process !== 'undefined' && typeof process.emitWarning === 'function') {
23
+ process.emitWarning(warnMsg, 'SecurityWarning');
24
+ } else {
25
+ console.warn(warnMsg);
26
+ }
27
+ }
28
+
18
29
  /**
19
30
  * Session middleware
20
- * Sets up express-session
31
+ * Sets up express-session with secure cookie enforcement
21
32
  */
22
- const sessionMiddleware = session(authManager.getSessionConfig());
33
+ const sessionMiddleware = session({
34
+ ...sessionConfig,
35
+ cookie: {
36
+ httpOnly: true,
37
+ sameSite: 'lax',
38
+ secure: 'auto',
39
+ ...(sessionConfig.cookie || {}),
40
+ },
41
+ });
23
42
 
24
43
  /**
25
44
  * Cookie parser middleware (needed for signed cookies / remember me)
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Content-Type Compressibility Filter
3
+ * Determines if a given MIME type benefits from HTTP compression.
4
+ * @module core/compression/compressible
5
+ */
6
+
7
+ // Regex patterns for MIME types that benefit from compression
8
+ const COMPRESSIBLE_TYPE_REGEX = /^(?:text\/[a-z0-9.+_-]+|application\/(?:javascript|x-javascript|json|ld\+json|manifest\+json|graphql\+json|geo\+json|xml|atom\+xml|rss\+xml|xhtml\+xml|x-yaml|yaml|vnd\.api\+json|schema\+json|hal\+json)|image\/svg\+xml)$/i;
9
+
10
+ // Regex patterns for already-compressed or non-compressible types
11
+ const INCOMPRESSIBLE_TYPE_REGEX = /^(?:image\/(?:jpeg|png|gif|webp|avif|heic|heif)|video\/[a-z0-9.+_-]+|audio\/[a-z0-9.+_-]+|application\/(?:zip|gzip|x-gzip|x-bzip2|x-compress|x-7z-compressed|x-rar-compressed|pdf|wasm|octet-stream))$/i;
12
+
13
+ /**
14
+ * Determine if a Content-Type is compressible
15
+ * @param {string|undefined|null} contentType - Raw Content-Type header (e.g. 'text/html; charset=utf-8')
16
+ * @returns {boolean}
17
+ */
18
+ function isCompressible(contentType) {
19
+ if (!contentType || typeof contentType !== 'string') {
20
+ return false;
21
+ }
22
+
23
+ // Extract base MIME type (strip parameters like ; charset=utf-8)
24
+ const semiIndex = contentType.indexOf(';');
25
+ const mime = (semiIndex !== -1 ? contentType.slice(0, semiIndex) : contentType).trim().toLowerCase();
26
+
27
+ if (!mime) {
28
+ return false;
29
+ }
30
+
31
+ // Fast negative check
32
+ if (INCOMPRESSIBLE_TYPE_REGEX.test(mime)) {
33
+ return false;
34
+ }
35
+
36
+ // Check compressible match
37
+ if (COMPRESSIBLE_TYPE_REGEX.test(mime)) {
38
+ return true;
39
+ }
40
+
41
+ // General text or suffix match (+json, +xml, +yaml)
42
+ if (mime.startsWith('text/') || mime.endsWith('+json') || mime.endsWith('+xml') || mime.endsWith('+yaml')) {
43
+ return true;
44
+ }
45
+
46
+ return false;
47
+ }
48
+
49
+ module.exports = {
50
+ isCompressible,
51
+ };
@@ -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
+ };