@nexpress/plugin-webhook-relay 0.2.0

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,31 @@
1
+ import * as _nexpress_plugin_sdk from '@nexpress/plugin-sdk';
2
+ import { z } from 'zod';
3
+
4
+ declare const configSchema: z.ZodObject<{
5
+ endpointUrl: z.ZodOptional<z.ZodString>;
6
+ signingSecret: z.ZodOptional<z.ZodString>;
7
+ includeDrafts: z.ZodDefault<z.ZodBoolean>;
8
+ timeoutMs: z.ZodDefault<z.ZodNumber>;
9
+ }, z.core.$strip>;
10
+ type WebhookRelayConfig = z.infer<typeof configSchema>;
11
+ interface WebhookRelayPayload {
12
+ event: string;
13
+ collection: string;
14
+ documentId: string | null;
15
+ status: string | null;
16
+ at: string;
17
+ }
18
+ type HookData = Record<string, unknown> & {
19
+ collection?: string;
20
+ doc?: Record<string, unknown>;
21
+ };
22
+ declare function buildPayload(event: string, data: HookData): WebhookRelayPayload;
23
+ declare function signPayload(payload: WebhookRelayPayload, secret: string): string;
24
+ declare const webhookRelayPlugin: _nexpress_plugin_sdk.NpResolvedPlugin<{
25
+ includeDrafts: boolean;
26
+ timeoutMs: number;
27
+ endpointUrl?: string | undefined;
28
+ signingSecret?: string | undefined;
29
+ }>;
30
+
31
+ export { type WebhookRelayConfig, type WebhookRelayPayload, buildPayload, webhookRelayPlugin as default, signPayload, webhookRelayPlugin };
package/dist/index.js ADDED
@@ -0,0 +1,129 @@
1
+ // src/index.ts
2
+ import { createHmac } from "crypto";
3
+ import {
4
+ definePlugin,
5
+ npAdminActionError,
6
+ npAdminStatus
7
+ } from "@nexpress/plugin-sdk";
8
+ import { z } from "zod";
9
+ var configSchema = z.object({
10
+ endpointUrl: z.string().url().optional().describe("Webhook endpoint URL"),
11
+ signingSecret: z.string().optional().describe("Optional HMAC signing secret").meta({ sensitive: true }),
12
+ includeDrafts: z.boolean().default(false).describe("Send draft document events"),
13
+ timeoutMs: z.number().int().min(500).max(3e4).default(5e3).describe("Request timeout")
14
+ });
15
+ function pickDoc(data) {
16
+ return data.doc && typeof data.doc === "object" ? data.doc : data;
17
+ }
18
+ function buildPayload(event, data) {
19
+ const doc = pickDoc(data);
20
+ const id = typeof doc.id === "string" ? doc.id : null;
21
+ const status = typeof doc.status === "string" ? doc.status : null;
22
+ const collection = typeof data.collection === "string" ? data.collection : "unknown";
23
+ return {
24
+ event,
25
+ collection,
26
+ documentId: id,
27
+ status,
28
+ at: (/* @__PURE__ */ new Date()).toISOString()
29
+ };
30
+ }
31
+ function signPayload(payload, secret) {
32
+ return createHmac("sha256", secret).update(JSON.stringify(payload)).digest("hex");
33
+ }
34
+ var webhookRelayPlugin = definePlugin({
35
+ manifest: {
36
+ id: "webhook-relay",
37
+ version: "0.1.0",
38
+ name: "Webhook Relay",
39
+ description: "Relays content lifecycle events to a configured webhook endpoint and exposes admin delivery diagnostics.",
40
+ author: { name: "NexPress" },
41
+ license: "MIT",
42
+ nexpress: { minVersion: "0.1.0" },
43
+ allowedHosts: ["*"],
44
+ capabilities: ["network:fetch", "storage:kv"],
45
+ agent: {
46
+ description: "Integration plugin example that combines content hooks, outbound fetch, plugin storage, and declarative admin widgets.",
47
+ category: "integration",
48
+ tags: ["webhook", "integration", "content-hooks", "admin"]
49
+ }
50
+ },
51
+ configSchema,
52
+ hooks: {
53
+ "content:afterCreate": ({ data, ctx }) => deliver("content:afterCreate", data, ctx),
54
+ "content:afterUpdate": ({ data, ctx }) => deliver("content:afterUpdate", data, ctx),
55
+ "content:afterDelete": ({ data, ctx }) => deliver("content:afterDelete", data, ctx)
56
+ },
57
+ admin: {
58
+ widgets: [
59
+ {
60
+ id: "last-delivery",
61
+ label: "Last delivery",
62
+ kind: "status",
63
+ actionId: "lastDelivery"
64
+ }
65
+ ],
66
+ actions: [
67
+ {
68
+ id: "test-delivery",
69
+ label: "Send test delivery",
70
+ actionId: "sendTest",
71
+ confirm: "Send a test webhook delivery now?"
72
+ }
73
+ ]
74
+ },
75
+ setup: (ctx) => {
76
+ ctx.actions.registerStatus("lastDelivery", async () => {
77
+ const last = await ctx.storage.get("last-delivery");
78
+ return last ? npAdminStatus(last.ok ? "ok" : "warn", last.message) : npAdminStatus("warn", "No deliveries recorded yet.");
79
+ });
80
+ ctx.actions.register("sendTest", async () => {
81
+ const result = await deliver(
82
+ "webhook:test",
83
+ { collection: "test", doc: { id: "test", status: "published" } },
84
+ ctx
85
+ );
86
+ return result.ok ? { ok: true, data: result.message } : npAdminActionError(result.message);
87
+ });
88
+ }
89
+ });
90
+ async function deliver(event, data, ctx) {
91
+ const payload = buildPayload(event, data);
92
+ if (!ctx.config.includeDrafts && payload.status === "draft") {
93
+ return { ok: true, message: "Skipped draft document." };
94
+ }
95
+ if (!ctx.config.endpointUrl) {
96
+ const message2 = "Webhook endpoint is not configured.";
97
+ await ctx.storage.set("last-delivery", { ok: false, message: message2 }, { ttl: 30 * 24 * 60 * 60 });
98
+ return { ok: false, message: message2 };
99
+ }
100
+ const headers = { "content-type": "application/json" };
101
+ if (ctx.config.signingSecret) {
102
+ headers["x-np-signature"] = signPayload(payload, ctx.config.signingSecret);
103
+ }
104
+ const res = await ctx.http.fetch(ctx.config.endpointUrl, {
105
+ method: "POST",
106
+ headers,
107
+ body: { ...payload },
108
+ timeoutMs: ctx.config.timeoutMs
109
+ });
110
+ const ok = res.ok;
111
+ const message = ok ? `Delivered ${payload.event}` : `Endpoint returned HTTP ${res.status}`;
112
+ await ctx.storage.set(
113
+ "last-delivery",
114
+ { ok, message, at: payload.at },
115
+ { ttl: 30 * 24 * 60 * 60 }
116
+ );
117
+ if (!ok) {
118
+ ctx.log.warn("Webhook delivery failed", { status: String(res.status), event: payload.event });
119
+ }
120
+ return { ok, message };
121
+ }
122
+ var index_default = webhookRelayPlugin;
123
+ export {
124
+ buildPayload,
125
+ index_default as default,
126
+ signPayload,
127
+ webhookRelayPlugin
128
+ };
129
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["import { createHmac } from \"node:crypto\";\n\nimport {\n definePlugin,\n npAdminActionError,\n npAdminStatus,\n type NpPluginContext,\n} from \"@nexpress/plugin-sdk\";\nimport { z } from \"zod\";\n\nconst configSchema = z.object({\n endpointUrl: z.string().url().optional().describe(\"Webhook endpoint URL\"),\n signingSecret: z\n .string()\n .optional()\n .describe(\"Optional HMAC signing secret\")\n .meta({ sensitive: true }),\n includeDrafts: z.boolean().default(false).describe(\"Send draft document events\"),\n timeoutMs: z.number().int().min(500).max(30000).default(5000).describe(\"Request timeout\"),\n});\n\nexport type WebhookRelayConfig = z.infer<typeof configSchema>;\n\nexport interface WebhookRelayPayload {\n event: string;\n collection: string;\n documentId: string | null;\n status: string | null;\n at: string;\n}\n\ntype HookData = Record<string, unknown> & {\n collection?: string;\n doc?: Record<string, unknown>;\n};\n\nfunction pickDoc(data: HookData): Record<string, unknown> {\n return data.doc && typeof data.doc === \"object\" ? data.doc : data;\n}\n\nexport function buildPayload(event: string, data: HookData): WebhookRelayPayload {\n const doc = pickDoc(data);\n const id = typeof doc.id === \"string\" ? doc.id : null;\n const status = typeof doc.status === \"string\" ? doc.status : null;\n const collection = typeof data.collection === \"string\" ? data.collection : \"unknown\";\n\n return {\n event,\n collection,\n documentId: id,\n status,\n at: new Date().toISOString(),\n };\n}\n\nexport function signPayload(payload: WebhookRelayPayload, secret: string): string {\n return createHmac(\"sha256\", secret).update(JSON.stringify(payload)).digest(\"hex\");\n}\n\nexport const webhookRelayPlugin = definePlugin<WebhookRelayConfig>({\n manifest: {\n id: \"webhook-relay\",\n version: \"0.1.0\",\n name: \"Webhook Relay\",\n description:\n \"Relays content lifecycle events to a configured webhook endpoint and exposes admin delivery diagnostics.\",\n author: { name: \"NexPress\" },\n license: \"MIT\",\n nexpress: { minVersion: \"0.1.0\" },\n allowedHosts: [\"*\"],\n capabilities: [\"network:fetch\", \"storage:kv\"],\n agent: {\n description:\n \"Integration plugin example that combines content hooks, outbound fetch, plugin storage, and declarative admin widgets.\",\n category: \"integration\",\n tags: [\"webhook\", \"integration\", \"content-hooks\", \"admin\"],\n },\n },\n configSchema,\n hooks: {\n \"content:afterCreate\": ({ data, ctx }) => deliver(\"content:afterCreate\", data, ctx),\n \"content:afterUpdate\": ({ data, ctx }) => deliver(\"content:afterUpdate\", data, ctx),\n \"content:afterDelete\": ({ data, ctx }) => deliver(\"content:afterDelete\", data, ctx),\n },\n admin: {\n widgets: [\n {\n id: \"last-delivery\",\n label: \"Last delivery\",\n kind: \"status\",\n actionId: \"lastDelivery\",\n },\n ],\n actions: [\n {\n id: \"test-delivery\",\n label: \"Send test delivery\",\n actionId: \"sendTest\",\n confirm: \"Send a test webhook delivery now?\",\n },\n ],\n },\n setup: (ctx) => {\n ctx.actions.registerStatus(\"lastDelivery\", async () => {\n const last = await ctx.storage.get<{ ok: boolean; message: string }>(\"last-delivery\");\n return last\n ? npAdminStatus(last.ok ? \"ok\" : \"warn\", last.message)\n : npAdminStatus(\"warn\", \"No deliveries recorded yet.\");\n });\n\n ctx.actions.register(\"sendTest\", async () => {\n const result = await deliver(\n \"webhook:test\",\n { collection: \"test\", doc: { id: \"test\", status: \"published\" } },\n ctx,\n );\n return result.ok ? { ok: true, data: result.message } : npAdminActionError(result.message);\n });\n },\n});\n\nasync function deliver(\n event: string,\n data: Record<string, unknown>,\n ctx: Pick<NpPluginContext<WebhookRelayConfig>, \"config\" | \"http\" | \"storage\" | \"log\">,\n): Promise<{ ok: boolean; message: string }> {\n const payload = buildPayload(event, data);\n\n if (!ctx.config.includeDrafts && payload.status === \"draft\") {\n return { ok: true, message: \"Skipped draft document.\" };\n }\n\n if (!ctx.config.endpointUrl) {\n const message = \"Webhook endpoint is not configured.\";\n await ctx.storage.set(\"last-delivery\", { ok: false, message }, { ttl: 30 * 24 * 60 * 60 });\n return { ok: false, message };\n }\n\n const headers: Record<string, string> = { \"content-type\": \"application/json\" };\n if (ctx.config.signingSecret) {\n headers[\"x-np-signature\"] = signPayload(payload, ctx.config.signingSecret);\n }\n\n const res = await ctx.http.fetch(ctx.config.endpointUrl, {\n method: \"POST\",\n headers,\n body: { ...payload },\n timeoutMs: ctx.config.timeoutMs,\n });\n const ok = res.ok;\n const message = ok ? `Delivered ${payload.event}` : `Endpoint returned HTTP ${res.status}`;\n await ctx.storage.set(\n \"last-delivery\",\n { ok, message, at: payload.at },\n { ttl: 30 * 24 * 60 * 60 },\n );\n\n if (!ok) {\n ctx.log.warn(\"Webhook delivery failed\", { status: String(res.status), event: payload.event });\n }\n\n return { ok, message };\n}\n\nexport default webhookRelayPlugin;\n"],"mappings":";AAAA,SAAS,kBAAkB;AAE3B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OAEK;AACP,SAAS,SAAS;AAElB,IAAM,eAAe,EAAE,OAAO;AAAA,EAC5B,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,sBAAsB;AAAA,EACxE,eAAe,EACZ,OAAO,EACP,SAAS,EACT,SAAS,8BAA8B,EACvC,KAAK,EAAE,WAAW,KAAK,CAAC;AAAA,EAC3B,eAAe,EAAE,QAAQ,EAAE,QAAQ,KAAK,EAAE,SAAS,4BAA4B;AAAA,EAC/E,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,GAAG,EAAE,IAAI,GAAK,EAAE,QAAQ,GAAI,EAAE,SAAS,iBAAiB;AAC1F,CAAC;AAiBD,SAAS,QAAQ,MAAyC;AACxD,SAAO,KAAK,OAAO,OAAO,KAAK,QAAQ,WAAW,KAAK,MAAM;AAC/D;AAEO,SAAS,aAAa,OAAe,MAAqC;AAC/E,QAAM,MAAM,QAAQ,IAAI;AACxB,QAAM,KAAK,OAAO,IAAI,OAAO,WAAW,IAAI,KAAK;AACjD,QAAM,SAAS,OAAO,IAAI,WAAW,WAAW,IAAI,SAAS;AAC7D,QAAM,aAAa,OAAO,KAAK,eAAe,WAAW,KAAK,aAAa;AAE3E,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,YAAY;AAAA,IACZ;AAAA,IACA,KAAI,oBAAI,KAAK,GAAE,YAAY;AAAA,EAC7B;AACF;AAEO,SAAS,YAAY,SAA8B,QAAwB;AAChF,SAAO,WAAW,UAAU,MAAM,EAAE,OAAO,KAAK,UAAU,OAAO,CAAC,EAAE,OAAO,KAAK;AAClF;AAEO,IAAM,qBAAqB,aAAiC;AAAA,EACjE,UAAU;AAAA,IACR,IAAI;AAAA,IACJ,SAAS;AAAA,IACT,MAAM;AAAA,IACN,aACE;AAAA,IACF,QAAQ,EAAE,MAAM,WAAW;AAAA,IAC3B,SAAS;AAAA,IACT,UAAU,EAAE,YAAY,QAAQ;AAAA,IAChC,cAAc,CAAC,GAAG;AAAA,IAClB,cAAc,CAAC,iBAAiB,YAAY;AAAA,IAC5C,OAAO;AAAA,MACL,aACE;AAAA,MACF,UAAU;AAAA,MACV,MAAM,CAAC,WAAW,eAAe,iBAAiB,OAAO;AAAA,IAC3D;AAAA,EACF;AAAA,EACA;AAAA,EACA,OAAO;AAAA,IACL,uBAAuB,CAAC,EAAE,MAAM,IAAI,MAAM,QAAQ,uBAAuB,MAAM,GAAG;AAAA,IAClF,uBAAuB,CAAC,EAAE,MAAM,IAAI,MAAM,QAAQ,uBAAuB,MAAM,GAAG;AAAA,IAClF,uBAAuB,CAAC,EAAE,MAAM,IAAI,MAAM,QAAQ,uBAAuB,MAAM,GAAG;AAAA,EACpF;AAAA,EACA,OAAO;AAAA,IACL,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,OAAO;AAAA,QACP,MAAM;AAAA,QACN,UAAU;AAAA,MACZ;AAAA,IACF;AAAA,IACA,SAAS;AAAA,MACP;AAAA,QACE,IAAI;AAAA,QACJ,OAAO;AAAA,QACP,UAAU;AAAA,QACV,SAAS;AAAA,MACX;AAAA,IACF;AAAA,EACF;AAAA,EACA,OAAO,CAAC,QAAQ;AACd,QAAI,QAAQ,eAAe,gBAAgB,YAAY;AACrD,YAAM,OAAO,MAAM,IAAI,QAAQ,IAAsC,eAAe;AACpF,aAAO,OACH,cAAc,KAAK,KAAK,OAAO,QAAQ,KAAK,OAAO,IACnD,cAAc,QAAQ,6BAA6B;AAAA,IACzD,CAAC;AAED,QAAI,QAAQ,SAAS,YAAY,YAAY;AAC3C,YAAM,SAAS,MAAM;AAAA,QACnB;AAAA,QACA,EAAE,YAAY,QAAQ,KAAK,EAAE,IAAI,QAAQ,QAAQ,YAAY,EAAE;AAAA,QAC/D;AAAA,MACF;AACA,aAAO,OAAO,KAAK,EAAE,IAAI,MAAM,MAAM,OAAO,QAAQ,IAAI,mBAAmB,OAAO,OAAO;AAAA,IAC3F,CAAC;AAAA,EACH;AACF,CAAC;AAED,eAAe,QACb,OACA,MACA,KAC2C;AAC3C,QAAM,UAAU,aAAa,OAAO,IAAI;AAExC,MAAI,CAAC,IAAI,OAAO,iBAAiB,QAAQ,WAAW,SAAS;AAC3D,WAAO,EAAE,IAAI,MAAM,SAAS,0BAA0B;AAAA,EACxD;AAEA,MAAI,CAAC,IAAI,OAAO,aAAa;AAC3B,UAAMA,WAAU;AAChB,UAAM,IAAI,QAAQ,IAAI,iBAAiB,EAAE,IAAI,OAAO,SAAAA,SAAQ,GAAG,EAAE,KAAK,KAAK,KAAK,KAAK,GAAG,CAAC;AACzF,WAAO,EAAE,IAAI,OAAO,SAAAA,SAAQ;AAAA,EAC9B;AAEA,QAAM,UAAkC,EAAE,gBAAgB,mBAAmB;AAC7E,MAAI,IAAI,OAAO,eAAe;AAC5B,YAAQ,gBAAgB,IAAI,YAAY,SAAS,IAAI,OAAO,aAAa;AAAA,EAC3E;AAEA,QAAM,MAAM,MAAM,IAAI,KAAK,MAAM,IAAI,OAAO,aAAa;AAAA,IACvD,QAAQ;AAAA,IACR;AAAA,IACA,MAAM,EAAE,GAAG,QAAQ;AAAA,IACnB,WAAW,IAAI,OAAO;AAAA,EACxB,CAAC;AACD,QAAM,KAAK,IAAI;AACf,QAAM,UAAU,KAAK,aAAa,QAAQ,KAAK,KAAK,0BAA0B,IAAI,MAAM;AACxF,QAAM,IAAI,QAAQ;AAAA,IAChB;AAAA,IACA,EAAE,IAAI,SAAS,IAAI,QAAQ,GAAG;AAAA,IAC9B,EAAE,KAAK,KAAK,KAAK,KAAK,GAAG;AAAA,EAC3B;AAEA,MAAI,CAAC,IAAI;AACP,QAAI,IAAI,KAAK,2BAA2B,EAAE,QAAQ,OAAO,IAAI,MAAM,GAAG,OAAO,QAAQ,MAAM,CAAC;AAAA,EAC9F;AAEA,SAAO,EAAE,IAAI,QAAQ;AACvB;AAEA,IAAO,gBAAQ;","names":["message"]}
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@nexpress/plugin-webhook-relay",
3
+ "version": "0.2.0",
4
+ "description": "Webhook relay plugin for NexPress content lifecycle events.",
5
+ "license": "MIT",
6
+ "author": "Nexpress",
7
+ "homepage": "https://github.com/nexpress-cms/nexpress#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/nexpress-cms/nexpress.git",
11
+ "directory": "packages/plugins/webhook-relay"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/nexpress-cms/nexpress/issues"
15
+ },
16
+ "keywords": [
17
+ "nexpress",
18
+ "plugin",
19
+ "webhook",
20
+ "integration"
21
+ ],
22
+ "type": "module",
23
+ "main": "./dist/index.js",
24
+ "types": "./dist/index.d.ts",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "import": "./dist/index.js",
29
+ "default": "./dist/index.js"
30
+ }
31
+ },
32
+ "files": [
33
+ "dist"
34
+ ],
35
+ "engines": {
36
+ "node": ">=20"
37
+ },
38
+ "dependencies": {
39
+ "@nexpress/plugin-sdk": "workspace:*",
40
+ "zod": "^4.4.3"
41
+ },
42
+ "devDependencies": {
43
+ "@types/node": "^22.0.0",
44
+ "tsup": "^8.5.0",
45
+ "typescript": "^5.8.0",
46
+ "vitest": "^4.1.6"
47
+ },
48
+ "scripts": {
49
+ "build": "tsup",
50
+ "dev": "tsup --watch --no-clean",
51
+ "clean": "rm -rf dist",
52
+ "typecheck": "tsc --noEmit",
53
+ "test": "vitest run",
54
+ "lint": "eslint . --cache --cache-location node_modules/.cache/eslint"
55
+ }
56
+ }