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.
Files changed (94) 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/.agents/skills/webspresso-admin-and-plugins/SKILL.md +122 -0
  23. package/.agents/skills/webspresso-framework/SKILL.md +56 -0
  24. package/.agents/skills/webspresso-orm-and-database/SKILL.md +119 -0
  25. package/.agents/skills/webspresso-routing-and-api/SKILL.md +114 -0
  26. package/.agents/skills/webspresso-services/SKILL.md +101 -0
  27. package/.cursorrules +14 -0
  28. package/AGENTS.md +175 -0
  29. package/CLAUDE.md +54 -0
  30. package/README.md +18 -14
  31. package/bin/commands/admin-password.js +2 -1
  32. package/bin/commands/admin-setup.js +1 -1
  33. package/bin/commands/agents-init.js +209 -0
  34. package/bin/commands/api.js +114 -48
  35. package/bin/commands/audit-prune.js +1 -1
  36. package/bin/commands/doctor.js +35 -0
  37. package/bin/commands/favicon-generate.js +1 -1
  38. package/bin/commands/new.js +13 -2
  39. package/bin/commands/orm-map.js +5 -4
  40. package/bin/commands/page.js +75 -89
  41. package/bin/commands/polar-migrate.js +1 -1
  42. package/bin/commands/seed.js +1 -1
  43. package/bin/commands/service.js +77 -0
  44. package/bin/webspresso.js +2 -2
  45. package/core/auth/hash.js +10 -5
  46. package/core/compileSchema.js +1 -1
  47. package/core/errors/middleware.js +22 -2
  48. package/core/errors/normalize.js +29 -2
  49. package/{utils/schemaCache.js → core/schema-cache.js} +2 -3
  50. package/index.d.ts +306 -0
  51. package/index.js +35 -1
  52. package/package.json +36 -14
  53. package/plugins/admin-panel/app.js +10 -9
  54. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +22 -1
  55. package/plugins/admin-panel/core/api-extensions.js +4 -0
  56. package/plugins/basic-auth/index.js +3 -1
  57. package/plugins/content/client/inline-edit.js +12 -12
  58. package/plugins/cors.js +6 -7
  59. package/plugins/csrf/index.js +3 -2
  60. package/plugins/csv/engine.js +447 -0
  61. package/plugins/csv/index.js +94 -0
  62. package/plugins/csv/middleware.js +80 -0
  63. package/plugins/csv/services.js +138 -0
  64. package/plugins/file-manager/admin-component.js +613 -0
  65. package/plugins/file-manager/api-handlers.js +261 -0
  66. package/plugins/file-manager/core.js +601 -0
  67. package/plugins/file-manager/index.js +139 -0
  68. package/plugins/file-manager/security.js +217 -0
  69. package/plugins/index.js +45 -79
  70. package/plugins/mcp/transports/sse.js +4 -2
  71. package/plugins/site-analytics/admin-component.js +7 -8
  72. package/plugins/xlsx/engine.js +397 -0
  73. package/plugins/xlsx/index.js +91 -0
  74. package/plugins/xlsx/middleware.js +87 -0
  75. package/plugins/xlsx/services.js +118 -0
  76. package/src/api/api-loader.js +233 -0
  77. package/src/api/define-api.js +35 -0
  78. package/src/discovery/file-route-parser.js +201 -0
  79. package/src/discovery/scan-routes.js +265 -0
  80. package/src/file-router.js +9 -1
  81. package/src/modules/define-module.js +99 -0
  82. package/src/modules/module-discovery.js +117 -0
  83. package/src/pages/define-page.js +36 -0
  84. package/src/pages/page-loader.js +214 -0
  85. package/src/routing/mount-discovered-routes.js +78 -0
  86. package/src/routing/route-table.js +191 -0
  87. package/src/server.js +112 -15
  88. package/src/services/builtins/file-manager.js +178 -0
  89. package/src/services/discovery.js +6 -1
  90. package/src/services/index.js +6 -11
  91. package/bin/commands/skill.js +0 -187
  92. package/templates/skills/webspresso-usage/REFERENCE-framework.md +0 -289
  93. package/templates/skills/webspresso-usage/REFERENCE-kernel.md +0 -93
  94. package/templates/skills/webspresso-usage/SKILL.md +0 -48
@@ -0,0 +1,233 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso API Loader
5
+ * Handles validation and execution of defineApi() endpoints
6
+ * @module src/api/api-loader
7
+ */
8
+
9
+ const { ZodError } = require('zod');
10
+ const { z: zForApi } = require('../../core/validation');
11
+ const { resolveMiddlewares } = require('../file-router');
12
+
13
+ /**
14
+ * Compiles a schema definition from an API module
15
+ * @param {Object|Function} schemaDef
16
+ * @returns {Object|null}
17
+ */
18
+ function compileApiSchema(schemaDef) {
19
+ if (!schemaDef) return null;
20
+ if (typeof schemaDef === 'function') {
21
+ try {
22
+ return schemaDef({ z: zForApi });
23
+ } catch (err) {
24
+ throw new Error(`Failed to compile API schema: ${err.message}`);
25
+ }
26
+ }
27
+ if (typeof schemaDef === 'object') {
28
+ return schemaDef;
29
+ }
30
+ return null;
31
+ }
32
+
33
+ /**
34
+ * Validates request input against compiled schema
35
+ * @param {import('express').Request} req
36
+ * @param {Object} compiledSchema
37
+ */
38
+ function validateRequestInput(req, compiledSchema) {
39
+ req.input = {
40
+ body: undefined,
41
+ params: undefined,
42
+ query: undefined,
43
+ headers: undefined,
44
+ };
45
+
46
+ if (!compiledSchema) return;
47
+
48
+ if (compiledSchema.body && typeof compiledSchema.body.parse === 'function') {
49
+ req.input.body = compiledSchema.body.parse(req.body);
50
+ } else {
51
+ req.input.body = req.body;
52
+ }
53
+
54
+ if (compiledSchema.params && typeof compiledSchema.params.parse === 'function') {
55
+ req.input.params = compiledSchema.params.parse(req.params);
56
+ } else {
57
+ req.input.params = req.params;
58
+ }
59
+
60
+ if (compiledSchema.query && typeof compiledSchema.query.parse === 'function') {
61
+ req.input.query = compiledSchema.query.parse(req.query);
62
+ } else {
63
+ req.input.query = req.query;
64
+ }
65
+
66
+ if (compiledSchema.headers && typeof compiledSchema.headers.parse === 'function') {
67
+ req.input.headers = compiledSchema.headers.parse(req.headers);
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Creates an Express route handler for a discovered API descriptor
73
+ * @param {Object} descriptor - Route descriptor
74
+ * @param {Object} context - Server context
75
+ * @returns {Function}
76
+ */
77
+ function createApiHandler(descriptor, context) {
78
+ const {
79
+ middlewares = {},
80
+ serviceRegistry = null,
81
+ db = null,
82
+ options: appOptions = {},
83
+ } = context;
84
+
85
+ const isDev = process.env.NODE_ENV === 'development';
86
+ let cachedMiddlewares = null;
87
+ let cachedApiDef = null;
88
+ let cachedHandlerFn = null;
89
+ let cachedCompiledSchema = null;
90
+
91
+ return async (req, res, next) => {
92
+ try {
93
+ // 1. Reload module in development
94
+ if (isDev) {
95
+ try {
96
+ const resolvedPath = require.resolve(descriptor.file);
97
+ if (require.cache[resolvedPath]) {
98
+ delete require.cache[resolvedPath];
99
+ cachedMiddlewares = null;
100
+ cachedApiDef = null;
101
+ cachedHandlerFn = null;
102
+ cachedCompiledSchema = null;
103
+ }
104
+ } catch {}
105
+ }
106
+
107
+ let apiDef = cachedApiDef;
108
+ let handlerFn = cachedHandlerFn;
109
+ let compiledSchema = cachedCompiledSchema;
110
+
111
+ if (!apiDef) {
112
+ let apiModule = {};
113
+ try {
114
+ apiModule = require(descriptor.file);
115
+ } catch (loadErr) {
116
+ return next(loadErr);
117
+ }
118
+
119
+ apiDef = typeof apiModule === 'function' ? { handler: apiModule } : apiModule.default || apiModule;
120
+ handlerFn = apiDef.handler || (typeof apiModule === 'function' ? apiModule : null);
121
+
122
+ if (typeof handlerFn !== 'function') {
123
+ throw new Error(`API file "${descriptor.source}" does not export a valid handler function`);
124
+ }
125
+
126
+ compiledSchema = compileApiSchema(apiDef.schema);
127
+
128
+ if (!isDev) {
129
+ cachedApiDef = apiDef;
130
+ cachedHandlerFn = handlerFn;
131
+ cachedCompiledSchema = compiledSchema;
132
+ }
133
+ }
134
+
135
+ // 2. Injections
136
+ if (db) req.db = db;
137
+ if (serviceRegistry) {
138
+ req.service = (name, input, opts) =>
139
+ serviceRegistry.call(name, input, { req, res, db }, opts);
140
+ }
141
+
142
+ // 3. Schema validation
143
+ if (compiledSchema) {
144
+ try {
145
+ validateRequestInput(req, compiledSchema);
146
+ } catch (err) {
147
+ if (err instanceof ZodError || err.name === 'ZodError' || Array.isArray(err?.issues)) {
148
+ return res.status(400).json({
149
+ error: 'Validation Error',
150
+ issues: err.issues,
151
+ });
152
+ }
153
+ throw err;
154
+ }
155
+ } else {
156
+ req.input = {
157
+ body: req.body,
158
+ params: req.params,
159
+ query: req.query,
160
+ };
161
+ }
162
+
163
+ // 4. Resolve and execute middlewares (module-local first, cached across requests)
164
+ if (!cachedMiddlewares) {
165
+ const moduleMiddlewares = descriptor.moduleConfig?.middlewares || {};
166
+ const combinedRegistry = Object.assign({}, middlewares, moduleMiddlewares);
167
+ cachedMiddlewares = resolveMiddlewares(apiDef.middleware, combinedRegistry);
168
+ }
169
+
170
+ for (const mw of cachedMiddlewares) {
171
+ if (typeof mw !== 'function') continue;
172
+ await new Promise((resolve, reject) => {
173
+ try {
174
+ mw(req, res, (err) => {
175
+ if (err) reject(err);
176
+ else resolve();
177
+ });
178
+ } catch (syncErr) {
179
+ reject(syncErr);
180
+ }
181
+ });
182
+ if (res.headersSent) return;
183
+ }
184
+
185
+ // 5. Execute handler
186
+ const apiCtx = {
187
+ req,
188
+ res,
189
+ params: req.params || {},
190
+ query: req.query || {},
191
+ body: req.body || {},
192
+ input: req.input,
193
+ service: (name, input, opts) =>
194
+ serviceRegistry ? serviceRegistry.call(name, input, { req, res, db }, opts) : null,
195
+ module: descriptor.module || null,
196
+ config: appOptions,
197
+ logger: console,
198
+ db,
199
+ };
200
+
201
+ req.ctx = apiCtx;
202
+ res.service = apiCtx.service;
203
+ res.input = apiCtx.input;
204
+ res.module = apiCtx.module;
205
+
206
+ const result = await handlerFn(req, res, apiCtx);
207
+
208
+ // 6. Auto-respond if handler returned a value and response not yet sent
209
+ if (!res.headersSent) {
210
+ if (result !== undefined) {
211
+ return res.json(result);
212
+ }
213
+ }
214
+ } catch (err) {
215
+ if (err && typeof err === 'object') {
216
+ if (!err.route) err.route = descriptor.path;
217
+ if (!err.method) err.method = descriptor.method ? descriptor.method.toUpperCase() : (req.method || 'GET');
218
+ if (!err.source) err.source = descriptor.source || descriptor.file;
219
+ if (!err.file) err.file = descriptor.file;
220
+ if (!err.module) err.module = descriptor.module || null;
221
+ if (!err.phase) err.phase = 'handler';
222
+ if (!err.requestId) err.requestId = req.id || (req.headers && req.headers['x-request-id']) || null;
223
+ }
224
+ return next(err);
225
+ }
226
+ };
227
+ }
228
+
229
+ module.exports = {
230
+ createApiHandler,
231
+ compileApiSchema,
232
+ validateRequestInput,
233
+ };
@@ -0,0 +1,35 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso API Definition Helper
5
+ * @module src/api/define-api
6
+ */
7
+
8
+ /**
9
+ * Defines an API route with schema validation, response metadata, and handler
10
+ * @param {Object} config
11
+ * @param {string} [config.description] - Endpoint description for OpenAPI
12
+ * @param {Array<string>} [config.tags] - Endpoint tags for OpenAPI
13
+ * @param {Array<string|Function>} [config.middleware] - Named or functional middlewares
14
+ * @param {Object|Function} [config.schema] - Request schemas (params, query, body, headers)
15
+ * @param {Object} [config.response] - Expected response schemas by status code
16
+ * @param {Function} config.handler - Request handler: async (req, ctx) => data
17
+ * @returns {Object}
18
+ */
19
+ function defineApi(config = {}) {
20
+ if (typeof config === 'function') {
21
+ return {
22
+ handler: config,
23
+ __isWebspressoApi: true,
24
+ };
25
+ }
26
+
27
+ return {
28
+ ...config,
29
+ __isWebspressoApi: true,
30
+ };
31
+ }
32
+
33
+ module.exports = {
34
+ defineApi,
35
+ };
@@ -0,0 +1,201 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Webspresso File Route Parser
5
+ * Converts relative filesystem paths into Express route paths and HTTP methods
6
+ * @module src/discovery/file-route-parser
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ const SUPPORTED_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'options', 'head']);
13
+ const SUPPORTED_EXTENSIONS = new Set(['.js', '.njk']);
14
+ const METHOD_SUFFIX_REGEX = /\.([a-zA-Z]+)\.js$/;
15
+ const TEMPORARY_FILE_REGEX = /(?:~|\.swp|\.tmp|\.bak|\.DS_Store)$/;
16
+
17
+ /**
18
+ * Safely scans a directory recursively for route files
19
+ * @param {string} dirPath - Absolute directory path
20
+ * @param {string} [baseDir=dirPath] - Base directory for computing relative paths
21
+ * @returns {Array<{ absolutePath: string, relativePath: string }>}
22
+ */
23
+ function scanDirSafely(dirPath, baseDir = dirPath) {
24
+ const results = [];
25
+ if (!fs.existsSync(dirPath)) return results;
26
+
27
+ try {
28
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
29
+
30
+ for (const entry of entries) {
31
+ const name = entry.name;
32
+ if (isPrivateOrIgnored(name)) continue;
33
+
34
+ const fullPath = path.join(dirPath, name);
35
+ const relPath = path.relative(baseDir, fullPath);
36
+
37
+ if (entry.isDirectory()) {
38
+ results.push(...scanDirSafely(fullPath, baseDir));
39
+ } else if (entry.isFile()) {
40
+ const ext = path.extname(name);
41
+ if (SUPPORTED_EXTENSIONS.has(ext)) {
42
+ results.push({
43
+ absolutePath: fullPath,
44
+ relativePath: relPath,
45
+ });
46
+ }
47
+ }
48
+ }
49
+ } catch (err) {
50
+ console.warn(`[webspresso] Failed to scan directory ${dirPath}:`, err.message);
51
+ }
52
+
53
+ return results;
54
+ }
55
+
56
+ /**
57
+ * Checks if a filename or directory name should be ignored (private or temporary)
58
+ * @param {string} name - File or directory basename
59
+ * @returns {boolean}
60
+ */
61
+ function isPrivateOrIgnored(name) {
62
+ if (!name || typeof name !== 'string') return true;
63
+ // Hidden files/directories (.git, .DS_Store, .login.js.swp)
64
+ if (name.startsWith('.')) return true;
65
+ // Private files/directories (_components, _helpers, _private.js)
66
+ if (name.startsWith('_')) return true;
67
+ // Temporary editor files
68
+ if (TEMPORARY_FILE_REGEX.test(name)) return true;
69
+ return false;
70
+ }
71
+
72
+ /**
73
+ * Converts parameter markers [id] -> :id and catch-all [...path] -> *
74
+ * @param {string} segment
75
+ * @returns {string}
76
+ */
77
+ function transformDynamicSegment(segment) {
78
+ if (!segment || typeof segment !== 'string') return segment || '';
79
+ if (segment.startsWith('[...') && segment.endsWith(']')) {
80
+ const paramName = segment.slice(4, -1).trim();
81
+ return paramName ? `*${paramName}` : '*';
82
+ }
83
+ if (segment.startsWith('[') && segment.endsWith(']')) {
84
+ const paramName = segment.slice(1, -1).trim();
85
+ return paramName ? `:${paramName}` : segment;
86
+ }
87
+ return segment;
88
+ }
89
+
90
+ /**
91
+ * Normalizes a URL prefix to have a leading slash and no trailing slash
92
+ * @param {string} [prefix='']
93
+ * @returns {string}
94
+ */
95
+ function normalizePrefix(prefix = '') {
96
+ if (!prefix || typeof prefix !== 'string') return '';
97
+ let clean = prefix.trim().replace(/\\/g, '/');
98
+ clean = clean.replace(/\/+/g, '/');
99
+ if (clean === '/' || !clean) return '';
100
+ if (!clean.startsWith('/')) clean = '/' + clean;
101
+ if (clean.endsWith('/')) clean = clean.slice(0, -1);
102
+ return clean === '/' ? '' : clean;
103
+ }
104
+
105
+ /**
106
+ * Parses a relative file path for an API or Page route
107
+ * @param {string} relativePath - Relative path from directory root (e.g. 'users/[id].get.js' or 'blog/[slug].js')
108
+ * @param {Object} [options]
109
+ * @param {string} [options.type='page'] - 'page' | 'api'
110
+ * @param {string} [options.prefix=''] - Base prefix (e.g. '/api' or '/auth')
111
+ * @returns {{ path: string, method: string, isValid: boolean, isPrivate: boolean, originalMethodPart?: string }}
112
+ */
113
+ function parseFileRoute(relativePath, options = {}) {
114
+ if (!relativePath || typeof relativePath !== 'string') {
115
+ return { path: '', method: '', isValid: false, isPrivate: false };
116
+ }
117
+
118
+ const { type = 'page', prefix = '' } = options;
119
+
120
+ // Normalize path separators to forward slashes
121
+ const normalizedPath = relativePath.split(path.sep).join('/').replace(/\\/g, '/');
122
+ const segments = normalizedPath.split('/').filter(Boolean);
123
+
124
+ // Check if any segment is private or ignored
125
+ for (const seg of segments) {
126
+ if (isPrivateOrIgnored(seg)) {
127
+ return { path: '', method: '', isValid: false, isPrivate: true };
128
+ }
129
+ }
130
+
131
+ const filename = segments[segments.length - 1];
132
+ if (!filename) {
133
+ return { path: '', method: '', isValid: false, isPrivate: false };
134
+ }
135
+
136
+ let method = 'get';
137
+ let cleanFilename = filename;
138
+ let originalMethodPart = null;
139
+
140
+ // Check for HTTP method suffix: e.g. health.get.js, [id].patch.js
141
+ const methodMatch = filename.match(METHOD_SUFFIX_REGEX);
142
+ if (methodMatch) {
143
+ const rawMethod = methodMatch[1].toLowerCase();
144
+ originalMethodPart = rawMethod;
145
+ if (SUPPORTED_METHODS.has(rawMethod)) {
146
+ method = rawMethod;
147
+ cleanFilename = filename.replace(`.${methodMatch[1]}.js`, '');
148
+ } else {
149
+ // Invalid HTTP method in filename
150
+ return { path: '', method: rawMethod, isValid: false, isPrivate: false, invalidMethod: true };
151
+ }
152
+ } else if (filename.endsWith('.js')) {
153
+ cleanFilename = filename.slice(0, -3);
154
+ } else if (filename.endsWith('.njk')) {
155
+ cleanFilename = filename.slice(0, -4);
156
+ }
157
+
158
+ // Replace last segment with cleanFilename
159
+ const routeSegments = [...segments.slice(0, -1), cleanFilename];
160
+
161
+ // Process dynamic brackets in segments
162
+ const transformedSegments = routeSegments.map(transformDynamicSegment);
163
+
164
+ // Handle 'index' at the end of path
165
+ if (transformedSegments[transformedSegments.length - 1] === 'index') {
166
+ transformedSegments.pop();
167
+ }
168
+
169
+ let routePath = '/' + transformedSegments.join('/');
170
+ if (routePath === '//' || routePath === '') {
171
+ routePath = '/';
172
+ }
173
+
174
+ // Combine with prefix
175
+ const cleanPrefix = normalizePrefix(prefix);
176
+ if (cleanPrefix) {
177
+ if (routePath === '/') {
178
+ routePath = cleanPrefix;
179
+ } else {
180
+ routePath = `${cleanPrefix}${routePath}`;
181
+ }
182
+ }
183
+
184
+ return {
185
+ path: routePath,
186
+ method: method.toUpperCase(),
187
+ isValid: true,
188
+ isPrivate: false,
189
+ originalMethodPart,
190
+ };
191
+ }
192
+
193
+ module.exports = {
194
+ parseFileRoute,
195
+ isPrivateOrIgnored,
196
+ normalizePrefix,
197
+ transformDynamicSegment,
198
+ scanDirSafely,
199
+ SUPPORTED_METHODS,
200
+ SUPPORTED_EXTENSIONS,
201
+ };