webspresso 0.0.90 → 0.0.93

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 (125) hide show
  1. package/README.md +98 -2509
  2. package/bin/commands/admin-password.js +8 -2
  3. package/bin/commands/api.js +7 -1
  4. package/bin/commands/favicon-generate.js +6 -4
  5. package/bin/commands/mcp.js +162 -0
  6. package/bin/commands/new.js +7 -1
  7. package/bin/commands/page.js +7 -1
  8. package/bin/commands/seed.js +7 -1
  9. package/bin/commands/service.js +199 -0
  10. package/bin/commands/skill.js +7 -1
  11. package/bin/webspresso.js +4 -0
  12. package/core/auth/middleware.js +9 -0
  13. package/core/compileSchema.js +1 -5
  14. package/core/compression/compressible.js +63 -0
  15. package/core/compression/compression-stream.js +59 -0
  16. package/core/compression/index.js +25 -0
  17. package/core/compression/middleware.js +310 -0
  18. package/core/compression/negotiate.js +165 -0
  19. package/core/errors/base.js +31 -0
  20. package/core/errors/domain.js +193 -0
  21. package/core/errors/http.js +133 -0
  22. package/core/errors/index.js +77 -0
  23. package/core/errors/middleware.js +162 -0
  24. package/core/errors/normalize.js +171 -0
  25. package/core/kernel/app.js +2 -1
  26. package/core/kernel/index.js +5 -1
  27. package/core/orm/cache/fingerprint.js +23 -5
  28. package/core/orm/complexity.js +120 -0
  29. package/core/orm/eager-loader.js +54 -9
  30. package/core/orm/events.js +64 -14
  31. package/core/orm/index.js +83 -12
  32. package/core/orm/model.js +13 -2
  33. package/core/orm/query-builder.js +30 -4
  34. package/core/orm/repository.js +57 -25
  35. package/core/orm/transaction.js +39 -5
  36. package/core/orm/utils.js +6 -3
  37. package/core/queue/adapters/database.js +317 -0
  38. package/core/queue/adapters/memory.js +176 -0
  39. package/core/queue/adapters/redis.js +357 -0
  40. package/core/queue/index.js +21 -0
  41. package/core/queue/job.js +103 -0
  42. package/core/queue/manager.js +413 -0
  43. package/core/realtime/adapters/redis.js +431 -0
  44. package/core/realtime/adapters/socket-io.js +203 -0
  45. package/core/realtime/adapters/sse.js +210 -0
  46. package/core/realtime/adapters/websocket.js +337 -0
  47. package/core/realtime/auth-manager.js +140 -0
  48. package/core/realtime/core.js +359 -0
  49. package/core/realtime/emitter.js +98 -0
  50. package/core/realtime/errors.js +71 -0
  51. package/core/realtime/index.js +79 -0
  52. package/core/realtime/reconnect-manager.js +118 -0
  53. package/core/realtime/registry.js +243 -0
  54. package/core/shutdown/http-adapter.js +134 -0
  55. package/core/shutdown/index.js +12 -0
  56. package/core/shutdown/shutdown-manager.js +312 -0
  57. package/core/ssr/index.js +13 -0
  58. package/core/ssr/stream.js +174 -0
  59. package/core/validation/index.js +70 -0
  60. package/index.d.ts +924 -53
  61. package/index.js +110 -1
  62. package/package.json +30 -2
  63. package/plugins/admin-panel/api.js +37 -4
  64. package/plugins/admin-panel/app.js +57 -17
  65. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +10 -2
  66. package/plugins/admin-panel/client/parts/04-field-renderers.js +102 -58
  67. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +51 -27
  68. package/plugins/admin-panel/client/parts/07-model-list.js +9 -4
  69. package/plugins/admin-panel/client/parts/08-record-list.js +37 -26
  70. package/plugins/admin-panel/client/parts/09-record-form.js +168 -19
  71. package/plugins/admin-panel/core/admin-module.js +2 -2
  72. package/plugins/admin-panel/core/api-extensions.js +149 -0
  73. package/plugins/admin-panel/field-renderers/rich-text.js +3 -2
  74. package/plugins/admin-panel/index.js +155 -23
  75. package/plugins/admin-panel/modules/bulk-actions.js +9 -2
  76. package/plugins/admin-panel/modules/custom-pages.js +144 -45
  77. package/plugins/admin-panel/modules/dashboard.js +8 -1
  78. package/plugins/admin-panel/modules/menu.js +85 -0
  79. package/plugins/admin-panel/modules/user-management.js +5 -2
  80. package/plugins/admin-panel/vendor/mithril.min.js +1 -0
  81. package/plugins/admin-panel/vendor/quill.min.js +7 -0
  82. package/plugins/admin-panel/vendor/quill.snow.min.css +945 -0
  83. package/plugins/admin-panel/vendor/tailwind.min.js +65 -0
  84. package/plugins/basic-auth/index.js +293 -0
  85. package/plugins/data-exchange/export-xlsx.js +28 -2
  86. package/plugins/data-exchange/import.js +55 -16
  87. package/plugins/data-exchange/index.js +19 -0
  88. package/plugins/data-exchange/parse-table.js +14 -5
  89. package/plugins/data-exchange/record-selection.js +2 -0
  90. package/plugins/email/index.js +14 -1
  91. package/plugins/index.js +33 -0
  92. package/plugins/mcp/auth.js +104 -0
  93. package/plugins/mcp/discovery.js +88 -0
  94. package/plugins/mcp/index.js +208 -0
  95. package/plugins/mcp/prompts.js +149 -0
  96. package/plugins/mcp/providers/orm.js +325 -0
  97. package/plugins/mcp/providers/services.js +190 -0
  98. package/plugins/mcp/providers/system.js +87 -0
  99. package/plugins/mcp/server.js +472 -0
  100. package/plugins/mcp/transports/sse.js +139 -0
  101. package/plugins/mcp/transports/stdio.js +108 -0
  102. package/plugins/queue/index.js +108 -0
  103. package/plugins/realtime/index.js +150 -0
  104. package/plugins/recaptcha/index.js +11 -0
  105. package/plugins/redirect/index.js +9 -1
  106. package/plugins/rest-resources/index.js +280 -11
  107. package/plugins/upload/index.js +16 -0
  108. package/src/app-context.js +46 -6
  109. package/src/file-router.js +366 -130
  110. package/src/helpers.js +20 -2
  111. package/src/njk-frontmatter.js +24 -14
  112. package/src/plugin-manager.js +124 -4
  113. package/src/server.js +332 -54
  114. package/src/services/builtins/auth.js +273 -0
  115. package/src/services/builtins/exchange.js +229 -0
  116. package/src/services/builtins/mail.js +134 -0
  117. package/src/services/builtins/media.js +196 -0
  118. package/src/services/builtins/system.js +221 -0
  119. package/src/services/discovery.js +141 -0
  120. package/src/services/executor.js +292 -0
  121. package/src/services/index.js +70 -0
  122. package/src/services/memoize.js +256 -0
  123. package/src/services/registry.js +290 -0
  124. package/src/services/validator.js +230 -0
  125. package/templates/skills/webspresso-usage/REFERENCE-framework.md +6 -4
@@ -0,0 +1,290 @@
1
+ /**
2
+ * Webspresso Service Registry
3
+ * Central registry holding discovered and programmatically registered services
4
+ * @module src/services/registry
5
+ */
6
+
7
+ const path = require('path');
8
+ const { ConfigurationError, WebspressoError } = require('../../core/errors');
9
+ const { discoverServices } = require('./discovery');
10
+ const { executeService } = require('./executor');
11
+ const { memoize } = require('./memoize');
12
+
13
+ class ServiceRegistry {
14
+ /**
15
+ * @param {Object} [options]
16
+ * @param {string} [options.servicesDir] - Path to services directory
17
+ * @param {boolean} [options.isDev=false] - Whether in development mode
18
+ * @param {Object} [options.logger] - Logger instance
19
+ */
20
+ constructor(options = {}) {
21
+ this.servicesDir = options.servicesDir ? path.resolve(options.servicesDir) : null;
22
+ this.isDev = Boolean(options.isDev);
23
+ this.logger = options.logger || null;
24
+
25
+ /** @type {Map<string, Object>} */
26
+ this.services = new Map();
27
+
28
+ /** @type {Map<string, string>} */
29
+ this.aliases = new Map();
30
+ }
31
+
32
+ /**
33
+ * Discover and load all services from servicesDir
34
+ * @returns {ServiceRegistry}
35
+ */
36
+ load() {
37
+ if (!this.servicesDir) {
38
+ return this;
39
+ }
40
+
41
+ const discovered = discoverServices(this.servicesDir);
42
+
43
+ for (const item of discovered) {
44
+ const { name, aliases, filePath } = item;
45
+
46
+ // Duplicate check
47
+ if (this.services.has(name)) {
48
+ const existing = this.services.get(name);
49
+ throw new ConfigurationError(
50
+ `Duplicate service registered: ${name} (files: ${existing.filePath} and ${filePath})`
51
+ );
52
+ }
53
+
54
+ // In dev mode, ensure fresh require
55
+ if (this.isDev) {
56
+ try {
57
+ const resolved = require.resolve(filePath);
58
+ delete require.cache[resolved];
59
+ } catch (e) {}
60
+ }
61
+
62
+ let mod;
63
+ try {
64
+ mod = require(filePath);
65
+ } catch (err) {
66
+ throw new ConfigurationError(`Failed to load service file '${filePath}': ${err.message}`, {
67
+ cause: err,
68
+ });
69
+ }
70
+
71
+ const raw = mod && mod.__esModule && mod.default ? mod.default : (mod && mod.default ? mod.default : mod);
72
+ const definition = typeof raw === 'function' ? { handler: raw } : raw;
73
+
74
+ if (!definition || typeof definition !== 'object') {
75
+ throw new ConfigurationError(
76
+ `Service file '${filePath}' must export an object or handler function`
77
+ );
78
+ }
79
+
80
+ if (typeof definition.handler !== 'function') {
81
+ throw new ConfigurationError(
82
+ `Service '${name}' in '${filePath}' must provide a handler function`
83
+ );
84
+ }
85
+
86
+ let handler = definition.handler;
87
+ let memoizeFn = null;
88
+
89
+ if (definition.cache) {
90
+ const memoizeOpts = typeof definition.cache === 'object' && definition.cache !== null
91
+ ? definition.cache
92
+ : { ttl: definition.cache };
93
+ memoizeFn = memoize(definition.handler, memoizeOpts);
94
+ handler = memoizeFn;
95
+ }
96
+
97
+ const serviceDef = {
98
+ name,
99
+ filePath,
100
+ schema: definition.schema || null,
101
+ handler,
102
+ rawHandler: definition.handler,
103
+ memoize: memoizeFn,
104
+ metadata: definition.metadata || {},
105
+ description: definition.description || definition.metadata?.description || '',
106
+ auth: definition.auth,
107
+ cache: definition.cache,
108
+ timeout: definition.timeout,
109
+ transaction: definition.transaction,
110
+ };
111
+
112
+ this.services.set(name, serviceDef);
113
+
114
+ for (const alias of aliases) {
115
+ this.aliases.set(alias, name);
116
+ }
117
+ }
118
+
119
+ return this;
120
+ }
121
+
122
+ /**
123
+ * Programmatically register a service
124
+ * @param {string} name - Service name (e.g. 'user.get')
125
+ * @param {Object|Function} definition - Service definition or handler function
126
+ * @returns {ServiceRegistry}
127
+ */
128
+ register(name, definition) {
129
+ if (!name || typeof name !== 'string') {
130
+ throw new WebspressoError('Service name must be a non-empty string');
131
+ }
132
+
133
+ if (this.services.has(name)) {
134
+ throw new ConfigurationError(`Duplicate service registered: ${name}`);
135
+ }
136
+
137
+ const def = typeof definition === 'function' ? { handler: definition } : definition;
138
+
139
+ if (!def || typeof def !== 'object' || typeof def.handler !== 'function') {
140
+ throw new ConfigurationError(`Service '${name}' must provide a handler function`);
141
+ }
142
+
143
+ let handler = def.handler;
144
+ let memoizeFn = null;
145
+
146
+ if (def.cache) {
147
+ const memoizeOpts = typeof def.cache === 'object' && def.cache !== null
148
+ ? def.cache
149
+ : { ttl: def.cache };
150
+ memoizeFn = memoize(def.handler, memoizeOpts);
151
+ handler = memoizeFn;
152
+ }
153
+
154
+ const serviceDef = {
155
+ name,
156
+ filePath: def.filePath || null,
157
+ schema: def.schema || null,
158
+ handler,
159
+ rawHandler: def.handler,
160
+ memoize: memoizeFn,
161
+ metadata: def.metadata || {},
162
+ description: def.description || def.metadata?.description || '',
163
+ auth: def.auth,
164
+ cache: def.cache,
165
+ timeout: def.timeout,
166
+ transaction: def.transaction,
167
+ };
168
+
169
+ this.services.set(name, serviceDef);
170
+ return this;
171
+ }
172
+
173
+ /**
174
+ * Get a service definition by name or alias
175
+ * @param {string} name - Service name
176
+ * @returns {Object|undefined}
177
+ */
178
+ get(name) {
179
+ if (!name || typeof name !== 'string') {
180
+ return undefined;
181
+ }
182
+
183
+ if (this.services.has(name)) {
184
+ return this.services.get(name);
185
+ }
186
+
187
+ const aliasedName = this.aliases.get(name);
188
+ if (aliasedName && this.services.has(aliasedName)) {
189
+ return this.services.get(aliasedName);
190
+ }
191
+
192
+ return undefined;
193
+ }
194
+
195
+ /**
196
+ * Check if a service exists
197
+ * @param {string} name - Service name
198
+ * @returns {boolean}
199
+ */
200
+ has(name) {
201
+ return this.get(name) !== undefined;
202
+ }
203
+
204
+ /**
205
+ * List all registered service names
206
+ * @returns {string[]}
207
+ */
208
+ list() {
209
+ return Array.from(this.services.keys());
210
+ }
211
+
212
+ /**
213
+ * Invalidate cached results for a specific service and input
214
+ * @param {string} name - Service name
215
+ * @param {*} [input] - Input arguments
216
+ * @param {Object} [ctx] - Context
217
+ * @returns {boolean} Whether cache was invalidated
218
+ */
219
+ invalidate(name, input, ctx) {
220
+ const serviceDef = this.get(name);
221
+ if (serviceDef && serviceDef.memoize) {
222
+ return serviceDef.memoize.invalidate(input, ctx);
223
+ }
224
+ return false;
225
+ }
226
+
227
+ /**
228
+ * Clear cache for a specific service or all services
229
+ * @param {string} [name] - Optional service name (omitting clears all)
230
+ * @returns {boolean}
231
+ */
232
+ clearCache(name) {
233
+ if (name) {
234
+ const serviceDef = this.get(name);
235
+ if (serviceDef && serviceDef.memoize) {
236
+ serviceDef.memoize.clear();
237
+ return true;
238
+ }
239
+ return false;
240
+ }
241
+
242
+ for (const serviceDef of this.services.values()) {
243
+ if (serviceDef.memoize) {
244
+ serviceDef.memoize.clear();
245
+ }
246
+ }
247
+ return true;
248
+ }
249
+
250
+ /**
251
+ * Clear all service caches
252
+ * @returns {boolean}
253
+ */
254
+ clearAllCaches() {
255
+ return this.clearCache();
256
+ }
257
+
258
+ /**
259
+ * Execute a service by name
260
+ * @param {string} name - Service name
261
+ * @param {*} [input={}] - Input data
262
+ * @param {Object} [ctx={}] - Context object
263
+ * @param {Object} [options={}] - Options
264
+ * @returns {Promise<*>}
265
+ */
266
+ call(name, input = {}, ctx = {}, options = {}) {
267
+ return executeService(this, name, input, ctx, options);
268
+ }
269
+
270
+ /**
271
+ * Alias for call()
272
+ */
273
+ execute(name, input = {}, ctx = {}, options = {}) {
274
+ return this.call(name, input, ctx, options);
275
+ }
276
+
277
+ /**
278
+ * Reload services from disk (useful in development)
279
+ * @returns {ServiceRegistry}
280
+ */
281
+ reload() {
282
+ this.services.clear();
283
+ this.aliases.clear();
284
+ return this.load();
285
+ }
286
+ }
287
+
288
+ module.exports = {
289
+ ServiceRegistry,
290
+ };
@@ -0,0 +1,230 @@
1
+ /**
2
+ * Webspresso Services Input Validation
3
+ * Powered by Zod with sync/async validation, prototype poisoning defense, and functional schema compilation
4
+ * @module src/services/validator
5
+ */
6
+
7
+ const { ValidationError } = require('../../core/errors');
8
+ const { z: zForServices, sanitizeInput, hasDangerousKeys } = require('../../core/validation');
9
+ const z = zForServices;
10
+
11
+ /**
12
+ * Compile a concise type string descriptor into a Zod schema
13
+ * @param {string} typeName - Type name descriptor (e.g. 'number', 'string', 'email', 'string?')
14
+ * @returns {z.ZodTypeAny}
15
+ */
16
+ function descriptorToZod(typeName) {
17
+ const isOptional = typeName.endsWith('?');
18
+ const baseType = isOptional ? typeName.slice(0, -1).toLowerCase() : typeName.toLowerCase();
19
+
20
+ let schema;
21
+ switch (baseType) {
22
+ case 'string':
23
+ case 'text':
24
+ schema = z.string();
25
+ break;
26
+ case 'number':
27
+ case 'int':
28
+ case 'integer':
29
+ case 'float':
30
+ schema = z.number();
31
+ break;
32
+ case 'boolean':
33
+ case 'bool':
34
+ schema = z.boolean();
35
+ break;
36
+ case 'email':
37
+ schema = z.string().email();
38
+ break;
39
+ case 'uuid':
40
+ schema = z.string().uuid();
41
+ break;
42
+ case 'nanoid':
43
+ schema = zForServices.nanoid ? zForServices.nanoid() : z.string();
44
+ break;
45
+ case 'array':
46
+ schema = z.array(z.any());
47
+ break;
48
+ case 'object':
49
+ schema = z.record(z.any());
50
+ break;
51
+ case 'date':
52
+ schema = z.date().or(z.string().datetime()).or(z.string().refine(val => !Number.isNaN(Date.parse(val)), { message: 'Invalid date string' }));
53
+ break;
54
+ case 'function':
55
+ schema = z.function();
56
+ break;
57
+ case 'any':
58
+ default:
59
+ schema = z.any();
60
+ break;
61
+ }
62
+
63
+ return isOptional ? schema.optional() : schema;
64
+ }
65
+
66
+ /**
67
+ * Compile service schema definition into an executable validator
68
+ * @param {Function|Object|null} rawSchema - Schema definition
69
+ * @returns {Object|Function|null} Compiled schema
70
+ */
71
+ function compileServiceSchema(rawSchema) {
72
+ if (!rawSchema) return null;
73
+
74
+ // If already a Zod schema
75
+ if (rawSchema instanceof z.ZodType || typeof rawSchema.safeParse === 'function') {
76
+ return rawSchema;
77
+ }
78
+
79
+ // If function: check if ({ z }) schema compiler or custom validator function
80
+ if (typeof rawSchema === 'function') {
81
+ try {
82
+ const compiled = rawSchema({ z: zForServices });
83
+ if (compiled && (compiled instanceof z.ZodType || typeof compiled.safeParse === 'function')) {
84
+ return compiled;
85
+ }
86
+ } catch (e) {
87
+ // If invoking with { z } throws, treat as a direct validator function
88
+ }
89
+ return rawSchema;
90
+ }
91
+
92
+ // If object descriptor: { id: 'number', name: 'string', email: 'email' }
93
+ if (typeof rawSchema === 'object' && rawSchema !== null) {
94
+ const keys = Object.keys(rawSchema);
95
+ if (keys.length === 0) {
96
+ return null;
97
+ }
98
+ const shape = {};
99
+ for (const [key, val] of Object.entries(rawSchema)) {
100
+ if (typeof val === 'string') {
101
+ shape[key] = descriptorToZod(val);
102
+ } else if (val instanceof z.ZodType || (val && typeof val.safeParse === 'function')) {
103
+ shape[key] = val;
104
+ } else if (typeof val === 'object' && val !== null) {
105
+ const isOpt = val.optional === true || val.required === false;
106
+ let base = val.type ? descriptorToZod(String(val.type)) : z.any();
107
+ shape[key] = isOpt ? base.optional() : base;
108
+ } else if (typeof val === 'function') {
109
+ shape[key] = z.custom(val);
110
+ } else {
111
+ shape[key] = z.any();
112
+ }
113
+ }
114
+ return z.object(shape);
115
+ }
116
+
117
+ return rawSchema;
118
+ }
119
+
120
+ /**
121
+ * Format Zod validation errors into a clean string and fields map
122
+ * @param {z.ZodError} zodError
123
+ * @param {string} serviceName
124
+ * @returns {{ message: string, fields: Record<string, string> }}
125
+ */
126
+ function formatZodErrors(zodError, serviceName) {
127
+ const fields = {};
128
+ const lines = [];
129
+
130
+ for (const issue of zodError.issues || []) {
131
+ const pathKey = issue.path && issue.path.length > 0 ? issue.path.join('.') : '_root';
132
+ const fieldMsg = issue.message || 'invalid value';
133
+ fields[pathKey] = fieldMsg;
134
+ lines.push(`- ${pathKey}: ${fieldMsg}`);
135
+ }
136
+
137
+ const message = `Invalid service input for ${serviceName}:\n${lines.join('\n')}`;
138
+ return { message, fields };
139
+ }
140
+
141
+ /**
142
+ * Validate service input against compiled schema (supports sync & async Zod/custom schemas)
143
+ * @param {Object|Function|null} schema - Schema definition
144
+ * @param {*} input - Input to validate
145
+ * @param {string} serviceName - Service name
146
+ * @returns {*|Promise<*>} Validated input
147
+ * @throws {ValidationError} When validation fails
148
+ */
149
+ function validateServiceInput(schema, input, serviceName) {
150
+ const sanitized = sanitizeInput(input);
151
+
152
+ if (!schema) {
153
+ return sanitized;
154
+ }
155
+
156
+ const compiled = compileServiceSchema(schema);
157
+ if (!compiled) {
158
+ return sanitized;
159
+ }
160
+
161
+ // 1. Zod schema validation
162
+ if (compiled instanceof z.ZodType || typeof compiled.safeParse === 'function') {
163
+ try {
164
+ const result = compiled.safeParse(sanitized);
165
+ if (!result.success) {
166
+ const { message, fields } = formatZodErrors(result.error, serviceName);
167
+ throw new ValidationError(message, {
168
+ fields,
169
+ details: result.error.issues,
170
+ });
171
+ }
172
+ return result.data;
173
+ } catch (err) {
174
+ if (err instanceof ValidationError) {
175
+ throw err;
176
+ }
177
+ // If error occurred because of async refinements, evaluate with safeParseAsync
178
+ if (typeof compiled.safeParseAsync === 'function') {
179
+ return compiled.safeParseAsync(sanitized).then((asyncResult) => {
180
+ if (!asyncResult.success) {
181
+ const { message, fields } = formatZodErrors(asyncResult.error, serviceName);
182
+ throw new ValidationError(message, {
183
+ fields,
184
+ details: asyncResult.error.issues,
185
+ });
186
+ }
187
+ return asyncResult.data;
188
+ });
189
+ }
190
+ throw err;
191
+ }
192
+ }
193
+
194
+ // 2. Custom validation function (sync or async)
195
+ if (typeof compiled === 'function') {
196
+ try {
197
+ const res = compiled(sanitized);
198
+ if (res instanceof Promise) {
199
+ return res.then((asyncRes) => {
200
+ if (asyncRes === false) {
201
+ throw new ValidationError(`Invalid service input for ${serviceName}: custom validation rejected input`);
202
+ }
203
+ return typeof asyncRes === 'object' && asyncRes !== null ? asyncRes : sanitized;
204
+ });
205
+ }
206
+ if (res === false) {
207
+ throw new ValidationError(`Invalid service input for ${serviceName}: custom validation rejected input`);
208
+ }
209
+ return typeof res === 'object' && res !== null ? res : sanitized;
210
+ } catch (err) {
211
+ if (err instanceof ValidationError) {
212
+ throw err;
213
+ }
214
+ throw new ValidationError(`Invalid service input for ${serviceName}: ${err.message}`, {
215
+ cause: err,
216
+ });
217
+ }
218
+ }
219
+
220
+ return sanitized;
221
+ }
222
+
223
+ module.exports = {
224
+ z: zForServices,
225
+ compileServiceSchema,
226
+ descriptorToZod,
227
+ formatZodErrors,
228
+ sanitizeInput,
229
+ validateServiceInput,
230
+ };
@@ -4,8 +4,10 @@ Sections follow the source repo layout (`index.js`, `src/`, `core/orm`, `doc/ind
4
4
 
5
5
  ## 1. What this framework is
6
6
 
7
- - **SSR**: Express + **Nunjucks**; URLs map to files under `pages/`.
8
- - **API**: File-based handlers under `pages/api/` with method suffixes and optional **Zod** validation (`req.input`).
7
+ - **Architecture**: Modern full-stack Node.js framework with Next.js/Nuxt-style file-based routing.
8
+ - **SSR**: Native server-side rendering with **Nunjucks**; URLs map directly to files under `pages/`.
9
+ - **API**: 100% file-based handlers under `pages/api/` with HTTP method suffixes (`.get.js`, `.post.js`, `[id].delete.js`) and declarative **Zod** validation (`req.input`).
10
+ - ⚠️ **CRITICAL ANTI-PATTERN**: **NEVER** create a `routes/` or `src/routes/` directory. **NEVER** use `app.get()`, `app.post()`, or `express.Router()`. All application routes must live under `pages/` and `pages/api/`.
9
11
  - **i18n**: JSON locales; `t('key')` in templates.
10
12
  - **ORM**: Knex-backed layer in `core/orm` — `defineModel`, `zdb` schema helpers, repositories, query builder, migrations.
11
13
  - **Plugins**: Register in `createApp({ plugins })`; optional `db` passed as `ctx.db`.
@@ -60,7 +62,7 @@ project/
60
62
  | `pageAssets` | Opt-in **`true`** or **`{ enabled?, stylesheets?, scripts? }`**. When on, route **`load()`** may return reserved keys **`stylesheets`** (string or list; also `{ href, media? }` objects) and **`scripts`** (string, `{ src, defer?, async?, type? }`, or list). They are removed from the root Nunjucks context and passed as **`pageHead`** with **`pageAssets: true`**. The app layout must print them (see [`views/layout.njk`](../../../views/layout.njk) in the package). Default **off** — `stylesheets` / `scripts` in **`load()`** behave as normal data keys. |
61
63
  | `clientRuntime` | Opt-in **`{ alpine?: boolean \| object, swup?: boolean \| object }`**. Serves **`/__webspresso/client-runtime/*`** (Alpine 3, swup 4 + Head + Scripts plugins + bootstrap). Template context **`clientRuntime`**; include [`views/partials/webspresso-client-runtime.njk`](../../../views/partials/webspresso-client-runtime.njk) and set **`<main id="swup">`** when swup is on. Env overrides: **`WEBSPRESSO_ALPINE`**, **`WEBSPRESSO_SWUP`** (`1` or `true`). Admin / dev dashboard HTML is unchanged (Mithril). Use **`data-no-swup`** on links for full page loads. HTMX is not used. |
62
64
  | `auth` | `AuthManager` from **`createAuth()`** / **`quickAuth()`** (`webspresso/core/auth`). Mounts cookie parser + **`express-session`** + per-request **`authenticate`**; sets **`req.user`**, **`req.auth`**. Injects named route middleware **`auth`** and **`guest`** (overwrites same keys in `middlewares` if you passed both — avoid reusing those names for custom handlers). |
63
- | `setupRoutes(app, ctx)` | **Register custom Express routes here** — runs **after** file routes and plugins’ `onRoutesReady`, **before** 404. **`ctx.clientRuntime`** is the resolved flags. **`ctx.authMiddleware`** is set when `auth` was passed (guards: `requireAuth`, `requireGuest`, `requireCan`, `requireVerified`, …). Do not rely on `app.get` *after* `createApp` returns unless routes are appended before the 404 middleware (see [`src/server.js`](../../../src/server.js)). |
65
+ | `setupRoutes(app, ctx)` | Low-level server extension hook (e.g. mounting third-party sub-apps or raw middleware). ⚠️ **NEVER** use `setupRoutes` to define application domain or API routes. All application endpoints **MUST** live under `pages/` and `pages/api/`. |
64
66
 
65
67
  **Returns:** `{ app, nunjucksEnv, pluginManager, authMiddleware }` — `authMiddleware` is **`null`** when `auth` was not configured.
66
68
 
@@ -267,7 +269,7 @@ Pass **`db`** into **`createApp({ db })`** so **`ctx.db`** works in pages and pl
267
269
 
268
270
  ## 14. Pitfalls (for agents)
269
271
 
270
- 1. **Custom Express routes** — use **`setupRoutes`**, not only `app.use` after `createApp` returns.
272
+ 1. **NEVER use Express routes (`app.get` / `routes/`)** — Webspresso is 100% file-based. Never create `routes/` or call `app.get()`. All routes must live in `pages/` and `pages/api/`.
271
273
  2. **File-router order** — understand precedence vs manual routes.
272
274
  3. **`ctx.db`** — only when `createApp({ db })` is set.
273
275
  4. **ORM hidden fields** — never expose to clients unintentionally.