andrewpopov/webhook-kit

GitHub: andrewpopov/webhook-kit

一个零依赖的 Node.js 出站 webhook 投递库,提供强制 HMAC-SHA256 签名、SSRF 防护、重放验证及有界并发投递等安全核心能力。

Stars: 0 | Forks: 0

# @andrewpopov/webhook-kit 适用于 Node 服务的、与框架无关的**出站 webhook 投递**。它接管了各个仓库一直在重复实现且容易产生偏差的安全敏感核心: - 强制的 HMAC-SHA256 签名,基于 `` `${timestamp}.${deliveryId}.${body}` ``,并附带 `X-Webhook-Signature: sha256=`、`X-Webhook-Timestamp` 以及唯一的 `X-Webhook-Delivery-Id` header。 - 强制的**触发时 SSRF 重新检查** hook —— 应用注入自己的 URL guard,并在每次尝试时运行。该 guard 必须使用固定的传输协议,以实现全面的 DNS 重绑定保护。 - 每次尝试的独立超时和 `redirect: 'manual'`(3xx 响应无法绕过 guard)。 - 有界的扇出并发和错误隔离:投递失败会在结果中返回。 - 配套的**接收方验证器**,以便订阅者(以及你的测试)可以验证此库签名的内容,并设有有效时间窗口。 零运行时依赖 —— 仅使用 Node `crypto` 和全局 `fetch`(Node ≥ 20)。 ## 安装 ``` npm install github:andrewpopov/webhook-kit#v1.0.1 ``` ## 发送 ``` import { deliverWebhooks, matchesEvent } from '@andrewpopov/webhook-kit'; async function fire(event: string, payload: Record) { const targets = (await listActiveWebhooks()) .filter((w) => matchesEvent(JSON.parse(w.events), event)) .map((w) => ({ url: w.url, secret: w.secret, id: w.id })); // Each consumer keeps its own body shape — the library signs whatever you pass. const body = JSON.stringify({ event, timestamp: new Date().toISOString(), ...payload }); const results = await deliverWebhooks(targets, body, { assertSafeUrl: (url) => assertPublicHttpUrl(url), // your SSRF guard; throw to skip timeoutMs: 10_000, concurrency: 8, }); for (const r of results) { if (r.skipped) log.warn('webhook skipped (unsafe url)', { id: r.id }); else if (!r.ok) log.warn('webhook delivery failed', { id: r.id, status: r.status, err: r.error?.message }); } } ``` body 结构和 SSRF guard 均由外部注入,从而保留了使用方确切的传输约定和日志记录 —— 仅共享签名/传输核心。 ## API | 导出 | 用途 | |---|---| | `deliverWebhook(target, body, opts)` | 投递一个经过签名并在触发时受保护的 webhook;如果缺少任一控制项,则永远不会发送。 | | `deliverWebhooks(targets, body, opts)` | 以有界并发投递多个 webhook;每个目标返回一个 `DeliveryResult`。 | | `deliverWebhookUnsafe(target, body, opts?)` | 仅供迁移使用的显式逃生舱,用于未签名或不受保护的投递。 | | `buildSignedHeaders(secret, body, opts)` | 包含签名、时间戳和唯一投递 ID 的 headers。 | | `signWebhookDelivery(secret, timestamp, deliveryId, body)` | 当前的 `sha256=` 签名字符串。 | | `verifyWebhookDelivery(params)` | 推荐的接收方验证:签名、新鲜度以及原子重放声明。 | | `verifyWebhookSignature(params)` | 仅用于传统的签名/新鲜度验证;无重放声明。 | | `matchesEvent(subscribed, event)` | 支持 `*` 通配符的事件匹配。 | | `generateWebhookSecret()` | 256 位十六进制签名密钥。 | | `resolveSecretRotation(input)` | 始终进行签名的密钥更新(清除 → 轮换,绝不移除)。 | `DeliverOptions`:必填项为 `assertSafeUrl`、`timeoutMs`(默认为 10000)、`fetchImpl`、`now`、`contentType` 以及 `concurrency`(默认为 8)。若要进行安全投递,必须提供 `WebhookTarget.secret`;如果缺少密钥,将返回跳过结果且不会发送。 ## 签名方案 ``` X-Webhook-Timestamp: X-Webhook-Delivery-Id: X-Webhook-Signature: sha256=HMAC_SHA256(secret, `${X-Webhook-Timestamp}.${X-Webhook-Delivery-Id}.${rawBody}`) ``` 接收方会重构这三部分输入,以恒定时间方式对比 HMAC,并拒绝超出其有效时间窗口的投递。使用带有重放存储的 `verifyWebhookDelivery`,其 `claim(deliveryId, expiresAt)` 操作必须是原子的:当 ID 已被声明时,它必须返回 `false`,并将声明保留至 `expiresAt`。本 package 提供协议和失败即关闭(fail-closed)的约定;存储选择仍由应用自行决定。 `signWebhookBody` 和 `verifyWebhookSignature` 保留用于传统的传输兼容性。它们仅检查签名和新鲜度,因此不提供重放预防。 ## 本地验证 ``` npm ci npm run verify npm audit --omit=dev --audit-level=high ``` ## 标准 请参阅 [`STANDARDS.md`](./STANDARDS.md)(从 `agent_brain/knowledge/shared-package-standards.md` 同步)。 ## 项目政策
标签:MITM代理, 自动化攻击