@emdash-cms/plugin-webhook-notifier 0.1.0 → 0.1.2

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandbox-entry.mjs","names":[],"sources":["../src/sandbox-entry.ts"],"sourcesContent":["/**\n * Sandbox Entry Point -- Webhook Notifier\n *\n * Canonical plugin implementation using the standard format.\n * Runs in both trusted (in-process) and sandboxed (isolate) modes.\n */\n\nimport { definePlugin } from \"emdash\";\nimport type { PluginContext } from \"emdash\";\n\ninterface ContentSaveEvent {\n\tcontent: Record<string, unknown>;\n\tcollection: string;\n\tisNew: boolean;\n}\n\ninterface ContentDeleteEvent {\n\tid: string;\n\tcollection: string;\n}\n\ninterface MediaUploadEvent {\n\tmedia: { id: string };\n}\n\ninterface WebhookPayload {\n\tevent: string;\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// ── SSRF protection ──\n\nconst IPV6_BRACKET_PATTERN = /^\\[|\\]$/g;\nconst BLOCKED_HOSTNAMES = new Set([\"localhost\", \"metadata.google.internal\", \"[::1]\"]);\nconst PRIVATE_RANGES = [\n\t{ start: (127 << 24) >>> 0, end: ((127 << 24) | 0x00ffffff) >>> 0 },\n\t{ start: (10 << 24) >>> 0, end: ((10 << 24) | 0x00ffffff) >>> 0 },\n\t{\n\t\tstart: ((172 << 24) | (16 << 16)) >>> 0,\n\t\tend: ((172 << 24) | (31 << 16) | 0xffff) >>> 0,\n\t},\n\t{\n\t\tstart: ((192 << 24) | (168 << 16)) >>> 0,\n\t\tend: ((192 << 24) | (168 << 16) | 0xffff) >>> 0,\n\t},\n\t{\n\t\tstart: ((169 << 24) | (254 << 16)) >>> 0,\n\t\tend: ((169 << 24) | (254 << 16) | 0xffff) >>> 0,\n\t},\n\t{ start: 0, end: 0x00ffffff },\n];\n\nfunction validateWebhookUrl(url: string): void {\n\tlet parsed: URL;\n\ttry {\n\t\tparsed = new URL(url);\n\t} catch {\n\t\tthrow new Error(\"Invalid webhook URL\");\n\t}\n\tif (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n\t\tthrow new Error(`Webhook URL scheme '${parsed.protocol}' is not allowed`);\n\t}\n\tconst hostname = parsed.hostname.replace(IPV6_BRACKET_PATTERN, \"\");\n\tif (BLOCKED_HOSTNAMES.has(hostname.toLowerCase())) {\n\t\tthrow new Error(\"Webhook URLs targeting internal hosts are not allowed\");\n\t}\n\tconst parts = hostname.split(\".\");\n\tif (parts.length === 4) {\n\t\tconst nums = parts.map(Number);\n\t\tif (nums.every((n) => !isNaN(n) && n >= 0 && n <= 255)) {\n\t\t\tconst ip = ((nums[0]! << 24) | (nums[1]! << 16) | (nums[2]! << 8) | nums[3]!) >>> 0;\n\t\t\tif (PRIVATE_RANGES.some((r) => ip >= r.start && ip <= r.end)) {\n\t\t\t\tthrow new Error(\"Webhook URLs targeting private IP addresses are not allowed\");\n\t\t\t}\n\t\t}\n\t}\n\tif (\n\t\thostname === \"::1\" ||\n\t\thostname.startsWith(\"fe80:\") ||\n\t\thostname.startsWith(\"fc\") ||\n\t\thostname.startsWith(\"fd\")\n\t) {\n\t\tthrow new Error(\"Webhook URLs targeting internal addresses are not allowed\");\n\t}\n}\n\n// ── Webhook delivery ──\n\ntype FetchFn = (url: string, init?: RequestInit) => Promise<Response>;\ntype LogFn = PluginContext[\"log\"];\n\nasync function sendWebhook(\n\tfetchFn: FetchFn,\n\tlog: LogFn,\n\turl: string,\n\tpayload: WebhookPayload,\n\ttoken: string | undefined,\n\tmaxRetries: number,\n): Promise<{ success: boolean; status?: number; error?: string }> {\n\tvalidateWebhookUrl(url);\n\n\tlet lastError: string | undefined;\n\tlet lastStatus: number | undefined;\n\n\tfor (let attempt = 1; attempt <= maxRetries; attempt++) {\n\t\ttry {\n\t\t\tconst headers: Record<string, string> = {\n\t\t\t\t\"Content-Type\": \"application/json\",\n\t\t\t\t\"X-EmDash-Event\": payload.event,\n\t\t\t};\n\t\t\tif (token) headers[\"Authorization\"] = `Bearer ${token}`;\n\n\t\t\tconst response = await fetchFn(url, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders,\n\t\t\t\tbody: JSON.stringify(payload),\n\t\t\t});\n\n\t\t\tlastStatus = response.status;\n\t\t\tif (response.ok) {\n\t\t\t\tlog.info(`Delivered ${payload.event} to ${url} (${response.status})`);\n\t\t\t\treturn { success: true, status: response.status };\n\t\t\t}\n\n\t\t\tlastError = `HTTP ${response.status}: ${response.statusText}`;\n\t\t\tlog.warn(`Attempt ${attempt}/${maxRetries} failed: ${lastError}`);\n\t\t} catch (error) {\n\t\t\tlastError = error instanceof Error ? error.message : \"Unknown error\";\n\t\t\tlog.warn(`Attempt ${attempt}/${maxRetries} failed: ${lastError}`);\n\t\t}\n\n\t\tif (attempt < maxRetries) {\n\t\t\tawait new Promise((resolve) => setTimeout(resolve, 100 * Math.pow(2, attempt - 1)));\n\t\t}\n\t}\n\n\tlog.error(`Failed to deliver ${payload.event} after ${maxRetries} attempts`);\n\treturn { success: false, status: lastStatus, error: lastError };\n}\n\n// ── Helpers ──\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction getString(value: unknown, key: string): string | undefined {\n\tif (!isRecord(value)) return undefined;\n\tconst v = value[key];\n\treturn typeof v === \"string\" ? v : undefined;\n}\n\nconst MAX_RETRIES = 3;\n\nasync function getConfig(ctx: PluginContext) {\n\tconst url = await ctx.kv.get<string>(\"settings:webhookUrl\");\n\tconst token = await ctx.kv.get<string>(\"settings:secretToken\");\n\tconst enabled = await ctx.kv.get<boolean>(\"settings:enabled\");\n\treturn { url, token, enabled };\n}\n\nfunction getFetchFn(ctx: PluginContext): FetchFn {\n\tif (!ctx.http) {\n\t\tthrow new Error(\"Webhook notifier requires network:fetch capability\");\n\t}\n\treturn ctx.http.fetch;\n}\n\n// ── Plugin definition ──\n\nexport default definePlugin({\n\thooks: {\n\t\t\"content:afterSave\": {\n\t\t\tpriority: 210,\n\t\t\ttimeout: 10000,\n\t\t\tdependencies: [\"audit-log\"],\n\t\t\terrorPolicy: \"continue\",\n\t\t\thandler: async (event: ContentSaveEvent, ctx: PluginContext) => {\n\t\t\t\tconst { url, token, enabled } = await getConfig(ctx);\n\t\t\t\tif (enabled === false || !url) return;\n\n\t\t\t\tconst contentId =\n\t\t\t\t\ttypeof event.content.id === \"string\" ? event.content.id : String(event.content.id);\n\n\t\t\t\tconst payload: WebhookPayload = {\n\t\t\t\t\tevent: event.isNew ? \"content:create\" : \"content:update\",\n\t\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\t\tcollection: event.collection,\n\t\t\t\t\tresourceId: contentId,\n\t\t\t\t\tresourceType: \"content\",\n\t\t\t\t\tmetadata: {\n\t\t\t\t\t\tslug: event.content.slug,\n\t\t\t\t\t\tstatus: event.content.status,\n\t\t\t\t\t},\n\t\t\t\t};\n\n\t\t\t\tawait sendWebhook(getFetchFn(ctx), ctx.log, url, payload, token ?? undefined, MAX_RETRIES);\n\t\t\t},\n\t\t},\n\n\t\t\"content:afterDelete\": {\n\t\t\tpriority: 210,\n\t\t\ttimeout: 10000,\n\t\t\tdependencies: [\"audit-log\"],\n\t\t\terrorPolicy: \"continue\",\n\t\t\thandler: async (event: ContentDeleteEvent, ctx: PluginContext) => {\n\t\t\t\tconst { url, token, enabled } = await getConfig(ctx);\n\t\t\t\tif (enabled === false || !url) return;\n\n\t\t\t\tconst payload: WebhookPayload = {\n\t\t\t\t\tevent: \"content:delete\",\n\t\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\t\tcollection: event.collection,\n\t\t\t\t\tresourceId: event.id,\n\t\t\t\t\tresourceType: \"content\",\n\t\t\t\t};\n\n\t\t\t\tawait sendWebhook(getFetchFn(ctx), ctx.log, url, payload, token ?? undefined, MAX_RETRIES);\n\t\t\t},\n\t\t},\n\n\t\t\"media:afterUpload\": {\n\t\t\tpriority: 210,\n\t\t\ttimeout: 10000,\n\t\t\terrorPolicy: \"continue\",\n\t\t\thandler: async (event: MediaUploadEvent, ctx: PluginContext) => {\n\t\t\t\tconst { url, token, enabled } = await getConfig(ctx);\n\t\t\t\tif (enabled === false || !url) return;\n\n\t\t\t\tconst payload: WebhookPayload = {\n\t\t\t\t\tevent: \"media:upload\",\n\t\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\t\tresourceId: event.media.id,\n\t\t\t\t\tresourceType: \"media\",\n\t\t\t\t};\n\n\t\t\t\tawait sendWebhook(getFetchFn(ctx), ctx.log, url, payload, token ?? undefined, MAX_RETRIES);\n\t\t\t},\n\t\t},\n\t},\n\n\troutes: {\n\t\tadmin: {\n\t\t\thandler: async (\n\t\t\t\trouteCtx: { input: unknown; request: { url: string } },\n\t\t\t\tctx: PluginContext,\n\t\t\t) => {\n\t\t\t\tconst interaction = routeCtx.input as {\n\t\t\t\t\ttype: string;\n\t\t\t\t\tpage?: string;\n\t\t\t\t\taction_id?: string;\n\t\t\t\t\tvalue?: string;\n\t\t\t\t\tvalues?: Record<string, unknown>;\n\t\t\t\t};\n\n\t\t\t\tif (interaction.type === \"page_load\" && interaction.page === \"widget:webhook-status\") {\n\t\t\t\t\treturn buildStatusWidget(ctx);\n\t\t\t\t}\n\t\t\t\tif (interaction.type === \"page_load\" && interaction.page === \"/settings\") {\n\t\t\t\t\treturn buildSettingsPage(ctx);\n\t\t\t\t}\n\t\t\t\tif (interaction.type === \"form_submit\" && interaction.action_id === \"save_settings\") {\n\t\t\t\t\treturn saveSettings(ctx, interaction.values ?? {});\n\t\t\t\t}\n\t\t\t\tif (interaction.type === \"block_action\" && interaction.action_id === \"test_webhook\") {\n\t\t\t\t\treturn testWebhook(ctx);\n\t\t\t\t}\n\t\t\t\treturn { blocks: [] };\n\t\t\t},\n\t\t},\n\n\t\tstatus: {\n\t\t\thandler: async (_routeCtx: { input: unknown; request: unknown }, ctx: PluginContext) => {\n\t\t\t\ttry {\n\t\t\t\t\tconst url = await ctx.kv.get<string>(\"settings:webhookUrl\");\n\t\t\t\t\tconst enabled = await ctx.kv.get<boolean>(\"settings:enabled\");\n\t\t\t\t\tconst deliveries = ctx.storage.deliveries!;\n\t\t\t\t\tconst successful = await deliveries.count({ status: \"success\" });\n\t\t\t\t\tconst failed = await deliveries.count({ status: \"failed\" });\n\t\t\t\t\tconst pending = await deliveries.count({ status: \"pending\" });\n\n\t\t\t\t\treturn {\n\t\t\t\t\t\tconfigured: !!url,\n\t\t\t\t\t\tenabled: enabled ?? true,\n\t\t\t\t\t\tstats: { successful, failed, pending },\n\t\t\t\t\t};\n\t\t\t\t} catch (error) {\n\t\t\t\t\tctx.log.error(\"Failed to get status\", error);\n\t\t\t\t\treturn {\n\t\t\t\t\t\tconfigured: false,\n\t\t\t\t\t\tenabled: true,\n\t\t\t\t\t\tstats: { successful: 0, failed: 0, pending: 0 },\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\n\t\tsettings: {\n\t\t\thandler: async (_routeCtx: { input: unknown; request: unknown }, ctx: PluginContext) => {\n\t\t\t\ttry {\n\t\t\t\t\tconst settings = await ctx.kv.list(\"settings:\");\n\t\t\t\t\tconst map: Record<string, unknown> = {};\n\t\t\t\t\tfor (const entry of settings) {\n\t\t\t\t\t\tmap[entry.key.replace(\"settings:\", \"\")] = entry.value;\n\t\t\t\t\t}\n\t\t\t\t\treturn {\n\t\t\t\t\t\twebhookUrl: typeof map.webhookUrl === \"string\" ? map.webhookUrl : \"\",\n\t\t\t\t\t\tenabled: typeof map.enabled === \"boolean\" ? map.enabled : true,\n\t\t\t\t\t\tincludeData: typeof map.includeData === \"boolean\" ? map.includeData : false,\n\t\t\t\t\t\tevents: typeof map.events === \"string\" ? map.events : \"all\",\n\t\t\t\t\t};\n\t\t\t\t} catch (error) {\n\t\t\t\t\tctx.log.error(\"Failed to get settings\", error);\n\t\t\t\t\treturn { webhookUrl: \"\", enabled: true, includeData: false, events: \"all\" };\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\n\t\t\"settings/save\": {\n\t\t\thandler: async (routeCtx: { input: unknown; request: unknown }, ctx: PluginContext) => {\n\t\t\t\ttry {\n\t\t\t\t\tconst input = isRecord(routeCtx.input) ? routeCtx.input : {};\n\t\t\t\t\tif (typeof input.webhookUrl === \"string\")\n\t\t\t\t\t\tawait ctx.kv.set(\"settings:webhookUrl\", input.webhookUrl);\n\t\t\t\t\tif (typeof input.enabled === \"boolean\")\n\t\t\t\t\t\tawait ctx.kv.set(\"settings:enabled\", input.enabled);\n\t\t\t\t\tif (typeof input.includeData === \"boolean\")\n\t\t\t\t\t\tawait ctx.kv.set(\"settings:includeData\", input.includeData);\n\t\t\t\t\tif (typeof input.events === \"string\") await ctx.kv.set(\"settings:events\", input.events);\n\t\t\t\t\treturn { success: true };\n\t\t\t\t} catch (error) {\n\t\t\t\t\tctx.log.error(\"Failed to save settings\", error);\n\t\t\t\t\treturn { success: false, error: String(error) };\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\n\t\ttest: {\n\t\t\thandler: async (routeCtx: { input: unknown; request: unknown }, ctx: PluginContext) => {\n\t\t\t\tconst testUrl = getString(routeCtx.input, \"url\");\n\t\t\t\tif (!testUrl) return { success: false, error: \"No webhook URL provided\" };\n\n\t\t\t\tconst token = await ctx.kv.get<string>(\"settings:secretToken\");\n\n\t\t\t\tconst testPayload: WebhookPayload = {\n\t\t\t\t\tevent: \"content:create\",\n\t\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\t\tresourceId: \"test-\" + Date.now(),\n\t\t\t\t\tresourceType: \"content\",\n\t\t\t\t\tmetadata: { test: true, message: \"Webhook test from EmDash CMS\" },\n\t\t\t\t};\n\n\t\t\t\tconst result = await sendWebhook(\n\t\t\t\t\tgetFetchFn(ctx),\n\t\t\t\t\tctx.log,\n\t\t\t\t\ttestUrl,\n\t\t\t\t\ttestPayload,\n\t\t\t\t\ttoken ?? undefined,\n\t\t\t\t\t1,\n\t\t\t\t);\n\t\t\t\treturn {\n\t\t\t\t\tsuccess: result.success,\n\t\t\t\t\tstatus: result.status,\n\t\t\t\t\terror: result.error,\n\t\t\t\t\tpayload: testPayload,\n\t\t\t\t};\n\t\t\t},\n\t\t},\n\t},\n});\n\n// ── Block Kit admin helpers ──\n\nasync function buildStatusWidget(ctx: PluginContext) {\n\ttry {\n\t\tconst url = await ctx.kv.get<string>(\"settings:webhookUrl\");\n\t\tconst enabled = await ctx.kv.get<boolean>(\"settings:enabled\");\n\t\tconst isConfigured = !!url && enabled !== false;\n\n\t\tlet successful = 0;\n\t\tlet failed = 0;\n\t\tlet pending = 0;\n\t\ttry {\n\t\t\tconst deliveries = ctx.storage.deliveries!;\n\t\t\tsuccessful = await deliveries.count({ status: \"success\" });\n\t\t\tfailed = await deliveries.count({ status: \"failed\" });\n\t\t\tpending = await deliveries.count({ status: \"pending\" });\n\t\t} catch {\n\t\t\t// Storage not available yet\n\t\t}\n\n\t\tconst blocks: unknown[] = [\n\t\t\t{\n\t\t\t\ttype: \"fields\",\n\t\t\t\tfields: [\n\t\t\t\t\t{\n\t\t\t\t\t\tlabel: \"Status\",\n\t\t\t\t\t\tvalue: isConfigured ? \"Active\" : \"Not Configured\",\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tlabel: \"Endpoint\",\n\t\t\t\t\t\tvalue: url ? url : \"None\",\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t},\n\t\t];\n\n\t\tif (isConfigured) {\n\t\t\tblocks.push({\n\t\t\t\ttype: \"stats\",\n\t\t\t\tstats: [\n\t\t\t\t\t{ label: \"Delivered\", value: String(successful) },\n\t\t\t\t\t{ label: \"Failed\", value: String(failed) },\n\t\t\t\t\t{ label: \"Pending\", value: String(pending) },\n\t\t\t\t],\n\t\t\t});\n\t\t} else {\n\t\t\tblocks.push({\n\t\t\t\ttype: \"context\",\n\t\t\t\ttext: \"Configure a webhook URL in settings to start sending events.\",\n\t\t\t});\n\t\t}\n\n\t\treturn { blocks };\n\t} catch (error) {\n\t\tctx.log.error(\"Failed to build status widget\", error);\n\t\treturn { blocks: [{ type: \"context\", text: \"Failed to load webhook status\" }] };\n\t}\n}\n\nasync function buildSettingsPage(ctx: PluginContext) {\n\ttry {\n\t\tconst webhookUrl = (await ctx.kv.get<string>(\"settings:webhookUrl\")) ?? \"\";\n\t\tconst enabled = (await ctx.kv.get<boolean>(\"settings:enabled\")) ?? true;\n\t\tconst includeData = (await ctx.kv.get<boolean>(\"settings:includeData\")) ?? false;\n\t\tconst events = (await ctx.kv.get<string>(\"settings:events\")) ?? \"all\";\n\n\t\tconst payloadPreview = JSON.stringify(\n\t\t\t{\n\t\t\t\tevent: \"content:create\",\n\t\t\t\ttimestamp: new Date().toISOString(),\n\t\t\t\tcollection: \"posts\",\n\t\t\t\tresourceId: \"abc123\",\n\t\t\t\tresourceType: \"content\",\n\t\t\t\t...(includeData && {\n\t\t\t\t\tdata: { title: \"Example Post\", slug: \"example-post\" },\n\t\t\t\t}),\n\t\t\t\tmetadata: { slug: \"example-post\", status: \"published\" },\n\t\t\t},\n\t\t\tnull,\n\t\t\t2,\n\t\t);\n\n\t\treturn {\n\t\t\tblocks: [\n\t\t\t\t{ type: \"header\", text: \"Webhook Settings\" },\n\t\t\t\t{\n\t\t\t\t\ttype: \"context\",\n\t\t\t\t\ttext: \"Send notifications to external services when content changes.\",\n\t\t\t\t},\n\t\t\t\t{ type: \"divider\" },\n\t\t\t\t{\n\t\t\t\t\ttype: \"form\",\n\t\t\t\t\tblock_id: \"webhook-settings\",\n\t\t\t\t\tfields: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text_input\",\n\t\t\t\t\t\t\taction_id: \"webhookUrl\",\n\t\t\t\t\t\t\tlabel: \"Webhook URL\",\n\t\t\t\t\t\t\tinitial_value: webhookUrl,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"secret_input\",\n\t\t\t\t\t\t\taction_id: \"secretToken\",\n\t\t\t\t\t\t\tlabel: \"Secret Token\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"toggle\",\n\t\t\t\t\t\t\taction_id: \"enabled\",\n\t\t\t\t\t\t\tlabel: \"Enable Webhooks\",\n\t\t\t\t\t\t\tinitial_value: enabled,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"select\",\n\t\t\t\t\t\t\taction_id: \"events\",\n\t\t\t\t\t\t\tlabel: \"Events to Send\",\n\t\t\t\t\t\t\toptions: [\n\t\t\t\t\t\t\t\t{ label: \"All events\", value: \"all\" },\n\t\t\t\t\t\t\t\t{ label: \"Content changes only\", value: \"content\" },\n\t\t\t\t\t\t\t\t{ label: \"Media uploads only\", value: \"media\" },\n\t\t\t\t\t\t\t],\n\t\t\t\t\t\t\tinitial_value: events,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"toggle\",\n\t\t\t\t\t\t\taction_id: \"includeData\",\n\t\t\t\t\t\t\tlabel: \"Include Content Data\",\n\t\t\t\t\t\t\tinitial_value: includeData,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tsubmit: { label: \"Save Settings\", action_id: \"save_settings\" },\n\t\t\t\t},\n\t\t\t\t{ type: \"divider\" },\n\t\t\t\t{ type: \"section\", text: \"**Payload Preview**\" },\n\t\t\t\t{ type: \"section\", text: \"```json\\n\" + payloadPreview + \"\\n```\" },\n\t\t\t\t{\n\t\t\t\t\ttype: \"actions\",\n\t\t\t\t\telements: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"button\",\n\t\t\t\t\t\t\ttext: \"Test Webhook\",\n\t\t\t\t\t\t\taction_id: \"test_webhook\",\n\t\t\t\t\t\t\tstyle: \"primary\",\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t],\n\t\t};\n\t} catch (error) {\n\t\tctx.log.error(\"Failed to build settings page\", error);\n\t\treturn { blocks: [{ type: \"context\", text: \"Failed to load settings\" }] };\n\t}\n}\n\nasync function saveSettings(ctx: PluginContext, values: Record<string, unknown>) {\n\ttry {\n\t\tif (typeof values.webhookUrl === \"string\")\n\t\t\tawait ctx.kv.set(\"settings:webhookUrl\", values.webhookUrl);\n\t\tif (typeof values.secretToken === \"string\" && values.secretToken !== \"\")\n\t\t\tawait ctx.kv.set(\"settings:secretToken\", values.secretToken);\n\t\tif (typeof values.enabled === \"boolean\") await ctx.kv.set(\"settings:enabled\", values.enabled);\n\t\tif (typeof values.events === \"string\") await ctx.kv.set(\"settings:events\", values.events);\n\t\tif (typeof values.includeData === \"boolean\")\n\t\t\tawait ctx.kv.set(\"settings:includeData\", values.includeData);\n\n\t\treturn {\n\t\t\t...(await buildSettingsPage(ctx)),\n\t\t\ttoast: { message: \"Settings saved\", type: \"success\" },\n\t\t};\n\t} catch (error) {\n\t\tctx.log.error(\"Failed to save settings\", error);\n\t\treturn {\n\t\t\tblocks: [{ type: \"banner\", style: \"error\", text: \"Failed to save settings\" }],\n\t\t\ttoast: { message: \"Failed to save settings\", type: \"error\" },\n\t\t};\n\t}\n}\n\nasync function testWebhook(ctx: PluginContext) {\n\tconst url = await ctx.kv.get<string>(\"settings:webhookUrl\");\n\tif (!url) {\n\t\treturn {\n\t\t\tblocks: [{ type: \"banner\", style: \"warning\", text: \"Enter a webhook URL first.\" }],\n\t\t\ttoast: { message: \"No webhook URL configured\", type: \"error\" },\n\t\t};\n\t}\n\n\tconst token = await ctx.kv.get<string>(\"settings:secretToken\");\n\tconst testPayload: WebhookPayload = {\n\t\tevent: \"content:create\",\n\t\ttimestamp: new Date().toISOString(),\n\t\tresourceId: \"test-\" + Date.now(),\n\t\tresourceType: \"content\",\n\t\tmetadata: { test: true, message: \"Webhook test from EmDash CMS\" },\n\t};\n\n\ttry {\n\t\tconst result = await sendWebhook(\n\t\t\tgetFetchFn(ctx),\n\t\t\tctx.log,\n\t\t\turl,\n\t\t\ttestPayload,\n\t\t\ttoken ?? undefined,\n\t\t\t1,\n\t\t);\n\n\t\tif (result.success) {\n\t\t\treturn {\n\t\t\t\t...(await buildSettingsPage(ctx)),\n\t\t\t\ttoast: { message: `Test sent -- HTTP ${result.status}`, type: \"success\" },\n\t\t\t};\n\t\t}\n\t\treturn {\n\t\t\t...(await buildSettingsPage(ctx)),\n\t\t\ttoast: {\n\t\t\t\tmessage: `Test failed: ${result.error ?? \"Unknown error\"}`,\n\t\t\t\ttype: \"error\",\n\t\t\t},\n\t\t};\n\t} catch (error) {\n\t\tconst msg = error instanceof Error ? error.message : String(error);\n\t\treturn {\n\t\t\t...(await buildSettingsPage(ctx)),\n\t\t\ttoast: { message: `Test failed: ${msg}`, type: \"error\" },\n\t\t};\n\t}\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,15 +1,18 @@
1
1
  {
2
2
  "name": "@emdash-cms/plugin-webhook-notifier",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Webhook notification plugin for EmDash CMS - posts to external URLs on content changes",
5
5
  "type": "module",
6
- "main": "src/index.ts",
6
+ "main": "dist/index.mjs",
7
7
  "exports": {
8
- ".": "./src/index.ts",
9
- "./sandbox": "./src/sandbox-entry.ts"
8
+ ".": {
9
+ "import": "./dist/index.mjs",
10
+ "types": "./dist/index.d.mts"
11
+ },
12
+ "./sandbox": "./dist/sandbox-entry.mjs"
10
13
  },
11
14
  "files": [
12
- "src"
15
+ "dist"
13
16
  ],
14
17
  "keywords": [
15
18
  "emdash",
@@ -22,10 +25,12 @@
22
25
  "author": "Matt Kane",
23
26
  "license": "MIT",
24
27
  "peerDependencies": {
25
- "emdash": "0.1.0"
28
+ "emdash": ">=0.2.0"
29
+ },
30
+ "devDependencies": {
31
+ "tsdown": "0.20.3",
32
+ "typescript": "^5.9.3"
26
33
  },
27
- "devDependencies": {},
28
- "dependencies": {},
29
34
  "optionalDependencies": {},
30
35
  "repository": {
31
36
  "type": "git",
@@ -33,6 +38,8 @@
33
38
  "directory": "packages/plugins/webhook-notifier"
34
39
  },
35
40
  "scripts": {
41
+ "build": "tsdown src/index.ts src/sandbox-entry.ts --format esm --dts --clean",
42
+ "dev": "tsdown src/index.ts src/sandbox-entry.ts --format esm --dts --watch",
36
43
  "typecheck": "tsgo --noEmit"
37
44
  }
38
45
  }
package/src/index.ts DELETED
@@ -1,50 +0,0 @@
1
- /**
2
- * Webhook Notifier Plugin for EmDash CMS
3
- *
4
- * Posts to external URLs when content changes occur.
5
- *
6
- * Features:
7
- * - Configurable webhook URLs (admin settings)
8
- * - Secret token for authentication (encrypted)
9
- * - Retry logic with exponential backoff
10
- * - Event filtering by collection and action
11
- * - Manual trigger via API route
12
- *
13
- * Demonstrates:
14
- * - network:fetch:any capability (unrestricted outbound for user-configured URLs)
15
- * - settings.secret() for encrypted tokens
16
- * - apiRoutes for custom endpoints
17
- * - content:afterDelete hook
18
- * - Hook dependencies (runs after audit-log)
19
- * - errorPolicy: "continue" (don't block save on webhook failure)
20
- */
21
-
22
- import type { PluginDescriptor } from "emdash";
23
-
24
- export interface WebhookPayload {
25
- event: "content:create" | "content:update" | "content:delete" | "media:upload";
26
- timestamp: string;
27
- collection?: string;
28
- resourceId: string;
29
- resourceType: "content" | "media";
30
- data?: Record<string, unknown>;
31
- metadata?: Record<string, unknown>;
32
- }
33
-
34
- /**
35
- * Create the webhook notifier plugin descriptor
36
- */
37
- export function webhookNotifierPlugin(): PluginDescriptor {
38
- return {
39
- id: "webhook-notifier",
40
- version: "0.1.0",
41
- format: "standard",
42
- entrypoint: "@emdash-cms/plugin-webhook-notifier/sandbox",
43
- capabilities: ["network:fetch:any"],
44
- storage: {
45
- deliveries: { indexes: ["timestamp", "webhookUrl", "status"] },
46
- },
47
- adminPages: [{ path: "/settings", label: "Webhook Settings", icon: "send" }],
48
- adminWidgets: [{ id: "status", title: "Webhooks", size: "third" }],
49
- };
50
- }