# @effected/webhooks 中文文档

规范地址：https://npmcn.com/package/%40effected/webhooks/

版本：0\.6\.0
npm 元数据更新时间：2026-11-10T15:11:04.249Z
中文译文更新时间：2026-11-14T16:23:14.765Z
软件包许可：MIT

本文由上游 README 翻译，可能存在偏差，请结合原文核对 API、命令与配置。版权与许可归原作者所有。

原始 npm 页面：https://www.npmjs.com/package/%40effected/webhooks
原文对照：https://npmcn.com/package/%40effected/webhooks/?lang=original
项目主页：https://github.com/spencerbeggs/effected/tree/main/packages/webhooks#readme
源码仓库：https://github.com/spencerbeggs/effected

## 关键信息（来自 npm 元数据）

- 包体积（解压后）：72.7 kB，15 个文件
- 模块格式：ES Module
- TypeScript 类型：包内自带
- Node.js 要求：\>=24\.11\.0
- 运行依赖：1 个
- peer 依赖：effect ~4\.1\.0
- 首次发布：2026-10-23T03:52:21.066Z
- 最新版本发布：2026-11-10T15:11:03.475Z
- 近 30 天下载量：1980（2026-10-13 至 2026-11-11，npm 官方下载统计）
- 已发布版本数：10（近 12 个月 10 个）

## 中文简介

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

## 安装

```sh
npm install @effected/webhooks
```

也可以使用：`pnpm add @effected/webhooks`、`yarn add @effected/webhooks`、`bun add @effected/webhooks`

## README

# @ effect/webhooks

[![npm](https://img.shields.io/npm/v/@effected%2Fwebhooks?label=npm&color=cb3837)](https://www.npmjs.com/package/@effected/webhooks) [![License: MIT](https://img.shields.io/badge/License-MIT-4caf50.svg)](https://opensource.org/licenses/MIT) [![Node.js %3E%3D24.11.0](https://img.shields.io/badge/Node.js-%3E%3D24.11.0-5fa04e.svg)](https://nodejs.org/)

核实寄件人是否签了入境的网路快递,声称是寄件人寄来的,因为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`](https://www.npmjs.com/package/@effected/pnpm-plugin-effected)。 。 。 .
> 
> **稳定:不稳定.** 此软件包的 API 表面尚未考虑 完成并可能改变 `0.x` 释放 准确的版本。 满 政策: [释放策略](https://github.com/spencerbeggs/effected#release-strategy)。 。 。 .

## 为什么

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

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

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

## 安装

```bash
npm install @effected/webhooks effect
```

```bash
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.verifyAttested` | `Mac.Mac` |
| `Webhooks.checkFreshness` | 无 |

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

| 服务 | 任何 WebCrypto 运行时间( 包括工人) | 节点 |
| --- | --- | --- |
| `Mac.Mac` | `Mac.layerWebCrypto.pipe(Layer.provide(SecureRandom.layerWebCrypto))` 从 `effect/crypto` | `NodeCrypto.layerMac` 从 `@effect/platform-node` |

```ts
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`,作为工人或任何以获取为基础的服务器接收:

```ts
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 图像

```ts
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`。 。 。 .

### 条纹

```ts
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, 办事员, 重新发送)

```ts
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`。 。 。 .

### 卫生

```ts
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`数字 :

```ts
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 标题

```ts
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`。这四个供应商预设了比较编码的字符串,因为他们的发送者只写一个拼写.

### 一个共同的秘密

```ts
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` 要么接受现场秘密 要么接受证据

```ts
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个 |

```ts
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` 从 [提供什么](https://npmcn.com/package/%40effected/webhooks/#readme-what-to-provide) 并在测试中建立交付,以测试中以外的执行方式签署,例如 `node:crypto`是因为 `createHmac`,或供应商自己的SDK(如果有的话)`stripe`是因为 `generateTestHeaderString`, (中文(简体) ), `svix`是因为 `Webhook.sign`) (中文(简体) ). 通过固定 `receivedAt` 并用时间戳签,不需要测试 `TestClock`打开 `Mac.layerWebCrypto` 每个 `verify` 等待一个真正的承诺, 所以测试叉一个然后调整 `TestClock` 可以吊起:等待它,或者把钟锁上。 测试失败路径以及幸福路径: 以完全一个方式改变每个签名字节的每个部分并断言 `bad_signature`。 。 。 .

## 它不会做的

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

## 许可证

[麻省理工学院](https://npmcn.com/package/%40effected/webhooks/LICENSE)

## 常见问题

### @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 和提交记录判断。

