@canopy-io/nestjs

中文文档
0.9.1发布于 2026/11/07MITTypeScriptESM + CJS构建来源已验证近 30 天下载 2956

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

中文使用文档
文档目录15 节
  1. 安装
  2. 特征
  3. 登记册
  4. 守卫一条路
  5. 如果你在全球注册的话
  6. 范围
  7. 如果Canopy给你的代币
  8. 客户
  9. 未关闭
  10. 如何作出决定
  11. 证书需要什么
  12. 被取消的用户还能做什么
  13. 触发缓存出错
  14. 更改日志
  15. 许可证

@canopy-io/nestjs (英语)

正式NestJS整合 树冠 ——B2B SaaS的分级身份和准入管理.

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

安装

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 为了打破融合.
  • 一张有限度的支票. 它自己的评价超时和重试计数,独立于全客户端的计数,在尝试间等待被封.
  • 呼叫者挂起时中止在快递和快递上, 而不是完成一个决定 没有人会读.

登记册

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

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,
  }),
});

守卫一条路

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,走节点的分界线——所以一个赋予给父母的角色是继承的,没有你建模它.

步行发生在你的过程中,而不是在网络上。 见 如何作出决定。 。 。 .

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

如果你在全球注册的话

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

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

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

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

范围

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

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

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

@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 有值得信赖的东西读.

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,
});
@UseGuards(CanopyTokenGuard, CanopyGuard)
@RequirePermission("documents.read")
findAll() {}

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

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

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

@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 回答:

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,
});
@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,并允许 只有在双方持有许可时:

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

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

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

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

客户

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

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 客户端,带有重试策略、页码和打出错误.

未关闭

没有路经过 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 如何使几页以上的重建保持一致.

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 —— 您的其余路径保留缓存 :

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

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

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) (中文(简体) ).

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

更改日志

变动情况 记录每个出版的版本。 软件包是 前 1.0 : 当表面仍在沉积时, 小起落可能带有断裂 更改后,请在选择前先阅读条目.

许可证

MIT Canopy身份认证公司.

版本与发布记录

@canopy-io/nestjs 共发布 27 个版本,首次发布于 2026/07/28;过去 12 个月发布了 27 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.9.1latest2026/11/072 天前
0.9.02026/11/063 天前
0.8.12026/11/053 天前
0.8.02026/11/054 天前
0.7.62026/11/045 天前
0.7.52026/11/017 天前

关于 @canopy-io/nestjs 的常见问题

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