webspresso 0.0.102 → 0.0.104

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 (91) hide show
  1. package/.agents/ORM.md +13 -4
  2. package/.agents/skills/webspresso-orm-and-database/SKILL.md +4 -5
  3. package/README.md +2 -2
  4. package/bin/commands/dev.js +3 -2
  5. package/bin/utils/orm-map-html.js +2 -8
  6. package/core/auth/jwt.js +12 -21
  7. package/core/auth/oauth.js +38 -14
  8. package/core/auth/totp.js +3 -15
  9. package/core/auth/user-agent.js +59 -60
  10. package/core/compression/compression-stream.js +2 -2
  11. package/core/compression/middleware.js +1 -1
  12. package/core/compression/negotiate.js +11 -4
  13. package/core/content/renderer.js +1 -13
  14. package/core/errors/middleware.js +2 -8
  15. package/core/events/dispatcher.js +56 -33
  16. package/core/forms/form-builder.js +1 -9
  17. package/core/openapi/orm-components.js +2 -3
  18. package/core/orm/cache/listeners.js +12 -2
  19. package/core/orm/index.js +5 -0
  20. package/core/orm/json-fields.js +7 -1
  21. package/core/orm/model.js +99 -8
  22. package/core/orm/query-builder.js +71 -25
  23. package/core/orm/query-filters.js +393 -0
  24. package/core/orm/repository.js +42 -17
  25. package/core/orm/schema-helpers.js +49 -0
  26. package/core/orm/scopes.js +40 -9
  27. package/core/orm/types.js +12 -1
  28. package/core/queue/manager.js +5 -0
  29. package/core/shutdown/shutdown-manager.js +16 -16
  30. package/core/utils/crypto.js +73 -0
  31. package/core/utils/escape-html.js +7 -0
  32. package/core/utils/fetch.js +120 -0
  33. package/core/utils/get-client-ip.js +6 -0
  34. package/core/utils/html.js +72 -0
  35. package/core/utils/ip.js +86 -0
  36. package/core/utils/safe-compare.js +3 -0
  37. package/core/validation/index.js +9 -3
  38. package/index.d.ts +33 -6
  39. package/index.js +10 -0
  40. package/package.json +2 -1
  41. package/plugins/admin-panel/auth.js +15 -7
  42. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +82 -10
  43. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +4 -2
  44. package/plugins/admin-panel/client/parts/08-record-list.js +3 -2
  45. package/plugins/admin-panel/client/parts/10-export-registry.js +6 -0
  46. package/plugins/admin-panel/core/api-extensions.js +1 -75
  47. package/plugins/admin-panel/lib/login-rate-limiter.js +15 -13
  48. package/plugins/api-key/crypto.js +1 -24
  49. package/plugins/audit-log/api-handlers.js +11 -1
  50. package/plugins/audit-log/middleware.js +50 -6
  51. package/plugins/basic-auth/index.js +2 -26
  52. package/plugins/cors.js +2 -2
  53. package/plugins/csrf/index.js +3 -15
  54. package/plugins/email/service.js +32 -1
  55. package/plugins/file-manager/admin-component.js +3 -1
  56. package/plugins/file-manager/api-handlers.js +10 -17
  57. package/plugins/file-manager/core.js +34 -4
  58. package/plugins/file-manager/index.js +11 -9
  59. package/plugins/file-manager/security.js +18 -6
  60. package/plugins/mcp/auth.js +1 -13
  61. package/plugins/mcp/index.js +23 -1
  62. package/plugins/mcp/transports/sse.js +10 -1
  63. package/plugins/orm-cache-admin/admin-component.js +3 -2
  64. package/plugins/polar/src/api.js +9 -2
  65. package/plugins/polar/src/webhooks.js +3 -8
  66. package/plugins/recaptcha/helpers.js +1 -7
  67. package/plugins/recaptcha/verify.js +37 -23
  68. package/plugins/redirect/index.js +51 -3
  69. package/plugins/rest-resources/index.js +1 -207
  70. package/plugins/schema-explorer/admin-component.js +4 -2
  71. package/plugins/schema-explorer.js +40 -29
  72. package/plugins/seo-checker/index.js +5 -2
  73. package/plugins/site-analytics/bot-patterns.js +3 -0
  74. package/plugins/site-analytics/client-error-handler.js +3 -1
  75. package/plugins/site-analytics/index.js +10 -1
  76. package/plugins/site-analytics/tracking.js +265 -63
  77. package/plugins/sitemap.js +18 -15
  78. package/plugins/swagger.js +9 -14
  79. package/plugins/webhooks/crypto.js +1 -18
  80. package/plugins/webhooks/dispatcher.js +6 -1
  81. package/plugins/webhooks/matcher.js +25 -10
  82. package/plugins/webhooks/sender.js +32 -2
  83. package/src/api/api-loader.js +1 -1
  84. package/src/dev-reload/index.js +47 -6
  85. package/src/discovery/scan-routes.js +14 -4
  86. package/src/file-router.js +62 -6
  87. package/src/helpers.js +157 -32
  88. package/src/plugin-manager.js +34 -13
  89. package/src/routing/route-table.js +14 -4
  90. package/src/server.js +25 -9
  91. package/src/services/validator.js +57 -32
@@ -8,8 +8,7 @@ const { getAllModels } = require('../../core/orm/model');
8
8
  const { omit } = require('../../core/orm/utils');
9
9
  const { trimUrlPathSlashes } = require('../../core/url-path-normalize');
10
10
  const { validateQueryComplexity } = require('../../core/orm/complexity');
11
-
12
- const RESERVED_QUERY_KEYS = new Set(['page', 'perPage', 'sort', 'order', 'include', 'trashed']);
11
+ const { applyColumnFilters, RESERVED_QUERY_KEYS } = require('../../core/orm/query-filters');
13
12
 
14
13
  /**
15
14
  * Pluralize a PascalCase model name to a URL segment (e.g. User -> users, Company -> companies)
@@ -234,212 +233,7 @@ function parseSortParams(model, rawSort, rawOrder) {
234
233
  return result;
235
234
  }
236
235
 
237
- function applyColumnFilters(query, countQuery, model, req) {
238
- let q = query;
239
- let c = countQuery;
240
-
241
- function applyCondition(col, op, rawVal) {
242
- if (!model.columns.has(col)) return;
243
- const meta = model.columns.get(col);
244
- const colType = meta?.type;
245
-
246
- let operator = String(op || 'eq').toLowerCase();
247
- let val = rawVal;
248
-
249
- // Normalize booleans
250
- if (colType === 'boolean' && typeof val === 'string') {
251
- if (val === 'true' || val === '1') val = true;
252
- else if (val === 'false' || val === '0') val = false;
253
- }
254
-
255
- switch (operator) {
256
- case 'eq':
257
- case '=':
258
- if (val !== undefined && val !== '') {
259
- q = q.where(col, '=', val);
260
- c = c.where(col, '=', val);
261
- }
262
- break;
263
-
264
- case 'ne':
265
- case '!=':
266
- case '<>':
267
- if (val !== undefined && val !== '') {
268
- q = q.where(col, '!=', val);
269
- c = c.where(col, '!=', val);
270
- }
271
- break;
272
-
273
- case 'gt':
274
- case '>':
275
- if (val !== undefined && val !== '') {
276
- q = q.where(col, '>', val);
277
- c = c.where(col, '>', val);
278
- }
279
- break;
280
-
281
- case 'gte':
282
- case '>=':
283
- if (val !== undefined && val !== '') {
284
- q = q.where(col, '>=', val);
285
- c = c.where(col, '>=', val);
286
- }
287
- break;
288
-
289
- case 'lt':
290
- case '<':
291
- if (val !== undefined && val !== '') {
292
- q = q.where(col, '<', val);
293
- c = c.where(col, '<', val);
294
- }
295
- break;
296
-
297
- case 'lte':
298
- case '<=':
299
- if (val !== undefined && val !== '') {
300
- q = q.where(col, '<=', val);
301
- c = c.where(col, '<=', val);
302
- }
303
- break;
304
-
305
- case 'in': {
306
- const arr = Array.isArray(val)
307
- ? val
308
- : (typeof val === 'string' ? val.split(',').map(s => s.trim()).filter(Boolean) : [val]);
309
- if (arr.length > 0) {
310
- q = q.whereIn(col, arr);
311
- c = c.whereIn(col, arr);
312
- }
313
- break;
314
- }
315
-
316
- case 'nin':
317
- case 'notin': {
318
- const arr = Array.isArray(val)
319
- ? val
320
- : (typeof val === 'string' ? val.split(',').map(s => s.trim()).filter(Boolean) : [val]);
321
- if (arr.length > 0) {
322
- q = q.whereNotIn(col, arr);
323
- c = c.whereNotIn(col, arr);
324
- }
325
- break;
326
- }
327
-
328
- case 'like':
329
- if (val !== undefined && val !== '') {
330
- q = q.where(col, 'like', String(val));
331
- c = c.where(col, 'like', String(val));
332
- }
333
- break;
334
-
335
- case 'ilike':
336
- if (val !== undefined && val !== '') {
337
- q = q.where(col, 'ilike', String(val));
338
- c = c.where(col, 'ilike', String(val));
339
- }
340
- break;
341
-
342
- case 'contains':
343
- if (val !== undefined && val !== '') {
344
- q = q.where(col, 'like', `%${String(val)}%`);
345
- c = c.where(col, 'like', `%${String(val)}%`);
346
- }
347
- break;
348
-
349
- case 'startswith':
350
- if (val !== undefined && val !== '') {
351
- q = q.where(col, 'like', `${String(val)}%`);
352
- c = c.where(col, 'like', `${String(val)}%`);
353
- }
354
- break;
355
-
356
- case 'endswith':
357
- if (val !== undefined && val !== '') {
358
- q = q.where(col, 'like', `%${String(val)}`);
359
- c = c.where(col, 'like', `%${String(val)}`);
360
- }
361
- break;
362
-
363
- case 'isnull':
364
- case 'is_null': {
365
- const isTrue = val === true || val === 'true' || val === '1' || val === 1 || val === '';
366
- if (isTrue) {
367
- q = q.whereNull(col);
368
- c = c.whereNull(col);
369
- }
370
- break;
371
- }
372
-
373
- case 'isnotnull':
374
- case 'is_not_null': {
375
- const isTrue = val === true || val === 'true' || val === '1' || val === 1 || val === '';
376
- if (isTrue) {
377
- q = q.whereNotNull(col);
378
- c = c.whereNotNull(col);
379
- }
380
- break;
381
- }
382
-
383
- case 'between': {
384
- let parts = [];
385
- if (Array.isArray(val)) {
386
- parts = val;
387
- } else if (typeof val === 'string') {
388
- parts = val.split(',').map(s => s.trim());
389
- }
390
- if (parts.length === 2 && parts[0] !== '' && parts[1] !== '') {
391
- q = q.where(col, '>=', parts[0]).where(col, '<=', parts[1]);
392
- c = c.where(col, '>=', parts[0]).where(col, '<=', parts[1]);
393
- }
394
- break;
395
- }
396
-
397
- default:
398
- if (val !== undefined && val !== '') {
399
- q = q.where(col, '=', val);
400
- c = c.where(col, '=', val);
401
- }
402
- break;
403
- }
404
- }
405
-
406
- // 1. Process dedicated filter object ?filter[col]=... or ?filter[col][op]=...
407
- if (req.query.filter && typeof req.query.filter === 'object') {
408
- for (const [col, colFilter] of Object.entries(req.query.filter)) {
409
- if (!model.columns.has(col)) continue;
410
- if (colFilter && typeof colFilter === 'object' && !Array.isArray(colFilter)) {
411
- if (colFilter.op !== undefined && colFilter.value !== undefined) {
412
- applyCondition(col, colFilter.op, colFilter.value);
413
- } else {
414
- for (const [op, val] of Object.entries(colFilter)) {
415
- applyCondition(col, op, val);
416
- }
417
- }
418
- } else if (colFilter !== undefined && colFilter !== '') {
419
- applyCondition(col, 'eq', colFilter);
420
- }
421
- }
422
- }
423
236
 
424
- // 2. Process top-level query parameters ?price[gte]=100 or ?status=active
425
- for (const [key, value] of Object.entries(req.query)) {
426
- if (RESERVED_QUERY_KEYS.has(key) || key === 'filter') {
427
- continue;
428
- }
429
- if (!model.columns.has(key)) {
430
- continue;
431
- }
432
- if (value && typeof value === 'object' && !Array.isArray(value)) {
433
- for (const [op, val] of Object.entries(value)) {
434
- applyCondition(key, op, val);
435
- }
436
- } else if (value !== undefined && value !== '') {
437
- applyCondition(key, 'eq', value);
438
- }
439
- }
440
-
441
- return { query: q, countQuery: c };
442
- }
443
237
 
444
238
  /**
445
239
  * @param {Object} options
@@ -20,10 +20,12 @@ function generateSchemaExplorerAdminComponent(options = {}) {
20
20
  function getSchemaApi(method, path) {
21
21
  var adminPath = window.__ADMIN_PATH__ || '/_admin';
22
22
  var url = adminPath + '/api${apiPrefix}' + (path || '');
23
- return fetch(url, {
23
+ var fetchFn = (typeof window !== 'undefined' && window.Admin && window.Admin.fetch) ? window.Admin.fetch : fetch;
24
+ return fetchFn(url, {
24
25
  method: method || 'GET',
25
26
  credentials: 'include',
26
- headers: { 'Accept': 'application/json' }
27
+ headers: { 'Accept': 'application/json' },
28
+ timeout: 30000,
27
29
  }).then(function(res) {
28
30
  if (!res.ok) {
29
31
  return res.json().catch(function() { return { error: res.statusText }; }).then(function(err) {
@@ -59,6 +59,25 @@ function schemaExplorerPlugin(options = {}) {
59
59
 
60
60
  const pageTitle = title || adminTitle || menuLabel;
61
61
 
62
+ let cachedGraph = null;
63
+ let cachedGraphTime = 0;
64
+ const GRAPH_CACHE_TTL = 5000; // 5 seconds in dev, infinite in prod
65
+
66
+ function getEnrichedGraph() {
67
+ const isProd = process.env.NODE_ENV === 'production';
68
+ const now = Date.now();
69
+ if (cachedGraph && (isProd || (now - cachedGraphTime) < GRAPH_CACHE_TTL)) {
70
+ return cachedGraph;
71
+ }
72
+ const rawModels = serializeAllModels({ exclude, includeColumns, includeRelations, includeScopes });
73
+ const relationships = extractAllRelationships(rawModels);
74
+ const enriched = enrichModelsWithGraph(rawModels, relationships);
75
+ const mermaid = generateMermaidErDiagram(rawModels, relationships);
76
+ cachedGraph = { rawModels, relationships, enriched, mermaid };
77
+ cachedGraphTime = now;
78
+ return cachedGraph;
79
+ }
80
+
62
81
  return {
63
82
  name: 'schema-explorer',
64
83
  version: '1.0.0',
@@ -66,6 +85,14 @@ function schemaExplorerPlugin(options = {}) {
66
85
 
67
86
  // Expose API for other plugins & programmatic consumption
68
87
  api: {
88
+ /**
89
+ * Invalidate schema explorer cache
90
+ */
91
+ invalidateCache() {
92
+ cachedGraph = null;
93
+ cachedGraphTime = 0;
94
+ },
95
+
69
96
  /**
70
97
  * Get all models metadata
71
98
  * @returns {Object[]}
@@ -79,9 +106,7 @@ function schemaExplorerPlugin(options = {}) {
79
106
  * @returns {Object[]}
80
107
  */
81
108
  getEnrichedModels() {
82
- const raw = serializeAllModels({ exclude, includeColumns, includeRelations, includeScopes });
83
- const rels = extractAllRelationships(raw);
84
- return enrichModelsWithGraph(raw, rels);
109
+ return getEnrichedGraph().enriched;
85
110
  },
86
111
 
87
112
  /**
@@ -109,8 +134,7 @@ function schemaExplorerPlugin(options = {}) {
109
134
  * @returns {Object[]}
110
135
  */
111
136
  getRelationships() {
112
- const raw = serializeAllModels({ exclude, includeColumns, includeRelations, includeScopes });
113
- return extractAllRelationships(raw);
137
+ return getEnrichedGraph().relationships;
114
138
  },
115
139
 
116
140
  /**
@@ -118,9 +142,7 @@ function schemaExplorerPlugin(options = {}) {
118
142
  * @returns {string}
119
143
  */
120
144
  getMermaid() {
121
- const raw = serializeAllModels({ exclude, includeColumns, includeRelations, includeScopes });
122
- const rels = extractAllRelationships(raw);
123
- return generateMermaidErDiagram(raw, rels);
145
+ return getEnrichedGraph().mermaid;
124
146
  },
125
147
 
126
148
  extractAllRelationships,
@@ -250,21 +272,17 @@ function schemaExplorerPlugin(options = {}) {
250
272
  method: 'get',
251
273
  path: '',
252
274
  handler: (req, res) => {
253
- const rawModels = serializeAllModels({ exclude, includeColumns, includeRelations, includeScopes });
254
- const relationships = extractAllRelationships(rawModels);
255
- const models = enrichModelsWithGraph(rawModels, relationships);
256
- const mermaid = generateMermaidErDiagram(rawModels, relationships);
257
-
275
+ const graph = getEnrichedGraph();
258
276
  res.json({
259
277
  meta: {
260
278
  version: '1.0.0',
261
279
  generatedAt: new Date().toISOString(),
262
- modelCount: models.length,
263
- relationCount: relationships.length,
280
+ modelCount: graph.enriched.length,
281
+ relationCount: graph.relationships.length,
264
282
  },
265
- models,
266
- relationships,
267
- mermaid,
283
+ models: graph.enriched,
284
+ relationships: graph.relationships,
285
+ mermaid: graph.mermaid,
268
286
  });
269
287
  },
270
288
  },
@@ -272,19 +290,14 @@ function schemaExplorerPlugin(options = {}) {
272
290
  method: 'get',
273
291
  path: '/relations',
274
292
  handler: (req, res) => {
275
- const rawModels = serializeAllModels({ exclude, includeColumns, includeRelations, includeScopes });
276
- const relationships = extractAllRelationships(rawModels);
277
- res.json({ relationships });
293
+ res.json({ relationships: getEnrichedGraph().relationships });
278
294
  },
279
295
  },
280
296
  {
281
297
  method: 'get',
282
298
  path: '/mermaid',
283
299
  handler: (req, res) => {
284
- const rawModels = serializeAllModels({ exclude, includeColumns, includeRelations, includeScopes });
285
- const relationships = extractAllRelationships(rawModels);
286
- const mermaid = generateMermaidErDiagram(rawModels, relationships);
287
- res.json({ mermaid });
300
+ res.json({ mermaid: getEnrichedGraph().mermaid });
288
301
  },
289
302
  },
290
303
  {
@@ -298,10 +311,8 @@ function schemaExplorerPlugin(options = {}) {
298
311
  return res.status(404).json({ error: `Model "${modelName}" not found` });
299
312
  }
300
313
 
301
- const rawModels = serializeAllModels({ exclude, includeColumns, includeRelations, includeScopes });
302
- const relationships = extractAllRelationships(rawModels);
303
- const models = enrichModelsWithGraph(rawModels, relationships);
304
- const found = models.find(m => m.name === modelName);
314
+ const graph = getEnrichedGraph();
315
+ const found = graph.enriched.find(m => m.name === modelName);
305
316
 
306
317
  res.json({ model: found });
307
318
  },
@@ -50,12 +50,15 @@ function seoCheckerPlugin(options = {}) {
50
50
  // Inject panel styles into head
51
51
  ctx.injectHead(generatePanelStyles(), { priority: 100 });
52
52
 
53
+ const safeSettings = JSON.stringify(mergedSettings).replace(/</g, '\\u003c').replace(/>/g, '\\u003e');
54
+ const safeChecks = JSON.stringify(checkDefinitions).replace(/</g, '\\u003c').replace(/>/g, '\\u003e');
55
+
53
56
  // Inject panel HTML and analyzer script into body
54
57
  ctx.injectBody(`
55
58
  ${generatePanelHtml(checkDefinitions)}
56
59
  <script>
57
- window.__SEO_CHECKER_SETTINGS__ = ${JSON.stringify(mergedSettings)};
58
- window.__SEO_CHECKER_CHECKS__ = ${JSON.stringify(checkDefinitions)};
60
+ window.__SEO_CHECKER_SETTINGS__ = ${safeSettings};
61
+ window.__SEO_CHECKER_CHECKS__ = ${safeChecks};
59
62
  </script>
60
63
  <script>${analyzerScript}</script>
61
64
  `);
@@ -27,9 +27,12 @@ const BOT_PATTERNS = [
27
27
  { pattern: /petalbot/i, name: 'PetalBot' },
28
28
  { pattern: /gptbot/i, name: 'GPTBot' },
29
29
  { pattern: /claudebot/i, name: 'ClaudeBot' },
30
+ { pattern: /amazonbot/i, name: 'Amazon' },
30
31
  { pattern: /anthropic/i, name: 'Anthropic' },
31
32
  { pattern: /ccbot/i, name: 'CCBot' },
32
33
  { pattern: /bytespider/i, name: 'ByteSpider' },
34
+ { pattern: /dataforseo/i, name: 'DataForSEO' },
35
+ { pattern: /yisouspider/i, name: 'Yisou' },
33
36
  { pattern: /open-research-bot/i, name: 'open-research-bot' },
34
37
  { pattern: /ptb12/i, name: 'PTB12' },
35
38
  { pattern: /curl\//i, name: 'curl' },
@@ -4,6 +4,8 @@
4
4
  * @module plugins/site-analytics/client-error-handler
5
5
  */
6
6
 
7
+ const { formatDbError } = require('./tracking');
8
+
7
9
  const DEFAULT_TABLE = 'analytics_client_errors';
8
10
 
9
11
  /**
@@ -71,7 +73,7 @@ function createErrorReportHandler(options) {
71
73
 
72
74
  res.status(202).json({ ok: true });
73
75
  } catch (e) {
74
- console.error('[site-analytics] Client error report failed:', e.message);
76
+ console.error('[site-analytics] Client error report failed:', formatDbError(e));
75
77
  res.status(500).json({ error: 'Report failed' });
76
78
  }
77
79
  };
@@ -140,6 +140,8 @@ function siteAnalyticsPlugin(options = {}) {
140
140
  }
141
141
  }
142
142
 
143
+ let trackingMiddleware = null;
144
+
143
145
  return {
144
146
  name: 'site-analytics',
145
147
  version: '1.0.0',
@@ -177,13 +179,17 @@ function siteAnalyticsPlugin(options = {}) {
177
179
  register(ctx) {
178
180
  const knex = db.knex || db;
179
181
 
180
- const trackingMiddleware = createTrackingMiddleware({
182
+ trackingMiddleware = createTrackingMiddleware({
181
183
  knex,
182
184
  excludePaths,
183
185
  trackBots,
184
186
  tableName,
185
187
  batchSize: options.batchSize ?? 20,
186
188
  flushIntervalMs: options.flushIntervalMs ?? 3000,
189
+ maxRetries: options.maxRetries ?? 3,
190
+ maxBackoffMs: options.maxBackoffMs ?? 60000,
191
+ maxQueueSize: options.maxQueueSize ?? 5000,
192
+ errorLogIntervalMs: options.errorLogIntervalMs ?? 60000,
187
193
  });
188
194
 
189
195
  ctx.app.use(trackingMiddleware);
@@ -199,15 +205,18 @@ function siteAnalyticsPlugin(options = {}) {
199
205
 
200
206
  return () => {
201
207
  stopCleanup();
208
+ if (trackingMiddleware && trackingMiddleware.stop) trackingMiddleware.stop();
202
209
  };
203
210
  },
204
211
 
205
212
  dispose() {
206
213
  stopCleanup();
214
+ if (trackingMiddleware && trackingMiddleware.stop) trackingMiddleware.stop();
207
215
  },
208
216
 
209
217
  shutdown() {
210
218
  stopCleanup();
219
+ if (trackingMiddleware && trackingMiddleware.stop) trackingMiddleware.stop();
211
220
  },
212
221
 
213
222
  onRoutesReady(ctx) {