webspresso 0.0.103 → 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 (60) hide show
  1. package/bin/commands/dev.js +3 -2
  2. package/bin/utils/orm-map-html.js +2 -8
  3. package/core/auth/jwt.js +2 -4
  4. package/core/auth/oauth.js +38 -14
  5. package/core/auth/totp.js +3 -15
  6. package/core/auth/user-agent.js +1 -21
  7. package/core/content/renderer.js +1 -13
  8. package/core/errors/middleware.js +2 -8
  9. package/core/forms/form-builder.js +1 -9
  10. package/core/orm/index.js +5 -0
  11. package/core/orm/query-filters.js +393 -0
  12. package/core/shutdown/shutdown-manager.js +16 -16
  13. package/core/utils/crypto.js +73 -0
  14. package/core/utils/escape-html.js +7 -0
  15. package/core/utils/fetch.js +120 -0
  16. package/core/utils/get-client-ip.js +6 -0
  17. package/core/utils/html.js +72 -0
  18. package/core/utils/ip.js +86 -0
  19. package/core/utils/safe-compare.js +3 -0
  20. package/index.d.ts +11 -1
  21. package/index.js +10 -0
  22. package/package.json +1 -1
  23. package/plugins/admin-panel/auth.js +15 -7
  24. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +82 -10
  25. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +4 -2
  26. package/plugins/admin-panel/client/parts/08-record-list.js +3 -2
  27. package/plugins/admin-panel/client/parts/10-export-registry.js +6 -0
  28. package/plugins/admin-panel/core/api-extensions.js +1 -75
  29. package/plugins/admin-panel/lib/login-rate-limiter.js +15 -13
  30. package/plugins/api-key/crypto.js +1 -24
  31. package/plugins/audit-log/api-handlers.js +11 -1
  32. package/plugins/audit-log/middleware.js +11 -2
  33. package/plugins/basic-auth/index.js +2 -26
  34. package/plugins/cors.js +2 -2
  35. package/plugins/csrf/index.js +3 -15
  36. package/plugins/file-manager/admin-component.js +3 -1
  37. package/plugins/file-manager/core.js +34 -4
  38. package/plugins/mcp/auth.js +1 -13
  39. package/plugins/mcp/index.js +23 -1
  40. package/plugins/mcp/transports/sse.js +10 -1
  41. package/plugins/orm-cache-admin/admin-component.js +3 -2
  42. package/plugins/polar/src/api.js +9 -2
  43. package/plugins/polar/src/webhooks.js +3 -8
  44. package/plugins/recaptcha/helpers.js +1 -7
  45. package/plugins/recaptcha/verify.js +37 -23
  46. package/plugins/redirect/index.js +51 -3
  47. package/plugins/rest-resources/index.js +1 -207
  48. package/plugins/schema-explorer/admin-component.js +4 -2
  49. package/plugins/schema-explorer.js +40 -29
  50. package/plugins/seo-checker/index.js +5 -2
  51. package/plugins/site-analytics/tracking.js +19 -16
  52. package/plugins/sitemap.js +18 -15
  53. package/plugins/swagger.js +9 -14
  54. package/plugins/webhooks/crypto.js +1 -18
  55. package/plugins/webhooks/dispatcher.js +6 -1
  56. package/plugins/webhooks/matcher.js +25 -10
  57. package/plugins/webhooks/sender.js +32 -2
  58. package/src/dev-reload/index.js +47 -6
  59. package/src/helpers.js +21 -9
  60. package/src/server.js +2 -2
@@ -48,6 +48,25 @@ function normalizeEvents(events) {
48
48
  return ['*'];
49
49
  }
50
50
 
51
+ const regexCache = new Map();
52
+ const MAX_REGEX_CACHE = 1000;
53
+
54
+ function getPatternRegex(pattern) {
55
+ let cached = regexCache.get(pattern);
56
+ if (!cached) {
57
+ if (regexCache.size >= MAX_REGEX_CACHE) {
58
+ const first = regexCache.keys().next().value;
59
+ if (first !== undefined) regexCache.delete(first);
60
+ }
61
+ const escaped = pattern
62
+ .replace(/[.+?^${}()|[\]\\]/g, '\\$&') // escape regex specials except *
63
+ .replace(/\*/g, '.*');
64
+ cached = new RegExp(`^${escaped}$`, 'i');
65
+ regexCache.set(pattern, cached);
66
+ }
67
+ return cached;
68
+ }
69
+
51
70
  /**
52
71
  * Test whether an event pattern matches an actual event name
53
72
  *
@@ -56,12 +75,15 @@ function normalizeEvents(events) {
56
75
  * @returns {boolean}
57
76
  */
58
77
  function matchEvent(pattern, eventName) {
59
- if (!pattern || !eventName) {
78
+ if (typeof pattern !== 'string' || typeof eventName !== 'string') {
60
79
  return false;
61
80
  }
62
81
 
63
82
  const p = pattern.trim();
64
83
  const e = eventName.trim();
84
+ if (!p || !e) {
85
+ return false;
86
+ }
65
87
 
66
88
  // 1. Wildcard matches all
67
89
  if (p === '*') {
@@ -73,15 +95,8 @@ function matchEvent(pattern, eventName) {
73
95
  return true;
74
96
  }
75
97
 
76
- // 3. Glob / Wildcard conversion
77
- // Convert 'user.*' to regular expression '^user\.[^.]+$' or '^user\..+$'
78
- // Support prefix, middle, or suffix wildcards safely
79
- const escaped = p
80
- .replace(/[.+?^${}()|[\]\\]/g, '\\$&') // escape regex specials except *
81
- .replace(/\*/g, '.*');
82
-
83
- const regex = new RegExp(`^${escaped}$`, 'i');
84
- return regex.test(e);
98
+ // 3. Glob / Wildcard conversion with bounded cache
99
+ return getPatternRegex(p).test(e);
85
100
  }
86
101
 
87
102
  /**
@@ -32,7 +32,8 @@ const { signPayload } = require('./crypto');
32
32
  * timestamp: number
33
33
  * }>}
34
34
  */
35
- async function sendWebhook(options) {
35
+ async function sendWebhook(options = {}) {
36
+ const opts = options || {};
36
37
  const {
37
38
  url,
38
39
  event,
@@ -43,7 +44,7 @@ async function sendWebhook(options) {
43
44
  timeoutMs = 10000,
44
45
  wrapPayload = true,
45
46
  maxResponseBodyLength = 2048,
46
- } = options;
47
+ } = opts;
47
48
 
48
49
  if (!url || typeof url !== 'string') {
49
50
  return {
@@ -57,6 +58,32 @@ async function sendWebhook(options) {
57
58
  };
58
59
  }
59
60
 
61
+ let parsedUrl;
62
+ try {
63
+ parsedUrl = new URL(url);
64
+ if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
65
+ return {
66
+ success: false,
67
+ statusCode: 0,
68
+ responseBody: '',
69
+ durationMs: 0,
70
+ error: `Invalid webhook URL protocol: "${parsedUrl.protocol}". Only http: and https: are supported.`,
71
+ deliveryId,
72
+ timestamp: Math.floor(Date.now() / 1000),
73
+ };
74
+ }
75
+ } catch (err) {
76
+ return {
77
+ success: false,
78
+ statusCode: 0,
79
+ responseBody: '',
80
+ durationMs: 0,
81
+ error: 'Invalid webhook URL: destination URL is malformed.',
82
+ deliveryId,
83
+ timestamp: Math.floor(Date.now() / 1000),
84
+ };
85
+ }
86
+
60
87
  const timestamp = Math.floor(Date.now() / 1000);
61
88
 
62
89
  // Construct request body
@@ -116,6 +143,9 @@ async function sendWebhook(options) {
116
143
  timeoutId = setTimeout(() => {
117
144
  controller.abort();
118
145
  }, timeoutMs);
146
+ if (timeoutId && timeoutId.unref) {
147
+ timeoutId.unref();
148
+ }
119
149
 
120
150
  const response = await fetch(url, {
121
151
  method: 'POST',
@@ -15,6 +15,8 @@ const CLIENT_RUNTIME_SCRIPT = `(function() {
15
15
 
16
16
  var es = null;
17
17
  var reconnectTimer = null;
18
+ var wasConnected = false;
19
+ var reconnectDelay = 200;
18
20
 
19
21
  function reloadCss(targetFile) {
20
22
  var links = document.querySelectorAll('link[rel="stylesheet"]');
@@ -166,6 +168,36 @@ const CLIENT_RUNTIME_SCRIPT = `(function() {
166
168
  document.addEventListener('keydown', onKeyDown);
167
169
  }
168
170
 
171
+ function safeReload(reason) {
172
+ clearOverlay();
173
+ var reasonMsg = reason ? ' (' + reason + ')' : '';
174
+ console.log('[Webspresso] 🔄 Reloading page' + reasonMsg + '...');
175
+
176
+ // Ping server with lightweight HEAD request to make sure it is ready before full reload.
177
+ // This prevents browser "This site can't be reached" (ERR_CONNECTION_REFUSED) screens
178
+ // when the dev server is in the middle of restarting.
179
+ function pingAndReload() {
180
+ if (typeof window.fetch !== 'function') {
181
+ window.location.reload();
182
+ return;
183
+ }
184
+
185
+ fetch(window.location.href, { method: 'HEAD', cache: 'no-store' })
186
+ .then(function(res) {
187
+ if (res.ok || res.status < 500) {
188
+ window.location.reload();
189
+ } else {
190
+ setTimeout(pingAndReload, 150);
191
+ }
192
+ })
193
+ .catch(function() {
194
+ setTimeout(pingAndReload, 150);
195
+ });
196
+ }
197
+
198
+ setTimeout(pingAndReload, 30);
199
+ }
200
+
169
201
  function connect() {
170
202
  if (es) {
171
203
  try { es.close(); } catch (_) {}
@@ -173,6 +205,16 @@ const CLIENT_RUNTIME_SCRIPT = `(function() {
173
205
 
174
206
  es = new window.EventSource('${SSE_ENDPOINT_PATH}');
175
207
 
208
+ es.addEventListener('open', function() {
209
+ reconnectDelay = 200;
210
+ if (wasConnected) {
211
+ console.log('[Webspresso] 🔄 Dev server reconnected, reloading page...');
212
+ safeReload('server reconnected');
213
+ return;
214
+ }
215
+ wasConnected = true;
216
+ });
217
+
176
218
  es.addEventListener('reload', function(e) {
177
219
  try {
178
220
  var data = JSON.parse(e.data);
@@ -180,10 +222,7 @@ const CLIENT_RUNTIME_SCRIPT = `(function() {
180
222
  clearOverlay();
181
223
  reloadCss(data.file);
182
224
  } else if (data.type === 'full') {
183
- clearOverlay();
184
- var reasonMsg = data.reason ? ' (' + data.reason + ')' : '';
185
- console.log('[Webspresso] 🔄 Reloading page' + reasonMsg + '...');
186
- window.location.reload();
225
+ safeReload(data.reason);
187
226
  } else if (data.type === 'error') {
188
227
  showOverlay(data);
189
228
  } else if (data.type === 'clear-error') {
@@ -195,12 +234,14 @@ const CLIENT_RUNTIME_SCRIPT = `(function() {
195
234
  });
196
235
 
197
236
  es.onerror = function() {
198
- es.close();
237
+ try { es.close(); } catch (_) {}
199
238
  if (!reconnectTimer) {
239
+ var delay = reconnectDelay;
240
+ reconnectDelay = Math.min(reconnectDelay * 1.5, 1500);
200
241
  reconnectTimer = setTimeout(function() {
201
242
  reconnectTimer = null;
202
243
  connect();
203
- }, 2000);
244
+ }, delay);
204
245
  }
205
246
  };
206
247
  }
package/src/helpers.js CHANGED
@@ -627,15 +627,7 @@ class AssetManager {
627
627
  * Escape HTML special characters
628
628
  */
629
629
  escapeHtml(str) {
630
- if (!str || typeof str !== 'string' || !/[&"'<>]/.test(str)) {
631
- return str;
632
- }
633
- return str
634
- .replace(/&/g, '&amp;')
635
- .replace(/"/g, '&quot;')
636
- .replace(/'/g, '&#39;')
637
- .replace(/</g, '&lt;')
638
- .replace(/>/g, '&gt;');
630
+ return require('../core/utils/html').escapeHtml(str);
639
631
  }
640
632
  }
641
633
 
@@ -2204,6 +2196,26 @@ const utils = {
2204
2196
 
2205
2197
  pageAssets(nameOrAttrs, attrs) {
2206
2198
  return getAssetManager().pageAssets(nameOrAttrs, attrs);
2199
+ },
2200
+
2201
+ fetchWithTimeout(url, options) {
2202
+ return require('../core/utils/fetch').fetchWithTimeout(url, options);
2203
+ },
2204
+
2205
+ safeCompare(a, b) {
2206
+ return require('../core/utils/crypto').safeCompare(a, b);
2207
+ },
2208
+
2209
+ escapeHtml(value) {
2210
+ return require('../core/utils/html').escapeHtml(value);
2211
+ },
2212
+
2213
+ unescapeHtml(value) {
2214
+ return require('../core/utils/html').unescapeHtml(value);
2215
+ },
2216
+
2217
+ getClientIp(req, fallback) {
2218
+ return require('../core/utils/ip').getClientIp(req, fallback);
2207
2219
  }
2208
2220
  };
2209
2221
 
package/src/server.js CHANGED
@@ -642,8 +642,8 @@ function createApp(options = {}) {
642
642
 
643
643
  const shutdownConfig = {
644
644
  enabled: true,
645
- mode: 'graceful',
646
- timeout: 10_000,
645
+ mode: isDev ? 'force' : 'graceful',
646
+ timeout: isDev ? 1000 : 10_000,
647
647
  ...(options.shutdown || options.server?.shutdown || {}),
648
648
  };
649
649