webspresso 0.0.39 → 0.0.41

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webspresso",
3
- "version": "0.0.39",
3
+ "version": "0.0.41",
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
  "bin": {
@@ -263,23 +263,23 @@ function createExtensionApiHandlers(options) {
263
263
  return res.status(404).json({ error: 'Model not found or not enabled' });
264
264
  }
265
265
 
266
- // Get field metadata
267
- const column = model.columns.get(field);
268
- if (!column) {
266
+ // Get field metadata - model.columns is Map<string, ColumnMeta>
267
+ const columnMeta = model.columns.get(field);
268
+ if (!columnMeta) {
269
269
  return res.status(400).json({ error: `Field "${field}" not found in model` });
270
270
  }
271
271
 
272
272
  // Validate field type - only allow enum and boolean
273
- const columnMeta = column._meta || {};
274
- const isEnum = columnMeta.enum && Array.isArray(columnMeta.enum);
275
- const isBoolean = columnMeta.type === 'boolean' || column._def?.typeName === 'ZodBoolean';
273
+ const enumValues = columnMeta.enumValues || columnMeta.enum;
274
+ const isEnum = enumValues && Array.isArray(enumValues);
275
+ const isBoolean = columnMeta.type === 'boolean';
276
276
 
277
277
  if (!isEnum && !isBoolean) {
278
278
  return res.status(400).json({ error: `Field "${field}" is not an enum or boolean type` });
279
279
  }
280
280
 
281
281
  // Validate value for enum fields
282
- if (isEnum && !columnMeta.enum.includes(value)) {
282
+ if (isEnum && !enumValues.includes(value)) {
283
283
  return res.status(400).json({ error: `Invalid value "${value}" for enum field "${field}"` });
284
284
  }
285
285
 
@@ -352,17 +352,19 @@ function createExtensionApiHandlers(options) {
352
352
 
353
353
  const bulkFields = [];
354
354
 
355
- for (const [fieldName, column] of model.columns.entries()) {
356
- const columnMeta = column._meta || {};
357
- const isEnum = columnMeta.enum && Array.isArray(columnMeta.enum);
358
- const isBoolean = columnMeta.type === 'boolean' || column._def?.typeName === 'ZodBoolean';
355
+ // model.columns is a Map<string, ColumnMeta> - values are already metadata objects
356
+ for (const [fieldName, columnMeta] of model.columns.entries()) {
357
+ // Check for enum - schema uses 'enumValues' property
358
+ const enumValues = columnMeta.enumValues || columnMeta.enum;
359
+ const isEnum = enumValues && Array.isArray(enumValues);
360
+ const isBoolean = columnMeta.type === 'boolean';
359
361
 
360
362
  if (isEnum) {
361
363
  bulkFields.push({
362
364
  name: fieldName,
363
365
  type: 'enum',
364
366
  label: columnMeta.label || fieldName,
365
- options: columnMeta.enum.map(v => ({ value: v, label: v })),
367
+ options: enumValues.map(v => ({ value: v, label: v })),
366
368
  });
367
369
  } else if (isBoolean) {
368
370
  bulkFields.push({
@@ -58,6 +58,12 @@ function adminPanelPlugin(options = {}) {
58
58
  name: 'admin-panel',
59
59
  version: '2.0.0',
60
60
  description: 'Modular admin panel for Webspresso with extensions support',
61
+
62
+ // CSP requirements for Quill.js rich text editor
63
+ csp: {
64
+ styleSrc: ['https://cdn.quilljs.com'],
65
+ scriptSrc: ['https://cdn.quilljs.com'],
66
+ },
61
67
  enabled,
62
68
  registry, // Expose registry for external configuration
63
69
 
@@ -122,6 +122,7 @@ class PluginManager {
122
122
  this.registeredFilters = new Map(); // name -> filter function
123
123
  this.routes = []; // Collected route metadata
124
124
  this.customRoutes = []; // Routes added by plugins
125
+ this.cspDirectives = new Map(); // directive -> Set of sources (from plugins)
125
126
  this.app = null;
126
127
  this.nunjucksEnv = null;
127
128
  }
@@ -263,6 +264,11 @@ class PluginManager {
263
264
  this.pluginAPIs.set(plugin.name, boundAPI);
264
265
  }
265
266
 
267
+ // Collect CSP requirements from plugin
268
+ if (plugin.csp) {
269
+ this._collectCspDirectives(plugin.csp);
270
+ }
271
+
266
272
  // Call register hook
267
273
  if (typeof plugin.register === 'function') {
268
274
  await plugin.register(ctx);
@@ -295,6 +301,11 @@ class PluginManager {
295
301
  this.pluginAPIs.set(plugin.name, boundAPI);
296
302
  }
297
303
 
304
+ // Collect CSP requirements from plugin
305
+ if (plugin.csp) {
306
+ this._collectCspDirectives(plugin.csp);
307
+ }
308
+
298
309
  // Call register hook (sync - if plugin has async register, it won't wait)
299
310
  if (typeof plugin.register === 'function') {
300
311
  plugin.register(ctx);
@@ -436,6 +447,35 @@ class PluginManager {
436
447
  }
437
448
  }
438
449
 
450
+ /**
451
+ * Collect CSP directives from a plugin
452
+ * @param {Object} csp - CSP directives object { styleSrc: [...], scriptSrc: [...], ... }
453
+ */
454
+ _collectCspDirectives(csp) {
455
+ for (const [directive, sources] of Object.entries(csp)) {
456
+ if (!this.cspDirectives.has(directive)) {
457
+ this.cspDirectives.set(directive, new Set());
458
+ }
459
+ const directiveSet = this.cspDirectives.get(directive);
460
+ const sourceArray = Array.isArray(sources) ? sources : [sources];
461
+ for (const source of sourceArray) {
462
+ directiveSet.add(source);
463
+ }
464
+ }
465
+ }
466
+
467
+ /**
468
+ * Get merged CSP directives from all plugins
469
+ * @returns {Object} CSP directives object to merge with helmet config
470
+ */
471
+ getCspDirectives() {
472
+ const directives = {};
473
+ for (const [directive, sources] of this.cspDirectives) {
474
+ directives[directive] = Array.from(sources);
475
+ }
476
+ return directives;
477
+ }
478
+
439
479
  /**
440
480
  * Get all registered helpers (to be merged with fsy)
441
481
  */
package/src/server.js CHANGED
@@ -203,10 +203,45 @@ function createApp(options = {}) {
203
203
  // Security headers with Helmet
204
204
  if (helmetConfig !== false) {
205
205
  const defaultConfig = getDefaultHelmetConfig(isDev);
206
- const finalConfig = helmetConfig === undefined || helmetConfig === true
206
+ let finalConfig = helmetConfig === undefined || helmetConfig === true
207
207
  ? defaultConfig
208
208
  : { ...defaultConfig, ...helmetConfig };
209
209
 
210
+ // Collect CSP requirements from plugins (before they're fully registered)
211
+ if (plugins && Array.isArray(plugins) && finalConfig.contentSecurityPolicy) {
212
+ const pluginCspSources = {
213
+ styleSrc: new Set(),
214
+ scriptSrc: new Set(),
215
+ imgSrc: new Set(),
216
+ fontSrc: new Set(),
217
+ connectSrc: new Set(),
218
+ frameSrc: new Set(),
219
+ };
220
+
221
+ for (const plugin of plugins) {
222
+ if (plugin && plugin.csp) {
223
+ for (const [directive, sources] of Object.entries(plugin.csp)) {
224
+ if (pluginCspSources[directive]) {
225
+ const sourceArray = Array.isArray(sources) ? sources : [sources];
226
+ for (const source of sourceArray) {
227
+ pluginCspSources[directive].add(source);
228
+ }
229
+ }
230
+ }
231
+ }
232
+ }
233
+
234
+ // Merge plugin CSP sources with default config
235
+ if (finalConfig.contentSecurityPolicy && finalConfig.contentSecurityPolicy.directives) {
236
+ const directives = finalConfig.contentSecurityPolicy.directives;
237
+ for (const [directive, sources] of Object.entries(pluginCspSources)) {
238
+ if (sources.size > 0 && directives[directive]) {
239
+ directives[directive] = [...directives[directive], ...Array.from(sources)];
240
+ }
241
+ }
242
+ }
243
+ }
244
+
210
245
  app.use(helmet(finalConfig));
211
246
  }
212
247