@blokjs/trigger-webhook 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.
- package/CHANGELOG.md +22 -0
- package/dist/WebhookTrigger.d.ts +129 -0
- package/dist/WebhookTrigger.js +355 -0
- package/dist/index.d.ts +70 -0
- package/dist/index.js +76 -0
- package/package.json +33 -0
- package/src/WebhookTrigger.test.ts +163 -0
- package/src/WebhookTrigger.ts +480 -0
- package/src/index.ts +80 -0
- package/tsconfig.json +32 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @blok/trigger-webhook
|
|
4
|
+
*
|
|
5
|
+
* Webhook trigger for Blok workflows.
|
|
6
|
+
* Handle webhook events from external services.
|
|
7
|
+
*
|
|
8
|
+
* Supported Services:
|
|
9
|
+
* - GitHub (push, pull_request, issues, releases, etc.)
|
|
10
|
+
* - Stripe (payment_intent, checkout.session, customer, etc.)
|
|
11
|
+
* - Shopify (orders, products, customers, etc.)
|
|
12
|
+
* - Custom webhooks (any service with signature verification)
|
|
13
|
+
*
|
|
14
|
+
* Features:
|
|
15
|
+
* - Signature verification (HMAC-SHA256)
|
|
16
|
+
* - Event type filtering
|
|
17
|
+
* - Source-specific handlers
|
|
18
|
+
* - Custom source registration
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* import { WebhookTrigger } from "@blok/trigger-webhook";
|
|
23
|
+
*
|
|
24
|
+
* class MyWebhookTrigger extends WebhookTrigger {
|
|
25
|
+
* protected nodes = myNodes;
|
|
26
|
+
* protected workflows = myWorkflows;
|
|
27
|
+
* }
|
|
28
|
+
*
|
|
29
|
+
* const trigger = new MyWebhookTrigger();
|
|
30
|
+
* await trigger.listen();
|
|
31
|
+
*
|
|
32
|
+
* // In your HTTP endpoint handler:
|
|
33
|
+
* app.post("/webhooks/:source", async (req, res) => {
|
|
34
|
+
* const rawBody = JSON.stringify(req.body);
|
|
35
|
+
* const result = await trigger.handleWebhook(
|
|
36
|
+
* req.params.source,
|
|
37
|
+
* rawBody,
|
|
38
|
+
* req.headers as Record<string, string>
|
|
39
|
+
* );
|
|
40
|
+
* res.status(200).json({ received: true });
|
|
41
|
+
* });
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* Workflow Definition:
|
|
45
|
+
* ```typescript
|
|
46
|
+
* Workflow({ name: "github-push", version: "1.0.0" })
|
|
47
|
+
* .addTrigger("webhook", {
|
|
48
|
+
* source: "github",
|
|
49
|
+
* events: ["push", "pull_request.*"],
|
|
50
|
+
* secret: process.env.GITHUB_WEBHOOK_SECRET,
|
|
51
|
+
* })
|
|
52
|
+
* .addStep({ ... });
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* Custom Source Handler:
|
|
56
|
+
* ```typescript
|
|
57
|
+
* import { WebhookTrigger } from "@blok/trigger-webhook";
|
|
58
|
+
*
|
|
59
|
+
* WebhookTrigger.registerSourceHandler("my-service", {
|
|
60
|
+
* getEventType: (headers, body) => body.event_type,
|
|
61
|
+
* getSignature: (headers) => headers["x-my-signature"],
|
|
62
|
+
* verifySignature: (rawBody, signature, secret) => {
|
|
63
|
+
* // Your verification logic
|
|
64
|
+
* return { valid: true };
|
|
65
|
+
* },
|
|
66
|
+
* getEventId: (headers, body) => body.id,
|
|
67
|
+
* });
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
71
|
+
exports.sourceHandlers = exports.WebhookTrigger = void 0;
|
|
72
|
+
// Core exports
|
|
73
|
+
var WebhookTrigger_1 = require("./WebhookTrigger");
|
|
74
|
+
Object.defineProperty(exports, "WebhookTrigger", { enumerable: true, get: function () { return WebhookTrigger_1.WebhookTrigger; } });
|
|
75
|
+
Object.defineProperty(exports, "sourceHandlers", { enumerable: true, get: function () { return WebhookTrigger_1.sourceHandlers; } });
|
|
76
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi9zcmMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtBQUFBOzs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0dBbUVHOzs7QUFFSCxlQUFlO0FBQ2YsbURBTTBCO0FBTHpCLGdIQUFBLGNBQWMsT0FBQTtBQUNkLGdIQUFBLGNBQWMsT0FBQSIsInNvdXJjZXNDb250ZW50IjpbIi8qKlxuICogQGJsb2svdHJpZ2dlci13ZWJob29rXG4gKlxuICogV2ViaG9vayB0cmlnZ2VyIGZvciBCbG9rIHdvcmtmbG93cy5cbiAqIEhhbmRsZSB3ZWJob29rIGV2ZW50cyBmcm9tIGV4dGVybmFsIHNlcnZpY2VzLlxuICpcbiAqIFN1cHBvcnRlZCBTZXJ2aWNlczpcbiAqIC0gR2l0SHViIChwdXNoLCBwdWxsX3JlcXVlc3QsIGlzc3VlcywgcmVsZWFzZXMsIGV0Yy4pXG4gKiAtIFN0cmlwZSAocGF5bWVudF9pbnRlbnQsIGNoZWNrb3V0LnNlc3Npb24sIGN1c3RvbWVyLCBldGMuKVxuICogLSBTaG9waWZ5IChvcmRlcnMsIHByb2R1Y3RzLCBjdXN0b21lcnMsIGV0Yy4pXG4gKiAtIEN1c3RvbSB3ZWJob29rcyAoYW55IHNlcnZpY2Ugd2l0aCBzaWduYXR1cmUgdmVyaWZpY2F0aW9uKVxuICpcbiAqIEZlYXR1cmVzOlxuICogLSBTaWduYXR1cmUgdmVyaWZpY2F0aW9uIChITUFDLVNIQTI1NilcbiAqIC0gRXZlbnQgdHlwZSBmaWx0ZXJpbmdcbiAqIC0gU291cmNlLXNwZWNpZmljIGhhbmRsZXJzXG4gKiAtIEN1c3RvbSBzb3VyY2UgcmVnaXN0cmF0aW9uXG4gKlxuICogQGV4YW1wbGVcbiAqIGBgYHR5cGVzY3JpcHRcbiAqIGltcG9ydCB7IFdlYmhvb2tUcmlnZ2VyIH0gZnJvbSBcIkBibG9rL3RyaWdnZXItd2ViaG9va1wiO1xuICpcbiAqIGNsYXNzIE15V2ViaG9va1RyaWdnZXIgZXh0ZW5kcyBXZWJob29rVHJpZ2dlciB7XG4gKiAgIHByb3RlY3RlZCBub2RlcyA9IG15Tm9kZXM7XG4gKiAgIHByb3RlY3RlZCB3b3JrZmxvd3MgPSBteVdvcmtmbG93cztcbiAqIH1cbiAqXG4gKiBjb25zdCB0cmlnZ2VyID0gbmV3IE15V2ViaG9va1RyaWdnZXIoKTtcbiAqIGF3YWl0IHRyaWdnZXIubGlzdGVuKCk7XG4gKlxuICogLy8gSW4geW91ciBIVFRQIGVuZHBvaW50IGhhbmRsZXI6XG4gKiBhcHAucG9zdChcIi93ZWJob29rcy86c291cmNlXCIsIGFzeW5jIChyZXEsIHJlcykgPT4ge1xuICogICBjb25zdCByYXdCb2R5ID0gSlNPTi5zdHJpbmdpZnkocmVxLmJvZHkpO1xuICogICBjb25zdCByZXN1bHQgPSBhd2FpdCB0cmlnZ2VyLmhhbmRsZVdlYmhvb2soXG4gKiAgICAgcmVxLnBhcmFtcy5zb3VyY2UsXG4gKiAgICAgcmF3Qm9keSxcbiAqICAgICByZXEuaGVhZGVycyBhcyBSZWNvcmQ8c3RyaW5nLCBzdHJpbmc+XG4gKiAgICk7XG4gKiAgIHJlcy5zdGF0dXMoMjAwKS5qc29uKHsgcmVjZWl2ZWQ6IHRydWUgfSk7XG4gKiB9KTtcbiAqIGBgYFxuICpcbiAqIFdvcmtmbG93IERlZmluaXRpb246XG4gKiBgYGB0eXBlc2NyaXB0XG4gKiBXb3JrZmxvdyh7IG5hbWU6IFwiZ2l0aHViLXB1c2hcIiwgdmVyc2lvbjogXCIxLjAuMFwiIH0pXG4gKiAgIC5hZGRUcmlnZ2VyKFwid2ViaG9va1wiLCB7XG4gKiAgICAgc291cmNlOiBcImdpdGh1YlwiLFxuICogICAgIGV2ZW50czogW1wicHVzaFwiLCBcInB1bGxfcmVxdWVzdC4qXCJdLFxuICogICAgIHNlY3JldDogcHJvY2Vzcy5lbnYuR0lUSFVCX1dFQkhPT0tfU0VDUkVULFxuICogICB9KVxuICogICAuYWRkU3RlcCh7IC4uLiB9KTtcbiAqIGBgYFxuICpcbiAqIEN1c3RvbSBTb3VyY2UgSGFuZGxlcjpcbiAqIGBgYHR5cGVzY3JpcHRcbiAqIGltcG9ydCB7IFdlYmhvb2tUcmlnZ2VyIH0gZnJvbSBcIkBibG9rL3RyaWdnZXItd2ViaG9va1wiO1xuICpcbiAqIFdlYmhvb2tUcmlnZ2VyLnJlZ2lzdGVyU291cmNlSGFuZGxlcihcIm15LXNlcnZpY2VcIiwge1xuICogICBnZXRFdmVudFR5cGU6IChoZWFkZXJzLCBib2R5KSA9PiBib2R5LmV2ZW50X3R5cGUsXG4gKiAgIGdldFNpZ25hdHVyZTogKGhlYWRlcnMpID0+IGhlYWRlcnNbXCJ4LW15LXNpZ25hdHVyZVwiXSxcbiAqICAgdmVyaWZ5U2lnbmF0dXJlOiAocmF3Qm9keSwgc2lnbmF0dXJlLCBzZWNyZXQpID0+IHtcbiAqICAgICAvLyBZb3VyIHZlcmlmaWNhdGlvbiBsb2dpY1xuICogICAgIHJldHVybiB7IHZhbGlkOiB0cnVlIH07XG4gKiAgIH0sXG4gKiAgIGdldEV2ZW50SWQ6IChoZWFkZXJzLCBib2R5KSA9PiBib2R5LmlkLFxuICogfSk7XG4gKiBgYGBcbiAqL1xuXG4vLyBDb3JlIGV4cG9ydHNcbmV4cG9ydCB7XG5cdFdlYmhvb2tUcmlnZ2VyLFxuXHRzb3VyY2VIYW5kbGVycyxcblx0dHlwZSBXZWJob29rRXZlbnQsXG5cdHR5cGUgVmVyaWZpY2F0aW9uUmVzdWx0LFxuXHR0eXBlIFdlYmhvb2tTb3VyY2VIYW5kbGVyLFxufSBmcm9tIFwiLi9XZWJob29rVHJpZ2dlclwiO1xuXG4vLyBSZS1leHBvcnQgdHlwZXMgZnJvbSBoZWxwZXIgZm9yIGNvbnZlbmllbmNlXG5leHBvcnQgdHlwZSB7IFdlYmhvb2tUcmlnZ2VyT3B0cyB9IGZyb20gXCJAYmxvay9oZWxwZXJcIjtcbiJdfQ==
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@blokjs/trigger-webhook",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Webhook trigger for Blok workflows - supports GitHub, Stripe, Shopify, and custom webhooks",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "rm -rf dist && bun run tsc",
|
|
10
|
+
"build:dev": "tsc --watch",
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"test:dev": "vitest"
|
|
13
|
+
},
|
|
14
|
+
"author": "Deskree Technologies Inc.",
|
|
15
|
+
"license": "Apache-2.0",
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@blokjs/helper": "workspace:*",
|
|
18
|
+
"@blokjs/runner": "workspace:*",
|
|
19
|
+
"@blokjs/shared": "workspace:*",
|
|
20
|
+
"@opentelemetry/api": "^1.9.0",
|
|
21
|
+
"uuid": "^11.1.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^22.15.21",
|
|
25
|
+
"@types/uuid": "^11.0.0",
|
|
26
|
+
"typescript": "^5.8.3",
|
|
27
|
+
"vitest": "^4.0.18"
|
|
28
|
+
},
|
|
29
|
+
"private": false,
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebhookTrigger Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import crypto from "crypto";
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
import { sourceHandlers } from "./WebhookTrigger";
|
|
8
|
+
|
|
9
|
+
describe("WebhookTrigger", () => {
|
|
10
|
+
describe("WebhookEvent Interface", () => {
|
|
11
|
+
it("should accept valid webhook event structure", () => {
|
|
12
|
+
const event = {
|
|
13
|
+
id: "event-123",
|
|
14
|
+
source: "github",
|
|
15
|
+
eventType: "push",
|
|
16
|
+
payload: { ref: "refs/heads/main" },
|
|
17
|
+
headers: { "x-github-event": "push" },
|
|
18
|
+
signature: "sha256=abc123",
|
|
19
|
+
timestamp: new Date(),
|
|
20
|
+
rawBody: '{"ref":"refs/heads/main"}',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
expect(event.id).toBe("event-123");
|
|
24
|
+
expect(event.source).toBe("github");
|
|
25
|
+
expect(event.eventType).toBe("push");
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("Source Handlers", () => {
|
|
31
|
+
describe("GitHub Handler", () => {
|
|
32
|
+
const handler = sourceHandlers.github;
|
|
33
|
+
|
|
34
|
+
it("should extract event type from headers", () => {
|
|
35
|
+
const headers = { "x-github-event": "push" };
|
|
36
|
+
expect(handler.getEventType(headers, {})).toBe("push");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("should extract signature from headers", () => {
|
|
40
|
+
const headers = { "x-hub-signature-256": "sha256=abc123" };
|
|
41
|
+
expect(handler.getSignature(headers)).toBe("sha256=abc123");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("should verify valid signature", () => {
|
|
45
|
+
const secret = "my-secret";
|
|
46
|
+
const rawBody = '{"action":"created"}';
|
|
47
|
+
const hmac = crypto.createHmac("sha256", secret);
|
|
48
|
+
const signature = "sha256=" + hmac.update(rawBody).digest("hex");
|
|
49
|
+
|
|
50
|
+
const result = handler.verifySignature(rawBody, signature, secret);
|
|
51
|
+
expect(result.valid).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should reject invalid signature", () => {
|
|
55
|
+
const result = handler.verifySignature('{"action":"created"}', "sha256=invalid", "my-secret");
|
|
56
|
+
expect(result.valid).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should extract event ID from headers", () => {
|
|
60
|
+
const headers = { "x-github-delivery": "delivery-123" };
|
|
61
|
+
expect(handler.getEventId(headers, {})).toBe("delivery-123");
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("Stripe Handler", () => {
|
|
66
|
+
const handler = sourceHandlers.stripe;
|
|
67
|
+
|
|
68
|
+
it("should extract event type from body", () => {
|
|
69
|
+
const body = { type: "payment_intent.succeeded" };
|
|
70
|
+
expect(handler.getEventType({}, body)).toBe("payment_intent.succeeded");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("should extract signature from headers", () => {
|
|
74
|
+
const headers = { "stripe-signature": "t=123,v1=abc" };
|
|
75
|
+
expect(handler.getSignature(headers)).toBe("t=123,v1=abc");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should verify valid Stripe signature", () => {
|
|
79
|
+
const secret = "whsec_test";
|
|
80
|
+
const rawBody = '{"type":"test"}';
|
|
81
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
82
|
+
const payload = `${timestamp}.${rawBody}`;
|
|
83
|
+
const hmac = crypto.createHmac("sha256", secret);
|
|
84
|
+
const sig = hmac.update(payload).digest("hex");
|
|
85
|
+
const signature = `t=${timestamp},v1=${sig}`;
|
|
86
|
+
|
|
87
|
+
const result = handler.verifySignature(rawBody, signature, secret);
|
|
88
|
+
expect(result.valid).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("should extract event ID from body", () => {
|
|
92
|
+
const body = { id: "evt_123" };
|
|
93
|
+
expect(handler.getEventId({}, body)).toBe("evt_123");
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("Shopify Handler", () => {
|
|
98
|
+
const handler = sourceHandlers.shopify;
|
|
99
|
+
|
|
100
|
+
it("should extract event type from headers", () => {
|
|
101
|
+
const headers = { "x-shopify-topic": "orders/create" };
|
|
102
|
+
expect(handler.getEventType(headers, {})).toBe("orders/create");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should extract signature from headers", () => {
|
|
106
|
+
const headers = { "x-shopify-hmac-sha256": "abc123base64==" };
|
|
107
|
+
expect(handler.getSignature(headers)).toBe("abc123base64==");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should extract event ID from headers", () => {
|
|
111
|
+
const headers = { "x-shopify-webhook-id": "webhook-123" };
|
|
112
|
+
expect(handler.getEventId(headers, {})).toBe("webhook-123");
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
describe("Custom Handler", () => {
|
|
117
|
+
const handler = sourceHandlers.custom;
|
|
118
|
+
|
|
119
|
+
it("should extract event type from headers or body", () => {
|
|
120
|
+
expect(handler.getEventType({ "x-event-type": "custom.event" }, {})).toBe("custom.event");
|
|
121
|
+
expect(handler.getEventType({}, { event: "body.event" })).toBe("body.event");
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("should extract signature from headers", () => {
|
|
125
|
+
const headers = { "x-signature": "sig123" };
|
|
126
|
+
expect(handler.getSignature(headers)).toBe("sig123");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("should verify valid custom signature", () => {
|
|
130
|
+
const secret = "custom-secret";
|
|
131
|
+
const rawBody = '{"data":"test"}';
|
|
132
|
+
const hmac = crypto.createHmac("sha256", secret);
|
|
133
|
+
const signature = hmac.update(rawBody).digest("hex");
|
|
134
|
+
|
|
135
|
+
const result = handler.verifySignature(rawBody, signature, secret);
|
|
136
|
+
expect(result.valid).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe("WebhookTriggerOpts Schema", () => {
|
|
142
|
+
it("should validate webhook trigger configuration", () => {
|
|
143
|
+
const validConfig = {
|
|
144
|
+
source: "github",
|
|
145
|
+
events: ["push", "pull_request.*"],
|
|
146
|
+
secret: "my-webhook-secret",
|
|
147
|
+
path: "/webhooks/github",
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
expect(validConfig.source).toBe("github");
|
|
151
|
+
expect(validConfig.events).toContain("push");
|
|
152
|
+
expect(validConfig.secret).toBeDefined();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("should support wildcard events", () => {
|
|
156
|
+
const config = {
|
|
157
|
+
source: "stripe",
|
|
158
|
+
events: ["payment_intent.*", "checkout.session.*"],
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
expect(config.events).toContain("payment_intent.*");
|
|
162
|
+
});
|
|
163
|
+
});
|