@effected/webhooks

中文文档
0.6.0发布于 2026/11/10MITTypeScriptES Module构建来源已验证近 30 天下载 1980

对 Effect v4 的输入 Webhook 签名验证:标准 Webhooks, GitHub, Frede, Sanity, HMAC 头和共享密钥预设.

中文使用文档
文档目录17 节
  1. 为什么
  2. 安装
  3. 提供什么
  4. 构建一个 WebhookRequest
  5. 验证交付
  6. GitHub 图像
  7. 条纹
  8. 标准 Webhooks (Svix, 办事员, 重新发送)
  9. 卫生
  10. 线性
  11. 自定义 HMAC 标题
  12. 一个共同的秘密
  13. 存档并重放一个共享的密件
  14. 错误
  15. 测试
  16. 它不会做的
  17. 许可证

@ effect/webhooks

npm License: MIT Node.js %3E%3D24.11.0

核实寄件人是否签了入境的网路快递,声称是寄件人寄来的,因为Effect v4. 预设覆盖了标准Webhooks(Svix,以及因此Clerk and Resend),GitHub,Frede和Sanity. 一个通用的HMAC头覆盖了Linear和其他赤色HMAC发送者,一个共享机密头覆盖了Cloudflare. 每一个验证器都是在纯数据之上的函数(标题查找、生体、接收时间和秘密),没有HTTP框架,没有 node: 导入,所以它运行在一个节点服务器和一个 Cloudflare Worker 类似.

预1.0.0。 。 。 . 此软件包是 @effected/* 工具箱,在 稳定效果 v4并仍在 0.x 发展。 为了保持你的 effect 财务报告和审定财务报表 @effect/* 该工具包在线上构建和测试、安装 @effected/pnpm-plugin-effected。 。 。 .

稳定:不稳定. 此软件包的 API 表面尚未考虑 完成并可能改变 0.x 释放 准确的版本。 满 政策: 释放策略。 。 。 .

为什么

网络呼克签名是HMAC,核心 effect/crypto是因为 Mac 计算一个。 难点在于每个供应商的阴谋

  • 签名的字节 . 条纹和圣洁标志 ${t}. 然后是身体 和标准的Webhooks符号 ${id}.${timestamp}. 然后是遗体。 每一个必须是生体字节,从不重复序列化的剖面.
  • 那个秘密. 标准 Webhooks 基础64 解码其 whsec_ 密入密钥 条纹使用 whsec_ 密钥为字串。 将两个和每个签名悄悄地交换出不匹配.
  • 头语法. 几个 v1 在秘密轮换期间可以有签名, v0 和 SHA-1 标题必须被忽略,最后一个 t 赢了.
  • 令发. 在时间戳之前检查签名,所以一个没有签名的探测器不会得知容忍窗口.
  • 钟. Staleness是根据你收到送货的时间而不是墙上钟来判断的,所以从排队中重新播放的送货,就像它到达时那样.

如果你只从发送者那里得到HMAC 没有时间戳的身体, HmacHeader 这里有几行 Hmac.verify 从 @effected/crypto 是同一个检查一个级别。 使用核心 Mac 只有在你自在地写解码 和经常的比较的时候.

安装

npm install @effected/webhooks effect
pnpm add @effected/webhooks effect

在节点上,它需要Node.js QQ24.11.0;它还运行在任何其他WebCrypto运行时间上,Cloudflare Workers也包括在内. effect v4是唯一的对等者,和 @effected/crypto 带着它 全体 @effected/* 软件包仅限无害环境管理.

提供什么

运行要求数
StandardWebhooks.verify, (中文(简体) ), GitHub.verify, (中文(简体) ), Stripe.verify, (中文(简体) ), Sanity.verify, (中文(简体) ), HmacHeader.verify, (中文(简体) ), SharedSecret.verify, (中文(简体) ), SharedSecret.attest, (中文(简体) ), SharedSecret.verifyAttestedMac.Mac
Webhooks.checkFreshness无

无需核查 Digest 或者说 SecureRandom 本身; SecureRandom 是网络晶体 Mac 层被构建.

服务任何 WebCrypto 运行时间( 包括工人)节点
Mac.MacMac.layerWebCrypto.pipe(Layer.provide(SecureRandom.layerWebCrypto)) 从 effect/cryptoNodeCrypto.layerMac 从 @effect/platform-node
import { Layer } from "effect";
import { Mac, SecureRandom } from "effect/crypto";

const MacLive = Mac.layerWebCrypto.pipe(Layer.provide(SecureRandom.layerWebCrypto));

构建一个 WebhookRequest

每个核查员都得 WebhookRequest(单位:千美元) header 搜索预设用小写名调用, 原始 body 字节 receivedAt 作为时间 DateTime.Utc,终点 secret 作为 Redacted 字符串,以销售商显示的形式显示。 修改取来的一个 Request,作为工人或任何以获取为基础的服务器接收:

import type { WebhookRequest } from "@effected/webhooks";
import { DateTime, Effect, Option } from "effect";
import type { Redacted } from "effect";

const fromRequest = (request: Request, secret: Redacted.Redacted<string>) =>
  Effect.gen(function* () {
    const body = new Uint8Array(yield* Effect.promise(() => request.arrayBuffer()));
    const receivedAt = yield* DateTime.now;
    const delivery: WebhookRequest = {
      header: (name) => Option.fromNullishOr(request.headers.get(name)),
      body,
      receivedAt,
      secret,
    };
    return delivery;
  });
// Effect<WebhookRequest>

适配器不需要修剪或过滤. 预设将空值视为不存在,签名则预设了修剪值,并将空白值视为缺失值。 在重新处理存储的快递时,通过原始到达时间为 receivedAt。 。 。 .

验证交付

GitHub 图像

import type { WebhookRequest } from "@effected/webhooks";
import { GitHub } from "@effected/webhooks";

const verifyGitHub = (delivery: WebhookRequest) => GitHub.verify(delivery);
// Effect<void, WebhookVerificationError, Mac>

检查 x-hub-signature-256数字 : sha256= 和小写Hex HMAC-SHA256 被秘密的UTF-8字节所覆盖的身体. SHA-1号 x-hub-signature 被忽略。 GitHub 表示没有时间戳, 所以重播看起来完全像重递; 复制上 x-github-delivery。 。 。 .

条纹

import type { WebhookRequest } from "@effected/webhooks";
import { Stripe } from "@effected/webhooks";

const verifyStripe = (delivery: WebhookRequest) => Stripe.verify(delivery, { tolerance: "10 minutes" });
// Effect<StripeVerified, WebhookVerificationError, Mac>

检查 stripe-signature,列表 key=value 双人 最后一个有效 t (Unix秒)胜出,任意 v1 可能匹配,它覆盖一个秘密卷 当花纹标志 与两个秘密 v0 被忽略。 关键是 whsec_ 秘密字符串本身,未解码. StripeVerified 携带签名 timestamp。 。 。 . tolerance 是任意 Duration.Input 默认为 Webhooks.DEFAULT_TOLERANCE五分钟左右.

标准 Webhooks (Svix, 办事员, 重新发送)

import type { WebhookRequest } from "@effected/webhooks";
import { StandardWebhooks } from "@effected/webhooks";

const verifySvix = (delivery: WebhookRequest) => StandardWebhooks.verify(delivery);
// Effect<StandardWebhooksVerified, WebhookVerificationError, Mac>

读取 svix-id, (中文(简体) ), svix-timestamp 财务报告和审定财务报表 svix-signature,每个掉回 其无品牌 webhook- 名称。 签名头是一份空间分隔列表,其中包含 v1,<base64>,其中任何可能匹配的。 秘诀是 whsec_ (前缀是可选的)后有严格的,相加的基数64,其解码字节为键. StandardWebhooksVerified 携带信件 id发送者在每次重试时都会重复, 重复一遍,签名的 timestamp。 。 。 .

卫生

import type { WebhookRequest } from "@effected/webhooks";
import { Sanity } from "@effected/webhooks";

const verifySanity = (delivery: WebhookRequest) => Sanity.verify(delivery);
// Effect<SanityVerified, WebhookVerificationError, Mac>

检查 sanity-webhook-signature, (中文(简体) ), t=<unix milliseconds>,v1=<signature> 和一个 v1: 未添加基数64url HMAC-SHA256, 含 ${t}. 和尸体, 在秘密的UTF-8字节下。 请注意: t 以毫秒计,与斯德雷克不同. SanityVerified 携带签名 timestamp。 。 。 .

线性

没有线性预设, 因为线性不需要: 它用一个赤色的HMAC在身体上签名 linear-signature,哪个 HmacHeader 在JSON体内贴上时间戳 webhookTimestamp,以毫秒计。 验证签名,解码主体,然后检查新鲜度 Webhooks.checkFreshness数字 :

import type { WebhookRequest } from "@effected/webhooks";
import { HmacHeader, Webhooks } from "@effected/webhooks";
import { Effect, Schema } from "effect";

const LinearPayload = Schema.Struct({ action: Schema.String, webhookTimestamp: Schema.Number });

const verifyLinear = (delivery: WebhookRequest) =>
  Effect.gen(function* () {
    yield* HmacHeader.verify(delivery, { header: "linear-signature", encoding: "hex" });
    const payload = yield* Schema.decodeUnknownEffect(Schema.fromJsonString(LinearPayload))(
      new TextDecoder().decode(delivery.body),
    );
    yield* Webhooks.checkFreshness(payload.webhookTimestamp, delivery.receivedAt, Webhooks.DEFAULT_TOLERANCE);
    return payload;
  });
// Effect<{ readonly action: string; readonly webhookTimestamp: number }, WebhookVerificationError | SchemaError, Mac>

checkFreshness 使用一个 DateTime 或历时几毫秒。 无编号 DateTime 可代表(超出范围, NaN,无限)是 stale_timestamp永远不要,所以发送者提供的价值 可以直接进入。 选择的第四个参数为错误上的预设命名; 它默认为 HmacHeader。 。 。 .

自定义 HMAC 标题

import type { WebhookRequest } from "@effected/webhooks";
import { HmacHeader } from "@effected/webhooks";

const verifyCustom = (delivery: WebhookRequest) =>
  HmacHeader.verify(delivery, { header: "x-signature", encoding: "base64", algorithm: "SHA-512", prefix: "sha512=" });
// Effect<void, WebhookVerificationError, Mac>

对于HMAC在秘密的UTF-8字节下单独将尸体送去的任何发送者来说. header 是小写头名, encoding 实值 hex 或加词 base64, (中文(简体) ), algorithm 实值 SHA-256 (默认)或 SHA-512,以及 prefix 是一个起始值的正文。 签名被解码并比作字节,所以在两种情况下的十六进制都会验证;未加码的碱基64不解码并是 bad_signature。这四个供应商预设了比较编码的字符串,因为他们的发送者只写一个拼写.

一个共同的秘密

import type { WebhookRequest } from "@effected/webhooks";
import { SharedSecret } from "@effected/webhooks";

const verifyCloudflare = (delivery: WebhookRequest) => SharedSecret.verify(delivery, { header: "cf-webhook-auth" });
// Effect<void, WebhookVerificationError, Mac>

对于一个将秘密本身放入标题的发送者来说,比如Cloudflare的 cf-webhook-auth,或路径隐藏您的代理移动到其中。 比较一下 ConstantTime.secretsEqual 从 @effected/crypto,MAC首先在新密钥下两侧,因此密钥的长度和相匹配的前缀都没有漏出. 这就是为什么它需要 Mac 我也一样。 精确的比对值 恰好是检索返回的, 从未被修剪过, 因为修剪会放松一个秘密的比较。 获取 Request是因为 Headers 已经将空白区域周围的条纹画出后, 您才能看到它, 所以通过一个添加密钥匹配的适配器 .

存档并重放一个共享的密件

共享秘密供应商的"签名"是路由秘密本身,所以原始交货的档案决不能保留该信头,没有它,存档交货就不能在重放时重新验证. SharedSecret.attest 薄荷能代替它存档 SharedSecret.verifyAttested 要么接受现场秘密 要么接受证据

import type { WebhookRequest } from "@effected/webhooks";
import { SharedSecret } from "@effected/webhooks";
import { Effect } from "effect";

const route = { header: "cf-webhook-auth", attestation: "x-archive-attestation", context: ["cloudflare", "production"] };

// On arrival: verify the live secret, then mint the attestation to archive instead of the secret header.
const receive = (delivery: WebhookRequest) =>
  Effect.gen(function* () {
    yield* SharedSecret.verify(delivery, { header: route.header });
    return yield* SharedSecret.attest(delivery, { context: route.context });
  });
// Effect<string, WebhookVerificationError | AttestationContextError, Mac>

// On arrival or on replay: the live secret when the delivery carries it, else the archived attestation.
const verifyRoute = (delivery: WebhookRequest) => SharedSecret.verifyAttested(delivery, route);
// Effect<void, WebhookVerificationError | AttestationContextError, Mac>

一种证明是小写为hex HMAC-SHA256,在路由秘密的UTF-8字节下,一个域相分离的前缀,由生体接后. 前缀是 effected-attestation.v1.<count>.<part>.<part>.: 已版本标记 (SharedSecret.ATTESTATION_TAG),上下文部分的数目,然后是每个部分,然后是每个部分 .用于 ["cloudflare", "production"] 这是 effected-attestation.v1.2.cloudflare.production.标记保留了一种证明,因为任何供应商的HMAC在重复使用的秘密下将尸体翻倍,上下文将它与它的路线相接,伯爵也保留了一部分不越过边界进入身体. 每个上下文部分必须非空而不得包含 .; 否则这两个函数都会失败 AttestationContextError (与犯罪 index (单位:千美元) reason 联 合 国 empty 或者说 contains_separator),在读出任何标题之前.

verifyAttested 跑两步 何时 header 送货时带有非空值 将现场秘密比作原始信息 SharedSecret.verify 比较一下,这个答案是最后的:一个错误的秘密是 bad_signature 哪怕是有效的证明 白空间值也是错误的秘密 否则它会读到 attestation 头和检查它与 Hmac.verify 从 @effected/crypto,它解码了六分音符,并在常时比较字节,所以两种都验证. 都没有 missing_signature关闭 header 只接受证明。 将证明书以供应商从未发送过的标题归档, 并剥去信头 从电线上到来的东西, 所以证明书只从你的档案里读回来。 伪造一个需要这个秘密,所以接受这个秘密是不会承认的.

错误

每个预设 verify 失败 WebhookVerificationError 一个人 它携带 reason,路由键; preset,正在核实计划(StandardWebhooks, (中文(简体) ), GitHub, (中文(简体) ), Stripe, (中文(简体) ), Sanity, (中文(简体) ), HmacHeader 或者说 SharedSecret)和可选人可读 detail路径 reason永远不要 message 或者说 detail从来不带秘密 签名或尸体.

reason含义典型状态
missing_signature签名头,或用于 SharedSecret 密信头, 缺席或为空 .401 (中文(简体) )
bad_signature没有签名匹配, 或者签名头已存在, 但它的语法除外: 一个不是 GitHub 值 sha256= 和64个小写十六进制数字,一个没有的条纹标题 t 或者没有 v1,一个不是 t=<ms>,v1=<sig>,一个缺失 HmacHeader 前缀,或不解码的签名.401 (中文(简体) )
malformed除签名外, 方案需要的页眉是不存在的或形状不好的: 一个标准的 Webhooks id 或 timettamp, 或者一个不是 Unix 秒 1 到 15 位数的时标 .400个
stale_timestamp签名匹配,但签名的时间戳 坐在容忍度之外 receivedAt不管怎样 分界线是新鲜的.400个
import type { WebhookVerificationError } from "@effected/webhooks";

const statusFor = (error: WebhookVerificationError): 400 | 401 =>
  error.reason === "missing_signature" || error.reason === "bad_signature" ? 401 : 400;

因为签名是先检查的, 一份有差错签名报告的烂快递 bad_signature。 。 。 . SharedSecret.attest 财务报告和审定财务报表 SharedSecret.verifyAttested 也可以失败 AttestationContextError 对于空或包含的上下文部分 .;它被打出是因为一个上下文通常是从路由来构建的,所以一个呼叫者可以回答它而不是崩溃. 接线错误是缺陷,而不是被键入的失败:不是的标准 Webhooks 密钥 whsec_ base64, 用于 HMAC 预设的空密件( 该核心 ) Mac 拒绝为 InvalidKey),或 a CryptoError 行为不检 Mac 层。 在 WebCrypto 层上, 无运行时间 crypto.subtle 也会死. WebhookVerificationReason 财务报告和审定财务报表 WebhookPreset 以图示导出.

测试

包裹没有测试双倍。 提供 MacLive 从 提供什么 并在测试中建立交付,以测试中以外的执行方式签署,例如 node:crypto是因为 createHmac,或供应商自己的SDK(如果有的话)stripe是因为 generateTestHeaderString, (中文(简体) ), svix是因为 Webhook.sign) (中文(简体) ). 通过固定 receivedAt 并用时间戳签,不需要测试 TestClock打开 Mac.layerWebCrypto 每个 verify 等待一个真正的承诺, 所以测试叉一个然后调整 TestClock 可以吊起:等待它,或者把钟锁上。 测试失败路径以及幸福路径: 以完全一个方式改变每个签名字节的每个部分并断言 bad_signature。 。 。 .

它不会做的

  • 决定交付身份. 它返回签名的部分(电文ID和计划所在的时间戳);去重复、路由和供应商命名都是你的.
  • 解码有效载荷. 解码已核实 body 用你自己的计谋.
  • 环绕 HTTP 框架 . 输入是纯数据,所以符合核心 effect/http一个工人 Request 或者没有适配器包的测试.
  • 读取壁钟. 新鲜度总是根据 receivedAt 你通过.

许可证

麻省理工学院

版本与发布记录

@effected/webhooks 共发布 10 个版本,首次发布于 2026/10/23;过去 12 个月发布了 10 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.6.0latest2026/11/104 天前
0.5.02026/11/095 天前
0.4.02026/11/0311 天前
0.3.02026/11/0113 天前
0.2.02026/10/2916 天前
0.1.32026/10/2619 天前

关于 @effected/webhooks 的常见问题

@effected/webhooks 是什么?

对 Effect v4 的输入 Webhook 签名验证:标准 Webhooks, GitHub, Frede, Sanity, HMAC 头和共享密钥预设.

如何安装 @effected/webhooks?

在项目目录运行 npm install @effected/webhooks,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:effect(~4.1.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@effected/webhooks 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@effected/webhooks 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@effected/webhooks 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=24.11.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@effected/webhooks 可以商用吗?

@effected/webhooks 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@effected/webhooks 的下载量有多少?

根据 npm 官方下载统计,@effected/webhooks 在 2026/10/13 – 2026/11/11 这 30 天内被下载 1,980 次,日均约 66 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@effected/webhooks 还在维护吗?

最新版本 0.6.0 发布于 2026/11/10(4 天前);至今共发布 10 个版本,其中过去 12 个月发布了 10 个;首次发布于 2026/10/23。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。