webspresso 0.0.95 → 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/ADMIN.md +225 -0
- package/.agents/AGENTS.md +72 -0
- package/.agents/ARCHITECTURE.md +119 -0
- package/.agents/AUTH.md +103 -0
- package/.agents/CLI.md +115 -0
- package/.agents/COMPRESSION.md +59 -0
- package/.agents/CONVENTIONS.md +46 -0
- package/.agents/EMAIL.md +141 -0
- package/.agents/ERRORS.md +201 -0
- package/.agents/MCP.md +103 -0
- package/.agents/ORM.md +230 -0
- package/.agents/PLUGINS.md +419 -0
- package/.agents/QUEUE.md +113 -0
- package/.agents/REALTIME.md +213 -0
- package/.agents/ROUTING.md +292 -0
- package/.agents/SERVICES.md +341 -0
- package/.agents/SHUTDOWN.md +67 -0
- package/.agents/STREAMING.md +73 -0
- package/.agents/WORKFLOW.md +27 -0
- package/.agents/internal/DOCUMENTATION_REVIEW.md +485 -0
- package/.agents/internal/TECHNICAL_REVIEW.md +428 -0
- 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/.cursorrules +14 -0
- package/AGENTS.md +175 -0
- package/CLAUDE.md +54 -0
- package/README.md +18 -14
- package/bin/commands/admin-password.js +2 -1
- package/bin/commands/admin-setup.js +1 -1
- package/bin/commands/agents-init.js +209 -0
- package/bin/commands/api.js +114 -48
- package/bin/commands/audit-prune.js +1 -1
- package/bin/commands/doctor.js +35 -0
- package/bin/commands/favicon-generate.js +1 -1
- package/bin/commands/new.js +13 -2
- package/bin/commands/orm-map.js +5 -4
- package/bin/commands/page.js +75 -89
- package/bin/commands/polar-migrate.js +1 -1
- package/bin/commands/seed.js +1 -1
- package/bin/commands/service.js +77 -0
- package/bin/webspresso.js +2 -2
- package/core/auth/hash.js +10 -5
- package/core/compileSchema.js +1 -1
- package/core/errors/middleware.js +22 -2
- package/core/errors/normalize.js +29 -2
- package/{utils/schemaCache.js → core/schema-cache.js} +2 -3
- package/index.d.ts +306 -0
- package/index.js +35 -1
- package/package.json +36 -14
- package/plugins/admin-panel/app.js +10 -9
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +22 -1
- 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/csrf/index.js +3 -2
- package/plugins/csv/engine.js +447 -0
- package/plugins/csv/index.js +94 -0
- package/plugins/csv/middleware.js +80 -0
- package/plugins/csv/services.js +138 -0
- package/plugins/file-manager/admin-component.js +613 -0
- package/plugins/file-manager/api-handlers.js +261 -0
- package/plugins/file-manager/core.js +601 -0
- package/plugins/file-manager/index.js +139 -0
- package/plugins/file-manager/security.js +217 -0
- package/plugins/index.js +45 -79
- package/plugins/mcp/transports/sse.js +4 -2
- package/plugins/site-analytics/admin-component.js +7 -8
- package/plugins/xlsx/engine.js +397 -0
- package/plugins/xlsx/index.js +91 -0
- package/plugins/xlsx/middleware.js +87 -0
- package/plugins/xlsx/services.js +118 -0
- package/src/api/api-loader.js +233 -0
- package/src/api/define-api.js +35 -0
- package/src/discovery/file-route-parser.js +201 -0
- package/src/discovery/scan-routes.js +265 -0
- package/src/file-router.js +9 -1
- package/src/modules/define-module.js +99 -0
- package/src/modules/module-discovery.js +117 -0
- package/src/pages/define-page.js +36 -0
- package/src/pages/page-loader.js +214 -0
- package/src/routing/mount-discovered-routes.js +78 -0
- package/src/routing/route-table.js +191 -0
- package/src/server.js +112 -15
- package/src/services/builtins/file-manager.js +178 -0
- package/src/services/discovery.js +6 -1
- package/src/services/index.js +6 -11
- package/bin/commands/skill.js +0 -187
- package/templates/skills/webspresso-usage/REFERENCE-framework.md +0 -289
- package/templates/skills/webspresso-usage/REFERENCE-kernel.md +0 -93
- package/templates/skills/webspresso-usage/SKILL.md +0 -48
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso File Manager Plugin
|
|
3
|
+
* Filesystem-first media library, directory browser and asset picker for Admin Panel.
|
|
4
|
+
* @module plugins/file-manager
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { createFileManagerApiHandlers } = require('./api-handlers');
|
|
9
|
+
const { generateFileManagerComponent } = require('./admin-component');
|
|
10
|
+
const { createFileManagerServices } = require('../../src/services/builtins/file-manager');
|
|
11
|
+
const core = require('./core');
|
|
12
|
+
const security = require('./security');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {Object} [options]
|
|
16
|
+
* @param {string} [options.baseDir='./public/uploads'] - Filesystem directory to manage
|
|
17
|
+
* @param {string} [options.publicBasePath='/uploads'] - URL prefix for stored files
|
|
18
|
+
* @param {string} [options.apiPrefix='/files'] - API subpath under Admin API
|
|
19
|
+
* @param {string} [options.pageTitle='Dosya Yöneticisi'] - Title of admin page
|
|
20
|
+
* @param {string} [options.menuLabel='Dosya Yöneticisi'] - Sidebar menu label
|
|
21
|
+
* @param {number} [options.menuOrder=50] - Sidebar menu ordering
|
|
22
|
+
* @param {number} [options.maxUploadBytes=10485760] - Max upload size in bytes (10MB default)
|
|
23
|
+
* @param {number|null} [options.maxTotalStorageBytes] - Storage quota limit in bytes (optional)
|
|
24
|
+
* @param {string[]|null} [options.allowedExtensions] - Whitelist of allowed extensions
|
|
25
|
+
* @param {string[]|null} [options.allowedMimeTypes] - Whitelist of allowed MIME types
|
|
26
|
+
* @param {Record<string, string[]>|null} [options.permissions] - RBAC permissions map
|
|
27
|
+
* @param {Object} [options.security] - Security configurations
|
|
28
|
+
* @param {boolean} [options.security.validateMagicBytes=true] - Validate file signatures
|
|
29
|
+
* @param {boolean} [options.security.sanitizeSvg=true] - Sanitize SVG XSS payloads
|
|
30
|
+
* @param {boolean} [options.includePage=true] - Whether to register admin menu item & page
|
|
31
|
+
*/
|
|
32
|
+
function fileManagerPlugin(options = {}) {
|
|
33
|
+
const {
|
|
34
|
+
baseDir = path.join(process.cwd(), 'public', 'uploads'),
|
|
35
|
+
publicBasePath = '/uploads',
|
|
36
|
+
apiPrefix = '/files',
|
|
37
|
+
pageTitle = 'Dosya Yöneticisi',
|
|
38
|
+
menuLabel = 'Dosya Yöneticisi',
|
|
39
|
+
menuOrder = 50,
|
|
40
|
+
maxUploadBytes = 10 * 1024 * 1024,
|
|
41
|
+
maxTotalStorageBytes = null,
|
|
42
|
+
allowedExtensions = null,
|
|
43
|
+
allowedMimeTypes = null,
|
|
44
|
+
permissions = null,
|
|
45
|
+
security: securityOpts = { validateMagicBytes: true, sanitizeSvg: true },
|
|
46
|
+
includePage = true,
|
|
47
|
+
} = options;
|
|
48
|
+
|
|
49
|
+
const resolvedBaseDir = path.resolve(baseDir);
|
|
50
|
+
|
|
51
|
+
const handlers = createFileManagerApiHandlers({
|
|
52
|
+
baseDir: resolvedBaseDir,
|
|
53
|
+
publicBasePath,
|
|
54
|
+
maxUploadBytes,
|
|
55
|
+
maxTotalStorageBytes,
|
|
56
|
+
allowedExtensions,
|
|
57
|
+
allowedMimeTypes,
|
|
58
|
+
permissions,
|
|
59
|
+
security: securityOpts,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
name: 'file-manager',
|
|
64
|
+
version: '1.0.0',
|
|
65
|
+
description: 'Filesystem-first media library, directory explorer & asset picker',
|
|
66
|
+
dependencies: { 'admin-panel': '*' },
|
|
67
|
+
|
|
68
|
+
api: {
|
|
69
|
+
...core,
|
|
70
|
+
security,
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
register(ctx) {
|
|
74
|
+
// Register built-in services in app service registry if available
|
|
75
|
+
if (ctx.app && ctx.app.serviceRegistry) {
|
|
76
|
+
const services = createFileManagerServices({
|
|
77
|
+
baseDir: resolvedBaseDir,
|
|
78
|
+
publicBasePath,
|
|
79
|
+
});
|
|
80
|
+
for (const [name, def] of Object.entries(services)) {
|
|
81
|
+
if (!ctx.app.serviceRegistry.has(name)) {
|
|
82
|
+
ctx.app.serviceRegistry.register(name, def);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
onRoutesReady(ctx) {
|
|
89
|
+
const adminApi = ctx.usePlugin('admin-panel');
|
|
90
|
+
if (!adminApi) {
|
|
91
|
+
console.warn('[file-manager] admin-panel plugin not found, skipping Admin UI registration');
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
adminApi.registerModule({
|
|
96
|
+
id: 'files',
|
|
97
|
+
pages: includePage
|
|
98
|
+
? [{
|
|
99
|
+
id: 'files',
|
|
100
|
+
title: pageTitle,
|
|
101
|
+
path: '/files',
|
|
102
|
+
icon: 'folder',
|
|
103
|
+
component: generateFileManagerComponent({
|
|
104
|
+
apiPrefix,
|
|
105
|
+
publicBasePath,
|
|
106
|
+
}),
|
|
107
|
+
}]
|
|
108
|
+
: [],
|
|
109
|
+
|
|
110
|
+
menu: includePage
|
|
111
|
+
? [{
|
|
112
|
+
id: 'files',
|
|
113
|
+
label: menuLabel,
|
|
114
|
+
path: '/files',
|
|
115
|
+
icon: 'folder',
|
|
116
|
+
order: menuOrder,
|
|
117
|
+
}]
|
|
118
|
+
: [],
|
|
119
|
+
|
|
120
|
+
api: {
|
|
121
|
+
prefix: apiPrefix,
|
|
122
|
+
routes: [
|
|
123
|
+
{ method: 'get', path: '', handler: handlers.list },
|
|
124
|
+
{ method: 'post', path: '/mkdir', handler: handlers.mkdir },
|
|
125
|
+
{ method: 'post', path: '/rename', handler: handlers.rename },
|
|
126
|
+
{ method: 'post', path: '/move', handler: handlers.move },
|
|
127
|
+
{ method: 'post', path: '/delete', handler: handlers.delete },
|
|
128
|
+
{ method: 'post', path: '/upload', handler: handlers.upload },
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
module.exports = fileManagerPlugin;
|
|
137
|
+
module.exports.fileManagerPlugin = fileManagerPlugin;
|
|
138
|
+
module.exports.core = core;
|
|
139
|
+
module.exports.security = security;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File Manager Security Subsystem — Magic byte verification, dangerous extension blocking & SVG sanitization.
|
|
3
|
+
* Zero external dependencies, ultra-high performance O(1) buffer checks.
|
|
4
|
+
* @module plugins/file-manager/security
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const sanitizeHtml = require('sanitize-html');
|
|
8
|
+
const { SecurityError, ValidationError } = require('../../core/errors');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Universal list of dangerous/executable extensions (lowercase without dot)
|
|
12
|
+
*/
|
|
13
|
+
const DANGEROUS_EXTENSIONS = new Set([
|
|
14
|
+
// Scripts & Executables
|
|
15
|
+
'php', 'phtml', 'php3', 'php4', 'php5', 'php7', 'php8', 'phps', 'phar',
|
|
16
|
+
'exe', 'dll', 'so', 'dylib', 'bin', 'elf',
|
|
17
|
+
'sh', 'bash', 'zsh', 'csh', 'ksh', 'fish', 'bat', 'cmd', 'ps1', 'psm1',
|
|
18
|
+
'vbs', 'vbe', 'wsf', 'wsh', 'scr', 'cpl', 'msc', 'hta',
|
|
19
|
+
'jar', 'war', 'ear', 'class',
|
|
20
|
+
'jsp', 'jspx', 'jsw', 'jsv', 'jspf',
|
|
21
|
+
'asp', 'aspx', 'axd', 'asx', 'ashx', 'asmx',
|
|
22
|
+
'cgi', 'pl', 'pm', 'py', 'pyc', 'pyo', 'rb',
|
|
23
|
+
// System & Config files
|
|
24
|
+
'htaccess', 'htpasswd', 'ini', 'env', 'config', 'conf', 'passwd', 'shadow',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Known magic numbers table for high-speed signature verification.
|
|
29
|
+
* Only validates when extension matches known binary format.
|
|
30
|
+
*/
|
|
31
|
+
const MAGIC_NUMBERS = {
|
|
32
|
+
// JPEG / JPG
|
|
33
|
+
jpg: (buf) => buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff,
|
|
34
|
+
jpeg: (buf) => buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff,
|
|
35
|
+
|
|
36
|
+
// PNG
|
|
37
|
+
png: (buf) =>
|
|
38
|
+
buf.length >= 8 &&
|
|
39
|
+
buf[0] === 0x89 &&
|
|
40
|
+
buf[1] === 0x50 &&
|
|
41
|
+
buf[2] === 0x4e &&
|
|
42
|
+
buf[3] === 0x47 &&
|
|
43
|
+
buf[4] === 0x0d &&
|
|
44
|
+
buf[5] === 0x0a &&
|
|
45
|
+
buf[6] === 0x1a &&
|
|
46
|
+
buf[7] === 0x0a,
|
|
47
|
+
|
|
48
|
+
// GIF
|
|
49
|
+
gif: (buf) =>
|
|
50
|
+
buf.length >= 6 &&
|
|
51
|
+
buf[0] === 0x47 &&
|
|
52
|
+
buf[1] === 0x49 &&
|
|
53
|
+
buf[2] === 0x46 &&
|
|
54
|
+
buf[3] === 0x38 &&
|
|
55
|
+
(buf[4] === 0x37 || buf[4] === 0x39) &&
|
|
56
|
+
buf[5] === 0x61,
|
|
57
|
+
|
|
58
|
+
// WebP: RIFF at 0..3 and WEBP at 8..11
|
|
59
|
+
webp: (buf) =>
|
|
60
|
+
buf.length >= 12 &&
|
|
61
|
+
buf[0] === 0x52 &&
|
|
62
|
+
buf[1] === 0x49 &&
|
|
63
|
+
buf[2] === 0x46 &&
|
|
64
|
+
buf[3] === 0x46 &&
|
|
65
|
+
buf[8] === 0x57 &&
|
|
66
|
+
buf[9] === 0x45 &&
|
|
67
|
+
buf[10] === 0x42 &&
|
|
68
|
+
buf[11] === 0x50,
|
|
69
|
+
|
|
70
|
+
// PDF: %PDF-
|
|
71
|
+
pdf: (buf) =>
|
|
72
|
+
buf.length >= 5 &&
|
|
73
|
+
buf[0] === 0x25 &&
|
|
74
|
+
buf[1] === 0x50 &&
|
|
75
|
+
buf[2] === 0x44 &&
|
|
76
|
+
buf[3] === 0x46 &&
|
|
77
|
+
buf[4] === 0x2d,
|
|
78
|
+
|
|
79
|
+
// ZIP (and DOCX/XLSX/PPTX)
|
|
80
|
+
zip: (buf) =>
|
|
81
|
+
buf.length >= 4 &&
|
|
82
|
+
buf[0] === 0x50 &&
|
|
83
|
+
buf[1] === 0x4b &&
|
|
84
|
+
(buf[2] === 0x03 || buf[2] === 0x05 || buf[2] === 0x07) &&
|
|
85
|
+
(buf[3] === 0x04 || buf[3] === 0x06 || buf[3] === 0x08),
|
|
86
|
+
docx: (buf) => MAGIC_NUMBERS.zip(buf),
|
|
87
|
+
xlsx: (buf) => MAGIC_NUMBERS.zip(buf),
|
|
88
|
+
pptx: (buf) => MAGIC_NUMBERS.zip(buf),
|
|
89
|
+
|
|
90
|
+
// MP3: ID3 or frame sync 0xFFE / 0xFFF
|
|
91
|
+
mp3: (buf) =>
|
|
92
|
+
(buf.length >= 3 && buf[0] === 0x49 && buf[1] === 0x44 && buf[2] === 0x33) ||
|
|
93
|
+
(buf.length >= 2 && buf[0] === 0xff && (buf[1] & 0xe0) === 0xe0),
|
|
94
|
+
|
|
95
|
+
// MP4: ....ftyp at bytes 4..7
|
|
96
|
+
mp4: (buf) =>
|
|
97
|
+
buf.length >= 8 &&
|
|
98
|
+
buf[4] === 0x66 &&
|
|
99
|
+
buf[5] === 0x74 &&
|
|
100
|
+
buf[6] === 0x79 &&
|
|
101
|
+
buf[7] === 0x70,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Validate that a filename contains no dangerous extensions or double-extension tricks.
|
|
106
|
+
* @param {string} filename
|
|
107
|
+
* @returns {string} Clean lowercase extension
|
|
108
|
+
*/
|
|
109
|
+
function validateSafeExtension(filename) {
|
|
110
|
+
if (!filename || typeof filename !== 'string') {
|
|
111
|
+
throw new ValidationError('Filename is required');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const cleanName = filename.toLowerCase().trim();
|
|
115
|
+
const parts = cleanName.split('.').filter(Boolean);
|
|
116
|
+
|
|
117
|
+
if (parts.length === 0) {
|
|
118
|
+
throw new ValidationError('Filename must have a valid extension');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Check all dotted segments against dangerous extensions to block double extensions (e.g. payload.php.jpg)
|
|
122
|
+
for (let i = 1; i < parts.length; i++) {
|
|
123
|
+
const segment = parts[i];
|
|
124
|
+
if (DANGEROUS_EXTENSIONS.has(segment)) {
|
|
125
|
+
throw new SecurityError(
|
|
126
|
+
`Dangerous extension or double-extension detected: .${segment}`,
|
|
127
|
+
400
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const finalExt = parts[parts.length - 1];
|
|
133
|
+
if (DANGEROUS_EXTENSIONS.has(finalExt)) {
|
|
134
|
+
throw new SecurityError(`File type .${finalExt} is forbidden for upload`, 403);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return finalExt;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Verify that the buffer's magic bytes match the claimed extension.
|
|
142
|
+
* O(1) time complexity, minimal memory overhead.
|
|
143
|
+
* @param {Buffer} buffer
|
|
144
|
+
* @param {string} ext
|
|
145
|
+
* @returns {boolean}
|
|
146
|
+
*/
|
|
147
|
+
function validateMagicBytes(buffer, ext) {
|
|
148
|
+
if (!buffer || !Buffer.isBuffer(buffer)) {
|
|
149
|
+
throw new ValidationError('Valid buffer is required for magic byte check');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const cleanExt = (ext || '').replace(/^\./, '').toLowerCase();
|
|
153
|
+
const validator = MAGIC_NUMBERS[cleanExt];
|
|
154
|
+
|
|
155
|
+
if (validator) {
|
|
156
|
+
const isValid = validator(buffer);
|
|
157
|
+
if (!isValid) {
|
|
158
|
+
throw new SecurityError(
|
|
159
|
+
`File signature mismatch: file content does not match expected .${cleanExt} format`,
|
|
160
|
+
400
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return true;
|
|
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
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Sanitize SVG content against Stored XSS attacks.
|
|
201
|
+
* Strips <script>, <foreignObject>, <iframe>, <embed>, <object> and on* event handlers.
|
|
202
|
+
* @param {Buffer|string} content
|
|
203
|
+
* @returns {Buffer} Sanitized SVG buffer
|
|
204
|
+
*/
|
|
205
|
+
function sanitizeSvgContent(content) {
|
|
206
|
+
const str = Buffer.isBuffer(content) ? content.toString('utf8') : String(content);
|
|
207
|
+
const sanitized = sanitizeHtml(str, SVG_SANITIZE_OPTIONS);
|
|
208
|
+
return Buffer.from(sanitized, 'utf8');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
module.exports = {
|
|
212
|
+
DANGEROUS_EXTENSIONS,
|
|
213
|
+
MAGIC_NUMBERS,
|
|
214
|
+
validateSafeExtension,
|
|
215
|
+
validateMagicBytes,
|
|
216
|
+
sanitizeSvgContent,
|
|
217
|
+
};
|
package/plugins/index.js
CHANGED
|
@@ -3,86 +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
|
-
|
|
45
6
|
module.exports = {
|
|
46
|
-
sitemapPlugin,
|
|
47
|
-
analyticsPlugin,
|
|
48
|
-
dashboardPlugin,
|
|
49
|
-
schemaExplorerPlugin,
|
|
50
|
-
adminPanelPlugin,
|
|
51
|
-
seoCheckerPlugin,
|
|
52
|
-
siteAnalyticsPlugin,
|
|
53
|
-
auditLogPlugin,
|
|
54
|
-
recaptchaPlugin,
|
|
55
|
-
swaggerPlugin,
|
|
56
|
-
healthCheckPlugin,
|
|
57
|
-
restResourcePlugin,
|
|
58
|
-
ormCacheAdminPlugin,
|
|
59
|
-
uploadPlugin,
|
|
60
|
-
createLocalFileProvider,
|
|
61
|
-
dataExchangePlugin,
|
|
62
|
-
redirectPlugin,
|
|
63
|
-
rateLimitPlugin,
|
|
64
|
-
emailPlugin,
|
|
65
|
-
contentPlugin,
|
|
66
|
-
csrfPlugin,
|
|
67
|
-
corsPlugin,
|
|
68
|
-
basicAuthPlugin,
|
|
69
|
-
createBasicAuthMiddleware,
|
|
70
|
-
realtimePlugin,
|
|
71
|
-
realtime,
|
|
72
|
-
createRealtime,
|
|
73
|
-
websocket,
|
|
74
|
-
createWebSocketAdapter,
|
|
75
|
-
sse,
|
|
76
|
-
createSseAdapter,
|
|
77
|
-
socketIo,
|
|
78
|
-
createSocketIoAdapter,
|
|
79
|
-
redis,
|
|
80
|
-
createRedisAdapter,
|
|
81
|
-
queuePlugin
|
|
82
|
-
mcpPlugin,
|
|
83
|
-
createMcpServer,
|
|
84
|
-
McpServer,
|
|
85
|
-
polarPlugin,
|
|
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; },
|
|
86
52
|
};
|
|
87
53
|
|
|
88
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
|
]),
|