@canopy-io/nestjs 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Canopy Identity Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # @canopy-io/nestjs
2
+
3
+ Official NestJS integration for [Canopy](https://canopy-io.com) — hierarchical identity and access management for B2B SaaS.
4
+
5
+ Turns a permission check into a decorator on the route instead of a call in every handler.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @canopy-io/nestjs
11
+ ```
12
+
13
+ Requires NestJS 10 or 11 and Node 18 or later. `@canopy-io/node` comes with it.
14
+
15
+ ## Register
16
+
17
+ ```ts
18
+ import { CanopyModule } from "@canopy-io/nestjs";
19
+
20
+ @Module({
21
+ imports: [
22
+ CanopyModule.forRoot<AuthedRequest>({
23
+ apiKey: process.env.CANOPY_API_KEY,
24
+ resolveIdentity: (request) => request.user.sub,
25
+ resolveNode: (request) => request.params.orgId,
26
+ }),
27
+ ],
28
+ })
29
+ export class AppModule {}
30
+ ```
31
+
32
+ The two resolvers are how the guard learns _who_ is asking and _where_. Where those live is your application's business — a claim on a verified token, a route parameter, a tenant on the session — so you supply them rather than the library guessing. Pass your own request type to `forRoot` and both resolvers are typed against it.
33
+
34
+ When the key comes from something injectable, use `forRootAsync`:
35
+
36
+ ```ts
37
+ CanopyModule.forRootAsync({
38
+ imports: [ConfigModule],
39
+ inject: [ConfigService],
40
+ useFactory: (config: ConfigService) => ({
41
+ apiKey: config.getOrThrow("CANOPY_API_KEY"),
42
+ resolveIdentity: (request) => request.user.sub,
43
+ resolveNode: (request) => request.params.orgId,
44
+ }),
45
+ });
46
+ ```
47
+
48
+ ## Guard a route
49
+
50
+ ```ts
51
+ import { CanopyGuard, RequirePermission } from "@canopy-io/nestjs";
52
+
53
+ @Controller("orgs/:orgId/orders")
54
+ @UseGuards(CanopyGuard)
55
+ export class OrdersController {
56
+ @Post(":orderId/refund")
57
+ @RequirePermission("orders.refund")
58
+ refund() {}
59
+ }
60
+ ```
61
+
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.
63
+
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.
65
+
66
+ ### Scope
67
+
68
+ The default is `node`, which is the strict question: does this identity hold the permission _at this node_.
69
+
70
+ ```ts
71
+ @RequirePermission("reports.view", { scope: "app_wide" })
72
+ ```
73
+
74
+ `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
+
76
+ ## The client
77
+
78
+ For anything the guard does not cover, inject the client:
79
+
80
+ ```ts
81
+ import { InjectCanopy, type Canopy } from "@canopy-io/nestjs";
82
+
83
+ @Injectable()
84
+ export class OrdersService {
85
+ constructor(@InjectCanopy() private readonly canopy: Canopy) {}
86
+
87
+ async assign(identityId: string, nodeId: string, roleId: string) {
88
+ await this.canopy.assignments.create({
89
+ identity_id: identityId,
90
+ node_id: nodeId,
91
+ role_id: roleId,
92
+ });
93
+ }
94
+ }
95
+ ```
96
+
97
+ It is the same [`@canopy-io/node`](../node) client, with its retry policy, pagination and typed errors.
98
+
99
+ ## Failing closed
100
+
101
+ There is no path through `CanopyGuard` that allows a request whose decision is unknown. Each of these ends the request:
102
+
103
+ - no identity resolves — `403`
104
+ - 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"
107
+
108
+ The unreachable case is logged with its cause before the request is refused.
109
+
110
+ ## License
111
+
112
+ MIT © Canopy Identity Inc.
package/dist/index.cjs ADDED
@@ -0,0 +1,165 @@
1
+ 'use strict';
2
+
3
+ var common = require('@nestjs/common');
4
+ var node = require('@canopy-io/node');
5
+ var core = require('@nestjs/core');
6
+
7
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
8
+ var __decorateClass = (decorators, target, key, kind) => {
9
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
10
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
11
+ if (decorator = decorators[i])
12
+ result = (decorator(result)) || result;
13
+ return result;
14
+ };
15
+ var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
16
+
17
+ // src/tokens.ts
18
+ var CANOPY_OPTIONS = "CANOPY_OPTIONS";
19
+ var CANOPY_CLIENT = "CANOPY_CLIENT";
20
+ var CANOPY_PERMISSION = "CANOPY_PERMISSION";
21
+
22
+ // src/canopy.guard.ts
23
+ exports.CanopyGuard = class CanopyGuard {
24
+ constructor(reflector, canopy, options) {
25
+ this.reflector = reflector;
26
+ this.canopy = canopy;
27
+ this.options = options;
28
+ }
29
+ reflector;
30
+ canopy;
31
+ options;
32
+ logger = new common.Logger(exports.CanopyGuard.name);
33
+ async canActivate(context) {
34
+ const requirement = this.reflector.getAllAndOverride(CANOPY_PERMISSION, [context.getHandler(), context.getClass()]);
35
+ if (!requirement) {
36
+ return true;
37
+ }
38
+ const request = context.switchToHttp().getRequest();
39
+ const identityId = this.options.resolveIdentity(request);
40
+ if (!identityId) {
41
+ throw new common.ForbiddenException(
42
+ "No identity on the request. Canopy cannot evaluate a permission without one."
43
+ );
44
+ }
45
+ const decision = await this.evaluate(requirement, identityId, request);
46
+ if (!decision.allowed) {
47
+ throw new common.ForbiddenException(
48
+ `Identity does not hold ${requirement.permission}.`
49
+ );
50
+ }
51
+ return true;
52
+ }
53
+ /**
54
+ * Asks Canopy the question the decorator recorded.
55
+ *
56
+ * Every failure is converted into a thrown exception rather than a `false`
57
+ * decision, so a caller cannot mistake "we could not tell" for "denied by
58
+ * policy" — and neither outcome lets the request proceed.
59
+ */
60
+ async evaluate(requirement, identityId, request) {
61
+ const query = this.buildQuery(requirement, identityId, request);
62
+ try {
63
+ return await this.canopy.permissions.evaluate(query);
64
+ } catch (error) {
65
+ this.logger.error(
66
+ `Could not evaluate ${requirement.permission} for ${identityId}`,
67
+ error instanceof Error ? error.stack : String(error)
68
+ );
69
+ throw new common.ServiceUnavailableException(
70
+ "Authorization is temporarily unavailable."
71
+ );
72
+ }
73
+ }
74
+ buildQuery(requirement, identityId, request) {
75
+ if (requirement.scope === "app_wide") {
76
+ return {
77
+ identity_id: identityId,
78
+ permission: requirement.permission,
79
+ scope: "app_wide"
80
+ };
81
+ }
82
+ const nodeId = this.options.resolveNode?.(request);
83
+ if (!nodeId) {
84
+ throw new common.ForbiddenException(
85
+ `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.`
86
+ );
87
+ }
88
+ return {
89
+ identity_id: identityId,
90
+ permission: requirement.permission,
91
+ scope: "node",
92
+ node_id: nodeId
93
+ };
94
+ }
95
+ };
96
+ exports.CanopyGuard = __decorateClass([
97
+ common.Injectable(),
98
+ __decorateParam(0, common.Inject(core.Reflector)),
99
+ __decorateParam(1, common.Inject(CANOPY_CLIENT)),
100
+ __decorateParam(2, common.Inject(CANOPY_OPTIONS))
101
+ ], exports.CanopyGuard);
102
+
103
+ // src/canopy.module.ts
104
+ exports.CanopyModule = class CanopyModule {
105
+ static forRoot(options) {
106
+ const providers = [
107
+ { provide: CANOPY_OPTIONS, useValue: options },
108
+ {
109
+ provide: CANOPY_CLIENT,
110
+ useFactory: () => new node.Canopy(options)
111
+ },
112
+ exports.CanopyGuard
113
+ ];
114
+ return {
115
+ module: exports.CanopyModule,
116
+ global: options.isGlobal ?? false,
117
+ providers,
118
+ exports: [CANOPY_CLIENT, CANOPY_OPTIONS, exports.CanopyGuard]
119
+ };
120
+ }
121
+ /**
122
+ * The same wiring when the API key comes from something injectable, such as
123
+ * `ConfigService`, rather than being available at module-definition time.
124
+ */
125
+ static forRootAsync(options) {
126
+ const providers = [
127
+ {
128
+ provide: CANOPY_OPTIONS,
129
+ useFactory: options.useFactory,
130
+ inject: options.inject ?? []
131
+ },
132
+ {
133
+ provide: CANOPY_CLIENT,
134
+ useFactory: (resolved) => new node.Canopy(resolved),
135
+ inject: [CANOPY_OPTIONS]
136
+ },
137
+ exports.CanopyGuard
138
+ ];
139
+ return {
140
+ module: exports.CanopyModule,
141
+ global: options.isGlobal ?? false,
142
+ // Spread rather than assigned: under exactOptionalPropertyTypes an
143
+ // explicit `imports: undefined` is not the same as no `imports` at all.
144
+ ...options.imports ? { imports: options.imports } : {},
145
+ providers,
146
+ exports: [CANOPY_CLIENT, CANOPY_OPTIONS, exports.CanopyGuard]
147
+ };
148
+ }
149
+ };
150
+ exports.CanopyModule = __decorateClass([
151
+ common.Module({})
152
+ ], exports.CanopyModule);
153
+ var InjectCanopy = () => common.Inject(CANOPY_CLIENT);
154
+ var RequirePermission = (permission, options = {}) => common.SetMetadata(CANOPY_PERMISSION, {
155
+ permission,
156
+ scope: options.scope ?? "node"
157
+ });
158
+
159
+ exports.CANOPY_CLIENT = CANOPY_CLIENT;
160
+ exports.CANOPY_OPTIONS = CANOPY_OPTIONS;
161
+ exports.CANOPY_PERMISSION = CANOPY_PERMISSION;
162
+ exports.InjectCanopy = InjectCanopy;
163
+ exports.RequirePermission = RequirePermission;
164
+ //# sourceMappingURL=index.cjs.map
165
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tokens.ts","../src/canopy.guard.ts","../src/canopy.module.ts","../src/inject-canopy.decorator.ts","../src/require-permission.decorator.ts"],"names":["CanopyGuard","Logger","ForbiddenException","ServiceUnavailableException","Injectable","Reflector","CanopyModule","Canopy","Module","Inject","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/** 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 Logger,\n ServiceUnavailableException,\n} from \"@nestjs/common\";\nimport { Reflector } from \"@nestjs/core\";\n\nimport type { Canopy, RequestBody, ResponseBody } from \"@canopy-io/node\";\n\n/** Read off the generated operation, so it follows the API rather than restating it. */\ntype EvaluateQuery = RequestBody<\"ApiPermissionsController_evaluate\">;\ntype EvaluateDecision = ResponseBody<\"ApiPermissionsController_evaluate\">;\n\nimport type { CanopyModuleOptions } from \"./options.js\";\nimport type { PermissionRequirement } from \"./require-permission.decorator.js\";\nimport { CANOPY_CLIENT, CANOPY_OPTIONS, CANOPY_PERMISSION } from \"./tokens.js\";\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 is\n * safe to apply globally and opt into per route.\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_CLIENT) private readonly canopy: Canopy,\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 request: unknown = context.switchToHttp().getRequest();\n const identityId = 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(requirement, identityId, request);\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 ): Promise<EvaluateDecision> {\n const query = this.buildQuery(requirement, identityId, request);\n\n try {\n return await this.canopy.permissions.evaluate(query);\n } catch (error) {\n // Deliberately not re-thrown as-is: a transport failure must not reach\n // the 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 new ServiceUnavailableException(\n \"Authorization is temporarily unavailable.\",\n );\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 const nodeId = this.options.resolveNode?.(request);\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","import { type DynamicModule, Module, type Provider } from \"@nestjs/common\";\n\nimport { Canopy } from \"@canopy-io/node\";\n\nimport { CanopyGuard } from \"./canopy.guard.js\";\nimport type {\n CanopyModuleAsyncOptions,\n CanopyModuleOptions,\n} from \"./options.js\";\nimport { CANOPY_CLIENT, CANOPY_OPTIONS } 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@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 CanopyGuard,\n ];\n\n return {\n module: CanopyModule,\n global: options.isGlobal ?? false,\n providers,\n exports: [CANOPY_CLIENT, CANOPY_OPTIONS, CanopyGuard],\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 CanopyGuard,\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: [CANOPY_CLIENT, CANOPY_OPTIONS, CanopyGuard],\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.\n */\nexport type PermissionScope = \"node\" | \"app_wide\";\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 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"]}
@@ -0,0 +1,180 @@
1
+ import { ModuleMetadata, InjectionToken, DynamicModule, CanActivate, ExecutionContext } from '@nestjs/common';
2
+ import { CanopyClientOptions, Canopy } from '@canopy-io/node';
3
+ export { Canopy } from '@canopy-io/node';
4
+ import { Reflector } from '@nestjs/core';
5
+
6
+ /**
7
+ * Pulls a value out of the incoming request.
8
+ *
9
+ * Returning nothing is a denial, not an error — see `CanopyGuard`. That is
10
+ * deliberate: a request whose identity cannot be established is exactly the
11
+ * request that must not be allowed through.
12
+ *
13
+ * `TRequest` is a parameter rather than a fixed type because Nest runs on
14
+ * Express and Fastify, and because the interesting fields — `request.user`,
15
+ * a tenant on the session — are put there by the application's own middleware.
16
+ * Pass your request type to `CanopyModule.forRoot<AuthedRequest>(...)` and
17
+ * these resolvers are typed against it.
18
+ */
19
+ type RequestResolver<TRequest> = (request: TRequest) => string | null | undefined;
20
+ /**
21
+ * How the guard turns a request into an authorization question.
22
+ *
23
+ * Everything `CanopyClientOptions` accepts is accepted here too, so the client
24
+ * is configured in the same place as the guard rather than in two.
25
+ */
26
+ interface CanopyModuleOptions<TRequest = unknown> extends CanopyClientOptions {
27
+ /**
28
+ * Which identity the request is acting as — usually a claim off the verified
29
+ * access token your auth layer already attached.
30
+ */
31
+ resolveIdentity: RequestResolver<TRequest>;
32
+ /**
33
+ * Which hierarchy node the request is touching, for the default `node`
34
+ * scope. Omit it only if every guarded route uses `scope: "app_wide"`;
35
+ * a `node` check with no node resolves to a denial.
36
+ */
37
+ resolveNode?: RequestResolver<TRequest>;
38
+ /**
39
+ * Register the module globally so feature modules need not import it.
40
+ * Defaults to false, matching `@nestjs/config`.
41
+ */
42
+ isGlobal?: boolean;
43
+ }
44
+ /**
45
+ * The same options, produced by a factory — for when the API key lives behind
46
+ * something injectable such as `ConfigService`.
47
+ *
48
+ * Extends `Pick<ModuleMetadata, "imports">` rather than restating the type,
49
+ * which is how Nest's own `*AsyncOptions` are declared.
50
+ */
51
+ interface CanopyModuleAsyncOptions<TRequest = unknown> extends Pick<ModuleMetadata, "imports"> {
52
+ inject?: InjectionToken[];
53
+ useFactory: (...args: never[]) => Promise<CanopyModuleOptions<TRequest>> | CanopyModuleOptions<TRequest>;
54
+ isGlobal?: boolean;
55
+ }
56
+
57
+ /**
58
+ * Wires one configured `Canopy` client and the guard that uses it into a Nest
59
+ * application.
60
+ *
61
+ * ```ts
62
+ * CanopyModule.forRoot<AuthedRequest>({
63
+ * apiKey: process.env.CANOPY_API_KEY,
64
+ * resolveIdentity: (request) => request.user.sub,
65
+ * resolveNode: (request) => request.params.orgId,
66
+ * })
67
+ * ```
68
+ *
69
+ * The client is a singleton because it holds no per-request state — it is a
70
+ * `fetch` wrapper with credentials — so there is nothing to gain from building
71
+ * one per request and a connection pool to lose.
72
+ */
73
+ declare class CanopyModule {
74
+ static forRoot<TRequest = unknown>(options: CanopyModuleOptions<TRequest>): DynamicModule;
75
+ /**
76
+ * The same wiring when the API key comes from something injectable, such as
77
+ * `ConfigService`, rather than being available at module-definition time.
78
+ */
79
+ static forRootAsync<TRequest = unknown>(options: CanopyModuleAsyncOptions<TRequest>): DynamicModule;
80
+ }
81
+
82
+ /**
83
+ * Enforces `@RequirePermission` by asking Canopy, once, per guarded request.
84
+ *
85
+ * The guard fails closed at every step. An unresolvable identity, a `node`
86
+ * check with no node, a denial, or an unreachable API all end the request —
87
+ * there is no path through this class that allows a request whose decision is
88
+ * unknown. That is the property worth protecting when reading this code: an
89
+ * `allow` must be something Canopy actually said.
90
+ *
91
+ * A route with no requirement is allowed through untouched, so the guard is
92
+ * safe to apply globally and opt into per route.
93
+ */
94
+ declare class CanopyGuard implements CanActivate {
95
+ private readonly reflector;
96
+ private readonly canopy;
97
+ private readonly options;
98
+ private readonly logger;
99
+ constructor(reflector: Reflector, canopy: Canopy, options: CanopyModuleOptions<unknown>);
100
+ canActivate(context: ExecutionContext): Promise<boolean>;
101
+ /**
102
+ * Asks Canopy the question the decorator recorded.
103
+ *
104
+ * Every failure is converted into a thrown exception rather than a `false`
105
+ * decision, so a caller cannot mistake "we could not tell" for "denied by
106
+ * policy" — and neither outcome lets the request proceed.
107
+ */
108
+ private evaluate;
109
+ private buildQuery;
110
+ }
111
+
112
+ /**
113
+ * Injects the configured client.
114
+ *
115
+ * ```ts
116
+ * constructor(@InjectCanopy() private readonly canopy: Canopy) {}
117
+ * ```
118
+ *
119
+ * Named rather than resolved by type because this package is bundled with
120
+ * esbuild, which emits no `design:paramtypes` — so type-only injection would
121
+ * fail at runtime with a confusing "cannot resolve dependency" rather than at
122
+ * build time.
123
+ */
124
+ declare const InjectCanopy: () => ParameterDecorator;
125
+
126
+ /**
127
+ * Which question the guard asks Canopy.
128
+ *
129
+ * `node` walks the node's lineage, so a role granted on an ancestor is
130
+ * inherited at the node being touched. `app_wide` asks only whether the
131
+ * identity holds the permission anywhere in the Environment.
132
+ */
133
+ type PermissionScope = "node" | "app_wide";
134
+ /** What `@RequirePermission` records for the guard to read back. */
135
+ interface PermissionRequirement {
136
+ readonly permission: string;
137
+ readonly scope: PermissionScope;
138
+ }
139
+ interface RequirePermissionOptions {
140
+ /**
141
+ * Defaults to `node`.
142
+ *
143
+ * `app_wide` is opt-in on purpose. It answers a coarser question and returns
144
+ * no effective node, which makes it right for deciding whether to render a
145
+ * menu item and wrong for guarding a resource that belongs to one — a
146
+ * distinction that is easy to get backwards if the looser check is the
147
+ * default.
148
+ */
149
+ readonly scope?: PermissionScope;
150
+ }
151
+ /**
152
+ * Require a Canopy permission before a route runs.
153
+ *
154
+ * ```ts
155
+ * @UseGuards(CanopyGuard)
156
+ * @RequirePermission("orders.refund")
157
+ * @Post(":orderId/refund")
158
+ * refund() {}
159
+ * ```
160
+ *
161
+ * Applies to a single handler or to a whole controller; a handler's own
162
+ * requirement wins over its controller's.
163
+ */
164
+ declare const RequirePermission: (permission: string, options?: RequirePermissionOptions) => MethodDecorator & ClassDecorator;
165
+
166
+ /**
167
+ * Injection tokens.
168
+ *
169
+ * These are strings rather than classes because esbuild does not emit
170
+ * `design:paramtypes`, so nothing here can be injected by constructor type
171
+ * alone — every dependency is named with an explicit `@Inject`.
172
+ */
173
+ /** The resolved `CanopyModuleOptions`. */
174
+ declare const CANOPY_OPTIONS = "CANOPY_OPTIONS";
175
+ /** The configured `Canopy` client. */
176
+ declare const CANOPY_CLIENT = "CANOPY_CLIENT";
177
+ /** Metadata key holding what `@RequirePermission` recorded on a handler. */
178
+ declare const CANOPY_PERMISSION = "CANOPY_PERMISSION";
179
+
180
+ export { CANOPY_CLIENT, CANOPY_OPTIONS, CANOPY_PERMISSION, CanopyGuard, CanopyModule, type CanopyModuleAsyncOptions, type CanopyModuleOptions, InjectCanopy, type PermissionRequirement, type PermissionScope, type RequestResolver, RequirePermission, type RequirePermissionOptions };
@@ -0,0 +1,180 @@
1
+ import { ModuleMetadata, InjectionToken, DynamicModule, CanActivate, ExecutionContext } from '@nestjs/common';
2
+ import { CanopyClientOptions, Canopy } from '@canopy-io/node';
3
+ export { Canopy } from '@canopy-io/node';
4
+ import { Reflector } from '@nestjs/core';
5
+
6
+ /**
7
+ * Pulls a value out of the incoming request.
8
+ *
9
+ * Returning nothing is a denial, not an error — see `CanopyGuard`. That is
10
+ * deliberate: a request whose identity cannot be established is exactly the
11
+ * request that must not be allowed through.
12
+ *
13
+ * `TRequest` is a parameter rather than a fixed type because Nest runs on
14
+ * Express and Fastify, and because the interesting fields — `request.user`,
15
+ * a tenant on the session — are put there by the application's own middleware.
16
+ * Pass your request type to `CanopyModule.forRoot<AuthedRequest>(...)` and
17
+ * these resolvers are typed against it.
18
+ */
19
+ type RequestResolver<TRequest> = (request: TRequest) => string | null | undefined;
20
+ /**
21
+ * How the guard turns a request into an authorization question.
22
+ *
23
+ * Everything `CanopyClientOptions` accepts is accepted here too, so the client
24
+ * is configured in the same place as the guard rather than in two.
25
+ */
26
+ interface CanopyModuleOptions<TRequest = unknown> extends CanopyClientOptions {
27
+ /**
28
+ * Which identity the request is acting as — usually a claim off the verified
29
+ * access token your auth layer already attached.
30
+ */
31
+ resolveIdentity: RequestResolver<TRequest>;
32
+ /**
33
+ * Which hierarchy node the request is touching, for the default `node`
34
+ * scope. Omit it only if every guarded route uses `scope: "app_wide"`;
35
+ * a `node` check with no node resolves to a denial.
36
+ */
37
+ resolveNode?: RequestResolver<TRequest>;
38
+ /**
39
+ * Register the module globally so feature modules need not import it.
40
+ * Defaults to false, matching `@nestjs/config`.
41
+ */
42
+ isGlobal?: boolean;
43
+ }
44
+ /**
45
+ * The same options, produced by a factory — for when the API key lives behind
46
+ * something injectable such as `ConfigService`.
47
+ *
48
+ * Extends `Pick<ModuleMetadata, "imports">` rather than restating the type,
49
+ * which is how Nest's own `*AsyncOptions` are declared.
50
+ */
51
+ interface CanopyModuleAsyncOptions<TRequest = unknown> extends Pick<ModuleMetadata, "imports"> {
52
+ inject?: InjectionToken[];
53
+ useFactory: (...args: never[]) => Promise<CanopyModuleOptions<TRequest>> | CanopyModuleOptions<TRequest>;
54
+ isGlobal?: boolean;
55
+ }
56
+
57
+ /**
58
+ * Wires one configured `Canopy` client and the guard that uses it into a Nest
59
+ * application.
60
+ *
61
+ * ```ts
62
+ * CanopyModule.forRoot<AuthedRequest>({
63
+ * apiKey: process.env.CANOPY_API_KEY,
64
+ * resolveIdentity: (request) => request.user.sub,
65
+ * resolveNode: (request) => request.params.orgId,
66
+ * })
67
+ * ```
68
+ *
69
+ * The client is a singleton because it holds no per-request state — it is a
70
+ * `fetch` wrapper with credentials — so there is nothing to gain from building
71
+ * one per request and a connection pool to lose.
72
+ */
73
+ declare class CanopyModule {
74
+ static forRoot<TRequest = unknown>(options: CanopyModuleOptions<TRequest>): DynamicModule;
75
+ /**
76
+ * The same wiring when the API key comes from something injectable, such as
77
+ * `ConfigService`, rather than being available at module-definition time.
78
+ */
79
+ static forRootAsync<TRequest = unknown>(options: CanopyModuleAsyncOptions<TRequest>): DynamicModule;
80
+ }
81
+
82
+ /**
83
+ * Enforces `@RequirePermission` by asking Canopy, once, per guarded request.
84
+ *
85
+ * The guard fails closed at every step. An unresolvable identity, a `node`
86
+ * check with no node, a denial, or an unreachable API all end the request —
87
+ * there is no path through this class that allows a request whose decision is
88
+ * unknown. That is the property worth protecting when reading this code: an
89
+ * `allow` must be something Canopy actually said.
90
+ *
91
+ * A route with no requirement is allowed through untouched, so the guard is
92
+ * safe to apply globally and opt into per route.
93
+ */
94
+ declare class CanopyGuard implements CanActivate {
95
+ private readonly reflector;
96
+ private readonly canopy;
97
+ private readonly options;
98
+ private readonly logger;
99
+ constructor(reflector: Reflector, canopy: Canopy, options: CanopyModuleOptions<unknown>);
100
+ canActivate(context: ExecutionContext): Promise<boolean>;
101
+ /**
102
+ * Asks Canopy the question the decorator recorded.
103
+ *
104
+ * Every failure is converted into a thrown exception rather than a `false`
105
+ * decision, so a caller cannot mistake "we could not tell" for "denied by
106
+ * policy" — and neither outcome lets the request proceed.
107
+ */
108
+ private evaluate;
109
+ private buildQuery;
110
+ }
111
+
112
+ /**
113
+ * Injects the configured client.
114
+ *
115
+ * ```ts
116
+ * constructor(@InjectCanopy() private readonly canopy: Canopy) {}
117
+ * ```
118
+ *
119
+ * Named rather than resolved by type because this package is bundled with
120
+ * esbuild, which emits no `design:paramtypes` — so type-only injection would
121
+ * fail at runtime with a confusing "cannot resolve dependency" rather than at
122
+ * build time.
123
+ */
124
+ declare const InjectCanopy: () => ParameterDecorator;
125
+
126
+ /**
127
+ * Which question the guard asks Canopy.
128
+ *
129
+ * `node` walks the node's lineage, so a role granted on an ancestor is
130
+ * inherited at the node being touched. `app_wide` asks only whether the
131
+ * identity holds the permission anywhere in the Environment.
132
+ */
133
+ type PermissionScope = "node" | "app_wide";
134
+ /** What `@RequirePermission` records for the guard to read back. */
135
+ interface PermissionRequirement {
136
+ readonly permission: string;
137
+ readonly scope: PermissionScope;
138
+ }
139
+ interface RequirePermissionOptions {
140
+ /**
141
+ * Defaults to `node`.
142
+ *
143
+ * `app_wide` is opt-in on purpose. It answers a coarser question and returns
144
+ * no effective node, which makes it right for deciding whether to render a
145
+ * menu item and wrong for guarding a resource that belongs to one — a
146
+ * distinction that is easy to get backwards if the looser check is the
147
+ * default.
148
+ */
149
+ readonly scope?: PermissionScope;
150
+ }
151
+ /**
152
+ * Require a Canopy permission before a route runs.
153
+ *
154
+ * ```ts
155
+ * @UseGuards(CanopyGuard)
156
+ * @RequirePermission("orders.refund")
157
+ * @Post(":orderId/refund")
158
+ * refund() {}
159
+ * ```
160
+ *
161
+ * Applies to a single handler or to a whole controller; a handler's own
162
+ * requirement wins over its controller's.
163
+ */
164
+ declare const RequirePermission: (permission: string, options?: RequirePermissionOptions) => MethodDecorator & ClassDecorator;
165
+
166
+ /**
167
+ * Injection tokens.
168
+ *
169
+ * These are strings rather than classes because esbuild does not emit
170
+ * `design:paramtypes`, so nothing here can be injected by constructor type
171
+ * alone — every dependency is named with an explicit `@Inject`.
172
+ */
173
+ /** The resolved `CanopyModuleOptions`. */
174
+ declare const CANOPY_OPTIONS = "CANOPY_OPTIONS";
175
+ /** The configured `Canopy` client. */
176
+ declare const CANOPY_CLIENT = "CANOPY_CLIENT";
177
+ /** Metadata key holding what `@RequirePermission` recorded on a handler. */
178
+ declare const CANOPY_PERMISSION = "CANOPY_PERMISSION";
179
+
180
+ export { CANOPY_CLIENT, CANOPY_OPTIONS, CANOPY_PERMISSION, CanopyGuard, CanopyModule, type CanopyModuleAsyncOptions, type CanopyModuleOptions, InjectCanopy, type PermissionRequirement, type PermissionScope, type RequestResolver, RequirePermission, type RequirePermissionOptions };
package/dist/index.js ADDED
@@ -0,0 +1,159 @@
1
+ import { Injectable, Inject, Module, Logger, ForbiddenException, ServiceUnavailableException, SetMetadata } from '@nestjs/common';
2
+ import { Canopy } from '@canopy-io/node';
3
+ import { Reflector } from '@nestjs/core';
4
+
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __decorateClass = (decorators, target, key, kind) => {
7
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
8
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
9
+ if (decorator = decorators[i])
10
+ result = (decorator(result)) || result;
11
+ return result;
12
+ };
13
+ var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
14
+
15
+ // src/tokens.ts
16
+ var CANOPY_OPTIONS = "CANOPY_OPTIONS";
17
+ var CANOPY_CLIENT = "CANOPY_CLIENT";
18
+ var CANOPY_PERMISSION = "CANOPY_PERMISSION";
19
+
20
+ // src/canopy.guard.ts
21
+ var CanopyGuard = class {
22
+ constructor(reflector, canopy, options) {
23
+ this.reflector = reflector;
24
+ this.canopy = canopy;
25
+ this.options = options;
26
+ }
27
+ reflector;
28
+ canopy;
29
+ options;
30
+ logger = new Logger(CanopyGuard.name);
31
+ async canActivate(context) {
32
+ const requirement = this.reflector.getAllAndOverride(CANOPY_PERMISSION, [context.getHandler(), context.getClass()]);
33
+ if (!requirement) {
34
+ return true;
35
+ }
36
+ const request = context.switchToHttp().getRequest();
37
+ const identityId = this.options.resolveIdentity(request);
38
+ if (!identityId) {
39
+ throw new ForbiddenException(
40
+ "No identity on the request. Canopy cannot evaluate a permission without one."
41
+ );
42
+ }
43
+ const decision = await this.evaluate(requirement, identityId, request);
44
+ if (!decision.allowed) {
45
+ throw new ForbiddenException(
46
+ `Identity does not hold ${requirement.permission}.`
47
+ );
48
+ }
49
+ return true;
50
+ }
51
+ /**
52
+ * Asks Canopy the question the decorator recorded.
53
+ *
54
+ * Every failure is converted into a thrown exception rather than a `false`
55
+ * decision, so a caller cannot mistake "we could not tell" for "denied by
56
+ * policy" — and neither outcome lets the request proceed.
57
+ */
58
+ async evaluate(requirement, identityId, request) {
59
+ const query = this.buildQuery(requirement, identityId, request);
60
+ try {
61
+ return await this.canopy.permissions.evaluate(query);
62
+ } catch (error) {
63
+ this.logger.error(
64
+ `Could not evaluate ${requirement.permission} for ${identityId}`,
65
+ error instanceof Error ? error.stack : String(error)
66
+ );
67
+ throw new ServiceUnavailableException(
68
+ "Authorization is temporarily unavailable."
69
+ );
70
+ }
71
+ }
72
+ buildQuery(requirement, identityId, request) {
73
+ if (requirement.scope === "app_wide") {
74
+ return {
75
+ identity_id: identityId,
76
+ permission: requirement.permission,
77
+ scope: "app_wide"
78
+ };
79
+ }
80
+ const nodeId = this.options.resolveNode?.(request);
81
+ if (!nodeId) {
82
+ throw new ForbiddenException(
83
+ `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.`
84
+ );
85
+ }
86
+ return {
87
+ identity_id: identityId,
88
+ permission: requirement.permission,
89
+ scope: "node",
90
+ node_id: nodeId
91
+ };
92
+ }
93
+ };
94
+ CanopyGuard = __decorateClass([
95
+ Injectable(),
96
+ __decorateParam(0, Inject(Reflector)),
97
+ __decorateParam(1, Inject(CANOPY_CLIENT)),
98
+ __decorateParam(2, Inject(CANOPY_OPTIONS))
99
+ ], CanopyGuard);
100
+
101
+ // src/canopy.module.ts
102
+ var CanopyModule = class {
103
+ static forRoot(options) {
104
+ const providers = [
105
+ { provide: CANOPY_OPTIONS, useValue: options },
106
+ {
107
+ provide: CANOPY_CLIENT,
108
+ useFactory: () => new Canopy(options)
109
+ },
110
+ CanopyGuard
111
+ ];
112
+ return {
113
+ module: CanopyModule,
114
+ global: options.isGlobal ?? false,
115
+ providers,
116
+ exports: [CANOPY_CLIENT, CANOPY_OPTIONS, CanopyGuard]
117
+ };
118
+ }
119
+ /**
120
+ * The same wiring when the API key comes from something injectable, such as
121
+ * `ConfigService`, rather than being available at module-definition time.
122
+ */
123
+ static forRootAsync(options) {
124
+ const providers = [
125
+ {
126
+ provide: CANOPY_OPTIONS,
127
+ useFactory: options.useFactory,
128
+ inject: options.inject ?? []
129
+ },
130
+ {
131
+ provide: CANOPY_CLIENT,
132
+ useFactory: (resolved) => new Canopy(resolved),
133
+ inject: [CANOPY_OPTIONS]
134
+ },
135
+ CanopyGuard
136
+ ];
137
+ return {
138
+ module: CanopyModule,
139
+ global: options.isGlobal ?? false,
140
+ // Spread rather than assigned: under exactOptionalPropertyTypes an
141
+ // explicit `imports: undefined` is not the same as no `imports` at all.
142
+ ...options.imports ? { imports: options.imports } : {},
143
+ providers,
144
+ exports: [CANOPY_CLIENT, CANOPY_OPTIONS, CanopyGuard]
145
+ };
146
+ }
147
+ };
148
+ CanopyModule = __decorateClass([
149
+ Module({})
150
+ ], CanopyModule);
151
+ var InjectCanopy = () => Inject(CANOPY_CLIENT);
152
+ var RequirePermission = (permission, options = {}) => SetMetadata(CANOPY_PERMISSION, {
153
+ permission,
154
+ scope: options.scope ?? "node"
155
+ });
156
+
157
+ export { CANOPY_CLIENT, CANOPY_OPTIONS, CANOPY_PERMISSION, CanopyGuard, CanopyModule, InjectCanopy, RequirePermission };
158
+ //# sourceMappingURL=index.js.map
159
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tokens.ts","../src/canopy.guard.ts","../src/canopy.module.ts","../src/inject-canopy.decorator.ts","../src/require-permission.decorator.ts"],"names":["Inject"],"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/** 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 Logger,\n ServiceUnavailableException,\n} from \"@nestjs/common\";\nimport { Reflector } from \"@nestjs/core\";\n\nimport type { Canopy, RequestBody, ResponseBody } from \"@canopy-io/node\";\n\n/** Read off the generated operation, so it follows the API rather than restating it. */\ntype EvaluateQuery = RequestBody<\"ApiPermissionsController_evaluate\">;\ntype EvaluateDecision = ResponseBody<\"ApiPermissionsController_evaluate\">;\n\nimport type { CanopyModuleOptions } from \"./options.js\";\nimport type { PermissionRequirement } from \"./require-permission.decorator.js\";\nimport { CANOPY_CLIENT, CANOPY_OPTIONS, CANOPY_PERMISSION } from \"./tokens.js\";\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 is\n * safe to apply globally and opt into per route.\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_CLIENT) private readonly canopy: Canopy,\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 request: unknown = context.switchToHttp().getRequest();\n const identityId = 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(requirement, identityId, request);\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 ): Promise<EvaluateDecision> {\n const query = this.buildQuery(requirement, identityId, request);\n\n try {\n return await this.canopy.permissions.evaluate(query);\n } catch (error) {\n // Deliberately not re-thrown as-is: a transport failure must not reach\n // the 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 new ServiceUnavailableException(\n \"Authorization is temporarily unavailable.\",\n );\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 const nodeId = this.options.resolveNode?.(request);\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","import { type DynamicModule, Module, type Provider } from \"@nestjs/common\";\n\nimport { Canopy } from \"@canopy-io/node\";\n\nimport { CanopyGuard } from \"./canopy.guard.js\";\nimport type {\n CanopyModuleAsyncOptions,\n CanopyModuleOptions,\n} from \"./options.js\";\nimport { CANOPY_CLIENT, CANOPY_OPTIONS } 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@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 CanopyGuard,\n ];\n\n return {\n module: CanopyModule,\n global: options.isGlobal ?? false,\n providers,\n exports: [CANOPY_CLIENT, CANOPY_OPTIONS, CanopyGuard],\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 CanopyGuard,\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: [CANOPY_CLIENT, CANOPY_OPTIONS, CanopyGuard],\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.\n */\nexport type PermissionScope = \"node\" | \"app_wide\";\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 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 ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@canopy-io/nestjs",
3
+ "version": "0.1.0",
4
+ "description": "Official NestJS integration for Canopy — hierarchical identity and access management.",
5
+ "license": "MIT",
6
+ "author": "Canopy Identity Inc.",
7
+ "homepage": "https://github.com/canopy-identity/canopy-javascript#readme",
8
+ "bugs": "https://github.com/canopy-identity/canopy-javascript/issues",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/canopy-identity/canopy-javascript.git",
12
+ "directory": "packages/nestjs"
13
+ },
14
+ "keywords": [
15
+ "canopy",
16
+ "nestjs",
17
+ "authorization",
18
+ "rbac",
19
+ "iam",
20
+ "permissions",
21
+ "guard"
22
+ ],
23
+ "type": "module",
24
+ "sideEffects": false,
25
+ "main": "./dist/index.cjs",
26
+ "module": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js",
32
+ "require": "./dist/index.cjs"
33
+ },
34
+ "./package.json": "./package.json"
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "README.md",
39
+ "LICENSE"
40
+ ],
41
+ "engines": {
42
+ "node": ">=18"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
46
+ },
47
+ "scripts": {
48
+ "build": "tsup",
49
+ "typecheck": "tsc --noEmit",
50
+ "test": "vitest run",
51
+ "test:watch": "vitest"
52
+ },
53
+ "dependencies": {
54
+ "@canopy-io/node": "^0.1.0"
55
+ },
56
+ "peerDependencies": {
57
+ "@nestjs/common": "^10.0.0 || ^11.0.0",
58
+ "@nestjs/core": "^10.0.0 || ^11.0.0",
59
+ "reflect-metadata": "^0.1.13 || ^0.2.0"
60
+ },
61
+ "devDependencies": {
62
+ "@nestjs/common": "^11.1.21",
63
+ "@nestjs/core": "^11.1.21",
64
+ "@nestjs/platform-express": "^11.1.28",
65
+ "@nestjs/testing": "^11.1.21",
66
+ "reflect-metadata": "^0.2.2",
67
+ "rxjs": "^7.8.1"
68
+ }
69
+ }