@canopy-io/nestjs 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -12,6 +12,19 @@ npm install @canopy-io/nestjs
12
12
 
13
13
  Requires NestJS 10 or 11 and Node 18 or later. `@canopy-io/node` comes with it.
14
14
 
15
+ ## Features
16
+
17
+ - **One registration.** `CanopyModule.forRoot` for a static key, `forRootAsync` when it comes from something injectable. Pass your own request type and the resolvers are typed against it.
18
+ - **`@RequirePermission` on the route.** The check is a declaration on the handler rather than a call inside it. A route without it passes through untouched, so the guard can be registered globally and opted into.
19
+ - **`CanopyGuard`** answers that declaration **in your own process**, at the node the request names — inheriting a role granted on a parent without you modelling the walk.
20
+ - **No network call per request.** The guard holds the identity's grant roots and one shared copy of your hierarchy, refreshed at most once a minute, so a guarded route normally reaches no network at all.
21
+ - **Two scopes.** `node` is the default and the strict question; `app_wide` asks only whether the identity holds the permission anywhere in the Environment.
22
+ - **`CanopyTokenGuard`** verifies a Canopy-issued bearer token locally against the published signing keys and attaches the claims, for applications with no auth layer in front. Additive — skip it if you already run Passport or your own JWT middleware.
23
+ - **`@InjectCanopy()`** hands you the full `@canopy-io/node` client for everything the guard does not cover.
24
+ - **Fails closed, with the status telling you which failure it was.** `403` for a denial, `503` for an undecidable one, `500` for a broken integration.
25
+ - **A bounded check.** Its own evaluation timeout and retry count, separate from the client-wide ones, with the wait between attempts capped.
26
+ - **Aborts when the caller hangs up**, on both Express and Fastify, rather than finishing a decision no one will read.
27
+
15
28
  ## Register
16
29
 
17
30
  ```ts
@@ -21,8 +34,8 @@ import { CanopyModule } from "@canopy-io/nestjs";
21
34
  imports: [
22
35
  CanopyModule.forRoot<AuthedRequest>({
23
36
  apiKey: process.env.CANOPY_API_KEY,
24
- resolveIdentity: (request) => request.user.sub,
25
- resolveNode: (request) => request.params.orgId,
37
+ resolveIdentity: (request) => request.user?.sub,
38
+ resolveNode: (request) => request.params?.orgId,
26
39
  }),
27
40
  ],
28
41
  })
@@ -39,8 +52,8 @@ CanopyModule.forRootAsync({
39
52
  inject: [ConfigService],
40
53
  useFactory: (config: ConfigService) => ({
41
54
  apiKey: config.getOrThrow("CANOPY_API_KEY"),
42
- resolveIdentity: (request) => request.user.sub,
43
- resolveNode: (request) => request.params.orgId,
55
+ resolveIdentity: (request) => request.user?.sub,
56
+ resolveNode: (request) => request.params?.orgId,
44
57
  }),
45
58
  });
46
59
  ```
@@ -59,9 +72,26 @@ export class OrdersController {
59
72
  }
60
73
  ```
61
74
 
62
- That asks Canopy whether the identity holds `orders.refund` **at** `orgId`, walking the node's lineage — so a role granted on a parent is inherited without you modelling it.
75
+ That answers whether the identity holds `orders.refund` **at** `orgId`, walking the node's lineage — so a role granted on a parent is inherited without you modelling it.
76
+
77
+ The walk happens in your process, not over the network. See [How the decision is reached](#how-the-decision-is-reached).
78
+
79
+ A route with no `@RequirePermission` passes through untouched, so the guard can be registered globally and opted into per route. A requirement on a handler wins over one on its controller.
80
+
81
+ ### If you register it globally
63
82
 
64
- A route with no `@RequirePermission` passes through untouched, so the guard is safe to register globally and opt into per route. A requirement on a handler wins over one on its controller.
83
+ Nest runs global guards **before** controller- and route-scoped ones. So a globally-registered `CanopyGuard` runs before a route-level `AuthGuard` has put anything on the request, and `resolveIdentity` finds nothing — every guarded route answers `403`.
84
+
85
+ Register your authentication guard globally too, ahead of this one:
86
+
87
+ ```ts
88
+ providers: [
89
+ { provide: APP_GUARD, useClass: JwtAuthGuard }, // must come first
90
+ { provide: APP_GUARD, useClass: CanopyGuard },
91
+ ];
92
+ ```
93
+
94
+ Global guards run in registration order. If your authentication is route-level, apply `CanopyGuard` at the route as well — `@UseGuards(JwtAuthGuard, CanopyGuard)` — where left-to-right order holds.
65
95
 
66
96
  ### Scope
67
97
 
@@ -73,6 +103,49 @@ The default is `node`, which is the strict question: does this identity hold the
73
103
 
74
104
  `app_wide` asks only whether the identity holds the permission anywhere in the Environment, and returns no effective node. It is right for deciding whether to show a menu item and wrong for guarding a resource that belongs to a node — which is why it is opt-in rather than the default.
75
105
 
106
+ ## If Canopy issues your tokens
107
+
108
+ `resolveIdentity` assumes something upstream already verified the caller. If
109
+ you have no auth layer yet, `CanopyTokenGuard` is that layer: it verifies the
110
+ bearer token against Canopy's published signing keys and attaches the claims,
111
+ so `resolveIdentity` has something trustworthy to read.
112
+
113
+ ```ts
114
+ CanopyModule.forRoot<AuthedRequest>({
115
+ apiKey: process.env.CANOPY_API_KEY,
116
+ verify: { audience: process.env.CANOPY_OAUTH_CLIENT_ID }, // omit for Direct API
117
+ resolveIdentity: (request) => request.canopyToken?.sub,
118
+ });
119
+ ```
120
+
121
+ ```ts
122
+ @UseGuards(CanopyTokenGuard, CanopyGuard)
123
+ @RequirePermission("documents.read")
124
+ findAll() {}
125
+ ```
126
+
127
+ **Order matters.** Nest runs guards left to right, and `CanopyGuard` reads what
128
+ `CanopyTokenGuard` attaches. Reversed, every request is denied — the identity
129
+ is resolved before anything has established one.
130
+
131
+ Verification is local: one fetch of the key set, then a signature check per
132
+ request with no network at all.
133
+
134
+ A rejected token is a 401 whatever the detail, with the specific code left on
135
+ `error.cause` for your logs rather than handed to the caller — who has no use
136
+ for the difference and should not be told which check failed. The exception is
137
+ `token.jwks_unavailable`, which answers **503**: not being able to verify is not
138
+ the same as failing to verify, the caller's token may be perfectly good, and
139
+ telling a client its token is stale during a key-server outage only aims a
140
+ refresh storm at the thing that is already down.
141
+
142
+ Claims land on `request.canopyToken`, not `request.user` — that one belongs to
143
+ Passport, and quietly overwriting it would be somebody's difficult afternoon.
144
+ Use `attachTokenAs` to put them elsewhere.
145
+
146
+ Skip this guard entirely if you already run Passport or your own JWT
147
+ middleware. It is additive; nothing else in the package depends on it.
148
+
76
149
  ## The client
77
150
 
78
151
  For anything the guard does not cover, inject the client:
@@ -100,12 +173,101 @@ It is the same [`@canopy-io/node`](../node) client, with its retry policy, pagin
100
173
 
101
174
  There is no path through `CanopyGuard` that allows a request whose decision is unknown. Each of these ends the request:
102
175
 
103
- - no identity resolves — `403`
176
+ - no identity resolves, including when a resolver throws on an unauthenticated request — `403`
104
177
  - a `node` check with no node, or with no `resolveNode` configured — `403`
105
- - Canopy answers `allowed: false` — `403`
106
- - Canopy cannot be reached — `503`, deliberately not `403`, because "we could not decide" is not "you are not allowed"
178
+ - the identity holds the permission nowhere, or nowhere on this node's lineage — `403`
179
+ - Canopy has never heard of the identity (`404`) — `403`
180
+ - a cache miss cannot be filled because Canopy is unreachable, rate-limiting, or answers `5xx` — `503`, deliberately not `403`, because "we could not decide" is not "you are not allowed"
181
+ - Canopy rejects the API key (`401`/`403`), or refuses the read (other `4xx`) — `500`, because a misconfiguration is not a temporary condition and no retry will fix it
182
+ - the hierarchy this credential can read does not contain the node — `500`, naming the cause. Undecidable, not denied: answering `403` would take out every node-scoped route while looking like ordinary policy
183
+ - the caller hung up mid-check — the abort is propagated, not turned into a `503`, because nothing failed and no one is waiting for an answer
184
+
185
+ A held answer is never served past its window to paper over an outage. Once it expires, an unfillable read is a `503` — extending it silently would extend the revocation window with it, without anyone choosing to.
186
+
187
+ Everything except the caller hanging up is logged with its cause before the request is refused.
188
+
189
+ The `500` cases are worth alerting on: they mean the integration is broken rather than the service being slow. Reporting them as `503` would bury a bad API key under what looks like an outage.
190
+
191
+ ## How the decision is reached
192
+
193
+ The guard does not ask Canopy per request. It asks a different question, once, and answers every later check from the result.
194
+
195
+ Rather than "may this identity act **here**", it reads "**where** may this identity act" — the nodes each permission was granted at. Those grant roots are not expanded through their descendants, because a grant already means _this node and everything beneath it_; expanding would restate your hierarchy once per identity, and would be largest for the near-root grants your administrators hold.
196
+
197
+ So two things are held, split by what they depend on:
198
+
199
+ - **grant roots, per identity** — small, a handful of assignments
200
+ - **your hierarchy, once per process** — shared by every identity, since its shape does not depend on who is asking
201
+
202
+ 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.
203
+
204
+ **Both are held for at most 60 seconds**, which is therefore the delay between an access change and it taking effect. The hierarchy is revalidated rather than re-read — a conditional request that normally answers `304`, so nothing transfers.
205
+
206
+ ```ts
207
+ CanopyModule.forRoot({
208
+ apiKey: process.env.CANOPY_API_KEY,
209
+ authorizationTtlMs: 30_000,
210
+ resolveIdentity: (request) => request.user?.sub,
211
+ });
212
+ ```
213
+
214
+ Shortening that window makes revocation take effect sooner and costs more calls. Below the gap between a user's requests it stops saving anything at all — every request finds the cache expired and refetches, which is the per-request traffic this exists to remove. Human-paced traffic has multi-second gaps, so a few seconds can cost full price for no benefit.
215
+
216
+ ### What the credential needs
217
+
218
+ A `full_access` API key needs nothing further. A **scoped** key must carry both
219
+ of the permissions the guard reads with, because a scoped key is granted
220
+ exactly the permission keys listed on it:
221
+
222
+ - **`identity.view`** — to read an identity's grant roots.
223
+ - **`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.
224
+
225
+ Miss the first and every guarded route answers `500` naming the failed read.
226
+ Miss the second and the tree comes back **empty with a `200`** — the API is
227
+ answering "here is everything you may see", which is nothing. The guard does
228
+ not treat that as an absence of grants: an incomplete hierarchy makes a
229
+ node-scoped check undecidable, so it raises rather than denying. Silently
230
+ denying there would take out every node-scoped route while every response still
231
+ looked healthy.
232
+
233
+ `app_wide` checks need only `identity.view`; they never consult the hierarchy.
234
+
235
+ ### What a revoked user can still do
236
+
237
+ For up to the window, a request that should now be denied is allowed. That is the cost of not asking every time, and it is worth stating to whoever owns your access-review process rather than leaving it to be discovered.
238
+
239
+ The window covers **every** change that can alter an answer, including a moved node — reparenting changes what an inherited grant reaches even though no grant itself changed, which is why the hierarchy is revalidated on the same cadence and not a slower one.
240
+
241
+ Two ways out, depending on how much you need.
242
+
243
+ For a **single** high-value operation, ask the API directly through the injected client — the rest of your routes keep the cache:
244
+
245
+ ```ts
246
+ const { allowed } = await this.canopy.permissions.evaluate({
247
+ identity_id: identityId,
248
+ permission: "payments.release",
249
+ scope: "node",
250
+ node_id: nodeId,
251
+ });
252
+ ```
253
+
254
+ For an application that cannot tolerate a stale allow **anywhere**, set the window to zero. Every guarded request then reads fresh, which reinstates a round trip per request — the cost this design exists to remove, so choose it knowingly:
255
+
256
+ ```ts
257
+ CanopyModule.forRoot({
258
+ apiKey: process.env.CANOPY_API_KEY,
259
+ authorizationTtlMs: 0,
260
+ resolveIdentity: (request) => request.user?.sub,
261
+ });
262
+ ```
263
+
264
+ ### Bounding a cache miss
265
+
266
+ A hit costs nothing. A miss reads from Canopy on the request path and holds an inbound request open, so it is bounded: a 5s per-attempt deadline rather than the client-wide 30s, and 1 retry rather than 2, with each wait between attempts capped at one deadline so a `Retry-After` header cannot hold the request past it. Both are settable with `evaluateTimeoutMs` and `evaluateMaxRetries`, and both apply to these reads alone — everything else through the injected client keeps the client-wide values.
267
+
268
+ **Hanging up when the caller does.** If the client disconnects while a miss is being filled, the guard ends that check rather than finishing a decision no one will read. It watches the response for a close that arrives before anything was written, on Express (where Nest returns the Node response) and on Fastify (where the real response is on `reply.raw`).
107
269
 
108
- The unreachable case is logged with its cause before the request is refused.
270
+ The read itself is deliberately **not** cancelled: it may be shared with other requests that are still waiting on it, and it warms the cache either way.
109
271
 
110
272
  ## License
111
273
 
package/dist/index.cjs CHANGED
@@ -17,17 +17,32 @@ var __decorateParam = (index, decorator) => (target, key) => decorator(target, k
17
17
  // src/tokens.ts
18
18
  var CANOPY_OPTIONS = "CANOPY_OPTIONS";
19
19
  var CANOPY_CLIENT = "CANOPY_CLIENT";
20
+ var CANOPY_AUTHORIZER = "CANOPY_AUTHORIZER";
21
+ var CANOPY_TOKEN_VERIFIER = "CANOPY_TOKEN_VERIFIER";
20
22
  var CANOPY_PERMISSION = "CANOPY_PERMISSION";
21
23
 
22
24
  // src/canopy.guard.ts
25
+ function isClosable(value) {
26
+ return typeof value === "object" && value !== null && typeof value.once === "function" && typeof value.removeListener === "function";
27
+ }
28
+ function resolveStream(response) {
29
+ const raw = response?.raw;
30
+ if (isClosable(raw)) {
31
+ return raw;
32
+ }
33
+ if (isClosable(response)) {
34
+ return response;
35
+ }
36
+ return void 0;
37
+ }
23
38
  exports.CanopyGuard = class CanopyGuard {
24
- constructor(reflector, canopy, options) {
39
+ constructor(reflector, authorizer, options) {
25
40
  this.reflector = reflector;
26
- this.canopy = canopy;
41
+ this.authorizer = authorizer;
27
42
  this.options = options;
28
43
  }
29
44
  reflector;
30
- canopy;
45
+ authorizer;
31
46
  options;
32
47
  logger = new common.Logger(exports.CanopyGuard.name);
33
48
  async canActivate(context) {
@@ -35,14 +50,20 @@ exports.CanopyGuard = class CanopyGuard {
35
50
  if (!requirement) {
36
51
  return true;
37
52
  }
38
- const request = context.switchToHttp().getRequest();
39
- const identityId = this.options.resolveIdentity(request);
53
+ const http = context.switchToHttp();
54
+ const request = http.getRequest();
55
+ const identityId = resolveSafely(this.options.resolveIdentity, request);
40
56
  if (!identityId) {
41
57
  throw new common.ForbiddenException(
42
58
  "No identity on the request. Canopy cannot evaluate a permission without one."
43
59
  );
44
60
  }
45
- const decision = await this.evaluate(requirement, identityId, request);
61
+ const decision = await this.evaluate(
62
+ requirement,
63
+ identityId,
64
+ request,
65
+ watchDisconnect(http)
66
+ );
46
67
  if (!decision.allowed) {
47
68
  throw new common.ForbiddenException(
48
69
  `Identity does not hold ${requirement.permission}.`
@@ -57,19 +78,66 @@ exports.CanopyGuard = class CanopyGuard {
57
78
  * decision, so a caller cannot mistake "we could not tell" for "denied by
58
79
  * policy" — and neither outcome lets the request proceed.
59
80
  */
60
- async evaluate(requirement, identityId, request) {
81
+ async evaluate(requirement, identityId, request, cancellation) {
61
82
  const query = this.buildQuery(requirement, identityId, request);
62
83
  try {
63
- return await this.canopy.permissions.evaluate(query);
84
+ return await this.authorizer.evaluate(
85
+ query,
86
+ cancellation ? { signal: cancellation.signal } : {}
87
+ );
64
88
  } catch (error) {
89
+ if (cancellation?.signal.aborted) {
90
+ throw error;
91
+ }
65
92
  this.logger.error(
66
93
  `Could not evaluate ${requirement.permission} for ${identityId}`,
67
94
  error instanceof Error ? error.stack : String(error)
68
95
  );
69
- throw new common.ServiceUnavailableException(
96
+ throw this.toHttpException(error);
97
+ } finally {
98
+ cancellation?.dispose();
99
+ }
100
+ }
101
+ /**
102
+ * Turn a failed check into the status that describes it.
103
+ *
104
+ * Every branch denies — nothing here can let a request through — but they say
105
+ * different things, and the difference is what makes an incident diagnosable.
106
+ * Reporting a rejected API key as "temporarily unavailable" invites a retry
107
+ * that will never succeed and hides a misconfiguration behind an outage.
108
+ */
109
+ 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.");
132
+ }
133
+ if (error.isRateLimited || error.statusCode >= 500) {
134
+ return new common.ServiceUnavailableException(
70
135
  "Authorization is temporarily unavailable."
71
136
  );
72
137
  }
138
+ return new common.InternalServerErrorException(
139
+ "Authorization could not be evaluated."
140
+ );
73
141
  }
74
142
  buildQuery(requirement, identityId, request) {
75
143
  if (requirement.scope === "app_wide") {
@@ -79,7 +147,7 @@ exports.CanopyGuard = class CanopyGuard {
79
147
  scope: "app_wide"
80
148
  };
81
149
  }
82
- const nodeId = this.options.resolveNode?.(request);
150
+ const nodeId = this.options.resolveNode ? resolveSafely(this.options.resolveNode, request) : void 0;
83
151
  if (!nodeId) {
84
152
  throw new common.ForbiddenException(
85
153
  `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.`
@@ -96,11 +164,102 @@ exports.CanopyGuard = class CanopyGuard {
96
164
  exports.CanopyGuard = __decorateClass([
97
165
  common.Injectable(),
98
166
  __decorateParam(0, common.Inject(core.Reflector)),
99
- __decorateParam(1, common.Inject(CANOPY_CLIENT)),
167
+ __decorateParam(1, common.Inject(CANOPY_AUTHORIZER)),
100
168
  __decorateParam(2, common.Inject(CANOPY_OPTIONS))
101
169
  ], exports.CanopyGuard);
170
+ function resolveSafely(resolve, request) {
171
+ try {
172
+ return resolve(
173
+ request
174
+ );
175
+ } catch {
176
+ return void 0;
177
+ }
178
+ }
179
+ function watchDisconnect(http) {
180
+ const raw = resolveStream(http.getResponse?.());
181
+ if (!raw) {
182
+ return void 0;
183
+ }
184
+ const controller = new AbortController();
185
+ const onClose = () => {
186
+ if (raw.writableEnded === true) {
187
+ return;
188
+ }
189
+ controller.abort(
190
+ new DOMException("The client closed the request", "AbortError")
191
+ );
192
+ };
193
+ raw.once("close", onClose);
194
+ return {
195
+ signal: controller.signal,
196
+ dispose: () => raw.removeListener("close", onClose)
197
+ };
198
+ }
199
+ exports.CanopyTokenGuard = class CanopyTokenGuard {
200
+ constructor(verifier, options) {
201
+ this.verifier = verifier;
202
+ this.options = options;
203
+ }
204
+ verifier;
205
+ options;
206
+ async canActivate(context) {
207
+ const request = context.switchToHttp().getRequest();
208
+ const token = readBearerToken(request.headers?.["authorization"]);
209
+ if (token === null) {
210
+ throw new common.UnauthorizedException(
211
+ "Missing bearer token in the Authorization header."
212
+ );
213
+ }
214
+ let claims;
215
+ try {
216
+ claims = await this.verifier.verify(token);
217
+ } catch (error) {
218
+ if (node.isCanopyTokenError(error) && error.code === "token.jwks_unavailable") {
219
+ throw new common.ServiceUnavailableException(
220
+ "Authentication is temporarily unavailable.",
221
+ { cause: error }
222
+ );
223
+ }
224
+ if (node.isCanopyTokenError(error)) {
225
+ throw new common.UnauthorizedException("Invalid or expired access token.", {
226
+ cause: error
227
+ });
228
+ }
229
+ throw error;
230
+ }
231
+ request[this.options.attachTokenAs ?? "canopyToken"] = claims;
232
+ return true;
233
+ }
234
+ };
235
+ exports.CanopyTokenGuard = __decorateClass([
236
+ common.Injectable(),
237
+ __decorateParam(0, common.Inject(CANOPY_TOKEN_VERIFIER)),
238
+ __decorateParam(1, common.Inject(CANOPY_OPTIONS))
239
+ ], exports.CanopyTokenGuard);
240
+ function readBearerToken(header) {
241
+ const value = Array.isArray(header) ? header[0] : header;
242
+ if (typeof value !== "string") {
243
+ return null;
244
+ }
245
+ const match = /^bearer\s+(.+)$/i.exec(value.trim());
246
+ if (!match?.[1]) {
247
+ return null;
248
+ }
249
+ const token = match[1].trim();
250
+ return token.length > 0 ? token : null;
251
+ }
102
252
 
103
253
  // src/canopy.module.ts
254
+ var DEFAULT_EVALUATE_TIMEOUT_MS = 5e3;
255
+ var DEFAULT_EVALUATE_MAX_RETRIES = 1;
256
+ function buildAuthorizer(client, options) {
257
+ return new node.LocalAuthorizer(client.client, {
258
+ ...options.authorizationTtlMs === void 0 ? {} : { ttlMs: options.authorizationTtlMs },
259
+ timeoutMs: options.evaluateTimeoutMs ?? DEFAULT_EVALUATE_TIMEOUT_MS,
260
+ maxRetries: options.evaluateMaxRetries ?? DEFAULT_EVALUATE_MAX_RETRIES
261
+ });
262
+ }
104
263
  exports.CanopyModule = class CanopyModule {
105
264
  static forRoot(options) {
106
265
  const providers = [
@@ -109,13 +268,30 @@ exports.CanopyModule = class CanopyModule {
109
268
  provide: CANOPY_CLIENT,
110
269
  useFactory: () => new node.Canopy(options)
111
270
  },
112
- exports.CanopyGuard
271
+ {
272
+ provide: CANOPY_TOKEN_VERIFIER,
273
+ useFactory: () => new node.TokenVerifier(options.verify)
274
+ },
275
+ {
276
+ provide: CANOPY_AUTHORIZER,
277
+ useFactory: (client) => buildAuthorizer(client, options),
278
+ inject: [CANOPY_CLIENT]
279
+ },
280
+ exports.CanopyGuard,
281
+ exports.CanopyTokenGuard
113
282
  ];
114
283
  return {
115
284
  module: exports.CanopyModule,
116
285
  global: options.isGlobal ?? false,
117
286
  providers,
118
- exports: [CANOPY_CLIENT, CANOPY_OPTIONS, exports.CanopyGuard]
287
+ exports: [
288
+ CANOPY_CLIENT,
289
+ CANOPY_OPTIONS,
290
+ CANOPY_TOKEN_VERIFIER,
291
+ CANOPY_AUTHORIZER,
292
+ exports.CanopyGuard,
293
+ exports.CanopyTokenGuard
294
+ ]
119
295
  };
120
296
  }
121
297
  /**
@@ -134,7 +310,18 @@ exports.CanopyModule = class CanopyModule {
134
310
  useFactory: (resolved) => new node.Canopy(resolved),
135
311
  inject: [CANOPY_OPTIONS]
136
312
  },
137
- exports.CanopyGuard
313
+ {
314
+ provide: CANOPY_TOKEN_VERIFIER,
315
+ useFactory: (resolved) => new node.TokenVerifier(resolved.verify),
316
+ inject: [CANOPY_OPTIONS]
317
+ },
318
+ {
319
+ provide: CANOPY_AUTHORIZER,
320
+ useFactory: (client, resolved) => buildAuthorizer(client, resolved),
321
+ inject: [CANOPY_CLIENT, CANOPY_OPTIONS]
322
+ },
323
+ exports.CanopyGuard,
324
+ exports.CanopyTokenGuard
138
325
  ];
139
326
  return {
140
327
  module: exports.CanopyModule,
@@ -143,7 +330,14 @@ exports.CanopyModule = class CanopyModule {
143
330
  // explicit `imports: undefined` is not the same as no `imports` at all.
144
331
  ...options.imports ? { imports: options.imports } : {},
145
332
  providers,
146
- exports: [CANOPY_CLIENT, CANOPY_OPTIONS, exports.CanopyGuard]
333
+ exports: [
334
+ CANOPY_CLIENT,
335
+ CANOPY_OPTIONS,
336
+ CANOPY_TOKEN_VERIFIER,
337
+ CANOPY_AUTHORIZER,
338
+ exports.CanopyGuard,
339
+ exports.CanopyTokenGuard
340
+ ]
147
341
  };
148
342
  }
149
343
  };
@@ -156,9 +350,11 @@ var RequirePermission = (permission, options = {}) => common.SetMetadata(CANOPY_
156
350
  scope: options.scope ?? "node"
157
351
  });
158
352
 
353
+ exports.CANOPY_AUTHORIZER = CANOPY_AUTHORIZER;
159
354
  exports.CANOPY_CLIENT = CANOPY_CLIENT;
160
355
  exports.CANOPY_OPTIONS = CANOPY_OPTIONS;
161
356
  exports.CANOPY_PERMISSION = CANOPY_PERMISSION;
357
+ exports.CANOPY_TOKEN_VERIFIER = CANOPY_TOKEN_VERIFIER;
162
358
  exports.InjectCanopy = InjectCanopy;
163
359
  exports.RequirePermission = RequirePermission;
164
360
  //# sourceMappingURL=index.cjs.map