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.
- package/.agents/skills/webspresso-admin-and-plugins/SKILL.md +122 -0
- package/.agents/skills/webspresso-framework/SKILL.md +56 -0
- package/.agents/skills/webspresso-orm-and-database/SKILL.md +119 -0
- package/.agents/skills/webspresso-routing-and-api/SKILL.md +114 -0
- package/.agents/skills/webspresso-services/SKILL.md +101 -0
- package/bin/commands/admin-password.js +2 -1
- package/bin/commands/admin-setup.js +1 -1
- package/bin/commands/agents-init.js +51 -11
- package/bin/commands/audit-prune.js +1 -1
- package/bin/commands/favicon-generate.js +1 -1
- package/bin/commands/orm-map.js +5 -4
- package/bin/commands/polar-migrate.js +1 -1
- package/bin/commands/seed.js +1 -1
- package/core/auth/hash.js +10 -5
- package/package.json +32 -12
- package/plugins/admin-panel/app.js +10 -9
- package/plugins/admin-panel/core/api-extensions.js +4 -0
- package/plugins/basic-auth/index.js +3 -1
- package/plugins/content/client/inline-edit.js +12 -12
- package/plugins/cors.js +6 -7
- package/plugins/file-manager/core.js +1 -1
- package/plugins/file-manager/security.js +33 -23
- package/plugins/index.js +45 -87
- package/plugins/mcp/transports/sse.js +4 -2
- package/plugins/site-analytics/admin-component.js +7 -8
- package/src/file-router.js +3 -1
- package/src/services/discovery.js +6 -1
- package/src/services/index.js +6 -13
package/bin/commands/seed.js
CHANGED
|
@@ -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
|
|
97
|
-
*
|
|
98
|
-
*
|
|
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
|
-
|
|
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.
|
|
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
|
-
"
|
|
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": "^
|
|
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.
|
|
109
|
-
"nodemailer": "^
|
|
125
|
+
"multer": "^2.4.0",
|
|
126
|
+
"nodemailer": "^10.0.10",
|
|
110
127
|
"nunjucks": "^3.2.4",
|
|
111
|
-
"sanitize-html": "^2.17.
|
|
112
|
-
"sharp": "^0.35.
|
|
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": "^
|
|
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": "^
|
|
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": "^
|
|
177
|
+
"vitest": "^4.1.11",
|
|
178
|
+
"vitest-doctor": "^0.3.0"
|
|
161
179
|
},
|
|
162
180
|
"engines": {
|
|
163
|
-
"node": ">=
|
|
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
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
309
|
+
content = comp.view.call(comp, vnode);
|
|
309
310
|
} else if (typeof comp === 'function') {
|
|
310
|
-
content = comp.call(
|
|
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
|
|
253
|
-
|
|
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 =
|
|
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
|
-
|
|
291
|
-
|
|
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
|
-
|
|
107
|
-
|
|
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, '/').
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
207
|
-
|
|
206
|
+
AnalyticsPage.loadData(vnode);
|
|
207
|
+
AnalyticsPage.loadChartJs(vnode);
|
|
208
208
|
if (typeof runAdminAutoRefresh === 'function') {
|
|
209
209
|
vnode.state._stopPoll = runAdminAutoRefresh(function() {
|
|
210
|
-
|
|
211
|
-
}
|
|
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
|
-
|
|
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() {
|
|
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() {
|
|
305
|
+
onclick: function() { AnalyticsPage.setDays(vnode, d); },
|
|
307
306
|
}, 'Last ' + d + ' days');
|
|
308
307
|
})),
|
|
309
308
|
]),
|
package/src/file-router.js
CHANGED
|
@@ -753,7 +753,9 @@ function detectLocale(req) {
|
|
|
753
753
|
}
|
|
754
754
|
}
|
|
755
755
|
|
|
756
|
-
const acceptLanguage = req.get
|
|
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
|
-
|
|
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
|
/**
|