webspresso 0.0.98 → 0.0.100
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/ADMIN.md +11 -1
- package/.agents/AGENTS.md +2 -2
- package/.agents/AUTH.md +415 -0
- package/.agents/CLI.md +47 -0
- package/.agents/ERRORS.md +59 -0
- package/.agents/ORM.md +178 -4
- package/.agents/PLUGINS.md +113 -8
- package/.agents/QUEUE.md +92 -0
- package/.agents/ROUTING.md +160 -7
- package/.agents/STREAMING.md +35 -0
- package/AGENTS.md +2 -0
- package/README.md +145 -93
- package/bin/commands/add-docker.js +467 -0
- package/bin/commands/deploy-check.js +339 -0
- package/bin/webspresso.js +4 -0
- package/core/auth/index.js +123 -1
- package/core/auth/magic-link.js +170 -0
- package/core/auth/manager.js +477 -3
- package/core/auth/middleware.js +118 -5
- package/core/auth/oauth.js +517 -0
- package/core/auth/qrcode.js +499 -0
- package/core/auth/session-manager.js +255 -0
- package/core/auth/sessions.js +351 -0
- package/core/auth/tokens.js +1 -0
- package/core/auth/totp.js +507 -0
- package/core/auth/user-agent.js +161 -0
- package/core/orm/eager-loader.js +144 -10
- package/core/orm/index.js +10 -1
- package/core/orm/model.js +55 -1
- package/core/orm/query-builder.js +90 -4
- package/core/orm/repository.js +327 -16
- package/core/orm/scopes.js +3 -2
- package/core/orm/search.js +213 -0
- package/core/orm/types.js +6 -1
- package/core/queue/cron.js +544 -0
- package/core/queue/index.js +8 -1
- package/core/queue/manager.js +100 -1
- package/core/queue/scheduler.js +415 -0
- package/core/sse/index.js +19 -0
- package/core/sse/stream.js +430 -0
- package/core/ssr/stream.js +62 -2
- package/index.d.ts +794 -6
- package/index.js +48 -0
- package/package.json +7 -5
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +323 -31
- package/plugins/admin-panel/client/parts/08-record-list.js +5 -0
- package/plugins/admin-panel/client/verify-spa-parts.js +2 -0
- package/plugins/admin-panel/field-renderers/file-upload.js +187 -104
- package/plugins/admin-panel/field-renderers/index.js +2 -0
- package/plugins/admin-panel/modules/admin-users.js +1 -1
- package/plugins/api-key/crypto.js +101 -0
- package/plugins/api-key/index.js +288 -0
- package/plugins/api-key/middleware.js +461 -0
- package/plugins/api-key/migration-template.js +48 -0
- package/plugins/api-key/model.js +50 -0
- package/plugins/api-key/scopes.js +125 -0
- package/plugins/audit-log/admin-component.js +487 -70
- package/plugins/audit-log/api-handlers.js +114 -0
- package/plugins/audit-log/index.js +24 -0
- package/plugins/content/client/inline-edit.js +2 -46
- package/plugins/cors.js +13 -2
- package/plugins/email/auth-bridge.js +66 -0
- package/plugins/email/bundled-templates.js +15 -0
- package/plugins/email/templates/auth-magic-link.mjml +13 -0
- package/plugins/file-manager/admin-component.js +69 -53
- package/plugins/file-manager/index.js +25 -8
- package/plugins/index.js +8 -0
- package/plugins/oauth/index.js +338 -0
- package/plugins/queue/index.js +4 -0
- package/plugins/webhooks/admin-component.js +608 -0
- package/plugins/webhooks/api-handlers.js +237 -0
- package/plugins/webhooks/crypto.js +144 -0
- package/plugins/webhooks/dispatcher.js +708 -0
- package/plugins/webhooks/index.js +263 -0
- package/plugins/webhooks/matcher.js +113 -0
- package/plugins/webhooks/migration-template.js +75 -0
- package/plugins/webhooks/models.js +98 -0
- package/plugins/webhooks/sender.js +176 -0
- package/src/api/api-loader.js +1 -0
- package/src/bundle/index.js +2 -1
- package/src/discovery/file-route-parser.js +44 -1
- package/src/discovery/scan-routes.js +2 -2
- package/src/file-router.js +310 -104
- package/src/helpers.js +16 -1
- package/src/njk-frontmatter.js +3 -0
- package/src/routing/cascading.js +490 -0
- package/src/routing/middleware-resolver.js +92 -0
- package/src/server.js +6 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhook Sender Module
|
|
3
|
+
* Zero-dependency HTTP/HTTPS delivery using native fetch with signatures & timeouts
|
|
4
|
+
* @module plugins/webhooks/sender
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const crypto = require('crypto');
|
|
10
|
+
const { signPayload } = require('./crypto');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Send a webhook HTTP request to a target endpoint
|
|
14
|
+
*
|
|
15
|
+
* @param {Object} options
|
|
16
|
+
* @param {string} options.url - Destination URL
|
|
17
|
+
* @param {string} options.event - Event name (e.g. 'order.created')
|
|
18
|
+
* @param {any} options.payload - Event payload data
|
|
19
|
+
* @param {string} [options.secret] - Signing secret
|
|
20
|
+
* @param {string} [options.deliveryId] - Unique delivery ID
|
|
21
|
+
* @param {Object} [options.headers] - Custom headers to merge
|
|
22
|
+
* @param {number} [options.timeoutMs=10000] - Request timeout in milliseconds
|
|
23
|
+
* @param {boolean} [options.wrapPayload=true] - Wrap data in standard event envelope
|
|
24
|
+
* @param {number} [options.maxResponseBodyLength=2048] - Max response length to store
|
|
25
|
+
* @returns {Promise<{
|
|
26
|
+
* success: boolean,
|
|
27
|
+
* statusCode: number,
|
|
28
|
+
* responseBody: string,
|
|
29
|
+
* durationMs: number,
|
|
30
|
+
* error: string|null,
|
|
31
|
+
* deliveryId: string,
|
|
32
|
+
* timestamp: number
|
|
33
|
+
* }>}
|
|
34
|
+
*/
|
|
35
|
+
async function sendWebhook(options) {
|
|
36
|
+
const {
|
|
37
|
+
url,
|
|
38
|
+
event,
|
|
39
|
+
payload,
|
|
40
|
+
secret,
|
|
41
|
+
deliveryId = crypto.randomUUID ? crypto.randomUUID() : `dlv_${Date.now()}_${crypto.randomBytes(6).toString('hex')}`,
|
|
42
|
+
headers: customHeaders = {},
|
|
43
|
+
timeoutMs = 10000,
|
|
44
|
+
wrapPayload = true,
|
|
45
|
+
maxResponseBodyLength = 2048,
|
|
46
|
+
} = options;
|
|
47
|
+
|
|
48
|
+
if (!url || typeof url !== 'string') {
|
|
49
|
+
return {
|
|
50
|
+
success: false,
|
|
51
|
+
statusCode: 0,
|
|
52
|
+
responseBody: '',
|
|
53
|
+
durationMs: 0,
|
|
54
|
+
error: 'Invalid webhook URL: destination URL is required.',
|
|
55
|
+
deliveryId,
|
|
56
|
+
timestamp: Math.floor(Date.now() / 1000),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
61
|
+
|
|
62
|
+
// Construct request body
|
|
63
|
+
let bodyObj = payload;
|
|
64
|
+
if (wrapPayload && (typeof payload !== 'object' || payload === null || !payload.event)) {
|
|
65
|
+
bodyObj = {
|
|
66
|
+
id: deliveryId,
|
|
67
|
+
event,
|
|
68
|
+
timestamp,
|
|
69
|
+
data: payload,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const bodyStr = typeof bodyObj === 'string' ? bodyObj : JSON.stringify(bodyObj);
|
|
73
|
+
|
|
74
|
+
// Calculate cryptographic signatures if secret is configured
|
|
75
|
+
let signatureHeaders = {};
|
|
76
|
+
if (secret) {
|
|
77
|
+
const { signature, timestampedSignature } = signPayload(bodyStr, secret, { timestamp });
|
|
78
|
+
signatureHeaders = {
|
|
79
|
+
'x-webspresso-signature': signature,
|
|
80
|
+
'x-hub-signature-256': signature,
|
|
81
|
+
'webhook-signature': timestampedSignature,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Parse custom headers if stored as JSON string
|
|
86
|
+
let parsedCustomHeaders = {};
|
|
87
|
+
if (typeof customHeaders === 'string') {
|
|
88
|
+
try {
|
|
89
|
+
parsedCustomHeaders = JSON.parse(customHeaders);
|
|
90
|
+
} catch (e) {
|
|
91
|
+
parsedCustomHeaders = {};
|
|
92
|
+
}
|
|
93
|
+
} else if (typeof customHeaders === 'object' && customHeaders !== null) {
|
|
94
|
+
parsedCustomHeaders = customHeaders;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const requestHeaders = {
|
|
98
|
+
'content-type': 'application/json',
|
|
99
|
+
'accept': 'application/json, text/plain, */*',
|
|
100
|
+
'user-agent': 'Webspresso-Webhooks/1.0',
|
|
101
|
+
'x-webspresso-event': event,
|
|
102
|
+
'x-webspresso-delivery': deliveryId,
|
|
103
|
+
'x-webspresso-timestamp': String(timestamp),
|
|
104
|
+
'webhook-id': deliveryId,
|
|
105
|
+
'webhook-timestamp': String(timestamp),
|
|
106
|
+
...signatureHeaders,
|
|
107
|
+
...parsedCustomHeaders,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const startTime = Date.now();
|
|
111
|
+
let controller = null;
|
|
112
|
+
let timeoutId = null;
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
controller = new AbortController();
|
|
116
|
+
timeoutId = setTimeout(() => {
|
|
117
|
+
controller.abort();
|
|
118
|
+
}, timeoutMs);
|
|
119
|
+
|
|
120
|
+
const response = await fetch(url, {
|
|
121
|
+
method: 'POST',
|
|
122
|
+
headers: requestHeaders,
|
|
123
|
+
body: bodyStr,
|
|
124
|
+
signal: controller.signal,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
clearTimeout(timeoutId);
|
|
128
|
+
const durationMs = Date.now() - startTime;
|
|
129
|
+
|
|
130
|
+
let responseBody = '';
|
|
131
|
+
try {
|
|
132
|
+
const rawText = await response.text();
|
|
133
|
+
responseBody = rawText ? rawText.slice(0, maxResponseBodyLength) : '';
|
|
134
|
+
} catch (textErr) {
|
|
135
|
+
responseBody = '';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const success = response.status >= 200 && response.status < 300;
|
|
139
|
+
const error = success ? null : `HTTP ${response.status}: ${response.statusText || 'Delivery Failed'}`;
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
success,
|
|
143
|
+
statusCode: response.status,
|
|
144
|
+
responseBody,
|
|
145
|
+
durationMs,
|
|
146
|
+
error,
|
|
147
|
+
deliveryId,
|
|
148
|
+
timestamp,
|
|
149
|
+
bodyStr,
|
|
150
|
+
};
|
|
151
|
+
} catch (err) {
|
|
152
|
+
if (timeoutId) {
|
|
153
|
+
clearTimeout(timeoutId);
|
|
154
|
+
}
|
|
155
|
+
const durationMs = Date.now() - startTime;
|
|
156
|
+
const isTimeout = err.name === 'AbortError' || err.code === 'ABORT_ERR';
|
|
157
|
+
const errorMsg = isTimeout
|
|
158
|
+
? `Webhook delivery timed out after ${timeoutMs}ms`
|
|
159
|
+
: (err.message || 'Network error');
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
success: false,
|
|
163
|
+
statusCode: 0,
|
|
164
|
+
responseBody: '',
|
|
165
|
+
durationMs,
|
|
166
|
+
error: errorMsg,
|
|
167
|
+
deliveryId,
|
|
168
|
+
timestamp,
|
|
169
|
+
bodyStr,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = {
|
|
175
|
+
sendWebhook,
|
|
176
|
+
};
|
package/src/api/api-loader.js
CHANGED
package/src/bundle/index.js
CHANGED
|
@@ -132,7 +132,8 @@ function expandGlob(pattern, cwd = process.cwd()) {
|
|
|
132
132
|
|
|
133
133
|
const isRecursive = normalized.includes('**');
|
|
134
134
|
const regexPattern = '^' + normalized
|
|
135
|
-
.replace(
|
|
135
|
+
.replace(/\\/g, '\\\\')
|
|
136
|
+
.replace(/[.+?^${}()|[\]]/g, '\\$&')
|
|
136
137
|
.replace(/\*\*\//g, '(.+/)?')
|
|
137
138
|
.replace(/\*\*/g, '.*')
|
|
138
139
|
.replace(/\*/g, '[^/]*') + '$';
|
|
@@ -158,8 +158,11 @@ function parseFileRoute(relativePath, options = {}) {
|
|
|
158
158
|
// Replace last segment with cleanFilename
|
|
159
159
|
const routeSegments = [...segments.slice(0, -1), cleanFilename];
|
|
160
160
|
|
|
161
|
+
// Filter out route group segments (e.g. (marketing), (auth))
|
|
162
|
+
const filteredSegments = routeSegments.filter((seg) => !isRouteGroupSegment(seg));
|
|
163
|
+
|
|
161
164
|
// Process dynamic brackets in segments
|
|
162
|
-
const transformedSegments =
|
|
165
|
+
const transformedSegments = filteredSegments.map(transformDynamicSegment);
|
|
163
166
|
|
|
164
167
|
// Handle 'index' at the end of path
|
|
165
168
|
if (transformedSegments[transformedSegments.length - 1] === 'index') {
|
|
@@ -190,8 +193,48 @@ function parseFileRoute(relativePath, options = {}) {
|
|
|
190
193
|
};
|
|
191
194
|
}
|
|
192
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Checks if a path segment is a route group, e.g. '(marketing)', '(auth)'
|
|
198
|
+
* @param {string} segment
|
|
199
|
+
* @returns {boolean}
|
|
200
|
+
*/
|
|
201
|
+
function isRouteGroupSegment(segment) {
|
|
202
|
+
return (
|
|
203
|
+
typeof segment === 'string' &&
|
|
204
|
+
segment.length > 2 &&
|
|
205
|
+
segment.charCodeAt(0) === 40 /* '(' */ &&
|
|
206
|
+
segment.charCodeAt(segment.length - 1) === 41 /* ')' */
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Checks if a file path belongs to an API route, accounting for optional parent route groups.
|
|
212
|
+
* E.g., 'api/users.get.js' -> true
|
|
213
|
+
* 'api/(v1)/users.get.js' -> true
|
|
214
|
+
* '(marketing)/api/newsletter.post.js' -> true
|
|
215
|
+
* 'about.njk' -> false
|
|
216
|
+
* @param {string} file
|
|
217
|
+
* @returns {boolean}
|
|
218
|
+
*/
|
|
219
|
+
function isApiRouteFile(file) {
|
|
220
|
+
if (!file || typeof file !== 'string') return false;
|
|
221
|
+
const normalized = file.split(/[/\\]/).join('/');
|
|
222
|
+
if (normalized.startsWith('api/')) return true;
|
|
223
|
+
const parts = normalized.split('/');
|
|
224
|
+
const apiIndex = parts.indexOf('api');
|
|
225
|
+
if (apiIndex > 0) {
|
|
226
|
+
const prefix = parts.slice(0, apiIndex);
|
|
227
|
+
if (prefix.every(isRouteGroupSegment)) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
|
|
193
234
|
module.exports = {
|
|
194
235
|
parseFileRoute,
|
|
236
|
+
isRouteGroupSegment,
|
|
237
|
+
isApiRouteFile,
|
|
195
238
|
isPrivateOrIgnored,
|
|
196
239
|
normalizePrefix,
|
|
197
240
|
transformDynamicSegment,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
const fs = require('fs');
|
|
10
10
|
const path = require('path');
|
|
11
|
-
const { parseFileRoute, isPrivateOrIgnored } = require('./file-route-parser');
|
|
11
|
+
const { parseFileRoute, isPrivateOrIgnored, isApiRouteFile } = require('./file-route-parser');
|
|
12
12
|
const { discoverModuleDetails } = require('../modules/module-discovery');
|
|
13
13
|
|
|
14
14
|
const SUPPORTED_EXTENSIONS = new Set(['.js', '.njk']);
|
|
@@ -99,7 +99,7 @@ function scanRoutes(options = {}) {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
// Skip api directory inside pages (handled by classic mountPages or explicit api scan)
|
|
102
|
-
const isUnderApi =
|
|
102
|
+
const isUnderApi = isApiRouteFile(relativePath);
|
|
103
103
|
if (isUnderApi) {
|
|
104
104
|
continue;
|
|
105
105
|
}
|