webspresso 0.0.96 → 0.0.97

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.
@@ -14,7 +14,6 @@ const fs = require('fs');
14
14
  const path = require('path');
15
15
  const { execSync } = require('child_process');
16
16
  const { loadDbConfig } = require('../utils/db');
17
- const { getSeedFileTemplate } = require('../utils/seed');
18
17
 
19
18
  function registerCommand(program) {
20
19
  program
@@ -136,6 +135,7 @@ function registerCommand(program) {
136
135
  }
137
136
 
138
137
  // Create seed index file
138
+ const { getSeedFileTemplate } = require('../utils/seed');
139
139
  const seedIndex = getSeedFileTemplate();
140
140
 
141
141
  fs.writeFileSync(seedIndexPath, seedIndex);
package/core/auth/hash.js CHANGED
@@ -92,14 +92,19 @@ function generateToken(length = 32) {
92
92
  }
93
93
 
94
94
  /**
95
- * Hash a token for storage (SHA-256)
96
- * Used for remember me tokens - stored hashed, compared hashed
97
- * @param {string} token - Plain token
98
- * @returns {string} Hashed token
95
+ * Hash a high-entropy random lookup token for database storage (SHA-256).
96
+ * Note: Used exclusively for 256-bit cryptographically secure random lookup tokens
97
+ * (remember-me tokens, email verification tokens, password reset tokens), NOT user passwords.
98
+ * User passwords must always be hashed using `hash()` (bcrypt).
99
+ *
100
+ * // CodeQL [js/insufficient-password-hash] SHA-256 is intentionally used for 256-bit high-entropy random lookup token hashing, not user passwords.
101
+ * @param {string} token - High-entropy random token
102
+ * @returns {string} Hex-encoded SHA-256 digest
99
103
  */
100
104
  function hashToken(token) {
101
105
  const crypto = require('crypto');
102
- return crypto.createHash('sha256').update(token).digest('hex');
106
+ // CodeQL [js/insufficient-password-hash] False positive: token is a 256-bit random string, not a user password
107
+ return crypto.createHash('sha256').update(String(token)).digest('hex');
103
108
  }
104
109
 
105
110
  module.exports = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webspresso",
3
- "version": "0.0.96",
3
+ "version": "0.0.97",
4
4
  "description": "Minimal, production-ready SSR framework for Node.js with file-based routing, Nunjucks templating, built-in i18n, and CLI tooling",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -38,10 +38,27 @@
38
38
  },
39
39
  "scripts": {
40
40
  "test": "vitest run",
41
+ "test:unit": "vitest run tests/unit/",
42
+ "test:integration": "vitest run tests/integration/",
41
43
  "test:security": "vitest run tests/security/",
42
- "security": "npm run test:security && npm audit",
44
+ "test:orm": "vitest run tests/**/orm* tests/**/database*",
45
+ "test:auth": "vitest run tests/**/auth* tests/unit/server-auth-branches.test.js",
46
+ "test:admin": "vitest run tests/**/admin*",
47
+ "test:services": "vitest run tests/**/service*",
48
+ "test:plugins": "vitest run tests/**/plugin* tests/unit/*plugin*.test.js",
49
+ "test:core": "vitest run tests/unit/core* tests/unit/server* tests/unit/routing* tests/unit/template*",
50
+ "test:branches": "vitest run tests/unit/*branches.test.js",
51
+ "test:fast": "vitest run tests/unit/*branches.test.js tests/unit/auth.test.js tests/unit/services.test.js tests/unit/server.test.js",
43
52
  "test:watch": "vitest",
44
53
  "test:coverage": "vitest run --coverage",
54
+ "test:coverage:branches": "vitest run tests/unit/*branches.test.js --coverage",
55
+ "test:coverage:unit": "vitest run tests/unit/ --coverage",
56
+ "test:coverage:core": "vitest run tests/unit/core* tests/unit/server* tests/unit/routing* tests/unit/template* --coverage",
57
+ "test:coverage:auth": "vitest run tests/**/auth* tests/unit/server-auth-branches.test.js --coverage",
58
+ "test:coverage:orm": "vitest run tests/**/orm* tests/**/database* --coverage",
59
+ "test:coverage:services": "vitest run tests/**/service* --coverage",
60
+ "test:coverage:plugins": "vitest run tests/**/plugin* tests/unit/*plugin*.test.js --coverage",
61
+ "test:coverage:fast": "vitest run tests/unit/*branches.test.js tests/unit/auth.test.js tests/unit/services.test.js tests/unit/server.test.js --coverage",
45
62
  "test:e2e": "playwright test",
46
63
  "test:e2e:ui": "playwright test --ui",
47
64
  "test:e2e:debug": "playwright test --debug",
@@ -96,7 +113,7 @@
96
113
  "commander": "^11.1.0",
97
114
  "connect-timeout": "^1.9.1",
98
115
  "cookie-parser": "^1.4.7",
99
- "csv-parse": "^5.6.0",
116
+ "csv-parse": "^7.0.2",
100
117
  "dayjs": "^1.11.19",
101
118
  "exceljs": "^4.4.0",
102
119
  "express": "^5.2.1",
@@ -105,11 +122,11 @@
105
122
  "inquirer": "^8.2.6",
106
123
  "knex": "^3.1.0",
107
124
  "mjml": "^5.4.0",
108
- "multer": "^2.1.1",
109
- "nodemailer": "^9.0.3",
125
+ "multer": "^2.4.0",
126
+ "nodemailer": "^10.0.10",
110
127
  "nunjucks": "^3.2.4",
111
- "sanitize-html": "^2.17.3",
112
- "sharp": "^0.35.3",
128
+ "sanitize-html": "^2.17.7",
129
+ "sharp": "^0.35.4",
113
130
  "swup": "^4.8.3",
114
131
  "yaml": "^2.8.3",
115
132
  "zod": "^3.23.0",
@@ -144,11 +161,11 @@
144
161
  }
145
162
  },
146
163
  "devDependencies": {
147
- "@faker-js/faker": "^9.9.0",
164
+ "@faker-js/faker": "^10.6.0",
148
165
  "@playwright/test": "^1.48.0",
149
166
  "@types/express": "^4.17.21",
150
167
  "@types/node": "^20.14.0",
151
- "@vitest/coverage-v8": "^3.0.0",
168
+ "@vitest/coverage-v8": "^4.1.11",
152
169
  "better-sqlite3": "^11.10.0",
153
170
  "chokidar": "^3.5.3",
154
171
  "docboot": "^0.3.0",
@@ -157,16 +174,19 @@
157
174
  "release-it": "^20.2.1",
158
175
  "supertest": "^6.3.4",
159
176
  "typescript": "~5.6.3",
160
- "vitest": "^3.0.0"
177
+ "vitest": "^4.1.11",
178
+ "vitest-doctor": "^0.3.0"
161
179
  },
162
180
  "engines": {
163
- "node": ">=18.0.0"
181
+ "node": ">=20.0.0"
164
182
  },
165
183
  "overrides": {
166
184
  "basic-ftp": "^5.3.0",
167
185
  "tar": "^7.5.11",
168
186
  "undici": "^6.24.0",
169
- "uuid": "^11.1.1"
187
+ "uuid": "^11.1.1",
188
+ "js-yaml": "^5.4.2",
189
+ "qs": "^6.16.0"
170
190
  },
171
191
  "allowScripts": {
172
192
  "better-sqlite3@11.10.0": true
@@ -248,9 +248,10 @@ function wrapCustomPageWithLayout(page, rawComp) {
248
248
  return {
249
249
  oninit: function(vnode) {
250
250
  try {
251
- vnode.state.instance = typeof rawComp === 'function' ? rawComp.call(this, vnode) : rawComp;
252
- if (vnode.state.instance && typeof vnode.state.instance.oninit === 'function') {
253
- return vnode.state.instance.oninit.call(this, vnode);
251
+ const instance = typeof rawComp === 'function' ? rawComp.call(rawComp, vnode) : rawComp;
252
+ vnode.state.instance = instance;
253
+ if (instance && typeof instance.oninit === 'function') {
254
+ return instance.oninit.call(instance, vnode);
254
255
  }
255
256
  } catch (err) {
256
257
  console.error('[Admin Panel] Error in oninit for custom page "' + page.id + '":', err);
@@ -260,7 +261,7 @@ function wrapCustomPageWithLayout(page, rawComp) {
260
261
  oncreate: function(vnode) {
261
262
  try {
262
263
  const comp = vnode.state.instance || rawComp;
263
- if (comp && typeof comp.oncreate === 'function') return comp.oncreate.call(this, vnode);
264
+ if (comp && typeof comp.oncreate === 'function') return comp.oncreate.call(comp, vnode);
264
265
  } catch (err) {
265
266
  console.error('[Admin Panel] Error in oncreate for custom page "' + page.id + '":', err);
266
267
  }
@@ -268,7 +269,7 @@ function wrapCustomPageWithLayout(page, rawComp) {
268
269
  onupdate: function(vnode) {
269
270
  try {
270
271
  const comp = vnode.state.instance || rawComp;
271
- if (comp && typeof comp.onupdate === 'function') return comp.onupdate.call(this, vnode);
272
+ if (comp && typeof comp.onupdate === 'function') return comp.onupdate.call(comp, vnode);
272
273
  } catch (err) {
273
274
  console.error('[Admin Panel] Error in onupdate for custom page "' + page.id + '":', err);
274
275
  }
@@ -276,7 +277,7 @@ function wrapCustomPageWithLayout(page, rawComp) {
276
277
  onbeforeremove: function(vnode) {
277
278
  try {
278
279
  const comp = vnode.state.instance || rawComp;
279
- if (comp && typeof comp.onbeforeremove === 'function') return comp.onbeforeremove.call(this, vnode);
280
+ if (comp && typeof comp.onbeforeremove === 'function') return comp.onbeforeremove.call(comp, vnode);
280
281
  } catch (err) {
281
282
  console.error('[Admin Panel] Error in onbeforeremove for custom page "' + page.id + '":', err);
282
283
  }
@@ -284,7 +285,7 @@ function wrapCustomPageWithLayout(page, rawComp) {
284
285
  onremove: function(vnode) {
285
286
  try {
286
287
  const comp = vnode.state.instance || rawComp;
287
- if (comp && typeof comp.onremove === 'function') return comp.onremove.call(this, vnode);
288
+ if (comp && typeof comp.onremove === 'function') return comp.onremove.call(comp, vnode);
288
289
  } catch (err) {
289
290
  console.error('[Admin Panel] Error in onremove for custom page "' + page.id + '":', err);
290
291
  }
@@ -305,9 +306,9 @@ function wrapCustomPageWithLayout(page, rawComp) {
305
306
  var content;
306
307
  try {
307
308
  if (comp && typeof comp.view === 'function') {
308
- content = comp.view.call(this, vnode);
309
+ content = comp.view.call(comp, vnode);
309
310
  } else if (typeof comp === 'function') {
310
- content = comp.call(this, vnode);
311
+ content = comp.call(comp, vnode);
311
312
  } else {
312
313
  content = comp;
313
314
  }
@@ -24,6 +24,10 @@ let npmVersionCache = {
24
24
  };
25
25
 
26
26
  function fetchLatestNpmVersion(force = false) {
27
+ if ((process.env.NODE_ENV === 'test' || process.env.VITEST) && !force) {
28
+ return Promise.resolve(npmVersionCache.latestVersion || getWebspressoVersion());
29
+ }
30
+
27
31
  const now = Date.now();
28
32
  if (!force && npmVersionCache.latestVersion && (now - npmVersionCache.checkedAt < 30 * 60 * 1000)) {
29
33
  return Promise.resolve(npmVersionCache.latestVersion);
@@ -117,7 +117,9 @@ function sendUnauthorized(req, res, config) {
117
117
 
118
118
  if (config.challenge !== false) {
119
119
  const realm = String(config.realm || 'Restricted Area')
120
- .split(/[\r\n]/)[0]
120
+ .split(/[\r\n\0]/)[0]
121
+ .replace(/[\t]/g, ' ')
122
+ .replace(/\\/g, '\\\\')
121
123
  .replace(/"/g, '\\"');
122
124
  res.setHeader('WWW-Authenticate', `Basic realm="${realm}"`);
123
125
  }
@@ -248,9 +248,14 @@
248
248
  el.textContent = '';
249
249
  return;
250
250
  }
251
+ if (typeof window.DOMPurify !== 'undefined' && typeof window.DOMPurify.sanitize === 'function') {
252
+ el.innerHTML = window.DOMPurify.sanitize(htmlStr);
253
+ return;
254
+ }
251
255
  try {
252
- var parser = new DOMParser();
253
- var doc = parser.parseFromString(htmlStr, 'text/html');
256
+ var template = document.createElement('template');
257
+ template.innerHTML = htmlStr;
258
+ var content = template.content;
254
259
  var allowedTags = {
255
260
  p: 1, br: 1, b: 1, i: 1, strong: 1, em: 1, u: 1, s: 1, strike: 1,
256
261
  h1: 1, h2: 1, h3: 1, h4: 1, h5: 1, h6: 1, ul: 1, ol: 1, li: 1,
@@ -260,14 +265,14 @@
260
265
  var allowedAttrs = {
261
266
  href: 1, title: 1, target: 1, rel: 1, class: 1, id: 1, src: 1, alt: 1, width: 1, height: 1
262
267
  };
263
- var allNodes = doc.body.querySelectorAll('*');
268
+ var allNodes = content.querySelectorAll('*');
264
269
  for (var i = allNodes.length - 1; i >= 0; i--) {
265
270
  var node = allNodes[i];
266
271
  var tag = node.tagName.toLowerCase();
267
272
  if (!allowedTags[tag]) {
268
273
  if (node.parentNode) {
269
274
  node.parentNode.removeChild(node);
270
- } else {
275
+ } else if (typeof node.remove === 'function') {
271
276
  node.remove();
272
277
  }
273
278
  continue;
@@ -275,7 +280,7 @@
275
280
  var attrs = Array.prototype.slice.call(node.attributes);
276
281
  for (var j = 0; j < attrs.length; j++) {
277
282
  var attrName = attrs[j].name.toLowerCase();
278
- var attrVal = attrs[j].value.toLowerCase().replace(/[\x00-\x20\s]+/g, '').trim();
283
+ var attrVal = (attrs[j].value || '').toLowerCase().replace(/[\x00-\x20\s]+/g, '').trim();
279
284
  if (attrName.indexOf('on') === 0 || !allowedAttrs[attrName]) {
280
285
  node.removeAttribute(attrs[j].name);
281
286
  } else if ((attrName === 'href' || attrName === 'src' || attrName === 'action' || attrName === 'formaction' || attrName === 'xlink:href') &&
@@ -287,13 +292,8 @@
287
292
  }
288
293
  }
289
294
 
290
- // Transfer DOM nodes directly to target element
291
- while (el.firstChild) {
292
- el.removeChild(el.firstChild);
293
- }
294
- while (doc.body.firstChild) {
295
- el.appendChild(doc.body.firstChild);
296
- }
295
+ el.textContent = '';
296
+ el.appendChild(content.cloneNode(true));
297
297
  } catch (e) {
298
298
  el.textContent = htmlStr;
299
299
  }
package/plugins/cors.js CHANGED
@@ -101,15 +101,14 @@ function createCorsMiddleware(options = {}) {
101
101
  return next(err);
102
102
  }
103
103
 
104
- if (allowedOrigin) {
104
+ if (allowedOrigin && allowedOrigin !== '*') {
105
105
  res.setHeader('Access-Control-Allow-Origin', allowedOrigin);
106
- if (allowedOrigin !== '*') {
107
- res.setHeader('Vary', 'Origin');
106
+ res.setHeader('Vary', 'Origin');
107
+ if (config.credentials) {
108
+ res.setHeader('Access-Control-Allow-Credentials', 'true');
108
109
  }
109
- }
110
-
111
- if (config.credentials && allowedOrigin && allowedOrigin !== '*') {
112
- res.setHeader('Access-Control-Allow-Credentials', 'true');
110
+ } else if (allowedOrigin === '*' && !config.credentials) {
111
+ res.setHeader('Access-Control-Allow-Origin', '*');
113
112
  }
114
113
 
115
114
  if (exposedHeadersStr) {
@@ -217,7 +217,7 @@ function buildBreadcrumbs(relPath) {
217
217
  const breadcrumbs = [{ name: 'Root', path: '' }];
218
218
  if (!relPath) return breadcrumbs;
219
219
 
220
- const segments = relPath.replace(/\\/g, '/').replace(/^\/+|\/+$/g, '').split('/').filter(Boolean);
220
+ const segments = relPath.replace(/\\/g, '/').split('/').filter(Boolean);
221
221
  let current = '';
222
222
 
223
223
  for (const seg of segments) {
@@ -4,6 +4,7 @@
4
4
  * @module plugins/file-manager/security
5
5
  */
6
6
 
7
+ const sanitizeHtml = require('sanitize-html');
7
8
  const { SecurityError, ValidationError } = require('../../core/errors');
8
9
 
9
10
  /**
@@ -164,6 +165,37 @@ function validateMagicBytes(buffer, ext) {
164
165
  return true;
165
166
  }
166
167
 
168
+ const SVG_SANITIZE_OPTIONS = {
169
+ allowedTags: [
170
+ 'svg', 'g', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon',
171
+ 'ellipse', 'text', 'tspan', 'defs', 'clipPath', 'mask', 'pattern',
172
+ 'image', 'linearGradient', 'radialGradient', 'stop', 'use', 'symbol',
173
+ 'title', 'desc', 'a',
174
+ ],
175
+ allowedAttributes: {
176
+ '*': [
177
+ 'id', 'class', 'style', 'xml:space', 'xmlns', 'xmlns:xlink', 'xmlns:svg',
178
+ 'version', 'viewbox', 'viewBox', 'width', 'height', 'x', 'y', 'dx', 'dy',
179
+ 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'r', 'rx', 'ry', 'd', 'points',
180
+ 'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin',
181
+ 'stroke-miterlimit', 'stroke-dasharray', 'stroke-dashoffset', 'opacity',
182
+ 'fill-opacity', 'stroke-opacity', 'fill-rule', 'clip-rule',
183
+ 'transform', 'font-family', 'font-size', 'font-weight', 'text-anchor',
184
+ 'clip-path', 'mask', 'gradientUnits', 'gradientTransform', 'offset',
185
+ 'stop-color', 'stop-opacity', 'href', 'xlink:href', 'preserveAspectRatio',
186
+ ],
187
+ },
188
+ allowedSchemes: ['http', 'https'],
189
+ allowedSchemesByTag: {
190
+ a: ['http', 'https', 'mailto'],
191
+ image: ['http', 'https', 'data'],
192
+ use: ['#'],
193
+ },
194
+ allowProtocolRelative: false,
195
+ selfClosing: ['rect', 'circle', 'line', 'polyline', 'polygon', 'path', 'stop', 'use', 'image', 'ellipse'],
196
+ disallowedTagsMode: 'discard',
197
+ };
198
+
167
199
  /**
168
200
  * Sanitize SVG content against Stored XSS attacks.
169
201
  * Strips <script>, <foreignObject>, <iframe>, <embed>, <object> and on* event handlers.
@@ -172,29 +204,7 @@ function validateMagicBytes(buffer, ext) {
172
204
  */
173
205
  function sanitizeSvgContent(content) {
174
206
  const str = Buffer.isBuffer(content) ? content.toString('utf8') : String(content);
175
-
176
- // Quick check: if no dangerous keywords, return original buffer directly for maximum speed
177
- if (!/<(script|foreignobject|iframe|embed|object|meta|link|style)/i.test(str) &&
178
- !/\s+on[a-z]+\s*=/i.test(str) &&
179
- !/javascript:/i.test(str) &&
180
- !/data:text\/html/i.test(str)) {
181
- return Buffer.isBuffer(content) ? content : Buffer.from(str, 'utf8');
182
- }
183
-
184
- let sanitized = str
185
- // Remove script tags and content
186
- .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script\s*>/gi, '')
187
- // Remove dangerous containers
188
- .replace(/<foreignObject\b[^<]*(?:(?!<\/foreignObject>)<[^<]*)*<\/foreignObject\s*>/gi, '')
189
- .replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe\s*>/gi, '')
190
- .replace(/<embed\b[^>]*>/gi, '')
191
- .replace(/<object\b[^<]*(?:(?!<\/object>)<[^<]*)*<\/object\s*>/gi, '')
192
- // Remove inline event handlers (onload, onclick, onerror, onmouseover, etc.)
193
- .replace(/\s+on[a-zA-Z]+\s*=\s*(?:"[^"]*"|'[^']*'|[^\s>]+)/gi, '')
194
- // Remove javascript: and data:text/html URIs in href and xlink:href
195
- .replace(/(?:href|xlink:href)\s*=\s*(?:"\s*javascript:[^"]*"|'\s*javascript:[^']*')/gi, 'href=""')
196
- .replace(/(?:href|xlink:href)\s*=\s*(?:"\s*data:text\/html[^"]*"|'\s*data:text\/html[^']*')/gi, 'href=""');
197
-
207
+ const sanitized = sanitizeHtml(str, SVG_SANITIZE_OPTIONS);
198
208
  return Buffer.from(sanitized, 'utf8');
199
209
  }
200
210
 
package/plugins/index.js CHANGED
@@ -3,94 +3,52 @@
3
3
  * Export all built-in plugins
4
4
  */
5
5
 
6
- const sitemapPlugin = require('./sitemap');
7
- const analyticsPlugin = require('./analytics');
8
- const dashboardPlugin = require('./dashboard/index');
9
- const schemaExplorerPlugin = require('./schema-explorer');
10
- const adminPanelPlugin = require('./admin-panel');
11
- const seoCheckerPlugin = require('./seo-checker');
12
- const siteAnalyticsPlugin = require('./site-analytics');
13
- const auditLogPlugin = require('./audit-log');
14
- const recaptchaPlugin = require('./recaptcha');
15
- const swaggerPlugin = require('./swagger');
16
- const healthCheckPlugin = require('./health-check');
17
- const restResourcePlugin = require('./rest-resources');
18
- const ormCacheAdminPlugin = require('./orm-cache-admin');
19
- const { uploadPlugin, createLocalFileProvider } = require('./upload');
20
- /** Register after adminPanelPlugin (same db + adminPath) for session and routes. */
21
- const { dataExchangePlugin } = require('./data-exchange');
22
- const { redirectPlugin } = require('./redirect');
23
- const { rateLimitPlugin } = require('./rate-limit');
24
- const emailPlugin = require('./email');
25
- const contentPlugin = require('./content');
26
- const csrfPlugin = require('./csrf');
27
- const corsPlugin = require('./cors');
28
- const { basicAuthPlugin, createBasicAuthMiddleware } = require('./basic-auth');
29
- const {
30
- realtimePlugin,
31
- realtime,
32
- createRealtime,
33
- websocket,
34
- createWebSocketAdapter,
35
- sse,
36
- createSseAdapter,
37
- socketIo,
38
- createSocketIoAdapter,
39
- redis,
40
- createRedisAdapter,
41
- } = require('./realtime');
42
- const { mcpPlugin, createMcpServer, McpServer } = require('./mcp');
43
- const polarPlugin = require('./polar');
44
- const fileManagerPlugin = require('./file-manager');
45
- const { xlsxPlugin, xlsx } = require('./xlsx');
46
- const { csvPlugin, csv } = require('./csv');
47
-
48
6
  module.exports = {
49
- sitemapPlugin,
50
- analyticsPlugin,
51
- dashboardPlugin,
52
- schemaExplorerPlugin,
53
- adminPanelPlugin,
54
- seoCheckerPlugin,
55
- siteAnalyticsPlugin,
56
- auditLogPlugin,
57
- recaptchaPlugin,
58
- swaggerPlugin,
59
- healthCheckPlugin,
60
- restResourcePlugin,
61
- ormCacheAdminPlugin,
62
- uploadPlugin,
63
- createLocalFileProvider,
64
- dataExchangePlugin,
65
- redirectPlugin,
66
- rateLimitPlugin,
67
- emailPlugin,
68
- contentPlugin,
69
- csrfPlugin,
70
- corsPlugin,
71
- basicAuthPlugin,
72
- createBasicAuthMiddleware,
73
- realtimePlugin,
74
- realtime,
75
- createRealtime,
76
- websocket,
77
- createWebSocketAdapter,
78
- sse,
79
- createSseAdapter,
80
- socketIo,
81
- createSocketIoAdapter,
82
- redis,
83
- createRedisAdapter,
84
- queuePlugin: require('./queue'),
85
- mcpPlugin,
86
- createMcpServer,
87
- McpServer,
88
- polarPlugin,
89
- fileManagerPlugin,
90
- xlsxPlugin,
91
- xlsx,
92
- csvPlugin,
93
- csv,
7
+ get sitemapPlugin() { return require('./sitemap'); },
8
+ get analyticsPlugin() { return require('./analytics'); },
9
+ get dashboardPlugin() { return require('./dashboard/index'); },
10
+ get schemaExplorerPlugin() { return require('./schema-explorer'); },
11
+ get adminPanelPlugin() { return require('./admin-panel'); },
12
+ get seoCheckerPlugin() { return require('./seo-checker'); },
13
+ get siteAnalyticsPlugin() { return require('./site-analytics'); },
14
+ get auditLogPlugin() { return require('./audit-log'); },
15
+ get recaptchaPlugin() { return require('./recaptcha'); },
16
+ get swaggerPlugin() { return require('./swagger'); },
17
+ get healthCheckPlugin() { return require('./health-check'); },
18
+ get restResourcePlugin() { return require('./rest-resources'); },
19
+ get ormCacheAdminPlugin() { return require('./orm-cache-admin'); },
20
+ get uploadPlugin() { return require('./upload').uploadPlugin; },
21
+ get createLocalFileProvider() { return require('./upload').createLocalFileProvider; },
22
+ get dataExchangePlugin() { return require('./data-exchange').dataExchangePlugin; },
23
+ get redirectPlugin() { return require('./redirect').redirectPlugin; },
24
+ get rateLimitPlugin() { return require('./rate-limit').rateLimitPlugin; },
25
+ get emailPlugin() { return require('./email'); },
26
+ get contentPlugin() { return require('./content'); },
27
+ get csrfPlugin() { return require('./csrf'); },
28
+ get corsPlugin() { return require('./cors'); },
29
+ get basicAuthPlugin() { return require('./basic-auth').basicAuthPlugin; },
30
+ get createBasicAuthMiddleware() { return require('./basic-auth').createBasicAuthMiddleware; },
31
+ get realtimePlugin() { return require('./realtime').realtimePlugin; },
32
+ get realtime() { return require('./realtime').realtime; },
33
+ get createRealtime() { return require('./realtime').createRealtime; },
34
+ get websocket() { return require('./realtime').websocket; },
35
+ get createWebSocketAdapter() { return require('./realtime').createWebSocketAdapter; },
36
+ get sse() { return require('./realtime').sse; },
37
+ get createSseAdapter() { return require('./realtime').createSseAdapter; },
38
+ get socketIo() { return require('./realtime').socketIo; },
39
+ get createSocketIoAdapter() { return require('./realtime').createSocketIoAdapter; },
40
+ get redis() { return require('./realtime').redis; },
41
+ get createRedisAdapter() { return require('./realtime').createRedisAdapter; },
42
+ get queuePlugin() { return require('./queue'); },
43
+ get mcpPlugin() { return require('./mcp').mcpPlugin; },
44
+ get createMcpServer() { return require('./mcp').createMcpServer; },
45
+ get McpServer() { return require('./mcp').McpServer; },
46
+ get polarPlugin() { return require('./polar'); },
47
+ get fileManagerPlugin() { return require('./file-manager'); },
48
+ get xlsxPlugin() { return require('./xlsx').xlsxPlugin; },
49
+ get xlsx() { return require('./xlsx').xlsx; },
50
+ get csvPlugin() { return require('./csv').csvPlugin; },
51
+ get csv() { return require('./csv').csv; },
94
52
  };
95
53
 
96
54
 
@@ -18,8 +18,10 @@ const { createJsonRpcError, ERROR_CODES } = require('../server');
18
18
  */
19
19
  function mountSseTransport(options) {
20
20
  const { app, server, path: basePath = '/_mcp', authMiddleware } = options;
21
-
22
- const normalizedBase = basePath.replace(/\/+$/, '');
21
+ let normalizedBase = String(basePath || '/_mcp');
22
+ while (normalizedBase.length > 1 && normalizedBase.endsWith('/')) {
23
+ normalizedBase = normalizedBase.slice(0, -1);
24
+ }
23
25
  const sessions = new Map();
24
26
 
25
27
  const middlewareChain = authMiddleware ? [authMiddleware] : [];
@@ -203,12 +203,12 @@ var AnalyticsPage = {
203
203
  vnode.state.chartLoaded = false;
204
204
  vnode.state._stopPoll = null;
205
205
  vnode.state.chartDataVersion = 0;
206
- this.loadData(vnode);
207
- this.loadChartJs(vnode);
206
+ AnalyticsPage.loadData(vnode);
207
+ AnalyticsPage.loadChartJs(vnode);
208
208
  if (typeof runAdminAutoRefresh === 'function') {
209
209
  vnode.state._stopPoll = runAdminAutoRefresh(function() {
210
- this.loadData(vnode);
211
- }.bind(this));
210
+ AnalyticsPage.loadData(vnode);
211
+ });
212
212
  }
213
213
  },
214
214
 
@@ -270,12 +270,11 @@ var AnalyticsPage = {
270
270
  setDays: function(vnode, d) {
271
271
  vnode.state.days = d;
272
272
  if (chartInstance) { chartInstance.destroy(); chartInstance = null; }
273
- this.loadData(vnode);
273
+ AnalyticsPage.loadData(vnode);
274
274
  },
275
275
 
276
276
  view: function(vnode) {
277
277
  var s = vnode.state;
278
- var self = this;
279
278
 
280
279
  return m(Layout, [
281
280
  m(Breadcrumb, { items: [{ label: 'Analytics', href: '/analytics' }] }),
@@ -294,7 +293,7 @@ var AnalyticsPage = {
294
293
  ? m(RefreshIconButton, {
295
294
  title: 'Refresh analytics',
296
295
  spinning: s.loading,
297
- onclick: function() { self.loadData(vnode); },
296
+ onclick: function() { AnalyticsPage.loadData(vnode); },
298
297
  })
299
298
  : null,
300
299
  // Day filter
@@ -303,7 +302,7 @@ var AnalyticsPage = {
303
302
  class: s.days === d
304
303
  ? 'bg-white text-gray-900 shadow-sm dark:bg-slate-700 dark:text-slate-100'
305
304
  : 'text-gray-500 hover:text-gray-700 dark:text-slate-400 dark:hover:text-slate-200',
306
- onclick: function() { self.setDays(vnode, d); },
305
+ onclick: function() { AnalyticsPage.setDays(vnode, d); },
307
306
  }, 'Last ' + d + ' days');
308
307
  })),
309
308
  ]),
@@ -753,7 +753,9 @@ function detectLocale(req) {
753
753
  }
754
754
  }
755
755
 
756
- const acceptLanguage = req.get('Accept-Language');
756
+ const acceptLanguage = typeof req.get === 'function'
757
+ ? req.get('Accept-Language')
758
+ : (req.headers && (req.headers['accept-language'] || req.headers['Accept-Language']));
757
759
  if (acceptLanguage) {
758
760
  const langPart = acceptLanguage.split(',')[0];
759
761
  const a = normalizeLocaleCandidate(langPart);
@@ -16,7 +16,12 @@ const VALID_EXTENSIONS = new Set(['.js', '.mjs', '.cjs']);
16
16
  * @returns {string}
17
17
  */
18
18
  function toCamelCase(str) {
19
- return str.replace(/[-_]+([a-zA-Z0-9])/g, (_, char) => char.toUpperCase());
19
+ if (!str) return '';
20
+ const parts = str.split(/[-_]+/);
21
+ return parts.map((part, index) => {
22
+ if (!part) return '';
23
+ return index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1);
24
+ }).join('');
20
25
  }
21
26
 
22
27
  /**