@dudousxd/nestjs-authz 0.8.1 → 0.9.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.
Files changed (122) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/README.md +279 -0
  3. package/dist/authz.guard.d.ts +48 -0
  4. package/dist/authz.guard.d.ts.map +1 -0
  5. package/dist/authz.guard.js +144 -0
  6. package/dist/authz.guard.js.map +1 -0
  7. package/dist/can-endpoint.controller.d.ts +0 -19
  8. package/dist/can-endpoint.controller.d.ts.map +1 -1
  9. package/dist/can-endpoint.controller.js +18 -9
  10. package/dist/can-endpoint.controller.js.map +1 -1
  11. package/dist/decision-provider.d.ts +25 -16
  12. package/dist/decision-provider.d.ts.map +1 -1
  13. package/dist/decision-provider.js +11 -1
  14. package/dist/decision-provider.js.map +1 -1
  15. package/dist/decisions.d.ts +24 -0
  16. package/dist/decisions.d.ts.map +1 -0
  17. package/dist/decisions.js +87 -0
  18. package/dist/decisions.js.map +1 -0
  19. package/dist/decorator/roles.decorator.d.ts +6 -12
  20. package/dist/decorator/roles.decorator.d.ts.map +1 -1
  21. package/dist/decorator/roles.decorator.js +13 -15
  22. package/dist/decorator/roles.decorator.js.map +1 -1
  23. package/dist/diagnostics.d.ts +15 -5
  24. package/dist/diagnostics.d.ts.map +1 -1
  25. package/dist/diagnostics.js +3 -1
  26. package/dist/diagnostics.js.map +1 -1
  27. package/dist/errors/exceptions.d.ts +0 -8
  28. package/dist/errors/exceptions.d.ts.map +1 -1
  29. package/dist/errors/exceptions.js +0 -14
  30. package/dist/errors/exceptions.js.map +1 -1
  31. package/dist/gate.d.ts +83 -218
  32. package/dist/gate.d.ts.map +1 -1
  33. package/dist/gate.js +209 -611
  34. package/dist/gate.js.map +1 -1
  35. package/dist/helpers.d.ts +46 -0
  36. package/dist/helpers.d.ts.map +1 -0
  37. package/dist/helpers.js +89 -0
  38. package/dist/helpers.js.map +1 -0
  39. package/dist/index.d.ts +28 -21
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +30 -13
  42. package/dist/index.js.map +1 -1
  43. package/dist/links.d.ts +39 -0
  44. package/dist/links.d.ts.map +1 -0
  45. package/dist/links.js +57 -0
  46. package/dist/links.js.map +1 -0
  47. package/dist/module.d.ts +23 -22
  48. package/dist/module.d.ts.map +1 -1
  49. package/dist/module.js +129 -158
  50. package/dist/module.js.map +1 -1
  51. package/dist/official.d.ts +39 -0
  52. package/dist/official.d.ts.map +1 -0
  53. package/dist/official.js +44 -0
  54. package/dist/official.js.map +1 -0
  55. package/dist/policies/authz-policy.d.ts +42 -0
  56. package/dist/policies/authz-policy.d.ts.map +1 -0
  57. package/dist/policies/authz-policy.js +80 -0
  58. package/dist/policies/authz-policy.js.map +1 -0
  59. package/dist/policies/gate.policy.d.ts +18 -0
  60. package/dist/policies/gate.policy.d.ts.map +1 -0
  61. package/dist/policies/gate.policy.js +37 -0
  62. package/dist/policies/gate.policy.js.map +1 -0
  63. package/dist/policies/index.d.ts +5 -0
  64. package/dist/policies/index.d.ts.map +1 -0
  65. package/dist/policies/index.js +5 -0
  66. package/dist/policies/index.js.map +1 -0
  67. package/dist/policies/permission.policy.d.ts +27 -0
  68. package/dist/policies/permission.policy.d.ts.map +1 -0
  69. package/dist/policies/permission.policy.js +61 -0
  70. package/dist/policies/permission.policy.js.map +1 -0
  71. package/dist/policies/role.policy.d.ts +27 -0
  72. package/dist/policies/role.policy.d.ts.map +1 -0
  73. package/dist/policies/role.policy.js +51 -0
  74. package/dist/policies/role.policy.js.map +1 -0
  75. package/dist/policy-index.d.ts +51 -0
  76. package/dist/policy-index.d.ts.map +1 -0
  77. package/dist/policy-index.js +135 -0
  78. package/dist/policy-index.js.map +1 -0
  79. package/dist/rbac.d.ts +92 -0
  80. package/dist/rbac.d.ts.map +1 -0
  81. package/dist/rbac.js +293 -0
  82. package/dist/rbac.js.map +1 -0
  83. package/dist/resource-resolver.d.ts +15 -17
  84. package/dist/resource-resolver.d.ts.map +1 -1
  85. package/dist/resource-resolver.js +8 -8
  86. package/dist/resource-resolver.js.map +1 -1
  87. package/dist/runtime.d.ts +31 -0
  88. package/dist/runtime.d.ts.map +1 -0
  89. package/dist/runtime.js +40 -0
  90. package/dist/runtime.js.map +1 -0
  91. package/dist/tokens.d.ts +4 -8
  92. package/dist/tokens.d.ts.map +1 -1
  93. package/dist/tokens.js +4 -8
  94. package/dist/tokens.js.map +1 -1
  95. package/dist/types.d.ts +52 -171
  96. package/dist/types.d.ts.map +1 -1
  97. package/package.json +19 -10
  98. package/skills/authz-enforcement/SKILL.md +67 -135
  99. package/skills/authz-policies/SKILL.md +43 -117
  100. package/skills/authz-query-scopes/SKILL.md +16 -9
  101. package/skills/authz-rbac-seams/SKILL.md +29 -12
  102. package/skills/authz-setup/SKILL.md +45 -106
  103. package/dist/decorator/can.decorator.d.ts +0 -29
  104. package/dist/decorator/can.decorator.d.ts.map +0 -1
  105. package/dist/decorator/can.decorator.js +0 -23
  106. package/dist/decorator/can.decorator.js.map +0 -1
  107. package/dist/decorator/policy.decorator.d.ts +0 -18
  108. package/dist/decorator/policy.decorator.d.ts.map +0 -1
  109. package/dist/decorator/policy.decorator.js +0 -28
  110. package/dist/decorator/policy.decorator.js.map +0 -1
  111. package/dist/guard/can.guard.d.ts +0 -31
  112. package/dist/guard/can.guard.d.ts.map +0 -1
  113. package/dist/guard/can.guard.js +0 -92
  114. package/dist/guard/can.guard.js.map +0 -1
  115. package/dist/guard/roles.guard.d.ts +0 -21
  116. package/dist/guard/roles.guard.d.ts.map +0 -1
  117. package/dist/guard/roles.guard.js +0 -50
  118. package/dist/guard/roles.guard.js.map +0 -1
  119. package/dist/policy-registry.d.ts +0 -46
  120. package/dist/policy-registry.d.ts.map +0 -1
  121. package/dist/policy-registry.js +0 -101
  122. package/dist/policy-registry.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,114 @@
1
1
  # @dudousxd/nestjs-authz
2
2
 
3
+ ## 0.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#67](https://github.com/DavideCarvalho/nestjs-authz/pull/67) [`32e967f`](https://github.com/DavideCarvalho/nestjs-authz/commit/32e967ffbcd68a97e8effc77af9c16bbbafdc918) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Build on NestJS's first-party `@nestjs/authorization` instead of a parallel engine. Requires
8
+ NestJS 12, `@nestjs/authorization` ^0.1.3 and Node 22+.
9
+
10
+ **Use the official API** for policies (`@Policy()`), route checks (`@Can(Policy, 'ability',
11
+ args?)`, `Can.Anyone()`), the global deny-by-default `AuthorizationGuard`, the current user
12
+ (`getUser`) and `AuthorizationService`. Import `AuthorizationModule.forRoot()` next to
13
+ `AuthzModule.forRoot()`.
14
+
15
+ **Added on top:**
16
+
17
+ - `RolePolicy` (`has`, `hasAll`) and `PermissionPolicy` (`has`, `hasAll`, wildcards) as
18
+ official policies over the RBAC seams; `GatePolicy` (`allows`) for `gate.define()` gates.
19
+ - `@Can()` helpers: `byId(Resource, { param? })` (loads the record with the `ResourceResolver`;
20
+ `authorizedResource(req, Resource)` reads it back), `roles()`, `permission()` /
21
+ `permissions()`, `ability()`, `staticCanArgs()`. Static resolvers carry their args on
22
+ `Symbol.for('@dudousxd/static-can-args')`.
23
+ - `@Roles(...)` and `@Permission(...)` sugar that writes official `@Can` metadata.
24
+ - `@PolicyFor(Resource)`: links a resource to its official policy (and applies `@Policy()`).
25
+ - `AuthzPolicy` base class, `superAdminBefore()` and `decisionProviderBefore()`: the
26
+ `superAdmin` hook and the `DECISION_PROVIDER` (e.g. the Cerbos adapter at
27
+ `@dudousxd/nestjs-authz/cerbos`) for official policies. `superAdmin` receives the resource.
28
+ `RolePolicy` / `PermissionPolicy` ask only `superAdmin`.
29
+ - `Gate.allows/denies/authorize(Policy, ability, ...args)` (delegates to `AuthorizationService`)
30
+ next to the string form; `allowsMany` also takes `{ policy, ability, args }`.
31
+ - The `authz:decision` channel now also carries the official guard's decisions (allowed and
32
+ denied, `reason: 'route'`, with `policy` and `handler`).
33
+ - `PolicyIndex`, `routeRequirements()`, `readCanRequirements()`, `isOfficialPolicy()`,
34
+ `currentUserOf()`.
35
+ - The per-request cache (nestjs-context store) now spans the official guard and the handler:
36
+ `RolePolicy` / `PermissionPolicy` and `Gate` calls share one `getPermissions`, one
37
+ `getRoles` and one `resolveUser` per user per request.
38
+ - `Gate` calls without `forUser()` read `@nestjs/authentication`'s `AuthenticationContext.user`
39
+ (optional peer, resolved through `ModuleRef`). Inside its scope (a call it handles, or
40
+ `AuthenticationContext.run()`) its answer is final, so an anonymous call is a guest;
41
+ nestjs-context's user ref is read only when there is no authentication scope at all.
42
+ - A migration mode, `AuthzModule.forRoot({ unannotated: 'allow' })` (`forRootAsync`: next to
43
+ `useFactory`), for apps that cannot annotate every route at once. With
44
+ `AuthorizationModule.forRoot({ globalGuard: false })`, it registers a global `AuthzGuard` that
45
+ hands every route with `@Can()` requirements to the official `AuthorizationGuard` (same user,
46
+ 401/403 and `denied` event) and lets unannotated routes through. The startup fails if the
47
+ official global guard is still on. Default `'deny'`: nothing is registered. `AuthzGuard` is
48
+ exported for `@UseGuards()`.
49
+ - `decorators?: ClassDecorator[]`: class decorators applied to every controller the module
50
+ mounts (the `POST /authz/can` endpoint), which are open to the official guard by default
51
+ (`authorization:can-anyone`). `canEndpoint` and `decorators` are static options.
52
+
53
+ **Removed, with no compatibility shim** (move each to the official form): this package's
54
+ `Can` and `Policy` exports and `CanOptions`. The string `@Can('update', Post)` →
55
+ `@Can(PostPolicy, 'update', byId(Post))` (`Can` from `@nestjs/authorization`);
56
+ `@Can('create', Post, { classLevel: true })` → `@Can(PostPolicy, 'create')`;
57
+ `@Can('access-admin')` → `@Can(GatePolicy, 'allows', ability('access-admin'))`;
58
+ `@Policy(Post)` → official `@Policy()` + `@PolicyFor(Post)` (or `@PolicyFor(Post)` alone).
59
+
60
+ **Also removed:** `CanGuard`, `RolesGuard`, `PolicyRegistry` (use `PolicyIndex`), `CAN_METADATA`,
61
+ `ROLES_METADATA`, the `policies` and `after` options, deny messages on policy results
62
+ (`PolicyResponse` / `PolicyResult` remain only as `DecisionProvider` verdicts),
63
+ `PolicyNotDecoratedException`, `ResourceResolverMissingException`, `AfterHook`,
64
+ `PolicyMethod`, `PolicyBeforeHook`, `PolicyInstance`.
65
+
66
+ **The policy has the final word:** string abilities now ask `superAdmin` →
67
+ `DECISION_PROVIDER` → the linked policy → the persisted permissions → the ad-hoc gate. A
68
+ persisted permission named like a policy ability (`update`) no longer bypasses the policy, in
69
+ checks and in `gate.scope()`; only `superAdmin` does. Permissions still grant named abilities
70
+ no policy defines (`posts.publish`).
71
+
72
+ **Behavior changes from the official guard:** routes with no `@Can()` are denied (add
73
+ `@Can.Anyone()`, or migrate with `unannotated: 'allow'`); class- and method-level requirements add up; guests reach policies as
74
+ `null` and are answered 401; only a literal `true` allows; `superAdmin` applies to policies
75
+ that extend `AuthzPolicy` (and to the built-in ones, string `Gate` checks and `gate.scope`).
76
+ `gate.hasRole()` stays a plain fact (no `superAdmin`); `RolePolicy` honors it. A
77
+ `DecisionProvider` and `superAdmin` see a guest as `null` (was `undefined`).
78
+
79
+ - [#67](https://github.com/DavideCarvalho/nestjs-authz/pull/67) [`32e967f`](https://github.com/DavideCarvalho/nestjs-authz/commit/32e967ffbcd68a97e8effc77af9c16bbbafdc918) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - `@PolicyFor(Resource)` now throws when `Resource` is already linked to a different policy,
80
+ instead of silently replacing the process-wide link. Linking the same pair again stays a
81
+ no-op, and a re-declared class of the same name (a test helper declaring its policy per
82
+ call, a hot reload) still replaces the link.
83
+
84
+ ### Patch Changes
85
+
86
+ - [#67](https://github.com/DavideCarvalho/nestjs-authz/pull/67) [`32e967f`](https://github.com/DavideCarvalho/nestjs-authz/commit/32e967ffbcd68a97e8effc77af9c16bbbafdc918) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Fix packages that could be published without files their `exports` point to. `tsc` builds no longer use a `.tsbuildinfo` that can outlive `dist/` and skip emitting files. A new `check:exports` step (also `prepack`) fails the release if any `exports`/`main`/`types` path is missing.
87
+
88
+ - [#67](https://github.com/DavideCarvalho/nestjs-authz/pull/67) [`32e967f`](https://github.com/DavideCarvalho/nestjs-authz/commit/32e967ffbcd68a97e8effc77af9c16bbbafdc918) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Document that a `DecisionProvider` deny `message` no longer reaches the 403 in 0.9. The official
89
+ guard answers `{ statusCode: 403, message: 'Forbidden' }`, and `@nestjs/authorization` has no
90
+ deny-with-reason. Added to the 0.8 → 0.9 migration table, with the workaround and what it costs.
91
+ `PolicyResponse`'s doc comment no longer claims the message is published on the decision event.
92
+
93
+ - [#67](https://github.com/DavideCarvalho/nestjs-authz/pull/67) [`32e967f`](https://github.com/DavideCarvalho/nestjs-authz/commit/32e967ffbcd68a97e8effc77af9c16bbbafdc918) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - A `Gate` string check that reaches a linked `AuthzPolicy` now asks `superAdmin` and the
94
+ `DECISION_PROVIDER` once, not again in the policy's `before()`. Same decision order and verdicts.
95
+
96
+ - [#67](https://github.com/DavideCarvalho/nestjs-authz/pull/67) [`32e967f`](https://github.com/DavideCarvalho/nestjs-authz/commit/32e967ffbcd68a97e8effc77af9c16bbbafdc918) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Importing `AuthzModule` without `@nestjs/authorization`'s `AuthorizationModule` now fails the boot
97
+ with an error naming the missing `AuthorizationModule.forRoot()` import, instead of Nest's
98
+ unresolvable-dependency error on `Gate` / `AuthzGuard`.
99
+
100
+ - [#67](https://github.com/DavideCarvalho/nestjs-authz/pull/67) [`32e967f`](https://github.com/DavideCarvalho/nestjs-authz/commit/32e967ffbcd68a97e8effc77af9c16bbbafdc918) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - `PolicyIndex` no longer freezes its first discovery scan: it rescans when a module or provider
101
+ is added (a lazy module) or a policy was not instantiated yet, so `resourcePolicies()`,
102
+ `classAbilities()` (the Inertia ability map) and `gate.scope()` see the same policies.
103
+
104
+ - [#67](https://github.com/DavideCarvalho/nestjs-authz/pull/67) [`32e967f`](https://github.com/DavideCarvalho/nestjs-authz/commit/32e967ffbcd68a97e8effc77af9c16bbbafdc918) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - `unannotated: 'allow'`: explain the official boot error. `@nestjs/authorization` 0.1.3 recognizes only its
105
+ own `AuthorizationGuard` when it checks at startup that each `@Can()` is enforced, so with
106
+ `globalGuard: false` it logs "`@Can() is not enforced on …`" for the routes the global `AuthzGuard`
107
+ enforces. `AuthzModule` now logs a line right after it that names the same handlers and says the
108
+ error is expected in this mode. `AuthzGuard` keeps no official brand on purpose: with it the
109
+ official check would report every route this mode lets through as denied. Documented in the
110
+ README's migration section.
111
+
3
112
  ## 0.8.1
4
113
 
5
114
  ### Patch Changes
package/README.md ADDED
@@ -0,0 +1,279 @@
1
+ <p align="center">
2
+ <a href="https://davidecarvalho.github.io/aviary/docs/authz">
3
+ <img src="https://raw.githubusercontent.com/DavideCarvalho/nestjs-authz/main/.github/banner.svg" alt="@dudousxd/nestjs-authz — an Aviary library. Call sign: Falcon.">
4
+ </a>
5
+ </p>
6
+
7
+ <p align="center">
8
+ <b><a href="https://davidecarvalho.github.io/aviary/docs/authz">📖 Read the documentation</a></b>
9
+ &nbsp;·&nbsp; part of the <a href="https://davidecarvalho.github.io/aviary/"><b>Aviary</b></a> ecosystem for NestJS
10
+ </p>
11
+
12
+ ---
13
+
14
+ # `@dudousxd/nestjs-authz`
15
+
16
+ Extras on top of NestJS's first-party [`@nestjs/authorization`](https://docs.nestjs.com/security/authorization).
17
+ Policies, `@Can()`, the global deny-by-default guard and `AuthorizationService` come from the
18
+ official package: read its docs first. This package adds what it does not have:
19
+
20
+ - **Persisted RBAC as official policies**: `RolePolicy` and `PermissionPolicy` (wildcards,
21
+ tenant-scoped roles, direct permissions, role sources), backed by the TypeORM / MikroORM /
22
+ Prisma / Drizzle adapters, fetched once per request.
23
+ - **Helpers for the official `@Can()`**: `byId(Post)`, `roles(...)`, `permission(...)`,
24
+ `ability(...)`, `staticCanArgs(...)`; sugar `@Roles(...)` / `@Permission(...)`.
25
+ - **A global super-admin** and an **external policy engine** (Cerbos, OPA, …): extend
26
+ `AuthzPolicy`.
27
+ - **A `Gate`** for services: string abilities, batches (`allowsMany`), ad-hoc gates
28
+ (`define`), roles, query scopes (`scope` + the adapters' `applyScope`).
29
+ - **Frontend and tooling**: Inertia props, a typed `can()` from codegen, client/React
30
+ helpers, test helpers, and an allow + deny decision event for Telescope.
31
+
32
+ See [`DESIGN.md`](https://github.com/DavideCarvalho/nestjs-authz/blob/main/DESIGN.md) for the design and the trade-offs.
33
+
34
+ ## Install
35
+
36
+ ```sh
37
+ pnpm add @nestjs/authorization @dudousxd/nestjs-authz
38
+ ```
39
+
40
+ Requires NestJS 12 and Node 22+. Two optional peers: `@nestjs/authentication` gives the `Gate`
41
+ its ambient user (`AuthenticationContext.user`); `@dudousxd/nestjs-context` gives it one too
42
+ (outside an authentication scope), plus the per-request permission cache and
43
+ tenant-scoped roles.
44
+
45
+ ```ts
46
+ @Module({
47
+ imports: [
48
+ AuthorizationModule.forRoot(), // official: guard, @Can, AuthorizationService
49
+ AuthzModule.forRoot({ superAdmin: (user) => user?.isRoot === true }),
50
+ ],
51
+ providers: [PostPolicy],
52
+ })
53
+ export class AppModule {}
54
+ ```
55
+
56
+ ## Policies (official) + `@PolicyFor`
57
+
58
+ ```ts
59
+ import { Can, Policy } from '@nestjs/authorization';
60
+ import { AuthzPolicy, PolicyFor, authorizedResource, byId } from '@dudousxd/nestjs-authz';
61
+
62
+ @Policy()
63
+ @PolicyFor(Post) // optional: links Post → PostPolicy for the extras
64
+ export class PostPolicy extends AuthzPolicy { // optional: honors `superAdmin`
65
+ update(user: User | null, post: Post) {
66
+ return post.authorId === user?.id;
67
+ }
68
+ }
69
+
70
+ @Controller('posts')
71
+ export class PostController {
72
+ @Patch(':id')
73
+ @Can(PostPolicy, 'update', byId(Post)) // byId loads the Post named by :id
74
+ update(@Req() req: Request) {
75
+ const post = authorizedResource(req, Post); // the record byId loaded, no second query
76
+ }
77
+ }
78
+ ```
79
+
80
+ `byId` uses the `ResourceResolver` given to `AuthzModule.forRoot({ resourceResolver })`
81
+ (default: an `{ id }` object from the route param; `idParam` renames it, `byId(Post, { param })`
82
+ per route). A record that is not found is a 403 before the policy runs.
83
+
84
+ `@PolicyFor(Post)` is what lets the extras go from a resource to its policy:
85
+ `gate.allows('update', post)`, `gate.scope(Post)`, the client ability map. The official API
86
+ never needs it, and `@PolicyFor` applies the official `@Policy()` itself.
87
+
88
+ ## Roles and permissions
89
+
90
+ ```ts
91
+ @Roles('admin', 'editor') // any of
92
+ @Can(RolePolicy, 'hasAll', roles('admin', 'editor')) // all of
93
+ @Permission('posts.publish') // any of; posts.* grants it
94
+ @Can(PermissionPolicy, 'hasAll', permission('posts.publish', 'posts.delete'))
95
+ ```
96
+
97
+ Roles come from `user.roles` / `user.role` (or `resolveRoles`) and from the RBAC adapter's role
98
+ store; permissions from the adapter:
99
+
100
+ ```ts
101
+ AuthzRbacModule.forRootAsync({
102
+ inject: [DataSource],
103
+ useFactory: (ds: DataSource) => ({ store: new TypeOrmAuthzStore(ds) }),
104
+ });
105
+ ```
106
+
107
+ Within one request (a nestjs-context store), a user's permissions and the adapter's roles are
108
+ fetched once, however many checks run: the official guard's `RolePolicy` / `PermissionPolicy`
109
+ and the handler's `Gate` calls share the fetch. The entry is keyed by the row the user names
110
+ (its class or `type`, plus `id`); a plain object without `type` is keyed by identity. A grant
111
+ made mid-request applies from the next request. Adapters set `exhaustivePermissions`, so a
112
+ permission the set lacks is answered without another query.
113
+
114
+ `roles()`, `permission()`, `ability()` and `staticCanArgs()` return resolvers whose arguments do
115
+ not depend on the call. They carry them on a non-enumerable `Symbol.for('@dudousxd/static-can-args')`
116
+ property, so tools such as the Aviary agent can evaluate a requirement without a request.
117
+
118
+ ## Super-admin
119
+
120
+ The official guard runs only each policy's own `before()`. Extend `AuthzPolicy` (or call
121
+ `superAdminBefore(user, ability, resource)` from your `before()`) and the `superAdmin` hook
122
+ grants every ability of that policy. The hook receives the resource (the ability's first
123
+ argument), so it can scope the bypass. `RolePolicy` and `PermissionPolicy` extend it.
124
+
125
+ ## The `Gate`
126
+
127
+ ```ts
128
+ constructor(private readonly gate: Gate) {}
129
+
130
+ await this.gate.forUser(user).authorize(PostPolicy, 'update', post); // = AuthorizationService.authorize
131
+ await this.gate.forUser(user).allows('update', post); // the policy linked to Post
132
+ await this.gate.forUser(user).allows('posts.publish'); // a persisted permission
133
+
134
+ this.gate.define('access-admin', (user) => user.isStaff);
135
+ // on a route: @Can(GatePolicy, 'allows', ability('access-admin'))
136
+
137
+ const results = await this.gate.forUser(user).allowsMany([
138
+ { ability: 'update', resource: post },
139
+ { policy: ReportPolicy, ability: 'export', args: ['csv'] },
140
+ ]); // one permission fetch
141
+
142
+ const where = await this.gate.forUser(user).scope(Post); // from PostPolicy.scope(user)
143
+ ```
144
+
145
+ String abilities ask `superAdmin`, then the external decision point (below), then the policy
146
+ linked to the resource, then the persisted permissions, then the ad-hoc gate. **The policy has
147
+ the final word**: holding a permission named like one of its abilities (`update`) does not
148
+ bypass it; only `superAdmin` does. Permissions grant the named abilities no policy defines
149
+ (`posts.publish`). The same goes for `scope()`: a permission scopes an entity only when no
150
+ policy method can.
151
+
152
+ Policy decisions are always the official service's. `authorize()` throws the official
153
+ `AuthorizationError`, which the official module turns into a 401/403.
154
+
155
+ Without `forUser`, the user is, in order:
156
+
157
+ 1. `@nestjs/authentication`'s `AuthenticationContext.user`, when that optional peer is
158
+ installed and its module imported (found through Nest's `ModuleRef`), and its scope is active:
159
+ a call it handles, or `AuthenticationContext.run()`. Its answer is final there: `null` is a
160
+ guest, and nestjs-context is not asked. It is `AsyncLocalStorage`-backed, correct in handlers
161
+ and everything they await;
162
+ 2. outside any authentication scope (a queue worker, a cron job, or without the package),
163
+ nestjs-context's user ref, hydrated by `resolveUser` (once per request) when configured;
164
+ 3. a guest (`null`).
165
+
166
+ ## Decisions
167
+
168
+ Every `Gate` check and every requirement the official guard enforced is published, allowed or
169
+ denied, on the `aviary:authz:decision` diagnostics channel (`authzDecisionChannel`). The official
170
+ package publishes denials only, on `nestjs:authorization:denied`.
171
+
172
+ ## Migrating from 0.8
173
+
174
+ 0.8 and earlier had their own engine. 0.9 runs on `@nestjs/authorization`, and the old
175
+ decorators are gone: move each one to its official form.
176
+
177
+ | 0.8 | 0.9 |
178
+ | --- | --- |
179
+ | `@Policy(Post)` | `@Policy()` from `@nestjs/authorization` + `@PolicyFor(Post)` (or `@PolicyFor(Post)` alone) |
180
+ | `@Can('update', Post)` | `@Can(PostPolicy, 'update', byId(Post))`, `@Can` from `@nestjs/authorization` |
181
+ | `@Can('create', Post, { classLevel: true })` | `@Can(PostPolicy, 'create')` |
182
+ | `@Can('access-admin')` | `@Can(GatePolicy, 'allows', ability('access-admin'))` |
183
+ | `@Roles('admin')` + `RolesGuard` | `@Roles('admin')`, enforced by the official guard |
184
+ | `CanGuard`, `RolesGuard`, `PolicyRegistry` | removed: the official `AuthorizationGuard`; `PolicyIndex` |
185
+ | `AuthzModule.forRoot({ policies })` | register policies as providers (or `AuthorizationModule.forRoot({ policies })`) |
186
+ | `superAdmin` applied everywhere | applied by `AuthzPolicy` subclasses, the built-in policies and the `Gate` |
187
+ | `after` hook, `{ allowed, message }` results | removed: only a literal `true` allows |
188
+ | current user from nestjs-context on routes | the official `getUser` (default `request.user`) |
189
+ | `gate.allows()` user from nestjs-context | `AuthenticationContext.user` inside its scope (`null` there is a guest), nestjs-context outside it |
190
+ | a persisted permission named like a policy ability grants it | the policy decides (only `superAdmin` bypasses it), in checks and in `scope()` |
191
+ | `DecisionProvider` / `superAdmin` see a guest as `undefined` | `null` |
192
+ | `DecisionProvider` asked by the Gate on every check | asked by `AuthzPolicy.before()` on routes, and by `Gate` string abilities |
193
+ | `import … from '@dudousxd/nestjs-authz-inertia/client'` | `import … from '@dudousxd/nestjs-authz-client'` (the subpath is removed) |
194
+
195
+ Behavior the official guard brings:
196
+
197
+ - **Deny by default.** A handler with no `@Can()` is denied; add `@Can.Anyone()` (or
198
+ `@Public()` from `@nestjs/authentication`) where anyone may call it.
199
+ - **Class and method requirements add up.** A class-level `@Roles` no longer gives way to the
200
+ method's.
201
+ - **Guests reach the policy as `null`.** Type the user `User | null`; a guest denial is a 401.
202
+
203
+ ### Migrating one controller at a time: `unannotated: 'allow'`
204
+
205
+ The official guard denies every route that declares no `@Can()`: the app's public routes, and
206
+ every controller another library mounts. 0.8 let them through. Turning authorization on for a
207
+ large app in one go is rarely possible, so this package has a migration mode (the official guard
208
+ has no such option):
209
+
210
+ 1. **Turn the official global guard off**, and switch the migration mode on:
211
+
212
+ ```ts
213
+ imports: [
214
+ AuthenticationModule.forRoot({ ... }), // or your Passport guard, first
215
+ AuthorizationModule.forRoot({ globalGuard: false }),
216
+ AuthzModule.forRoot({ unannotated: 'allow' }), // forRootAsync: next to useFactory
217
+ ]
218
+ ```
219
+
220
+ `AuthzModule` registers its own global `AuthzGuard`. A route with official `@Can()`
221
+ requirements (class or method, `@Roles`, `@Permission`) is handed
222
+ to the official `AuthorizationGuard` as is: same `getUser`, same policies, same 401 for a guest
223
+ and 403 for a user, same `denied` event. A route with no requirement is let through. The app
224
+ fails to start if the official global guard is still on, because it would keep denying them.
225
+
226
+ 2. **Drop the 0.8 guard and move the old decorators.** Remove `RolesGuard` / `CanGuard` from
227
+ `APP_GUARD` and `@UseGuards()`, and rewrite each string `@Can` / `@Policy(Post)` with the table
228
+ above (`@Can('page.documents')` → `@Can(GatePolicy, 'allows', ability('page.documents'))`).
229
+ `@Roles('admin')` and `@Permission(...)` are unchanged. All of them are enforced by `AuthzGuard`.
230
+
231
+ 3. **Annotate the open routes**, one controller at a time: `@Can.Anyone()` where anyone may call
232
+ it, `@Public()` (from `@nestjs/authentication`) where no sign-in is needed, a `@Can(...)`
233
+ everywhere else.
234
+
235
+ 4. **Switch to deny by default** when every route declares something: remove `unannotated` (or set
236
+ `'deny'`) and `globalGuard: false`. The official guard is global again, and a forgotten
237
+ `@Can()` is a 403 instead of an open route.
238
+
239
+ A route that slipped through in step 3 is open in this mode, so keep it for the migration only.
240
+
241
+ ## External policy engines (Cerbos, OPA, …)
242
+
243
+ Register a `DecisionProvider` under `DECISION_PROVIDER` to hand decisions to an external policy
244
+ decision point. Unlike the grant-only RBAC seam, it can **allow or deny** a resource-aware check.
245
+ Returning `undefined` abstains. The provider can also batch `gate.allowsMany`, and it can map the
246
+ engine's query plan onto `gate.scope()`. It runs right after the `superAdmin` hook:
247
+
248
+ - on routes, in the `before()` of every policy that extends `AuthzPolicy`, with the ability and
249
+ its first argument (`byId(Post)`'s record). `RolePolicy` and `PermissionPolicy` do not ask it:
250
+ their abilities name roles and permissions, not resources. Call `decisionProviderBefore()`
251
+ from your own `before()` when a policy cannot extend `AuthzPolicy`;
252
+ - in the `Gate`, before the policy, the permissions and the ad-hoc gate of a string ability.
253
+ `allowsMany` asks `decideMany` once for the batch.
254
+
255
+ A guest reaches it as `null`. A Cerbos adapter ships in the box as a subpath with no extra
256
+ dependency; bring your own `@cerbos/http` or `@cerbos/grpc` client:
257
+
258
+ ```ts
259
+ import { DECISION_PROVIDER } from '@dudousxd/nestjs-authz';
260
+ import { CerbosDecisionProvider } from '@dudousxd/nestjs-authz/cerbos';
261
+ import { HTTP } from '@cerbos/http';
262
+
263
+ providers: [{
264
+ provide: DECISION_PROVIDER,
265
+ useValue: new CerbosDecisionProvider({
266
+ client: new HTTP(process.env.CERBOS_URL!), // or (user) => per-tenant client | undefined
267
+ principal: (u) => u && { id: u.id, roles: u.roles, attr: { tenantId: u.tenantId } },
268
+ resource: (ability, r) => (r instanceof Post ? { kind: 'app:post', id: r.id, attr: { ownerId: r.ownerId } } : undefined),
269
+ scope: { resource: (entity) => (entity === Post ? { kind: 'app:post' } : undefined) },
270
+ }),
271
+ }]
272
+ ```
273
+
274
+ The adapter fails **closed** by default. An unreachable PDP denies, and an unsupported query plan
275
+ scopes to no rows. Set `onFailure: 'abstain'` to fall back to your own policies instead.
276
+
277
+ ## License
278
+
279
+ MIT © Davi Carvalho
@@ -0,0 +1,48 @@
1
+ import { AuthorizationGuard } from '@nestjs/authorization';
2
+ import { type CanActivate, type ExecutionContext, type OnApplicationBootstrap, type OnModuleInit } from '@nestjs/common';
3
+ import { DiscoveryService, MetadataScanner } from '@nestjs/core';
4
+ /**
5
+ * The migration guard `AuthzModule.forRoot({ unannotated: 'allow' })` registers globally.
6
+ *
7
+ * A route with official `@Can()` requirements (on the class or the method, including
8
+ * `@Roles` and `@Permission`, which write the same metadata) is
9
+ * handed to the official `AuthorizationGuard` as is: the same evaluator, the same `getUser`,
10
+ * the same 401/403, the same denied event. A route that declares no requirement is let
11
+ * through, where the official guard would deny it. That lets an app turn authorization on
12
+ * one controller at a time.
13
+ *
14
+ * It needs `AuthorizationModule.forRoot({ globalGuard: false })`: the official global guard
15
+ * would still deny the unannotated routes. The application fails to start when both run.
16
+ *
17
+ * It deliberately does not carry the official guard's brand. `@nestjs/authorization` 0.1.3
18
+ * recognizes a guard that enforces `@Can()` only by that brand, and with it every route this
19
+ * guard lets through would be logged as "AuthorizationGuard denies every call to …". Without it,
20
+ * the official module logs "@Can() is not enforced on …" for the routes this guard does enforce;
21
+ * {@link AuthzGuardStartupCheck} explains that line right after it.
22
+ */
23
+ export declare class AuthzGuard implements CanActivate {
24
+ private readonly official;
25
+ constructor(official?: AuthorizationGuard);
26
+ canActivate(context: ExecutionContext): Promise<boolean> | boolean;
27
+ }
28
+ /**
29
+ * With `unannotated: 'allow'`:
30
+ *
31
+ * - fails the startup when the official module still registers its own global guard: that guard
32
+ * runs too, and denies every unannotated route, so the option would do nothing;
33
+ * - after the official module's startup check, explains its "@Can() is not enforced on …" error.
34
+ * That check sees only `AuthorizationGuard` (by its brand), not the global `AuthzGuard` that
35
+ * enforces those routes, so it reports them at every boot in this mode.
36
+ */
37
+ export declare class AuthzGuardStartupCheck implements OnModuleInit, OnApplicationBootstrap {
38
+ private readonly discovery;
39
+ private readonly metadataScanner;
40
+ private readonly logger;
41
+ constructor(discovery: DiscoveryService, metadataScanner: MetadataScanner);
42
+ /** Runs after every `onModuleInit()`, so after the official error it explains. */
43
+ onApplicationBootstrap(): void;
44
+ /** Handlers with a `@Can()` requirement and no official guard of their own: AuthzGuard's alone. */
45
+ private routesOnlyAuthzGuardEnforces;
46
+ onModuleInit(): void;
47
+ }
48
+ //# sourceMappingURL=authz.guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authz.guard.d.ts","sourceRoot":"","sources":["../src/authz.guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EACL,KAAK,WAAW,EAChB,KAAK,gBAAgB,EAIrB,KAAK,sBAAsB,EAC3B,KAAK,YAAY,EAGlB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAMjE;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBACa,UAAW,YAAW,WAAW;IAC5C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;gBAEM,QAAQ,CAAC,EAAE,kBAAkB;IAIjF,WAAW,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO;CAInE;AAuBD;;;;;;;;GAQG;AACH,qBACa,sBAAuB,YAAW,YAAY,EAAE,sBAAsB;IAI/E,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,eAAe;IAJlC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA6B;gBAGjC,SAAS,EAAE,gBAAgB,EAC3B,eAAe,EAAE,eAAe;IAGnD,kFAAkF;IAClF,sBAAsB,IAAI,IAAI;IAc9B,mGAAmG;IACnG,OAAO,CAAC,4BAA4B;IAuBpC,YAAY,IAAI,IAAI;CAgBrB"}
@@ -0,0 +1,144 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
11
+ return function (target, key) { decorator(target, key, paramIndex); }
12
+ };
13
+ import { AuthorizationGuard } from '@nestjs/authorization';
14
+ import { Inject, Injectable, Logger, Optional, } from '@nestjs/common';
15
+ import { GUARDS_METADATA } from '@nestjs/common/constants.js';
16
+ import { DiscoveryService, MetadataScanner } from '@nestjs/core';
17
+ import { requireOfficial, routeRequirements } from './official.js';
18
+ /** The static brand `@nestjs/authorization` puts on `AuthorizationGuard` (and its subclasses). */
19
+ const OFFICIAL_GUARD_BRAND = Symbol.for('@nestjs/authorization:guard');
20
+ /**
21
+ * The migration guard `AuthzModule.forRoot({ unannotated: 'allow' })` registers globally.
22
+ *
23
+ * A route with official `@Can()` requirements (on the class or the method, including
24
+ * `@Roles` and `@Permission`, which write the same metadata) is
25
+ * handed to the official `AuthorizationGuard` as is: the same evaluator, the same `getUser`,
26
+ * the same 401/403, the same denied event. A route that declares no requirement is let
27
+ * through, where the official guard would deny it. That lets an app turn authorization on
28
+ * one controller at a time.
29
+ *
30
+ * It needs `AuthorizationModule.forRoot({ globalGuard: false })`: the official global guard
31
+ * would still deny the unannotated routes. The application fails to start when both run.
32
+ *
33
+ * It deliberately does not carry the official guard's brand. `@nestjs/authorization` 0.1.3
34
+ * recognizes a guard that enforces `@Can()` only by that brand, and with it every route this
35
+ * guard lets through would be logged as "AuthorizationGuard denies every call to …". Without it,
36
+ * the official module logs "@Can() is not enforced on …" for the routes this guard does enforce;
37
+ * {@link AuthzGuardStartupCheck} explains that line right after it.
38
+ */
39
+ let AuthzGuard = class AuthzGuard {
40
+ official;
41
+ constructor(official) {
42
+ this.official = requireOfficial(official, 'AuthorizationGuard', 'AuthzGuard');
43
+ }
44
+ canActivate(context) {
45
+ if (routeRequirements(context.getClass(), context.getHandler()).length === 0)
46
+ return true;
47
+ return this.official.canActivate(context);
48
+ }
49
+ };
50
+ AuthzGuard = __decorate([
51
+ Injectable(),
52
+ __param(0, Optional()),
53
+ __param(0, Inject(AuthorizationGuard)),
54
+ __metadata("design:paramtypes", [AuthorizationGuard])
55
+ ], AuthzGuard);
56
+ export { AuthzGuard };
57
+ /** True when `value` is the official `AuthorizationGuard` (or a subclass of it). */
58
+ function isOfficialGuard(value) {
59
+ const type = value?.constructor;
60
+ return typeof type === 'function' && type[OFFICIAL_GUARD_BRAND] === true;
61
+ }
62
+ /** True when `guard` (a class from `@UseGuards()`, or an instance) carries the official brand. */
63
+ function isBrandedGuard(guard) {
64
+ const type = typeof guard === 'function' ? guard : guard?.constructor;
65
+ return (typeof type === 'function' &&
66
+ Boolean(type[OFFICIAL_GUARD_BRAND]));
67
+ }
68
+ const list = (wheres) => wheres.length > 3
69
+ ? `${wheres.slice(0, 3).join(', ')} and ${wheres.length - 3} more`
70
+ : wheres.join(', ');
71
+ /**
72
+ * With `unannotated: 'allow'`:
73
+ *
74
+ * - fails the startup when the official module still registers its own global guard: that guard
75
+ * runs too, and denies every unannotated route, so the option would do nothing;
76
+ * - after the official module's startup check, explains its "@Can() is not enforced on …" error.
77
+ * That check sees only `AuthorizationGuard` (by its brand), not the global `AuthzGuard` that
78
+ * enforces those routes, so it reports them at every boot in this mode.
79
+ */
80
+ let AuthzGuardStartupCheck = class AuthzGuardStartupCheck {
81
+ discovery;
82
+ metadataScanner;
83
+ logger = new Logger('AuthzModule');
84
+ constructor(discovery, metadataScanner) {
85
+ this.discovery = discovery;
86
+ this.metadataScanner = metadataScanner;
87
+ }
88
+ /** Runs after every `onModuleInit()`, so after the official error it explains. */
89
+ onApplicationBootstrap() {
90
+ const enforced = this.routesOnlyAuthzGuardEnforces();
91
+ if (enforced.length === 0)
92
+ return;
93
+ const official = '"@Can() is not enforced on …"';
94
+ this.logger.log([
95
+ `unannotated: 'allow': the global AuthzGuard enforces @Can() on ${list(enforced)}.`,
96
+ `@nestjs/authorization logs ${official} for these handlers because it only recognizes`,
97
+ 'its own AuthorizationGuard; in this mode that error is expected. It goes away when you',
98
+ "remove `unannotated: 'allow'` and turn the official global guard back on.",
99
+ ].join(' '));
100
+ }
101
+ /** Handlers with a `@Can()` requirement and no official guard of their own: AuthzGuard's alone. */
102
+ routesOnlyAuthzGuardEnforces() {
103
+ const wheres = [];
104
+ const seen = new Set();
105
+ for (const wrapper of [...this.discovery.getControllers(), ...this.discovery.getProviders()]) {
106
+ const type = wrapper.metatype;
107
+ if (typeof type !== 'function' || wrapper.isFactory || !type.prototype || seen.has(type)) {
108
+ continue;
109
+ }
110
+ seen.add(type);
111
+ const classGuards = Reflect.getMetadata(GUARDS_METADATA, type) ?? [];
112
+ for (const key of this.metadataScanner.getAllMethodNames(type.prototype)) {
113
+ const method = type.prototype[key];
114
+ // An undecorated method under a class-level @Can() is a helper, not a handler.
115
+ if (typeof method !== 'function' || Reflect.getOwnMetadataKeys(method).length === 0)
116
+ continue;
117
+ if (routeRequirements(type, method).length === 0)
118
+ continue;
119
+ const guards = [...classGuards, ...(Reflect.getMetadata(GUARDS_METADATA, method) ?? [])];
120
+ if (!guards.some(isBrandedGuard))
121
+ wheres.push(`${type.name}.${key}`);
122
+ }
123
+ }
124
+ return wheres;
125
+ }
126
+ onModuleInit() {
127
+ const officialGlobal = this.discovery
128
+ .getProviders()
129
+ .some((wrapper) => typeof wrapper.token === 'string' &&
130
+ wrapper.token.startsWith('APP_GUARD') &&
131
+ isOfficialGuard(wrapper.instance));
132
+ if (officialGlobal) {
133
+ throw new Error("AuthzModule: `unannotated: 'allow'` needs AuthorizationModule.forRoot({ globalGuard: false }). " +
134
+ "The official global guard is registered too, and it denies every route that declares no @Can(), so 'allow' would have no effect.");
135
+ }
136
+ }
137
+ };
138
+ AuthzGuardStartupCheck = __decorate([
139
+ Injectable(),
140
+ __metadata("design:paramtypes", [DiscoveryService,
141
+ MetadataScanner])
142
+ ], AuthzGuardStartupCheck);
143
+ export { AuthzGuardStartupCheck };
144
+ //# sourceMappingURL=authz.guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"authz.guard.js","sourceRoot":"","sources":["../src/authz.guard.ts"],"names":[],"mappings":"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"}
@@ -30,25 +30,6 @@ export interface CanBatchResultItem {
30
30
  } | null;
31
31
  allowed: boolean;
32
32
  }
33
- /**
34
- * Build the opt-in fallback controller class, mounted at `path`. Kept as a
35
- * factory so the route path is configurable via `AuthzModule.forRoot({ canEndpoint })`.
36
- *
37
- * The endpoint runs `gate.allows(ability, resource?)` for the CURRENT (context)
38
- * user.
39
- *
40
- * RESOURCE REHYDRATION — how per-instance decisions resolve:
41
- * the client sends a `{ type, id }` shim whose constructor is `Object`, which never
42
- * matches a registered instance `@Policy` (those are keyed by the entity constructor).
43
- * To bridge that, register `resourceLoaders` in `AuthzModule.forRoot({ resourceLoaders })`
44
- * (keyed by the `type` name): when a loader exists for `resource.type`, the endpoint
45
- * `await`s `loader(id)` and authorizes the REAL entity, so the instance `@Policy`
46
- * matches and its method runs. A loader returning nullish → `{ allowed: false }` (not
47
- * found). With NO loader for the type the raw shim is forwarded as before, so only
48
- * class-level abilities and ad-hoc gates resolve and a resource-bound ability denies
49
- * (a deliberate, documented fall-through, not a silent dead-end). An unresolved ability
50
- * is likewise treated as a deny (never a 500).
51
- */
52
33
  export declare function createCanController(path: string): Type<unknown>;
53
34
  /** Default mount path for the fallback endpoint (matches codegen's `/authz/can`). */
54
35
  export declare const DEFAULT_CAN_ENDPOINT_PATH = "authz/can";
@@ -1 +1 @@
1
- {"version":3,"file":"can-endpoint.controller.d.ts","sourceRoot":"","sources":["../src/can-endpoint.controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsC,KAAK,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAM/E;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CAC1D;AAED,gDAAgD;AAChD,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;GAKG;AACH,MAAM,MAAM,mBAAmB,GAAG,cAAc,EAAE,CAAC;AAEnD,iFAAiF;AACjF,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACzD,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,CAiH/D;AAED,qFAAqF;AACrF,eAAO,MAAM,yBAAyB,cAAc,CAAC"}
1
+ {"version":3,"file":"can-endpoint.controller.d.ts","sourceRoot":"","sources":["../src/can-endpoint.controller.ts"],"names":[],"mappings":"AACA,OAAO,EAML,KAAK,IAAI,EAEV,MAAM,gBAAgB,CAAC;AAOxB;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;CAC1D;AAED,gDAAgD;AAChD,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;GAKG;AACH,MAAM,MAAM,mBAAmB,GAAG,cAAc,EAAE,CAAC;AAEnD,iFAAiF;AACjF,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACzD,OAAO,EAAE,OAAO,CAAC;CAClB;AAwBD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,CAyH/D;AAED,qFAAqF;AACrF,eAAO,MAAM,yBAAyB,cAAc,CAAC"}