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,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso Webhooks Plugin
|
|
3
|
+
* Outgoing Webhook Dispatcher, Signature Verification, Event Matching & Admin Inspector
|
|
4
|
+
* @module plugins/webhooks
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const { generateSecret, safeCompare, signPayload, verifySignature } = require('./crypto');
|
|
10
|
+
const { matchEvent, matchesAnyEvent, normalizeEvents } = require('./matcher');
|
|
11
|
+
const { sendWebhook } = require('./sender');
|
|
12
|
+
const { createWebhookEndpointModel, createWebhookDeliveryModel } = require('./models');
|
|
13
|
+
const { generateWebhooksMigration } = require('./migration-template');
|
|
14
|
+
const { WebhookDispatcher, generateDeliveryId } = require('./dispatcher');
|
|
15
|
+
const { createWebhookApiHandlers } = require('./api-handlers');
|
|
16
|
+
const { generateWebhooksComponent } = require('./admin-component');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Webhooks Plugin Factory
|
|
20
|
+
*
|
|
21
|
+
* @param {Object} [options]
|
|
22
|
+
* @param {any} [options.db] - Webspresso database or Knex instance
|
|
23
|
+
* @param {any} [options.knex] - Knex instance
|
|
24
|
+
* @param {string} [options.endpointsTable='webhook_endpoints']
|
|
25
|
+
* @param {string} [options.deliveriesTable='webhook_deliveries']
|
|
26
|
+
* @param {number} [options.maxAttempts=3] - Maximum retry attempts
|
|
27
|
+
* @param {number} [options.backoffBaseMs=1000] - Base delay for exponential backoff (ms)
|
|
28
|
+
* @param {number} [options.timeoutMs=10000] - Request timeout (ms)
|
|
29
|
+
* @param {boolean} [options.wrapPayload=true] - Wrap data in standard event envelope
|
|
30
|
+
* @param {any} [options.queue] - Webspresso QueueManager instance
|
|
31
|
+
* @param {Array<Object>} [options.endpoints] - Static in-memory endpoints
|
|
32
|
+
* @param {string} [options.apiPrefix='/webhooks'] - Admin API prefix
|
|
33
|
+
* @param {boolean} [options.includeDefaultPage=true] - Register Admin Panel page & menu
|
|
34
|
+
* @param {string} [options.pageTitle='Webhooks'] - Admin page title
|
|
35
|
+
* @param {number} [options.menuOrder=92] - Admin sidebar menu order
|
|
36
|
+
* @returns {Object} Webspresso Plugin Definition
|
|
37
|
+
*/
|
|
38
|
+
function webhooksPlugin(options = {}) {
|
|
39
|
+
const {
|
|
40
|
+
endpointsTable = 'webhook_endpoints',
|
|
41
|
+
deliveriesTable = 'webhook_deliveries',
|
|
42
|
+
apiPrefix = '/webhooks',
|
|
43
|
+
includeDefaultPage = true,
|
|
44
|
+
pageTitle = 'Webhooks',
|
|
45
|
+
menuOrder = 92,
|
|
46
|
+
} = options;
|
|
47
|
+
|
|
48
|
+
const dispatcher = new WebhookDispatcher({
|
|
49
|
+
...options,
|
|
50
|
+
endpointsTable,
|
|
51
|
+
deliveriesTable,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const apiHandlers = createWebhookApiHandlers(dispatcher, options);
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
name: 'webhooks',
|
|
58
|
+
version: '1.0.0',
|
|
59
|
+
description: 'Outgoing Webhook Dispatcher with HMAC-SHA256 signatures, wildcard matching, retries, and Admin UI',
|
|
60
|
+
options,
|
|
61
|
+
|
|
62
|
+
api: {
|
|
63
|
+
dispatcher,
|
|
64
|
+
dispatch: (event, payload, opts) => dispatcher.dispatch(event, payload, opts),
|
|
65
|
+
pingEndpoint: (id) => dispatcher.pingEndpoint(id),
|
|
66
|
+
retryDelivery: (id) => dispatcher.retryDelivery(id),
|
|
67
|
+
createEndpoint: (data) => dispatcher.createEndpoint(data),
|
|
68
|
+
getEndpoint: (id) => dispatcher.getEndpoint(id),
|
|
69
|
+
updateEndpoint: (id, data) => dispatcher.updateEndpoint(id, data),
|
|
70
|
+
deleteEndpoint: (id) => dispatcher.deleteEndpoint(id),
|
|
71
|
+
listEndpoints: (filters) => dispatcher.listEndpoints(filters),
|
|
72
|
+
listDeliveries: (filters) => dispatcher.listDeliveries(filters),
|
|
73
|
+
getDelivery: (id) => dispatcher.getDelivery(id),
|
|
74
|
+
purgeDeliveries: (opts) => dispatcher.purgeDeliveries(opts),
|
|
75
|
+
generateSecret,
|
|
76
|
+
safeCompare,
|
|
77
|
+
signPayload,
|
|
78
|
+
verifySignature,
|
|
79
|
+
matchEvent,
|
|
80
|
+
matchesAnyEvent,
|
|
81
|
+
sendWebhook,
|
|
82
|
+
getMigrationTemplate: (epTable, dlvTable) =>
|
|
83
|
+
generateWebhooksMigration(epTable || endpointsTable, dlvTable || deliveriesTable),
|
|
84
|
+
getModels: () => ({
|
|
85
|
+
WebhookEndpoint: createWebhookEndpointModel(endpointsTable),
|
|
86
|
+
WebhookDelivery: createWebhookDeliveryModel(deliveriesTable),
|
|
87
|
+
}),
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
register(ctx) {
|
|
91
|
+
const db = options.db || (ctx && ctx.db);
|
|
92
|
+
if (db && typeof db.registerModel === 'function' && typeof db.hasModel === 'function') {
|
|
93
|
+
if (!db.hasModel('WebhookEndpoint')) {
|
|
94
|
+
db.registerModel(createWebhookEndpointModel(endpointsTable));
|
|
95
|
+
}
|
|
96
|
+
if (!db.hasModel('WebhookDelivery')) {
|
|
97
|
+
db.registerModel(createWebhookDeliveryModel(deliveriesTable));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// If DB was not passed in plugin options, bind it from ctx
|
|
102
|
+
if (!dispatcher.db && db) {
|
|
103
|
+
dispatcher.db = db;
|
|
104
|
+
dispatcher.knex = db.knex || db;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Attach dispatcher to app and request objects
|
|
108
|
+
if (ctx.app) {
|
|
109
|
+
ctx.app.webhooks = dispatcher;
|
|
110
|
+
ctx.app.use((req, res, next) => {
|
|
111
|
+
req.webhooks = dispatcher;
|
|
112
|
+
next();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Register built-in services in app.serviceRegistry if available
|
|
116
|
+
if (ctx.app.serviceRegistry && typeof ctx.app.serviceRegistry.register === 'function') {
|
|
117
|
+
if (!ctx.app.serviceRegistry.has('webhook.dispatch')) {
|
|
118
|
+
ctx.app.serviceRegistry.register('webhook.dispatch', {
|
|
119
|
+
handler: async (input) => dispatcher.dispatch(input.event, input.payload, input),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
if (!ctx.app.serviceRegistry.has('webhook.ping')) {
|
|
123
|
+
ctx.app.serviceRegistry.register('webhook.ping', {
|
|
124
|
+
handler: async (input) => dispatcher.pingEndpoint(input.endpointId),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
if (!ctx.app.serviceRegistry.has('webhook.retry')) {
|
|
128
|
+
ctx.app.serviceRegistry.register('webhook.retry', {
|
|
129
|
+
handler: async (input) => dispatcher.retryDelivery(input.deliveryId),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Integrate with core/queue if queue is present
|
|
135
|
+
const queueManager = options.queue || ctx.app.queue;
|
|
136
|
+
if (queueManager && typeof queueManager.define === 'function') {
|
|
137
|
+
dispatcher.setQueue(queueManager);
|
|
138
|
+
if (!queueManager.handlers?.has?.('webhooks.deliver')) {
|
|
139
|
+
queueManager.define('webhooks.deliver', async (job) => {
|
|
140
|
+
const { endpointId, deliveryId, event, payload, attempt } = job.data;
|
|
141
|
+
const endpoint = await dispatcher.getEndpoint(endpointId);
|
|
142
|
+
if (!endpoint) {
|
|
143
|
+
throw new Error(`Endpoint #${endpointId} no longer exists.`);
|
|
144
|
+
}
|
|
145
|
+
const outcome = await dispatcher.executeDelivery({
|
|
146
|
+
endpoint,
|
|
147
|
+
deliveryId,
|
|
148
|
+
event,
|
|
149
|
+
payload,
|
|
150
|
+
attempt: attempt || 1,
|
|
151
|
+
autoRetry: false, // Let queue handle retries
|
|
152
|
+
});
|
|
153
|
+
if (!outcome.success) {
|
|
154
|
+
throw new Error(outcome.error || `Delivery failed with status ${outcome.statusCode}`);
|
|
155
|
+
}
|
|
156
|
+
return outcome;
|
|
157
|
+
}, {
|
|
158
|
+
attempts: options.maxAttempts || 3,
|
|
159
|
+
backoff: { type: 'exponential', delay: options.backoffBaseMs || 1000 },
|
|
160
|
+
timeout: options.timeoutMs || 10000,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return () => {
|
|
167
|
+
dispatcher.dispose();
|
|
168
|
+
};
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
dispose() {
|
|
172
|
+
dispatcher.dispose();
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
shutdown() {
|
|
176
|
+
dispatcher.dispose();
|
|
177
|
+
},
|
|
178
|
+
|
|
179
|
+
onRoutesReady(ctx) {
|
|
180
|
+
const adminApi = ctx.usePlugin('admin-panel');
|
|
181
|
+
if (!adminApi) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
adminApi.registerModule({
|
|
186
|
+
id: 'webhooks',
|
|
187
|
+
title: pageTitle,
|
|
188
|
+
|
|
189
|
+
pages: includeDefaultPage
|
|
190
|
+
? [{
|
|
191
|
+
id: 'webhooks',
|
|
192
|
+
title: pageTitle,
|
|
193
|
+
path: '/webhooks',
|
|
194
|
+
icon: 'send',
|
|
195
|
+
component: generateWebhooksComponent({ apiPrefix }),
|
|
196
|
+
}]
|
|
197
|
+
: [],
|
|
198
|
+
|
|
199
|
+
menu: includeDefaultPage
|
|
200
|
+
? [{
|
|
201
|
+
id: 'webhooks',
|
|
202
|
+
label: pageTitle,
|
|
203
|
+
path: '/webhooks',
|
|
204
|
+
icon: 'send',
|
|
205
|
+
order: menuOrder,
|
|
206
|
+
}]
|
|
207
|
+
: [],
|
|
208
|
+
|
|
209
|
+
api: {
|
|
210
|
+
prefix: apiPrefix,
|
|
211
|
+
routes: [
|
|
212
|
+
{ method: 'get', path: '/endpoints', handler: apiHandlers.listEndpointsHandler },
|
|
213
|
+
{ method: 'post', path: '/endpoints', handler: apiHandlers.createEndpointHandler },
|
|
214
|
+
{ method: 'get', path: '/endpoints/:id', handler: apiHandlers.getEndpointHandler },
|
|
215
|
+
{ method: 'put', path: '/endpoints/:id', handler: apiHandlers.updateEndpointHandler },
|
|
216
|
+
{ method: 'patch', path: '/endpoints/:id', handler: apiHandlers.updateEndpointHandler },
|
|
217
|
+
{ method: 'delete', path: '/endpoints/:id', handler: apiHandlers.deleteEndpointHandler },
|
|
218
|
+
{ method: 'post', path: '/endpoints/:id/ping', handler: apiHandlers.pingEndpointHandler },
|
|
219
|
+
{ method: 'get', path: '/deliveries', handler: apiHandlers.listDeliveriesHandler },
|
|
220
|
+
{ method: 'get', path: '/deliveries/:id', handler: apiHandlers.getDeliveryHandler },
|
|
221
|
+
{ method: 'post', path: '/deliveries/:id/retry', handler: apiHandlers.retryDeliveryHandler },
|
|
222
|
+
{ method: 'post', path: '/deliveries/purge', handler: apiHandlers.purgeDeliveriesHandler },
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
webhooksPlugin.webhooksPlugin = webhooksPlugin;
|
|
231
|
+
webhooksPlugin.WebhookDispatcher = WebhookDispatcher;
|
|
232
|
+
webhooksPlugin.generateSecret = generateSecret;
|
|
233
|
+
webhooksPlugin.safeCompare = safeCompare;
|
|
234
|
+
webhooksPlugin.signPayload = signPayload;
|
|
235
|
+
webhooksPlugin.verifySignature = verifySignature;
|
|
236
|
+
webhooksPlugin.matchEvent = matchEvent;
|
|
237
|
+
webhooksPlugin.matchesAnyEvent = matchesAnyEvent;
|
|
238
|
+
webhooksPlugin.normalizeEvents = normalizeEvents;
|
|
239
|
+
webhooksPlugin.sendWebhook = sendWebhook;
|
|
240
|
+
webhooksPlugin.generateDeliveryId = generateDeliveryId;
|
|
241
|
+
webhooksPlugin.generateWebhooksMigration = generateWebhooksMigration;
|
|
242
|
+
webhooksPlugin.createWebhookEndpointModel = createWebhookEndpointModel;
|
|
243
|
+
webhooksPlugin.createWebhookDeliveryModel = createWebhookDeliveryModel;
|
|
244
|
+
webhooksPlugin.createWebhookApiHandlers = createWebhookApiHandlers;
|
|
245
|
+
webhooksPlugin.generateWebhooksComponent = generateWebhooksComponent;
|
|
246
|
+
|
|
247
|
+
module.exports = webhooksPlugin;
|
|
248
|
+
module.exports.webhooksPlugin = webhooksPlugin;
|
|
249
|
+
module.exports.WebhookDispatcher = WebhookDispatcher;
|
|
250
|
+
module.exports.generateSecret = generateSecret;
|
|
251
|
+
module.exports.safeCompare = safeCompare;
|
|
252
|
+
module.exports.signPayload = signPayload;
|
|
253
|
+
module.exports.verifySignature = verifySignature;
|
|
254
|
+
module.exports.matchEvent = matchEvent;
|
|
255
|
+
module.exports.matchesAnyEvent = matchesAnyEvent;
|
|
256
|
+
module.exports.normalizeEvents = normalizeEvents;
|
|
257
|
+
module.exports.sendWebhook = sendWebhook;
|
|
258
|
+
module.exports.generateDeliveryId = generateDeliveryId;
|
|
259
|
+
module.exports.generateWebhooksMigration = generateWebhooksMigration;
|
|
260
|
+
module.exports.createWebhookEndpointModel = createWebhookEndpointModel;
|
|
261
|
+
module.exports.createWebhookDeliveryModel = createWebhookDeliveryModel;
|
|
262
|
+
module.exports.createWebhookApiHandlers = createWebhookApiHandlers;
|
|
263
|
+
module.exports.generateWebhooksComponent = generateWebhooksComponent;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhooks Event Pattern Matcher
|
|
3
|
+
* Supports wildcard matching (*, user.*, order.item.*)
|
|
4
|
+
* @module plugins/webhooks/matcher
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Normalize an event filter into an array of strings
|
|
11
|
+
*
|
|
12
|
+
* @param {string|string[]|any} events
|
|
13
|
+
* @returns {string[]}
|
|
14
|
+
*/
|
|
15
|
+
function normalizeEvents(events) {
|
|
16
|
+
if (!events) {
|
|
17
|
+
return ['*'];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (Array.isArray(events)) {
|
|
21
|
+
return events
|
|
22
|
+
.map((e) => (typeof e === 'string' ? e.trim() : ''))
|
|
23
|
+
.filter(Boolean);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (typeof events === 'string') {
|
|
27
|
+
const trimmed = events.trim();
|
|
28
|
+
if (!trimmed) return ['*'];
|
|
29
|
+
|
|
30
|
+
// If JSON array string like '["user.*", "order.created"]'
|
|
31
|
+
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(trimmed);
|
|
34
|
+
if (Array.isArray(parsed)) {
|
|
35
|
+
return parsed.map((e) => (typeof e === 'string' ? e.trim() : '')).filter(Boolean);
|
|
36
|
+
}
|
|
37
|
+
} catch (err) {
|
|
38
|
+
// Fall back to comma-separated
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return trimmed
|
|
43
|
+
.split(',')
|
|
44
|
+
.map((e) => e.trim())
|
|
45
|
+
.filter(Boolean);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return ['*'];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Test whether an event pattern matches an actual event name
|
|
53
|
+
*
|
|
54
|
+
* @param {string} pattern - E.g. '*', 'user.*', 'order.created', 'billing.invoice.paid'
|
|
55
|
+
* @param {string} eventName - E.g. 'user.created', 'order.created'
|
|
56
|
+
* @returns {boolean}
|
|
57
|
+
*/
|
|
58
|
+
function matchEvent(pattern, eventName) {
|
|
59
|
+
if (!pattern || !eventName) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const p = pattern.trim();
|
|
64
|
+
const e = eventName.trim();
|
|
65
|
+
|
|
66
|
+
// 1. Wildcard matches all
|
|
67
|
+
if (p === '*') {
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// 2. Exact match
|
|
72
|
+
if (p === e) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
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);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check if an event name matches any pattern in a list
|
|
89
|
+
*
|
|
90
|
+
* @param {string|string[]} patterns
|
|
91
|
+
* @param {string} eventName
|
|
92
|
+
* @returns {boolean}
|
|
93
|
+
*/
|
|
94
|
+
function matchesAnyEvent(patterns, eventName) {
|
|
95
|
+
const list = normalizeEvents(patterns);
|
|
96
|
+
if (list.length === 0) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (const pattern of list) {
|
|
101
|
+
if (matchEvent(pattern, eventName)) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = {
|
|
110
|
+
normalizeEvents,
|
|
111
|
+
matchEvent,
|
|
112
|
+
matchesAnyEvent,
|
|
113
|
+
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration template for webhook_endpoints and webhook_deliveries tables
|
|
3
|
+
* @module plugins/webhooks/migration-template
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Generate migration file content for webhooks tables
|
|
10
|
+
*
|
|
11
|
+
* @param {string} [endpointsTable='webhook_endpoints']
|
|
12
|
+
* @param {string} [deliveriesTable='webhook_deliveries']
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
function generateWebhooksMigration(
|
|
16
|
+
endpointsTable = 'webhook_endpoints',
|
|
17
|
+
deliveriesTable = 'webhook_deliveries'
|
|
18
|
+
) {
|
|
19
|
+
return `/**
|
|
20
|
+
* Migration: Create ${endpointsTable} and ${deliveriesTable} tables
|
|
21
|
+
* Auto-generated by webhooks plugin
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
exports.up = async function(knex) {
|
|
25
|
+
const hasEndpoints = await knex.schema.hasTable('${endpointsTable}');
|
|
26
|
+
if (!hasEndpoints) {
|
|
27
|
+
await knex.schema.createTable('${endpointsTable}', (table) => {
|
|
28
|
+
table.increments('id').primary();
|
|
29
|
+
table.string('name', 255).notNullable();
|
|
30
|
+
table.string('url', 2048).notNullable();
|
|
31
|
+
table.string('secret', 255).notNullable();
|
|
32
|
+
table.text('events').notNullable().defaultTo('["*"]');
|
|
33
|
+
table.boolean('is_active').notNullable().defaultTo(true).index();
|
|
34
|
+
table.string('description', 1000).nullable();
|
|
35
|
+
table.text('headers').nullable();
|
|
36
|
+
table.text('metadata').nullable();
|
|
37
|
+
table.timestamp('created_at').defaultTo(knex.fn.now()).notNullable().index();
|
|
38
|
+
table.timestamp('updated_at').defaultTo(knex.fn.now()).notNullable();
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const hasDeliveries = await knex.schema.hasTable('${deliveriesTable}');
|
|
43
|
+
if (!hasDeliveries) {
|
|
44
|
+
await knex.schema.createTable('${deliveriesTable}', (table) => {
|
|
45
|
+
table.increments('id').primary();
|
|
46
|
+
table.integer('endpoint_id').unsigned().notNullable().index();
|
|
47
|
+
table.string('event', 255).notNullable().index();
|
|
48
|
+
table.text('payload').notNullable();
|
|
49
|
+
table.integer('status_code').nullable();
|
|
50
|
+
table.boolean('success').notNullable().defaultTo(false).index();
|
|
51
|
+
table.integer('duration_ms').nullable();
|
|
52
|
+
table.text('error').nullable();
|
|
53
|
+
table.text('response_body').nullable();
|
|
54
|
+
table.integer('attempt').notNullable().defaultTo(1);
|
|
55
|
+
table.integer('max_attempts').notNullable().defaultTo(3);
|
|
56
|
+
table.timestamp('next_retry_at').nullable().index();
|
|
57
|
+
table.timestamp('created_at').defaultTo(knex.fn.now()).notNullable().index();
|
|
58
|
+
table.timestamp('updated_at').defaultTo(knex.fn.now()).notNullable();
|
|
59
|
+
|
|
60
|
+
table.index(['endpoint_id', 'created_at']);
|
|
61
|
+
table.index(['event', 'success']);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
exports.down = async function(knex) {
|
|
67
|
+
await knex.schema.dropTableIfExists('${deliveriesTable}');
|
|
68
|
+
await knex.schema.dropTableIfExists('${endpointsTable}');
|
|
69
|
+
};
|
|
70
|
+
`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = {
|
|
74
|
+
generateWebhooksMigration,
|
|
75
|
+
};
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webhooks ORM Models
|
|
3
|
+
* Webspresso ORM model definitions for WebhookEndpoint and WebhookDelivery
|
|
4
|
+
* @module plugins/webhooks/models
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const { defineModel } = require('../../core/orm/model');
|
|
10
|
+
const { zdb } = require('../../core/orm');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create WebhookEndpoint model definition
|
|
14
|
+
*
|
|
15
|
+
* @param {string} [tableName='webhook_endpoints']
|
|
16
|
+
* @returns {import('../../core/orm/types').ModelDefinition}
|
|
17
|
+
*/
|
|
18
|
+
function createWebhookEndpointModel(tableName = 'webhook_endpoints') {
|
|
19
|
+
const schema = zdb.schema({
|
|
20
|
+
id: zdb.id(),
|
|
21
|
+
name: zdb.string({ maxLength: 255 }),
|
|
22
|
+
url: zdb.string({ maxLength: 2048 }),
|
|
23
|
+
secret: zdb.string({ maxLength: 255 }),
|
|
24
|
+
events: zdb.text({ default: '["*"]' }),
|
|
25
|
+
is_active: zdb.boolean({ default: true }),
|
|
26
|
+
description: zdb.string({ maxLength: 1000, nullable: true }),
|
|
27
|
+
headers: zdb.json({ nullable: true }),
|
|
28
|
+
metadata: zdb.json({ nullable: true }),
|
|
29
|
+
created_at: zdb.timestamp({ auto: 'create' }),
|
|
30
|
+
updated_at: zdb.timestamp({ auto: 'update' }),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return defineModel({
|
|
34
|
+
name: 'WebhookEndpoint',
|
|
35
|
+
table: tableName,
|
|
36
|
+
schema,
|
|
37
|
+
scopes: {
|
|
38
|
+
timestamps: true,
|
|
39
|
+
active: (query) => query.where('is_active', true),
|
|
40
|
+
},
|
|
41
|
+
hidden: ['secret'], // Protect signing secret from serialization leaks
|
|
42
|
+
relations: {
|
|
43
|
+
deliveries: {
|
|
44
|
+
type: 'hasMany',
|
|
45
|
+
model: () => 'WebhookDelivery',
|
|
46
|
+
foreignKey: 'endpoint_id',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Create WebhookDelivery model definition
|
|
54
|
+
*
|
|
55
|
+
* @param {string} [tableName='webhook_deliveries']
|
|
56
|
+
* @returns {import('../../core/orm/types').ModelDefinition}
|
|
57
|
+
*/
|
|
58
|
+
function createWebhookDeliveryModel(tableName = 'webhook_deliveries') {
|
|
59
|
+
const schema = zdb.schema({
|
|
60
|
+
id: zdb.id(),
|
|
61
|
+
endpoint_id: zdb.integer(),
|
|
62
|
+
event: zdb.string({ maxLength: 255 }),
|
|
63
|
+
payload: zdb.text(),
|
|
64
|
+
status_code: zdb.integer({ nullable: true }),
|
|
65
|
+
success: zdb.boolean({ default: false }),
|
|
66
|
+
duration_ms: zdb.integer({ nullable: true }),
|
|
67
|
+
error: zdb.text({ nullable: true }),
|
|
68
|
+
response_body: zdb.text({ nullable: true }),
|
|
69
|
+
attempt: zdb.integer({ default: 1 }),
|
|
70
|
+
max_attempts: zdb.integer({ default: 3 }),
|
|
71
|
+
next_retry_at: zdb.timestamp({ nullable: true }),
|
|
72
|
+
created_at: zdb.timestamp({ auto: 'create' }),
|
|
73
|
+
updated_at: zdb.timestamp({ auto: 'update' }),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return defineModel({
|
|
77
|
+
name: 'WebhookDelivery',
|
|
78
|
+
table: tableName,
|
|
79
|
+
schema,
|
|
80
|
+
scopes: {
|
|
81
|
+
timestamps: true,
|
|
82
|
+
successful: (query) => query.where('success', true),
|
|
83
|
+
failed: (query) => query.where('success', false),
|
|
84
|
+
},
|
|
85
|
+
relations: {
|
|
86
|
+
endpoint: {
|
|
87
|
+
type: 'belongsTo',
|
|
88
|
+
model: () => 'WebhookEndpoint',
|
|
89
|
+
foreignKey: 'endpoint_id',
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = {
|
|
96
|
+
createWebhookEndpointModel,
|
|
97
|
+
createWebhookDeliveryModel,
|
|
98
|
+
};
|