@dudousxd/nestjs-authz 0.8.0 → 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 (132) hide show
  1. package/CHANGELOG.md +115 -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 -209
  32. package/dist/gate.d.ts.map +1 -1
  33. package/dist/gate.js +209 -577
  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/permission-cache.d.ts +18 -9
  56. package/dist/permission-cache.d.ts.map +1 -1
  57. package/dist/permission-cache.js +87 -24
  58. package/dist/permission-cache.js.map +1 -1
  59. package/dist/permission-matcher.d.ts +13 -0
  60. package/dist/permission-matcher.d.ts.map +1 -1
  61. package/dist/permission-matcher.js +38 -2
  62. package/dist/permission-matcher.js.map +1 -1
  63. package/dist/permission-provider.d.ts +11 -0
  64. package/dist/permission-provider.d.ts.map +1 -1
  65. package/dist/policies/authz-policy.d.ts +42 -0
  66. package/dist/policies/authz-policy.d.ts.map +1 -0
  67. package/dist/policies/authz-policy.js +80 -0
  68. package/dist/policies/authz-policy.js.map +1 -0
  69. package/dist/policies/gate.policy.d.ts +18 -0
  70. package/dist/policies/gate.policy.d.ts.map +1 -0
  71. package/dist/policies/gate.policy.js +37 -0
  72. package/dist/policies/gate.policy.js.map +1 -0
  73. package/dist/policies/index.d.ts +5 -0
  74. package/dist/policies/index.d.ts.map +1 -0
  75. package/dist/policies/index.js +5 -0
  76. package/dist/policies/index.js.map +1 -0
  77. package/dist/policies/permission.policy.d.ts +27 -0
  78. package/dist/policies/permission.policy.d.ts.map +1 -0
  79. package/dist/policies/permission.policy.js +61 -0
  80. package/dist/policies/permission.policy.js.map +1 -0
  81. package/dist/policies/role.policy.d.ts +27 -0
  82. package/dist/policies/role.policy.d.ts.map +1 -0
  83. package/dist/policies/role.policy.js +51 -0
  84. package/dist/policies/role.policy.js.map +1 -0
  85. package/dist/policy-index.d.ts +51 -0
  86. package/dist/policy-index.d.ts.map +1 -0
  87. package/dist/policy-index.js +135 -0
  88. package/dist/policy-index.js.map +1 -0
  89. package/dist/rbac.d.ts +92 -0
  90. package/dist/rbac.d.ts.map +1 -0
  91. package/dist/rbac.js +293 -0
  92. package/dist/rbac.js.map +1 -0
  93. package/dist/resource-resolver.d.ts +15 -17
  94. package/dist/resource-resolver.d.ts.map +1 -1
  95. package/dist/resource-resolver.js +8 -8
  96. package/dist/resource-resolver.js.map +1 -1
  97. package/dist/runtime.d.ts +31 -0
  98. package/dist/runtime.d.ts.map +1 -0
  99. package/dist/runtime.js +40 -0
  100. package/dist/runtime.js.map +1 -0
  101. package/dist/tokens.d.ts +4 -8
  102. package/dist/tokens.d.ts.map +1 -1
  103. package/dist/tokens.js +4 -8
  104. package/dist/tokens.js.map +1 -1
  105. package/dist/types.d.ts +52 -169
  106. package/dist/types.d.ts.map +1 -1
  107. package/package.json +19 -10
  108. package/skills/authz-enforcement/SKILL.md +67 -131
  109. package/skills/authz-policies/SKILL.md +43 -117
  110. package/skills/authz-query-scopes/SKILL.md +16 -9
  111. package/skills/authz-rbac-seams/SKILL.md +39 -12
  112. package/skills/authz-setup/SKILL.md +45 -106
  113. package/dist/decorator/can.decorator.d.ts +0 -29
  114. package/dist/decorator/can.decorator.d.ts.map +0 -1
  115. package/dist/decorator/can.decorator.js +0 -23
  116. package/dist/decorator/can.decorator.js.map +0 -1
  117. package/dist/decorator/policy.decorator.d.ts +0 -18
  118. package/dist/decorator/policy.decorator.d.ts.map +0 -1
  119. package/dist/decorator/policy.decorator.js +0 -28
  120. package/dist/decorator/policy.decorator.js.map +0 -1
  121. package/dist/guard/can.guard.d.ts +0 -31
  122. package/dist/guard/can.guard.d.ts.map +0 -1
  123. package/dist/guard/can.guard.js +0 -92
  124. package/dist/guard/can.guard.js.map +0 -1
  125. package/dist/guard/roles.guard.d.ts +0 -21
  126. package/dist/guard/roles.guard.d.ts.map +0 -1
  127. package/dist/guard/roles.guard.js +0 -50
  128. package/dist/guard/roles.guard.js.map +0 -1
  129. package/dist/policy-registry.d.ts +0 -46
  130. package/dist/policy-registry.d.ts.map +0 -1
  131. package/dist/policy-registry.js +0 -101
  132. package/dist/policy-registry.js.map +0 -1
package/dist/gate.js CHANGED
@@ -10,223 +10,51 @@ var __metadata = (this && this.__metadata) || function (k, v) {
10
10
  var __param = (this && this.__param) || function (paramIndex, decorator) {
11
11
  return function (target, key) { decorator(target, key, paramIndex); }
12
12
  };
13
- import { InjectCapability } from '@dudousxd/nestjs-diagnostics/nestjs';
14
- import { ForbiddenException, Inject, Injectable, Optional } from '@nestjs/common';
15
- import { ModuleRef } from '@nestjs/core';
13
+ import { AuthorizationError, AuthorizationService } from '@nestjs/authorization';
14
+ import { Inject, Injectable, Optional } from '@nestjs/common';
15
+ import { normalizeVerdict } from './decision-provider.js';
16
16
  import { publishAuthzDecision } from './diagnostics.js';
17
17
  import { AbilityNotResolvedException, AmbiguousAbilityException } from './errors/exceptions.js';
18
+ import { isOfficialPolicy, requireOfficial } from './official.js';
18
19
  import { PermissionCache } from './permission-cache.js';
19
- import { permissionSatisfied } from './permission-matcher.js';
20
- import { PolicyRegistry } from './policy-registry.js';
21
- import { defaultRoleResolver } from './role-provider.js';
20
+ import { withAskedCheck } from './policies/authz-policy.js';
21
+ import { PolicyIndex } from './policy-index.js';
22
+ import { AuthzRbac } from './rbac.js';
22
23
  import { normalizeScope, scopeAll, scopeNone, } from './scope.js';
23
- import { AUTHZ_MODULE_OPTIONS, CONTEXT_ACCESSOR, DECISION_PROVIDER, PERMISSION_PROVIDER, ROLE_PROVIDER, } from './tokens.js';
24
- /** Normalize a `boolean | PolicyResponse | null | undefined` into a {@link NormalizedResult}. */
25
- function normalizeResult(value) {
26
- if (value == null)
27
- return { allowed: undefined };
28
- if (typeof value === 'boolean')
29
- return { allowed: value };
30
- // `exactOptionalPropertyTypes`: only attach `message` when actually present.
31
- return value.message === undefined
32
- ? { allowed: Boolean(value.allowed) }
33
- : { allowed: Boolean(value.allowed), message: value.message };
34
- }
35
- /**
36
- * Attach `message` to a decision only when defined (honors `exactOptionalPropertyTypes`,
37
- * which forbids an explicit `message: undefined` on an optional property).
38
- */
39
- function withMessage(decision, message) {
40
- return message === undefined ? decision : { ...decision, message };
41
- }
42
- /**
43
- * Build the `ForbiddenException` for a denied `authorize(...)`, surfacing the
44
- * decision's `reason` (and the policy/hook `message` when present) instead of
45
- * discarding it. The message string defaults to the generic `Unauthorized: <ability>`
46
- * for backward compatibility; the structured body carries `ability` + `reason` so a
47
- * 403 is debuggable by clients and the diagnostics watcher alike.
48
- */
49
- function forbidden(ability, decision) {
50
- const message = decision.message ?? `Unauthorized: ${ability}`;
51
- return new ForbiddenException({
52
- statusCode: 403,
53
- error: 'Forbidden',
54
- message,
55
- ability,
56
- reason: decision.reason,
57
- });
58
- }
59
- // A sentinel marking "no user resolved" distinct from a legitimately-`undefined`
60
- // user. `forUser(undefined)` explicitly authorizes an anonymous user.
61
- const NO_USER = Symbol('authz:no-user');
24
+ import { AUTHZ_MODULE_OPTIONS } from './tokens.js';
25
+ const isPolicyCheck = (item) => typeof item.policy === 'function';
62
26
  /**
63
- * Key under which the per-request {@link PermissionCache} is stashed on the
64
- * nestjs-context store. `Symbol.for` so the same key resolves across copies of
65
- * this module (e.g. multiple installs in a monorepo) sharing one store.
66
- */
67
- const REQUEST_PERMISSION_CACHE = Symbol.for('@dudousxd/nestjs-authz:request-permission-cache');
68
- /**
69
- * Laravel-style authorization Gate.
27
+ * A façade over the official `AuthorizationService` for service-layer checks, with what
28
+ * the official service does not have:
70
29
  *
71
- * Resolves an ability against either a registered `@Policy` method (by ability
72
- * name, matched against the resource's policy) or an ad-hoc gate defined via
73
- * {@link define}. Applies the global `superAdmin` before-hook and the per-policy
74
- * `before` hook with short-circuit semantics.
30
+ * - **string abilities**, Laravel-style: `gate.allows('update', post)` finds the policy
31
+ * linked to `Post` with `@PolicyFor(Post)`; `gate.allows('access-admin')` runs an
32
+ * ad-hoc gate from {@link define}; `gate.allows('posts.publish')` asks the persisted
33
+ * permissions. Asks `superAdmin` first, then the external decision point
34
+ * (`DECISION_PROVIDER`), then the policy — which has the final word: a persisted
35
+ * permission named like the ability does not bypass it — then the persisted
36
+ * permissions, then the ad-hoc gate.
37
+ * - **official policy checks**: `gate.allows(PostPolicy, 'update', post)`, exactly
38
+ * `AuthorizationService.can()`.
39
+ * - **batches** ({@link allowsMany}) sharing one permission fetch, **query scopes**
40
+ * ({@link scope}), **roles** ({@link hasRole}), an allow+deny decision event, and an
41
+ * ambient user when none is passed ({@link forUser} passes one): `@nestjs/authentication`'s
42
+ * `AuthenticationContext.user` when that package is installed, else nestjs-context's.
75
43
  *
76
- * The current user comes from the optional {@link ContextAccessor} (nestjs-context)
77
- * when present, or is supplied explicitly via {@link forUser}.
44
+ * Every policy decision is the official service's: the policy's own `before()`, then
45
+ * the ability, allowed only on a literal `true`.
78
46
  */
79
47
  let Gate = class Gate {
80
- policies;
81
- context;
82
- moduleRef;
83
- permissionProvider;
84
- roleProvider;
85
- decisionProvider;
48
+ index;
49
+ rbac;
86
50
  gates = new Map();
87
51
  superAdmin;
88
- after;
89
- resolveUser;
90
- roleResolver;
91
- // Memoized ModuleRef seam lookups. These tokens are singletons bound once at
92
- // module init, so the (potentially throwing) non-strict container scan only
93
- // needs to run once per seam — never on every authorization check.
94
- contextResolved = false;
95
- contextCached;
96
- permissionProviderResolved = false;
97
- permissionProviderCached;
98
- roleProviderResolved = false;
99
- roleProviderCached;
100
- decisionProviderResolved = false;
101
- decisionProviderCached;
102
- constructor(policies, options, context, moduleRef, permissionProvider, roleProvider, decisionProvider) {
103
- this.policies = policies;
104
- this.context = context;
105
- this.moduleRef = moduleRef;
106
- this.permissionProvider = permissionProvider;
107
- this.roleProvider = roleProvider;
108
- this.decisionProvider = decisionProvider;
52
+ authz;
53
+ constructor(authz, index, rbac, options) {
54
+ this.index = index;
55
+ this.rbac = rbac;
56
+ this.authz = requireOfficial(authz, 'AuthorizationService', 'Gate');
109
57
  this.superAdmin = options?.superAdmin;
110
- this.after = options?.after;
111
- this.resolveUser = options?.resolveUser;
112
- this.roleResolver = options?.resolveRoles ?? defaultRoleResolver;
113
- }
114
- /**
115
- * Locate the context accessor. Prefers the value injected into this module;
116
- * falls back to a non-strict {@link ModuleRef} lookup so an accessor provided
117
- * by ANY module (e.g. a global ContextModule, or the app root) is still found.
118
- */
119
- resolveContext() {
120
- if (this.context)
121
- return this.context;
122
- if (this.contextResolved)
123
- return this.contextCached;
124
- this.contextResolved = true;
125
- if (!this.moduleRef)
126
- return this.contextCached;
127
- try {
128
- this.contextCached = this.moduleRef.get(CONTEXT_ACCESSOR, { strict: false });
129
- }
130
- catch {
131
- this.contextCached = undefined;
132
- }
133
- return this.contextCached;
134
- }
135
- /**
136
- * Locate the optional {@link PermissionProvider} (the RBAC seam). Prefers the value
137
- * injected into this module; falls back to a non-strict {@link ModuleRef} lookup so a
138
- * provider registered by ANY module (e.g. the RBAC adapter's global module) is found.
139
- */
140
- resolvePermissionProvider() {
141
- if (this.permissionProvider)
142
- return this.permissionProvider;
143
- if (this.permissionProviderResolved)
144
- return this.permissionProviderCached;
145
- this.permissionProviderResolved = true;
146
- if (!this.moduleRef)
147
- return this.permissionProviderCached;
148
- try {
149
- this.permissionProviderCached = this.moduleRef.get(PERMISSION_PROVIDER, {
150
- strict: false,
151
- });
152
- }
153
- catch {
154
- this.permissionProviderCached = undefined;
155
- }
156
- return this.permissionProviderCached;
157
- }
158
- /**
159
- * Locate the optional {@link RoleProvider} (the coarse role seam). Prefers the value
160
- * injected into this module; falls back to a non-strict {@link ModuleRef} lookup so a
161
- * provider registered by ANY module (e.g. the RBAC adapter's global module) is found.
162
- */
163
- resolveRoleProvider() {
164
- if (this.roleProvider)
165
- return this.roleProvider;
166
- if (this.roleProviderResolved)
167
- return this.roleProviderCached;
168
- this.roleProviderResolved = true;
169
- if (!this.moduleRef)
170
- return this.roleProviderCached;
171
- try {
172
- this.roleProviderCached = this.moduleRef.get(ROLE_PROVIDER, { strict: false });
173
- }
174
- catch {
175
- this.roleProviderCached = undefined;
176
- }
177
- return this.roleProviderCached;
178
- }
179
- /**
180
- * Locate the optional {@link DecisionProvider} (the external-PDP seam). Same lookup strategy as
181
- * {@link resolvePermissionProvider}: injected value first, then a memoized non-strict scan.
182
- */
183
- resolveDecisionProvider() {
184
- if (this.decisionProvider)
185
- return this.decisionProvider;
186
- if (this.decisionProviderResolved)
187
- return this.decisionProviderCached;
188
- this.decisionProviderResolved = true;
189
- if (!this.moduleRef)
190
- return this.decisionProviderCached;
191
- try {
192
- this.decisionProviderCached = this.moduleRef.get(DECISION_PROVIDER, {
193
- strict: false,
194
- });
195
- }
196
- catch {
197
- this.decisionProviderCached = undefined;
198
- }
199
- return this.decisionProviderCached;
200
- }
201
- /**
202
- * The per-request {@link PermissionCache}, stashed on the nestjs-context store so
203
- * every check within one HTTP request shares a single `getPermissions` fetch
204
- * (kills the N+1 a list page would otherwise cause). Returns `undefined` when no
205
- * context store is available (standalone use), in which case callers fall back to
206
- * a transient cache (e.g. one per `allowsMany` batch) or no memoization.
207
- */
208
- resolveRequestCache() {
209
- const context = this.resolveContext();
210
- // `get()` is part of the ContextAccessor contract but a stub/partial accessor
211
- // may omit it (or it may throw outside a request scope) — degrade to "no
212
- // request cache" rather than failing the check.
213
- if (!context || typeof context.get !== 'function')
214
- return undefined;
215
- let store;
216
- try {
217
- store = context.get();
218
- }
219
- catch {
220
- store = undefined;
221
- }
222
- if (!store)
223
- return undefined;
224
- const existing = store[REQUEST_PERMISSION_CACHE];
225
- if (existing instanceof PermissionCache)
226
- return existing;
227
- const cache = new PermissionCache();
228
- store[REQUEST_PERMISSION_CACHE] = cache;
229
- return cache;
230
58
  }
231
59
  /** Register an ad-hoc, model-less gate resolved by `ability` name. */
232
60
  define(ability, fn) {
@@ -237,445 +65,253 @@ let Gate = class Gate {
237
65
  hasGate(ability) {
238
66
  return this.gates.has(ability);
239
67
  }
240
- /**
241
- * Names of every ad-hoc gate registered via {@link define}. Used by integrations
242
- * (e.g. `@dudousxd/nestjs-authz-inertia`) that enumerate the user's class-level
243
- * abilities to share them as Inertia props — no network round-trip needed.
244
- */
68
+ /** Names of every ad-hoc gate registered with {@link define}. */
245
69
  gateNames() {
246
70
  return [...this.gates.keys()];
247
71
  }
248
72
  /**
249
- * Bind an explicit user, bypassing the context accessor. Use when no
250
- * nestjs-context is wired, or to check a user other than the current one.
251
- *
252
- * A nullish user (`forUser(undefined)` / `forUser(null)`) is an explicit
253
- * anonymous request: it maps to the same deny path as an unauthenticated
254
- * context (`NO_USER`), so policies/gates are never invoked with `undefined` and
255
- * never throw. The `resolveUser` hook is NOT applied to `forUser` — the value
256
- * you pass is used verbatim.
73
+ * Bind an explicit user. `null`/`undefined` is a guest. The usual way to check in a
74
+ * service: pass the user your handler received.
257
75
  */
258
76
  forUser(user) {
259
- return new BoundGate(this, user == null ? NO_USER : user);
77
+ return new BoundGate(this, user ?? null);
260
78
  }
261
79
  /**
262
- * Resolve the current user from the context accessor, or `NO_USER`.
263
- * When a `resolveUser` hook is configured, hydrate the full entity from the
264
- * context's {@link UserRef}; a hook returning nullish maps to `NO_USER`.
80
+ * The ambient user, for calls without {@link forUser}: `@nestjs/authentication`'s
81
+ * `AuthenticationContext.user` when that optional peer is installed and has one, else
82
+ * nestjs-context's user ref (hydrated by `resolveUser` when configured, once per
83
+ * request). `null` without either.
265
84
  */
266
- async currentUser() {
267
- const context = this.resolveContext();
268
- if (!context)
269
- return NO_USER;
270
- const ref = context.userRef();
271
- if (ref === undefined)
272
- return NO_USER;
273
- if (this.resolveUser) {
274
- const hydrated = await this.resolveUser(ref);
275
- return hydrated == null ? NO_USER : hydrated;
276
- }
277
- return ref;
85
+ currentUser() {
86
+ return this.rbac.currentUser();
278
87
  }
279
- // --- public API (operates on the current/context user) ---
280
- async allows(ability, resource) {
281
- return this.check(await this.currentUser(), ability, resource, this.resolveRequestCache());
88
+ async allows(first, ...rest) {
89
+ return this.allowsFor(await this.currentUser(), first, rest);
282
90
  }
283
- async denies(ability, resource) {
284
- return !(await this.allows(ability, resource));
91
+ async denies(first, ...rest) {
92
+ return !(await this.allowsFor(await this.currentUser(), first, rest));
285
93
  }
286
- async authorize(ability, resource) {
287
- const decision = await this.decide(await this.currentUser(), ability, resource, this.resolveRequestCache());
288
- if (!decision.allowed)
289
- throw forbidden(ability, decision);
94
+ async authorize(first, ...rest) {
95
+ return this.authorizeFor(await this.currentUser(), first, rest);
290
96
  }
291
- /**
292
- * Authorize MANY abilities for the current (context) user in a single pass. The
293
- * user is resolved ONCE and a single {@link PermissionCache} is shared across all
294
- * items, so a list page (N cards) costs one user resolution + at most one
295
- * permission fetch instead of N of each. Order is preserved; each result echoes
296
- * its input `ability`/`resource` plus the `allowed` verdict.
297
- *
298
- * Fault-isolating: an item whose ability is unresolved/ambiguous denies just
299
- * THAT item (it does not throw and abort the batch) — a batch is a best-effort
300
- * verdict list, not an enforcement gate.
301
- */
97
+ /** Check many abilities at once (one user, one permission fetch). A failing item denies. */
302
98
  async allowsMany(items) {
303
- return this.checkMany(await this.currentUser(), items);
99
+ return this.allowsManyFor(await this.currentUser(), items);
304
100
  }
305
101
  /**
306
- * Resolve the QUERY SCOPE constraint for the current (context) user against
307
- * `entity` — the `accessibleBy` / Pundit `policy_scope` concept. Returns an
308
- * ORM-neutral {@link ScopeConstraint} an adapter (e.g. the TypeORM `applyScope`
309
- * helper) turns into a parameterized `WHERE`.
310
- *
311
- * Consistent with the Gate's single-resource semantics:
312
- * - super-admin / before-grant / permission-provider grant → `allow-all`,
313
- * - anonymous, or no policy/scope → `deny-all` (default-deny),
314
- * - otherwise the policy's `scope` (or `viewAny`-style) constraint.
315
- *
316
- * `ability` (default `'viewAny'`) names the permission-provider check used for
317
- * the allow-all grant and the policy fallback method.
102
+ * The query scope of `entity` (a resource class linked with `@PolicyFor`, or the
103
+ * policy class itself) for the current user, from the policy's `scope(user, entity)`
104
+ * method (else the `ability` method, `viewAny` by default). See {@link ScopeConstraint}.
318
105
  */
319
106
  async scope(entity, ability = 'viewAny') {
320
- return this.resolveScope(await this.currentUser(), entity, ability);
107
+ return this.scopeFor(await this.currentUser(), entity, ability);
321
108
  }
322
- // --- coarse role checks (operate on the current/context user) ---
323
- /** True when the current user holds `role`. */
109
+ /** True when the current user holds `role` (a fact: `superAdmin` does not apply). */
324
110
  async hasRole(role) {
325
- return this.checkRoles(await this.currentUser(), [role]);
111
+ return this.rbac.hasAnyRole(await this.currentUser(), [role]);
326
112
  }
327
- /** True when the current user holds ANY of `roles`. */
113
+ /** True when the current user holds any of `roles`. */
328
114
  async hasAnyRole(roles) {
329
- return this.checkRoles(await this.currentUser(), roles);
115
+ return this.rbac.hasAnyRole(await this.currentUser(), roles);
330
116
  }
331
- // --- internal: used by BoundGate too ---
117
+ // --- explicit user (BoundGate) ---
332
118
  /** @internal */
333
- allowsForUser(user, ability, resource) {
334
- return this.check(user, ability, resource, this.resolveRequestCache());
119
+ async allowsFor(user, first, rest) {
120
+ return (await this.decide(user, first, rest, this.rbac.requestCache(), false)).allowed;
335
121
  }
336
- /** @internal — batch check for an explicit/bound user (see {@link allowsMany}). */
337
- allowsManyForUser(user, items) {
338
- return this.checkMany(user, items);
122
+ /** @internal */
123
+ async authorizeFor(user, first, rest) {
124
+ const decision = await this.decide(user, first, rest, this.rbac.requestCache(), true);
125
+ if (!decision.allowed) {
126
+ throw new AuthorizationError(user == null ? 'unauthenticated' : 'forbidden', decision.policy, typeof first === 'string' ? first : String(rest[0]));
127
+ }
339
128
  }
340
- /**
341
- * Run a batch of checks against one already-resolved user, sharing a single
342
- * {@link PermissionCache} so the provider's `getPermissions` runs at most once.
343
- * Prefers the per-request cache (so the batch also coalesces with other checks in
344
- * the same request); falls back to a transient per-batch cache when standalone.
345
- */
346
- async checkMany(user, items) {
347
- const cache = this.resolveRequestCache() ?? new PermissionCache();
129
+ /** @internal */
130
+ async allowsManyFor(user, items) {
131
+ const cache = this.rbac.requestCache() ?? new PermissionCache();
348
132
  const prefetched = await this.prefetchDecisions(user, items);
349
133
  const out = [];
350
134
  for (const [index, item] of items.entries()) {
351
135
  let allowed;
352
136
  try {
353
- allowed = (await this.decide(user, item.ability, item.resource, cache, prefetched?.[index]))
137
+ const [first, rest] = isPolicyCheck(item)
138
+ ? [item.policy, [item.ability, ...(item.args ?? [])]]
139
+ : [item.ability, item.resource === undefined ? [] : [item.resource]];
140
+ allowed = (await this.decide(user, first, rest, cache, false, prefetched.get(index)))
354
141
  .allowed;
355
142
  }
356
143
  catch {
357
- // Unresolved/ambiguous ability → deny just this item (don't abort the batch).
358
144
  allowed = false;
359
145
  }
360
- out.push(item.resource === undefined
361
- ? { ability: item.ability, allowed }
362
- : { ability: item.ability, resource: item.resource, allowed });
146
+ out.push({ ...item, allowed });
363
147
  }
364
148
  return out;
365
149
  }
366
150
  /**
367
- * One {@link DecisionProvider.decideMany} round-trip for a whole batch, when the provider
368
- * supports it. Returns `undefined` (→ per-item `decide`) otherwise. A batch-call failure makes
369
- * every item fall back to its own `decide` call rather than failing the batch.
151
+ * One `decideMany` round-trip to the external decision point for the string abilities
152
+ * of a batch, when it supports batching. Empty otherwise, and when the batch call fails
153
+ * (each item then asks `decide` on its own).
370
154
  */
371
- async prefetchDecisions(maybeUser, items) {
372
- const provider = this.resolveDecisionProvider();
373
- if (!provider || typeof provider.decideMany !== 'function' || items.length === 0) {
374
- return undefined;
155
+ async prefetchDecisions(user, items) {
156
+ const out = new Map();
157
+ const provider = this.rbac.decisions();
158
+ if (!provider || typeof provider.decideMany !== 'function')
159
+ return out;
160
+ const indexes = [];
161
+ const requests = [];
162
+ for (const [index, item] of items.entries()) {
163
+ if (isPolicyCheck(item))
164
+ continue;
165
+ indexes.push(index);
166
+ requests.push(item.resource === undefined
167
+ ? { ability: item.ability }
168
+ : { ability: item.ability, resource: item.resource });
375
169
  }
376
- const user = maybeUser === NO_USER ? undefined : maybeUser;
170
+ if (requests.length === 0)
171
+ return out;
377
172
  try {
378
- const verdicts = await provider.decideMany(user, items.map((i) => i.resource === undefined
379
- ? { ability: i.ability }
380
- : { ability: i.ability, resource: i.resource }));
381
- if (!Array.isArray(verdicts) || verdicts.length !== items.length)
382
- return undefined;
383
- return verdicts.map((verdict) => ({ verdict }));
173
+ const verdicts = await provider.decideMany(user, requests);
174
+ if (!Array.isArray(verdicts) || verdicts.length !== requests.length)
175
+ return out;
176
+ for (const [i, index] of indexes.entries())
177
+ out.set(index, { verdict: verdicts[i] });
384
178
  }
385
179
  catch {
386
- return undefined;
180
+ out.clear();
387
181
  }
388
- }
389
- /** @internal — used by {@link BoundGate.authorize} to surface the deny reason/message. */
390
- decideForUser(user, ability, resource) {
391
- return this.decide(user, ability, resource, this.resolveRequestCache());
182
+ return out;
392
183
  }
393
184
  /** @internal */
394
- hasAnyRoleForUser(user, roles) {
395
- return this.checkRoles(user, roles);
185
+ async hasAnyRoleFor(user, roles) {
186
+ return this.rbac.hasAnyRole(user, roles);
396
187
  }
397
- /** @internal — used by {@link BoundGate.scope}. */
398
- scopeForUser(user, entity, ability = 'viewAny') {
399
- return this.resolveScope(user, entity, ability);
400
- }
401
- /**
402
- * Resolve a query-scope constraint for `entity`, mirroring {@link resolveBase}'s
403
- * grant order so scoping stays consistent with single-resource decisions:
404
- *
405
- * 1. super-admin hook grants → `allow-all`;
406
- * 1b. decision provider `planScope` → its constraint (unless it abstains);
407
- * 2. (authenticated) permission-provider grant for `ability` → `allow-all`;
408
- * 3. the policy's `scope` (or `viewAny`-style) method → its constraint;
409
- * 4. otherwise (anonymous, no policy, no scope method) → `deny-all`.
410
- *
411
- * The policy `before` hook is also honored as an allow/deny grant before the
412
- * scope method runs, so a policy that broadly allows/denies in `before` scopes
413
- * the same way it decides.
414
- */
415
- async resolveScope(maybeUser, entity, ability) {
416
- const user = maybeUser === NO_USER ? undefined : maybeUser;
417
- // 1. Global super-admin hook → allow-all (a `false` does not deny here, it
418
- // just falls through, mirroring resolveBase's grant-or-fall-through).
419
- const sa = normalizeResult(await this.superAdmin?.(user, ability, entity));
420
- if (sa.allowed === true)
188
+ /** @internal */
189
+ async scopeFor(user, entity, ability) {
190
+ if ((await this.superAdmin?.(user, ability, entity)) === true)
421
191
  return scopeAll;
422
- // 1b. External decision point's query plan (e.g. Cerbos PlanResources) → used as-is;
423
- // `undefined` abstains.
424
- const planner = this.resolveDecisionProvider();
192
+ // The external decision point's query plan (e.g. Cerbos PlanResources), used as-is.
193
+ const planner = this.rbac.decisions();
425
194
  if (planner && typeof planner.planScope === 'function') {
426
195
  const planned = await planner.planScope(user, entity, ability);
427
196
  if (planned !== undefined)
428
197
  return planned;
429
198
  }
430
- // 2. Permission-provider grant for the scope ability → allow-all.
431
- if (maybeUser !== NO_USER &&
432
- (await this.permissionProviderGrants(user, ability, undefined, this.resolveRequestCache()))) {
433
- return scopeAll;
199
+ // The policy has the final word; a persisted permission only scopes an entity
200
+ // without a policy method to ask.
201
+ const policy = isOfficialPolicy(entity) ? entity : this.index.policyFor(entity);
202
+ const instance = policy
203
+ ? this.index.instanceOf(policy)
204
+ : undefined;
205
+ const method = (instance && typeof instance.scope === 'function' ? instance.scope : instance?.[ability]);
206
+ if (!instance || typeof method !== 'function') {
207
+ return (await this.rbac.grants(user, ability)) ? scopeAll : scopeNone;
434
208
  }
435
- // 3. Policy scope (or viewAny-style fallback) method.
436
- const policy = this.policies.forResource(entity);
437
- if (policy) {
438
- const scopeMethod = this.resolveScopeMethod(policy, ability);
439
- if (scopeMethod) {
440
- const before = policy.before;
441
- if (typeof before === 'function') {
442
- const result = normalizeResult(await before.call(policy, user, ability));
443
- if (result.allowed === true)
444
- return scopeAll;
445
- if (result.allowed === false)
446
- return scopeNone;
447
- }
448
- // Anonymous users see no rows (default-deny, like the single-resource path).
449
- if (maybeUser === NO_USER)
450
- return scopeNone;
451
- return normalizeScope(await scopeMethod.call(policy, user, entity));
452
- }
209
+ const before = instance.before;
210
+ if (typeof before === 'function') {
211
+ const early = await before.call(instance, user ?? null, ability);
212
+ if (early === true)
213
+ return scopeAll;
214
+ if (early === false)
215
+ return scopeNone;
453
216
  }
454
- // 4. No way to scope → deny-all.
455
- return scopeNone;
456
- }
457
- /**
458
- * Pick the policy's scope method: the conventional `scope`, else the `ability`
459
- * method (e.g. `viewAny`) when it is scope-shaped. Returns `undefined` when the
460
- * policy declares neither.
461
- */
462
- resolveScopeMethod(policy, ability) {
463
- const scope = policy.scope;
464
- if (typeof scope === 'function')
465
- return scope;
466
- const fallback = policy[ability];
467
- if (typeof fallback === 'function')
468
- return fallback;
469
- return undefined;
470
- }
471
- /**
472
- * Whether the registered {@link PermissionProvider} grants `ability` for an authenticated `user` —
473
- * the RBAC "before" grant shared by the single-resource ({@link resolveBase}) and scope
474
- * ({@link resolveScope}) paths, single-sourced here so the grant order can't drift between them.
475
- *
476
- * Grant-only: a `false`/nullish result is "no grant", never a deny. When the provider lists the
477
- * user's granted permissions, segment-based wildcard matching runs in the core (a granted
478
- * `posts.*` satisfies `posts.update`, `*` satisfies anything) regardless of adapter; a nullish
479
- * list defers to the exact-match `hasPermission`. The granted set is fetched through `cache` when
480
- * one is supplied, else via a direct fetch.
481
- */
482
- async permissionProviderGrants(user, ability, resource, cache) {
483
- const provider = this.resolvePermissionProvider();
484
- if (!provider)
485
- return false;
486
- if (typeof provider.getPermissions === 'function') {
487
- const satisfied = cache
488
- ? await cache.satisfies(provider, user, ability)
489
- : await (async () => {
490
- const granted = await provider.getPermissions?.(user);
491
- return granted == null ? undefined : permissionSatisfied(granted, ability);
492
- })();
493
- if (satisfied === true)
494
- return true;
495
- }
496
- return (await provider.hasPermission(user, ability, resource)) === true;
497
- }
498
- /**
499
- * Resolve the user's effective roles and test membership against `roles`. Returns
500
- * `false` for an anonymous (NO_USER) caller and whenever no source yields a role
501
- * (deny-by-default). Roles come from the UNION of the default/overridden
502
- * {@link RoleResolver} (reads the user object) and the optional {@link RoleProvider}
503
- * seam (a persisted store) — so an app needs neither to opt in.
504
- */
505
- async checkRoles(maybeUser, roles) {
506
- if (maybeUser === NO_USER || roles.length === 0)
507
- return false;
508
- const userRoles = await this.rolesOf(maybeUser);
509
- if (userRoles.size === 0)
510
- return false;
511
- return roles.some((r) => userRoles.has(r));
512
- }
513
- /** The current user's effective role names (resolver ∪ provider). */
514
- async rolesOf(user) {
515
- const out = new Set();
516
- const fromResolver = await this.roleResolver(user);
517
- if (Array.isArray(fromResolver)) {
518
- for (const r of fromResolver)
519
- if (typeof r === 'string')
520
- out.add(r);
521
- }
522
- const provider = this.resolveRoleProvider();
523
- if (provider) {
524
- const fromProvider = await provider.getRoles(user);
525
- if (Array.isArray(fromProvider)) {
526
- for (const r of fromProvider)
527
- if (typeof r === 'string')
528
- out.add(r);
529
- }
530
- }
531
- return out;
532
- }
533
- async check(maybeUser, ability, resource, cache) {
534
- return (await this.decide(maybeUser, ability, resource, cache)).allowed;
535
- }
536
- /**
537
- * Resolve a decision AND publish it for observers. Returns the full decision
538
- * (verdict + reason + optional deny message) so {@link authorize} can surface the
539
- * `reason`/`message` on the thrown `ForbiddenException`. {@link check} discards
540
- * everything but the verdict.
541
- */
542
- async decide(maybeUser, ability, resource, cache, prefetched) {
543
- const decision = await this.resolve(maybeUser, ability, resource, cache, prefetched);
544
- // Emit the decision for observers (e.g. the generic @dudousxd/nestjs-diagnostics-telescope watcher).
545
- // Loosely coupled via a diagnostics channel — zero-overhead when no subscriber,
546
- // and a publish failure can never affect the verdict. Only reached decisions are
547
- // emitted; an unresolved/ambiguous ability throws above and is intentionally silent.
548
- publishAuthzDecision(ability, decision.allowed, decision.reason, maybeUser === NO_USER ? undefined : maybeUser, resource);
217
+ if (user == null)
218
+ return scopeNone;
219
+ return normalizeScope(await method.call(instance, user, entity));
220
+ }
221
+ // --- resolution ---
222
+ /** Decide, publish the decision, and (in `authorize` mode) let the official service emit its denial. */
223
+ async decide(user, first, rest, cache, authorizing, prefetched) {
224
+ const ability = typeof first === 'string' ? first : String(rest[0]);
225
+ const resource = typeof first === 'string'
226
+ ? rest[0]
227
+ : rest[1];
228
+ const decision = typeof first === 'string'
229
+ ? await this.decideString(user, first, resource, cache, authorizing, prefetched)
230
+ : await this.decidePolicy(user, first, ability, rest.slice(1), authorizing);
231
+ publishAuthzDecision(ability, decision.allowed, decision.reason, user, resource, {
232
+ policy: decision.policy,
233
+ });
549
234
  return decision;
550
235
  }
551
- /**
552
- * Resolve an ability to a final verdict, the path that decided it, and an optional
553
- * deny message. Runs the global `after` hook over the base decision (see
554
- * {@link AfterHook} for override semantics). Throws
555
- * {@link AbilityNotResolvedException}/{@link AmbiguousAbilityException} when no
556
- * decision can be reached (those paths emit no decision and skip `after`).
557
- */
558
- async resolve(maybeUser, ability, resource, cache, prefetched) {
559
- const user = maybeUser === NO_USER ? undefined : maybeUser;
560
- const base = await this.resolveBase(maybeUser, ability, resource, cache, prefetched);
561
- // Global `after` hook (Laravel `Gate::after`). It may OVERRIDE only when the
562
- // base path produced no explicit verdict (`base.allowed === undefined`);
563
- // otherwise it is observe-only. A nullish/void return never changes anything.
564
- if (this.after) {
565
- const observed = base.allowed; // undefined ⇒ policy/gate had no opinion
566
- const after = normalizeResult(await this.after(user, ability, observed, resource));
567
- if (observed === undefined && after.allowed !== undefined) {
568
- return withMessage({ allowed: after.allowed, reason: 'after' }, after.message);
236
+ async decidePolicy(user, policy, ability, args, authorizing) {
237
+ const service = this.authz;
238
+ let allowed;
239
+ if (authorizing) {
240
+ try {
241
+ await service.authorize(policy, ability, user, ...args);
242
+ allowed = true;
569
243
  }
244
+ catch (error) {
245
+ if (!(error instanceof AuthorizationError))
246
+ throw error;
247
+ allowed = false;
248
+ }
249
+ }
250
+ else {
251
+ allowed = await service.can(policy, ability, user, ...args);
570
252
  }
571
- // Finalize: a base with "no opinion" (undefined) denies by default.
572
- return withMessage({ allowed: base.allowed === true, reason: base.reason }, base.message);
253
+ return { allowed, reason: 'policy', policy: policy.name };
573
254
  }
574
- /**
575
- * The pre-`after` resolution. `allowed` is `undefined` when the deciding path
576
- * (policy method / ad-hoc gate) returned a nullish value — "no opinion" — which
577
- * lets the {@link AfterHook} supply a verdict before the default-deny finalize.
578
- */
579
- async resolveBase(maybeUser, ability, resource, cache, prefetched) {
580
- const user = maybeUser === NO_USER ? undefined : maybeUser;
581
- // Global super-admin hook first (with the resource, so it can scope the bypass).
582
- const sa = normalizeResult(await this.superAdmin?.(user, ability, resource));
583
- if (sa.allowed === true)
584
- return withMessage({ allowed: true, reason: 'super-admin' }, sa.message);
585
- if (sa.allowed === false)
586
- return withMessage({ allowed: false, reason: 'super-admin' }, sa.message);
587
- // External decision point (Cerbos/OPA/...): authoritative allow OR deny; `undefined` abstains.
588
- const decisionProvider = this.resolveDecisionProvider();
589
- if (decisionProvider) {
590
- const verdict = normalizeResult(prefetched ? prefetched.verdict : await decisionProvider.decide(user, ability, resource));
255
+ async decideString(user, ability, resource, cache, authorizing, prefetched) {
256
+ if ((await this.superAdmin?.(user, ability, resource)) === true) {
257
+ return { allowed: true, reason: 'super-admin', policy: null };
258
+ }
259
+ // External decision point (Cerbos/OPA/...): its allow or deny is final; `undefined` abstains.
260
+ const provider = this.rbac.decisions();
261
+ if (provider) {
262
+ const verdict = normalizeVerdict(prefetched ? prefetched.verdict : await provider.decide(user, ability, resource));
591
263
  if (verdict.allowed !== undefined) {
592
- return withMessage({ allowed: verdict.allowed, reason: 'decision-provider' }, verdict.message);
264
+ return { allowed: verdict.allowed, reason: 'decision-provider', policy: null };
593
265
  }
594
266
  }
595
- // RBAC seam (Laravel/spatie `Gate::before` grant): if a PermissionProvider is
596
- // registered and the (authenticated) user holds the named permission, grant it.
597
- // Grant-only — a `false`/`undefined` result falls through to normal resolution,
598
- // so this never *denies* an ability a policy/gate would otherwise allow.
599
- if (maybeUser !== NO_USER &&
600
- (await this.permissionProviderGrants(user, ability, resource, cache))) {
601
- return { allowed: true, reason: 'permission-provider' };
602
- }
603
- const policy = this.resolvePolicy(ability, resource);
267
+ // A policy that defines the ability has the final word: a persisted permission
268
+ // named like the ability does not bypass it.
269
+ const policy = this.policyFor(ability, resource);
604
270
  if (policy) {
605
- const method = policy[ability];
606
- // The `before` hook may only answer abilities the policy actually defines.
607
- // Gate it on method existence FIRST so a policy with a `before` but no
608
- // matching method falls through to AbilityNotResolved instead of letting
609
- // `before` grant/deny an ability the policy never declared.
610
- if (typeof method === 'function') {
611
- const before = policy.before;
612
- if (typeof before === 'function') {
613
- const result = normalizeResult(await before.call(policy, user, ability));
614
- if (result.allowed === true)
615
- return withMessage({ allowed: true, reason: 'policy-before' }, result.message);
616
- if (result.allowed === false)
617
- return withMessage({ allowed: false, reason: 'policy-before' }, result.message);
618
- }
619
- // Anonymous users are denied unless a hook granted access above.
620
- if (maybeUser === NO_USER)
621
- return { allowed: false, reason: 'anonymous' };
622
- const result = normalizeResult((await method.call(policy, user, resource)));
623
- // A nullish policy return is "no opinion" → leave `allowed` undefined so
624
- // an `after` hook may decide; otherwise it default-denies in `resolve`.
625
- return withMessage({ allowed: result.allowed, reason: 'policy' }, result.message);
626
- }
271
+ const args = resource !== undefined && typeof resource !== 'function' ? [resource] : [];
272
+ // superAdmin and the decision point were asked above: an `AuthzPolicy` `before()`
273
+ // must not ask them again for this same check.
274
+ return withAskedCheck(user, ability, args, () => this.decidePolicy(user, policy, ability, args, authorizing));
275
+ }
276
+ if (await this.rbac.grants(user, ability, resource, cache)) {
277
+ return { allowed: true, reason: 'permission-provider', policy: null };
627
278
  }
628
- // Fall back to an ad-hoc gate.
629
279
  const gate = this.gates.get(ability);
630
280
  if (gate) {
631
- if (maybeUser === NO_USER)
632
- return { allowed: false, reason: 'anonymous' };
633
- const result = normalizeResult((await gate(user, resource)));
634
- return withMessage({ allowed: result.allowed, reason: 'gate' }, result.message);
281
+ if (user == null)
282
+ return { allowed: false, reason: 'anonymous', policy: null };
283
+ return { allowed: (await gate(user, resource)) === true, reason: 'gate', policy: null };
635
284
  }
636
285
  throw new AbilityNotResolvedException(ability);
637
286
  }
638
- resolvePolicy(ability, resource) {
287
+ /** The linked policy defining `ability` for `resource` (or for no resource, unambiguously). */
288
+ policyFor(ability, resource) {
639
289
  if (resource === undefined) {
640
- // Class-level ability with no resource: scan registered policies for the
641
- // method. If MORE THAN ONE policy defines it, picking by Map-insertion
642
- // order would be a silent, arbitrary (and likely wrong) choice — throw so
643
- // the caller disambiguates by passing the resource class explicitly.
644
- const matches = this.policies
645
- .all()
646
- .filter((policy) => typeof policy[ability] === 'function');
647
- if (matches.length === 0)
648
- return undefined;
290
+ const matches = this.index.linkedPoliciesWith(ability);
649
291
  if (matches.length > 1) {
650
- throw new AmbiguousAbilityException(ability, matches.map((p) => p.constructor?.name ?? 'Policy'));
292
+ throw new AmbiguousAbilityException(ability, matches.map((p) => p.name));
651
293
  }
652
294
  return matches[0];
653
295
  }
654
- // Resource may be an instance or a class (for class-level abilities passed as Type).
655
- if (typeof resource === 'function') {
656
- return this.policies.forResource(resource);
657
- }
658
- return this.policies.forInstance(resource);
296
+ const policy = this.index.policyOf(resource);
297
+ if (!policy)
298
+ return undefined;
299
+ return typeof policy.prototype[ability] === 'function'
300
+ ? policy
301
+ : undefined;
659
302
  }
660
303
  };
661
304
  Gate = __decorate([
662
305
  Injectable(),
663
- __param(1, Optional()),
664
- __param(1, Inject(AUTHZ_MODULE_OPTIONS)),
665
- __param(2, InjectCapability('context', 'accessor')),
306
+ __param(0, Optional()),
307
+ __param(0, Inject(AuthorizationService)),
666
308
  __param(3, Optional()),
667
- __param(4, Optional()),
668
- __param(4, Inject(PERMISSION_PROVIDER)),
669
- __param(5, Optional()),
670
- __param(5, Inject(ROLE_PROVIDER)),
671
- __param(6, Optional()),
672
- __param(6, Inject(DECISION_PROVIDER)),
673
- __metadata("design:paramtypes", [PolicyRegistry, Object, Object, ModuleRef, Object, Object, Object])
309
+ __param(3, Inject(AUTHZ_MODULE_OPTIONS)),
310
+ __metadata("design:paramtypes", [Object, PolicyIndex,
311
+ AuthzRbac, Object])
674
312
  ], Gate);
675
313
  export { Gate };
676
- /**
677
- * A {@link Gate} bound to an explicit user. Returned by {@link Gate.forUser}.
678
- */
314
+ /** A {@link Gate} bound to an explicit user. Returned by {@link Gate.forUser}. */
679
315
  export class BoundGate {
680
316
  gate;
681
317
  user;
@@ -683,32 +319,28 @@ export class BoundGate {
683
319
  this.gate = gate;
684
320
  this.user = user;
685
321
  }
686
- allows(ability, resource) {
687
- return this.gate.allowsForUser(this.user, ability, resource);
322
+ allows(first, ...rest) {
323
+ return this.gate.allowsFor(this.user, first, rest);
324
+ }
325
+ async denies(first, ...rest) {
326
+ return !(await this.gate.allowsFor(this.user, first, rest));
688
327
  }
689
- async denies(ability, resource) {
690
- return !(await this.allows(ability, resource));
328
+ authorize(first, ...rest) {
329
+ return this.gate.authorizeFor(this.user, first, rest);
691
330
  }
692
- /** Batch-authorize many abilities for the bound user in one pass (see {@link Gate.allowsMany}). */
331
+ /** Batch-check for the bound user (see {@link Gate.allowsMany}). */
693
332
  allowsMany(items) {
694
- return this.gate.allowsManyForUser(this.user, items);
333
+ return this.gate.allowsManyFor(this.user, items);
695
334
  }
696
- /** Resolve the query-scope constraint for the bound user (see {@link Gate.scope}). */
335
+ /** The query scope for the bound user (see {@link Gate.scope}). */
697
336
  scope(entity, ability = 'viewAny') {
698
- return this.gate.scopeForUser(this.user, entity, ability);
699
- }
700
- async authorize(ability, resource) {
701
- const decision = await this.gate.decideForUser(this.user, ability, resource);
702
- if (!decision.allowed)
703
- throw forbidden(ability, decision);
337
+ return this.gate.scopeFor(this.user, entity, ability);
704
338
  }
705
- /** True when the bound user holds `role`. */
706
339
  hasRole(role) {
707
- return this.gate.hasAnyRoleForUser(this.user, [role]);
340
+ return this.gate.hasAnyRoleFor(this.user, [role]);
708
341
  }
709
- /** True when the bound user holds ANY of `roles`. */
710
342
  hasAnyRole(roles) {
711
- return this.gate.hasAnyRoleForUser(this.user, roles);
343
+ return this.gate.hasAnyRoleFor(this.user, roles);
712
344
  }
713
345
  }
714
346
  //# sourceMappingURL=gate.js.map