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.
- package/bin/commands/dev.js +3 -2
- package/bin/utils/orm-map-html.js +2 -8
- package/core/auth/jwt.js +2 -4
- package/core/auth/oauth.js +38 -14
- package/core/auth/totp.js +3 -15
- package/core/auth/user-agent.js +1 -21
- package/core/content/renderer.js +1 -13
- package/core/errors/middleware.js +2 -8
- package/core/forms/form-builder.js +1 -9
- package/core/orm/index.js +5 -0
- package/core/orm/query-filters.js +393 -0
- package/core/shutdown/shutdown-manager.js +16 -16
- package/core/utils/crypto.js +73 -0
- package/core/utils/escape-html.js +7 -0
- package/core/utils/fetch.js +120 -0
- package/core/utils/get-client-ip.js +6 -0
- package/core/utils/html.js +72 -0
- package/core/utils/ip.js +86 -0
- package/core/utils/safe-compare.js +3 -0
- package/index.d.ts +11 -1
- package/index.js +10 -0
- package/package.json +1 -1
- package/plugins/admin-panel/auth.js +15 -7
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +82 -10
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +4 -2
- package/plugins/admin-panel/client/parts/08-record-list.js +3 -2
- package/plugins/admin-panel/client/parts/10-export-registry.js +6 -0
- package/plugins/admin-panel/core/api-extensions.js +1 -75
- package/plugins/admin-panel/lib/login-rate-limiter.js +15 -13
- package/plugins/api-key/crypto.js +1 -24
- package/plugins/audit-log/api-handlers.js +11 -1
- package/plugins/audit-log/middleware.js +11 -2
- package/plugins/basic-auth/index.js +2 -26
- package/plugins/cors.js +2 -2
- package/plugins/csrf/index.js +3 -15
- package/plugins/file-manager/admin-component.js +3 -1
- package/plugins/file-manager/core.js +34 -4
- package/plugins/mcp/auth.js +1 -13
- package/plugins/mcp/index.js +23 -1
- package/plugins/mcp/transports/sse.js +10 -1
- package/plugins/orm-cache-admin/admin-component.js +3 -2
- package/plugins/polar/src/api.js +9 -2
- package/plugins/polar/src/webhooks.js +3 -8
- package/plugins/recaptcha/helpers.js +1 -7
- package/plugins/recaptcha/verify.js +37 -23
- package/plugins/redirect/index.js +51 -3
- package/plugins/rest-resources/index.js +1 -207
- package/plugins/schema-explorer/admin-component.js +4 -2
- package/plugins/schema-explorer.js +40 -29
- package/plugins/seo-checker/index.js +5 -2
- package/plugins/site-analytics/tracking.js +19 -16
- package/plugins/sitemap.js +18 -15
- package/plugins/swagger.js +9 -14
- package/plugins/webhooks/crypto.js +1 -18
- package/plugins/webhooks/dispatcher.js +6 -1
- package/plugins/webhooks/matcher.js +25 -10
- package/plugins/webhooks/sender.js +32 -2
- package/src/dev-reload/index.js +47 -6
- package/src/helpers.js +21 -9
- 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 (
|
|
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
|
-
|
|
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
|
-
} =
|
|
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',
|
package/src/dev-reload/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
},
|
|
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
|
-
|
|
631
|
-
return str;
|
|
632
|
-
}
|
|
633
|
-
return str
|
|
634
|
-
.replace(/&/g, '&')
|
|
635
|
-
.replace(/"/g, '"')
|
|
636
|
-
.replace(/'/g, ''')
|
|
637
|
-
.replace(/</g, '<')
|
|
638
|
-
.replace(/>/g, '>');
|
|
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
|
|