@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
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebhookTrigger - Handle webhook events from external services
|
|
3
|
+
*
|
|
4
|
+
* Extends TriggerBase to process webhook events from:
|
|
5
|
+
* - GitHub (push, pull_request, issues, etc.)
|
|
6
|
+
* - Stripe (payment_intent, checkout.session, etc.)
|
|
7
|
+
* - Shopify (orders, products, customers)
|
|
8
|
+
* - Custom webhooks
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - Signature verification for security
|
|
12
|
+
* - Event type filtering
|
|
13
|
+
* - Retry support
|
|
14
|
+
* - Dead letter handling
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import crypto from "node:crypto";
|
|
18
|
+
import type { HelperResponse, WebhookTriggerOpts } from "@blokjs/helper";
|
|
19
|
+
import {
|
|
20
|
+
DefaultLogger,
|
|
21
|
+
type GlobalOptions,
|
|
22
|
+
type BlokService,
|
|
23
|
+
NodeMap,
|
|
24
|
+
TriggerBase,
|
|
25
|
+
type TriggerResponse,
|
|
26
|
+
} from "@blokjs/runner";
|
|
27
|
+
import type { Context, RequestContext } from "@blokjs/shared";
|
|
28
|
+
import { type Span, SpanStatusCode, metrics, trace } from "@opentelemetry/api";
|
|
29
|
+
import { v4 as uuid } from "uuid";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Webhook event structure
|
|
33
|
+
*/
|
|
34
|
+
export interface WebhookEvent {
|
|
35
|
+
/** Unique event ID */
|
|
36
|
+
id: string;
|
|
37
|
+
/** Source service (github, stripe, shopify, custom) */
|
|
38
|
+
source: string;
|
|
39
|
+
/** Event type (e.g., push, payment_intent.succeeded) */
|
|
40
|
+
eventType: string;
|
|
41
|
+
/** Event payload */
|
|
42
|
+
payload: unknown;
|
|
43
|
+
/** Request headers */
|
|
44
|
+
headers: Record<string, string>;
|
|
45
|
+
/** Signature (if provided) */
|
|
46
|
+
signature?: string;
|
|
47
|
+
/** Timestamp */
|
|
48
|
+
timestamp: Date;
|
|
49
|
+
/** Raw request body */
|
|
50
|
+
rawBody: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Signature verification result
|
|
55
|
+
*/
|
|
56
|
+
export interface VerificationResult {
|
|
57
|
+
valid: boolean;
|
|
58
|
+
error?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Webhook source handlers
|
|
63
|
+
*/
|
|
64
|
+
export interface WebhookSourceHandler {
|
|
65
|
+
/** Extract event type from request */
|
|
66
|
+
getEventType(headers: Record<string, string>, body: unknown): string;
|
|
67
|
+
/** Get signature from request */
|
|
68
|
+
getSignature(headers: Record<string, string>): string | undefined;
|
|
69
|
+
/** Verify signature */
|
|
70
|
+
verifySignature(rawBody: string, signature: string, secret: string): VerificationResult;
|
|
71
|
+
/** Get event ID */
|
|
72
|
+
getEventId(headers: Record<string, string>, body: unknown): string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Workflow model with webhook trigger configuration
|
|
77
|
+
*/
|
|
78
|
+
interface WebhookWorkflowModel {
|
|
79
|
+
path: string;
|
|
80
|
+
config: {
|
|
81
|
+
name: string;
|
|
82
|
+
version: string;
|
|
83
|
+
trigger?: {
|
|
84
|
+
webhook?: WebhookTriggerOpts;
|
|
85
|
+
[key: string]: unknown;
|
|
86
|
+
};
|
|
87
|
+
[key: string]: unknown;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Built-in source handlers
|
|
93
|
+
*/
|
|
94
|
+
const sourceHandlers: Record<string, WebhookSourceHandler> = {
|
|
95
|
+
github: {
|
|
96
|
+
getEventType: (headers) => headers["x-github-event"] || "unknown",
|
|
97
|
+
getSignature: (headers) => headers["x-hub-signature-256"] || headers["x-hub-signature"],
|
|
98
|
+
verifySignature: (rawBody, signature, secret) => {
|
|
99
|
+
const hmac = crypto.createHmac("sha256", secret);
|
|
100
|
+
const digest = `sha256=${hmac.update(rawBody).digest("hex")}`;
|
|
101
|
+
const sigBuffer = Buffer.from(signature);
|
|
102
|
+
const digestBuffer = Buffer.from(digest);
|
|
103
|
+
// Length check first to avoid timing attack on length
|
|
104
|
+
if (sigBuffer.length !== digestBuffer.length) {
|
|
105
|
+
return { valid: false, error: "Invalid GitHub signature" };
|
|
106
|
+
}
|
|
107
|
+
const valid = crypto.timingSafeEqual(sigBuffer, digestBuffer);
|
|
108
|
+
return { valid, error: valid ? undefined : "Invalid GitHub signature" };
|
|
109
|
+
},
|
|
110
|
+
getEventId: (headers) => headers["x-github-delivery"] || uuid(),
|
|
111
|
+
},
|
|
112
|
+
|
|
113
|
+
stripe: {
|
|
114
|
+
getEventType: (_, body) => (body as { type?: string })?.type || "unknown",
|
|
115
|
+
getSignature: (headers) => headers["stripe-signature"],
|
|
116
|
+
verifySignature: (rawBody, signature, secret) => {
|
|
117
|
+
// Stripe signature format: t=timestamp,v1=signature
|
|
118
|
+
const parts = signature.split(",").reduce(
|
|
119
|
+
(acc, part) => {
|
|
120
|
+
const [key, value] = part.split("=");
|
|
121
|
+
acc[key] = value;
|
|
122
|
+
return acc;
|
|
123
|
+
},
|
|
124
|
+
{} as Record<string, string>,
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const timestamp = parts.t;
|
|
128
|
+
const expectedSig = parts.v1;
|
|
129
|
+
|
|
130
|
+
if (!timestamp || !expectedSig) {
|
|
131
|
+
return { valid: false, error: "Invalid Stripe signature format" };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const payload = `${timestamp}.${rawBody}`;
|
|
135
|
+
const hmac = crypto.createHmac("sha256", secret);
|
|
136
|
+
const computedSig = hmac.update(payload).digest("hex");
|
|
137
|
+
|
|
138
|
+
const sigBuffer = Buffer.from(expectedSig);
|
|
139
|
+
const computedBuffer = Buffer.from(computedSig);
|
|
140
|
+
if (sigBuffer.length !== computedBuffer.length) {
|
|
141
|
+
return { valid: false, error: "Invalid Stripe signature" };
|
|
142
|
+
}
|
|
143
|
+
const valid = crypto.timingSafeEqual(sigBuffer, computedBuffer);
|
|
144
|
+
return { valid, error: valid ? undefined : "Invalid Stripe signature" };
|
|
145
|
+
},
|
|
146
|
+
getEventId: (_, body) => (body as { id?: string })?.id || uuid(),
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
shopify: {
|
|
150
|
+
getEventType: (headers) => headers["x-shopify-topic"] || "unknown",
|
|
151
|
+
getSignature: (headers) => headers["x-shopify-hmac-sha256"],
|
|
152
|
+
verifySignature: (rawBody, signature, secret) => {
|
|
153
|
+
const hmac = crypto.createHmac("sha256", secret);
|
|
154
|
+
const digest = hmac.update(rawBody, "utf8").digest("base64");
|
|
155
|
+
const sigBuffer = Buffer.from(signature, "base64");
|
|
156
|
+
const digestBuffer = Buffer.from(digest, "base64");
|
|
157
|
+
if (sigBuffer.length !== digestBuffer.length) {
|
|
158
|
+
return { valid: false, error: "Invalid Shopify signature" };
|
|
159
|
+
}
|
|
160
|
+
const valid = crypto.timingSafeEqual(sigBuffer, digestBuffer);
|
|
161
|
+
return { valid, error: valid ? undefined : "Invalid Shopify signature" };
|
|
162
|
+
},
|
|
163
|
+
getEventId: (headers) => headers["x-shopify-webhook-id"] || uuid(),
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
custom: {
|
|
167
|
+
getEventType: (headers, body) => headers["x-event-type"] || (body as { event?: string })?.event || "custom",
|
|
168
|
+
getSignature: (headers) => headers["x-signature"] || headers["x-webhook-signature"],
|
|
169
|
+
verifySignature: (rawBody, signature, secret) => {
|
|
170
|
+
// Default: HMAC-SHA256
|
|
171
|
+
const hmac = crypto.createHmac("sha256", secret);
|
|
172
|
+
const digest = hmac.update(rawBody).digest("hex");
|
|
173
|
+
const valid = signature === digest || signature === `sha256=${digest}`;
|
|
174
|
+
return { valid, error: valid ? undefined : "Invalid signature" };
|
|
175
|
+
},
|
|
176
|
+
getEventId: (headers, body) => headers["x-event-id"] || (body as { id?: string })?.id || uuid(),
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* WebhookTrigger - Handle webhook events
|
|
182
|
+
*/
|
|
183
|
+
export abstract class WebhookTrigger extends TriggerBase {
|
|
184
|
+
protected nodeMap: GlobalOptions = {} as GlobalOptions;
|
|
185
|
+
protected readonly tracer = trace.getTracer(
|
|
186
|
+
process.env.PROJECT_NAME || "trigger-webhook-workflow",
|
|
187
|
+
process.env.PROJECT_VERSION || "0.0.1",
|
|
188
|
+
);
|
|
189
|
+
protected readonly logger = new DefaultLogger();
|
|
190
|
+
protected webhookWorkflows: WebhookWorkflowModel[] = [];
|
|
191
|
+
|
|
192
|
+
// Subclasses provide these
|
|
193
|
+
protected abstract nodes: Record<string, BlokService<unknown>>;
|
|
194
|
+
protected abstract workflows: Record<string, HelperResponse>;
|
|
195
|
+
|
|
196
|
+
constructor() {
|
|
197
|
+
super();
|
|
198
|
+
this.loadNodes();
|
|
199
|
+
this.loadWorkflows();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Load nodes into the node map
|
|
204
|
+
*/
|
|
205
|
+
loadNodes(): void {
|
|
206
|
+
this.nodeMap.nodes = new NodeMap();
|
|
207
|
+
const nodeKeys = Object.keys(this.nodes);
|
|
208
|
+
for (const key of nodeKeys) {
|
|
209
|
+
this.nodeMap.nodes.addNode(key, this.nodes[key]);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Load workflows into the workflow map
|
|
215
|
+
*/
|
|
216
|
+
loadWorkflows(): void {
|
|
217
|
+
this.nodeMap.workflows = this.workflows;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Initialize webhook trigger (call after loading workflows)
|
|
222
|
+
*/
|
|
223
|
+
async listen(): Promise<number> {
|
|
224
|
+
const startTime = this.startCounter();
|
|
225
|
+
|
|
226
|
+
// Find all workflows with webhook triggers
|
|
227
|
+
this.webhookWorkflows = this.getWebhookWorkflows();
|
|
228
|
+
|
|
229
|
+
if (this.webhookWorkflows.length === 0) {
|
|
230
|
+
this.logger.log("No workflows with webhook triggers found");
|
|
231
|
+
} else {
|
|
232
|
+
this.logger.log(`Webhook trigger initialized. ${this.webhookWorkflows.length} workflow(s) registered`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Enable HMR in development mode
|
|
236
|
+
if (process.env.BLOK_HMR === "true" || process.env.NODE_ENV === "development") {
|
|
237
|
+
await this.enableHotReload();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return this.endCounter(startTime);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Stop the webhook trigger
|
|
245
|
+
*/
|
|
246
|
+
async stop(): Promise<void> {
|
|
247
|
+
this.webhookWorkflows = [];
|
|
248
|
+
this.logger.log("Webhook trigger stopped");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
protected override async onHmrWorkflowChange(): Promise<void> {
|
|
252
|
+
this.loadWorkflows();
|
|
253
|
+
this.webhookWorkflows = this.getWebhookWorkflows();
|
|
254
|
+
this.logger.log(`[HMR] Webhook workflows reloaded. ${this.webhookWorkflows.length} workflow(s) registered`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Process an incoming webhook request
|
|
259
|
+
* Call this from your HTTP endpoint handler
|
|
260
|
+
*/
|
|
261
|
+
async handleWebhook(
|
|
262
|
+
source: string,
|
|
263
|
+
rawBody: string,
|
|
264
|
+
headers: Record<string, string>,
|
|
265
|
+
): Promise<TriggerResponse | null> {
|
|
266
|
+
const handler = sourceHandlers[source] || sourceHandlers.custom;
|
|
267
|
+
|
|
268
|
+
// Parse body
|
|
269
|
+
let body: unknown;
|
|
270
|
+
try {
|
|
271
|
+
body = JSON.parse(rawBody);
|
|
272
|
+
} catch {
|
|
273
|
+
body = rawBody;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Create webhook event
|
|
277
|
+
const event: WebhookEvent = {
|
|
278
|
+
id: handler.getEventId(headers, body),
|
|
279
|
+
source,
|
|
280
|
+
eventType: handler.getEventType(headers, body),
|
|
281
|
+
payload: body,
|
|
282
|
+
headers,
|
|
283
|
+
signature: handler.getSignature(headers),
|
|
284
|
+
timestamp: new Date(),
|
|
285
|
+
rawBody,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// Find matching workflow
|
|
289
|
+
const workflow = this.findMatchingWorkflow(event);
|
|
290
|
+
if (!workflow) {
|
|
291
|
+
this.logger.log(`No matching workflow for webhook: ${source}/${event.eventType}`);
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const config = workflow.config.trigger?.webhook as WebhookTriggerOpts;
|
|
296
|
+
|
|
297
|
+
// Verify signature if secret is configured
|
|
298
|
+
if (config.secret && event.signature) {
|
|
299
|
+
const verification = handler.verifySignature(rawBody, event.signature, config.secret);
|
|
300
|
+
if (!verification.valid) {
|
|
301
|
+
this.logger.error(`Webhook signature verification failed: ${verification.error}`);
|
|
302
|
+
throw new Error(`Signature verification failed: ${verification.error}`);
|
|
303
|
+
}
|
|
304
|
+
} else if (config.secret && !event.signature) {
|
|
305
|
+
this.logger.error("Webhook signature missing but secret is configured");
|
|
306
|
+
throw new Error("Signature missing");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return this.executeWorkflow(event, workflow, config);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Get all workflows that have webhook triggers
|
|
314
|
+
*/
|
|
315
|
+
protected getWebhookWorkflows(): WebhookWorkflowModel[] {
|
|
316
|
+
const workflows: WebhookWorkflowModel[] = [];
|
|
317
|
+
|
|
318
|
+
for (const [path, workflow] of Object.entries(this.nodeMap.workflows || {})) {
|
|
319
|
+
const workflowConfig = (workflow as unknown as { _config: WebhookWorkflowModel["config"] })._config;
|
|
320
|
+
|
|
321
|
+
if (workflowConfig?.trigger) {
|
|
322
|
+
const triggerType = Object.keys(workflowConfig.trigger)[0];
|
|
323
|
+
|
|
324
|
+
if (triggerType === "webhook" && workflowConfig.trigger.webhook) {
|
|
325
|
+
workflows.push({
|
|
326
|
+
path,
|
|
327
|
+
config: workflowConfig,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return workflows;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Find workflow matching the webhook event
|
|
338
|
+
*/
|
|
339
|
+
protected findMatchingWorkflow(event: WebhookEvent): WebhookWorkflowModel | null {
|
|
340
|
+
for (const workflow of this.webhookWorkflows) {
|
|
341
|
+
const config = workflow.config.trigger?.webhook;
|
|
342
|
+
if (!config) continue;
|
|
343
|
+
|
|
344
|
+
// Check source match
|
|
345
|
+
if (config.source !== event.source) continue;
|
|
346
|
+
|
|
347
|
+
// Check event type match
|
|
348
|
+
if (config.events && config.events.length > 0) {
|
|
349
|
+
const matches = config.events.some((pattern) => {
|
|
350
|
+
// Support wildcards (e.g., "push", "pull_request.*")
|
|
351
|
+
if (pattern === "*") return true;
|
|
352
|
+
if (pattern.endsWith(".*")) {
|
|
353
|
+
const prefix = pattern.slice(0, -2);
|
|
354
|
+
return event.eventType.startsWith(prefix);
|
|
355
|
+
}
|
|
356
|
+
return pattern === event.eventType;
|
|
357
|
+
});
|
|
358
|
+
if (!matches) continue;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return workflow;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Execute a workflow for a webhook event
|
|
369
|
+
*/
|
|
370
|
+
protected async executeWorkflow(
|
|
371
|
+
event: WebhookEvent,
|
|
372
|
+
workflow: WebhookWorkflowModel,
|
|
373
|
+
_config: WebhookTriggerOpts,
|
|
374
|
+
): Promise<TriggerResponse> {
|
|
375
|
+
const executionId = uuid();
|
|
376
|
+
|
|
377
|
+
const defaultMeter = metrics.getMeter("default");
|
|
378
|
+
const webhookExecutions = defaultMeter.createCounter("webhook_executions", {
|
|
379
|
+
description: "Webhook executions",
|
|
380
|
+
});
|
|
381
|
+
const webhookErrors = defaultMeter.createCounter("webhook_errors", {
|
|
382
|
+
description: "Webhook execution errors",
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
return new Promise((resolve) => {
|
|
386
|
+
this.tracer.startActiveSpan(`webhook:${event.source}/${event.eventType}`, async (span: Span) => {
|
|
387
|
+
try {
|
|
388
|
+
const start = performance.now();
|
|
389
|
+
|
|
390
|
+
// Initialize configuration for this workflow
|
|
391
|
+
await this.configuration.init(workflow.path, this.nodeMap);
|
|
392
|
+
|
|
393
|
+
// Create context
|
|
394
|
+
const ctx: Context = this.createContext(undefined, workflow.path, executionId);
|
|
395
|
+
|
|
396
|
+
// Populate request with webhook event
|
|
397
|
+
ctx.request = {
|
|
398
|
+
body: event.payload,
|
|
399
|
+
headers: event.headers,
|
|
400
|
+
query: {},
|
|
401
|
+
params: {
|
|
402
|
+
source: event.source,
|
|
403
|
+
eventType: event.eventType,
|
|
404
|
+
eventId: event.id,
|
|
405
|
+
},
|
|
406
|
+
} as unknown as RequestContext;
|
|
407
|
+
|
|
408
|
+
// Store webhook context in vars
|
|
409
|
+
if (!ctx.vars) ctx.vars = {};
|
|
410
|
+
ctx.vars._webhook_event = {
|
|
411
|
+
id: event.id,
|
|
412
|
+
source: event.source,
|
|
413
|
+
eventType: event.eventType,
|
|
414
|
+
timestamp: event.timestamp.toISOString(),
|
|
415
|
+
hasSignature: String(!!event.signature),
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
ctx.logger.log(`Processing webhook: ${event.source}/${event.eventType} (${event.id})`);
|
|
419
|
+
|
|
420
|
+
// Execute workflow
|
|
421
|
+
const response: TriggerResponse = await this.run(ctx);
|
|
422
|
+
const end = performance.now();
|
|
423
|
+
|
|
424
|
+
// Set span attributes
|
|
425
|
+
span.setAttribute("success", true);
|
|
426
|
+
span.setAttribute("event_id", event.id);
|
|
427
|
+
span.setAttribute("source", event.source);
|
|
428
|
+
span.setAttribute("event_type", event.eventType);
|
|
429
|
+
span.setAttribute("workflow_path", workflow.path);
|
|
430
|
+
span.setAttribute("elapsed_ms", end - start);
|
|
431
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
432
|
+
|
|
433
|
+
// Record metrics
|
|
434
|
+
webhookExecutions.add(1, {
|
|
435
|
+
env: process.env.NODE_ENV,
|
|
436
|
+
source: event.source,
|
|
437
|
+
event_type: event.eventType,
|
|
438
|
+
workflow_name: this.configuration.name,
|
|
439
|
+
success: "true",
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
ctx.logger.log(`Webhook processed in ${(end - start).toFixed(2)}ms: ${event.id}`);
|
|
443
|
+
|
|
444
|
+
resolve(response);
|
|
445
|
+
} catch (error) {
|
|
446
|
+
const errorMessage = (error as Error).message;
|
|
447
|
+
|
|
448
|
+
// Set span error
|
|
449
|
+
span.setAttribute("success", false);
|
|
450
|
+
span.recordException(error as Error);
|
|
451
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: errorMessage });
|
|
452
|
+
|
|
453
|
+
// Record error metrics
|
|
454
|
+
webhookErrors.add(1, {
|
|
455
|
+
env: process.env.NODE_ENV,
|
|
456
|
+
source: event.source,
|
|
457
|
+
event_type: event.eventType,
|
|
458
|
+
workflow_name: this.configuration?.name || "unknown",
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
this.logger.error(`Webhook failed ${event.id}: ${errorMessage}`, (error as Error).stack);
|
|
462
|
+
|
|
463
|
+
throw error;
|
|
464
|
+
} finally {
|
|
465
|
+
span.end();
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Register a custom source handler
|
|
473
|
+
*/
|
|
474
|
+
static registerSourceHandler(source: string, handler: WebhookSourceHandler): void {
|
|
475
|
+
sourceHandlers[source] = handler;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export default WebhookTrigger;
|
|
480
|
+
export { sourceHandlers };
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @blokjs/trigger-webhook
|
|
3
|
+
*
|
|
4
|
+
* Webhook trigger for Blok workflows.
|
|
5
|
+
* Handle webhook events from external services.
|
|
6
|
+
*
|
|
7
|
+
* Supported Services:
|
|
8
|
+
* - GitHub (push, pull_request, issues, releases, etc.)
|
|
9
|
+
* - Stripe (payment_intent, checkout.session, customer, etc.)
|
|
10
|
+
* - Shopify (orders, products, customers, etc.)
|
|
11
|
+
* - Custom webhooks (any service with signature verification)
|
|
12
|
+
*
|
|
13
|
+
* Features:
|
|
14
|
+
* - Signature verification (HMAC-SHA256)
|
|
15
|
+
* - Event type filtering
|
|
16
|
+
* - Source-specific handlers
|
|
17
|
+
* - Custom source registration
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* import { WebhookTrigger } from "@blokjs/trigger-webhook";
|
|
22
|
+
*
|
|
23
|
+
* class MyWebhookTrigger extends WebhookTrigger {
|
|
24
|
+
* protected nodes = myNodes;
|
|
25
|
+
* protected workflows = myWorkflows;
|
|
26
|
+
* }
|
|
27
|
+
*
|
|
28
|
+
* const trigger = new MyWebhookTrigger();
|
|
29
|
+
* await trigger.listen();
|
|
30
|
+
*
|
|
31
|
+
* // In your HTTP endpoint handler:
|
|
32
|
+
* app.post("/webhooks/:source", async (req, res) => {
|
|
33
|
+
* const rawBody = JSON.stringify(req.body);
|
|
34
|
+
* const result = await trigger.handleWebhook(
|
|
35
|
+
* req.params.source,
|
|
36
|
+
* rawBody,
|
|
37
|
+
* req.headers as Record<string, string>
|
|
38
|
+
* );
|
|
39
|
+
* res.status(200).json({ received: true });
|
|
40
|
+
* });
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* Workflow Definition:
|
|
44
|
+
* ```typescript
|
|
45
|
+
* Workflow({ name: "github-push", version: "1.0.0" })
|
|
46
|
+
* .addTrigger("webhook", {
|
|
47
|
+
* source: "github",
|
|
48
|
+
* events: ["push", "pull_request.*"],
|
|
49
|
+
* secret: process.env.GITHUB_WEBHOOK_SECRET,
|
|
50
|
+
* })
|
|
51
|
+
* .addStep({ ... });
|
|
52
|
+
* ```
|
|
53
|
+
*
|
|
54
|
+
* Custom Source Handler:
|
|
55
|
+
* ```typescript
|
|
56
|
+
* import { WebhookTrigger } from "@blokjs/trigger-webhook";
|
|
57
|
+
*
|
|
58
|
+
* WebhookTrigger.registerSourceHandler("my-service", {
|
|
59
|
+
* getEventType: (headers, body) => body.event_type,
|
|
60
|
+
* getSignature: (headers) => headers["x-my-signature"],
|
|
61
|
+
* verifySignature: (rawBody, signature, secret) => {
|
|
62
|
+
* // Your verification logic
|
|
63
|
+
* return { valid: true };
|
|
64
|
+
* },
|
|
65
|
+
* getEventId: (headers, body) => body.id,
|
|
66
|
+
* });
|
|
67
|
+
* ```
|
|
68
|
+
*/
|
|
69
|
+
|
|
70
|
+
// Core exports
|
|
71
|
+
export {
|
|
72
|
+
WebhookTrigger,
|
|
73
|
+
sourceHandlers,
|
|
74
|
+
type WebhookEvent,
|
|
75
|
+
type VerificationResult,
|
|
76
|
+
type WebhookSourceHandler,
|
|
77
|
+
} from "./WebhookTrigger";
|
|
78
|
+
|
|
79
|
+
// Re-export types from helper for convenience
|
|
80
|
+
export type { WebhookTriggerOpts } from "@blokjs/helper";
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ts-node": {
|
|
3
|
+
"transpileOnly": true
|
|
4
|
+
},
|
|
5
|
+
"compilerOptions": {
|
|
6
|
+
"target": "ES2022",
|
|
7
|
+
"module": "es2022",
|
|
8
|
+
"lib": ["ES2022"],
|
|
9
|
+
"declaration": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"noImplicitAny": true,
|
|
12
|
+
"strictNullChecks": true,
|
|
13
|
+
"noImplicitThis": true,
|
|
14
|
+
"alwaysStrict": true,
|
|
15
|
+
"noUnusedLocals": false,
|
|
16
|
+
"noUnusedParameters": false,
|
|
17
|
+
"noImplicitReturns": true,
|
|
18
|
+
"noFallthroughCasesInSwitch": false,
|
|
19
|
+
"inlineSourceMap": true,
|
|
20
|
+
"inlineSources": true,
|
|
21
|
+
"experimentalDecorators": true,
|
|
22
|
+
"emitDecoratorMetadata": true,
|
|
23
|
+
"skipLibCheck": true,
|
|
24
|
+
"esModuleInterop": true,
|
|
25
|
+
"resolveJsonModule": true,
|
|
26
|
+
"outDir": "./dist",
|
|
27
|
+
"rootDir": "./src",
|
|
28
|
+
"moduleResolution": "bundler"
|
|
29
|
+
},
|
|
30
|
+
"include": ["src/**/*"],
|
|
31
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
32
|
+
}
|