@emdash-cms/plugin-webhook-notifier 0.0.3 → 0.1.1
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/LICENSE +9 -0
- package/dist/index.d.mts +19 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +32 -0
- package/dist/index.mjs.map +1 -0
- package/dist/sandbox-entry.d.mts +13 -0
- package/dist/sandbox-entry.d.mts.map +1 -0
- package/dist/sandbox-entry.mjs +533 -0
- package/dist/sandbox-entry.mjs.map +1 -0
- package/package.json +16 -9
- package/src/index.ts +0 -50
- package/src/sandbox-entry.ts +0 -602
package/LICENSE
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
The MIT License
|
|
2
|
+
|
|
3
|
+
Copyright 2026 Cloudflare Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { PluginDescriptor } from "emdash";
|
|
2
|
+
|
|
3
|
+
//#region src/index.d.ts
|
|
4
|
+
interface WebhookPayload {
|
|
5
|
+
event: "content:create" | "content:update" | "content:delete" | "media:upload";
|
|
6
|
+
timestamp: string;
|
|
7
|
+
collection?: string;
|
|
8
|
+
resourceId: string;
|
|
9
|
+
resourceType: "content" | "media";
|
|
10
|
+
data?: Record<string, unknown>;
|
|
11
|
+
metadata?: Record<string, unknown>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Create the webhook notifier plugin descriptor
|
|
15
|
+
*/
|
|
16
|
+
declare function webhookNotifierPlugin(): PluginDescriptor;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { WebhookPayload, webhookNotifierPlugin };
|
|
19
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;UAuBiB,cAAA;EAChB,KAAA;EACA,SAAA;EACA,UAAA;EACA,UAAA;EACA,YAAA;EACA,IAAA,GAAO,MAAA;EACP,QAAA,GAAW,MAAA;AAAA;;;;iBAMI,qBAAA,CAAA,GAAyB,gBAAA"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/index.ts
|
|
2
|
+
/**
|
|
3
|
+
* Create the webhook notifier plugin descriptor
|
|
4
|
+
*/
|
|
5
|
+
function webhookNotifierPlugin() {
|
|
6
|
+
return {
|
|
7
|
+
id: "webhook-notifier",
|
|
8
|
+
version: "0.1.0",
|
|
9
|
+
format: "standard",
|
|
10
|
+
entrypoint: "@emdash-cms/plugin-webhook-notifier/sandbox",
|
|
11
|
+
capabilities: ["network:fetch:any"],
|
|
12
|
+
storage: { deliveries: { indexes: [
|
|
13
|
+
"timestamp",
|
|
14
|
+
"webhookUrl",
|
|
15
|
+
"status"
|
|
16
|
+
] } },
|
|
17
|
+
adminPages: [{
|
|
18
|
+
path: "/settings",
|
|
19
|
+
label: "Webhook Settings",
|
|
20
|
+
icon: "send"
|
|
21
|
+
}],
|
|
22
|
+
adminWidgets: [{
|
|
23
|
+
id: "status",
|
|
24
|
+
title: "Webhooks",
|
|
25
|
+
size: "third"
|
|
26
|
+
}]
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
//#endregion
|
|
31
|
+
export { webhookNotifierPlugin };
|
|
32
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * Webhook Notifier Plugin for EmDash CMS\n *\n * Posts to external URLs when content changes occur.\n *\n * Features:\n * - Configurable webhook URLs (admin settings)\n * - Secret token for authentication (encrypted)\n * - Retry logic with exponential backoff\n * - Event filtering by collection and action\n * - Manual trigger via API route\n *\n * Demonstrates:\n * - network:fetch:any capability (unrestricted outbound for user-configured URLs)\n * - settings.secret() for encrypted tokens\n * - apiRoutes for custom endpoints\n * - content:afterDelete hook\n * - Hook dependencies (runs after audit-log)\n * - errorPolicy: \"continue\" (don't block save on webhook failure)\n */\n\nimport type { PluginDescriptor } from \"emdash\";\n\nexport interface WebhookPayload {\n\tevent: \"content:create\" | \"content:update\" | \"content:delete\" | \"media:upload\";\n\ttimestamp: string;\n\tcollection?: string;\n\tresourceId: string;\n\tresourceType: \"content\" | \"media\";\n\tdata?: Record<string, unknown>;\n\tmetadata?: Record<string, unknown>;\n}\n\n/**\n * Create the webhook notifier plugin descriptor\n */\nexport function webhookNotifierPlugin(): PluginDescriptor {\n\treturn {\n\t\tid: \"webhook-notifier\",\n\t\tversion: \"0.1.0\",\n\t\tformat: \"standard\",\n\t\tentrypoint: \"@emdash-cms/plugin-webhook-notifier/sandbox\",\n\t\tcapabilities: [\"network:fetch:any\"],\n\t\tstorage: {\n\t\t\tdeliveries: { indexes: [\"timestamp\", \"webhookUrl\", \"status\"] },\n\t\t},\n\t\tadminPages: [{ path: \"/settings\", label: \"Webhook Settings\", icon: \"send\" }],\n\t\tadminWidgets: [{ id: \"status\", title: \"Webhooks\", size: \"third\" }],\n\t};\n}\n"],"mappings":";;;;AAoCA,SAAgB,wBAA0C;AACzD,QAAO;EACN,IAAI;EACJ,SAAS;EACT,QAAQ;EACR,YAAY;EACZ,cAAc,CAAC,oBAAoB;EACnC,SAAS,EACR,YAAY,EAAE,SAAS;GAAC;GAAa;GAAc;GAAS,EAAE,EAC9D;EACD,YAAY,CAAC;GAAE,MAAM;GAAa,OAAO;GAAoB,MAAM;GAAQ,CAAC;EAC5E,cAAc,CAAC;GAAE,IAAI;GAAU,OAAO;GAAY,MAAM;GAAS,CAAC;EAClE"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as emdash from "emdash";
|
|
2
|
+
|
|
3
|
+
//#region src/sandbox-entry.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Sandbox Entry Point -- Webhook Notifier
|
|
6
|
+
*
|
|
7
|
+
* Canonical plugin implementation using the standard format.
|
|
8
|
+
* Runs in both trusted (in-process) and sandboxed (isolate) modes.
|
|
9
|
+
*/
|
|
10
|
+
declare const _default: emdash.StandardPluginDefinition;
|
|
11
|
+
//#endregion
|
|
12
|
+
export { _default as default };
|
|
13
|
+
//# sourceMappingURL=sandbox-entry.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sandbox-entry.d.mts","names":[],"sources":["../src/sandbox-entry.ts"],"mappings":""}
|
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
import { definePlugin } from "emdash";
|
|
2
|
+
|
|
3
|
+
//#region src/sandbox-entry.ts
|
|
4
|
+
/**
|
|
5
|
+
* Sandbox Entry Point -- Webhook Notifier
|
|
6
|
+
*
|
|
7
|
+
* Canonical plugin implementation using the standard format.
|
|
8
|
+
* Runs in both trusted (in-process) and sandboxed (isolate) modes.
|
|
9
|
+
*/
|
|
10
|
+
const IPV6_BRACKET_PATTERN = /^\[|\]$/g;
|
|
11
|
+
const BLOCKED_HOSTNAMES = new Set([
|
|
12
|
+
"localhost",
|
|
13
|
+
"metadata.google.internal",
|
|
14
|
+
"[::1]"
|
|
15
|
+
]);
|
|
16
|
+
const PRIVATE_RANGES = [
|
|
17
|
+
{
|
|
18
|
+
start: 127 << 24 >>> 0,
|
|
19
|
+
end: 2147483647
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
start: 10 << 24 >>> 0,
|
|
23
|
+
end: 184549375
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
start: 2886729728,
|
|
27
|
+
end: 2887778303
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
start: 3232235520,
|
|
31
|
+
end: 3232301055
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
start: 2851995648,
|
|
35
|
+
end: 2852061183
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
start: 0,
|
|
39
|
+
end: 16777215
|
|
40
|
+
}
|
|
41
|
+
];
|
|
42
|
+
function validateWebhookUrl(url) {
|
|
43
|
+
let parsed;
|
|
44
|
+
try {
|
|
45
|
+
parsed = new URL(url);
|
|
46
|
+
} catch {
|
|
47
|
+
throw new Error("Invalid webhook URL");
|
|
48
|
+
}
|
|
49
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error(`Webhook URL scheme '${parsed.protocol}' is not allowed`);
|
|
50
|
+
const hostname = parsed.hostname.replace(IPV6_BRACKET_PATTERN, "");
|
|
51
|
+
if (BLOCKED_HOSTNAMES.has(hostname.toLowerCase())) throw new Error("Webhook URLs targeting internal hosts are not allowed");
|
|
52
|
+
const parts = hostname.split(".");
|
|
53
|
+
if (parts.length === 4) {
|
|
54
|
+
const nums = parts.map(Number);
|
|
55
|
+
if (nums.every((n) => !isNaN(n) && n >= 0 && n <= 255)) {
|
|
56
|
+
const ip = (nums[0] << 24 | nums[1] << 16 | nums[2] << 8 | nums[3]) >>> 0;
|
|
57
|
+
if (PRIVATE_RANGES.some((r) => ip >= r.start && ip <= r.end)) throw new Error("Webhook URLs targeting private IP addresses are not allowed");
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (hostname === "::1" || hostname.startsWith("fe80:") || hostname.startsWith("fc") || hostname.startsWith("fd")) throw new Error("Webhook URLs targeting internal addresses are not allowed");
|
|
61
|
+
}
|
|
62
|
+
async function sendWebhook(fetchFn, log, url, payload, token, maxRetries) {
|
|
63
|
+
validateWebhookUrl(url);
|
|
64
|
+
let lastError;
|
|
65
|
+
let lastStatus;
|
|
66
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
67
|
+
try {
|
|
68
|
+
const headers = {
|
|
69
|
+
"Content-Type": "application/json",
|
|
70
|
+
"X-EmDash-Event": payload.event
|
|
71
|
+
};
|
|
72
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
73
|
+
const response = await fetchFn(url, {
|
|
74
|
+
method: "POST",
|
|
75
|
+
headers,
|
|
76
|
+
body: JSON.stringify(payload)
|
|
77
|
+
});
|
|
78
|
+
lastStatus = response.status;
|
|
79
|
+
if (response.ok) {
|
|
80
|
+
log.info(`Delivered ${payload.event} to ${url} (${response.status})`);
|
|
81
|
+
return {
|
|
82
|
+
success: true,
|
|
83
|
+
status: response.status
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
lastError = `HTTP ${response.status}: ${response.statusText}`;
|
|
87
|
+
log.warn(`Attempt ${attempt}/${maxRetries} failed: ${lastError}`);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
lastError = error instanceof Error ? error.message : "Unknown error";
|
|
90
|
+
log.warn(`Attempt ${attempt}/${maxRetries} failed: ${lastError}`);
|
|
91
|
+
}
|
|
92
|
+
if (attempt < maxRetries) await new Promise((resolve) => setTimeout(resolve, 100 * Math.pow(2, attempt - 1)));
|
|
93
|
+
}
|
|
94
|
+
log.error(`Failed to deliver ${payload.event} after ${maxRetries} attempts`);
|
|
95
|
+
return {
|
|
96
|
+
success: false,
|
|
97
|
+
status: lastStatus,
|
|
98
|
+
error: lastError
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function isRecord(value) {
|
|
102
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
103
|
+
}
|
|
104
|
+
function getString(value, key) {
|
|
105
|
+
if (!isRecord(value)) return void 0;
|
|
106
|
+
const v = value[key];
|
|
107
|
+
return typeof v === "string" ? v : void 0;
|
|
108
|
+
}
|
|
109
|
+
const MAX_RETRIES = 3;
|
|
110
|
+
async function getConfig(ctx) {
|
|
111
|
+
return {
|
|
112
|
+
url: await ctx.kv.get("settings:webhookUrl"),
|
|
113
|
+
token: await ctx.kv.get("settings:secretToken"),
|
|
114
|
+
enabled: await ctx.kv.get("settings:enabled")
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function getFetchFn(ctx) {
|
|
118
|
+
if (!ctx.http) throw new Error("Webhook notifier requires network:fetch capability");
|
|
119
|
+
return ctx.http.fetch;
|
|
120
|
+
}
|
|
121
|
+
var sandbox_entry_default = definePlugin({
|
|
122
|
+
hooks: {
|
|
123
|
+
"content:afterSave": {
|
|
124
|
+
priority: 210,
|
|
125
|
+
timeout: 1e4,
|
|
126
|
+
dependencies: ["audit-log"],
|
|
127
|
+
errorPolicy: "continue",
|
|
128
|
+
handler: async (event, ctx) => {
|
|
129
|
+
const { url, token, enabled } = await getConfig(ctx);
|
|
130
|
+
if (enabled === false || !url) return;
|
|
131
|
+
const contentId = typeof event.content.id === "string" ? event.content.id : String(event.content.id);
|
|
132
|
+
const payload = {
|
|
133
|
+
event: event.isNew ? "content:create" : "content:update",
|
|
134
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
135
|
+
collection: event.collection,
|
|
136
|
+
resourceId: contentId,
|
|
137
|
+
resourceType: "content",
|
|
138
|
+
metadata: {
|
|
139
|
+
slug: event.content.slug,
|
|
140
|
+
status: event.content.status
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
await sendWebhook(getFetchFn(ctx), ctx.log, url, payload, token ?? void 0, MAX_RETRIES);
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
"content:afterDelete": {
|
|
147
|
+
priority: 210,
|
|
148
|
+
timeout: 1e4,
|
|
149
|
+
dependencies: ["audit-log"],
|
|
150
|
+
errorPolicy: "continue",
|
|
151
|
+
handler: async (event, ctx) => {
|
|
152
|
+
const { url, token, enabled } = await getConfig(ctx);
|
|
153
|
+
if (enabled === false || !url) return;
|
|
154
|
+
const payload = {
|
|
155
|
+
event: "content:delete",
|
|
156
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
157
|
+
collection: event.collection,
|
|
158
|
+
resourceId: event.id,
|
|
159
|
+
resourceType: "content"
|
|
160
|
+
};
|
|
161
|
+
await sendWebhook(getFetchFn(ctx), ctx.log, url, payload, token ?? void 0, MAX_RETRIES);
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
"media:afterUpload": {
|
|
165
|
+
priority: 210,
|
|
166
|
+
timeout: 1e4,
|
|
167
|
+
errorPolicy: "continue",
|
|
168
|
+
handler: async (event, ctx) => {
|
|
169
|
+
const { url, token, enabled } = await getConfig(ctx);
|
|
170
|
+
if (enabled === false || !url) return;
|
|
171
|
+
const payload = {
|
|
172
|
+
event: "media:upload",
|
|
173
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
174
|
+
resourceId: event.media.id,
|
|
175
|
+
resourceType: "media"
|
|
176
|
+
};
|
|
177
|
+
await sendWebhook(getFetchFn(ctx), ctx.log, url, payload, token ?? void 0, MAX_RETRIES);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
routes: {
|
|
182
|
+
admin: { handler: async (routeCtx, ctx) => {
|
|
183
|
+
const interaction = routeCtx.input;
|
|
184
|
+
if (interaction.type === "page_load" && interaction.page === "widget:webhook-status") return buildStatusWidget(ctx);
|
|
185
|
+
if (interaction.type === "page_load" && interaction.page === "/settings") return buildSettingsPage(ctx);
|
|
186
|
+
if (interaction.type === "form_submit" && interaction.action_id === "save_settings") return saveSettings(ctx, interaction.values ?? {});
|
|
187
|
+
if (interaction.type === "block_action" && interaction.action_id === "test_webhook") return testWebhook(ctx);
|
|
188
|
+
return { blocks: [] };
|
|
189
|
+
} },
|
|
190
|
+
status: { handler: async (_routeCtx, ctx) => {
|
|
191
|
+
try {
|
|
192
|
+
const url = await ctx.kv.get("settings:webhookUrl");
|
|
193
|
+
const enabled = await ctx.kv.get("settings:enabled");
|
|
194
|
+
const deliveries = ctx.storage.deliveries;
|
|
195
|
+
const successful = await deliveries.count({ status: "success" });
|
|
196
|
+
const failed = await deliveries.count({ status: "failed" });
|
|
197
|
+
const pending = await deliveries.count({ status: "pending" });
|
|
198
|
+
return {
|
|
199
|
+
configured: !!url,
|
|
200
|
+
enabled: enabled ?? true,
|
|
201
|
+
stats: {
|
|
202
|
+
successful,
|
|
203
|
+
failed,
|
|
204
|
+
pending
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
} catch (error) {
|
|
208
|
+
ctx.log.error("Failed to get status", error);
|
|
209
|
+
return {
|
|
210
|
+
configured: false,
|
|
211
|
+
enabled: true,
|
|
212
|
+
stats: {
|
|
213
|
+
successful: 0,
|
|
214
|
+
failed: 0,
|
|
215
|
+
pending: 0
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
} },
|
|
220
|
+
settings: { handler: async (_routeCtx, ctx) => {
|
|
221
|
+
try {
|
|
222
|
+
const settings = await ctx.kv.list("settings:");
|
|
223
|
+
const map = {};
|
|
224
|
+
for (const entry of settings) map[entry.key.replace("settings:", "")] = entry.value;
|
|
225
|
+
return {
|
|
226
|
+
webhookUrl: typeof map.webhookUrl === "string" ? map.webhookUrl : "",
|
|
227
|
+
enabled: typeof map.enabled === "boolean" ? map.enabled : true,
|
|
228
|
+
includeData: typeof map.includeData === "boolean" ? map.includeData : false,
|
|
229
|
+
events: typeof map.events === "string" ? map.events : "all"
|
|
230
|
+
};
|
|
231
|
+
} catch (error) {
|
|
232
|
+
ctx.log.error("Failed to get settings", error);
|
|
233
|
+
return {
|
|
234
|
+
webhookUrl: "",
|
|
235
|
+
enabled: true,
|
|
236
|
+
includeData: false,
|
|
237
|
+
events: "all"
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
} },
|
|
241
|
+
"settings/save": { handler: async (routeCtx, ctx) => {
|
|
242
|
+
try {
|
|
243
|
+
const input = isRecord(routeCtx.input) ? routeCtx.input : {};
|
|
244
|
+
if (typeof input.webhookUrl === "string") await ctx.kv.set("settings:webhookUrl", input.webhookUrl);
|
|
245
|
+
if (typeof input.enabled === "boolean") await ctx.kv.set("settings:enabled", input.enabled);
|
|
246
|
+
if (typeof input.includeData === "boolean") await ctx.kv.set("settings:includeData", input.includeData);
|
|
247
|
+
if (typeof input.events === "string") await ctx.kv.set("settings:events", input.events);
|
|
248
|
+
return { success: true };
|
|
249
|
+
} catch (error) {
|
|
250
|
+
ctx.log.error("Failed to save settings", error);
|
|
251
|
+
return {
|
|
252
|
+
success: false,
|
|
253
|
+
error: String(error)
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
} },
|
|
257
|
+
test: { handler: async (routeCtx, ctx) => {
|
|
258
|
+
const testUrl = getString(routeCtx.input, "url");
|
|
259
|
+
if (!testUrl) return {
|
|
260
|
+
success: false,
|
|
261
|
+
error: "No webhook URL provided"
|
|
262
|
+
};
|
|
263
|
+
const token = await ctx.kv.get("settings:secretToken");
|
|
264
|
+
const testPayload = {
|
|
265
|
+
event: "content:create",
|
|
266
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
267
|
+
resourceId: "test-" + Date.now(),
|
|
268
|
+
resourceType: "content",
|
|
269
|
+
metadata: {
|
|
270
|
+
test: true,
|
|
271
|
+
message: "Webhook test from EmDash CMS"
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
const result = await sendWebhook(getFetchFn(ctx), ctx.log, testUrl, testPayload, token ?? void 0, 1);
|
|
275
|
+
return {
|
|
276
|
+
success: result.success,
|
|
277
|
+
status: result.status,
|
|
278
|
+
error: result.error,
|
|
279
|
+
payload: testPayload
|
|
280
|
+
};
|
|
281
|
+
} }
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
async function buildStatusWidget(ctx) {
|
|
285
|
+
try {
|
|
286
|
+
const url = await ctx.kv.get("settings:webhookUrl");
|
|
287
|
+
const enabled = await ctx.kv.get("settings:enabled");
|
|
288
|
+
const isConfigured = !!url && enabled !== false;
|
|
289
|
+
let successful = 0;
|
|
290
|
+
let failed = 0;
|
|
291
|
+
let pending = 0;
|
|
292
|
+
try {
|
|
293
|
+
const deliveries = ctx.storage.deliveries;
|
|
294
|
+
successful = await deliveries.count({ status: "success" });
|
|
295
|
+
failed = await deliveries.count({ status: "failed" });
|
|
296
|
+
pending = await deliveries.count({ status: "pending" });
|
|
297
|
+
} catch {}
|
|
298
|
+
const blocks = [{
|
|
299
|
+
type: "fields",
|
|
300
|
+
fields: [{
|
|
301
|
+
label: "Status",
|
|
302
|
+
value: isConfigured ? "Active" : "Not Configured"
|
|
303
|
+
}, {
|
|
304
|
+
label: "Endpoint",
|
|
305
|
+
value: url ? url : "None"
|
|
306
|
+
}]
|
|
307
|
+
}];
|
|
308
|
+
if (isConfigured) blocks.push({
|
|
309
|
+
type: "stats",
|
|
310
|
+
stats: [
|
|
311
|
+
{
|
|
312
|
+
label: "Delivered",
|
|
313
|
+
value: String(successful)
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
label: "Failed",
|
|
317
|
+
value: String(failed)
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
label: "Pending",
|
|
321
|
+
value: String(pending)
|
|
322
|
+
}
|
|
323
|
+
]
|
|
324
|
+
});
|
|
325
|
+
else blocks.push({
|
|
326
|
+
type: "context",
|
|
327
|
+
text: "Configure a webhook URL in settings to start sending events."
|
|
328
|
+
});
|
|
329
|
+
return { blocks };
|
|
330
|
+
} catch (error) {
|
|
331
|
+
ctx.log.error("Failed to build status widget", error);
|
|
332
|
+
return { blocks: [{
|
|
333
|
+
type: "context",
|
|
334
|
+
text: "Failed to load webhook status"
|
|
335
|
+
}] };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
async function buildSettingsPage(ctx) {
|
|
339
|
+
try {
|
|
340
|
+
const webhookUrl = await ctx.kv.get("settings:webhookUrl") ?? "";
|
|
341
|
+
const enabled = await ctx.kv.get("settings:enabled") ?? true;
|
|
342
|
+
const includeData = await ctx.kv.get("settings:includeData") ?? false;
|
|
343
|
+
const events = await ctx.kv.get("settings:events") ?? "all";
|
|
344
|
+
const payloadPreview = JSON.stringify({
|
|
345
|
+
event: "content:create",
|
|
346
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
347
|
+
collection: "posts",
|
|
348
|
+
resourceId: "abc123",
|
|
349
|
+
resourceType: "content",
|
|
350
|
+
...includeData && { data: {
|
|
351
|
+
title: "Example Post",
|
|
352
|
+
slug: "example-post"
|
|
353
|
+
} },
|
|
354
|
+
metadata: {
|
|
355
|
+
slug: "example-post",
|
|
356
|
+
status: "published"
|
|
357
|
+
}
|
|
358
|
+
}, null, 2);
|
|
359
|
+
return { blocks: [
|
|
360
|
+
{
|
|
361
|
+
type: "header",
|
|
362
|
+
text: "Webhook Settings"
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
type: "context",
|
|
366
|
+
text: "Send notifications to external services when content changes."
|
|
367
|
+
},
|
|
368
|
+
{ type: "divider" },
|
|
369
|
+
{
|
|
370
|
+
type: "form",
|
|
371
|
+
block_id: "webhook-settings",
|
|
372
|
+
fields: [
|
|
373
|
+
{
|
|
374
|
+
type: "text_input",
|
|
375
|
+
action_id: "webhookUrl",
|
|
376
|
+
label: "Webhook URL",
|
|
377
|
+
initial_value: webhookUrl
|
|
378
|
+
},
|
|
379
|
+
{
|
|
380
|
+
type: "secret_input",
|
|
381
|
+
action_id: "secretToken",
|
|
382
|
+
label: "Secret Token"
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
type: "toggle",
|
|
386
|
+
action_id: "enabled",
|
|
387
|
+
label: "Enable Webhooks",
|
|
388
|
+
initial_value: enabled
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
type: "select",
|
|
392
|
+
action_id: "events",
|
|
393
|
+
label: "Events to Send",
|
|
394
|
+
options: [
|
|
395
|
+
{
|
|
396
|
+
label: "All events",
|
|
397
|
+
value: "all"
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
label: "Content changes only",
|
|
401
|
+
value: "content"
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
label: "Media uploads only",
|
|
405
|
+
value: "media"
|
|
406
|
+
}
|
|
407
|
+
],
|
|
408
|
+
initial_value: events
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
type: "toggle",
|
|
412
|
+
action_id: "includeData",
|
|
413
|
+
label: "Include Content Data",
|
|
414
|
+
initial_value: includeData
|
|
415
|
+
}
|
|
416
|
+
],
|
|
417
|
+
submit: {
|
|
418
|
+
label: "Save Settings",
|
|
419
|
+
action_id: "save_settings"
|
|
420
|
+
}
|
|
421
|
+
},
|
|
422
|
+
{ type: "divider" },
|
|
423
|
+
{
|
|
424
|
+
type: "section",
|
|
425
|
+
text: "**Payload Preview**"
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
type: "code",
|
|
429
|
+
code: payloadPreview,
|
|
430
|
+
language: "json"
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
type: "actions",
|
|
434
|
+
elements: [{
|
|
435
|
+
type: "button",
|
|
436
|
+
text: "Test Webhook",
|
|
437
|
+
action_id: "test_webhook",
|
|
438
|
+
style: "primary"
|
|
439
|
+
}]
|
|
440
|
+
}
|
|
441
|
+
] };
|
|
442
|
+
} catch (error) {
|
|
443
|
+
ctx.log.error("Failed to build settings page", error);
|
|
444
|
+
return { blocks: [{
|
|
445
|
+
type: "context",
|
|
446
|
+
text: "Failed to load settings"
|
|
447
|
+
}] };
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
async function saveSettings(ctx, values) {
|
|
451
|
+
try {
|
|
452
|
+
if (typeof values.webhookUrl === "string") await ctx.kv.set("settings:webhookUrl", values.webhookUrl);
|
|
453
|
+
if (typeof values.secretToken === "string" && values.secretToken !== "") await ctx.kv.set("settings:secretToken", values.secretToken);
|
|
454
|
+
if (typeof values.enabled === "boolean") await ctx.kv.set("settings:enabled", values.enabled);
|
|
455
|
+
if (typeof values.events === "string") await ctx.kv.set("settings:events", values.events);
|
|
456
|
+
if (typeof values.includeData === "boolean") await ctx.kv.set("settings:includeData", values.includeData);
|
|
457
|
+
return {
|
|
458
|
+
...await buildSettingsPage(ctx),
|
|
459
|
+
toast: {
|
|
460
|
+
message: "Settings saved",
|
|
461
|
+
type: "success"
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
} catch (error) {
|
|
465
|
+
ctx.log.error("Failed to save settings", error);
|
|
466
|
+
return {
|
|
467
|
+
blocks: [{
|
|
468
|
+
type: "banner",
|
|
469
|
+
style: "error",
|
|
470
|
+
text: "Failed to save settings"
|
|
471
|
+
}],
|
|
472
|
+
toast: {
|
|
473
|
+
message: "Failed to save settings",
|
|
474
|
+
type: "error"
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
async function testWebhook(ctx) {
|
|
480
|
+
const url = await ctx.kv.get("settings:webhookUrl");
|
|
481
|
+
if (!url) return {
|
|
482
|
+
blocks: [{
|
|
483
|
+
type: "banner",
|
|
484
|
+
style: "warning",
|
|
485
|
+
text: "Enter a webhook URL first."
|
|
486
|
+
}],
|
|
487
|
+
toast: {
|
|
488
|
+
message: "No webhook URL configured",
|
|
489
|
+
type: "error"
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
const token = await ctx.kv.get("settings:secretToken");
|
|
493
|
+
const testPayload = {
|
|
494
|
+
event: "content:create",
|
|
495
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
496
|
+
resourceId: "test-" + Date.now(),
|
|
497
|
+
resourceType: "content",
|
|
498
|
+
metadata: {
|
|
499
|
+
test: true,
|
|
500
|
+
message: "Webhook test from EmDash CMS"
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
try {
|
|
504
|
+
const result = await sendWebhook(getFetchFn(ctx), ctx.log, url, testPayload, token ?? void 0, 1);
|
|
505
|
+
if (result.success) return {
|
|
506
|
+
...await buildSettingsPage(ctx),
|
|
507
|
+
toast: {
|
|
508
|
+
message: `Test sent -- HTTP ${result.status}`,
|
|
509
|
+
type: "success"
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
return {
|
|
513
|
+
...await buildSettingsPage(ctx),
|
|
514
|
+
toast: {
|
|
515
|
+
message: `Test failed: ${result.error ?? "Unknown error"}`,
|
|
516
|
+
type: "error"
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
} catch (error) {
|
|
520
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
521
|
+
return {
|
|
522
|
+
...await buildSettingsPage(ctx),
|
|
523
|
+
toast: {
|
|
524
|
+
message: `Test failed: ${msg}`,
|
|
525
|
+
type: "error"
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
//#endregion
|
|
532
|
+
export { sandbox_entry_default as default };
|
|
533
|
+
//# sourceMappingURL=sandbox-entry.mjs.map
|