webspresso 0.0.103 → 0.0.105
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/.agents/AGENTS.md +6 -0
- package/.agents/ARCHITECTURE.md +23 -0
- package/.agents/CACHE.md +147 -0
- package/.agents/CLI.md +1 -1
- package/.agents/EARLY_HINTS.md +120 -0
- package/.agents/HEALTH.md +118 -0
- package/.agents/LIMITS.md +109 -0
- package/.agents/ORM.md +155 -1
- package/.agents/PLUGINS.md +28 -0
- package/.agents/RESILIENCE.md +235 -0
- package/AGENTS.md +6 -0
- package/README.md +2 -2
- package/bin/commands/add-docker.js +6 -6
- package/bin/commands/dev.js +4 -3
- package/bin/commands/new.js +5 -5
- package/bin/commands/openapi.js +1 -1
- package/bin/commands/start.js +1 -1
- package/bin/utils/orm-map-html.js +2 -8
- package/bin/utils/project.js +2 -2
- package/core/auth/hash.js +5 -4
- package/core/auth/jwt.js +6 -57
- package/core/auth/magic-link.js +4 -5
- package/core/auth/manager.js +3 -5
- package/core/auth/oauth.js +45 -30
- package/core/auth/totp.js +5 -16
- package/core/auth/user-agent.js +1 -21
- package/core/cache/byte-size.js +123 -0
- package/core/cache/index.js +21 -0
- package/core/cache/lru.js +477 -0
- package/core/cache/tagged-lru.js +346 -0
- package/core/commands/registry.js +6 -20
- package/core/content/renderer.js +1 -13
- package/core/early-hints/index.js +267 -0
- package/core/errors/domain.js +48 -0
- package/core/errors/http.js +31 -0
- package/core/errors/index.js +10 -0
- package/core/errors/middleware.js +2 -8
- package/core/errors/normalize.js +88 -2
- package/core/events/discovery.js +6 -24
- package/core/events/dispatcher.js +2 -7
- package/core/forms/form-builder.js +1 -9
- package/core/health/index.js +41 -0
- package/core/health/middleware.js +213 -0
- package/core/health/normalize.js +140 -0
- package/core/health/probes.js +203 -0
- package/core/limits/index.js +34 -0
- package/core/limits/middleware.js +317 -0
- package/core/limits/normalize.js +222 -0
- package/core/orm/batch.js +70 -0
- package/core/orm/cache/fingerprint.js +2 -2
- package/core/orm/cache/memory-provider.js +27 -110
- package/core/orm/index.js +76 -8
- package/core/orm/json-fields.js +22 -8
- package/core/orm/model.js +10 -0
- package/core/orm/query-builder.js +63 -9
- package/core/orm/query-filters.js +393 -0
- package/core/orm/repository.js +318 -19
- package/core/orm/schema-helpers.js +8 -10
- package/core/orm/scopes.js +321 -1
- package/core/orm/transaction.js +10 -2
- package/core/orm/utils.js +4 -86
- package/core/queue/adapters/database.js +4 -8
- package/core/queue/manager.js +55 -41
- package/core/resilience/circuit-breaker.js +391 -0
- package/core/resilience/errors.js +63 -0
- package/core/resilience/index.js +53 -0
- package/core/resilience/registry.js +97 -0
- package/core/resilience/retry.js +203 -0
- package/core/schema-cache.js +9 -2
- package/core/shutdown/http-adapter.js +4 -1
- package/core/shutdown/shutdown-manager.js +132 -31
- package/core/utils/array.js +12 -0
- package/core/utils/base64url.js +8 -0
- package/core/utils/crypto.js +191 -0
- package/core/utils/ensure-leading-slash.js +6 -0
- package/core/utils/escape-html.js +7 -0
- package/core/utils/escape-regexp.js +7 -0
- package/core/utils/fetch.js +121 -0
- package/core/utils/fs.js +144 -0
- package/core/utils/get-client-ip.js +6 -0
- package/core/utils/hmac.js +6 -0
- package/core/utils/html.js +72 -0
- package/core/utils/ip.js +86 -0
- package/core/utils/json.js +98 -0
- package/core/utils/object.js +106 -0
- package/core/utils/random.js +9 -0
- package/core/utils/regex.js +77 -0
- package/core/utils/safe-compare.js +3 -0
- package/core/utils/sha256.js +6 -0
- package/core/utils/string.js +87 -0
- package/core/utils/strip-trailing-slash.js +6 -0
- package/core/utils/time.js +96 -0
- package/core/utils/timeout.js +348 -0
- package/core/utils/url.js +106 -0
- package/core/utils/walk.js +7 -0
- package/index.d.ts +627 -2
- package/index.js +158 -0
- package/package.json +1 -2
- 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 +3 -80
- package/plugins/admin-panel/index.js +1 -1
- package/plugins/admin-panel/lib/login-rate-limiter.js +15 -13
- package/plugins/admin-panel/modules/menu.js +1 -1
- package/plugins/api-key/crypto.js +3 -26
- package/plugins/api-key/scopes.js +5 -7
- package/plugins/audit-log/api-handlers.js +8 -7
- package/plugins/audit-log/middleware.js +11 -2
- package/plugins/audit-log/parse.js +3 -3
- package/plugins/basic-auth/index.js +2 -26
- package/plugins/cors.js +2 -2
- package/plugins/csrf/index.js +7 -23
- package/plugins/dashboard/app.js +2 -2
- package/plugins/dashboard/index.js +5 -5
- package/plugins/data-exchange/record-selection.js +2 -5
- package/plugins/email/api-handlers.js +5 -18
- package/plugins/email/auth-bridge.js +3 -2
- package/plugins/file-manager/admin-component.js +3 -1
- package/plugins/file-manager/core.js +34 -4
- package/plugins/health-check.js +2 -1
- package/plugins/index.js +2 -0
- package/plugins/lazy.js +5 -6
- package/plugins/mcp/auth.js +1 -13
- package/plugins/mcp/discovery.js +18 -17
- 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 +11 -3
- package/plugins/polar/src/webhooks.js +7 -13
- package/plugins/rate-limit/index.js +3 -1
- package/plugins/recaptcha/helpers.js +1 -7
- package/plugins/recaptcha/verify.js +37 -23
- package/plugins/redirect/index.js +57 -7
- package/plugins/rest-resources/index.js +1 -207
- package/plugins/rls/index.js +170 -0
- 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 +21 -18
- package/plugins/sitemap.js +21 -17
- package/plugins/swagger.js +13 -17
- package/plugins/upload/index.js +4 -3
- package/plugins/upload/orphan-pruner.js +4 -3
- package/plugins/webhooks/crypto.js +6 -28
- package/plugins/webhooks/dispatcher.js +14 -9
- package/plugins/webhooks/index.js +6 -0
- package/plugins/webhooks/matcher.js +28 -17
- package/plugins/webhooks/sender.js +40 -13
- package/src/app-context.js +41 -5
- package/src/dev/banner.js +6 -2
- package/src/dev-reload/index.js +47 -6
- package/src/file-router.js +1 -3
- package/src/helpers.js +180 -33
- package/src/modules/module-discovery.js +1 -9
- package/src/pages/page-loader.js +53 -1
- package/src/plugin-manager.js +90 -17
- package/src/server.js +137 -26
- package/src/services/discovery.js +19 -40
- package/src/services/executor.js +86 -25
- package/src/services/memoize.js +3 -25
- package/src/services/registry.js +6 -0
- package/src/template/precompile.js +8 -19
|
@@ -4,9 +4,7 @@
|
|
|
4
4
|
* @module plugins/webhooks/crypto
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const crypto = require('crypto');
|
|
7
|
+
const { safeCompare, hmacSha256, randomId } = require('../../core/utils/crypto');
|
|
10
8
|
|
|
11
9
|
/**
|
|
12
10
|
* Generate a cryptographically secure webhook signing secret
|
|
@@ -16,27 +14,7 @@ const crypto = require('crypto');
|
|
|
16
14
|
* @returns {string} E.g. 'whsec_e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934c'
|
|
17
15
|
*/
|
|
18
16
|
function generateSecret(prefix = 'whsec_', byteLength = 24) {
|
|
19
|
-
|
|
20
|
-
return `${prefix || ''}${bytes}`;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Perform constant-time string comparison to prevent timing attacks
|
|
25
|
-
*
|
|
26
|
-
* @param {string} a
|
|
27
|
-
* @param {string} b
|
|
28
|
-
* @returns {boolean}
|
|
29
|
-
*/
|
|
30
|
-
function safeCompare(a, b) {
|
|
31
|
-
if (typeof a !== 'string' || typeof b !== 'string') {
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
const bufA = Buffer.from(a, 'utf8');
|
|
35
|
-
const bufB = Buffer.from(b, 'utf8');
|
|
36
|
-
if (bufA.length !== bufB.length) {
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
return crypto.timingSafeEqual(bufA, bufB);
|
|
17
|
+
return randomId(prefix, byteLength, 'hex');
|
|
40
18
|
}
|
|
41
19
|
|
|
42
20
|
/**
|
|
@@ -59,12 +37,12 @@ function signPayload(payload, secret, options = {}) {
|
|
|
59
37
|
: Math.floor(Date.now() / 1000);
|
|
60
38
|
|
|
61
39
|
// 1. Direct payload HMAC-SHA256 (compatible with GitHub / standard webhooks)
|
|
62
|
-
const hex =
|
|
40
|
+
const hex = hmacSha256(payloadStr, secret, 'hex');
|
|
63
41
|
const signature = `sha256=${hex}`;
|
|
64
42
|
|
|
65
43
|
// 2. Timestamped HMAC-SHA256 (compatible with Stripe / Svix / Slack)
|
|
66
44
|
const toSign = `${timestamp}.${payloadStr}`;
|
|
67
|
-
const timestampedHex =
|
|
45
|
+
const timestampedHex = hmacSha256(toSign, secret, 'hex');
|
|
68
46
|
const timestampedSignature = `t=${timestamp},v1=${timestampedHex}`;
|
|
69
47
|
|
|
70
48
|
return {
|
|
@@ -121,7 +99,7 @@ function verifySignature(payload, signatureHeader, secret, options = {}) {
|
|
|
121
99
|
}
|
|
122
100
|
|
|
123
101
|
const toSign = `${timestamp}.${payloadStr}`;
|
|
124
|
-
const expectedHex =
|
|
102
|
+
const expectedHex = hmacSha256(toSign, secret, 'hex');
|
|
125
103
|
|
|
126
104
|
return signatures.some((sig) => safeCompare(sig, expectedHex));
|
|
127
105
|
}
|
|
@@ -132,7 +110,7 @@ function verifySignature(payload, signatureHeader, secret, options = {}) {
|
|
|
132
110
|
receivedHex = receivedHex.slice(7);
|
|
133
111
|
}
|
|
134
112
|
|
|
135
|
-
const expectedHex =
|
|
113
|
+
const expectedHex = hmacSha256(payloadStr, secret, 'hex');
|
|
136
114
|
return safeCompare(receivedHex, expectedHex);
|
|
137
115
|
}
|
|
138
116
|
|
|
@@ -8,9 +8,11 @@
|
|
|
8
8
|
|
|
9
9
|
const EventEmitter = require('events');
|
|
10
10
|
const crypto = require('crypto');
|
|
11
|
+
const { randomTimestampedId } = require('../../core/utils/crypto');
|
|
11
12
|
const { generateSecret } = require('./crypto');
|
|
12
13
|
const { matchesAnyEvent, normalizeEvents } = require('./matcher');
|
|
13
14
|
const { sendWebhook } = require('./sender');
|
|
15
|
+
const { safeJsonParse } = require('../../core/utils/json');
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Generate a standard unique delivery identifier
|
|
@@ -20,7 +22,7 @@ function generateDeliveryId() {
|
|
|
20
22
|
if (typeof crypto.randomUUID === 'function') {
|
|
21
23
|
return `dlv_${crypto.randomUUID().replace(/-/g, '')}`;
|
|
22
24
|
}
|
|
23
|
-
return
|
|
25
|
+
return randomTimestampedId('dlv', 8);
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
class WebhookDispatcher extends EventEmitter {
|
|
@@ -47,7 +49,10 @@ class WebhookDispatcher extends EventEmitter {
|
|
|
47
49
|
this.deliveriesTable = options.deliveriesTable || 'webhook_deliveries';
|
|
48
50
|
this.maxAttempts = typeof options.maxAttempts === 'number' ? options.maxAttempts : 3;
|
|
49
51
|
this.backoffBaseMs = typeof options.backoffBaseMs === 'number' ? options.backoffBaseMs : 1000;
|
|
50
|
-
|
|
52
|
+
const defaultWebhookTimeout = options.timeouts?.webhook || 10000;
|
|
53
|
+
this.timeoutMs = typeof options.timeoutMs === 'number'
|
|
54
|
+
? options.timeoutMs
|
|
55
|
+
: (typeof options.timeout === 'number' ? options.timeout : defaultWebhookTimeout);
|
|
51
56
|
this.wrapPayload = options.wrapPayload !== undefined ? options.wrapPayload : true;
|
|
52
57
|
this.queue = options.queue || null;
|
|
53
58
|
this.logger = options.logger || console;
|
|
@@ -461,9 +466,14 @@ class WebhookDispatcher extends EventEmitter {
|
|
|
461
466
|
attempt: attempt + 1,
|
|
462
467
|
autoRetry: true,
|
|
463
468
|
}).catch((err) => {
|
|
464
|
-
this.
|
|
469
|
+
if (this.listenerCount('error') > 0) {
|
|
470
|
+
this.emit('error', err);
|
|
471
|
+
} else {
|
|
472
|
+
console.error('[webhooks] Delivery retry failed:', err?.message || err);
|
|
473
|
+
}
|
|
465
474
|
});
|
|
466
475
|
}, delayMs);
|
|
476
|
+
if (timer.unref) timer.unref();
|
|
467
477
|
this._activeTimers.add(timer);
|
|
468
478
|
}
|
|
469
479
|
} else {
|
|
@@ -522,12 +532,7 @@ class WebhookDispatcher extends EventEmitter {
|
|
|
522
532
|
throw new Error(`Target endpoint #${delivery.endpoint_id} no longer exists.`);
|
|
523
533
|
}
|
|
524
534
|
|
|
525
|
-
|
|
526
|
-
try {
|
|
527
|
-
parsedPayload = JSON.parse(delivery.payload);
|
|
528
|
-
} catch (e) {
|
|
529
|
-
// Keep string if not json
|
|
530
|
-
}
|
|
535
|
+
const parsedPayload = safeJsonParse(delivery.payload, delivery.payload);
|
|
531
536
|
|
|
532
537
|
const nextAttempt = (delivery.attempt || 1) + 1;
|
|
533
538
|
return this.executeDelivery({
|
|
@@ -104,6 +104,12 @@ function webhooksPlugin(options = {}) {
|
|
|
104
104
|
dispatcher.knex = db.knex || db;
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
+
// Sync timeouts from app context if not explicitly passed
|
|
108
|
+
const timeouts = ctx?.timeouts || (ctx?.app && typeof ctx.app.get === 'function' ? ctx.app.get('timeouts') : null);
|
|
109
|
+
if (timeouts && timeouts.webhook && dispatcher.timeoutMs === 10000 && options.timeoutMs === undefined && options.timeout === undefined) {
|
|
110
|
+
dispatcher.timeoutMs = timeouts.webhook;
|
|
111
|
+
}
|
|
112
|
+
|
|
107
113
|
// Attach dispatcher to app and request objects
|
|
108
114
|
if (ctx.app) {
|
|
109
115
|
ctx.app.webhooks = dispatcher;
|
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
'use strict';
|
|
8
8
|
|
|
9
|
+
const { globToRegExp } = require('../../core/utils/regex');
|
|
10
|
+
const { safeJsonParse } = require('../../core/utils/json');
|
|
11
|
+
|
|
9
12
|
/**
|
|
10
13
|
* Normalize an event filter into an array of strings
|
|
11
14
|
*
|
|
@@ -29,13 +32,9 @@ function normalizeEvents(events) {
|
|
|
29
32
|
|
|
30
33
|
// If JSON array string like '["user.*", "order.created"]'
|
|
31
34
|
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
return parsed.map((e) => (typeof e === 'string' ? e.trim() : '')).filter(Boolean);
|
|
36
|
-
}
|
|
37
|
-
} catch (err) {
|
|
38
|
-
// Fall back to comma-separated
|
|
35
|
+
const parsed = safeJsonParse(trimmed, null);
|
|
36
|
+
if (Array.isArray(parsed)) {
|
|
37
|
+
return parsed.map((e) => (typeof e === 'string' ? e.trim() : '')).filter(Boolean);
|
|
39
38
|
}
|
|
40
39
|
}
|
|
41
40
|
|
|
@@ -48,6 +47,22 @@ function normalizeEvents(events) {
|
|
|
48
47
|
return ['*'];
|
|
49
48
|
}
|
|
50
49
|
|
|
50
|
+
const regexCache = new Map();
|
|
51
|
+
const MAX_REGEX_CACHE = 1000;
|
|
52
|
+
|
|
53
|
+
function getPatternRegex(pattern) {
|
|
54
|
+
let cached = regexCache.get(pattern);
|
|
55
|
+
if (!cached) {
|
|
56
|
+
if (regexCache.size >= MAX_REGEX_CACHE) {
|
|
57
|
+
const first = regexCache.keys().next().value;
|
|
58
|
+
if (first !== undefined) regexCache.delete(first);
|
|
59
|
+
}
|
|
60
|
+
cached = globToRegExp(pattern, { separator: null, flags: 'i' });
|
|
61
|
+
regexCache.set(pattern, cached);
|
|
62
|
+
}
|
|
63
|
+
return cached;
|
|
64
|
+
}
|
|
65
|
+
|
|
51
66
|
/**
|
|
52
67
|
* Test whether an event pattern matches an actual event name
|
|
53
68
|
*
|
|
@@ -56,12 +71,15 @@ function normalizeEvents(events) {
|
|
|
56
71
|
* @returns {boolean}
|
|
57
72
|
*/
|
|
58
73
|
function matchEvent(pattern, eventName) {
|
|
59
|
-
if (
|
|
74
|
+
if (typeof pattern !== 'string' || typeof eventName !== 'string') {
|
|
60
75
|
return false;
|
|
61
76
|
}
|
|
62
77
|
|
|
63
78
|
const p = pattern.trim();
|
|
64
79
|
const e = eventName.trim();
|
|
80
|
+
if (!p || !e) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
65
83
|
|
|
66
84
|
// 1. Wildcard matches all
|
|
67
85
|
if (p === '*') {
|
|
@@ -73,15 +91,8 @@ function matchEvent(pattern, eventName) {
|
|
|
73
91
|
return true;
|
|
74
92
|
}
|
|
75
93
|
|
|
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);
|
|
94
|
+
// 3. Glob / Wildcard conversion with bounded cache
|
|
95
|
+
return getPatternRegex(p).test(e);
|
|
85
96
|
}
|
|
86
97
|
|
|
87
98
|
/**
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
|
|
9
9
|
const crypto = require('crypto');
|
|
10
10
|
const { signPayload } = require('./crypto');
|
|
11
|
+
const { safeJsonParse } = require('../../core/utils/json');
|
|
12
|
+
const { parseDurationMs } = require('../../core/utils/time');
|
|
11
13
|
|
|
12
14
|
/**
|
|
13
15
|
* Send a webhook HTTP request to a target endpoint
|
|
@@ -32,7 +34,8 @@ const { signPayload } = require('./crypto');
|
|
|
32
34
|
* timestamp: number
|
|
33
35
|
* }>}
|
|
34
36
|
*/
|
|
35
|
-
async function sendWebhook(options) {
|
|
37
|
+
async function sendWebhook(options = {}) {
|
|
38
|
+
const opts = options || {};
|
|
36
39
|
const {
|
|
37
40
|
url,
|
|
38
41
|
event,
|
|
@@ -40,10 +43,11 @@ async function sendWebhook(options) {
|
|
|
40
43
|
secret,
|
|
41
44
|
deliveryId = crypto.randomUUID ? crypto.randomUUID() : `dlv_${Date.now()}_${crypto.randomBytes(6).toString('hex')}`,
|
|
42
45
|
headers: customHeaders = {},
|
|
43
|
-
timeoutMs = 10000,
|
|
44
46
|
wrapPayload = true,
|
|
45
47
|
maxResponseBodyLength = 2048,
|
|
46
|
-
} =
|
|
48
|
+
} = opts;
|
|
49
|
+
|
|
50
|
+
const timeoutMs = parseDurationMs(opts.timeout !== undefined ? opts.timeout : (opts.timeoutMs !== undefined ? opts.timeoutMs : 10000), 10000);
|
|
47
51
|
|
|
48
52
|
if (!url || typeof url !== 'string') {
|
|
49
53
|
return {
|
|
@@ -57,6 +61,32 @@ async function sendWebhook(options) {
|
|
|
57
61
|
};
|
|
58
62
|
}
|
|
59
63
|
|
|
64
|
+
let parsedUrl;
|
|
65
|
+
try {
|
|
66
|
+
parsedUrl = new URL(url);
|
|
67
|
+
if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {
|
|
68
|
+
return {
|
|
69
|
+
success: false,
|
|
70
|
+
statusCode: 0,
|
|
71
|
+
responseBody: '',
|
|
72
|
+
durationMs: 0,
|
|
73
|
+
error: `Invalid webhook URL protocol: "${parsedUrl.protocol}". Only http: and https: are supported.`,
|
|
74
|
+
deliveryId,
|
|
75
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
return {
|
|
80
|
+
success: false,
|
|
81
|
+
statusCode: 0,
|
|
82
|
+
responseBody: '',
|
|
83
|
+
durationMs: 0,
|
|
84
|
+
error: 'Invalid webhook URL: destination URL is malformed.',
|
|
85
|
+
deliveryId,
|
|
86
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
60
90
|
const timestamp = Math.floor(Date.now() / 1000);
|
|
61
91
|
|
|
62
92
|
// Construct request body
|
|
@@ -83,16 +113,10 @@ async function sendWebhook(options) {
|
|
|
83
113
|
}
|
|
84
114
|
|
|
85
115
|
// Parse custom headers if stored as JSON string
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
} catch (e) {
|
|
91
|
-
parsedCustomHeaders = {};
|
|
92
|
-
}
|
|
93
|
-
} else if (typeof customHeaders === 'object' && customHeaders !== null) {
|
|
94
|
-
parsedCustomHeaders = customHeaders;
|
|
95
|
-
}
|
|
116
|
+
const parsedHeadersObj = safeJsonParse(customHeaders, {});
|
|
117
|
+
const parsedCustomHeaders = (parsedHeadersObj && typeof parsedHeadersObj === 'object' && !Array.isArray(parsedHeadersObj))
|
|
118
|
+
? parsedHeadersObj
|
|
119
|
+
: {};
|
|
96
120
|
|
|
97
121
|
const requestHeaders = {
|
|
98
122
|
'content-type': 'application/json',
|
|
@@ -116,6 +140,9 @@ async function sendWebhook(options) {
|
|
|
116
140
|
timeoutId = setTimeout(() => {
|
|
117
141
|
controller.abort();
|
|
118
142
|
}, timeoutMs);
|
|
143
|
+
if (timeoutId && timeoutId.unref) {
|
|
144
|
+
timeoutId.unref();
|
|
145
|
+
}
|
|
119
146
|
|
|
120
147
|
const response = await fetch(url, {
|
|
121
148
|
method: 'POST',
|
package/src/app-context.js
CHANGED
|
@@ -3,18 +3,22 @@
|
|
|
3
3
|
* @module src/app-context
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
const { circuitBreakerRegistry, retry } = require('../core/resilience');
|
|
7
|
+
|
|
8
|
+
/** @type {{ db: object|null, shutdownManager: object|null, serviceRegistry: object|null, events: object|null, mailer: object|null, cache: object|null, breakers: object|null }} */
|
|
7
9
|
let context = {
|
|
8
10
|
db: null,
|
|
9
11
|
shutdownManager: null,
|
|
10
12
|
serviceRegistry: null,
|
|
11
13
|
events: null,
|
|
12
14
|
mailer: null,
|
|
15
|
+
cache: null,
|
|
16
|
+
breakers: circuitBreakerRegistry,
|
|
13
17
|
};
|
|
14
18
|
|
|
15
19
|
/**
|
|
16
20
|
* Merge into the current context (typically called once from createApp).
|
|
17
|
-
* @param {{ db?: object|null, shutdownManager?: object|null, serviceRegistry?: object|null, events?: object|null, mailer?: object|null }} partial
|
|
21
|
+
* @param {{ db?: object|null, shutdownManager?: object|null, serviceRegistry?: object|null, events?: object|null, mailer?: object|null, cache?: object|null }} partial
|
|
18
22
|
*/
|
|
19
23
|
function setAppContext(partial) {
|
|
20
24
|
context = { ...context, ...partial };
|
|
@@ -99,7 +103,7 @@ function hasEvents() {
|
|
|
99
103
|
* Clear context (e.g. between tests).
|
|
100
104
|
*/
|
|
101
105
|
function resetAppContext() {
|
|
102
|
-
context = { db: null, shutdownManager: null, serviceRegistry: null, events: null, mailer: null };
|
|
106
|
+
context = { db: null, shutdownManager: null, serviceRegistry: null, events: null, mailer: null, cache: null };
|
|
103
107
|
}
|
|
104
108
|
|
|
105
109
|
/**
|
|
@@ -117,7 +121,21 @@ function hasMailer() {
|
|
|
117
121
|
}
|
|
118
122
|
|
|
119
123
|
/**
|
|
120
|
-
*
|
|
124
|
+
* @returns {object|null}
|
|
125
|
+
*/
|
|
126
|
+
function getCache() {
|
|
127
|
+
return context.cache;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @returns {boolean}
|
|
132
|
+
*/
|
|
133
|
+
function hasCache() {
|
|
134
|
+
return context.cache != null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Express middleware: sets `req.db`, `req.service`, `req.events`, `req.mailer`, and `req.cache` from registered app context.
|
|
121
139
|
* File-based `pages/api/*` routes attach this automatically; use in `setupRoutes`
|
|
122
140
|
* for manually registered handlers that need `req.db` and `req.service`.
|
|
123
141
|
* @type {import('express').RequestHandler}
|
|
@@ -132,9 +150,25 @@ function attachDbMiddleware(req, res, next) {
|
|
|
132
150
|
if (context.mailer != null) {
|
|
133
151
|
req.mailer = context.mailer;
|
|
134
152
|
}
|
|
153
|
+
if (context.cache != null) {
|
|
154
|
+
req.cache = context.cache;
|
|
155
|
+
}
|
|
156
|
+
req.breakers = context.breakers || circuitBreakerRegistry;
|
|
157
|
+
req.breaker = (name, opts) => (context.breakers || circuitBreakerRegistry).getBreaker(name, opts);
|
|
158
|
+
req.retry = retry;
|
|
135
159
|
if (context.serviceRegistry != null) {
|
|
136
160
|
req.service = (name, input, opts) =>
|
|
137
|
-
context.serviceRegistry.call(name, input, req.context || {
|
|
161
|
+
context.serviceRegistry.call(name, input, req.context || {
|
|
162
|
+
req,
|
|
163
|
+
res,
|
|
164
|
+
db: context.db,
|
|
165
|
+
events: context.events,
|
|
166
|
+
mailer: context.mailer,
|
|
167
|
+
cache: context.cache,
|
|
168
|
+
breakers: req.breakers,
|
|
169
|
+
breaker: req.breaker,
|
|
170
|
+
retry,
|
|
171
|
+
}, opts);
|
|
138
172
|
}
|
|
139
173
|
next();
|
|
140
174
|
}
|
|
@@ -152,6 +186,8 @@ module.exports = {
|
|
|
152
186
|
hasEvents,
|
|
153
187
|
getMailer,
|
|
154
188
|
hasMailer,
|
|
189
|
+
getCache,
|
|
190
|
+
hasCache,
|
|
155
191
|
resetAppContext,
|
|
156
192
|
attachDbMiddleware,
|
|
157
193
|
};
|
package/src/dev/banner.js
CHANGED
|
@@ -94,7 +94,7 @@ function collectBannerSummary(context = {}) {
|
|
|
94
94
|
db,
|
|
95
95
|
serviceRegistry,
|
|
96
96
|
pluginManager,
|
|
97
|
-
port =
|
|
97
|
+
port = 7331,
|
|
98
98
|
host = 'localhost',
|
|
99
99
|
} = context;
|
|
100
100
|
|
|
@@ -141,9 +141,13 @@ function collectBannerSummary(context = {}) {
|
|
|
141
141
|
// 5. Plugins
|
|
142
142
|
let pluginsList = [];
|
|
143
143
|
let hasAdmin = false;
|
|
144
|
-
if (
|
|
144
|
+
if (context.hasAdmin !== undefined) {
|
|
145
|
+
hasAdmin = Boolean(context.hasAdmin);
|
|
146
|
+
} else if (pluginManager && pluginManager.plugins instanceof Map) {
|
|
145
147
|
pluginsList = Array.from(pluginManager.plugins.keys());
|
|
146
148
|
hasAdmin = pluginManager.plugins.has('adminPanel') || pluginManager.plugins.has('admin');
|
|
149
|
+
} else {
|
|
150
|
+
hasAdmin = true;
|
|
147
151
|
}
|
|
148
152
|
|
|
149
153
|
// URLs
|
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/file-router.js
CHANGED
|
@@ -32,6 +32,7 @@ const {
|
|
|
32
32
|
resolveNamedMiddleware,
|
|
33
33
|
resolveMiddlewares,
|
|
34
34
|
} = require('./routing/middleware-resolver');
|
|
35
|
+
const { escapeRegExp } = require('../core/utils/regex');
|
|
35
36
|
|
|
36
37
|
const NOOP = () => {};
|
|
37
38
|
const EMPTY_OBJECT = Object.freeze({});
|
|
@@ -372,9 +373,6 @@ function rewriteDynamicRouteMarkers(route) {
|
|
|
372
373
|
return out;
|
|
373
374
|
}
|
|
374
375
|
|
|
375
|
-
function escapeRegExp(s) {
|
|
376
|
-
return String(s).replace(/[\\^$*+?.()|[\]{}]/g, '\\$&');
|
|
377
|
-
}
|
|
378
376
|
|
|
379
377
|
/**
|
|
380
378
|
* Checks if a path segment is a route group, e.g. '(marketing)', '(auth)'.
|