webspresso 0.0.94 → 0.0.96

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 (83) hide show
  1. package/.agents/ADMIN.md +225 -0
  2. package/.agents/AGENTS.md +72 -0
  3. package/.agents/ARCHITECTURE.md +119 -0
  4. package/.agents/AUTH.md +103 -0
  5. package/.agents/CLI.md +115 -0
  6. package/.agents/COMPRESSION.md +59 -0
  7. package/.agents/CONVENTIONS.md +46 -0
  8. package/.agents/EMAIL.md +141 -0
  9. package/.agents/ERRORS.md +201 -0
  10. package/.agents/MCP.md +103 -0
  11. package/.agents/ORM.md +230 -0
  12. package/.agents/PLUGINS.md +419 -0
  13. package/.agents/QUEUE.md +113 -0
  14. package/.agents/REALTIME.md +213 -0
  15. package/.agents/ROUTING.md +292 -0
  16. package/.agents/SERVICES.md +341 -0
  17. package/.agents/SHUTDOWN.md +67 -0
  18. package/.agents/STREAMING.md +73 -0
  19. package/.agents/WORKFLOW.md +27 -0
  20. package/.agents/internal/DOCUMENTATION_REVIEW.md +485 -0
  21. package/.agents/internal/TECHNICAL_REVIEW.md +428 -0
  22. package/.cursorrules +14 -0
  23. package/AGENTS.md +175 -0
  24. package/CLAUDE.md +54 -0
  25. package/README.md +18 -14
  26. package/bin/commands/agents-init.js +169 -0
  27. package/bin/commands/api.js +114 -48
  28. package/bin/commands/doctor.js +35 -0
  29. package/bin/commands/new.js +13 -2
  30. package/bin/commands/page.js +75 -89
  31. package/bin/commands/service.js +77 -0
  32. package/bin/webspresso.js +2 -2
  33. package/core/compileSchema.js +1 -1
  34. package/core/errors/middleware.js +22 -2
  35. package/core/errors/normalize.js +29 -2
  36. package/{utils/schemaCache.js → core/schema-cache.js} +2 -3
  37. package/index.d.ts +306 -0
  38. package/index.js +35 -1
  39. package/package.json +19 -6
  40. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +22 -1
  41. package/plugins/csrf/index.js +3 -2
  42. package/plugins/csv/engine.js +447 -0
  43. package/plugins/csv/index.js +94 -0
  44. package/plugins/csv/middleware.js +80 -0
  45. package/plugins/csv/services.js +138 -0
  46. package/plugins/file-manager/admin-component.js +613 -0
  47. package/plugins/file-manager/api-handlers.js +261 -0
  48. package/plugins/file-manager/core.js +601 -0
  49. package/plugins/file-manager/index.js +139 -0
  50. package/plugins/file-manager/security.js +207 -0
  51. package/plugins/index.js +8 -0
  52. package/plugins/polar/CHANGELOG.md +18 -0
  53. package/plugins/polar/README.md +30 -7
  54. package/plugins/polar/index.js +6 -1
  55. package/plugins/polar/src/billing-hooks.js +51 -0
  56. package/plugins/polar/src/config.js +2 -0
  57. package/plugins/polar/src/rate-limit.js +96 -28
  58. package/plugins/polar/src/routes.js +3 -2
  59. package/plugins/polar/src/urls.js +26 -2
  60. package/plugins/polar/src/user-resolver.js +5 -1
  61. package/plugins/rate-limit/index.js +5 -1
  62. package/plugins/xlsx/engine.js +397 -0
  63. package/plugins/xlsx/index.js +91 -0
  64. package/plugins/xlsx/middleware.js +87 -0
  65. package/plugins/xlsx/services.js +118 -0
  66. package/src/api/api-loader.js +233 -0
  67. package/src/api/define-api.js +35 -0
  68. package/src/discovery/file-route-parser.js +201 -0
  69. package/src/discovery/scan-routes.js +265 -0
  70. package/src/file-router.js +6 -0
  71. package/src/modules/define-module.js +99 -0
  72. package/src/modules/module-discovery.js +117 -0
  73. package/src/pages/define-page.js +36 -0
  74. package/src/pages/page-loader.js +214 -0
  75. package/src/routing/mount-discovered-routes.js +78 -0
  76. package/src/routing/route-table.js +191 -0
  77. package/src/server.js +112 -15
  78. package/src/services/builtins/file-manager.js +178 -0
  79. package/src/services/index.js +2 -0
  80. package/bin/commands/skill.js +0 -187
  81. package/templates/skills/webspresso-usage/REFERENCE-framework.md +0 -289
  82. package/templates/skills/webspresso-usage/REFERENCE-kernel.md +0 -93
  83. package/templates/skills/webspresso-usage/SKILL.md +0 -48
@@ -0,0 +1,261 @@
1
+ /**
2
+ * File Manager Admin API Handlers with RBAC & Storage Quota
3
+ * @module plugins/file-manager/api-handlers
4
+ */
5
+
6
+ const multer = require('multer');
7
+ const {
8
+ listFiles,
9
+ createDirectory,
10
+ renameItem,
11
+ deleteItem,
12
+ moveItem,
13
+ saveUploadedFile,
14
+ calculateStorageUsage,
15
+ } = require('./core');
16
+
17
+ /**
18
+ * Create Multer middleware for in-memory upload parsing
19
+ * @param {number} maxBytes
20
+ * @param {number} maxFiles
21
+ */
22
+ function createUploadMiddleware(maxBytes, maxFiles = 20) {
23
+ const upload = multer({
24
+ storage: multer.memoryStorage(),
25
+ limits: { fileSize: maxBytes, files: maxFiles },
26
+ });
27
+ return upload.array('files', maxFiles);
28
+ }
29
+
30
+ /**
31
+ * Helper to check RBAC permissions
32
+ * @param {import('express').Request} req
33
+ * @param {string} action
34
+ * @param {Record<string, string[]>|null} permissions
35
+ * @returns {boolean}
36
+ */
37
+ function isActionAllowed(req, action, permissions) {
38
+ if (!permissions || !permissions[action]) {
39
+ return true; // No restriction specified
40
+ }
41
+
42
+ const allowedRoles = permissions[action];
43
+ if (!Array.isArray(allowedRoles) || allowedRoles.includes('*')) {
44
+ return true;
45
+ }
46
+
47
+ const adminUser = req.session && req.session.adminUser;
48
+ const userRole = adminUser ? (adminUser.role || 'admin') : 'admin';
49
+
50
+ return allowedRoles.includes(userRole);
51
+ }
52
+
53
+ /**
54
+ * @param {Object} options
55
+ * @param {string} options.baseDir
56
+ * @param {string} options.publicBasePath
57
+ * @param {number} [options.maxUploadBytes=10485760]
58
+ * @param {number|null} [options.maxTotalStorageBytes]
59
+ * @param {string[]|null} [options.allowedExtensions]
60
+ * @param {string[]|null} [options.allowedMimeTypes]
61
+ * @param {Record<string, string[]>|null} [options.permissions]
62
+ * @param {Object} [options.security]
63
+ */
64
+ function createFileManagerApiHandlers(options = {}) {
65
+ const baseDir = options.baseDir || './public/uploads';
66
+ const publicBasePath = options.publicBasePath || '/uploads';
67
+ const maxUploadBytes = options.maxUploadBytes || 10 * 1024 * 1024;
68
+ const maxTotalStorageBytes = options.maxTotalStorageBytes || null;
69
+ const allowedExtensions = options.allowedExtensions || null;
70
+ const allowedMimeTypes = options.allowedMimeTypes || null;
71
+ const permissions = options.permissions || null;
72
+ const securityOpts = options.security || {};
73
+
74
+ const parseUpload = createUploadMiddleware(maxUploadBytes);
75
+
76
+ return {
77
+ async list(req, res) {
78
+ if (!isActionAllowed(req, 'list', permissions)) {
79
+ return res.status(403).json({ success: false, error: 'Permission denied for action: list' });
80
+ }
81
+
82
+ try {
83
+ const relPath = req.query.path || '';
84
+ const search = req.query.search || undefined;
85
+ const type = req.query.type || undefined;
86
+ const sort = req.query.sort || 'name';
87
+ const order = req.query.order || 'asc';
88
+ const page = req.query.page ? parseInt(req.query.page, 10) : undefined;
89
+ const perPage = req.query.perPage ? parseInt(req.query.perPage, 10) : undefined;
90
+
91
+ const result = await listFiles(baseDir, relPath, {
92
+ search,
93
+ type,
94
+ sort,
95
+ order,
96
+ page,
97
+ perPage,
98
+ publicBasePath,
99
+ });
100
+
101
+ return res.json({ success: true, ...result });
102
+ } catch (err) {
103
+ const status = err.statusCode || err.status || 400;
104
+ return res.status(status).json({ success: false, error: err.message });
105
+ }
106
+ },
107
+
108
+ async mkdir(req, res) {
109
+ if (!isActionAllowed(req, 'mkdir', permissions)) {
110
+ return res.status(403).json({ success: false, error: 'Permission denied for action: mkdir' });
111
+ }
112
+
113
+ try {
114
+ const { path: relPath = '', name } = req.body || {};
115
+ if (!name) {
116
+ return res.status(400).json({ success: false, error: 'Directory name is required' });
117
+ }
118
+ const result = await createDirectory(baseDir, relPath, name);
119
+ return res.status(201).json({ success: true, data: result });
120
+ } catch (err) {
121
+ const status = err.statusCode || err.status || 400;
122
+ return res.status(status).json({ success: false, error: err.message });
123
+ }
124
+ },
125
+
126
+ async rename(req, res) {
127
+ if (!isActionAllowed(req, 'rename', permissions)) {
128
+ return res.status(403).json({ success: false, error: 'Permission denied for action: rename' });
129
+ }
130
+
131
+ try {
132
+ const { path: itemPath, newName } = req.body || {};
133
+ if (!itemPath || !newName) {
134
+ return res.status(400).json({ success: false, error: 'Path and newName are required' });
135
+ }
136
+ const result = await renameItem(baseDir, itemPath, newName);
137
+ return res.json({ success: true, data: result });
138
+ } catch (err) {
139
+ const status = err.statusCode || err.status || 400;
140
+ return res.status(status).json({ success: false, error: err.message });
141
+ }
142
+ },
143
+
144
+ async move(req, res) {
145
+ if (!isActionAllowed(req, 'move', permissions)) {
146
+ return res.status(403).json({ success: false, error: 'Permission denied for action: move' });
147
+ }
148
+
149
+ try {
150
+ const { source, destination = '' } = req.body || {};
151
+ if (!source) {
152
+ return res.status(400).json({ success: false, error: 'Source path is required' });
153
+ }
154
+ const result = await moveItem(baseDir, source, destination);
155
+ return res.json({ success: true, data: result });
156
+ } catch (err) {
157
+ const status = err.statusCode || err.status || 400;
158
+ return res.status(status).json({ success: false, error: err.message });
159
+ }
160
+ },
161
+
162
+ async delete(req, res) {
163
+ if (!isActionAllowed(req, 'delete', permissions)) {
164
+ return res.status(403).json({ success: false, error: 'Permission denied for action: delete' });
165
+ }
166
+
167
+ try {
168
+ const { path: itemPath } = req.body || {};
169
+ if (!itemPath) {
170
+ return res.status(400).json({ success: false, error: 'Path is required' });
171
+ }
172
+ const result = await deleteItem(baseDir, itemPath);
173
+ return res.json({ success: true, data: result });
174
+ } catch (err) {
175
+ const status = err.statusCode || err.status || 400;
176
+ return res.status(status).json({ success: false, error: err.message });
177
+ }
178
+ },
179
+
180
+ upload(req, res) {
181
+ if (!isActionAllowed(req, 'upload', permissions)) {
182
+ return res.status(403).json({ success: false, error: 'Permission denied for action: upload' });
183
+ }
184
+
185
+ parseUpload(req, res, async (err) => {
186
+ if (err) {
187
+ if (err instanceof multer.MulterError && err.code === 'LIMIT_FILE_SIZE') {
188
+ return res.status(413).json({ success: false, error: 'File too large' });
189
+ }
190
+ return res.status(400).json({ success: false, error: err.message || 'Upload error' });
191
+ }
192
+
193
+ const files = req.files || (req.file ? [req.file] : []);
194
+ if (!files || files.length === 0) {
195
+ return res.status(400).json({ success: false, error: 'No files uploaded' });
196
+ }
197
+
198
+ const targetRelPath = req.query.path || (req.body && req.body.path) || '';
199
+
200
+ // Storage Quota Check
201
+ if (maxTotalStorageBytes && typeof maxTotalStorageBytes === 'number') {
202
+ const currentUsage = await calculateStorageUsage(baseDir);
203
+ const incomingBytes = files.reduce((acc, f) => acc + (f.size || f.buffer?.length || 0), 0);
204
+ if (currentUsage + incomingBytes > maxTotalStorageBytes) {
205
+ return res.status(507).json({
206
+ success: false,
207
+ error: 'Storage quota exceeded for this instance',
208
+ });
209
+ }
210
+ }
211
+
212
+ // Validate allowed extensions and MIME types
213
+ for (const f of files) {
214
+ const ext = (f.originalname || '').split('.').pop().toLowerCase();
215
+ if (allowedExtensions && allowedExtensions.length && !allowedExtensions.includes(ext)) {
216
+ return res.status(400).json({
217
+ success: false,
218
+ error: `File extension .${ext} is not allowed`,
219
+ });
220
+ }
221
+ if (allowedMimeTypes && allowedMimeTypes.length && !allowedMimeTypes.includes(f.mimetype)) {
222
+ return res.status(415).json({
223
+ success: false,
224
+ error: `MIME type ${f.mimetype} is not allowed`,
225
+ });
226
+ }
227
+ }
228
+
229
+ try {
230
+ const uploaded = [];
231
+ for (const file of files) {
232
+ const saved = await saveUploadedFile(
233
+ baseDir,
234
+ targetRelPath,
235
+ file.buffer,
236
+ file.originalname,
237
+ file.mimetype,
238
+ publicBasePath,
239
+ securityOpts
240
+ );
241
+ uploaded.push(saved);
242
+ }
243
+
244
+ return res.status(201).json({
245
+ success: true,
246
+ files: uploaded,
247
+ count: uploaded.length,
248
+ });
249
+ } catch (saveErr) {
250
+ const status = saveErr.statusCode || saveErr.status || 500;
251
+ return res.status(status).json({ success: false, error: saveErr.message });
252
+ }
253
+ });
254
+ },
255
+ };
256
+ }
257
+
258
+ module.exports = {
259
+ createFileManagerApiHandlers,
260
+ isActionAllowed,
261
+ };