{"_id":"@canopy-io/nestjs","_rev":"5-8d27f9bab9bc9d31a81d5ebacdf37934","name":"@canopy-io/nestjs","dist-tags":{"latest":"0.2.1"},"versions":{"0.1.0":{"name":"@canopy-io/nestjs","version":"0.1.0","keywords":["canopy","nestjs","authorization","rbac","iam","permissions","guard"],"author":{"name":"Canopy Identity Inc."},"license":"MIT","_id":"@canopy-io/nestjs@0.1.0","maintainers":[{"name":"aaron-balthaser","email":"aaron.balthaser@canopy-io.com"}],"homepage":"https://github.com/canopy-identity/canopy-javascript#readme","bugs":{"url":"https://github.com/canopy-identity/canopy-javascript/issues"},"dist":{"shasum":"cd1b4bf8b445d798f9a966f693373370a37bf6a4","tarball":"https://registry.npmjs.org/@canopy-io/nestjs/-/nestjs-0.1.0.tgz","fileCount":9,"integrity":"sha512-cjuWPVEONDwYqh5FtHPFCn2AVdv25QmHNdkADT3C0RzsLJncsJObBP8CKbMJTlkcNfwp2TppqenBGgehpcDW5A==","signatures":[{"sig":"MEUCIE1BwJldvVhCn2zXzUd3kkP1alIeQCcWSG4oSMdTqwE9AiEAgw2EcQ0pJvpl9BsMGYuCGi3lcBZ8Jmgtu7ye/D0Ee7Q=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":58878},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./package.json":"./package.json"},"gitHead":"26dd5a67f4815bf777a1dc44b1b93d730c34be86","scripts":{"test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","test:watch":"vitest"},"_npmUser":{"name":"aaron-balthaser","email":"aaron.balthaser@canopy-io.com"},"repository":{"url":"git+https://github.com/canopy-identity/canopy-javascript.git","type":"git","directory":"packages/nestjs"},"_npmVersion":"10.8.2","description":"Official NestJS integration for Canopy — hierarchical identity and access management.","directories":{},"sideEffects":false,"_nodeVersion":"20.19.0","dependencies":{"@canopy-io/node":"^0.1.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"rxjs":"^7.8.1","@nestjs/core":"^11.1.21","@nestjs/common":"^11.1.21","@nestjs/testing":"^11.1.21","reflect-metadata":"^0.2.2","@nestjs/platform-express":"^11.1.28"},"peerDependencies":{"@nestjs/core":"^10.0.0 || ^11.0.0","@nestjs/common":"^10.0.0 || ^11.0.0","reflect-metadata":"^0.1.13 || ^0.2.0"},"_npmOperationalInternal":{"tmp":"tmp/nestjs_0.1.0_1785233079860_0.7771465356575749","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@canopy-io/nestjs","version":"0.1.1","keywords":["canopy","nestjs","authorization","rbac","iam","permissions","guard"],"author":{"name":"Canopy Identity Inc."},"license":"MIT","_id":"@canopy-io/nestjs@0.1.1","maintainers":[{"name":"aaron-balthaser","email":"aaron.balthaser@canopy-io.com"}],"homepage":"https://github.com/canopy-identity/canopy-javascript#readme","bugs":{"url":"https://github.com/canopy-identity/canopy-javascript/issues"},"dist":{"shasum":"27b77f56a68bd3f896e2c046169b37416849db9e","tarball":"https://registry.npmjs.org/@canopy-io/nestjs/-/nestjs-0.1.1.tgz","fileCount":9,"integrity":"sha512-/rJH6CyzUmK+Vy1e+XO1h5/ujih7rt98q2f/5UwVDfGLh86kLe3srL3BGHMhRUJzx/gjDuZrA0csl5uSrMiwVA==","signatures":[{"sig":"MEQCIA0H3SYCPIZYxluchJOLmQqwEwGHIQF2GOQ3v1XhuKLhAiASzAgpKE5xe0gk0u8BepgbbgNj9FrqQe5TIb57WNVw1g==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@canopy-io%2fnestjs@0.1.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":132235},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./package.json":"./package.json"},"gitHead":"1c584dcf66bef1cd2d0def86f62aa16711053427","scripts":{"test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","test:watch":"vitest"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:67bfd2ec-3cf5-4ad1-87e1-4965d8735579"}},"repository":{"url":"git+https://github.com/canopy-identity/canopy-javascript.git","type":"git","directory":"packages/nestjs"},"_npmVersion":"11.16.0","description":"Official NestJS integration for Canopy — hierarchical identity and access management.","directories":{},"sideEffects":false,"_nodeVersion":"24.18.0","dependencies":{"@canopy-io/node":"^0.1.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"rxjs":"^7.8.1","@nestjs/core":"^11.1.21","@nestjs/common":"^11.1.21","@nestjs/testing":"^11.1.21","reflect-metadata":"^0.2.2","@nestjs/platform-express":"^11.1.28","@nestjs/platform-fastify":"^11.1.28"},"peerDependencies":{"@nestjs/core":"^10.0.0 || ^11.0.0","@nestjs/common":"^10.0.0 || ^11.0.0","reflect-metadata":"^0.1.13 || ^0.2.0"},"_npmOperationalInternal":{"tmp":"tmp/nestjs_0.1.1_1786314079918_0.3780094854094449","host":"s3://npm-registry-packages-npm-production"}},"0.1.2":{"name":"@canopy-io/nestjs","version":"0.1.2","keywords":["canopy","nestjs","authorization","rbac","iam","permissions","guard"],"author":{"name":"Canopy Identity Inc."},"license":"MIT","_id":"@canopy-io/nestjs@0.1.2","maintainers":[{"name":"aaron-balthaser","email":"aaron.balthaser@canopy-io.com"}],"homepage":"https://github.com/canopy-identity/canopy-javascript#readme","bugs":{"url":"https://github.com/canopy-identity/canopy-javascript/issues"},"dist":{"shasum":"ecc2343f0c0782d5a0d7aa91b482c14de681256d","tarball":"https://registry.npmjs.org/@canopy-io/nestjs/-/nestjs-0.1.2.tgz","fileCount":9,"integrity":"sha512-19joAlMllQ9BL3125iRWHKKzu47D8H/okTaZ448j33fkW7MNSQrv3FnJt/UQoogV0pgYko1Wiqw20Rg7IKWEOQ==","signatures":[{"sig":"MEUCICMoXNhMU5YArnm+1JPiyBsVAWbIthvwQEA9DM1G9o1PAiEAorBY8SVFGxVWj24cSP6soBYO56oa0Ri8DDpOrzmrYjY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@canopy-io%2fnestjs@0.1.2","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":132334},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"gitHead":"ab4a62b70d6506f9add932544713c902ad74f855","scripts":{"test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","test:watch":"vitest"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:67bfd2ec-3cf5-4ad1-87e1-4965d8735579"}},"repository":{"url":"git+https://github.com/canopy-identity/canopy-javascript.git","type":"git","directory":"packages/nestjs"},"_npmVersion":"11.17.0","description":"Official NestJS integration for Canopy — hierarchical identity and access management.","directories":{},"sideEffects":false,"_nodeVersion":"24.19.0","dependencies":{"@canopy-io/node":"^0.2.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"rxjs":"^7.8.1","@nestjs/core":"^11.1.21","@nestjs/common":"^11.1.21","@nestjs/testing":"^11.1.21","reflect-metadata":"^0.2.2","@nestjs/platform-express":"^11.1.28","@nestjs/platform-fastify":"^11.1.28"},"peerDependencies":{"@nestjs/core":"^10.0.0 || ^11.0.0","@nestjs/common":"^10.0.0 || ^11.0.0","reflect-metadata":"^0.1.13 || ^0.2.0"},"_npmOperationalInternal":{"tmp":"tmp/nestjs_0.1.2_1788302454689_0.6041186483170922","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@canopy-io/nestjs","version":"0.2.0","keywords":["canopy","nestjs","authorization","rbac","iam","permissions","guard"],"author":{"name":"Canopy Identity Inc."},"license":"MIT","_id":"@canopy-io/nestjs@0.2.0","maintainers":[{"name":"aaron-balthaser","email":"aaron.balthaser@canopy-io.com"}],"homepage":"https://github.com/canopy-identity/canopy-javascript#readme","bugs":{"url":"https://github.com/canopy-identity/canopy-javascript/issues"},"dist":{"shasum":"c98ee592a4cf15c3f79dffabb30c56cc319e3c1a","tarball":"https://registry.npmjs.org/@canopy-io/nestjs/-/nestjs-0.2.0.tgz","fileCount":9,"integrity":"sha512-ea6pZGoXas4CDMTiCMks00zECoSuw8XNCAkyuY8IdsZEi5MMsdI9vmWzfNiJzr3RzrfGElnqxQUOw5CAbN+a1Q==","signatures":[{"sig":"MEQCIFryObtOwid0K1j3COlnjoU5seg3XKzp26747yTQAqKMAiBIOGx6uUIkXgT78td8INoPvXat1a7nxL6oXNtsHLDw9A==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@canopy-io%2fnestjs@0.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":144223},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"gitHead":"171b67e7e3f1a9c644ed7ea3f58e6e19713e714b","scripts":{"test":"vitest run","build":"tsup","typecheck":"tsc --noEmit","test:watch":"vitest"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:67bfd2ec-3cf5-4ad1-87e1-4965d8735579"}},"repository":{"url":"git+https://github.com/canopy-identity/canopy-javascript.git","type":"git","directory":"packages/nestjs"},"_npmVersion":"11.19.0","description":"Official NestJS integration for Canopy — hierarchical identity and access management.","directories":{},"sideEffects":false,"_nodeVersion":"24.20.0","dependencies":{"@canopy-io/node":"^0.3.0"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"rxjs":"^7.8.1","@nestjs/core":"^11.1.21","@nestjs/common":"^11.1.21","@nestjs/testing":"^11.1.21","reflect-metadata":"^0.2.2","@nestjs/platform-express":"^11.1.28","@nestjs/platform-fastify":"^11.1.28"},"peerDependencies":{"@nestjs/core":"^10.0.0 || ^11.0.0","@nestjs/common":"^10.0.0 || ^11.0.0","reflect-metadata":"^0.1.13 || ^0.2.0"},"_npmOperationalInternal":{"tmp":"tmp/nestjs_0.2.0_1788605877391_0.29996823980762755","host":"s3://npm-registry-packages-npm-production"}},"0.2.1":{"name":"@canopy-io/nestjs","version":"0.2.1","description":"Official NestJS integration for Canopy — hierarchical identity and access management.","license":"MIT","author":{"name":"Canopy Identity Inc."},"homepage":"https://github.com/canopy-identity/canopy-javascript#readme","bugs":{"url":"https://github.com/canopy-identity/canopy-javascript/issues"},"repository":{"type":"git","url":"git+https://github.com/canopy-identity/canopy-javascript.git","directory":"packages/nestjs"},"keywords":["canopy","nestjs","authorization","rbac","hierarchical-rbac","access-control","iam","permissions","multi-tenant","b2b-saas","sso","guard"],"type":"module","sideEffects":false,"main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"import":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"engines":{"node":">=18"},"publishConfig":{"access":"public"},"scripts":{"build":"tsup","typecheck":"tsc --noEmit","test":"vitest run","test:watch":"vitest"},"dependencies":{"@canopy-io/node":"^0.3.3"},"peerDependencies":{"@nestjs/common":"^10.0.0 || ^11.0.0","@nestjs/core":"^10.0.0 || ^11.0.0","reflect-metadata":"^0.1.13 || ^0.2.0"},"devDependencies":{"@nestjs/common":"^11.1.21","@nestjs/core":"^11.1.21","@nestjs/platform-express":"^11.1.28","@nestjs/platform-fastify":"^11.1.28","@nestjs/testing":"^11.1.21","reflect-metadata":"^0.2.2","rxjs":"^7.8.1"},"gitHead":"00148e10f8edcc92f105ea2b7dcca2cda0c614f6","_id":"@canopy-io/nestjs@0.2.1","_nodeVersion":"24.20.0","_npmVersion":"11.19.0","dist":{"integrity":"sha512-F5+qcMu9YMw8DUl3SWGFgk/Gzu0o9nUojEhhb5O1RPTgcNu4I8kSBpSKb4xxHNqnx8zRX60p/pdZDfrPFACrwg==","shasum":"23e69ae6541e2db80c0ae7277e75d64b1cb9cc30","tarball":"https://registry.npmjs.org/@canopy-io/nestjs/-/nestjs-0.2.1.tgz","fileCount":10,"unpackedSize":156394,"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@canopy-io%2fnestjs@0.2.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCMX0noltcoYPGfo4Qir2y7jzgSQq/f3UiiWriiQfVM5wIhAK5Me0UxU5z+ew6jQnTs/iFQEyn/34Embc+imReXBRGo"}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:67bfd2ec-3cf5-4ad1-87e1-4965d8735579"}},"directories":{},"maintainers":[{"name":"aaron-balthaser","email":"aaron.balthaser@canopy-io.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/nestjs_0.2.1_1789398709354_0.47637457619400214"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-28T10:04:39.704Z","modified":"2026-09-14T15:11:49.817Z","0.1.0":"2026-07-28T10:04:40.005Z","0.1.1":"2026-08-09T22:21:20.057Z","0.1.2":"2026-09-01T22:40:54.822Z","0.2.0":"2026-09-05T10:57:57.517Z","0.2.1":"2026-09-14T15:11:49.476Z"},"bugs":{"url":"https://github.com/canopy-identity/canopy-javascript/issues"},"author":{"name":"Canopy Identity Inc."},"license":"MIT","homepage":"https://github.com/canopy-identity/canopy-javascript#readme","keywords":["canopy","nestjs","authorization","rbac","hierarchical-rbac","access-control","iam","permissions","multi-tenant","b2b-saas","sso","guard"],"repository":{"type":"git","url":"git+https://github.com/canopy-identity/canopy-javascript.git","directory":"packages/nestjs"},"description":"Official NestJS integration for Canopy — hierarchical identity and access management.","maintainers":[{"name":"aaron-balthaser","email":"aaron.balthaser@canopy-io.com"}],"readme":"# @canopy-io/nestjs\n\nOfficial NestJS integration for [Canopy](https://canopy-io.com) — hierarchical identity and access management for B2B SaaS.\n\nTurns a permission check into a decorator on the route instead of a call in every handler.\n\n## Install\n\n```bash\nnpm install @canopy-io/nestjs\n```\n\nRequires NestJS 10 or 11 and Node 18 or later. `@canopy-io/node` comes with it.\n\n## Features\n\n- **One registration.** `CanopyModule.forRoot` for a static key, `forRootAsync` when it comes from something injectable. Pass your own request type and the resolvers are typed against it.\n- **`@RequirePermission` on the route.** The check is a declaration on the handler rather than a call inside it. A route without it passes through untouched, so the guard can be registered globally and opted into.\n- **`CanopyGuard`** answers that declaration **in your own process**, at the node the request names — inheriting a role granted on a parent without you modelling the walk.\n- **No network call per request.** The guard holds the identity's grant roots and one shared copy of your hierarchy, refreshed at most once a minute, so a guarded route normally reaches no network at all.\n- **Three scopes.** `node` is the default and the strict question; `app_wide` asks only whether the identity holds the permission anywhere in the Environment; `org` asks the node question at the organization the caller's token is acting in.\n- **`CanopyTokenGuard`** verifies a Canopy-issued bearer token locally against the published signing keys and attaches the claims, for applications with no auth layer in front. Additive — skip it if you already run Passport or your own JWT middleware.\n- **`@InjectCanopy()`** hands you the full `@canopy-io/node` client for everything the guard does not cover.\n- **Fails closed, with the status telling you which failure it was.** `403` for a denial, `503` for an undecidable one, `500` for a broken integration.\n- **A bounded check.** Its own evaluation timeout and retry count, separate from the client-wide ones, with the wait between attempts capped.\n- **Aborts when the caller hangs up**, on both Express and Fastify, rather than finishing a decision no one will read.\n\n## Register\n\n```ts\nimport { CanopyModule } from \"@canopy-io/nestjs\";\n\n@Module({\n  imports: [\n    CanopyModule.forRoot<AuthedRequest>({\n      apiKey: process.env.CANOPY_API_KEY,\n      resolveIdentity: (request) => request.user?.sub,\n      resolveNode: (request) => request.params?.orgId,\n    }),\n  ],\n})\nexport class AppModule {}\n```\n\nThe two resolvers are how the guard learns _who_ is asking and _where_. Where those live is your application's business — a claim on a verified token, a route parameter, a tenant on the session — so you supply them rather than the library guessing. Pass your own request type to `forRoot` and both resolvers are typed against it.\n\nWhen the key comes from something injectable, use `forRootAsync`:\n\n```ts\nCanopyModule.forRootAsync({\n  imports: [ConfigModule],\n  inject: [ConfigService],\n  useFactory: (config: ConfigService) => ({\n    apiKey: config.getOrThrow(\"CANOPY_API_KEY\"),\n    resolveIdentity: (request) => request.user?.sub,\n    resolveNode: (request) => request.params?.orgId,\n  }),\n});\n```\n\n## Guard a route\n\n```ts\nimport { CanopyGuard, RequirePermission } from \"@canopy-io/nestjs\";\n\n@Controller(\"orgs/:orgId/orders\")\n@UseGuards(CanopyGuard)\nexport class OrdersController {\n  @Post(\":orderId/refund\")\n  @RequirePermission(\"orders.refund\")\n  refund() {}\n}\n```\n\nThat answers whether the identity holds `orders.refund` **at** `orgId`, walking the node's lineage — so a role granted on a parent is inherited without you modelling it.\n\nThe walk happens in your process, not over the network. See [How the decision is reached](#how-the-decision-is-reached).\n\nA route with no `@RequirePermission` passes through untouched, so the guard can be registered globally and opted into per route. A requirement on a handler wins over one on its controller.\n\n### If you register it globally\n\nNest runs global guards **before** controller- and route-scoped ones. So a globally-registered `CanopyGuard` runs before a route-level `AuthGuard` has put anything on the request, and `resolveIdentity` finds nothing — every guarded route answers `403`.\n\nRegister your authentication guard globally too, ahead of this one:\n\n```ts\nproviders: [\n  { provide: APP_GUARD, useClass: JwtAuthGuard }, // must come first\n  { provide: APP_GUARD, useClass: CanopyGuard },\n];\n```\n\nGlobal guards run in registration order. If your authentication is route-level, apply `CanopyGuard` at the route as well — `@UseGuards(JwtAuthGuard, CanopyGuard)` — where left-to-right order holds.\n\n### Scope\n\nThe default is `node`, which is the strict question: does this identity hold the permission _at this node_.\n\n```ts\n@RequirePermission(\"reports.view\", { scope: \"app_wide\" })\n```\n\n`app_wide` asks only whether the identity holds the permission anywhere in the Environment, and returns no effective node. It is right for deciding whether to show a menu item and wrong for guarding a resource that belongs to a node — which is why it is opt-in rather than the default.\n\n```ts\n@RequirePermission(\"invoices.view\", { scope: \"org\" })\n```\n\n`org` is for Environments with the **organizations** container on, where a\ntoken carries the organization the session is acting in (`org_id`) and the one\nrole held there (`org_role`). The guard reads `org_id` off the verified claims\n`CanopyTokenGuard` attached and evaluates at that node — an organization _is_ a\nhierarchy node, and a membership is a role assignment at it, so no `resolveNode`\nand no node in the route's path are needed. A caller acting in no organization\nis denied: an org-scoped route has no meaning outside one.\n\nIf your own auth layer verifies tokens and parks the claims somewhere other\nthan `attachTokenAs`, configure `resolveOrg` — and feed it only a value read\noff a _verified_ token, because it decides which organization's grants answer\nthe check.\n\n## If Canopy issues your tokens\n\n`resolveIdentity` assumes something upstream already verified the caller. If\nyou have no auth layer yet, `CanopyTokenGuard` is that layer: it verifies the\nbearer token against Canopy's published signing keys and attaches the claims,\nso `resolveIdentity` has something trustworthy to read.\n\n```ts\nCanopyModule.forRoot<AuthedRequest>({\n  apiKey: process.env.CANOPY_API_KEY,\n  verify: { audience: process.env.CANOPY_OAUTH_CLIENT_ID }, // omit for Direct API\n  resolveIdentity: (request) => request.canopyToken?.sub,\n});\n```\n\n```ts\n@UseGuards(CanopyTokenGuard, CanopyGuard)\n@RequirePermission(\"documents.read\")\nfindAll() {}\n```\n\n**Order matters.** Nest runs guards left to right, and `CanopyGuard` reads what\n`CanopyTokenGuard` attaches. Reversed, every request is denied — the identity\nis resolved before anything has established one.\n\nVerification is local: one fetch of the key set, then a signature check per\nrequest with no network at all.\n\nA rejected token is a 401 whatever the detail, with the specific code left on\n`error.cause` for your logs rather than handed to the caller — who has no use\nfor the difference and should not be told which check failed. The exception is\n`token.jwks_unavailable`, which answers **503**: not being able to verify is not\nthe same as failing to verify, the caller's token may be perfectly good, and\ntelling a client its token is stale during a key-server outage only aims a\nrefresh storm at the thing that is already down.\n\nClaims land on `request.canopyToken`, not `request.user` — that one belongs to\nPassport, and quietly overwriting it would be somebody's difficult afternoon.\nUse `attachTokenAs` to put them elsewhere.\n\nSkip this guard entirely if you already run Passport or your own JWT\nmiddleware. It is additive; nothing else in the package depends on it.\n\n## The client\n\nFor anything the guard does not cover, inject the client:\n\n```ts\nimport { InjectCanopy, type Canopy } from \"@canopy-io/nestjs\";\n\n@Injectable()\nexport class OrdersService {\n  constructor(@InjectCanopy() private readonly canopy: Canopy) {}\n\n  async assign(identityId: string, nodeId: string, roleId: string) {\n    await this.canopy.assignments.create({\n      identity_id: identityId,\n      node_id: nodeId,\n      role_id: roleId,\n    });\n  }\n}\n```\n\nIt is the same [`@canopy-io/node`](../node) client, with its retry policy, pagination and typed errors.\n\n## Failing closed\n\nThere is no path through `CanopyGuard` that allows a request whose decision is unknown. Each of these ends the request:\n\n- no identity resolves, including when a resolver throws on an unauthenticated request — `403`\n- a `node` check with no node, or with no `resolveNode` configured — `403`\n- the identity holds the permission nowhere, or nowhere on this node's lineage — `403`\n- Canopy has never heard of the identity (`404`) — `403`\n- a cache miss cannot be filled because Canopy is unreachable, rate-limiting, or answers `5xx` — `503`, deliberately not `403`, because \"we could not decide\" is not \"you are not allowed\"\n- Canopy rejects the API key (`401`/`403`), or refuses the read (other `4xx`) — `500`, because a misconfiguration is not a temporary condition and no retry will fix it\n- the hierarchy this credential can read does not contain the node — `500`, naming the cause. Undecidable, not denied: answering `403` would take out every node-scoped route while looking like ordinary policy\n- the caller hung up mid-check — the abort is propagated, not turned into a `503`, because nothing failed and no one is waiting for an answer\n\nA held answer is never served past its window to paper over an outage. Once it expires, an unfillable read is a `503` — extending it silently would extend the revocation window with it, without anyone choosing to.\n\nEverything except the caller hanging up is logged with its cause before the request is refused.\n\nThe `500` cases are worth alerting on: they mean the integration is broken rather than the service being slow. Reporting them as `503` would bury a bad API key under what looks like an outage.\n\n## How the decision is reached\n\nThe guard does not ask Canopy per request. It asks a different question, once, and answers every later check from the result.\n\nRather than \"may this identity act **here**\", it reads \"**where** may this identity act\" — the nodes each permission was granted at. Those grant roots are not expanded through their descendants, because a grant already means _this node and everything beneath it_; expanding would restate your hierarchy once per identity, and would be largest for the near-root grants your administrators hold.\n\nSo two things are held, split by what they depend on:\n\n- **grant roots, per identity** — small, a handful of assignments\n- **your hierarchy, once per process** — shared by every identity, since its shape does not depend on who is asking\n\nand a check is a walk up from the node in question looking for a grant root. An `app_wide` check is simpler still: the permission appearing at all is the answer, and no hierarchy is needed.\n\n**Both are held for at most 60 seconds**, which is therefore the delay between an access change and it taking effect. The hierarchy is revalidated rather than re-read — a conditional request that normally answers `304`, so nothing transfers.\n\n```ts\nCanopyModule.forRoot({\n  apiKey: process.env.CANOPY_API_KEY,\n  authorizationTtlMs: 30_000,\n  resolveIdentity: (request) => request.user?.sub,\n});\n```\n\nShortening that window makes revocation take effect sooner and costs more calls. Below the gap between a user's requests it stops saving anything at all — every request finds the cache expired and refetches, which is the per-request traffic this exists to remove. Human-paced traffic has multi-second gaps, so a few seconds can cost full price for no benefit.\n\n### What the credential needs\n\nA `full_access` API key needs nothing further. A **scoped** key must carry both\nof the permissions the guard reads with, because a scoped key is granted\nexactly the permission keys listed on it:\n\n- **`identity.view`** — to read an identity's grant roots.\n- **`hierarchy.view`** — to read the hierarchy the walk runs against, taken as parent edges (`GET /api/v1/nodes/parents`) rather than the dashboard's tree.\n\nMiss the first and every guarded route answers `500` naming the failed read.\nMiss the second and the tree comes back **empty with a `200`** — the API is\nanswering \"here is everything you may see\", which is nothing. The guard does\nnot treat that as an absence of grants: an incomplete hierarchy makes a\nnode-scoped check undecidable, so it raises rather than denying. Silently\ndenying there would take out every node-scoped route while every response still\nlooked healthy.\n\n`app_wide` checks need only `identity.view`; they never consult the hierarchy.\n\n### What a revoked user can still do\n\nFor up to the window, a request that should now be denied is allowed. That is the cost of not asking every time, and it is worth stating to whoever owns your access-review process rather than leaving it to be discovered.\n\nThe window covers **every** change that can alter an answer, including a moved node — reparenting changes what an inherited grant reaches even though no grant itself changed, which is why the hierarchy is revalidated on the same cadence and not a slower one.\n\nTwo ways out, depending on how much you need.\n\nFor a **single** high-value operation, ask the API directly through the injected client — the rest of your routes keep the cache:\n\n```ts\nconst { allowed } = await this.canopy.permissions.evaluate({\n  identity_id: identityId,\n  permission: \"payments.release\",\n  scope: \"node\",\n  node_id: nodeId,\n});\n```\n\nFor an application that cannot tolerate a stale allow **anywhere**, set the window to zero. Every guarded request then reads fresh, which reinstates a round trip per request — the cost this design exists to remove, so choose it knowingly:\n\n```ts\nCanopyModule.forRoot({\n  apiKey: process.env.CANOPY_API_KEY,\n  authorizationTtlMs: 0,\n  resolveIdentity: (request) => request.user?.sub,\n});\n```\n\n### Bounding a cache miss\n\nA hit costs nothing. A miss reads from Canopy on the request path and holds an inbound request open, so it is bounded: a 5s per-attempt deadline rather than the client-wide 30s, and 1 retry rather than 2, with each wait between attempts capped at one deadline so a `Retry-After` header cannot hold the request past it. Both are settable with `evaluateTimeoutMs` and `evaluateMaxRetries`, and both apply to these reads alone — everything else through the injected client keeps the client-wide values.\n\n**Hanging up when the caller does.** If the client disconnects while a miss is being filled, the guard ends that check rather than finishing a decision no one will read. It watches the response for a close that arrives before anything was written, on Express (where Nest returns the Node response) and on Fastify (where the real response is on `reply.raw`).\n\nThe read itself is deliberately **not** cancelled: it may be shared with other requests that are still waiting on it, and it warms the cache either way.\n\n## Changelog\n\n[CHANGELOG.md](./CHANGELOG.md) records every published version. The package is\npre-1.0: while the surface is still settling, a minor bump may carry a breaking\nchange, so read the entry before taking one.\n\n## License\n\nMIT © Canopy Identity Inc.\n","readmeFilename":"README.md"}