# @canopy-io/nestjs 中文文档

规范地址：https://npmcn.com/package/%40canopy-io/nestjs/

版本：0\.9\.1
npm 元数据更新时间：2026-11-06T19:51:22.115Z
中文译文更新时间：2026-11-09T09:58:40.230Z
软件包许可：MIT

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

原始 npm 页面：https://www.npmjs.com/package/%40canopy-io/nestjs
原文对照：https://npmcn.com/package/%40canopy-io/nestjs/?lang=original
项目主页：https://github.com/canopy-identity/canopy-javascript#readme
源码仓库：https://github.com/canopy-identity/canopy-javascript

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

- 包体积（解压后）：208 kB，10 个文件
- 模块格式：ESM + CJS
- TypeScript 类型：包内自带
- Node.js 要求：\>=18
- 运行依赖：1 个
- peer 依赖：@nestjs/core ^10\.0\.0 \|\| ^11\.0\.0，@nestjs/common ^10\.0\.0 \|\| ^11\.0\.0，reflect-metadata ^0\.1\.13 \|\| ^0\.2\.0
- 首次发布：2026-07-28T10:04:39.704Z
- 最新版本发布：2026-11-06T19:51:21.824Z
- 近 30 天下载量：2956（2026-10-09 至 2026-11-07，npm 官方下载统计）
- 已发布版本数：27（近 12 个月 27 个）

## 中文简介

Canopy的官方NestJS整合——等级身份和访问管理.

## 安装

```sh
npm install @canopy-io/nestjs
```

也可以使用：`pnpm add @canopy-io/nestjs`、`yarn add @canopy-io/nestjs`、`bun add @canopy-io/nestjs`

## README

# @canopy-io/nestjs (英语)

正式NestJS整合 [树冠](https://canopy-io.com/) ——B2B SaaS的分级身份和准入管理.

将权限检查转换成路由中的装饰器,而不是每个处理器中的呼叫.

## 安装

```bash
npm install @canopy-io/nestjs
```

要求NestJS 10或11以及节点18或更后. `@canopy-io/node` 带着它.

## 特征

-   **一个注册.** `CanopyModule.forRoot` 静态键, `forRootAsync` 当它来自一些可以注射的东西。 传出自己的请求类型 解析者会按它打字.
-   **`@RequirePermission` 路上的车.** 检查是处理器上的声明,而不是内部的呼叫. 一条没有它的路线通过未受阻碍,因此可以在全球注册并选择进入.
-   **`CanopyGuard`** 对声明的答复 **在自己的过程中**,在节点上请求名称——继承一个赋予父方的角色,而无需您模拟行走.
-   **没有网络呼叫每个请求.** 守卫持有身份的授予根 和共享副本 您的等级, 最多刷新一次每分钟, 所以一个被守卫的路线 通常没有达到任何网络.
-   **三所观相.** `node` 是默认和严格的问题; `app_wide` 仅询问该身份是否持有环境许可; `org` 问一个节点问题 在组织 打电话者的标志正在采取行动.
-   **`CanopyTokenGuard`** 对照已公布的签名密钥,在当地核实一个Canopy签发的无记名代号,并附上债权,用于前方没有认证层的应用程序。 Additive — 如果您已经运行过护照或自己的JWT中间软件, 请跳过它 .
-   **`@AllowServiceTokens()`** 承认机器客户端 `client_credentials` 在为机器建造的路线上,由客户自给授权;所有其他路线继续拒绝.
-   **`@AllowDelegatedTokens()`** 允许一个机器客户在信使交换后代理一个人,只有在该人和客户都持有许可的情况下才授权.
-   **`@InjectCanopy()`** 手拿满了 `@canopy-io/node` 警卫不覆盖的一切客户端.
-   **失败者关闭, 状态告诉你这是失败.** `403` 以否认发誓, `503` 以不可测者发誓, `500` 为了打破融合.
-   **一张有限度的支票.** 它自己的评价超时和重试计数,独立于全客户端的计数,在尝试间等待被封.
-   **呼叫者挂起时中止**在快递和快递上, 而不是完成一个决定 没有人会读.

## 登记册

```ts
import { CanopyModule } from "@canopy-io/nestjs";

@Module({
  imports: [
    CanopyModule.forRoot<AuthedRequest>({
      isGlobal: true,
      apiKey: process.env.CANOPY_API_KEY,
      verify: { environmentId: process.env.CANOPY_ENVIRONMENT_ID },
      resolveIdentity: (request) => request.user?.sub,
      resolveNode: (request) => request.params?.orgId,
    }),
  ],
})
export class AppModule {}
```

注册一次,在根模块. `isGlobal: true` 使您的功能模块中的守护者能够看到它的提供者,被守护控制器通常住在那里;没有它,注册模块外的守护者无法解决其符号验证器,应用程序拒绝启动. 默认为 `false`,作为 `@nestjs/config` 已经.

`verify` 将模块接受的符号命名为环境(`environmentId`,或 `issuer` 自定义认证域或自托管实例;自托管实例可以设置 `CANOPY_BASE_URL` 进程,其中 `environmentId` 和客户都跟着。 需要:模块启动时会建立其符号验证器.

两个解脱者是守卫如何学习的 _谁_ 问和 _地点_在那些居住的地方,即你申请的业务——一个经过核实的代号上的债权,一个路由参数,一个会话上的租户——所以你提供它们而不是图书馆的猜测. 将您自己的请求类型传递到 `forRoot` 双方的决心都是对着它的.

当密钥来自可注入的东西时,使用 `forRootAsync`数字 :

```ts
CanopyModule.forRootAsync({
  isGlobal: true,
  imports: [ConfigModule],
  inject: [ConfigService],
  useFactory: (config: ConfigService) => ({
    apiKey: config.getOrThrow("CANOPY_API_KEY"),
    verify: { environmentId: config.getOrThrow("CANOPY_ENVIRONMENT_ID") },
    resolveIdentity: (request) => request.user?.sub,
    resolveNode: (request) => request.params?.orgId,
  }),
});
```

## 守卫一条路

```ts
import { CanopyGuard, RequirePermission } from "@canopy-io/nestjs";

@Controller("orgs/:orgId/orders")
@UseGuards(CanopyGuard)
export class OrdersController {
  @Post(":orderId/refund")
  @RequirePermission("orders.refund")
  refund() {}
}
```

这解答了身份是否存在 `orders.refund` **时间** `orgId`,走节点的分界线——所以一个赋予给父母的角色是继承的,没有你建模它.

步行发生在你的过程中,而不是在网络上。 见 [如何作出决定](https://npmcn.com/package/%40canopy-io/nestjs/#readme-how-the-decision-is-reached)。 。 。 .

没有的路由 `@RequirePermission` 通过未触地而过,因此警卫可以在全球注册,选择进入每条路线. 对操作员的要求胜过对控制员的要求.

### 如果你在全球注册的话

巢穴运行全球卫士 **在此之前** 控制器和路由范围。 所以,全球注册 `CanopyGuard` 在路由级别前运行 `AuthGuard` 已经按要求做了任何事 `resolveIdentity` 找不到任何线索 每一个警戒路线的答案 `403`。 。 。 .

在全球登记你的认证守卫, 在这之前:

```ts
providers: [
  { provide: APP_GUARD, useClass: JwtAuthGuard }, // must come first
  { provide: APP_GUARD, useClass: CanopyGuard },
];
```

全球卫视按注册顺序运行. 如果您的认证是路由级别, 请应用 `CanopyGuard` 在路线上也.. `@UseGuards(JwtAuthGuard, CanopyGuard)` ——左向右命令坚持的地方.

### 范围

默认是 `node`,这是一个严格的问题:该身份是否持有许可 _在这个节点_。 。 。 .

```ts
@RequirePermission("reports.view", { scope: "app_wide" })
```

`app_wide` 仅询问身份是否在环境的任何地方持有许可,并返回无效节点。 决定是否显示菜单项是正确的,而保护属于节点的资源是错误的——这就是它被选入而不是默认的原因.

```ts
@RequirePermission("invoices.view", { scope: "org" })
```

`org` 用于环境 **组织** 打开,其中 符号带会议正在采取行动的组织(`org_id`)和 在其中心发挥的作用`org_roles`可能没有。 警卫说 `org_id` 取消已核实的索赔 `CanopyTokenGuard` 附上并在该节点进行评价—— 组织 _实值_ 分级节点,成员的作用是在 无论是在它下面,所以没有 `resolveNode` 路由的节点是 需要帮助。 在任何组织中行事的打电话者均被拒绝: 没有任何意义.

这包括你自己的员工。 工作人员标志(在环境根基的作用, `org_scope: "all"`(没有) `org_id` 直到会话进入 组织 `switch-organization` 或主机 `organization=` 参数,而警卫从不把组织从路线取出. 进入是客户组织对其工作人员活动和 这样一条让工作人员走进这条路的路线就会跳过这两条路。 一次 内部, `org_id` 设定, 检查为成员, 相对于角色 工作人员扎根; `orgContext(claims).staff` 告诉你的界面是 a 工作人员会议。 对于一个菜单,需要"在这个组织的任何地方",使用 `holdsAnywhereUnder` 从 `@canopy-io/node`,这是一个读取,从来没有 警卫.

如果您自己的认证层 验证了信物和将权利要求停放在别处 比起来 `attachTokenAs`配置 `resolveOrg` - 仅输入一个值 关闭 a _已核实_ 指标,因为它决定了哪个组织的赠款答案 那张支票.

## 如果Canopy给你的代币

`resolveIdentity` 假设上游的东西 已经核实了呼叫者。 若为 你还没有认证层, `CanopyTokenGuard` 是那层,它验证 与Canopy公布的签名密钥并附上索赔要求的无记名证, 这样 `resolveIdentity` 有值得信赖的东西读.

```ts
CanopyModule.forRoot<AuthedRequest>({
  apiKey: process.env.CANOPY_API_KEY,
  verify: {
    environmentId: process.env.CANOPY_ENVIRONMENT_ID,
    audience: process.env.CANOPY_OAUTH_CLIENT_ID, // omit for Direct API
  },
  resolveIdentity: (request) => request.canopyToken?.sub,
});
```

```ts
@UseGuards(CanopyTokenGuard, CanopyGuard)
@RequirePermission("documents.read")
findAll() {}
```

**秩序事项.** 巢穴的卫兵从左到右 `CanopyGuard` 读什么 `CanopyTokenGuard` 随附. 倒置,每个请求都被拒绝——身份 解决之前,任何事情已经建立.

校验是本地的: 获取一个密钥集, 然后逐个签名检查 请求完全没有网络。 如果键是时无法到达 Canopy 警卫会用它持有的钥匙来核实.

所附索赔为: `CanopyTokenClaims`,加上任何习惯声称 环境映射到符号上 . `CanopyTokenRequest<TCustom>` 输入请求 警卫一直跟着那些名字,每个可选的, 所以一个操作员读 `request.canopyToken?.plan` 键入。 未存储或未存储的自定义索赔 与符号不符`custom_claims_overflow`),没有:将这一点视为没有 设定,绝不是传球.

```ts
@Get("quarterly")
quarterly(@Req() request: CanopyTokenRequest<{ plan?: "free" | "pro" }>) {
  if (request.canopyToken?.plan !== "pro") {
    throw new ForbiddenException();
  }
}
```

只有身份标志才能被接受 签入你申请的人. Canopy为自己的一位管理员或工作人员签名 `401` 除非您将其类型添加到 `verify.allowedTypes`。 。 。 .

机器客户端的代号( A)`type: "service"`从 `client_credentials` (赠款)答复 `401` 除了在一条路或控制器上承认 内容 `aud` 命名您的 API, 所以设置 `verify.serviceAudience` 给观众看您的API 回答:

```ts
CanopyModule.forRoot<AuthedRequest>({
  apiKey: process.env.CANOPY_API_KEY,
  verify: {
    environmentId: process.env.CANOPY_ENVIRONMENT_ID,
    serviceAudience: "https://api.example.com",
  },
  resolveIdentity: (request) => request.canopyToken?.sub,
});
```

```ts
@UseGuards(CanopyTokenGuard, CanopyGuard)
@AllowServiceTokens()
@RequirePermission("orders.import", { scope: "org" })
@Post("import")
import() {}
```

那边 `CanopyGuard` 授权机器客户从自己的赠款中,改为: 从核实的索赔,从来没有 从任何呼叫者发送。 象征着 `sub` 是客户的 `client_id`这样 `resolveIdentity: (request) => request.canopyToken?.sub` 两种服务。 一个机器客户端被绑在 组织有其 `org_id`这样 `scope: "org"` 一样的工作方式.

机器客户端代表一个人 `delegated` 符号( 见 `exchangeToken` 内 `@canopy-io/node`),答案为 `401` 除在一条路线上或 控制器携带 `@AllowDelegatedTokens()`。它被持有 `verify.serviceAudience` 我也一样。 那边 `CanopyGuard` 评估个人 (单位:千美元)`resolveIdentity` 仍读 `sub`)和客户端在 `act.sub`,并允许 只有在双方持有许可时:

```ts
@UseGuards(CanopyTokenGuard, CanopyGuard)
@AllowDelegatedTokens()
@RequirePermission("deals.read", { scope: "org" })
@Get()
list() {}
```

拒绝的代号是401 不管细节是什么 具体代码还留在上面 `error.cause` 而不是交给无用的召唤者 以表示差异,而不应告诉哪些检查失败。 例外的是 `token.jwks_unavailable`,答案为 **第503条**: 无法核实不是 与未能核实相同, 呼叫者的信号可能完全良好 告诉客户 它的代号在键盘服务员出局时是呆滞的 清新风暴 已经下来的东西.

土地权利要求 `request.canopyToken`没有 `request.user` - 那一个属于 护照 悄悄地写下来 会是某人的艰难的下午. 使用 `attachTokenAs` 把它们放到别的地方.

如果您已经运行过护照或自己的JWT, 则完全跳过此守护 中间软件。 它是添加剂;包件中没有任何其它东西取决于它.

## 客户

对于警卫不覆盖的任何东西,注射客户端:

```ts
import { InjectCanopy, type Canopy } from "@canopy-io/nestjs";

@Injectable()
export class OrdersService {
  constructor(@InjectCanopy() private readonly canopy: Canopy) {}

  async assign(identityId: string, nodeId: string, roleId: string) {
    await this.canopy.assignments.create({
      identity_id: identityId,
      node_id: nodeId,
      role_id: roleId,
    });
  }
}
```

一样 [`@canopy-io/node`](https://npmcn.com/package/%40canopy-io/node) 客户端,带有重试策略、页码和打出错误.

## 未关闭

没有路经过 `CanopyGuard` 允许请求其决定不明。 每项请求都结束:

-   没有解决身份问题,包括当一个解密者提出未经认证的请求时—— `403`
-   (单位:千美元) `node` 检查节点,或检查否 `resolveNode` 配置 - `403`
-   身份没有被允许, 也没有在这个节点的分界线上 `403`
-   Canopy从未听说过这个身份(`404`(b) —— `403`
-   由于 Canopy 无法到达、 限制速率或回答, 无法补足缓存错失 `5xx` —— 说 `503`,故意没有 `403`因为"我们不能决定"不是"你不允许"
-   Canopy 拒绝 API 密钥 (`401`编号 :`403`)或拒绝阅读(其他 `4xx`(b) —— `500`,因为一个错误的配置不是一个临时的条件,没有再试会修复它
-   此证书可以读取的等级结构并不包含节点 —— `500`,命名事业。 不可判定,不可否认:回答 `403` 将清除每一个节点范围 路线,同时看起来像普通政策
-   呼叫者挂断了中段——中止被传播,而不是变成一个 `503`因为没有失败 没有人在等待答案

被搁置的答案从不通过窗外的出道而送达。 过期后,无法填充的读取即为 `503` ——无声地扩展它将会用它来延长撤销窗口,而没有人选择这样做.

除打电话者外,在请求被拒绝之前,所有挂断的电话都登记在案.

这个 `500` 案例值得注意:它们意味着一体化被打破,而不是服务缓慢。 报告为: `503` 将一个糟糕的API钥匙埋在 看起来像停电.

## 如何作出决定

警卫不问卡诺比每个请求。 它问了一个不同的问题,一次, 并回答每个以后检查的结果.

而不是"也许这种身份行为" **这里**",写着"**地点** 允许这种身份行为”——每个许可的节点。 这些赠款来源没有通过他们的后代扩大,因为赠款已经意味着 _这个节点和下面的一切_扩大后,每个身份都会重现等级, 并且是最高的, 对于近根 授予您的管理员持有.

所以有两样东西 由它们所依赖的东西来划分:

-   **每个身份的赠款根数** ——小,少数任务
-   **您的等级, 每个进程一次** - 每一个身份都认同,因为其形状并不取决于谁在要求

支票就是从有关节点上走出来 寻找赠品根 一个 `app_wide` 检查更为简单: 出现的权限是答案, 不需要分级 .

**两人最多保持60秒**因此,这是出入权改变与其生效之间的延迟. `authorizationTtlMs` 改变它,最多5分钟`300_000`); 启动时丢出更长的值。 等级被重新验证而不是重新读取——对首页的有条件请求,通常会回答 `304`,所以没有转移。 两种读取均作一页并全文取出,100行取出一页;当等级变化后,每一页再读取一页(参见: [`LocalAuthorizer`](https://npmcn.com/package/%40canopy-io/node#authorizing-without-a-call-per-request) 如何使几页以上的重建保持一致.

```ts
CanopyModule.forRoot({
  apiKey: process.env.CANOPY_API_KEY,
  verify: { environmentId: process.env.CANOPY_ENVIRONMENT_ID },
  authorizationTtlMs: 30_000,
  resolveIdentity: (request) => request.user?.sub,
});
```

缩短该窗口使撤销生效更快,并花费更多的电话。 在用户请求之间的空隙下,它完全停止保存任何东西——每个请求都发现缓存已过期并反射出,这是存在以去除的每个请求流量. 人行速度的交通有多秒的缺口,因此几秒就能无益地花费出全价.

### 证书需要什么

A 类 `full_access` API 密钥不需要更多了. A 类 **范围** 键必须同时带有 的许可,因为授予了范围 密钥 精确的许可密钥 :

-   **`identity.view`** ——读出一个身份的赋能根.
-   **`hierarchy.view`** ——读取行走相向的等级,取为亲缘关系(`GET /api/v1/nodes/parents`,每页)而不是仪表板的节点列表.

错过了第一个和每个警戒路线的答案 `500` 命名失败的读取. 错过了第二,树又回来了 **空有一个 `200`** - API是: - API是: 回答"这里有你可能看到的一切", 这没什么。 警卫会的 不将此视为没有赠款:不完整的等级制使得 节点范围检查无法确定,所以它提出而不是否认. 安静 否认会摧毁每个节点的路线 而每个反应仍然 看起来很健康.

`app_wide` 只需检查 `identity.view`他们从不咨询等级.

### 被取消的用户还能做什么

直至窗口,现在应被拒绝的请求是允许的。 这是不每次询问的代价,值得向拥有访问审查程序的人说明,而不是让它被发现.

窗盖 **每个** 改变可以改变一个答案,包括改变一个已移动的节点的改变——重新确定继承的赠款的金额,尽管赠款本身没有改变,这就是为什么等级制度在同一节点上重新确认,而不是一个较慢的.

两条出路,取决于你需要多少.

对a **单人** 高值操作, 请直接通过注入的客户端询问 API —— 您的其余路径保留缓存 :

```ts
const { allowed } = await this.canopy.permissions.evaluate({
  identity_id: identityId,
  permission: "payments.release",
  scope: "node",
  node_id: nodeId,
});
```

对于无法容忍 stale 允许的应用程序 **任何地方**,将窗口设置为零。 每一个被监视的请求然后读取新鲜的,它恢复了每个请求的回程——这种设计存在去除的成本,所以在知情的情况下选择:

```ts
CanopyModule.forRoot({
  apiKey: process.env.CANOPY_API_KEY,
  verify: { environmentId: process.env.CANOPY_ENVIRONMENT_ID },
  authorizationTtlMs: 0,
  resolveIdentity: (request) => request.user?.sub,
});
```

### 触发缓存出错

击球没有代价。 在请求路径上读取 Canopy 的误差, 并持有一个输入请求打开, 因此它被限定: 每一次尝试的截止日期为 5 秒, 而不是整个客户端的 30 秒, 以及 1 个重试 而不是 2 个, 每一次尝试之间的等待 在一个最后期限内被限制 `Retry-After` 头部无法通过请求 。 两者都与 `evaluateTimeoutMs` 财务报告和审定财务报表 `evaluateMaxRetries`,而且两者都单独适用于这些读取——通过被注入的客户端保存全客户端值的其他一切.

**召师时起.** 如果客户端在错误被填充时断开, 守护者会结束检查而不是完成一个没人会读取的决定 。 它在快递(Nest返回节点的响应)和快递(真实的响应是在快递)上观看到的回复 `reply.raw`) (中文(简体) ).

阅读本身是故意的 **没有** 已取消 : 它可能与其他仍在等待它的请求共享, 它会使缓存变暖 .

## 更改日志

[变动情况](https://npmcn.com/package/%40canopy-io/nestjs/CHANGELOG.md) 记录每个出版的版本。 软件包是 前 1.0 : 当表面仍在沉积时, 小起落可能带有断裂 更改后,请在选择前先阅读条目.

## 许可证

MIT Canopy身份认证公司.

## 常见问题

### @canopy-io/nestjs 是什么？

Canopy的官方NestJS整合——等级身份和访问管理\.

### 如何安装 @canopy-io/nestjs？

在项目目录运行 npm install @canopy-io/nestjs，或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖：@nestjs/core（^10\.0\.0 \|\| ^11\.0\.0）、@nestjs/common（^10\.0\.0 \|\| ^11\.0\.0）、reflect-metadata（^0\.1\.13 \|\| ^0\.2\.0）。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖，Yarn 需要手动安装。

### @canopy-io/nestjs 支持 TypeScript 吗？

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

### @canopy-io/nestjs 支持 ESM 还是 CommonJS？

同时提供 ES Module 与 CommonJS 入口，import 和 require\(\) 都可以使用，打包工具会自动选择合适的版本。

### @canopy-io/nestjs 需要什么版本的 Node\.js？

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

### @canopy-io/nestjs 可以商用吗？

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

### @canopy-io/nestjs 的下载量有多少？

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

### @canopy-io/nestjs 还在维护吗？

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

