# @nxgt/janus-webhooks 中文文档

规范地址：https://npmcn.com/package/%40nxgt/janus-webhooks/

版本：0\.8\.7
npm 元数据更新时间：2026-11-02T00:53:37.384Z
中文译文更新时间：2026-11-02T02:39:14.852Z
软件包许可：MIT

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

原始 npm 页面：https://www.npmjs.com/package/%40nxgt/janus-webhooks
原文对照：https://npmcn.com/package/%40nxgt/janus-webhooks/?lang=original
源码仓库：https://github.com/softistx/nxgt-janus

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

- 包体积（解压后）：389 kB，81 个文件
- 模块格式：ES Module
- TypeScript 类型：包内自带
- 运行依赖：0 个
- peer 依赖：typescript ^6\.0\.3 \|\| ^7\.0\.0，@nxgt/janus ^0\.36\.0
- 首次发布：2026-09-26T23:40:17.483Z
- 最新版本发布：2026-11-02T00:53:37.230Z
- 已发布版本数：35（近 12 个月 35 个）

## 中文简介

@nxgt/janus用户事件的签名webhooks：标准的webhooks签名，通过可插拔的持久队列带回退的重试，旋转秘密，以及报告用完的交付——从不静默丢弃

## 安装

```sh
npm install @nxgt/janus-webhooks
```

也可以使用：`pnpm add @nxgt/janus-webhooks`、`yarn add @nxgt/janus-webhooks`、`bun add @nxgt/janus-webhooks`

## README

# @nxgt / janus-webhooks

[`@nxgt/janus`](https://www.npmjs.com/package/@nxgt/janus) 用户事件, 作为签名的webhook交付 [标准的人](https://www.standardwebhooks.com/) 规格:每 用户事件被发送到你的端点，用HMAC-SHA256签名，重试 失败——通过一个可能比流程更持久的队列——和每次交付 放弃了就报告，放弃了就不吭声。接收方检查 签名： `verifyWebhook`，或使用任何标准Webhooks库。

```ts
import { createMemoryStores, janus, scryptHasher } from '@nxgt/janus';
import { webhooks } from '@nxgt/janus-webhooks';
import { z } from 'zod';

const secret = process.env.WEBHOOK_SECRET; // whsec_…, from mintWebhookSecret()
if (!secret) throw new Error('WEBHOOK_SECRET is not set');

const listener = webhooks({
    endpoints: [{ url: 'https://crm.example.com/hooks/janus', secrets: [secret] }],
});

export const auth = janus({
    user: z.object({ email: z.email() }),
    password: { login: 'email' },
    store: createMemoryStores(),
    hasher: scryptHasher(),
    events: listener, // every user.created, user.emailVerified, user.passwordReset, user.passwordChanged, user.emailChanged, user.secondFactorEnabled, user.secondFactorDisabled, user.recoveryCodesRegenerated, user.recoveryCodeUsed, user.newDeviceSignedIn, user.avatarChanged, user.avatarRemoved, user.deleted
});

process.on('SIGTERM', async () => {
    await listener.close(); // waits for requests in flight, gives up the deliveries waiting for a retry
    process.exit(0);
});
```

> **0\. x。** 一个小版本仍然可能改变表面；更新日志告诉我们该怎么做。

## 安装

```sh
bun add @nxgt/janus-webhooks @nxgt/janus
bun add zod # the schema of the examples; any Standard Schema library will do
bun add -d typescript
```

两个对等体都需要： `@nxgt/janus` 0.17 -确切的范围是在 `peerDependencies` 和 `typescript` （6或7）。 `@nxgt/janus` 是一个 **同行**，所以它的一个副本定义 `UserEvent`。就像 `@nxgt/janus`，它期望 `"moduleResolution": "bundler"`. 它使用 `node:crypto`，全球 `fetch` 和 `process.emitWarning`：包子或 节点。

## 子路径

| 进口 | 它包含什么 |
| --- | --- |
| `@nxgt/janus-webhooks` | **发送和接收**： `webhooks()`， `verifyWebhook()`， `mintWebhookSecret()`，和 `WebhookQueue` 端口与其在内存中的引用， `createMemoryWebhookQueue()` |
| `@nxgt/janus-webhooks/conformance` | **对于队列适配器**套房a `WebhookQueue` 运行-参见 [API](https://npmcn.com/package/%40nxgt/janus-webhooks/#readme-api) |

适配器的规范是在每个情况下对一个新的空队列运行套件：

```ts
import { describe, it } from 'bun:test';
import { describeWebhookQueues } from '@nxgt/janus-webhooks/conformance';

describeWebhookQueues({
    name: 'my queue',
    runner: { describe, it }, // under bun test, pass them: they are not on globalThis
    harness: {
        async open() {
            const db = await openEmptyDatabase();
            return {
                queue: createMyWebhookQueue(db),
                faults: { fail: async (method) => db.failEvery(method) }, // leave out, and the outage cases pass with a JANUS_CONFORMANCE_SKIPPED warning
                close: () => db.drop(),
            };
        },
    },
});
```

这个 [队列指南](https://npmcn.com/package/%40nxgt/janus-webhooks/docs/guide/queues.md#testing-an-adapter) 每一个案例 失败的是什么？

## 使用

### 发送, `webhooks()`

`webhooks({ endpoints })` 听众回答道 `janus({ events })` 需要, 与一个 `close()` 为关闭。它将每个用户事件发送到每个端点 谁的 `types` 包括它-所有13种类型 `types` 缺席:

```ts
import { webhooks } from '@nxgt/janus-webhooks';

const listener = webhooks({
    endpoints: [
        { url: 'https://crm.example.com/hooks/janus', secrets: [crmSecret] },
        { url: 'https://search.example.com/hooks', secrets: [searchSecret], types: ['user.deleted'] },
    ],
    retries: ['5s', '5m', '30m', '2h', '5h', '10h', '10h'], // the default: at once, then these
    timeout: '10s', // the default, per request
    onGivingUp: async (delivery, reason) => {
        // reason.why: 'retriesRanOut' | 'closed' | 'endpointRemoved'; delivery.attempts: how many requests were sent
        await deadLetters.insert({ eventId: delivery.event.id, endpoint: delivery.endpoint, ...reason });
    },
});
```

这个 [发送指南](https://npmcn.com/package/%40nxgt/janus-webhooks/docs/guide/sending.md) 所有的选择，重试 时间表，何时放弃交付以及为什么放弃，电汇格式和测试。

### 〇在重启中幸存下来 `queue`

没有一个 `queue`，交付在进程的内存中等待：崩溃会丢失 重试仍在等待中。通过一项 `WebhookQueue` 每个进程共享，a 一个进程失败的重试被下一个进程发送：

```ts
import { createMemoryWebhookQueue, webhooks } from '@nxgt/janus-webhooks';

const secret = process.env.WEBHOOK_SECRET; // whsec_…, from mintWebhookSecret()
if (!secret) throw new Error('WEBHOOK_SECRET is not set');

// The reference, in memory: swap in a durable adapter of the port — such as
// createRedisWebhookQueue(redis) from @nxgt/janus-webhooks-redis — for a
// queue that survives a restart.
const queue = createMemoryWebhookQueue();

const listener = webhooks({
    endpoints: [{ id: 'crm', url: 'https://crm.example.com/hooks/janus', secrets: [secret] }],
    queue, // the listener now awaits the insert — one call to the queue, never an endpoint
});

process.on('SIGTERM', async () => {
    await listener.close(); // gives nothing up: what waits is sent by the next process
    process.exit(0);
});
```

`createMemoryWebhookQueue()` 是港口的参考，和 `@nxgt/janus-webhooks/conformance` 适配器运行的套件。这个 [队列指南](https://npmcn.com/package/%40nxgt/janus-webhooks/docs/guide/queues.md) 有合同，租约，端点id， 以及如何编写和测试适配器。

### 收到, `verifyWebhook()`

`verifyWebhook` 回答了 `UserEvent` 请求携带或 `null` 当它 没有一个端点的秘密被签名。在一个fetch风格的处理程序中：

```ts
import { verifyWebhook } from '@nxgt/janus-webhooks';

export async function receive(request: Request): Promise<Response> {
    const event = verifyWebhook({
        secrets: [secret],
        headers: request.headers,
        body: await request.text(), // the raw body: never request.json()
    });
    if (event === null) return new Response(null, { status: 401 }); // forged, altered, replayed late, or not a user event
    if (await handled.has(event.id)) return new Response(null, { status: 204 }); // a retry of one already handled

    await onUserEvent(event); // { id, type, occurredAt, userId, userType }
    await handled.add(event.id);
    return new Response(null, { status: 204 });
}
```

这个 [接受指导](https://npmcn.com/package/%40nxgt/janus-webhooks/docs/guide/receiving.md) 有选择的理由吗 用于 `null`， Hono和Node处理程序，处理一次事件，以及一个测试。

### 秘密, `mintWebhookSecret()`

```ts
import { mintWebhookSecret } from '@nxgt/janus-webhooks';

mintWebhookSecret(); // 'whsec_…': 32 random bytes, base64 — give the same one to the sender and the receiver
```

来自另一个标准Webhooks库的秘密被接受 它可以保存24到64字节 `whsec_`，范围规格成套。

## API

| 出口 | 它是什么？ |
| --- | --- |
| `webhooks(options)` | 倾听者 `janus({ events })`, `close()`． 选项: `endpoints` (每 `{ url, secrets, types?, id? }`)， `retries`， `timeout`， `onGivingUp`， `fetch`， `queue`， `concurrency`， `lease`， `poll`， `orphanGrace`。与一个 `queue` 听众回答插入的a `Promise`;如果没有一个，或者对于一个没有端点的事件， `undefined`。接线错误是一个问题。 `TypeError` 当侦听器被调用时，手动重建的非1事件也是如此 |
| `verifyWebhook(options)` | `{ secrets, headers, body, toleranceSeconds?, now? }` → `UserEvent | null`。获取头文件 `Headers` 或Node头记录。没有秘密，一个畸形的，一个 `toleranceSeconds` 这不是一个有限的秒数，或者a `now` 这是无效的 `Date` 是一个 `TypeError` |
| `mintWebhookSecret()` | 一个新的 `whsec_` 端点的秘密 |
| `createMemoryWebhookQueue()` | 参考 `WebhookQueue`，在记忆里：什么 `webhooks()` 不带a的用法 `queue`一个给两个人分享 `webhooks()` 在测试中 |
| `WebhookQueue`， `QueuedDelivery` | 持久队列实现的端口，以及它持有的一个交付：事件和端点的id，而不是URL或秘密 |
| `WebhooksOptions`， `WebhookEndpoint`， `Webhooks` | 什么 `webhooks()` 提问和回答 |
| `Delivery`， `GivingUp`， `Failure` | 什么 `onGivingUp` 收到: `{ event, url, endpoint, attempts }` - - - - - - `url` 是 `null` 用于 `endpointRemoved` 和 `{ why, status, error }`:一个 `GivingUp` 是一个 `Failure`最后一次尝试得到了什么，以及原因 |
| `VerifyOptions`， `HeadersLike`， `WebhookBody` | 什么 `verifyWebhook` ，以及连接上的JSON主体 |
| `@nxgt/janus-webhooks/conformance` | 队列适配器套件运行： `describeWebhookQueues({ name, harness, runner })`， `runWebhookQueueCase`，个案作为资料(`webhookQueueCases`， `webhookQueueOutageCases`， `allWebhookQueueCases`)， `referenceWebhookQueueHarness()`， `SKIP_REASONS`，以及它们的类型。它没有导入测试框架 |

## 陷阱

**重试在内存中等待，除非传递 `queue`.** 没有它，就会崩溃， 或者一个没有 `close()`，失去了每一次重试，仍然等待，一无所获 稍后发送。与一个 `queue` 每个进程共享一个进程留下的东西 等待-重试，请求被崩溃打断-下一个发送。

**至少一次，即使是跨重启。** 在A项下要求交货 在请求中途死亡的进程不会损失任何东西：一旦该请求过期 失效后，另一个进程会再次发送它——所以接收器可能会看到两次， 而坠机只需要一次尝试。接收方重复数据删除 `webhook-id`，因为他们已经必须进行重试。一个 `lease` 至少是 `timeout` 再加上一秒钟：工人在报告失败之前扩展了它， 所以慢一点 `onGivingUp` 不将交付交给另一个进程。

**呼叫 `close()` 在关闭。** 没有一个 `queue`，它首先发送每个 交货期： `close()` - - - - - - `concurrency` 一次，所以一个事件发生了一会儿 每个繁忙的槽位仍然得到它的尝试-等待请求进入 并放弃等待重试的交付，每个报告为 `closed` ——没有它，他们一言不发地消失了。有一个，它等待 请求在飞行中，不放弃任何东西。 `process.on('SIGTERM', () => listener.close())`.

**侦听器只等待插入操作。** 每次送货都要经过 队列;侦听器等待插入- a `Map` 在内存中，一个调用 `queue` -然后返回，第一个请求立即开始，所以 `signUp` 从不等待端点。与一个 `queue`，插入失败被拒绝； 和 `janus` 报告为 `JANUS_EVENT_FAILED` 使用事件的id。重试的 定时器不保持进程打开结束一个脚本 `await listener.close()`.

**端点的id是队列知道它的依据。** 没有一个 `id`，这是一个 URL的散列：在等待交付的情况下更改端点的URL 让他们没有终点，之后 `orphanGrace` （24小时） 作为 `endpointRemoved`, `delivery.url` `null`。给每个端点一个 `id` 当你第一次通过 `queue`：稍后添加id也会改变id。

**重定向是失败的，而且只有 `2xx` 是成功的。** 重定向不是 遵循:点 `url` 在最后的演讲中， `https://` - - - - - - `http://` 只 `localhost`.

**在进行任何解析之前，验证原始正文。** 签名覆盖字节数 发送: `await request.text()`,从来没有 `request.json()` 也不是主体解析器 重新连载了它。

**容差内的重播进行验证。** 请求被复制并再次发送 在 `toleranceSeconds` （300）通过，您已经重试的一个也通过 已处理：保持id已处理 `event.id`, `webhook-id` 标题- and 用a回答第二题 `2xx`什么都不做。

**用四个步骤旋转一个秘密。** 双方签字 （`secrets: [old, next]`)，让接收者都接受，然后丢掉旧的 来自发送者，最后来自接收者。

**secret的长度为24 ~ 64字节。** 过去的64年, `webhooks()` 和 `verifyWebhook()` 扔一个 `TypeError` -从0.4.0开始早期版本的长度为24。 以前的较长的秘密在升级时被拒绝，所以要远离它 首先，在运行mint的版本上 `mintWebhookSecret()`，旋转到 它在上面的四个步骤，然后升级。

**先升级接收方，再升级发送方。** 一个接收器 `verifyWebhook` 答案 `null` 对于它不知道的类型，所以传递失败，直到它知道 〇放弃 `@nxgt/janus-webhooks` 在0.3.0之前，这两者都不知道 `user.secondFactorEnabled` 也不 `user.secondFactorDisabled`，以及0.4.0之前 既不 `user.recoveryCodesRegenerated` 也不 `user.recoveryCodeUsed`, 在0.6.0之前 `user.passwordChanged` 也不 `user.emailChanged`, 在0.7.0之前不 `user.newDeviceSignedIn`，以及0.8.0之前的版本 `user.avatarChanged` 也不 `user.avatarRemoved`。在每个接收器都升级之前 给它的端点 `types` 它知道:

```ts
webhooks({
    endpoints: [{ url, secrets: [secret], types: ['user.created', 'user.emailVerified', 'user.passwordReset', 'user.deleted'] }],
});
```

该列表适用于0.3.0之前的接收器。0.3分之一。X，它知道 第二个因素的事件，而不是恢复代码，加上这两个：

```ts
types: ['user.created', 'user.emailVerified', 'user.passwordReset', 'user.secondFactorEnabled', 'user.secondFactorDisabled', 'user.deleted'],
```

1对0.4。X或0.5。X，它知道恢复代码的事件，但不知道 变化事件，加上这两个

```ts
types: ['user.created', 'user.emailVerified', 'user.passwordReset', 'user.secondFactorEnabled', 'user.secondFactorDisabled', 'user.recoveryCodesRegenerated', 'user.recoveryCodeUsed', 'user.deleted'],
```

1 / 0.6。X知道变化事件，但不知道新设备的变化， 加上这两个：

```ts
types: ['user.created', 'user.emailVerified', 'user.passwordReset', 'user.passwordChanged', 'user.emailChanged', 'user.secondFactorEnabled', 'user.secondFactorDisabled', 'user.recoveryCodesRegenerated', 'user.recoveryCodeUsed', 'user.deleted'],
```

**`user.passwordChanged` 张贴的时候没有 `first`** （在没有密码的帐户上设置的第一个密码）：接收方无法区分它和更改。

**`user.emailChanged` 张贴的时候没有 `formerEmail`.** `@nxgt/janus` 将用户之前的地址交给听者，这样通知就可以到达 收件箱; `webhooks()` 在队列之前丢弃它，因此没有端点、队列或 `onGivingUp` 报告中没有地址。接收器通过读取用户 `userId` 对于任何类型；把通知寄到原来的地址 `janus({ events })` 而不是听者，旁边 `webhooks()`：

```ts
const auth = janus({
    ...config,
    async events(event) {
        await listener(event); // webhooks(): every type, formerEmail dropped
        if (event.type === 'user.emailChanged' && event.formerEmail != null) {
            const user = await auth.get(event.userId);
            await mail.emailChanged({ name: user.name, formerEmail: event.formerEmail, newEmail: user.email });
        }
    },
});
```

**`user.avatarChanged` 和 `user.avatarRemoved` 张贴没有他们的 `formerAvatarId`**，出于同样的原因：图像id保留在进程中。 中删除前一个图像 `janus({ events })` 侦听器。

**`user.newDeviceSignedIn` 张贴的时候没有 `sessionId`**，为了同样的目的 原因：会话id还在进程中。发送新签到通知 这个 `janus({ events })` 听者，或从签到者自己的回答，其 `newDevice` 所以说。

**警告会指定URL的来源，而不是URL。** `JANUS_WEBHOOK_GAVE_UP` 写 `https://crm.example.com` 因为路径或查询可能包含的令牌 接收机的; `onGivingUp` 接收全部 `delivery.url`，所以不要登录 就像现在这样。队列根本不保存URL——只保存端点的URL id。

症状和修复程序已经出现 [故障排除](https://npmcn.com/package/%40nxgt/janus-webhooks/docs/troubleshooting.md).

## 类型安全，计数

**11个貌似合理的错误，11个在编译时被拒绝。** `test/types/webhooks.ts` 拥有一个 `@ts-expect-error` 每个错误，旁边的 必须保持编译的连接(`janus({ events: webhooks(…) })`，与 没有一个 `queue`)：没有秘密的端点，从 环境及未检查(`string | undefined`)，一种类型 `janus` 从来没有 发送一个重试，该重试不是 `Duration`，一个没有秘密的接收器，读一个 已验证事件，然后再检查是否为 `null`，缺少一个方法的队列 `concurrency` 写成字符串，端点 `id` 写成数字a `switch` 在 `reason.why` ,忘记 `endpointRemoved`，还有 `onGivingUp` ,上面写着 `delivery.url` 没有检查 `null` - - - 不再配置端点的URL。

## 文档

-   [指南](https://npmcn.com/package/%40nxgt/janus-webhooks/docs/README.md) -发送，接收和队列，每个选项的例子
-   [故障排除](https://npmcn.com/package/%40nxgt/janus-webhooks/docs/troubleshooting.md) -通过您看到的警告或错误
-   [路线图](https://npmcn.com/package/%40nxgt/janus-webhooks/docs/roadmap.md) -下一步是什么，什么是没有计划的

## 执照

麻省理工学院

## 常见问题

### @nxgt/janus-webhooks 是什么？

@nxgt/janus用户事件的签名webhooks：标准的webhooks签名，通过可插拔的持久队列带回退的重试，旋转秘密，以及报告用完的交付——从不静默丢弃

### 如何安装 @nxgt/janus-webhooks？

在项目目录运行 npm install @nxgt/janus-webhooks，或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖：typescript（^6\.0\.3 \|\| ^7\.0\.0）、@nxgt/janus（^0\.36\.0）。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖，Yarn 需要手动安装。

### @nxgt/janus-webhooks 支持 TypeScript 吗？

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

### @nxgt/janus-webhooks 支持 ESM 还是 CommonJS？

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

### @nxgt/janus-webhooks 可以商用吗？

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

### @nxgt/janus-webhooks 还在维护吗？

最新版本 0\.8\.7 发布于 2026/11/02（1 天前）；至今共发布 35 个版本，其中过去 12 个月发布了 35 个；首次发布于 2026/09/27。发布频率只是参考信号：功能稳定的工具可能很少需要更新，建议结合源码仓库的 issue 和提交记录判断。

