@dudousxd/nestjs-authz 0.2.0 → 0.3.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 (46) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/can-endpoint.controller.d.ts +35 -0
  3. package/dist/can-endpoint.controller.d.ts.map +1 -0
  4. package/dist/can-endpoint.controller.js +68 -0
  5. package/dist/can-endpoint.controller.js.map +1 -0
  6. package/dist/decorator/roles.decorator.d.ts +15 -0
  7. package/dist/decorator/roles.decorator.d.ts.map +1 -0
  8. package/dist/decorator/roles.decorator.js +19 -0
  9. package/dist/decorator/roles.decorator.js.map +1 -0
  10. package/dist/diagnostics.d.ts +42 -0
  11. package/dist/diagnostics.d.ts.map +1 -0
  12. package/dist/diagnostics.js +68 -0
  13. package/dist/diagnostics.js.map +1 -0
  14. package/dist/gate.d.ts +42 -1
  15. package/dist/gate.d.ts.map +1 -1
  16. package/dist/gate.js +116 -12
  17. package/dist/gate.js.map +1 -1
  18. package/dist/guard/roles.guard.d.ts +21 -0
  19. package/dist/guard/roles.guard.d.ts.map +1 -0
  20. package/dist/guard/roles.guard.js +50 -0
  21. package/dist/guard/roles.guard.js.map +1 -0
  22. package/dist/index.d.ts +9 -1
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +6 -1
  25. package/dist/index.js.map +1 -1
  26. package/dist/module.d.ts +5 -0
  27. package/dist/module.d.ts.map +1 -1
  28. package/dist/module.js +27 -1
  29. package/dist/module.js.map +1 -1
  30. package/dist/permission-provider.d.ts +2 -0
  31. package/dist/permission-provider.d.ts.map +1 -1
  32. package/dist/policy-registry.d.ts +21 -0
  33. package/dist/policy-registry.d.ts.map +1 -1
  34. package/dist/policy-registry.js +42 -0
  35. package/dist/policy-registry.js.map +1 -1
  36. package/dist/role-provider.d.ts +40 -0
  37. package/dist/role-provider.d.ts.map +1 -0
  38. package/dist/role-provider.js +32 -0
  39. package/dist/role-provider.js.map +1 -0
  40. package/dist/tokens.d.ts +14 -0
  41. package/dist/tokens.d.ts.map +1 -1
  42. package/dist/tokens.js +14 -0
  43. package/dist/tokens.js.map +1 -1
  44. package/dist/types.d.ts +33 -0
  45. package/dist/types.d.ts.map +1 -1
  46. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,75 @@
1
1
  # @dudousxd/nestjs-authz
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#2](https://github.com/DavideCarvalho/nestjs-authz/pull/2) [`2ecb0f4`](https://github.com/DavideCarvalho/nestjs-authz/commit/2ecb0f46342fa4527fc01f1097720f2e7bcf9aa7) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Add coarse, role-based authorization alongside the granular ability checks.
8
+
9
+ - **`@Roles('admin', 'teacher')` + `RolesGuard`**: a route is allowed when the current
10
+ user holds ANY of the listed roles. Registered as an `APP_GUARD` by `AuthzModule`
11
+ (inert on un-annotated routes), it resolves the current user exactly as the `Gate`
12
+ does and denies an unauthenticated request by default.
13
+ - **`Gate.hasRole(role)` / `Gate.hasAnyRole(roles[])`** (and `gate.forUser(user).hasRole(...)`),
14
+ async, resolving the user's effective roles and testing membership.
15
+ - **Pluggable role source, two layers (unioned):**
16
+ 1. A default `RoleResolver` that reads roles off the user object — `user.roles`
17
+ (`string[]`) OR `user.role` (`string | string[]`), normalized to a `string[]`.
18
+ This makes role checks work with ZERO RBAC tables. Override via
19
+ `AuthzModule.forRoot({ resolveRoles })`.
20
+ 2. An OPTIONAL `ROLE_PROVIDER` seam (`Symbol.for('@dudousxd/nestjs-authz:role-provider')`,
21
+ consulted with `@Optional()`) — mirroring `PERMISSION_PROVIDER` — so an RBAC adapter
22
+ can supply roles from a store. When both yield roles, the Gate unions them; when
23
+ neither does, the check denies.
24
+
25
+ Exports the `Roles` decorator, `RolesGuard`, `RoleResolver`, `defaultRoleResolver`,
26
+ `ROLE_PROVIDER`, `ROLES_METADATA`, and a `RoleProvider` interface. Purely additive — the
27
+ existing permission-provider / can-endpoint / diagnostics behavior is unchanged.
28
+
29
+ ### Patch Changes
30
+
31
+ - [#2](https://github.com/DavideCarvalho/nestjs-authz/pull/2) [`2ecb0f4`](https://github.com/DavideCarvalho/nestjs-authz/commit/2ecb0f46342fa4527fc01f1097720f2e7bcf9aa7) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Add `@dudousxd/nestjs-authz-inertia`: a 3-tier Inertia integration that lets client `can(...)`
32
+ checks resolve **without a network request** (the Laravel/Inertia model).
33
+
34
+ - **Tier 1 — shared props (no request):** `AuthzInertiaModule` (a global interceptor that calls
35
+ `req.inertia.share(...)`) or `createAuthzShare(gate, policyRegistry)` (a factory for
36
+ `InertiaModule.forRoot({ share })`) resolves the current user's class-level abilities — all
37
+ ad-hoc gates + class-level `@Policy` methods — into `props.auth.can` via direct in-process
38
+ `gate.allows(...)` calls.
39
+ - **Tier 2 — per-resource map (no request):** `authorizeResource(gate, instance, abilities)`
40
+ returns a `{ update, delete }` map a controller attaches to a serialized resource.
41
+ - **Tier 3 — fallback endpoint (last resort):** consumes core's opt-in `POST /authz/can`.
42
+ - **Framework-neutral client** (`@dudousxd/nestjs-authz-inertia/client`): an `AbilityStore`,
43
+ `hydrateFromInertiaProps` / `hydrateResource`, and a `createCan` resolver that reads hydrated
44
+ decisions synchronously (no fetch on a cache hit) and only falls back (fetch the endpoint, or
45
+ deny) when the ability/resource is unknown.
46
+ - **Denial filter (`AuthzDenialFilter`):** `AuthzInertiaModule.forRoot` also registers a global
47
+ `APP_FILTER` that converts `@Roles`/`@Can` denials (`ForbiddenException`) into a friendly 303
48
+ redirect on Inertia requests (`X-Inertia` header) — `/login` when unauthenticated, `/403` when
49
+ authenticated-but-forbidden (auth-state read from the optional `CONTEXT_ACCESSOR`). Non-Inertia
50
+ (REST) requests are rethrown and keep the normal 403 JSON. Configurable via
51
+ `forRoot({ denial: { loginUrl, forbiddenUrl, enabled, handler } })` (`enabled: false` opts out;
52
+ `handler(exception, host)` is a full escape hatch). Targets pass the same open-redirect guard
53
+ nestjs-inertia uses, falling back to a safe default if unsafe.
54
+
55
+ `@dudousxd/nestjs-authz` (patch): add an opt-in `POST /authz/can` fallback controller behind
56
+ `AuthzModule.forRoot({ canEndpoint: true })` (default off; accepts a custom path string). It runs
57
+ `gate.allows(ability, resource?)` for the current context user and returns `{ allowed }`, failing
58
+ closed for unresolved abilities. Also exposes `Gate.gateNames()` and `PolicyRegistry.classAbilities()`
59
+ so integrations can enumerate a user's class-level abilities.
60
+
61
+ - [#2](https://github.com/DavideCarvalho/nestjs-authz/pull/2) [`2ecb0f4`](https://github.com/DavideCarvalho/nestjs-authz/commit/2ecb0f46342fa4527fc01f1097720f2e7bcf9aa7) Thanks [@DavideCarvalho](https://github.com/DavideCarvalho)! - Add `@dudousxd/nestjs-authz-telescope`: a `@dudousxd/nestjs-telescope` extension that records every
62
+ authorization decision the `Gate` reaches (ability, allow/deny, the reason it was decided, the user
63
+ and the resource) as an `authorization` Telescope entry plus an "Authorization" dashboard page (a
64
+ top-N of denied abilities and a table of recent decisions) — so a 403 is debuggable. The extension's
65
+ `AuthorizationWatcher` subscribes to the new `nestjs-authz:decision` diagnostics channel; nothing is
66
+ emitted (and nothing recorded) when no observer is listening.
67
+
68
+ The core `Gate` now publishes each decision on a dependency-free `node:diagnostics_channel`
69
+ (`nestjs-authz:decision`, exported as `AUTHZ_DECISION_CHANNEL`) after a verdict is reached. The
70
+ emission is gated on `channel.hasSubscribers` and fully guarded, so it is zero-overhead with no
71
+ subscriber and can never affect a check. No existing behavior changes.
72
+
3
73
  ## 0.2.0
4
74
 
5
75
  ### Minor Changes
@@ -0,0 +1,35 @@
1
+ import { type Type } from '@nestjs/common';
2
+ /**
3
+ * Request body for the fallback `can` endpoint. Mirrors the payload emitted by
4
+ * the codegen `can()` helper: `{ ability, resource?: { type, id } | null }`.
5
+ */
6
+ export interface CanRequestBody {
7
+ ability: string;
8
+ resource?: {
9
+ type: string;
10
+ id?: string | number;
11
+ } | null;
12
+ }
13
+ /** Response shape: a single boolean verdict. */
14
+ export interface CanResponseBody {
15
+ allowed: boolean;
16
+ }
17
+ /**
18
+ * Build the opt-in fallback controller class, mounted at `path`. Kept as a
19
+ * factory so the route path is configurable via `AuthzModule.forRoot({ canEndpoint })`.
20
+ *
21
+ * The endpoint runs `gate.allows(ability, resource?)` for the CURRENT (context)
22
+ * user.
23
+ *
24
+ * LIMITATION — read this before relying on the fallback for resource decisions:
25
+ * the `{ type, id }` resource shim is forwarded as-is and NEVER matches a
26
+ * registered `@Policy` by constructor, so this endpoint can ONLY resolve
27
+ * class-level abilities and ad-hoc gates. Per-instance decisions MUST be
28
+ * hydrated via shared props / `authorizeResource` (tiers 1-2); a resource-bound
29
+ * ability that misses the client cache and falls through to `POST /authz/can`
30
+ * will DENY. An unresolved ability is likewise treated as a deny (never a 500).
31
+ */
32
+ export declare function createCanController(path: string): Type<unknown>;
33
+ /** Default mount path for the fallback endpoint (matches codegen's `/authz/can`). */
34
+ export declare const DEFAULT_CAN_ENDPOINT_PATH = "authz/can";
35
+ //# sourceMappingURL=can-endpoint.controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"can-endpoint.controller.d.ts","sourceRoot":"","sources":["../src/can-endpoint.controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,KAAK,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAI7D;;;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;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,CA0B/D;AAED,qFAAqF;AACrF,eAAO,MAAM,yBAAyB,cAAc,CAAC"}
@@ -0,0 +1,68 @@
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 { Body, Controller } from '@nestjs/common';
14
+ import { Post as HttpPost } from '@nestjs/common';
15
+ import { Gate } from './gate.js';
16
+ /**
17
+ * Build the opt-in fallback controller class, mounted at `path`. Kept as a
18
+ * factory so the route path is configurable via `AuthzModule.forRoot({ canEndpoint })`.
19
+ *
20
+ * The endpoint runs `gate.allows(ability, resource?)` for the CURRENT (context)
21
+ * user.
22
+ *
23
+ * LIMITATION — read this before relying on the fallback for resource decisions:
24
+ * the `{ type, id }` resource shim is forwarded as-is and NEVER matches a
25
+ * registered `@Policy` by constructor, so this endpoint can ONLY resolve
26
+ * class-level abilities and ad-hoc gates. Per-instance decisions MUST be
27
+ * hydrated via shared props / `authorizeResource` (tiers 1-2); a resource-bound
28
+ * ability that misses the client cache and falls through to `POST /authz/can`
29
+ * will DENY. An unresolved ability is likewise treated as a deny (never a 500).
30
+ */
31
+ export function createCanController(path) {
32
+ let AuthzCanController = class AuthzCanController {
33
+ gate;
34
+ constructor(gate) {
35
+ this.gate = gate;
36
+ }
37
+ async can(body) {
38
+ const ability = body?.ability;
39
+ if (typeof ability !== 'string' || ability.length === 0) {
40
+ return { allowed: false };
41
+ }
42
+ const resource = body?.resource ?? undefined;
43
+ try {
44
+ const allowed = await this.gate.allows(ability, resource == null ? undefined : resource);
45
+ return { allowed };
46
+ }
47
+ catch {
48
+ // Unresolved/ambiguous ability — fail closed.
49
+ return { allowed: false };
50
+ }
51
+ }
52
+ };
53
+ __decorate([
54
+ HttpPost(),
55
+ __param(0, Body()),
56
+ __metadata("design:type", Function),
57
+ __metadata("design:paramtypes", [Object]),
58
+ __metadata("design:returntype", Promise)
59
+ ], AuthzCanController.prototype, "can", null);
60
+ AuthzCanController = __decorate([
61
+ Controller(path),
62
+ __metadata("design:paramtypes", [Gate])
63
+ ], AuthzCanController);
64
+ return AuthzCanController;
65
+ }
66
+ /** Default mount path for the fallback endpoint (matches codegen's `/authz/can`). */
67
+ export const DEFAULT_CAN_ENDPOINT_PATH = 'authz/can';
68
+ //# sourceMappingURL=can-endpoint.controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"can-endpoint.controller.js","sourceRoot":"","sources":["../src/can-endpoint.controller.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,OAAO,EAAE,IAAI,EAAE,UAAU,EAAa,MAAM,gBAAgB,CAAC;AAC7D,OAAO,EAAE,IAAI,IAAI,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAgBjC;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAY;IAC9C,IACM,kBAAkB,GADxB,MACM,kBAAkB;QACO;QAA7B,YAA6B,IAAU;YAAV,SAAI,GAAJ,IAAI,CAAM;QAAG,CAAC;QAGrC,AAAN,KAAK,CAAC,GAAG,CAAS,IAAoB;YACpC,MAAM,OAAO,GAAG,IAAI,EAAE,OAAO,CAAC;YAC9B,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACxD,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;YAC5B,CAAC;YACD,MAAM,QAAQ,GAAG,IAAI,EAAE,QAAQ,IAAI,SAAS,CAAC;YAC7C,IAAI,CAAC;gBACH,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,CACpC,OAAO,EACP,QAAQ,IAAI,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAE,QAAmB,CACpD,CAAC;gBACF,OAAO,EAAE,OAAO,EAAE,CAAC;YACrB,CAAC;YAAC,MAAM,CAAC;gBACP,8CAA8C;gBAC9C,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;YAC5B,CAAC;QACH,CAAC;KACF,CAAA;IAjBO;QADL,QAAQ,EAAE;QACA,WAAA,IAAI,EAAE,CAAA;;;;iDAgBhB;IApBG,kBAAkB;QADvB,UAAU,CAAC,IAAI,CAAC;yCAEoB,IAAI;OADnC,kBAAkB,CAqBvB;IAED,OAAO,kBAAkB,CAAC;AAC5B,CAAC;AAED,qFAAqF;AACrF,MAAM,CAAC,MAAM,yBAAyB,GAAG,WAAW,CAAC"}
@@ -0,0 +1,15 @@
1
+ import 'reflect-metadata';
2
+ /**
3
+ * Guard a route by COARSE role membership: the request is allowed when the current
4
+ * user holds ANY of the listed roles. This is the broad "is teacher?" check, as
5
+ * opposed to the granular ability check of `@Can`.
6
+ *
7
+ * Roles come from the user object (`user.roles` / `user.role`) by default, plus the
8
+ * optional `ROLE_PROVIDER` seam (a persisted RBAC store) when registered — see
9
+ * {@link RolesGuard}.
10
+ *
11
+ * @example
12
+ * `@Roles('admin', 'teacher')` // allow if the user is an admin OR a teacher
13
+ */
14
+ export declare function Roles(...roles: string[]): MethodDecorator & ClassDecorator;
15
+ //# sourceMappingURL=roles.decorator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roles.decorator.d.ts","sourceRoot":"","sources":["../../src/decorator/roles.decorator.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAI1B;;;;;;;;;;;GAWG;AACH,wBAAgB,KAAK,CAAC,GAAG,KAAK,EAAE,MAAM,EAAE,GAAG,eAAe,GAAG,cAAc,CAE1E"}
@@ -0,0 +1,19 @@
1
+ import 'reflect-metadata';
2
+ import { SetMetadata } from '@nestjs/common';
3
+ import { ROLES_METADATA } from '../tokens.js';
4
+ /**
5
+ * Guard a route by COARSE role membership: the request is allowed when the current
6
+ * user holds ANY of the listed roles. This is the broad "is teacher?" check, as
7
+ * opposed to the granular ability check of `@Can`.
8
+ *
9
+ * Roles come from the user object (`user.roles` / `user.role`) by default, plus the
10
+ * optional `ROLE_PROVIDER` seam (a persisted RBAC store) when registered — see
11
+ * {@link RolesGuard}.
12
+ *
13
+ * @example
14
+ * `@Roles('admin', 'teacher')` // allow if the user is an admin OR a teacher
15
+ */
16
+ export function Roles(...roles) {
17
+ return SetMetadata(ROLES_METADATA, roles);
18
+ }
19
+ //# sourceMappingURL=roles.decorator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"roles.decorator.js","sourceRoot":"","sources":["../../src/decorator/roles.decorator.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAC1B,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,KAAK,CAAC,GAAG,KAAe;IACtC,OAAO,WAAW,CAAC,cAAc,EAAE,KAAK,CAAC,CAAC;AAC5C,CAAC"}
@@ -0,0 +1,42 @@
1
+ import diagnostics_channel from 'node:diagnostics_channel';
2
+ import type { Resource, User } from './types.js';
3
+ /**
4
+ * Channel name — the cross-repo wire contract with `@dudousxd/nestjs-authz-telescope`'s
5
+ * authorization watcher (and any other observer). Versioned by the payload's `v` field,
6
+ * not by name. Keep this string byte-identical on both sides.
7
+ */
8
+ export declare const AUTHZ_DECISION_CHANNEL = "nestjs-authz:decision";
9
+ /**
10
+ * Memoized by Node: the same channel object is returned for the same name. Read
11
+ * `.hasSubscribers` to gate any work before publishing — when nothing subscribes
12
+ * (the common case) emitting a decision is effectively free.
13
+ */
14
+ export declare const authzDecisionChannel: diagnostics_channel.Channel<unknown, unknown>;
15
+ /**
16
+ * Every authorization decision the {@link Gate} reaches, published once per
17
+ * `allows`/`authorize`/`denies` call. `reason` names which resolution path
18
+ * produced the verdict so a 403 is debuggable.
19
+ */
20
+ export interface AuthzDecisionDiagnostic {
21
+ v: 1;
22
+ /** The ability checked, e.g. `'update'` or `'access-admin'`. */
23
+ ability: string;
24
+ /** The verdict — `true` allow, `false` deny. */
25
+ allowed: boolean;
26
+ /** Which resolution path decided it (see {@link AuthzDecisionReason}). */
27
+ reason: AuthzDecisionReason;
28
+ /** A label for the resolved user (`<type>#<id>` / constructor name), or `null` when anonymous. */
29
+ userRef: string | null;
30
+ /** The resource class name the ability targeted, or `null` for a model-less ability. */
31
+ resourceType: string | null;
32
+ /** The resource's `id` when discoverable on the instance, else `null`. */
33
+ resourceId: string | number | null;
34
+ }
35
+ /** The resolution path that produced a decision — the "why" behind a 403 (or a grant). */
36
+ export type AuthzDecisionReason = 'super-admin' | 'permission-provider' | 'policy-before' | 'policy' | 'gate' | 'anonymous';
37
+ /**
38
+ * Publish a decision to the channel, but only when something is listening. Never
39
+ * throws — building the payload or notifying a subscriber must not break a check.
40
+ */
41
+ export declare function publishAuthzDecision(ability: string, allowed: boolean, reason: AuthzDecisionReason, user: User, resource: Resource | undefined): void;
42
+ //# sourceMappingURL=diagnostics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagnostics.d.ts","sourceRoot":"","sources":["../src/diagnostics.ts"],"names":[],"mappings":"AAAA,OAAO,mBAAmB,MAAM,0BAA0B,CAAC;AAC3D,OAAO,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAEjD;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,0BAA0B,CAAC;AAE9D;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,+CAAsD,CAAC;AAExF;;;;GAIG;AACH,MAAM,WAAW,uBAAuB;IACtC,CAAC,EAAE,CAAC,CAAC;IACL,gEAAgE;IAChE,OAAO,EAAE,MAAM,CAAC;IAChB,gDAAgD;IAChD,OAAO,EAAE,OAAO,CAAC;IACjB,0EAA0E;IAC1E,MAAM,EAAE,mBAAmB,CAAC;IAC5B,kGAAkG;IAClG,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,wFAAwF;IACxF,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,0EAA0E;IAC1E,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;CACpC;AAED,0FAA0F;AAC1F,MAAM,MAAM,mBAAmB,GAC3B,aAAa,GACb,qBAAqB,GACrB,eAAe,GACf,QAAQ,GACR,MAAM,GACN,WAAW,CAAC;AAEhB;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,OAAO,EAChB,MAAM,EAAE,mBAAmB,EAC3B,IAAI,EAAE,IAAI,EACV,QAAQ,EAAE,QAAQ,GAAG,SAAS,GAC7B,IAAI,CAgBN"}
@@ -0,0 +1,68 @@
1
+ import diagnostics_channel from 'node:diagnostics_channel';
2
+ /**
3
+ * Channel name — the cross-repo wire contract with `@dudousxd/nestjs-authz-telescope`'s
4
+ * authorization watcher (and any other observer). Versioned by the payload's `v` field,
5
+ * not by name. Keep this string byte-identical on both sides.
6
+ */
7
+ export const AUTHZ_DECISION_CHANNEL = 'nestjs-authz:decision';
8
+ /**
9
+ * Memoized by Node: the same channel object is returned for the same name. Read
10
+ * `.hasSubscribers` to gate any work before publishing — when nothing subscribes
11
+ * (the common case) emitting a decision is effectively free.
12
+ */
13
+ export const authzDecisionChannel = diagnostics_channel.channel(AUTHZ_DECISION_CHANNEL);
14
+ /**
15
+ * Publish a decision to the channel, but only when something is listening. Never
16
+ * throws — building the payload or notifying a subscriber must not break a check.
17
+ */
18
+ export function publishAuthzDecision(ability, allowed, reason, user, resource) {
19
+ if (!authzDecisionChannel.hasSubscribers)
20
+ return;
21
+ try {
22
+ const payload = {
23
+ v: 1,
24
+ ability,
25
+ allowed,
26
+ reason,
27
+ userRef: labelUser(user),
28
+ resourceType: resourceType(resource),
29
+ resourceId: resourceId(resource),
30
+ };
31
+ authzDecisionChannel.publish(payload);
32
+ }
33
+ catch {
34
+ // Observability must never break authorization.
35
+ }
36
+ }
37
+ /** A short, JSON-safe label for the user — `<type>#<id>`, a constructor name, or `null`. */
38
+ function labelUser(user) {
39
+ if (user == null)
40
+ return null;
41
+ if (typeof user === 'object') {
42
+ const obj = user;
43
+ const type = (obj.type ?? obj.constructor?.name);
44
+ const id = obj.id;
45
+ if (type && (typeof id === 'string' || typeof id === 'number'))
46
+ return `${type}#${id}`;
47
+ if (typeof obj.id === 'string' || typeof obj.id === 'number')
48
+ return String(obj.id);
49
+ return obj.constructor?.name ?? null;
50
+ }
51
+ return String(user);
52
+ }
53
+ /** The resource's class name (`Post`), or `null` when model-less. A class itself maps to its name. */
54
+ function resourceType(resource) {
55
+ if (resource == null)
56
+ return null;
57
+ if (typeof resource === 'function')
58
+ return resource.name ?? null;
59
+ return resource.constructor?.name ?? null;
60
+ }
61
+ /** The resource instance's `id`, when present and primitive; otherwise `null`. */
62
+ function resourceId(resource) {
63
+ if (resource == null || typeof resource === 'function')
64
+ return null;
65
+ const id = resource.id;
66
+ return typeof id === 'string' || typeof id === 'number' ? id : null;
67
+ }
68
+ //# sourceMappingURL=diagnostics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diagnostics.js","sourceRoot":"","sources":["../src/diagnostics.ts"],"names":[],"mappings":"AAAA,OAAO,mBAAmB,MAAM,0BAA0B,CAAC;AAG3D;;;;GAIG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,uBAAuB,CAAC;AAE9D;;;;GAIG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,mBAAmB,CAAC,OAAO,CAAC,sBAAsB,CAAC,CAAC;AAgCxF;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAClC,OAAe,EACf,OAAgB,EAChB,MAA2B,EAC3B,IAAU,EACV,QAA8B;IAE9B,IAAI,CAAC,oBAAoB,CAAC,cAAc;QAAE,OAAO;IACjD,IAAI,CAAC;QACH,MAAM,OAAO,GAA4B;YACvC,CAAC,EAAE,CAAC;YACJ,OAAO;YACP,OAAO;YACP,MAAM;YACN,OAAO,EAAE,SAAS,CAAC,IAAI,CAAC;YACxB,YAAY,EAAE,YAAY,CAAC,QAAQ,CAAC;YACpC,UAAU,EAAE,UAAU,CAAC,QAAQ,CAAC;SACjC,CAAC;QACF,oBAAoB,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IACxC,CAAC;IAAC,MAAM,CAAC;QACP,gDAAgD;IAClD,CAAC;AACH,CAAC;AAED,4FAA4F;AAC5F,SAAS,SAAS,CAAC,IAAU;IAC3B,IAAI,IAAI,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC;IAC9B,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC7B,MAAM,GAAG,GAAG,IAA+B,CAAC;QAC5C,MAAM,IAAI,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,GAAG,CAAC,WAAW,EAAE,IAAI,CAAuB,CAAC;QACvE,MAAM,EAAE,GAAG,GAAG,CAAC,EAAE,CAAC;QAClB,IAAI,IAAI,IAAI,CAAC,OAAO,EAAE,KAAK,QAAQ,IAAI,OAAO,EAAE,KAAK,QAAQ,CAAC;YAAE,OAAO,GAAG,IAAI,IAAI,EAAE,EAAE,CAAC;QACvF,IAAI,OAAO,GAAG,CAAC,EAAE,KAAK,QAAQ,IAAI,OAAO,GAAG,CAAC,EAAE,KAAK,QAAQ;YAAE,OAAO,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACpF,OAAO,GAAG,CAAC,WAAW,EAAE,IAAI,IAAI,IAAI,CAAC;IACvC,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC;AACtB,CAAC;AAED,sGAAsG;AACtG,SAAS,YAAY,CAAC,QAA8B;IAClD,IAAI,QAAQ,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC;IAClC,IAAI,OAAO,QAAQ,KAAK,UAAU;QAAE,OAAQ,QAA8B,CAAC,IAAI,IAAI,IAAI,CAAC;IACxF,OAAQ,QAAgD,CAAC,WAAW,EAAE,IAAI,IAAI,IAAI,CAAC;AACrF,CAAC;AAED,kFAAkF;AAClF,SAAS,UAAU,CAAC,QAA8B;IAChD,IAAI,QAAQ,IAAI,IAAI,IAAI,OAAO,QAAQ,KAAK,UAAU;QAAE,OAAO,IAAI,CAAC;IACpE,MAAM,EAAE,GAAI,QAAoC,CAAC,EAAE,CAAC;IACpD,OAAO,OAAO,EAAE,KAAK,QAAQ,IAAI,OAAO,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AACtE,CAAC"}
package/dist/gate.d.ts CHANGED
@@ -2,6 +2,7 @@ import { ModuleRef } from '@nestjs/core';
2
2
  import type { ContextAccessor } from './context-accessor.js';
3
3
  import type { PermissionProvider } from './permission-provider.js';
4
4
  import { PolicyRegistry } from './policy-registry.js';
5
+ import { type RoleProvider } from './role-provider.js';
5
6
  import type { AuthzModuleOptions, GateFn, Resource, User } from './types.js';
6
7
  declare const NO_USER: unique symbol;
7
8
  type MaybeUser = User | typeof NO_USER;
@@ -21,10 +22,12 @@ export declare class Gate {
21
22
  private readonly context?;
22
23
  private readonly moduleRef?;
23
24
  private readonly permissionProvider?;
25
+ private readonly roleProvider?;
24
26
  private readonly gates;
25
27
  private readonly superAdmin;
26
28
  private readonly resolveUser;
27
- constructor(policies: PolicyRegistry, options: AuthzModuleOptions | undefined, context?: ContextAccessor | undefined, moduleRef?: ModuleRef | undefined, permissionProvider?: PermissionProvider | undefined);
29
+ private readonly roleResolver;
30
+ constructor(policies: PolicyRegistry, options: AuthzModuleOptions | undefined, context?: ContextAccessor | undefined, moduleRef?: ModuleRef | undefined, permissionProvider?: PermissionProvider | undefined, roleProvider?: RoleProvider | undefined);
28
31
  /**
29
32
  * Locate the context accessor. Prefers the value injected into this module;
30
33
  * falls back to a non-strict {@link ModuleRef} lookup so an accessor provided
@@ -37,10 +40,22 @@ export declare class Gate {
37
40
  * provider registered by ANY module (e.g. the RBAC adapter's global module) is found.
38
41
  */
39
42
  private resolvePermissionProvider;
43
+ /**
44
+ * Locate the optional {@link RoleProvider} (the coarse role seam). Prefers the value
45
+ * injected into this module; falls back to a non-strict {@link ModuleRef} lookup so a
46
+ * provider registered by ANY module (e.g. the RBAC adapter's global module) is found.
47
+ */
48
+ private resolveRoleProvider;
40
49
  /** Register an ad-hoc, model-less gate resolved by `ability` name. */
41
50
  define(ability: string, fn: GateFn): this;
42
51
  /** True when an ad-hoc gate is registered for `ability`. */
43
52
  hasGate(ability: string): boolean;
53
+ /**
54
+ * Names of every ad-hoc gate registered via {@link define}. Used by integrations
55
+ * (e.g. `@dudousxd/nestjs-authz-inertia`) that enumerate the user's class-level
56
+ * abilities to share them as Inertia props — no network round-trip needed.
57
+ */
58
+ gateNames(): string[];
44
59
  /**
45
60
  * Bind an explicit user, bypassing the context accessor. Use when no
46
61
  * nestjs-context is wired, or to check a user other than the current one.
@@ -61,9 +76,31 @@ export declare class Gate {
61
76
  allows(ability: string, resource?: Resource): Promise<boolean>;
62
77
  denies(ability: string, resource?: Resource): Promise<boolean>;
63
78
  authorize(ability: string, resource?: Resource): Promise<void>;
79
+ /** True when the current user holds `role`. */
80
+ hasRole(role: string): Promise<boolean>;
81
+ /** True when the current user holds ANY of `roles`. */
82
+ hasAnyRole(roles: string[]): Promise<boolean>;
64
83
  /** @internal */
65
84
  allowsForUser(user: MaybeUser, ability: string, resource?: Resource): Promise<boolean>;
85
+ /** @internal */
86
+ hasAnyRoleForUser(user: MaybeUser, roles: string[]): Promise<boolean>;
87
+ /**
88
+ * Resolve the user's effective roles and test membership against `roles`. Returns
89
+ * `false` for an anonymous (NO_USER) caller and whenever no source yields a role
90
+ * (deny-by-default). Roles come from the UNION of the default/overridden
91
+ * {@link RoleResolver} (reads the user object) and the optional {@link RoleProvider}
92
+ * seam (a persisted store) — so an app needs neither to opt in.
93
+ */
94
+ private checkRoles;
95
+ /** The current user's effective role names (resolver ∪ provider). */
96
+ private rolesOf;
66
97
  private check;
98
+ /**
99
+ * Resolve an ability to a verdict plus the path that decided it. Throws
100
+ * {@link AbilityNotResolvedException}/{@link AmbiguousAbilityException} when no
101
+ * decision can be reached (those paths emit no decision).
102
+ */
103
+ private resolve;
67
104
  private resolvePolicy;
68
105
  }
69
106
  /**
@@ -76,6 +113,10 @@ export declare class BoundGate {
76
113
  allows(ability: string, resource?: Resource): Promise<boolean>;
77
114
  denies(ability: string, resource?: Resource): Promise<boolean>;
78
115
  authorize(ability: string, resource?: Resource): Promise<void>;
116
+ /** True when the bound user holds `role`. */
117
+ hasRole(role: string): Promise<boolean>;
118
+ /** True when the bound user holds ANY of `roles`. */
119
+ hasAnyRole(roles: string[]): Promise<boolean>;
79
120
  }
80
121
  export {};
81
122
  //# sourceMappingURL=gate.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../src/gate.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AACzC,OAAO,KAAK,EAAE,eAAe,EAAW,MAAM,uBAAuB,CAAC;AAEtE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AACnE,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,OAAO,KAAK,EACV,kBAAkB,EAClB,MAAM,EAGN,QAAQ,EAER,IAAI,EACL,MAAM,YAAY,CAAC;AAIpB,QAAA,MAAM,OAAO,eAA0B,CAAC;AACxC,KAAK,SAAS,GAAG,IAAI,GAAG,OAAO,OAAO,CAAC;AAEvC;;;;;;;;;;GAUG;AACH,qBACa,IAAI;IAMb,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAMzB,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAC;IAEzB,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC;IAG3B,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAC;IAhBtC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA6B;IACnD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6B;IACxD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAoC;gBAG7C,QAAQ,EAAE,cAAc,EAGzC,OAAO,EAAE,kBAAkB,GAAG,SAAS,EAGtB,OAAO,CAAC,EAAE,eAAe,YAAA,EAEzB,SAAS,CAAC,EAAE,SAAS,YAAA,EAGrB,kBAAkB,CAAC,EAAE,kBAAkB,YAAA;IAM1D;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAUtB;;;;OAIG;IACH,OAAO,CAAC,yBAAyB;IAUjC,sEAAsE;IACtE,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI;IAKzC,4DAA4D;IAC5D,OAAO,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;IAIjC;;;;;;;;;OASG;IACH,OAAO,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS;IAI9B;;;;OAIG;YACW,WAAW;IAcnB,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;IAI9D,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;IAI9D,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;IAQpE,gBAAgB;IAChB,aAAa,CAAC,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;YAIxE,KAAK;IAsDnB,OAAO,CAAC,aAAa;CAwBtB;AAED;;GAEG;AACH,qBAAa,SAAS;IAElB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,IAAI;gBADJ,IAAI,EAAE,IAAI,EACV,IAAI,EAAE,SAAS;IAGlC,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;IAIxD,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;IAI9D,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC;CAKrE"}
1
+ {"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../src/gate.ts"],"names":[],"mappings":"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"}