@canopy-io/nestjs 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # @canopy-io/nestjs
2
2
 
3
+ ## 0.4.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 436d639: Follows Canopy's paged hierarchy, grant, session and directory group reads. These endpoints now answer `{ items, pagination }`, at most 100 to a page, where they used to answer the whole collection.
8
+
9
+ `LocalAuthorizer`, and so `CanopyGuard`, reads every page of an identity's grant roots (now one row per permission and node) and every page of the hierarchy's parent edges, 100 at a time. Revalidation still costs one request: only the hierarchy's first page is sent with its `ETag`, and a `304` keeps the held copy. When the hierarchy has changed, every page is read again, and a rebuild over several pages is confirmed against the first page's `ETag` so a change made mid-read is not kept half-applied. `snapshot()` gains `grantPages` and `treePages`, the page requests behind `grantFetches` and `treeRequests`. Earlier versions deny every check against a Canopy that answers grants as one row per node, so upgrade before that API reaches you.
10
+
11
+ Caller shapes that changed:
12
+
13
+ - `identities.sessions(id, query?)` returns a `Paginator` instead of a promise of `{ items }`, and takes `page` and `take`. Use `for await`, `.all()` or `.first()`.
14
+ - `organizations.listDirectoryGroups(id, query?)` returns a `Paginator`, and takes `page`, `take`, `q` (display name or external id) and `sort_by` (`name`, the default, or `created`). The item type is exported as `OrganizationDirectoryGroupItem`.
15
+ - `permissions.evaluateBulk` takes between 1 and 100 checks; more is refused with a `400`.
16
+ - The generated types follow the API: `GET /api/v1/nodes` and `GET /api/v1/nodes/:id/tree` answer pages of flat nodes (`ScopedNodeResponseDto`, linked by `parent_node_id`) instead of a nested tree with its scope, `GET /api/v1/nodes/:id/children` is paged, and `IdentityGrantResponseDto` carries `node_id` instead of `nodes`. Several lists' `sort_by` is now typed to the columns each accepts.
17
+
18
+ `@canopy-io/nestjs` picks up the new authorizer; its own options and behaviour are unchanged.
19
+ - Updated dependencies [436d639]
20
+ - @canopy-io/node@0.9.0
21
+
22
+ ## 0.4.0
23
+
24
+ ### Minor Changes
25
+
26
+ - 621b9a6: `TokenVerifier` accepts identity tokens only by default. Tokens Canopy signs for its own administrators (`user`), staff (`platform`) and API key flows (`api_key`) are refused with `token.type_not_allowed` unless named in the new `allowedTypes` option; `CanopyTokenGuard` takes it under `verify.allowedTypes`.
27
+
28
+ When Canopy cannot be reached as cached signing keys come due for a refresh, the verifier keeps verifying with the keys it holds instead of failing every request until the issuer is back. A token signed by a key the verifier has never seen, during an outage, is reported as `token.jwks_unavailable` (503) rather than `token.key_not_found` (401).
29
+
30
+ New exports `tokenErrorStatus` and `authorizationErrorStatus` turn a failed verification or authorization check into the HTTP status and message to answer with; the NestJS guards use them. The node README's verification example now names its Environment, and documents the 503 and the per-process reach of `LocalAuthorizer.invalidate()`.
31
+
32
+ ### Patch Changes
33
+
34
+ - Updated dependencies [621b9a6]
35
+ - @canopy-io/node@0.8.0
36
+
3
37
  ## 0.3.1
4
38
 
5
39
  ### Patch Changes
package/README.md CHANGED
@@ -153,7 +153,12 @@ findAll() {}
153
153
  is resolved before anything has established one.
154
154
 
155
155
  Verification is local: one fetch of the key set, then a signature check per
156
- request with no network at all.
156
+ request with no network at all. If Canopy cannot be reached when the keys are
157
+ due for a refresh, the guard keeps verifying with the keys it holds.
158
+
159
+ Only identity tokens are accepted, the people who sign in to your application.
160
+ A token Canopy signed for one of its own administrators or staff answers `401`
161
+ unless you add its type to `verify.allowedTypes`.
157
162
 
158
163
  A rejected token is a 401 whatever the detail, with the specific code left on
159
164
  `error.cause` for your logs rather than handed to the caller — who has no use
@@ -225,7 +230,7 @@ So two things are held, split by what they depend on:
225
230
 
226
231
  and 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.
227
232
 
228
- **Both are held for at most 60 seconds**, which is therefore the delay between an access change and it taking effect. `authorizationTtlMs` changes it, up to five minutes (`300_000`); a longer value throws at startup. The hierarchy is revalidated rather than re-read — a conditional request that normally answers `304`, so nothing transfers.
233
+ **Both are held for at most 60 seconds**, which is therefore the delay between an access change and it taking effect. `authorizationTtlMs` changes it, up to five minutes (`300_000`); a longer value throws at startup. The hierarchy is revalidated rather than re-read — a conditional request for its first page that normally answers `304`, so nothing transfers. Both reads are paged and read in full, 100 rows to a page; when the hierarchy has changed, every page of it is read again (see [`LocalAuthorizer`](../node#authorizing-without-a-call-per-request) for how a rebuild over several pages is kept consistent).
229
234
 
230
235
  ```ts
231
236
  CanopyModule.forRoot({
@@ -245,7 +250,7 @@ of the permissions the guard reads with, because a scoped key is granted
245
250
  exactly the permission keys listed on it:
246
251
 
247
252
  - **`identity.view`** — to read an identity's grant roots.
248
- - **`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.
253
+ - **`hierarchy.view`** — to read the hierarchy the walk runs against, taken as parent edges (`GET /api/v1/nodes/parents`, every page) rather than the dashboard's node list.
249
254
 
250
255
  Miss the first and every guarded route answers `500` naming the failed read.
251
256
  Miss the second and the tree comes back **empty with a `200`** — the API is
package/dist/index.cjs CHANGED
@@ -104,40 +104,19 @@ exports.CanopyGuard = class CanopyGuard {
104
104
  * Every branch denies — nothing here can let a request through — but they say
105
105
  * different things, and the difference is what makes an incident diagnosable.
106
106
  * Reporting a rejected API key as "temporarily unavailable" invites a retry
107
- * that will never succeed and hides a misconfiguration behind an outage.
107
+ * that will never succeed and hides a misconfiguration behind an outage. The
108
+ * mapping is `authorizationErrorStatus` in `@canopy-io/node`, so an
109
+ * integration outside NestJS answers the same way.
108
110
  */
109
111
  toHttpException(error) {
110
- if (node.isCanopyAuthorizerError(error)) {
111
- return new common.InternalServerErrorException(
112
- `Authorization is misconfigured: ${error.message}`
113
- );
114
- }
115
- if (!node.isCanopyError(error)) {
116
- return new common.ServiceUnavailableException(
117
- "Authorization is temporarily unavailable."
118
- );
119
- }
120
- if (error.isAuthFailure) {
121
- return new common.InternalServerErrorException(
122
- "Authorization is misconfigured. Check the Canopy API key and its scopes."
123
- );
124
- }
125
- if (error.statusCode === 404 && error.code === null) {
126
- return new common.InternalServerErrorException(
127
- `Authorization is misconfigured: ${error.request.method} ${error.request.path} is not served by this Canopy. The SDK requires an API that exposes the identity grants endpoint.`
128
- );
129
- }
130
- if (error.statusCode === 404) {
131
- return new common.ForbiddenException("Could not evaluate the permission.");
112
+ const { status, message } = node.authorizationErrorStatus(error);
113
+ if (status === 403) {
114
+ return new common.ForbiddenException(message);
132
115
  }
133
- if (error.isRateLimited || error.statusCode >= 500) {
134
- return new common.ServiceUnavailableException(
135
- "Authorization is temporarily unavailable."
136
- );
116
+ if (status === 503) {
117
+ return new common.ServiceUnavailableException(message);
137
118
  }
138
- return new common.InternalServerErrorException(
139
- "Authorization could not be evaluated."
140
- );
119
+ return new common.InternalServerErrorException(message);
141
120
  }
142
121
  buildQuery(requirement, identityId, request) {
143
122
  if (requirement.scope === "app_wide") {
@@ -245,18 +224,14 @@ exports.CanopyTokenGuard = class CanopyTokenGuard {
245
224
  try {
246
225
  claims = await this.verifier.verify(token);
247
226
  } catch (error) {
248
- if (node.isCanopyTokenError(error) && error.code === "token.jwks_unavailable") {
249
- throw new common.ServiceUnavailableException(
250
- "Authentication is temporarily unavailable.",
251
- { cause: error }
252
- );
227
+ if (!node.isCanopyTokenError(error)) {
228
+ throw error;
253
229
  }
254
- if (node.isCanopyTokenError(error)) {
255
- throw new common.UnauthorizedException("Invalid or expired access token.", {
256
- cause: error
257
- });
230
+ const { status, message } = node.tokenErrorStatus(error);
231
+ if (status === 503) {
232
+ throw new common.ServiceUnavailableException(message, { cause: error });
258
233
  }
259
- throw error;
234
+ throw new common.UnauthorizedException(message, { cause: error });
260
235
  }
261
236
  request[this.options.attachTokenAs ?? "canopyToken"] = claims;
262
237
  return true;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/tokens.ts","../src/canopy.guard.ts","../src/canopy-token.guard.ts","../src/canopy.module.ts","../src/inject-canopy.decorator.ts","../src/require-permission.decorator.ts"],"names":["CanopyGuard","Logger","ForbiddenException","isCanopyAuthorizerError","InternalServerErrorException","isCanopyError","ServiceUnavailableException","Injectable","Reflector","CanopyTokenGuard","UnauthorizedException","isCanopyTokenError","Inject","LocalAuthorizer","CanopyModule","Canopy","TokenVerifier","Module","SetMetadata"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Injection tokens.\n *\n * These are strings rather than classes because esbuild does not emit\n * `design:paramtypes`, so nothing here can be injected by constructor type\n * alone — every dependency is named with an explicit `@Inject`.\n */\n\n/** The resolved `CanopyModuleOptions`. */\nexport const CANOPY_OPTIONS = \"CANOPY_OPTIONS\";\n\n/** The configured `Canopy` client. */\nexport const CANOPY_CLIENT = \"CANOPY_CLIENT\";\n\n/**\n * The configured `LocalAuthorizer`.\n *\n * Shared deliberately: it holds one copy of the hierarchy for the whole\n * process, so a second instance would double both the memory and the traffic\n * it exists to avoid.\n */\nexport const CANOPY_AUTHORIZER = \"CANOPY_AUTHORIZER\";\n\n/** The configured `TokenVerifier`, shared so its key cache is shared too. */\nexport const CANOPY_TOKEN_VERIFIER = \"CANOPY_TOKEN_VERIFIER\";\n\n/** Metadata key holding what `@RequirePermission` recorded on a handler. */\nexport const CANOPY_PERMISSION = \"CANOPY_PERMISSION\";\n","import {\n type CanActivate,\n type ExecutionContext,\n ForbiddenException,\n Inject,\n Injectable,\n InternalServerErrorException,\n Logger,\n ServiceUnavailableException,\n} from \"@nestjs/common\";\nimport { Reflector } from \"@nestjs/core\";\n\nimport {\n isCanopyAuthorizerError,\n isCanopyError,\n type LocalAuthorizer,\n type RequestBody,\n} from \"@canopy-io/node\";\n\nimport type { CanopyModuleOptions } from \"./options.js\";\nimport type { PermissionRequirement } from \"./require-permission.decorator.js\";\nimport {\n CANOPY_AUTHORIZER,\n CANOPY_OPTIONS,\n CANOPY_PERMISSION,\n} from \"./tokens.js\";\n\n/** Read off the generated operation, so it follows the API rather than restating it. */\ntype EvaluateQuery = RequestBody<\"ApiPermissionsController_evaluate\">;\n\n/**\n * Only the verdict, not the API's full explain payload.\n *\n * A decision reached in-process cannot report `granting_roles` or an effective\n * node — that reasoning lives on the server. The guard has never needed it, and\n * typing the narrow thing keeps that honest rather than implying a richer\n * answer than exists.\n */\ntype Verdict = { allowed: boolean };\n\ninterface Cancellation {\n signal: AbortSignal;\n dispose: () => void;\n}\n\n/** The Node stream underneath an Express or Fastify response. */\ninterface ClosableResponse {\n once: (event: string, listener: () => void) => unknown;\n removeListener: (event: string, listener: () => void) => unknown;\n writableEnded?: boolean;\n raw?: ClosableResponse;\n}\n\nfunction isClosable(value: unknown): value is ClosableResponse {\n return (\n typeof value === \"object\" &&\n value !== null &&\n typeof (value as ClosableResponse).once === \"function\" &&\n typeof (value as ClosableResponse).removeListener === \"function\"\n );\n}\n\n/**\n * Find the Node response stream behind whatever the adapter hands back.\n *\n * `.raw` is checked first and on its own merits, because the object Nest\n * returns is not always the stream. Express returns the `ServerResponse`\n * itself, which listens; Fastify returns a `Reply`, which is a plain object\n * with the real response on `.raw`. Testing the outer object first and\n * returning early on failure would skip `.raw` in exactly the case it exists\n * for.\n */\nfunction resolveStream(response: unknown): ClosableResponse | undefined {\n const raw = (response as { raw?: unknown } | null | undefined)?.raw;\n\n if (isClosable(raw)) {\n return raw;\n }\n\n if (isClosable(response)) {\n return response;\n }\n\n return undefined;\n}\n\n/**\n * Enforces `@RequirePermission` by asking Canopy, once, per guarded request.\n *\n * The guard fails closed at every step. An unresolvable identity, a `node`\n * check with no node, a denial, or an unreachable API all end the request —\n * there is no path through this class that allows a request whose decision is\n * unknown. That is the property worth protecting when reading this code: an\n * `allow` must be something Canopy actually said.\n *\n * A route with no requirement is allowed through untouched, so the guard can be\n * applied globally and opted into per route. Registered that way it runs before\n * controller- and route-scoped guards, so whatever authenticates the request has\n * to be registered globally ahead of it — otherwise `resolveIdentity` runs\n * before there is anything to resolve and every guarded route denies.\n */\n@Injectable()\nexport class CanopyGuard implements CanActivate {\n private readonly logger = new Logger(CanopyGuard.name);\n\n constructor(\n @Inject(Reflector) private readonly reflector: Reflector,\n @Inject(CANOPY_AUTHORIZER) private readonly authorizer: LocalAuthorizer,\n @Inject(CANOPY_OPTIONS)\n private readonly options: CanopyModuleOptions<unknown>,\n ) {}\n\n async canActivate(context: ExecutionContext): Promise<boolean> {\n const requirement = this.reflector.getAllAndOverride<\n PermissionRequirement | undefined\n >(CANOPY_PERMISSION, [context.getHandler(), context.getClass()]);\n\n if (!requirement) {\n return true;\n }\n\n const http = context.switchToHttp();\n const request: unknown = http.getRequest();\n const identityId = resolveSafely(this.options.resolveIdentity, request);\n\n if (!identityId) {\n throw new ForbiddenException(\n \"No identity on the request. Canopy cannot evaluate a permission without one.\",\n );\n }\n\n const decision = await this.evaluate(\n requirement,\n identityId,\n request,\n watchDisconnect(http),\n );\n\n if (!decision.allowed) {\n throw new ForbiddenException(\n `Identity does not hold ${requirement.permission}.`,\n );\n }\n\n return true;\n }\n\n /**\n * Asks Canopy the question the decorator recorded.\n *\n * Every failure is converted into a thrown exception rather than a `false`\n * decision, so a caller cannot mistake \"we could not tell\" for \"denied by\n * policy\" — and neither outcome lets the request proceed.\n */\n private async evaluate(\n requirement: PermissionRequirement,\n identityId: string,\n request: unknown,\n cancellation: Cancellation | undefined,\n ): Promise<Verdict> {\n const query = this.buildQuery(requirement, identityId, request);\n\n try {\n // Answered in-process. The authorizer holds the identity's grant roots\n // and a shared copy of the hierarchy, so the common case reaches no\n // network at all; only a cold or expired entry does, and its deadline\n // and retries are bounded by the same options as before.\n return await this.authorizer.evaluate(\n query,\n cancellation ? { signal: cancellation.signal } : {},\n );\n } catch (error) {\n // The caller hung up mid-check. There is no one left to answer, and this\n // is not Canopy failing — so it must not be logged or reported as though\n // it were. Rethrowing the abort ends the request without inventing a\n // decision.\n if (cancellation?.signal.aborted) {\n throw error;\n }\n\n // Deliberately not re-thrown as-is: a failure here must not reach the\n // client as though Canopy had answered.\n this.logger.error(\n `Could not evaluate ${requirement.permission} for ${identityId}`,\n error instanceof Error ? error.stack : String(error),\n );\n\n throw this.toHttpException(error);\n } finally {\n cancellation?.dispose();\n }\n }\n\n /**\n * Turn a failed check into the status that describes it.\n *\n * Every branch denies — nothing here can let a request through — but they say\n * different things, and the difference is what makes an incident diagnosable.\n * Reporting a rejected API key as \"temporarily unavailable\" invites a retry\n * that will never succeed and hides a misconfiguration behind an outage.\n */\n private toHttpException(error: unknown): Error {\n // The authorizer could not see far enough to decide — a credential that\n // cannot read the hierarchy, most likely. Not an outage: no retry fixes a\n // missing scope, and reporting it as one would bury a misconfiguration\n // under what looks like a blip.\n if (isCanopyAuthorizerError(error)) {\n return new InternalServerErrorException(\n `Authorization is misconfigured: ${error.message}`,\n );\n }\n\n if (!isCanopyError(error)) {\n // Never answered: unreachable, reset, or past the deadline.\n return new ServiceUnavailableException(\n \"Authorization is temporarily unavailable.\",\n );\n }\n\n // Canopy rejected our own credential. No retry fixes this.\n if (error.isAuthFailure) {\n return new InternalServerErrorException(\n \"Authorization is misconfigured. Check the Canopy API key and its scopes.\",\n );\n }\n\n // A 404 with no error code is Nest's own \"no such route\", which here means\n // this Canopy does not serve the endpoint the authorizer reads — an SDK\n // newer than the API it is pointed at. Denying would be catastrophic and\n // silent: every guarded route in the application would answer 403 with\n // nothing to suggest the cause is a version mismatch rather than policy.\n if (error.statusCode === 404 && error.code === null) {\n return new InternalServerErrorException(\n `Authorization is misconfigured: ${error.request.method} ${error.request.path} is not served by this Canopy. ` +\n \"The SDK requires an API that exposes the identity grants endpoint.\",\n );\n }\n\n // A 404 that does carry a code is a real answer — the identity is not in\n // this Environment. Deny rather than 500, the guard's whole thesis being\n // that an undecidable request does not proceed, but say nothing about what\n // was missing.\n if (error.statusCode === 404) {\n return new ForbiddenException(\"Could not evaluate the permission.\");\n }\n\n if (error.isRateLimited || error.statusCode >= 500) {\n return new ServiceUnavailableException(\n \"Authorization is temporarily unavailable.\",\n );\n }\n\n // A 4xx we did not anticipate is a malformed question — our bug, not a\n // transient condition.\n return new InternalServerErrorException(\n \"Authorization could not be evaluated.\",\n );\n }\n\n private buildQuery(\n requirement: PermissionRequirement,\n identityId: string,\n request: unknown,\n ): EvaluateQuery {\n if (requirement.scope === \"app_wide\") {\n return {\n identity_id: identityId,\n permission: requirement.permission,\n scope: \"app_wide\",\n };\n }\n\n // An organization is a hierarchy node, and a membership is a role\n // assignment at it — so the org question IS the node question, asked at\n // the node the caller's token says it is acting in.\n if (requirement.scope === \"org\") {\n const orgId = this.resolveOrg(request);\n\n if (!orgId) {\n throw new ForbiddenException(\n `No organization on the request. ${requirement.permission} is checked inside the caller's active organization, and this caller is not acting in one.`,\n );\n }\n\n return {\n identity_id: identityId,\n permission: requirement.permission,\n scope: \"node\",\n node_id: orgId,\n };\n }\n\n const nodeId = this.options.resolveNode\n ? resolveSafely(this.options.resolveNode, request)\n : undefined;\n\n if (!nodeId) {\n throw new ForbiddenException(\n `No node on the request. ${requirement.permission} is checked at a node; configure resolveNode, or declare the route as app_wide if that is genuinely the question.`,\n );\n }\n\n return {\n identity_id: identityId,\n permission: requirement.permission,\n scope: \"node\",\n node_id: nodeId,\n };\n }\n\n /**\n * The organization the request acts in: `resolveOrg` when configured,\n * otherwise the `org_id` claim off the verified token `CanopyTokenGuard`\n * attached. Both sources trace back to a verified token — the org id\n * decides whose grants answer the check, so it must never be readable off\n * anything the caller controls directly.\n */\n private resolveOrg(request: unknown): string | null | undefined {\n if (this.options.resolveOrg) {\n return resolveSafely(this.options.resolveOrg, request);\n }\n\n const attachedAs = this.options.attachTokenAs ?? \"canopyToken\";\n const claims = (request as Record<string, unknown> | null | undefined)?.[\n attachedAs\n ];\n\n const orgId = (claims as { org_id?: unknown } | undefined)?.org_id;\n\n return typeof orgId === \"string\" && orgId !== \"\" ? orgId : undefined;\n }\n}\n\n/**\n * Run a resolver, treating a throw as \"could not resolve\".\n *\n * Resolvers reach into the request — `request.user.sub` is the shape the docs\n * suggest and the one most applications write. On an unauthenticated request\n * `request.user` is undefined and that expression throws, which without this\n * would surface as a 500. An identity that cannot be established is exactly the\n * request that must be denied, so it becomes the same 403 as returning nothing.\n */\nfunction resolveSafely(\n resolve: (request: never) => string | null | undefined,\n request: unknown,\n): string | null | undefined {\n try {\n return (resolve as (request: unknown) => string | null | undefined)(\n request,\n );\n } catch {\n return undefined;\n }\n}\n\n/**\n * Abort the permission check if the caller hangs up while it is in flight.\n *\n * Watches the *response*, not the request. A request stream emits `close` on\n * every normal request once its body has been read, so listening there would\n * abort perfectly healthy checks; a response emits `close` before\n * `writableEnded` only when nothing was ever sent — which is exactly the\n * disconnect case. The `writableEnded` re-check at fire time keeps that true\n * even if the ordering differs by adapter.\n *\n * Returns undefined for a transport with no such response — a GraphQL or\n * microservice context — where the check simply runs to its deadline.\n */\nfunction watchDisconnect(http: {\n getResponse?: () => unknown;\n}): Cancellation | undefined {\n const raw = resolveStream(http.getResponse?.());\n\n if (!raw) {\n return undefined;\n }\n\n const controller = new AbortController();\n\n const onClose = () => {\n if (raw.writableEnded === true) {\n return;\n }\n\n controller.abort(\n new DOMException(\"The client closed the request\", \"AbortError\"),\n );\n };\n\n raw.once(\"close\", onClose);\n\n return {\n signal: controller.signal,\n dispose: () => raw.removeListener(\"close\", onClose),\n };\n}\n","import {\n type CanActivate,\n type ExecutionContext,\n Inject,\n Injectable,\n ServiceUnavailableException,\n UnauthorizedException,\n} from \"@nestjs/common\";\n\nimport {\n type CanopyTokenClaims,\n isCanopyTokenError,\n TokenVerifier,\n} from \"@canopy-io/node\";\n\nimport type { CanopyModuleOptions } from \"./options.js\";\nimport { CANOPY_OPTIONS, CANOPY_TOKEN_VERIFIER } from \"./tokens.js\";\n\n/**\n * Verifies the bearer token and attaches its claims to the request.\n *\n * This is the authentication half, and it is opt-in on purpose. `CanopyGuard`\n * answers \"may this identity do this?\"; something has to have established\n * *which* identity first, and `resolveIdentity` says as much — \"usually a\n * claim off the verified access token your auth layer already attached\".\n *\n * Use this guard when Canopy issues your tokens and you have no auth layer\n * yet. Skip it entirely if you already run Passport or your own JWT\n * middleware — it is additive, and nothing else in the package depends on it.\n *\n * ```ts\n * @UseGuards(CanopyTokenGuard, CanopyGuard)\n * @RequirePermission(\"documents.read\")\n * findAll() {}\n * ```\n *\n * Order matters: this must run first, because `CanopyGuard` reads what it\n * attaches. Nest runs guards left to right, so listing them in that order is\n * the whole of the wiring.\n *\n * Claims land on `request.canopyToken` by default, which is why\n * `resolveIdentity` is usually `(req) => req.canopyToken?.sub`. The property\n * is configurable so it cannot collide with a `request.user` an existing\n * middleware already owns.\n */\n@Injectable()\nexport class CanopyTokenGuard implements CanActivate {\n constructor(\n @Inject(CANOPY_TOKEN_VERIFIER) private readonly verifier: TokenVerifier,\n @Inject(CANOPY_OPTIONS)\n private readonly options: CanopyModuleOptions<unknown>,\n ) {}\n\n async canActivate(context: ExecutionContext): Promise<boolean> {\n const request = context.switchToHttp().getRequest<{\n headers?: Record<string, string | string[] | undefined>;\n [key: string]: unknown;\n }>();\n\n const token = readBearerToken(request.headers?.[\"authorization\"]);\n\n if (token === null) {\n throw new UnauthorizedException(\n \"Missing bearer token in the Authorization header.\",\n );\n }\n\n let claims: CanopyTokenClaims;\n\n try {\n claims = await this.verifier.verify(token);\n } catch (error) {\n // Not being able to verify is not the same as failing to verify. When\n // the key set cannot be read the caller's token may be perfectly good —\n // we simply cannot say — so this is ours to own, exactly as the\n // permission guard answers 503 rather than denying on an undecidable\n // check.\n //\n // Reporting it as 401 would be worse than merely inaccurate: a client\n // reads 401 as \"your token is stale\", discards a valid one and asks for\n // another, which aims a refresh storm at the issuer that is already\n // down. And an outage that only ever emits 4xx is invisible to anything\n // watching 5xx.\n if (\n isCanopyTokenError(error) &&\n error.code === \"token.jwks_unavailable\"\n ) {\n throw new ServiceUnavailableException(\n \"Authentication is temporarily unavailable.\",\n { cause: error },\n );\n }\n\n // Every other cause is the token itself, and a 401 whatever the detail:\n // the caller cannot fix a bad signature by being granted something. The\n // specific code stays on the error for logs rather than going to the\n // caller, who has no use for the difference and should not be told which\n // check failed.\n if (isCanopyTokenError(error)) {\n throw new UnauthorizedException(\"Invalid or expired access token.\", {\n cause: error,\n });\n }\n\n throw error;\n }\n\n request[this.options.attachTokenAs ?? \"canopyToken\"] = claims;\n\n return true;\n }\n}\n\n/**\n * The token out of an `Authorization: Bearer …` header.\n *\n * Returns null rather than throwing so the caller owns the message. The scheme\n * is matched case-insensitively because RFC 7235 says it is case-insensitive,\n * and clients do send `bearer`.\n */\nfunction readBearerToken(header: string | string[] | undefined): string | null {\n const value = Array.isArray(header) ? header[0] : header;\n\n if (typeof value !== \"string\") {\n return null;\n }\n\n // Split by hand rather than with `/^bearer\\s+(.+)$/i`: the header is the\n // caller's to write, and that pattern backtracks on a long run of spaces.\n const trimmed = value.trim();\n const scheme = trimmed.slice(0, 6);\n const separator = trimmed.charAt(6);\n\n if (scheme.toLowerCase() !== \"bearer\" || !/\\s/.test(separator)) {\n return null;\n }\n\n const token = trimmed.slice(7).trim();\n\n return token.length > 0 ? token : null;\n}\n","import { type DynamicModule, Module, type Provider } from \"@nestjs/common\";\n\nimport { Canopy, LocalAuthorizer, TokenVerifier } from \"@canopy-io/node\";\n\nimport { CanopyGuard } from \"./canopy.guard.js\";\nimport { CanopyTokenGuard } from \"./canopy-token.guard.js\";\nimport type {\n CanopyModuleAsyncOptions,\n CanopyModuleOptions,\n} from \"./options.js\";\nimport {\n CANOPY_AUTHORIZER,\n CANOPY_CLIENT,\n CANOPY_OPTIONS,\n CANOPY_TOKEN_VERIFIER,\n} from \"./tokens.js\";\n\n/**\n * Wires one configured `Canopy` client and the guard that uses it into a Nest\n * application.\n *\n * ```ts\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 *\n * The client is a singleton because it holds no per-request state — it is a\n * `fetch` wrapper with credentials — so there is nothing to gain from building\n * one per request and a connection pool to lose.\n */\n\n/**\n * Per-attempt deadline for a cache miss on the request path.\n *\n * Five seconds rather than the client's 30: this call sits on the request path,\n * so its deadline is time an inbound request spends waiting. An authorization\n * service that has not answered in five seconds is not going to save the\n * request.\n */\nconst DEFAULT_EVALUATE_TIMEOUT_MS = 5_000;\n\n/**\n * Retries for a cache miss on the request path.\n *\n * One, not the client's two. A deadline bounds an attempt; this bounds how many\n * of them an inbound request can wait through. One retry still absorbs a\n * transient blip, and caps the worst case at roughly two deadlines rather than\n * three.\n */\nconst DEFAULT_EVALUATE_MAX_RETRIES = 1;\n\n/**\n * The authorizer both registration paths build.\n *\n * The guard's `evaluateTimeoutMs` / `evaluateMaxRetries` still apply, but to\n * far fewer calls: they bound a cache miss rather than every request.\n */\nfunction buildAuthorizer<TRequest>(\n client: Canopy,\n options: CanopyModuleOptions<TRequest>,\n): LocalAuthorizer {\n return new LocalAuthorizer(client.client, {\n ...(options.authorizationTtlMs === undefined\n ? {}\n : { ttlMs: options.authorizationTtlMs }),\n timeoutMs: options.evaluateTimeoutMs ?? DEFAULT_EVALUATE_TIMEOUT_MS,\n maxRetries: options.evaluateMaxRetries ?? DEFAULT_EVALUATE_MAX_RETRIES,\n });\n}\n\n@Module({})\nexport class CanopyModule {\n static forRoot<TRequest = unknown>(\n options: CanopyModuleOptions<TRequest>,\n ): DynamicModule {\n const providers: Provider[] = [\n { provide: CANOPY_OPTIONS, useValue: options },\n {\n provide: CANOPY_CLIENT,\n useFactory: () => new Canopy(options),\n },\n {\n provide: CANOPY_TOKEN_VERIFIER,\n useFactory: () => new TokenVerifier(options.verify),\n },\n {\n provide: CANOPY_AUTHORIZER,\n useFactory: (client: Canopy) => buildAuthorizer(client, options),\n inject: [CANOPY_CLIENT],\n },\n CanopyGuard,\n CanopyTokenGuard,\n ];\n\n return {\n module: CanopyModule,\n global: options.isGlobal ?? false,\n providers,\n exports: [\n CANOPY_CLIENT,\n CANOPY_OPTIONS,\n CANOPY_TOKEN_VERIFIER,\n CANOPY_AUTHORIZER,\n CanopyGuard,\n CanopyTokenGuard,\n ],\n };\n }\n\n /**\n * The same wiring when the API key comes from something injectable, such as\n * `ConfigService`, rather than being available at module-definition time.\n */\n static forRootAsync<TRequest = unknown>(\n options: CanopyModuleAsyncOptions<TRequest>,\n ): DynamicModule {\n const providers: Provider[] = [\n {\n provide: CANOPY_OPTIONS,\n useFactory: options.useFactory,\n inject: options.inject ?? [],\n },\n {\n provide: CANOPY_CLIENT,\n useFactory: (resolved: CanopyModuleOptions<TRequest>) =>\n new Canopy(resolved),\n inject: [CANOPY_OPTIONS],\n },\n {\n provide: CANOPY_TOKEN_VERIFIER,\n useFactory: (resolved: CanopyModuleOptions<TRequest>) =>\n new TokenVerifier(resolved.verify),\n inject: [CANOPY_OPTIONS],\n },\n {\n provide: CANOPY_AUTHORIZER,\n useFactory: (client: Canopy, resolved: CanopyModuleOptions<TRequest>) =>\n buildAuthorizer(client, resolved),\n inject: [CANOPY_CLIENT, CANOPY_OPTIONS],\n },\n CanopyGuard,\n CanopyTokenGuard,\n ];\n\n return {\n module: CanopyModule,\n global: options.isGlobal ?? false,\n // Spread rather than assigned: under exactOptionalPropertyTypes an\n // explicit `imports: undefined` is not the same as no `imports` at all.\n ...(options.imports ? { imports: options.imports } : {}),\n providers,\n exports: [\n CANOPY_CLIENT,\n CANOPY_OPTIONS,\n CANOPY_TOKEN_VERIFIER,\n CANOPY_AUTHORIZER,\n CanopyGuard,\n CanopyTokenGuard,\n ],\n };\n }\n}\n","import { Inject } from \"@nestjs/common\";\n\nimport { CANOPY_CLIENT } from \"./tokens.js\";\n\n/**\n * Injects the configured client.\n *\n * ```ts\n * constructor(@InjectCanopy() private readonly canopy: Canopy) {}\n * ```\n *\n * Named rather than resolved by type because this package is bundled with\n * esbuild, which emits no `design:paramtypes` — so type-only injection would\n * fail at runtime with a confusing \"cannot resolve dependency\" rather than at\n * build time.\n */\nexport const InjectCanopy = (): ParameterDecorator => Inject(CANOPY_CLIENT);\n","import { SetMetadata } from \"@nestjs/common\";\n\nimport { CANOPY_PERMISSION } from \"./tokens.js\";\n\n/**\n * Which question the guard asks Canopy.\n *\n * `node` walks the node's lineage, so a role granted on an ancestor is\n * inherited at the node being touched. `app_wide` asks only whether the\n * identity holds the permission anywhere in the Environment. `org` is the\n * node question asked at the organization the caller's token is acting in —\n * the `org_id` claim — so the route needs no `resolveNode` and no node in\n * its path.\n */\nexport type PermissionScope = \"node\" | \"app_wide\" | \"org\";\n\n/** What `@RequirePermission` records for the guard to read back. */\nexport interface PermissionRequirement {\n readonly permission: string;\n readonly scope: PermissionScope;\n}\n\nexport interface RequirePermissionOptions {\n /**\n * Defaults to `node`.\n *\n * `app_wide` is opt-in on purpose. It answers a coarser question and returns\n * no effective node, which makes it right for deciding whether to render a\n * menu item and wrong for guarding a resource that belongs to one — a\n * distinction that is easy to get backwards if the looser check is the\n * default.\n *\n * `org` evaluates at the organization on the caller's verified token\n * (`org_id`), read from the claims `CanopyTokenGuard` attaches or from\n * `resolveOrg` if configured. A caller acting in no organization is denied\n * — an org-scoped route has no meaning outside one.\n */\n readonly scope?: PermissionScope;\n}\n\n/**\n * Require a Canopy permission before a route runs.\n *\n * ```ts\n * @UseGuards(CanopyGuard)\n * @RequirePermission(\"orders.refund\")\n * @Post(\":orderId/refund\")\n * refund() {}\n * ```\n *\n * Applies to a single handler or to a whole controller; a handler's own\n * requirement wins over its controller's.\n */\nexport const RequirePermission = (\n permission: string,\n options: RequirePermissionOptions = {},\n): MethodDecorator & ClassDecorator =>\n SetMetadata<string, PermissionRequirement>(CANOPY_PERMISSION, {\n permission,\n scope: options.scope ?? \"node\",\n });\n"]}
1
+ {"version":3,"sources":["../src/tokens.ts","../src/canopy.guard.ts","../src/canopy-token.guard.ts","../src/canopy.module.ts","../src/inject-canopy.decorator.ts","../src/require-permission.decorator.ts"],"names":["CanopyGuard","Logger","ForbiddenException","authorizationErrorStatus","ServiceUnavailableException","InternalServerErrorException","Injectable","Reflector","CanopyTokenGuard","UnauthorizedException","isCanopyTokenError","tokenErrorStatus","Inject","LocalAuthorizer","CanopyModule","Canopy","TokenVerifier","Module","SetMetadata"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Injection tokens.\n *\n * These are strings rather than classes because esbuild does not emit\n * `design:paramtypes`, so nothing here can be injected by constructor type\n * alone — every dependency is named with an explicit `@Inject`.\n */\n\n/** The resolved `CanopyModuleOptions`. */\nexport const CANOPY_OPTIONS = \"CANOPY_OPTIONS\";\n\n/** The configured `Canopy` client. */\nexport const CANOPY_CLIENT = \"CANOPY_CLIENT\";\n\n/**\n * The configured `LocalAuthorizer`.\n *\n * Shared deliberately: it holds one copy of the hierarchy for the whole\n * process, so a second instance would double both the memory and the traffic\n * it exists to avoid.\n */\nexport const CANOPY_AUTHORIZER = \"CANOPY_AUTHORIZER\";\n\n/** The configured `TokenVerifier`, shared so its key cache is shared too. */\nexport const CANOPY_TOKEN_VERIFIER = \"CANOPY_TOKEN_VERIFIER\";\n\n/** Metadata key holding what `@RequirePermission` recorded on a handler. */\nexport const CANOPY_PERMISSION = \"CANOPY_PERMISSION\";\n","import {\n type CanActivate,\n type ExecutionContext,\n ForbiddenException,\n Inject,\n Injectable,\n InternalServerErrorException,\n Logger,\n ServiceUnavailableException,\n} from \"@nestjs/common\";\nimport { Reflector } from \"@nestjs/core\";\n\nimport {\n authorizationErrorStatus,\n type LocalAuthorizer,\n type RequestBody,\n} from \"@canopy-io/node\";\n\nimport type { CanopyModuleOptions } from \"./options.js\";\nimport type { PermissionRequirement } from \"./require-permission.decorator.js\";\nimport {\n CANOPY_AUTHORIZER,\n CANOPY_OPTIONS,\n CANOPY_PERMISSION,\n} from \"./tokens.js\";\n\n/** Read off the generated operation, so it follows the API rather than restating it. */\ntype EvaluateQuery = RequestBody<\"ApiPermissionsController_evaluate\">;\n\n/**\n * Only the verdict, not the API's full explain payload.\n *\n * A decision reached in-process cannot report `granting_roles` or an effective\n * node — that reasoning lives on the server. The guard has never needed it, and\n * typing the narrow thing keeps that honest rather than implying a richer\n * answer than exists.\n */\ntype Verdict = { allowed: boolean };\n\ninterface Cancellation {\n signal: AbortSignal;\n dispose: () => void;\n}\n\n/** The Node stream underneath an Express or Fastify response. */\ninterface ClosableResponse {\n once: (event: string, listener: () => void) => unknown;\n removeListener: (event: string, listener: () => void) => unknown;\n writableEnded?: boolean;\n raw?: ClosableResponse;\n}\n\nfunction isClosable(value: unknown): value is ClosableResponse {\n return (\n typeof value === \"object\" &&\n value !== null &&\n typeof (value as ClosableResponse).once === \"function\" &&\n typeof (value as ClosableResponse).removeListener === \"function\"\n );\n}\n\n/**\n * Find the Node response stream behind whatever the adapter hands back.\n *\n * `.raw` is checked first and on its own merits, because the object Nest\n * returns is not always the stream. Express returns the `ServerResponse`\n * itself, which listens; Fastify returns a `Reply`, which is a plain object\n * with the real response on `.raw`. Testing the outer object first and\n * returning early on failure would skip `.raw` in exactly the case it exists\n * for.\n */\nfunction resolveStream(response: unknown): ClosableResponse | undefined {\n const raw = (response as { raw?: unknown } | null | undefined)?.raw;\n\n if (isClosable(raw)) {\n return raw;\n }\n\n if (isClosable(response)) {\n return response;\n }\n\n return undefined;\n}\n\n/**\n * Enforces `@RequirePermission` by asking Canopy, once, per guarded request.\n *\n * The guard fails closed at every step. An unresolvable identity, a `node`\n * check with no node, a denial, or an unreachable API all end the request —\n * there is no path through this class that allows a request whose decision is\n * unknown. That is the property worth protecting when reading this code: an\n * `allow` must be something Canopy actually said.\n *\n * A route with no requirement is allowed through untouched, so the guard can be\n * applied globally and opted into per route. Registered that way it runs before\n * controller- and route-scoped guards, so whatever authenticates the request has\n * to be registered globally ahead of it — otherwise `resolveIdentity` runs\n * before there is anything to resolve and every guarded route denies.\n */\n@Injectable()\nexport class CanopyGuard implements CanActivate {\n private readonly logger = new Logger(CanopyGuard.name);\n\n constructor(\n @Inject(Reflector) private readonly reflector: Reflector,\n @Inject(CANOPY_AUTHORIZER) private readonly authorizer: LocalAuthorizer,\n @Inject(CANOPY_OPTIONS)\n private readonly options: CanopyModuleOptions<unknown>,\n ) {}\n\n async canActivate(context: ExecutionContext): Promise<boolean> {\n const requirement = this.reflector.getAllAndOverride<\n PermissionRequirement | undefined\n >(CANOPY_PERMISSION, [context.getHandler(), context.getClass()]);\n\n if (!requirement) {\n return true;\n }\n\n const http = context.switchToHttp();\n const request: unknown = http.getRequest();\n const identityId = resolveSafely(this.options.resolveIdentity, request);\n\n if (!identityId) {\n throw new ForbiddenException(\n \"No identity on the request. Canopy cannot evaluate a permission without one.\",\n );\n }\n\n const decision = await this.evaluate(\n requirement,\n identityId,\n request,\n watchDisconnect(http),\n );\n\n if (!decision.allowed) {\n throw new ForbiddenException(\n `Identity does not hold ${requirement.permission}.`,\n );\n }\n\n return true;\n }\n\n /**\n * Asks Canopy the question the decorator recorded.\n *\n * Every failure is converted into a thrown exception rather than a `false`\n * decision, so a caller cannot mistake \"we could not tell\" for \"denied by\n * policy\" — and neither outcome lets the request proceed.\n */\n private async evaluate(\n requirement: PermissionRequirement,\n identityId: string,\n request: unknown,\n cancellation: Cancellation | undefined,\n ): Promise<Verdict> {\n const query = this.buildQuery(requirement, identityId, request);\n\n try {\n // Answered in-process. The authorizer holds the identity's grant roots\n // and a shared copy of the hierarchy, so the common case reaches no\n // network at all; only a cold or expired entry does, and its deadline\n // and retries are bounded by the same options as before.\n return await this.authorizer.evaluate(\n query,\n cancellation ? { signal: cancellation.signal } : {},\n );\n } catch (error) {\n // The caller hung up mid-check. There is no one left to answer, and this\n // is not Canopy failing — so it must not be logged or reported as though\n // it were. Rethrowing the abort ends the request without inventing a\n // decision.\n if (cancellation?.signal.aborted) {\n throw error;\n }\n\n // Deliberately not re-thrown as-is: a failure here must not reach the\n // client as though Canopy had answered.\n this.logger.error(\n `Could not evaluate ${requirement.permission} for ${identityId}`,\n error instanceof Error ? error.stack : String(error),\n );\n\n throw this.toHttpException(error);\n } finally {\n cancellation?.dispose();\n }\n }\n\n /**\n * Turn a failed check into the status that describes it.\n *\n * Every branch denies — nothing here can let a request through — but they say\n * different things, and the difference is what makes an incident diagnosable.\n * Reporting a rejected API key as \"temporarily unavailable\" invites a retry\n * that will never succeed and hides a misconfiguration behind an outage. The\n * mapping is `authorizationErrorStatus` in `@canopy-io/node`, so an\n * integration outside NestJS answers the same way.\n */\n private toHttpException(error: unknown): Error {\n const { status, message } = authorizationErrorStatus(error);\n\n if (status === 403) {\n return new ForbiddenException(message);\n }\n\n if (status === 503) {\n return new ServiceUnavailableException(message);\n }\n\n return new InternalServerErrorException(message);\n }\n\n private buildQuery(\n requirement: PermissionRequirement,\n identityId: string,\n request: unknown,\n ): EvaluateQuery {\n if (requirement.scope === \"app_wide\") {\n return {\n identity_id: identityId,\n permission: requirement.permission,\n scope: \"app_wide\",\n };\n }\n\n // An organization is a hierarchy node, and a membership is a role\n // assignment at it — so the org question IS the node question, asked at\n // the node the caller's token says it is acting in.\n if (requirement.scope === \"org\") {\n const orgId = this.resolveOrg(request);\n\n if (!orgId) {\n throw new ForbiddenException(\n `No organization on the request. ${requirement.permission} is checked inside the caller's active organization, and this caller is not acting in one.`,\n );\n }\n\n return {\n identity_id: identityId,\n permission: requirement.permission,\n scope: \"node\",\n node_id: orgId,\n };\n }\n\n const nodeId = this.options.resolveNode\n ? resolveSafely(this.options.resolveNode, request)\n : undefined;\n\n if (!nodeId) {\n throw new ForbiddenException(\n `No node on the request. ${requirement.permission} is checked at a node; configure resolveNode, or declare the route as app_wide if that is genuinely the question.`,\n );\n }\n\n return {\n identity_id: identityId,\n permission: requirement.permission,\n scope: \"node\",\n node_id: nodeId,\n };\n }\n\n /**\n * The organization the request acts in: `resolveOrg` when configured,\n * otherwise the `org_id` claim off the verified token `CanopyTokenGuard`\n * attached. Both sources trace back to a verified token — the org id\n * decides whose grants answer the check, so it must never be readable off\n * anything the caller controls directly.\n */\n private resolveOrg(request: unknown): string | null | undefined {\n if (this.options.resolveOrg) {\n return resolveSafely(this.options.resolveOrg, request);\n }\n\n const attachedAs = this.options.attachTokenAs ?? \"canopyToken\";\n const claims = (request as Record<string, unknown> | null | undefined)?.[\n attachedAs\n ];\n\n const orgId = (claims as { org_id?: unknown } | undefined)?.org_id;\n\n return typeof orgId === \"string\" && orgId !== \"\" ? orgId : undefined;\n }\n}\n\n/**\n * Run a resolver, treating a throw as \"could not resolve\".\n *\n * Resolvers reach into the request — `request.user.sub` is the shape the docs\n * suggest and the one most applications write. On an unauthenticated request\n * `request.user` is undefined and that expression throws, which without this\n * would surface as a 500. An identity that cannot be established is exactly the\n * request that must be denied, so it becomes the same 403 as returning nothing.\n */\nfunction resolveSafely(\n resolve: (request: never) => string | null | undefined,\n request: unknown,\n): string | null | undefined {\n try {\n return (resolve as (request: unknown) => string | null | undefined)(\n request,\n );\n } catch {\n return undefined;\n }\n}\n\n/**\n * Abort the permission check if the caller hangs up while it is in flight.\n *\n * Watches the *response*, not the request. A request stream emits `close` on\n * every normal request once its body has been read, so listening there would\n * abort perfectly healthy checks; a response emits `close` before\n * `writableEnded` only when nothing was ever sent — which is exactly the\n * disconnect case. The `writableEnded` re-check at fire time keeps that true\n * even if the ordering differs by adapter.\n *\n * Returns undefined for a transport with no such response — a GraphQL or\n * microservice context — where the check simply runs to its deadline.\n */\nfunction watchDisconnect(http: {\n getResponse?: () => unknown;\n}): Cancellation | undefined {\n const raw = resolveStream(http.getResponse?.());\n\n if (!raw) {\n return undefined;\n }\n\n const controller = new AbortController();\n\n const onClose = () => {\n if (raw.writableEnded === true) {\n return;\n }\n\n controller.abort(\n new DOMException(\"The client closed the request\", \"AbortError\"),\n );\n };\n\n raw.once(\"close\", onClose);\n\n return {\n signal: controller.signal,\n dispose: () => raw.removeListener(\"close\", onClose),\n };\n}\n","import {\n type CanActivate,\n type ExecutionContext,\n Inject,\n Injectable,\n ServiceUnavailableException,\n UnauthorizedException,\n} from \"@nestjs/common\";\n\nimport {\n type CanopyTokenClaims,\n isCanopyTokenError,\n tokenErrorStatus,\n TokenVerifier,\n} from \"@canopy-io/node\";\n\nimport type { CanopyModuleOptions } from \"./options.js\";\nimport { CANOPY_OPTIONS, CANOPY_TOKEN_VERIFIER } from \"./tokens.js\";\n\n/**\n * Verifies the bearer token and attaches its claims to the request.\n *\n * This is the authentication half, and it is opt-in on purpose. `CanopyGuard`\n * answers \"may this identity do this?\"; something has to have established\n * *which* identity first, and `resolveIdentity` says as much — \"usually a\n * claim off the verified access token your auth layer already attached\".\n *\n * Use this guard when Canopy issues your tokens and you have no auth layer\n * yet. Skip it entirely if you already run Passport or your own JWT\n * middleware — it is additive, and nothing else in the package depends on it.\n *\n * ```ts\n * @UseGuards(CanopyTokenGuard, CanopyGuard)\n * @RequirePermission(\"documents.read\")\n * findAll() {}\n * ```\n *\n * Order matters: this must run first, because `CanopyGuard` reads what it\n * attaches. Nest runs guards left to right, so listing them in that order is\n * the whole of the wiring.\n *\n * Claims land on `request.canopyToken` by default, which is why\n * `resolveIdentity` is usually `(req) => req.canopyToken?.sub`. The property\n * is configurable so it cannot collide with a `request.user` an existing\n * middleware already owns.\n */\n@Injectable()\nexport class CanopyTokenGuard implements CanActivate {\n constructor(\n @Inject(CANOPY_TOKEN_VERIFIER) private readonly verifier: TokenVerifier,\n @Inject(CANOPY_OPTIONS)\n private readonly options: CanopyModuleOptions<unknown>,\n ) {}\n\n async canActivate(context: ExecutionContext): Promise<boolean> {\n const request = context.switchToHttp().getRequest<{\n headers?: Record<string, string | string[] | undefined>;\n [key: string]: unknown;\n }>();\n\n const token = readBearerToken(request.headers?.[\"authorization\"]);\n\n if (token === null) {\n throw new UnauthorizedException(\n \"Missing bearer token in the Authorization header.\",\n );\n }\n\n let claims: CanopyTokenClaims;\n\n try {\n claims = await this.verifier.verify(token);\n } catch (error) {\n // Not being able to verify is not the same as failing to verify. When\n // the key set cannot be read the caller's token may be perfectly good —\n // we simply cannot say — so this is ours to own, exactly as the\n // permission guard answers 503 rather than denying on an undecidable\n // check.\n //\n // Reporting it as 401 would be worse than merely inaccurate: a client\n // reads 401 as \"your token is stale\", discards a valid one and asks for\n // another, which aims a refresh storm at the issuer that is already\n // down. And an outage that only ever emits 4xx is invisible to anything\n // watching 5xx.\n if (!isCanopyTokenError(error)) {\n throw error;\n }\n\n const { status, message } = tokenErrorStatus(error);\n\n if (status === 503) {\n throw new ServiceUnavailableException(message, { cause: error });\n }\n\n // Every other cause is the token itself, and a 401 whatever the detail:\n // the caller cannot fix a bad signature by being granted something. The\n // specific code stays on the error for logs rather than going to the\n // caller, who has no use for the difference and should not be told which\n // check failed.\n throw new UnauthorizedException(message, { cause: error });\n }\n\n request[this.options.attachTokenAs ?? \"canopyToken\"] = claims;\n\n return true;\n }\n}\n\n/**\n * The token out of an `Authorization: Bearer …` header.\n *\n * Returns null rather than throwing so the caller owns the message. The scheme\n * is matched case-insensitively because RFC 7235 says it is case-insensitive,\n * and clients do send `bearer`.\n */\nfunction readBearerToken(header: string | string[] | undefined): string | null {\n const value = Array.isArray(header) ? header[0] : header;\n\n if (typeof value !== \"string\") {\n return null;\n }\n\n // Split by hand rather than with `/^bearer\\s+(.+)$/i`: the header is the\n // caller's to write, and that pattern backtracks on a long run of spaces.\n const trimmed = value.trim();\n const scheme = trimmed.slice(0, 6);\n const separator = trimmed.charAt(6);\n\n if (scheme.toLowerCase() !== \"bearer\" || !/\\s/.test(separator)) {\n return null;\n }\n\n const token = trimmed.slice(7).trim();\n\n return token.length > 0 ? token : null;\n}\n","import { type DynamicModule, Module, type Provider } from \"@nestjs/common\";\n\nimport { Canopy, LocalAuthorizer, TokenVerifier } from \"@canopy-io/node\";\n\nimport { CanopyGuard } from \"./canopy.guard.js\";\nimport { CanopyTokenGuard } from \"./canopy-token.guard.js\";\nimport type {\n CanopyModuleAsyncOptions,\n CanopyModuleOptions,\n} from \"./options.js\";\nimport {\n CANOPY_AUTHORIZER,\n CANOPY_CLIENT,\n CANOPY_OPTIONS,\n CANOPY_TOKEN_VERIFIER,\n} from \"./tokens.js\";\n\n/**\n * Wires one configured `Canopy` client and the guard that uses it into a Nest\n * application.\n *\n * ```ts\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 *\n * The client is a singleton because it holds no per-request state — it is a\n * `fetch` wrapper with credentials — so there is nothing to gain from building\n * one per request and a connection pool to lose.\n */\n\n/**\n * Per-attempt deadline for a cache miss on the request path.\n *\n * Five seconds rather than the client's 30: this call sits on the request path,\n * so its deadline is time an inbound request spends waiting. An authorization\n * service that has not answered in five seconds is not going to save the\n * request.\n */\nconst DEFAULT_EVALUATE_TIMEOUT_MS = 5_000;\n\n/**\n * Retries for a cache miss on the request path.\n *\n * One, not the client's two. A deadline bounds an attempt; this bounds how many\n * of them an inbound request can wait through. One retry still absorbs a\n * transient blip, and caps the worst case at roughly two deadlines rather than\n * three.\n */\nconst DEFAULT_EVALUATE_MAX_RETRIES = 1;\n\n/**\n * The authorizer both registration paths build.\n *\n * The guard's `evaluateTimeoutMs` / `evaluateMaxRetries` still apply, but to\n * far fewer calls: they bound a cache miss rather than every request.\n */\nfunction buildAuthorizer<TRequest>(\n client: Canopy,\n options: CanopyModuleOptions<TRequest>,\n): LocalAuthorizer {\n return new LocalAuthorizer(client.client, {\n ...(options.authorizationTtlMs === undefined\n ? {}\n : { ttlMs: options.authorizationTtlMs }),\n timeoutMs: options.evaluateTimeoutMs ?? DEFAULT_EVALUATE_TIMEOUT_MS,\n maxRetries: options.evaluateMaxRetries ?? DEFAULT_EVALUATE_MAX_RETRIES,\n });\n}\n\n@Module({})\nexport class CanopyModule {\n static forRoot<TRequest = unknown>(\n options: CanopyModuleOptions<TRequest>,\n ): DynamicModule {\n const providers: Provider[] = [\n { provide: CANOPY_OPTIONS, useValue: options },\n {\n provide: CANOPY_CLIENT,\n useFactory: () => new Canopy(options),\n },\n {\n provide: CANOPY_TOKEN_VERIFIER,\n useFactory: () => new TokenVerifier(options.verify),\n },\n {\n provide: CANOPY_AUTHORIZER,\n useFactory: (client: Canopy) => buildAuthorizer(client, options),\n inject: [CANOPY_CLIENT],\n },\n CanopyGuard,\n CanopyTokenGuard,\n ];\n\n return {\n module: CanopyModule,\n global: options.isGlobal ?? false,\n providers,\n exports: [\n CANOPY_CLIENT,\n CANOPY_OPTIONS,\n CANOPY_TOKEN_VERIFIER,\n CANOPY_AUTHORIZER,\n CanopyGuard,\n CanopyTokenGuard,\n ],\n };\n }\n\n /**\n * The same wiring when the API key comes from something injectable, such as\n * `ConfigService`, rather than being available at module-definition time.\n */\n static forRootAsync<TRequest = unknown>(\n options: CanopyModuleAsyncOptions<TRequest>,\n ): DynamicModule {\n const providers: Provider[] = [\n {\n provide: CANOPY_OPTIONS,\n useFactory: options.useFactory,\n inject: options.inject ?? [],\n },\n {\n provide: CANOPY_CLIENT,\n useFactory: (resolved: CanopyModuleOptions<TRequest>) =>\n new Canopy(resolved),\n inject: [CANOPY_OPTIONS],\n },\n {\n provide: CANOPY_TOKEN_VERIFIER,\n useFactory: (resolved: CanopyModuleOptions<TRequest>) =>\n new TokenVerifier(resolved.verify),\n inject: [CANOPY_OPTIONS],\n },\n {\n provide: CANOPY_AUTHORIZER,\n useFactory: (client: Canopy, resolved: CanopyModuleOptions<TRequest>) =>\n buildAuthorizer(client, resolved),\n inject: [CANOPY_CLIENT, CANOPY_OPTIONS],\n },\n CanopyGuard,\n CanopyTokenGuard,\n ];\n\n return {\n module: CanopyModule,\n global: options.isGlobal ?? false,\n // Spread rather than assigned: under exactOptionalPropertyTypes an\n // explicit `imports: undefined` is not the same as no `imports` at all.\n ...(options.imports ? { imports: options.imports } : {}),\n providers,\n exports: [\n CANOPY_CLIENT,\n CANOPY_OPTIONS,\n CANOPY_TOKEN_VERIFIER,\n CANOPY_AUTHORIZER,\n CanopyGuard,\n CanopyTokenGuard,\n ],\n };\n }\n}\n","import { Inject } from \"@nestjs/common\";\n\nimport { CANOPY_CLIENT } from \"./tokens.js\";\n\n/**\n * Injects the configured client.\n *\n * ```ts\n * constructor(@InjectCanopy() private readonly canopy: Canopy) {}\n * ```\n *\n * Named rather than resolved by type because this package is bundled with\n * esbuild, which emits no `design:paramtypes` — so type-only injection would\n * fail at runtime with a confusing \"cannot resolve dependency\" rather than at\n * build time.\n */\nexport const InjectCanopy = (): ParameterDecorator => Inject(CANOPY_CLIENT);\n","import { SetMetadata } from \"@nestjs/common\";\n\nimport { CANOPY_PERMISSION } from \"./tokens.js\";\n\n/**\n * Which question the guard asks Canopy.\n *\n * `node` walks the node's lineage, so a role granted on an ancestor is\n * inherited at the node being touched. `app_wide` asks only whether the\n * identity holds the permission anywhere in the Environment. `org` is the\n * node question asked at the organization the caller's token is acting in —\n * the `org_id` claim — so the route needs no `resolveNode` and no node in\n * its path.\n */\nexport type PermissionScope = \"node\" | \"app_wide\" | \"org\";\n\n/** What `@RequirePermission` records for the guard to read back. */\nexport interface PermissionRequirement {\n readonly permission: string;\n readonly scope: PermissionScope;\n}\n\nexport interface RequirePermissionOptions {\n /**\n * Defaults to `node`.\n *\n * `app_wide` is opt-in on purpose. It answers a coarser question and returns\n * no effective node, which makes it right for deciding whether to render a\n * menu item and wrong for guarding a resource that belongs to one — a\n * distinction that is easy to get backwards if the looser check is the\n * default.\n *\n * `org` evaluates at the organization on the caller's verified token\n * (`org_id`), read from the claims `CanopyTokenGuard` attaches or from\n * `resolveOrg` if configured. A caller acting in no organization is denied\n * — an org-scoped route has no meaning outside one.\n */\n readonly scope?: PermissionScope;\n}\n\n/**\n * Require a Canopy permission before a route runs.\n *\n * ```ts\n * @UseGuards(CanopyGuard)\n * @RequirePermission(\"orders.refund\")\n * @Post(\":orderId/refund\")\n * refund() {}\n * ```\n *\n * Applies to a single handler or to a whole controller; a handler's own\n * requirement wins over its controller's.\n */\nexport const RequirePermission = (\n permission: string,\n options: RequirePermissionOptions = {},\n): MethodDecorator & ClassDecorator =>\n SetMetadata<string, PermissionRequirement>(CANOPY_PERMISSION, {\n permission,\n scope: options.scope ?? \"node\",\n });\n"]}
package/dist/index.d.cts CHANGED
@@ -160,7 +160,9 @@ declare class CanopyGuard implements CanActivate {
160
160
  * Every branch denies — nothing here can let a request through — but they say
161
161
  * different things, and the difference is what makes an incident diagnosable.
162
162
  * Reporting a rejected API key as "temporarily unavailable" invites a retry
163
- * that will never succeed and hides a misconfiguration behind an outage.
163
+ * that will never succeed and hides a misconfiguration behind an outage. The
164
+ * mapping is `authorizationErrorStatus` in `@canopy-io/node`, so an
165
+ * integration outside NestJS answers the same way.
164
166
  */
165
167
  private toHttpException;
166
168
  private buildQuery;
package/dist/index.d.ts CHANGED
@@ -160,7 +160,9 @@ declare class CanopyGuard implements CanActivate {
160
160
  * Every branch denies — nothing here can let a request through — but they say
161
161
  * different things, and the difference is what makes an incident diagnosable.
162
162
  * Reporting a rejected API key as "temporarily unavailable" invites a retry
163
- * that will never succeed and hides a misconfiguration behind an outage.
163
+ * that will never succeed and hides a misconfiguration behind an outage. The
164
+ * mapping is `authorizationErrorStatus` in `@canopy-io/node`, so an
165
+ * integration outside NestJS answers the same way.
164
166
  */
165
167
  private toHttpException;
166
168
  private buildQuery;
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { Injectable, Inject, Module, Logger, ForbiddenException, InternalServerErrorException, ServiceUnavailableException, UnauthorizedException, SetMetadata } from '@nestjs/common';
2
- import { isCanopyAuthorizerError, isCanopyError, isCanopyTokenError, Canopy, TokenVerifier, LocalAuthorizer } from '@canopy-io/node';
1
+ import { Injectable, Inject, Module, Logger, ForbiddenException, ServiceUnavailableException, InternalServerErrorException, UnauthorizedException, SetMetadata } from '@nestjs/common';
2
+ import { authorizationErrorStatus, isCanopyTokenError, tokenErrorStatus, Canopy, TokenVerifier, LocalAuthorizer } from '@canopy-io/node';
3
3
  import { Reflector } from '@nestjs/core';
4
4
 
5
5
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -102,40 +102,19 @@ var CanopyGuard = class {
102
102
  * Every branch denies — nothing here can let a request through — but they say
103
103
  * different things, and the difference is what makes an incident diagnosable.
104
104
  * Reporting a rejected API key as "temporarily unavailable" invites a retry
105
- * that will never succeed and hides a misconfiguration behind an outage.
105
+ * that will never succeed and hides a misconfiguration behind an outage. The
106
+ * mapping is `authorizationErrorStatus` in `@canopy-io/node`, so an
107
+ * integration outside NestJS answers the same way.
106
108
  */
107
109
  toHttpException(error) {
108
- if (isCanopyAuthorizerError(error)) {
109
- return new InternalServerErrorException(
110
- `Authorization is misconfigured: ${error.message}`
111
- );
112
- }
113
- if (!isCanopyError(error)) {
114
- return new ServiceUnavailableException(
115
- "Authorization is temporarily unavailable."
116
- );
117
- }
118
- if (error.isAuthFailure) {
119
- return new InternalServerErrorException(
120
- "Authorization is misconfigured. Check the Canopy API key and its scopes."
121
- );
122
- }
123
- if (error.statusCode === 404 && error.code === null) {
124
- return new InternalServerErrorException(
125
- `Authorization is misconfigured: ${error.request.method} ${error.request.path} is not served by this Canopy. The SDK requires an API that exposes the identity grants endpoint.`
126
- );
127
- }
128
- if (error.statusCode === 404) {
129
- return new ForbiddenException("Could not evaluate the permission.");
110
+ const { status, message } = authorizationErrorStatus(error);
111
+ if (status === 403) {
112
+ return new ForbiddenException(message);
130
113
  }
131
- if (error.isRateLimited || error.statusCode >= 500) {
132
- return new ServiceUnavailableException(
133
- "Authorization is temporarily unavailable."
134
- );
114
+ if (status === 503) {
115
+ return new ServiceUnavailableException(message);
135
116
  }
136
- return new InternalServerErrorException(
137
- "Authorization could not be evaluated."
138
- );
117
+ return new InternalServerErrorException(message);
139
118
  }
140
119
  buildQuery(requirement, identityId, request) {
141
120
  if (requirement.scope === "app_wide") {
@@ -243,18 +222,14 @@ var CanopyTokenGuard = class {
243
222
  try {
244
223
  claims = await this.verifier.verify(token);
245
224
  } catch (error) {
246
- if (isCanopyTokenError(error) && error.code === "token.jwks_unavailable") {
247
- throw new ServiceUnavailableException(
248
- "Authentication is temporarily unavailable.",
249
- { cause: error }
250
- );
225
+ if (!isCanopyTokenError(error)) {
226
+ throw error;
251
227
  }
252
- if (isCanopyTokenError(error)) {
253
- throw new UnauthorizedException("Invalid or expired access token.", {
254
- cause: error
255
- });
228
+ const { status, message } = tokenErrorStatus(error);
229
+ if (status === 503) {
230
+ throw new ServiceUnavailableException(message, { cause: error });
256
231
  }
257
- throw error;
232
+ throw new UnauthorizedException(message, { cause: error });
258
233
  }
259
234
  request[this.options.attachTokenAs ?? "canopyToken"] = claims;
260
235
  return true;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/tokens.ts","../src/canopy.guard.ts","../src/canopy-token.guard.ts","../src/canopy.module.ts","../src/inject-canopy.decorator.ts","../src/require-permission.decorator.ts"],"names":["ServiceUnavailableException","Injectable","Inject","TokenVerifier"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Injection tokens.\n *\n * These are strings rather than classes because esbuild does not emit\n * `design:paramtypes`, so nothing here can be injected by constructor type\n * alone — every dependency is named with an explicit `@Inject`.\n */\n\n/** The resolved `CanopyModuleOptions`. */\nexport const CANOPY_OPTIONS = \"CANOPY_OPTIONS\";\n\n/** The configured `Canopy` client. */\nexport const CANOPY_CLIENT = \"CANOPY_CLIENT\";\n\n/**\n * The configured `LocalAuthorizer`.\n *\n * Shared deliberately: it holds one copy of the hierarchy for the whole\n * process, so a second instance would double both the memory and the traffic\n * it exists to avoid.\n */\nexport const CANOPY_AUTHORIZER = \"CANOPY_AUTHORIZER\";\n\n/** The configured `TokenVerifier`, shared so its key cache is shared too. */\nexport const CANOPY_TOKEN_VERIFIER = \"CANOPY_TOKEN_VERIFIER\";\n\n/** Metadata key holding what `@RequirePermission` recorded on a handler. */\nexport const CANOPY_PERMISSION = \"CANOPY_PERMISSION\";\n","import {\n type CanActivate,\n type ExecutionContext,\n ForbiddenException,\n Inject,\n Injectable,\n InternalServerErrorException,\n Logger,\n ServiceUnavailableException,\n} from \"@nestjs/common\";\nimport { Reflector } from \"@nestjs/core\";\n\nimport {\n isCanopyAuthorizerError,\n isCanopyError,\n type LocalAuthorizer,\n type RequestBody,\n} from \"@canopy-io/node\";\n\nimport type { CanopyModuleOptions } from \"./options.js\";\nimport type { PermissionRequirement } from \"./require-permission.decorator.js\";\nimport {\n CANOPY_AUTHORIZER,\n CANOPY_OPTIONS,\n CANOPY_PERMISSION,\n} from \"./tokens.js\";\n\n/** Read off the generated operation, so it follows the API rather than restating it. */\ntype EvaluateQuery = RequestBody<\"ApiPermissionsController_evaluate\">;\n\n/**\n * Only the verdict, not the API's full explain payload.\n *\n * A decision reached in-process cannot report `granting_roles` or an effective\n * node — that reasoning lives on the server. The guard has never needed it, and\n * typing the narrow thing keeps that honest rather than implying a richer\n * answer than exists.\n */\ntype Verdict = { allowed: boolean };\n\ninterface Cancellation {\n signal: AbortSignal;\n dispose: () => void;\n}\n\n/** The Node stream underneath an Express or Fastify response. */\ninterface ClosableResponse {\n once: (event: string, listener: () => void) => unknown;\n removeListener: (event: string, listener: () => void) => unknown;\n writableEnded?: boolean;\n raw?: ClosableResponse;\n}\n\nfunction isClosable(value: unknown): value is ClosableResponse {\n return (\n typeof value === \"object\" &&\n value !== null &&\n typeof (value as ClosableResponse).once === \"function\" &&\n typeof (value as ClosableResponse).removeListener === \"function\"\n );\n}\n\n/**\n * Find the Node response stream behind whatever the adapter hands back.\n *\n * `.raw` is checked first and on its own merits, because the object Nest\n * returns is not always the stream. Express returns the `ServerResponse`\n * itself, which listens; Fastify returns a `Reply`, which is a plain object\n * with the real response on `.raw`. Testing the outer object first and\n * returning early on failure would skip `.raw` in exactly the case it exists\n * for.\n */\nfunction resolveStream(response: unknown): ClosableResponse | undefined {\n const raw = (response as { raw?: unknown } | null | undefined)?.raw;\n\n if (isClosable(raw)) {\n return raw;\n }\n\n if (isClosable(response)) {\n return response;\n }\n\n return undefined;\n}\n\n/**\n * Enforces `@RequirePermission` by asking Canopy, once, per guarded request.\n *\n * The guard fails closed at every step. An unresolvable identity, a `node`\n * check with no node, a denial, or an unreachable API all end the request —\n * there is no path through this class that allows a request whose decision is\n * unknown. That is the property worth protecting when reading this code: an\n * `allow` must be something Canopy actually said.\n *\n * A route with no requirement is allowed through untouched, so the guard can be\n * applied globally and opted into per route. Registered that way it runs before\n * controller- and route-scoped guards, so whatever authenticates the request has\n * to be registered globally ahead of it — otherwise `resolveIdentity` runs\n * before there is anything to resolve and every guarded route denies.\n */\n@Injectable()\nexport class CanopyGuard implements CanActivate {\n private readonly logger = new Logger(CanopyGuard.name);\n\n constructor(\n @Inject(Reflector) private readonly reflector: Reflector,\n @Inject(CANOPY_AUTHORIZER) private readonly authorizer: LocalAuthorizer,\n @Inject(CANOPY_OPTIONS)\n private readonly options: CanopyModuleOptions<unknown>,\n ) {}\n\n async canActivate(context: ExecutionContext): Promise<boolean> {\n const requirement = this.reflector.getAllAndOverride<\n PermissionRequirement | undefined\n >(CANOPY_PERMISSION, [context.getHandler(), context.getClass()]);\n\n if (!requirement) {\n return true;\n }\n\n const http = context.switchToHttp();\n const request: unknown = http.getRequest();\n const identityId = resolveSafely(this.options.resolveIdentity, request);\n\n if (!identityId) {\n throw new ForbiddenException(\n \"No identity on the request. Canopy cannot evaluate a permission without one.\",\n );\n }\n\n const decision = await this.evaluate(\n requirement,\n identityId,\n request,\n watchDisconnect(http),\n );\n\n if (!decision.allowed) {\n throw new ForbiddenException(\n `Identity does not hold ${requirement.permission}.`,\n );\n }\n\n return true;\n }\n\n /**\n * Asks Canopy the question the decorator recorded.\n *\n * Every failure is converted into a thrown exception rather than a `false`\n * decision, so a caller cannot mistake \"we could not tell\" for \"denied by\n * policy\" — and neither outcome lets the request proceed.\n */\n private async evaluate(\n requirement: PermissionRequirement,\n identityId: string,\n request: unknown,\n cancellation: Cancellation | undefined,\n ): Promise<Verdict> {\n const query = this.buildQuery(requirement, identityId, request);\n\n try {\n // Answered in-process. The authorizer holds the identity's grant roots\n // and a shared copy of the hierarchy, so the common case reaches no\n // network at all; only a cold or expired entry does, and its deadline\n // and retries are bounded by the same options as before.\n return await this.authorizer.evaluate(\n query,\n cancellation ? { signal: cancellation.signal } : {},\n );\n } catch (error) {\n // The caller hung up mid-check. There is no one left to answer, and this\n // is not Canopy failing — so it must not be logged or reported as though\n // it were. Rethrowing the abort ends the request without inventing a\n // decision.\n if (cancellation?.signal.aborted) {\n throw error;\n }\n\n // Deliberately not re-thrown as-is: a failure here must not reach the\n // client as though Canopy had answered.\n this.logger.error(\n `Could not evaluate ${requirement.permission} for ${identityId}`,\n error instanceof Error ? error.stack : String(error),\n );\n\n throw this.toHttpException(error);\n } finally {\n cancellation?.dispose();\n }\n }\n\n /**\n * Turn a failed check into the status that describes it.\n *\n * Every branch denies — nothing here can let a request through — but they say\n * different things, and the difference is what makes an incident diagnosable.\n * Reporting a rejected API key as \"temporarily unavailable\" invites a retry\n * that will never succeed and hides a misconfiguration behind an outage.\n */\n private toHttpException(error: unknown): Error {\n // The authorizer could not see far enough to decide — a credential that\n // cannot read the hierarchy, most likely. Not an outage: no retry fixes a\n // missing scope, and reporting it as one would bury a misconfiguration\n // under what looks like a blip.\n if (isCanopyAuthorizerError(error)) {\n return new InternalServerErrorException(\n `Authorization is misconfigured: ${error.message}`,\n );\n }\n\n if (!isCanopyError(error)) {\n // Never answered: unreachable, reset, or past the deadline.\n return new ServiceUnavailableException(\n \"Authorization is temporarily unavailable.\",\n );\n }\n\n // Canopy rejected our own credential. No retry fixes this.\n if (error.isAuthFailure) {\n return new InternalServerErrorException(\n \"Authorization is misconfigured. Check the Canopy API key and its scopes.\",\n );\n }\n\n // A 404 with no error code is Nest's own \"no such route\", which here means\n // this Canopy does not serve the endpoint the authorizer reads — an SDK\n // newer than the API it is pointed at. Denying would be catastrophic and\n // silent: every guarded route in the application would answer 403 with\n // nothing to suggest the cause is a version mismatch rather than policy.\n if (error.statusCode === 404 && error.code === null) {\n return new InternalServerErrorException(\n `Authorization is misconfigured: ${error.request.method} ${error.request.path} is not served by this Canopy. ` +\n \"The SDK requires an API that exposes the identity grants endpoint.\",\n );\n }\n\n // A 404 that does carry a code is a real answer — the identity is not in\n // this Environment. Deny rather than 500, the guard's whole thesis being\n // that an undecidable request does not proceed, but say nothing about what\n // was missing.\n if (error.statusCode === 404) {\n return new ForbiddenException(\"Could not evaluate the permission.\");\n }\n\n if (error.isRateLimited || error.statusCode >= 500) {\n return new ServiceUnavailableException(\n \"Authorization is temporarily unavailable.\",\n );\n }\n\n // A 4xx we did not anticipate is a malformed question — our bug, not a\n // transient condition.\n return new InternalServerErrorException(\n \"Authorization could not be evaluated.\",\n );\n }\n\n private buildQuery(\n requirement: PermissionRequirement,\n identityId: string,\n request: unknown,\n ): EvaluateQuery {\n if (requirement.scope === \"app_wide\") {\n return {\n identity_id: identityId,\n permission: requirement.permission,\n scope: \"app_wide\",\n };\n }\n\n // An organization is a hierarchy node, and a membership is a role\n // assignment at it — so the org question IS the node question, asked at\n // the node the caller's token says it is acting in.\n if (requirement.scope === \"org\") {\n const orgId = this.resolveOrg(request);\n\n if (!orgId) {\n throw new ForbiddenException(\n `No organization on the request. ${requirement.permission} is checked inside the caller's active organization, and this caller is not acting in one.`,\n );\n }\n\n return {\n identity_id: identityId,\n permission: requirement.permission,\n scope: \"node\",\n node_id: orgId,\n };\n }\n\n const nodeId = this.options.resolveNode\n ? resolveSafely(this.options.resolveNode, request)\n : undefined;\n\n if (!nodeId) {\n throw new ForbiddenException(\n `No node on the request. ${requirement.permission} is checked at a node; configure resolveNode, or declare the route as app_wide if that is genuinely the question.`,\n );\n }\n\n return {\n identity_id: identityId,\n permission: requirement.permission,\n scope: \"node\",\n node_id: nodeId,\n };\n }\n\n /**\n * The organization the request acts in: `resolveOrg` when configured,\n * otherwise the `org_id` claim off the verified token `CanopyTokenGuard`\n * attached. Both sources trace back to a verified token — the org id\n * decides whose grants answer the check, so it must never be readable off\n * anything the caller controls directly.\n */\n private resolveOrg(request: unknown): string | null | undefined {\n if (this.options.resolveOrg) {\n return resolveSafely(this.options.resolveOrg, request);\n }\n\n const attachedAs = this.options.attachTokenAs ?? \"canopyToken\";\n const claims = (request as Record<string, unknown> | null | undefined)?.[\n attachedAs\n ];\n\n const orgId = (claims as { org_id?: unknown } | undefined)?.org_id;\n\n return typeof orgId === \"string\" && orgId !== \"\" ? orgId : undefined;\n }\n}\n\n/**\n * Run a resolver, treating a throw as \"could not resolve\".\n *\n * Resolvers reach into the request — `request.user.sub` is the shape the docs\n * suggest and the one most applications write. On an unauthenticated request\n * `request.user` is undefined and that expression throws, which without this\n * would surface as a 500. An identity that cannot be established is exactly the\n * request that must be denied, so it becomes the same 403 as returning nothing.\n */\nfunction resolveSafely(\n resolve: (request: never) => string | null | undefined,\n request: unknown,\n): string | null | undefined {\n try {\n return (resolve as (request: unknown) => string | null | undefined)(\n request,\n );\n } catch {\n return undefined;\n }\n}\n\n/**\n * Abort the permission check if the caller hangs up while it is in flight.\n *\n * Watches the *response*, not the request. A request stream emits `close` on\n * every normal request once its body has been read, so listening there would\n * abort perfectly healthy checks; a response emits `close` before\n * `writableEnded` only when nothing was ever sent — which is exactly the\n * disconnect case. The `writableEnded` re-check at fire time keeps that true\n * even if the ordering differs by adapter.\n *\n * Returns undefined for a transport with no such response — a GraphQL or\n * microservice context — where the check simply runs to its deadline.\n */\nfunction watchDisconnect(http: {\n getResponse?: () => unknown;\n}): Cancellation | undefined {\n const raw = resolveStream(http.getResponse?.());\n\n if (!raw) {\n return undefined;\n }\n\n const controller = new AbortController();\n\n const onClose = () => {\n if (raw.writableEnded === true) {\n return;\n }\n\n controller.abort(\n new DOMException(\"The client closed the request\", \"AbortError\"),\n );\n };\n\n raw.once(\"close\", onClose);\n\n return {\n signal: controller.signal,\n dispose: () => raw.removeListener(\"close\", onClose),\n };\n}\n","import {\n type CanActivate,\n type ExecutionContext,\n Inject,\n Injectable,\n ServiceUnavailableException,\n UnauthorizedException,\n} from \"@nestjs/common\";\n\nimport {\n type CanopyTokenClaims,\n isCanopyTokenError,\n TokenVerifier,\n} from \"@canopy-io/node\";\n\nimport type { CanopyModuleOptions } from \"./options.js\";\nimport { CANOPY_OPTIONS, CANOPY_TOKEN_VERIFIER } from \"./tokens.js\";\n\n/**\n * Verifies the bearer token and attaches its claims to the request.\n *\n * This is the authentication half, and it is opt-in on purpose. `CanopyGuard`\n * answers \"may this identity do this?\"; something has to have established\n * *which* identity first, and `resolveIdentity` says as much — \"usually a\n * claim off the verified access token your auth layer already attached\".\n *\n * Use this guard when Canopy issues your tokens and you have no auth layer\n * yet. Skip it entirely if you already run Passport or your own JWT\n * middleware — it is additive, and nothing else in the package depends on it.\n *\n * ```ts\n * @UseGuards(CanopyTokenGuard, CanopyGuard)\n * @RequirePermission(\"documents.read\")\n * findAll() {}\n * ```\n *\n * Order matters: this must run first, because `CanopyGuard` reads what it\n * attaches. Nest runs guards left to right, so listing them in that order is\n * the whole of the wiring.\n *\n * Claims land on `request.canopyToken` by default, which is why\n * `resolveIdentity` is usually `(req) => req.canopyToken?.sub`. The property\n * is configurable so it cannot collide with a `request.user` an existing\n * middleware already owns.\n */\n@Injectable()\nexport class CanopyTokenGuard implements CanActivate {\n constructor(\n @Inject(CANOPY_TOKEN_VERIFIER) private readonly verifier: TokenVerifier,\n @Inject(CANOPY_OPTIONS)\n private readonly options: CanopyModuleOptions<unknown>,\n ) {}\n\n async canActivate(context: ExecutionContext): Promise<boolean> {\n const request = context.switchToHttp().getRequest<{\n headers?: Record<string, string | string[] | undefined>;\n [key: string]: unknown;\n }>();\n\n const token = readBearerToken(request.headers?.[\"authorization\"]);\n\n if (token === null) {\n throw new UnauthorizedException(\n \"Missing bearer token in the Authorization header.\",\n );\n }\n\n let claims: CanopyTokenClaims;\n\n try {\n claims = await this.verifier.verify(token);\n } catch (error) {\n // Not being able to verify is not the same as failing to verify. When\n // the key set cannot be read the caller's token may be perfectly good —\n // we simply cannot say — so this is ours to own, exactly as the\n // permission guard answers 503 rather than denying on an undecidable\n // check.\n //\n // Reporting it as 401 would be worse than merely inaccurate: a client\n // reads 401 as \"your token is stale\", discards a valid one and asks for\n // another, which aims a refresh storm at the issuer that is already\n // down. And an outage that only ever emits 4xx is invisible to anything\n // watching 5xx.\n if (\n isCanopyTokenError(error) &&\n error.code === \"token.jwks_unavailable\"\n ) {\n throw new ServiceUnavailableException(\n \"Authentication is temporarily unavailable.\",\n { cause: error },\n );\n }\n\n // Every other cause is the token itself, and a 401 whatever the detail:\n // the caller cannot fix a bad signature by being granted something. The\n // specific code stays on the error for logs rather than going to the\n // caller, who has no use for the difference and should not be told which\n // check failed.\n if (isCanopyTokenError(error)) {\n throw new UnauthorizedException(\"Invalid or expired access token.\", {\n cause: error,\n });\n }\n\n throw error;\n }\n\n request[this.options.attachTokenAs ?? \"canopyToken\"] = claims;\n\n return true;\n }\n}\n\n/**\n * The token out of an `Authorization: Bearer …` header.\n *\n * Returns null rather than throwing so the caller owns the message. The scheme\n * is matched case-insensitively because RFC 7235 says it is case-insensitive,\n * and clients do send `bearer`.\n */\nfunction readBearerToken(header: string | string[] | undefined): string | null {\n const value = Array.isArray(header) ? header[0] : header;\n\n if (typeof value !== \"string\") {\n return null;\n }\n\n // Split by hand rather than with `/^bearer\\s+(.+)$/i`: the header is the\n // caller's to write, and that pattern backtracks on a long run of spaces.\n const trimmed = value.trim();\n const scheme = trimmed.slice(0, 6);\n const separator = trimmed.charAt(6);\n\n if (scheme.toLowerCase() !== \"bearer\" || !/\\s/.test(separator)) {\n return null;\n }\n\n const token = trimmed.slice(7).trim();\n\n return token.length > 0 ? token : null;\n}\n","import { type DynamicModule, Module, type Provider } from \"@nestjs/common\";\n\nimport { Canopy, LocalAuthorizer, TokenVerifier } from \"@canopy-io/node\";\n\nimport { CanopyGuard } from \"./canopy.guard.js\";\nimport { CanopyTokenGuard } from \"./canopy-token.guard.js\";\nimport type {\n CanopyModuleAsyncOptions,\n CanopyModuleOptions,\n} from \"./options.js\";\nimport {\n CANOPY_AUTHORIZER,\n CANOPY_CLIENT,\n CANOPY_OPTIONS,\n CANOPY_TOKEN_VERIFIER,\n} from \"./tokens.js\";\n\n/**\n * Wires one configured `Canopy` client and the guard that uses it into a Nest\n * application.\n *\n * ```ts\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 *\n * The client is a singleton because it holds no per-request state — it is a\n * `fetch` wrapper with credentials — so there is nothing to gain from building\n * one per request and a connection pool to lose.\n */\n\n/**\n * Per-attempt deadline for a cache miss on the request path.\n *\n * Five seconds rather than the client's 30: this call sits on the request path,\n * so its deadline is time an inbound request spends waiting. An authorization\n * service that has not answered in five seconds is not going to save the\n * request.\n */\nconst DEFAULT_EVALUATE_TIMEOUT_MS = 5_000;\n\n/**\n * Retries for a cache miss on the request path.\n *\n * One, not the client's two. A deadline bounds an attempt; this bounds how many\n * of them an inbound request can wait through. One retry still absorbs a\n * transient blip, and caps the worst case at roughly two deadlines rather than\n * three.\n */\nconst DEFAULT_EVALUATE_MAX_RETRIES = 1;\n\n/**\n * The authorizer both registration paths build.\n *\n * The guard's `evaluateTimeoutMs` / `evaluateMaxRetries` still apply, but to\n * far fewer calls: they bound a cache miss rather than every request.\n */\nfunction buildAuthorizer<TRequest>(\n client: Canopy,\n options: CanopyModuleOptions<TRequest>,\n): LocalAuthorizer {\n return new LocalAuthorizer(client.client, {\n ...(options.authorizationTtlMs === undefined\n ? {}\n : { ttlMs: options.authorizationTtlMs }),\n timeoutMs: options.evaluateTimeoutMs ?? DEFAULT_EVALUATE_TIMEOUT_MS,\n maxRetries: options.evaluateMaxRetries ?? DEFAULT_EVALUATE_MAX_RETRIES,\n });\n}\n\n@Module({})\nexport class CanopyModule {\n static forRoot<TRequest = unknown>(\n options: CanopyModuleOptions<TRequest>,\n ): DynamicModule {\n const providers: Provider[] = [\n { provide: CANOPY_OPTIONS, useValue: options },\n {\n provide: CANOPY_CLIENT,\n useFactory: () => new Canopy(options),\n },\n {\n provide: CANOPY_TOKEN_VERIFIER,\n useFactory: () => new TokenVerifier(options.verify),\n },\n {\n provide: CANOPY_AUTHORIZER,\n useFactory: (client: Canopy) => buildAuthorizer(client, options),\n inject: [CANOPY_CLIENT],\n },\n CanopyGuard,\n CanopyTokenGuard,\n ];\n\n return {\n module: CanopyModule,\n global: options.isGlobal ?? false,\n providers,\n exports: [\n CANOPY_CLIENT,\n CANOPY_OPTIONS,\n CANOPY_TOKEN_VERIFIER,\n CANOPY_AUTHORIZER,\n CanopyGuard,\n CanopyTokenGuard,\n ],\n };\n }\n\n /**\n * The same wiring when the API key comes from something injectable, such as\n * `ConfigService`, rather than being available at module-definition time.\n */\n static forRootAsync<TRequest = unknown>(\n options: CanopyModuleAsyncOptions<TRequest>,\n ): DynamicModule {\n const providers: Provider[] = [\n {\n provide: CANOPY_OPTIONS,\n useFactory: options.useFactory,\n inject: options.inject ?? [],\n },\n {\n provide: CANOPY_CLIENT,\n useFactory: (resolved: CanopyModuleOptions<TRequest>) =>\n new Canopy(resolved),\n inject: [CANOPY_OPTIONS],\n },\n {\n provide: CANOPY_TOKEN_VERIFIER,\n useFactory: (resolved: CanopyModuleOptions<TRequest>) =>\n new TokenVerifier(resolved.verify),\n inject: [CANOPY_OPTIONS],\n },\n {\n provide: CANOPY_AUTHORIZER,\n useFactory: (client: Canopy, resolved: CanopyModuleOptions<TRequest>) =>\n buildAuthorizer(client, resolved),\n inject: [CANOPY_CLIENT, CANOPY_OPTIONS],\n },\n CanopyGuard,\n CanopyTokenGuard,\n ];\n\n return {\n module: CanopyModule,\n global: options.isGlobal ?? false,\n // Spread rather than assigned: under exactOptionalPropertyTypes an\n // explicit `imports: undefined` is not the same as no `imports` at all.\n ...(options.imports ? { imports: options.imports } : {}),\n providers,\n exports: [\n CANOPY_CLIENT,\n CANOPY_OPTIONS,\n CANOPY_TOKEN_VERIFIER,\n CANOPY_AUTHORIZER,\n CanopyGuard,\n CanopyTokenGuard,\n ],\n };\n }\n}\n","import { Inject } from \"@nestjs/common\";\n\nimport { CANOPY_CLIENT } from \"./tokens.js\";\n\n/**\n * Injects the configured client.\n *\n * ```ts\n * constructor(@InjectCanopy() private readonly canopy: Canopy) {}\n * ```\n *\n * Named rather than resolved by type because this package is bundled with\n * esbuild, which emits no `design:paramtypes` — so type-only injection would\n * fail at runtime with a confusing \"cannot resolve dependency\" rather than at\n * build time.\n */\nexport const InjectCanopy = (): ParameterDecorator => Inject(CANOPY_CLIENT);\n","import { SetMetadata } from \"@nestjs/common\";\n\nimport { CANOPY_PERMISSION } from \"./tokens.js\";\n\n/**\n * Which question the guard asks Canopy.\n *\n * `node` walks the node's lineage, so a role granted on an ancestor is\n * inherited at the node being touched. `app_wide` asks only whether the\n * identity holds the permission anywhere in the Environment. `org` is the\n * node question asked at the organization the caller's token is acting in —\n * the `org_id` claim — so the route needs no `resolveNode` and no node in\n * its path.\n */\nexport type PermissionScope = \"node\" | \"app_wide\" | \"org\";\n\n/** What `@RequirePermission` records for the guard to read back. */\nexport interface PermissionRequirement {\n readonly permission: string;\n readonly scope: PermissionScope;\n}\n\nexport interface RequirePermissionOptions {\n /**\n * Defaults to `node`.\n *\n * `app_wide` is opt-in on purpose. It answers a coarser question and returns\n * no effective node, which makes it right for deciding whether to render a\n * menu item and wrong for guarding a resource that belongs to one — a\n * distinction that is easy to get backwards if the looser check is the\n * default.\n *\n * `org` evaluates at the organization on the caller's verified token\n * (`org_id`), read from the claims `CanopyTokenGuard` attaches or from\n * `resolveOrg` if configured. A caller acting in no organization is denied\n * — an org-scoped route has no meaning outside one.\n */\n readonly scope?: PermissionScope;\n}\n\n/**\n * Require a Canopy permission before a route runs.\n *\n * ```ts\n * @UseGuards(CanopyGuard)\n * @RequirePermission(\"orders.refund\")\n * @Post(\":orderId/refund\")\n * refund() {}\n * ```\n *\n * Applies to a single handler or to a whole controller; a handler's own\n * requirement wins over its controller's.\n */\nexport const RequirePermission = (\n permission: string,\n options: RequirePermissionOptions = {},\n): MethodDecorator & ClassDecorator =>\n SetMetadata<string, PermissionRequirement>(CANOPY_PERMISSION, {\n permission,\n scope: options.scope ?? \"node\",\n });\n"]}
1
+ {"version":3,"sources":["../src/tokens.ts","../src/canopy.guard.ts","../src/canopy-token.guard.ts","../src/canopy.module.ts","../src/inject-canopy.decorator.ts","../src/require-permission.decorator.ts"],"names":["ServiceUnavailableException","Injectable","Inject","TokenVerifier"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Injection tokens.\n *\n * These are strings rather than classes because esbuild does not emit\n * `design:paramtypes`, so nothing here can be injected by constructor type\n * alone — every dependency is named with an explicit `@Inject`.\n */\n\n/** The resolved `CanopyModuleOptions`. */\nexport const CANOPY_OPTIONS = \"CANOPY_OPTIONS\";\n\n/** The configured `Canopy` client. */\nexport const CANOPY_CLIENT = \"CANOPY_CLIENT\";\n\n/**\n * The configured `LocalAuthorizer`.\n *\n * Shared deliberately: it holds one copy of the hierarchy for the whole\n * process, so a second instance would double both the memory and the traffic\n * it exists to avoid.\n */\nexport const CANOPY_AUTHORIZER = \"CANOPY_AUTHORIZER\";\n\n/** The configured `TokenVerifier`, shared so its key cache is shared too. */\nexport const CANOPY_TOKEN_VERIFIER = \"CANOPY_TOKEN_VERIFIER\";\n\n/** Metadata key holding what `@RequirePermission` recorded on a handler. */\nexport const CANOPY_PERMISSION = \"CANOPY_PERMISSION\";\n","import {\n type CanActivate,\n type ExecutionContext,\n ForbiddenException,\n Inject,\n Injectable,\n InternalServerErrorException,\n Logger,\n ServiceUnavailableException,\n} from \"@nestjs/common\";\nimport { Reflector } from \"@nestjs/core\";\n\nimport {\n authorizationErrorStatus,\n type LocalAuthorizer,\n type RequestBody,\n} from \"@canopy-io/node\";\n\nimport type { CanopyModuleOptions } from \"./options.js\";\nimport type { PermissionRequirement } from \"./require-permission.decorator.js\";\nimport {\n CANOPY_AUTHORIZER,\n CANOPY_OPTIONS,\n CANOPY_PERMISSION,\n} from \"./tokens.js\";\n\n/** Read off the generated operation, so it follows the API rather than restating it. */\ntype EvaluateQuery = RequestBody<\"ApiPermissionsController_evaluate\">;\n\n/**\n * Only the verdict, not the API's full explain payload.\n *\n * A decision reached in-process cannot report `granting_roles` or an effective\n * node — that reasoning lives on the server. The guard has never needed it, and\n * typing the narrow thing keeps that honest rather than implying a richer\n * answer than exists.\n */\ntype Verdict = { allowed: boolean };\n\ninterface Cancellation {\n signal: AbortSignal;\n dispose: () => void;\n}\n\n/** The Node stream underneath an Express or Fastify response. */\ninterface ClosableResponse {\n once: (event: string, listener: () => void) => unknown;\n removeListener: (event: string, listener: () => void) => unknown;\n writableEnded?: boolean;\n raw?: ClosableResponse;\n}\n\nfunction isClosable(value: unknown): value is ClosableResponse {\n return (\n typeof value === \"object\" &&\n value !== null &&\n typeof (value as ClosableResponse).once === \"function\" &&\n typeof (value as ClosableResponse).removeListener === \"function\"\n );\n}\n\n/**\n * Find the Node response stream behind whatever the adapter hands back.\n *\n * `.raw` is checked first and on its own merits, because the object Nest\n * returns is not always the stream. Express returns the `ServerResponse`\n * itself, which listens; Fastify returns a `Reply`, which is a plain object\n * with the real response on `.raw`. Testing the outer object first and\n * returning early on failure would skip `.raw` in exactly the case it exists\n * for.\n */\nfunction resolveStream(response: unknown): ClosableResponse | undefined {\n const raw = (response as { raw?: unknown } | null | undefined)?.raw;\n\n if (isClosable(raw)) {\n return raw;\n }\n\n if (isClosable(response)) {\n return response;\n }\n\n return undefined;\n}\n\n/**\n * Enforces `@RequirePermission` by asking Canopy, once, per guarded request.\n *\n * The guard fails closed at every step. An unresolvable identity, a `node`\n * check with no node, a denial, or an unreachable API all end the request —\n * there is no path through this class that allows a request whose decision is\n * unknown. That is the property worth protecting when reading this code: an\n * `allow` must be something Canopy actually said.\n *\n * A route with no requirement is allowed through untouched, so the guard can be\n * applied globally and opted into per route. Registered that way it runs before\n * controller- and route-scoped guards, so whatever authenticates the request has\n * to be registered globally ahead of it — otherwise `resolveIdentity` runs\n * before there is anything to resolve and every guarded route denies.\n */\n@Injectable()\nexport class CanopyGuard implements CanActivate {\n private readonly logger = new Logger(CanopyGuard.name);\n\n constructor(\n @Inject(Reflector) private readonly reflector: Reflector,\n @Inject(CANOPY_AUTHORIZER) private readonly authorizer: LocalAuthorizer,\n @Inject(CANOPY_OPTIONS)\n private readonly options: CanopyModuleOptions<unknown>,\n ) {}\n\n async canActivate(context: ExecutionContext): Promise<boolean> {\n const requirement = this.reflector.getAllAndOverride<\n PermissionRequirement | undefined\n >(CANOPY_PERMISSION, [context.getHandler(), context.getClass()]);\n\n if (!requirement) {\n return true;\n }\n\n const http = context.switchToHttp();\n const request: unknown = http.getRequest();\n const identityId = resolveSafely(this.options.resolveIdentity, request);\n\n if (!identityId) {\n throw new ForbiddenException(\n \"No identity on the request. Canopy cannot evaluate a permission without one.\",\n );\n }\n\n const decision = await this.evaluate(\n requirement,\n identityId,\n request,\n watchDisconnect(http),\n );\n\n if (!decision.allowed) {\n throw new ForbiddenException(\n `Identity does not hold ${requirement.permission}.`,\n );\n }\n\n return true;\n }\n\n /**\n * Asks Canopy the question the decorator recorded.\n *\n * Every failure is converted into a thrown exception rather than a `false`\n * decision, so a caller cannot mistake \"we could not tell\" for \"denied by\n * policy\" — and neither outcome lets the request proceed.\n */\n private async evaluate(\n requirement: PermissionRequirement,\n identityId: string,\n request: unknown,\n cancellation: Cancellation | undefined,\n ): Promise<Verdict> {\n const query = this.buildQuery(requirement, identityId, request);\n\n try {\n // Answered in-process. The authorizer holds the identity's grant roots\n // and a shared copy of the hierarchy, so the common case reaches no\n // network at all; only a cold or expired entry does, and its deadline\n // and retries are bounded by the same options as before.\n return await this.authorizer.evaluate(\n query,\n cancellation ? { signal: cancellation.signal } : {},\n );\n } catch (error) {\n // The caller hung up mid-check. There is no one left to answer, and this\n // is not Canopy failing — so it must not be logged or reported as though\n // it were. Rethrowing the abort ends the request without inventing a\n // decision.\n if (cancellation?.signal.aborted) {\n throw error;\n }\n\n // Deliberately not re-thrown as-is: a failure here must not reach the\n // client as though Canopy had answered.\n this.logger.error(\n `Could not evaluate ${requirement.permission} for ${identityId}`,\n error instanceof Error ? error.stack : String(error),\n );\n\n throw this.toHttpException(error);\n } finally {\n cancellation?.dispose();\n }\n }\n\n /**\n * Turn a failed check into the status that describes it.\n *\n * Every branch denies — nothing here can let a request through — but they say\n * different things, and the difference is what makes an incident diagnosable.\n * Reporting a rejected API key as \"temporarily unavailable\" invites a retry\n * that will never succeed and hides a misconfiguration behind an outage. The\n * mapping is `authorizationErrorStatus` in `@canopy-io/node`, so an\n * integration outside NestJS answers the same way.\n */\n private toHttpException(error: unknown): Error {\n const { status, message } = authorizationErrorStatus(error);\n\n if (status === 403) {\n return new ForbiddenException(message);\n }\n\n if (status === 503) {\n return new ServiceUnavailableException(message);\n }\n\n return new InternalServerErrorException(message);\n }\n\n private buildQuery(\n requirement: PermissionRequirement,\n identityId: string,\n request: unknown,\n ): EvaluateQuery {\n if (requirement.scope === \"app_wide\") {\n return {\n identity_id: identityId,\n permission: requirement.permission,\n scope: \"app_wide\",\n };\n }\n\n // An organization is a hierarchy node, and a membership is a role\n // assignment at it — so the org question IS the node question, asked at\n // the node the caller's token says it is acting in.\n if (requirement.scope === \"org\") {\n const orgId = this.resolveOrg(request);\n\n if (!orgId) {\n throw new ForbiddenException(\n `No organization on the request. ${requirement.permission} is checked inside the caller's active organization, and this caller is not acting in one.`,\n );\n }\n\n return {\n identity_id: identityId,\n permission: requirement.permission,\n scope: \"node\",\n node_id: orgId,\n };\n }\n\n const nodeId = this.options.resolveNode\n ? resolveSafely(this.options.resolveNode, request)\n : undefined;\n\n if (!nodeId) {\n throw new ForbiddenException(\n `No node on the request. ${requirement.permission} is checked at a node; configure resolveNode, or declare the route as app_wide if that is genuinely the question.`,\n );\n }\n\n return {\n identity_id: identityId,\n permission: requirement.permission,\n scope: \"node\",\n node_id: nodeId,\n };\n }\n\n /**\n * The organization the request acts in: `resolveOrg` when configured,\n * otherwise the `org_id` claim off the verified token `CanopyTokenGuard`\n * attached. Both sources trace back to a verified token — the org id\n * decides whose grants answer the check, so it must never be readable off\n * anything the caller controls directly.\n */\n private resolveOrg(request: unknown): string | null | undefined {\n if (this.options.resolveOrg) {\n return resolveSafely(this.options.resolveOrg, request);\n }\n\n const attachedAs = this.options.attachTokenAs ?? \"canopyToken\";\n const claims = (request as Record<string, unknown> | null | undefined)?.[\n attachedAs\n ];\n\n const orgId = (claims as { org_id?: unknown } | undefined)?.org_id;\n\n return typeof orgId === \"string\" && orgId !== \"\" ? orgId : undefined;\n }\n}\n\n/**\n * Run a resolver, treating a throw as \"could not resolve\".\n *\n * Resolvers reach into the request — `request.user.sub` is the shape the docs\n * suggest and the one most applications write. On an unauthenticated request\n * `request.user` is undefined and that expression throws, which without this\n * would surface as a 500. An identity that cannot be established is exactly the\n * request that must be denied, so it becomes the same 403 as returning nothing.\n */\nfunction resolveSafely(\n resolve: (request: never) => string | null | undefined,\n request: unknown,\n): string | null | undefined {\n try {\n return (resolve as (request: unknown) => string | null | undefined)(\n request,\n );\n } catch {\n return undefined;\n }\n}\n\n/**\n * Abort the permission check if the caller hangs up while it is in flight.\n *\n * Watches the *response*, not the request. A request stream emits `close` on\n * every normal request once its body has been read, so listening there would\n * abort perfectly healthy checks; a response emits `close` before\n * `writableEnded` only when nothing was ever sent — which is exactly the\n * disconnect case. The `writableEnded` re-check at fire time keeps that true\n * even if the ordering differs by adapter.\n *\n * Returns undefined for a transport with no such response — a GraphQL or\n * microservice context — where the check simply runs to its deadline.\n */\nfunction watchDisconnect(http: {\n getResponse?: () => unknown;\n}): Cancellation | undefined {\n const raw = resolveStream(http.getResponse?.());\n\n if (!raw) {\n return undefined;\n }\n\n const controller = new AbortController();\n\n const onClose = () => {\n if (raw.writableEnded === true) {\n return;\n }\n\n controller.abort(\n new DOMException(\"The client closed the request\", \"AbortError\"),\n );\n };\n\n raw.once(\"close\", onClose);\n\n return {\n signal: controller.signal,\n dispose: () => raw.removeListener(\"close\", onClose),\n };\n}\n","import {\n type CanActivate,\n type ExecutionContext,\n Inject,\n Injectable,\n ServiceUnavailableException,\n UnauthorizedException,\n} from \"@nestjs/common\";\n\nimport {\n type CanopyTokenClaims,\n isCanopyTokenError,\n tokenErrorStatus,\n TokenVerifier,\n} from \"@canopy-io/node\";\n\nimport type { CanopyModuleOptions } from \"./options.js\";\nimport { CANOPY_OPTIONS, CANOPY_TOKEN_VERIFIER } from \"./tokens.js\";\n\n/**\n * Verifies the bearer token and attaches its claims to the request.\n *\n * This is the authentication half, and it is opt-in on purpose. `CanopyGuard`\n * answers \"may this identity do this?\"; something has to have established\n * *which* identity first, and `resolveIdentity` says as much — \"usually a\n * claim off the verified access token your auth layer already attached\".\n *\n * Use this guard when Canopy issues your tokens and you have no auth layer\n * yet. Skip it entirely if you already run Passport or your own JWT\n * middleware — it is additive, and nothing else in the package depends on it.\n *\n * ```ts\n * @UseGuards(CanopyTokenGuard, CanopyGuard)\n * @RequirePermission(\"documents.read\")\n * findAll() {}\n * ```\n *\n * Order matters: this must run first, because `CanopyGuard` reads what it\n * attaches. Nest runs guards left to right, so listing them in that order is\n * the whole of the wiring.\n *\n * Claims land on `request.canopyToken` by default, which is why\n * `resolveIdentity` is usually `(req) => req.canopyToken?.sub`. The property\n * is configurable so it cannot collide with a `request.user` an existing\n * middleware already owns.\n */\n@Injectable()\nexport class CanopyTokenGuard implements CanActivate {\n constructor(\n @Inject(CANOPY_TOKEN_VERIFIER) private readonly verifier: TokenVerifier,\n @Inject(CANOPY_OPTIONS)\n private readonly options: CanopyModuleOptions<unknown>,\n ) {}\n\n async canActivate(context: ExecutionContext): Promise<boolean> {\n const request = context.switchToHttp().getRequest<{\n headers?: Record<string, string | string[] | undefined>;\n [key: string]: unknown;\n }>();\n\n const token = readBearerToken(request.headers?.[\"authorization\"]);\n\n if (token === null) {\n throw new UnauthorizedException(\n \"Missing bearer token in the Authorization header.\",\n );\n }\n\n let claims: CanopyTokenClaims;\n\n try {\n claims = await this.verifier.verify(token);\n } catch (error) {\n // Not being able to verify is not the same as failing to verify. When\n // the key set cannot be read the caller's token may be perfectly good —\n // we simply cannot say — so this is ours to own, exactly as the\n // permission guard answers 503 rather than denying on an undecidable\n // check.\n //\n // Reporting it as 401 would be worse than merely inaccurate: a client\n // reads 401 as \"your token is stale\", discards a valid one and asks for\n // another, which aims a refresh storm at the issuer that is already\n // down. And an outage that only ever emits 4xx is invisible to anything\n // watching 5xx.\n if (!isCanopyTokenError(error)) {\n throw error;\n }\n\n const { status, message } = tokenErrorStatus(error);\n\n if (status === 503) {\n throw new ServiceUnavailableException(message, { cause: error });\n }\n\n // Every other cause is the token itself, and a 401 whatever the detail:\n // the caller cannot fix a bad signature by being granted something. The\n // specific code stays on the error for logs rather than going to the\n // caller, who has no use for the difference and should not be told which\n // check failed.\n throw new UnauthorizedException(message, { cause: error });\n }\n\n request[this.options.attachTokenAs ?? \"canopyToken\"] = claims;\n\n return true;\n }\n}\n\n/**\n * The token out of an `Authorization: Bearer …` header.\n *\n * Returns null rather than throwing so the caller owns the message. The scheme\n * is matched case-insensitively because RFC 7235 says it is case-insensitive,\n * and clients do send `bearer`.\n */\nfunction readBearerToken(header: string | string[] | undefined): string | null {\n const value = Array.isArray(header) ? header[0] : header;\n\n if (typeof value !== \"string\") {\n return null;\n }\n\n // Split by hand rather than with `/^bearer\\s+(.+)$/i`: the header is the\n // caller's to write, and that pattern backtracks on a long run of spaces.\n const trimmed = value.trim();\n const scheme = trimmed.slice(0, 6);\n const separator = trimmed.charAt(6);\n\n if (scheme.toLowerCase() !== \"bearer\" || !/\\s/.test(separator)) {\n return null;\n }\n\n const token = trimmed.slice(7).trim();\n\n return token.length > 0 ? token : null;\n}\n","import { type DynamicModule, Module, type Provider } from \"@nestjs/common\";\n\nimport { Canopy, LocalAuthorizer, TokenVerifier } from \"@canopy-io/node\";\n\nimport { CanopyGuard } from \"./canopy.guard.js\";\nimport { CanopyTokenGuard } from \"./canopy-token.guard.js\";\nimport type {\n CanopyModuleAsyncOptions,\n CanopyModuleOptions,\n} from \"./options.js\";\nimport {\n CANOPY_AUTHORIZER,\n CANOPY_CLIENT,\n CANOPY_OPTIONS,\n CANOPY_TOKEN_VERIFIER,\n} from \"./tokens.js\";\n\n/**\n * Wires one configured `Canopy` client and the guard that uses it into a Nest\n * application.\n *\n * ```ts\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 *\n * The client is a singleton because it holds no per-request state — it is a\n * `fetch` wrapper with credentials — so there is nothing to gain from building\n * one per request and a connection pool to lose.\n */\n\n/**\n * Per-attempt deadline for a cache miss on the request path.\n *\n * Five seconds rather than the client's 30: this call sits on the request path,\n * so its deadline is time an inbound request spends waiting. An authorization\n * service that has not answered in five seconds is not going to save the\n * request.\n */\nconst DEFAULT_EVALUATE_TIMEOUT_MS = 5_000;\n\n/**\n * Retries for a cache miss on the request path.\n *\n * One, not the client's two. A deadline bounds an attempt; this bounds how many\n * of them an inbound request can wait through. One retry still absorbs a\n * transient blip, and caps the worst case at roughly two deadlines rather than\n * three.\n */\nconst DEFAULT_EVALUATE_MAX_RETRIES = 1;\n\n/**\n * The authorizer both registration paths build.\n *\n * The guard's `evaluateTimeoutMs` / `evaluateMaxRetries` still apply, but to\n * far fewer calls: they bound a cache miss rather than every request.\n */\nfunction buildAuthorizer<TRequest>(\n client: Canopy,\n options: CanopyModuleOptions<TRequest>,\n): LocalAuthorizer {\n return new LocalAuthorizer(client.client, {\n ...(options.authorizationTtlMs === undefined\n ? {}\n : { ttlMs: options.authorizationTtlMs }),\n timeoutMs: options.evaluateTimeoutMs ?? DEFAULT_EVALUATE_TIMEOUT_MS,\n maxRetries: options.evaluateMaxRetries ?? DEFAULT_EVALUATE_MAX_RETRIES,\n });\n}\n\n@Module({})\nexport class CanopyModule {\n static forRoot<TRequest = unknown>(\n options: CanopyModuleOptions<TRequest>,\n ): DynamicModule {\n const providers: Provider[] = [\n { provide: CANOPY_OPTIONS, useValue: options },\n {\n provide: CANOPY_CLIENT,\n useFactory: () => new Canopy(options),\n },\n {\n provide: CANOPY_TOKEN_VERIFIER,\n useFactory: () => new TokenVerifier(options.verify),\n },\n {\n provide: CANOPY_AUTHORIZER,\n useFactory: (client: Canopy) => buildAuthorizer(client, options),\n inject: [CANOPY_CLIENT],\n },\n CanopyGuard,\n CanopyTokenGuard,\n ];\n\n return {\n module: CanopyModule,\n global: options.isGlobal ?? false,\n providers,\n exports: [\n CANOPY_CLIENT,\n CANOPY_OPTIONS,\n CANOPY_TOKEN_VERIFIER,\n CANOPY_AUTHORIZER,\n CanopyGuard,\n CanopyTokenGuard,\n ],\n };\n }\n\n /**\n * The same wiring when the API key comes from something injectable, such as\n * `ConfigService`, rather than being available at module-definition time.\n */\n static forRootAsync<TRequest = unknown>(\n options: CanopyModuleAsyncOptions<TRequest>,\n ): DynamicModule {\n const providers: Provider[] = [\n {\n provide: CANOPY_OPTIONS,\n useFactory: options.useFactory,\n inject: options.inject ?? [],\n },\n {\n provide: CANOPY_CLIENT,\n useFactory: (resolved: CanopyModuleOptions<TRequest>) =>\n new Canopy(resolved),\n inject: [CANOPY_OPTIONS],\n },\n {\n provide: CANOPY_TOKEN_VERIFIER,\n useFactory: (resolved: CanopyModuleOptions<TRequest>) =>\n new TokenVerifier(resolved.verify),\n inject: [CANOPY_OPTIONS],\n },\n {\n provide: CANOPY_AUTHORIZER,\n useFactory: (client: Canopy, resolved: CanopyModuleOptions<TRequest>) =>\n buildAuthorizer(client, resolved),\n inject: [CANOPY_CLIENT, CANOPY_OPTIONS],\n },\n CanopyGuard,\n CanopyTokenGuard,\n ];\n\n return {\n module: CanopyModule,\n global: options.isGlobal ?? false,\n // Spread rather than assigned: under exactOptionalPropertyTypes an\n // explicit `imports: undefined` is not the same as no `imports` at all.\n ...(options.imports ? { imports: options.imports } : {}),\n providers,\n exports: [\n CANOPY_CLIENT,\n CANOPY_OPTIONS,\n CANOPY_TOKEN_VERIFIER,\n CANOPY_AUTHORIZER,\n CanopyGuard,\n CanopyTokenGuard,\n ],\n };\n }\n}\n","import { Inject } from \"@nestjs/common\";\n\nimport { CANOPY_CLIENT } from \"./tokens.js\";\n\n/**\n * Injects the configured client.\n *\n * ```ts\n * constructor(@InjectCanopy() private readonly canopy: Canopy) {}\n * ```\n *\n * Named rather than resolved by type because this package is bundled with\n * esbuild, which emits no `design:paramtypes` — so type-only injection would\n * fail at runtime with a confusing \"cannot resolve dependency\" rather than at\n * build time.\n */\nexport const InjectCanopy = (): ParameterDecorator => Inject(CANOPY_CLIENT);\n","import { SetMetadata } from \"@nestjs/common\";\n\nimport { CANOPY_PERMISSION } from \"./tokens.js\";\n\n/**\n * Which question the guard asks Canopy.\n *\n * `node` walks the node's lineage, so a role granted on an ancestor is\n * inherited at the node being touched. `app_wide` asks only whether the\n * identity holds the permission anywhere in the Environment. `org` is the\n * node question asked at the organization the caller's token is acting in —\n * the `org_id` claim — so the route needs no `resolveNode` and no node in\n * its path.\n */\nexport type PermissionScope = \"node\" | \"app_wide\" | \"org\";\n\n/** What `@RequirePermission` records for the guard to read back. */\nexport interface PermissionRequirement {\n readonly permission: string;\n readonly scope: PermissionScope;\n}\n\nexport interface RequirePermissionOptions {\n /**\n * Defaults to `node`.\n *\n * `app_wide` is opt-in on purpose. It answers a coarser question and returns\n * no effective node, which makes it right for deciding whether to render a\n * menu item and wrong for guarding a resource that belongs to one — a\n * distinction that is easy to get backwards if the looser check is the\n * default.\n *\n * `org` evaluates at the organization on the caller's verified token\n * (`org_id`), read from the claims `CanopyTokenGuard` attaches or from\n * `resolveOrg` if configured. A caller acting in no organization is denied\n * — an org-scoped route has no meaning outside one.\n */\n readonly scope?: PermissionScope;\n}\n\n/**\n * Require a Canopy permission before a route runs.\n *\n * ```ts\n * @UseGuards(CanopyGuard)\n * @RequirePermission(\"orders.refund\")\n * @Post(\":orderId/refund\")\n * refund() {}\n * ```\n *\n * Applies to a single handler or to a whole controller; a handler's own\n * requirement wins over its controller's.\n */\nexport const RequirePermission = (\n permission: string,\n options: RequirePermissionOptions = {},\n): MethodDecorator & ClassDecorator =>\n SetMetadata<string, PermissionRequirement>(CANOPY_PERMISSION, {\n permission,\n scope: options.scope ?? \"node\",\n });\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canopy-io/nestjs",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "Official NestJS integration for Canopy \u2014 hierarchical identity and access management.",
5
5
  "license": "MIT",
6
6
  "author": "Canopy Identity Inc.",
@@ -62,7 +62,7 @@
62
62
  "test:watch": "vitest"
63
63
  },
64
64
  "dependencies": {
65
- "@canopy-io/node": "^0.7.2"
65
+ "@canopy-io/node": "^0.9.0"
66
66
  },
67
67
  "peerDependencies": {
68
68
  "@nestjs/common": "^10.0.0 || ^11.0.0",