跳转到内容

Webhook

createWebhookManager 提供了 Webhook 的注册、签名验证和自动重试功能。

import { createWebhookManager } from "@ventostack/events";
const webhooks = createWebhookManager({
secret: process.env.WEBHOOK_SECRET!,
retries: 3, // 失败后重试次数
retryDelay: 1000, // 重试间隔(ms)
timeout: 10_000, // 请求超时(ms)
});
const WebhookModel = defineModel("webhooks", {
id: column.bigint({ primary: true, autoIncrement: true }),
userId: column.bigint(),
url: column.varchar({ length: 2048 }),
events: column.json<string[]>(),
secret: column.varchar({ length: 255 }),
});
// 用户通过 API 注册 Webhook
router.post("/webhooks", authMiddleware, async (ctx) => {
const { url, events } = await ctx.body<{ url: string; events: string[] }>();
const userId = ctx.state.user.sub;
await db.query(WebhookModel).insert({
userId,
url,
events,
secret: generateWebhookSecret(),
});
return ctx.json({ registered: true }, 201);
});
// 当有事件发生时触发所有订阅了该事件的 Webhook
async function triggerWebhooks(eventType: string, payload: unknown) {
const subscribers = await db.query(WebhookModel)
.whereRaw("events::jsonb @> $1", [JSON.stringify([eventType])])
.list();
await Promise.allSettled(
subscribers.map(sub =>
webhooks.send(sub.url, {
event: eventType,
payload,
timestamp: new Date().toISOString(),
}, sub.secret)
)
);
}
// 订单创建后触发
router.post("/orders", async (ctx) => {
const order = await createOrder(await ctx.body());
await triggerWebhooks("order.created", order);
return ctx.json(order, 201);
});

验证来自外部系统的 Webhook 签名:

router.post("/webhooks/github", async (ctx) => {
const signature = ctx.headers.get("x-hub-signature-256")!;
const body = await ctx.request.text();
const valid = webhooks.verify(body, signature, process.env.GITHUB_WEBHOOK_SECRET!);
if (!valid) {
return ctx.json({ error: "Invalid signature" }, 401);
}
const event = JSON.parse(body);
// 处理 GitHub 事件...
return ctx.json({ received: true });
});
interface WebhookPayload {
event: string;
payload: unknown;
timestamp: string;
}
interface WebhookManager {
send(url: string, payload: WebhookPayload, secret?: string): Promise<void>;
verify(body: string, signature: string, secret: string): boolean;
sign(body: string, secret: string): string;
}