@dudousxd/nestjs-authz 0.2.0 → 0.4.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 +84 -0
  2. package/dist/can-endpoint.controller.d.ts +39 -0
  3. package/dist/can-endpoint.controller.d.ts.map +1 -0
  4. package/dist/can-endpoint.controller.js +97 -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 +10 -2
  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 +13 -0
  27. package/dist/module.d.ts.map +1 -1
  28. package/dist/module.js +48 -2
  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 +29 -0
  41. package/dist/tokens.d.ts.map +1 -1
  42. package/dist/tokens.js +29 -0
  43. package/dist/tokens.js.map +1 -1
  44. package/dist/types.d.ts +67 -0
  45. package/dist/types.d.ts.map +1 -1
  46. package/package.json +1 -1
@@ -41,6 +41,48 @@ let PolicyRegistry = class PolicyRegistry {
41
41
  all() {
42
42
  return [...this.byResource.values()];
43
43
  }
44
+ /** All registered resource classes (insertion order). */
45
+ resources() {
46
+ return [...this.byResource.keys()];
47
+ }
48
+ /**
49
+ * Enumerate the CLASS-LEVEL ability method names declared on each registered
50
+ * policy, keyed by resource class. Used by integrations that pre-resolve a
51
+ * user's class-level abilities (e.g. to share them as Inertia props).
52
+ *
53
+ * Walks the policy prototype chain and collects own function-valued members,
54
+ * excluding `constructor` and the reserved `before` hook. Inherited Object
55
+ * members are skipped.
56
+ *
57
+ * Only methods that take NO resource instance are included — heuristically,
58
+ * arity `<= 1` (just `user`, e.g. `create(user)` / `viewAny(user)`). An
59
+ * instance method like `update(user, post)` is excluded: dispatching it
60
+ * against the resource CLASS would call it with the class constructor as
61
+ * `post` and write a bogus class-level verdict.
62
+ */
63
+ classAbilities() {
64
+ const out = [];
65
+ for (const [resource, policy] of this.byResource) {
66
+ const abilities = new Set();
67
+ let proto = Object.getPrototypeOf(policy);
68
+ while (proto && proto !== Object.prototype) {
69
+ for (const name of Object.getOwnPropertyNames(proto)) {
70
+ if (name === 'constructor' || name === 'before')
71
+ continue;
72
+ const member = policy[name];
73
+ // Class-level abilities take only `user` (arity <= 1). A method that
74
+ // also declares a resource param (arity >= 2) is instance-scoped and
75
+ // must not be dispatched against the class.
76
+ if (typeof member === 'function' && member.length <= 1) {
77
+ abilities.add(name);
78
+ }
79
+ }
80
+ proto = Object.getPrototypeOf(proto);
81
+ }
82
+ out.push({ resource, abilities: [...abilities] });
83
+ }
84
+ return out;
85
+ }
44
86
  };
45
87
  PolicyRegistry = __decorate([
46
88
  Injectable()
@@ -1 +1 @@
1
- {"version":3,"file":"policy-registry.js","sourceRoot":"","sources":["../src/policy-registry.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,UAAU,EAAa,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACpE,OAAO,EAAE,2BAA2B,EAAE,MAAM,wBAAwB,CAAC;AAGrE;;;GAGG;AAEI,IAAM,cAAc,GAApB,MAAM,cAAc;IACR,UAAU,GAAG,IAAI,GAAG,EAAiC,CAAC;IAEvE;;;OAGG;IACH,QAAQ,CAAC,MAAsB;QAC7B,MAAM,QAAQ,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC;QAC3C,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,EAAE,IAAI,IAAI,QAAQ,CAAC;YAClD,MAAM,IAAI,2BAA2B,CAAC,IAAI,CAAC,CAAC;QAC9C,CAAC;QACD,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACxC,CAAC;IAED,wEAAwE;IACxE,WAAW,CAAC,QAAuB;QACjC,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACvC,CAAC;IAED,+EAA+E;IAC/E,WAAW,CAAC,QAAgB;QAC1B,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,WAA4B,CAAC,CAAC;IACpE,CAAC;IAED,+DAA+D;IAC/D,GAAG,CAAC,QAAuB;QACzB,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACvC,CAAC;IAED,2DAA2D;IAC3D,GAAG;QACD,OAAO,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;IACvC,CAAC;CACF,CAAA;AAnCY,cAAc;IAD1B,UAAU,EAAE;GACA,cAAc,CAmC1B"}
1
+ {"version":3,"file":"policy-registry.js","sourceRoot":"","sources":["../src/policy-registry.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,UAAU,EAAa,MAAM,gBAAgB,CAAC;AACvD,OAAO,EAAE,iBAAiB,EAAE,MAAM,iCAAiC,CAAC;AACpE,OAAO,EAAE,2BAA2B,EAAE,MAAM,wBAAwB,CAAC;AAGrE;;;GAGG;AAEI,IAAM,cAAc,GAApB,MAAM,cAAc;IACR,UAAU,GAAG,IAAI,GAAG,EAAiC,CAAC;IAEvE;;;OAGG;IACH,QAAQ,CAAC,MAAsB;QAC7B,MAAM,QAAQ,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC;QAC3C,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,MAAM,IAAI,GAAG,MAAM,CAAC,WAAW,EAAE,IAAI,IAAI,QAAQ,CAAC;YAClD,MAAM,IAAI,2BAA2B,CAAC,IAAI,CAAC,CAAC;QAC9C,CAAC;QACD,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;IACxC,CAAC;IAED,wEAAwE;IACxE,WAAW,CAAC,QAAuB;QACjC,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACvC,CAAC;IAED,+EAA+E;IAC/E,WAAW,CAAC,QAAgB;QAC1B,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,WAA4B,CAAC,CAAC;IACpE,CAAC;IAED,+DAA+D;IAC/D,GAAG,CAAC,QAAuB;QACzB,OAAO,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACvC,CAAC;IAED,2DAA2D;IAC3D,GAAG;QACD,OAAO,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC,CAAC;IACvC,CAAC;IAED,yDAAyD;IACzD,SAAS;QACP,OAAO,CAAC,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,CAAC;IACrC,CAAC;IAED;;;;;;;;;;;;;;OAcG;IACH,cAAc;QACZ,MAAM,GAAG,GAA4D,EAAE,CAAC;QACxE,KAAK,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,IAAI,IAAI,CAAC,UAAU,EAAE,CAAC;YACjD,MAAM,SAAS,GAAG,IAAI,GAAG,EAAU,CAAC;YACpC,IAAI,KAAK,GAAkB,MAAM,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;YACzD,OAAO,KAAK,IAAI,KAAK,KAAK,MAAM,CAAC,SAAS,EAAE,CAAC;gBAC3C,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,mBAAmB,CAAC,KAAK,CAAC,EAAE,CAAC;oBACrD,IAAI,IAAI,KAAK,aAAa,IAAI,IAAI,KAAK,QAAQ;wBAAE,SAAS;oBAC1D,MAAM,MAAM,GAAI,MAAkC,CAAC,IAAI,CAAC,CAAC;oBACzD,qEAAqE;oBACrE,qEAAqE;oBACrE,4CAA4C;oBAC5C,IAAI,OAAO,MAAM,KAAK,UAAU,IAAI,MAAM,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;wBACvD,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;oBACtB,CAAC;gBACH,CAAC;gBACD,KAAK,GAAG,MAAM,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;YACvC,CAAC;YACD,GAAG,CAAC,IAAI,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,GAAG,SAAS,CAAC,EAAE,CAAC,CAAC;QACpD,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;CACF,CAAA;AA9EY,cAAc;IAD1B,UAAU,EAAE;GACA,cAAc,CA8E1B"}
@@ -0,0 +1,40 @@
1
+ import type { User } from './types.js';
2
+ /**
3
+ * Optional seam that lets a persisted RBAC layer supply a user's roles.
4
+ *
5
+ * This mirrors {@link PermissionProvider} but for COARSE, role-based checks
6
+ * (`gate.hasRole('teacher')`, `@Roles('admin')`). The core does NOT implement
7
+ * this — `@dudousxd/nestjs-authz-typeorm` (and the other RBAC adapters) provide an
8
+ * implementation and register it under the shared {@link ROLE_PROVIDER} token.
9
+ * When no provider is registered the default {@link RoleResolver} (reading roles
10
+ * off the user object) is the only source.
11
+ *
12
+ * `user` is the current user (whatever the app's auth layer produced; `undefined`
13
+ * when anonymous). Return the role names the user holds — an empty array (or
14
+ * nullish) contributes no roles. When both this provider and the default resolver
15
+ * yield roles, the Gate takes their UNION.
16
+ */
17
+ export interface RoleProvider {
18
+ getRoles(user: User): string[] | undefined | Promise<string[] | undefined>;
19
+ }
20
+ /**
21
+ * Strategy that derives a user's roles from the user object itself, with ZERO RBAC
22
+ * tables. The {@link defaultRoleResolver} reads `user.roles` (`string[]`) OR
23
+ * `user.role` (`string | string[]`) and normalizes both to a `string[]`. Apps that
24
+ * already carry a role on the user thus get role-checks for free; an app that needs
25
+ * a different shape overrides it via `AuthzModule.forRoot({ resolveRoles })`.
26
+ */
27
+ export type RoleResolver = (user: User) => string[] | undefined | Promise<string[] | undefined>;
28
+ /**
29
+ * Default {@link RoleResolver}: read roles directly off the user object.
30
+ *
31
+ * - `user.roles` is a `string[]` → used as-is.
32
+ * - `user.role` is a `string` → wrapped to `[role]`.
33
+ * - `user.role` is a `string[]` → used as-is.
34
+ * - neither present (or anonymous) → `[]` (no roles).
35
+ *
36
+ * Non-string entries are filtered out so a malformed field can never leak a
37
+ * truthy match.
38
+ */
39
+ export declare function defaultRoleResolver(user: User): string[];
40
+ //# sourceMappingURL=role-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"role-provider.d.ts","sourceRoot":"","sources":["../src/role-provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAEvC;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,EAAE,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;CAC5E;AAED;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,IAAI,EAAE,IAAI,KAAK,MAAM,EAAE,GAAG,SAAS,GAAG,OAAO,CAAC,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;AAEhG;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,IAAI,GAAG,MAAM,EAAE,CAaxD"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Default {@link RoleResolver}: read roles directly off the user object.
3
+ *
4
+ * - `user.roles` is a `string[]` → used as-is.
5
+ * - `user.role` is a `string` → wrapped to `[role]`.
6
+ * - `user.role` is a `string[]` → used as-is.
7
+ * - neither present (or anonymous) → `[]` (no roles).
8
+ *
9
+ * Non-string entries are filtered out so a malformed field can never leak a
10
+ * truthy match.
11
+ */
12
+ export function defaultRoleResolver(user) {
13
+ if (user == null || typeof user !== 'object')
14
+ return [];
15
+ const u = user;
16
+ const out = [];
17
+ if (Array.isArray(u.roles)) {
18
+ for (const r of u.roles)
19
+ if (typeof r === 'string')
20
+ out.push(r);
21
+ }
22
+ if (typeof u.role === 'string') {
23
+ out.push(u.role);
24
+ }
25
+ else if (Array.isArray(u.role)) {
26
+ for (const r of u.role)
27
+ if (typeof r === 'string')
28
+ out.push(r);
29
+ }
30
+ return out;
31
+ }
32
+ //# sourceMappingURL=role-provider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"role-provider.js","sourceRoot":"","sources":["../src/role-provider.ts"],"names":[],"mappings":"AA8BA;;;;;;;;;;GAUG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAU;IAC5C,IAAI,IAAI,IAAI,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAC;IACxD,MAAM,CAAC,GAAG,IAA2C,CAAC;IACtD,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;QAC3B,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK;YAAE,IAAI,OAAO,CAAC,KAAK,QAAQ;gBAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClE,CAAC;IACD,IAAI,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC/B,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACnB,CAAC;SAAM,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;QACjC,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI;YAAE,IAAI,OAAO,CAAC,KAAK,QAAQ;gBAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACjE,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
package/dist/tokens.d.ts CHANGED
@@ -2,10 +2,27 @@
2
2
  export declare const AUTHZ_MODULE_OPTIONS: unique symbol;
3
3
  /** Injection token for the optional {@link ResourceResolver} registered by the app. */
4
4
  export declare const RESOURCE_RESOLVER: unique symbol;
5
+ /**
6
+ * Injection token holding the optional resource-loader map consulted by the opt-in
7
+ * `POST /authz/can` endpoint to rehydrate a `{ type, id }` shim into the REAL entity
8
+ * instance before authorizing — so an instance-bound `@Policy` matches by constructor.
9
+ *
10
+ * Resolves to a `ResourceLoaderMap` (`Record<type, (id) => instance | Promise<instance>>`)
11
+ * keyed by the resource `type` name the client/codegen emits. Populated from the
12
+ * `resourceLoaders` forRoot option; an adapter MAY also bind this token to register
13
+ * loaders, mirroring the {@link PERMISSION_PROVIDER}/{@link ROLE_PROVIDER} seam.
14
+ *
15
+ * Shared via `Symbol.for(key)` (global registry) so an external package binding this
16
+ * same key resolves to the SAME symbol instance. Consulted with `@Optional()` — absent
17
+ * or empty, the endpoint behaves exactly as before (class-level / ad-hoc only).
18
+ */
19
+ export declare const RESOURCE_HYDRATOR: unique symbol;
5
20
  /** Metadata key for `@Policy(Resource)` — stores the resource class on the policy. */
6
21
  export declare const POLICY_RESOURCE_METADATA = "nestjs-authz:policy-resource";
7
22
  /** Metadata key for `@Can(ability, Resource?)` — stores the ability descriptor on a route. */
8
23
  export declare const CAN_METADATA = "nestjs-authz:can";
24
+ /** Metadata key for `@Roles(...roles)` — stores the allowed role names on a route. */
25
+ export declare const ROLES_METADATA = "nestjs-authz:roles";
9
26
  /**
10
27
  * Cross-lib injection token for an optional {@link PermissionProvider} — the seam the
11
28
  * RBAC adapter (`@dudousxd/nestjs-authz-typeorm`) registers so that a model-less,
@@ -18,6 +35,18 @@ export declare const CAN_METADATA = "nestjs-authz:can";
18
35
  * as before (backward-compatible: unknown abilities still throw).
19
36
  */
20
37
  export declare const PERMISSION_PROVIDER: unique symbol;
38
+ /**
39
+ * Cross-lib injection token for an optional {@link RoleProvider} — the seam an RBAC
40
+ * adapter (`@dudousxd/nestjs-authz-typeorm`) registers so coarse role-checks
41
+ * (`gate.hasRole('teacher')`, `@Roles('admin')`) consult a persisted role store.
42
+ *
43
+ * Like {@link PERMISSION_PROVIDER}, it shares the global symbol registry via
44
+ * `Symbol.for(key)` so an RBAC package registering this same key resolves to the
45
+ * SAME symbol instance without importing core internals. Consulted with
46
+ * `@Optional()`: when absent, only the default {@link RoleResolver} (roles read off
47
+ * the user object) supplies roles. When BOTH yield roles, the Gate unions them.
48
+ */
49
+ export declare const ROLE_PROVIDER: unique symbol;
21
50
  /**
22
51
  * Cross-lib injection token for the current-request context accessor, owned by
23
52
  * `@dudousxd/nestjs-context`. We do NOT import nestjs-context — instead we share
@@ -1 +1 @@
1
- {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,eAAO,MAAM,oBAAoB,eAA+C,CAAC;AAEjF,uFAAuF;AACvF,eAAO,MAAM,iBAAiB,eAAyD,CAAC;AAExF,sFAAsF;AACtF,eAAO,MAAM,wBAAwB,iCAAiC,CAAC;AAEvE,8FAA8F;AAC9F,eAAO,MAAM,YAAY,qBAAqB,CAAC;AAE/C;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB,eAA2D,CAAC;AAE5F;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,eAAkD,CAAC"}
1
+ {"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,eAAO,MAAM,oBAAoB,eAA+C,CAAC;AAEjF,uFAAuF;AACvF,eAAO,MAAM,iBAAiB,eAAyD,CAAC;AAExF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,iBAAiB,eAAyD,CAAC;AAExF,sFAAsF;AACtF,eAAO,MAAM,wBAAwB,iCAAiC,CAAC;AAEvE,8FAA8F;AAC9F,eAAO,MAAM,YAAY,qBAAqB,CAAC;AAE/C,sFAAsF;AACtF,eAAO,MAAM,cAAc,uBAAuB,CAAC;AAEnD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB,eAA2D,CAAC;AAE5F;;;;;;;;;;GAUG;AACH,eAAO,MAAM,aAAa,eAAqD,CAAC;AAEhF;;;;;;;;GAQG;AACH,eAAO,MAAM,gBAAgB,eAAkD,CAAC"}
package/dist/tokens.js CHANGED
@@ -2,10 +2,27 @@
2
2
  export const AUTHZ_MODULE_OPTIONS = Symbol.for('@dudousxd/nestjs-authz:options');
3
3
  /** Injection token for the optional {@link ResourceResolver} registered by the app. */
4
4
  export const RESOURCE_RESOLVER = Symbol.for('@dudousxd/nestjs-authz:resource-resolver');
5
+ /**
6
+ * Injection token holding the optional resource-loader map consulted by the opt-in
7
+ * `POST /authz/can` endpoint to rehydrate a `{ type, id }` shim into the REAL entity
8
+ * instance before authorizing — so an instance-bound `@Policy` matches by constructor.
9
+ *
10
+ * Resolves to a `ResourceLoaderMap` (`Record<type, (id) => instance | Promise<instance>>`)
11
+ * keyed by the resource `type` name the client/codegen emits. Populated from the
12
+ * `resourceLoaders` forRoot option; an adapter MAY also bind this token to register
13
+ * loaders, mirroring the {@link PERMISSION_PROVIDER}/{@link ROLE_PROVIDER} seam.
14
+ *
15
+ * Shared via `Symbol.for(key)` (global registry) so an external package binding this
16
+ * same key resolves to the SAME symbol instance. Consulted with `@Optional()` — absent
17
+ * or empty, the endpoint behaves exactly as before (class-level / ad-hoc only).
18
+ */
19
+ export const RESOURCE_HYDRATOR = Symbol.for('@dudousxd/nestjs-authz:resource-hydrator');
5
20
  /** Metadata key for `@Policy(Resource)` — stores the resource class on the policy. */
6
21
  export const POLICY_RESOURCE_METADATA = 'nestjs-authz:policy-resource';
7
22
  /** Metadata key for `@Can(ability, Resource?)` — stores the ability descriptor on a route. */
8
23
  export const CAN_METADATA = 'nestjs-authz:can';
24
+ /** Metadata key for `@Roles(...roles)` — stores the allowed role names on a route. */
25
+ export const ROLES_METADATA = 'nestjs-authz:roles';
9
26
  /**
10
27
  * Cross-lib injection token for an optional {@link PermissionProvider} — the seam the
11
28
  * RBAC adapter (`@dudousxd/nestjs-authz-typeorm`) registers so that a model-less,
@@ -18,6 +35,18 @@ export const CAN_METADATA = 'nestjs-authz:can';
18
35
  * as before (backward-compatible: unknown abilities still throw).
19
36
  */
20
37
  export const PERMISSION_PROVIDER = Symbol.for('@dudousxd/nestjs-authz:permission-provider');
38
+ /**
39
+ * Cross-lib injection token for an optional {@link RoleProvider} — the seam an RBAC
40
+ * adapter (`@dudousxd/nestjs-authz-typeorm`) registers so coarse role-checks
41
+ * (`gate.hasRole('teacher')`, `@Roles('admin')`) consult a persisted role store.
42
+ *
43
+ * Like {@link PERMISSION_PROVIDER}, it shares the global symbol registry via
44
+ * `Symbol.for(key)` so an RBAC package registering this same key resolves to the
45
+ * SAME symbol instance without importing core internals. Consulted with
46
+ * `@Optional()`: when absent, only the default {@link RoleResolver} (roles read off
47
+ * the user object) supplies roles. When BOTH yield roles, the Gate unions them.
48
+ */
49
+ export const ROLE_PROVIDER = Symbol.for('@dudousxd/nestjs-authz:role-provider');
21
50
  /**
22
51
  * Cross-lib injection token for the current-request context accessor, owned by
23
52
  * `@dudousxd/nestjs-context`. We do NOT import nestjs-context — instead we share
@@ -1 +1 @@
1
- {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,MAAM,CAAC,MAAM,oBAAoB,GAAG,MAAM,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAC;AAEjF,uFAAuF;AACvF,MAAM,CAAC,MAAM,iBAAiB,GAAG,MAAM,CAAC,GAAG,CAAC,0CAA0C,CAAC,CAAC;AAExF,sFAAsF;AACtF,MAAM,CAAC,MAAM,wBAAwB,GAAG,8BAA8B,CAAC;AAEvE,8FAA8F;AAC9F,MAAM,CAAC,MAAM,YAAY,GAAG,kBAAkB,CAAC;AAE/C;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC,GAAG,CAAC,4CAA4C,CAAC,CAAC;AAE5F;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,CAAC,GAAG,CAAC,mCAAmC,CAAC,CAAC"}
1
+ {"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA,uEAAuE;AACvE,MAAM,CAAC,MAAM,oBAAoB,GAAG,MAAM,CAAC,GAAG,CAAC,gCAAgC,CAAC,CAAC;AAEjF,uFAAuF;AACvF,MAAM,CAAC,MAAM,iBAAiB,GAAG,MAAM,CAAC,GAAG,CAAC,0CAA0C,CAAC,CAAC;AAExF;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,MAAM,CAAC,GAAG,CAAC,0CAA0C,CAAC,CAAC;AAExF,sFAAsF;AACtF,MAAM,CAAC,MAAM,wBAAwB,GAAG,8BAA8B,CAAC;AAEvE,8FAA8F;AAC9F,MAAM,CAAC,MAAM,YAAY,GAAG,kBAAkB,CAAC;AAE/C,sFAAsF;AACtF,MAAM,CAAC,MAAM,cAAc,GAAG,oBAAoB,CAAC;AAEnD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,MAAM,CAAC,GAAG,CAAC,4CAA4C,CAAC,CAAC;AAE5F;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,MAAM,CAAC,GAAG,CAAC,sCAAsC,CAAC,CAAC;AAEhF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,CAAC,GAAG,CAAC,mCAAmC,CAAC,CAAC"}
package/dist/types.d.ts CHANGED
@@ -31,6 +31,18 @@ export type PolicyInstance = Record<string, unknown> & {
31
31
  * An ad-hoc gate: a model-less ability resolved by name via `gate.define`.
32
32
  */
33
33
  export type GateFn = (user: User, resource?: Resource) => boolean | Promise<boolean>;
34
+ /**
35
+ * A resource loader: turns an `id` into the REAL entity instance for a resource
36
+ * `type`. Used by the opt-in `POST /authz/can` endpoint to rehydrate the client's
37
+ * `{ type, id }` shim before authorizing, so an instance-bound `@Policy` matches by
38
+ * constructor. Returning nullish signals "not found" (the endpoint denies). May be async.
39
+ */
40
+ export type ResourceLoader = (id: string | number) => unknown | Promise<unknown>;
41
+ /**
42
+ * Resource loaders keyed by the resource `type` name as emitted by the client/codegen
43
+ * (e.g. `'Post'`). See {@link AuthzModuleOptions.resourceLoaders}.
44
+ */
45
+ export type ResourceLoaderMap = Record<string, ResourceLoader>;
34
46
  /**
35
47
  * Global super-admin before-hook. Runs before any policy/gate; truthy → allow.
36
48
  * `void`/`undefined`/`false` falls through to normal resolution.
@@ -69,6 +81,19 @@ export interface AuthzModuleOptions {
69
81
  * you pass directly and never invokes this hook.
70
82
  */
71
83
  resolveUser?: (ref: UserRef) => User | undefined | Promise<User | undefined>;
84
+ /**
85
+ * Override the default {@link RoleResolver} used by coarse role-checks
86
+ * (`gate.hasRole('teacher')`, `@Roles('admin')`). The default reads `user.roles`
87
+ * (`string[]`) OR `user.role` (`string | string[]`) off the user object, so
88
+ * role-checks work with ZERO RBAC tables. Provide this to derive roles from a
89
+ * different shape. When the optional `ROLE_PROVIDER` seam is ALSO registered, the
90
+ * Gate unions both sources' roles.
91
+ *
92
+ * On the context path the resolver receives whatever the context produced (the
93
+ * raw {@link UserRef} unless `resolveUser` hydrated the entity); on the explicit
94
+ * `gate.forUser(entity)` path it receives exactly what you passed.
95
+ */
96
+ resolveRoles?: (user: User) => string[] | undefined | Promise<string[] | undefined>;
72
97
  /**
73
98
  * Override the default {@link ResourceResolver} used to load an instance for
74
99
  * `@Can(ability, Resource)` routes. Defaults to {@link IdParamResourceResolver}
@@ -83,6 +108,35 @@ export interface AuthzModuleOptions {
83
108
  * is supplied.
84
109
  */
85
110
  idParam?: string;
111
+ /**
112
+ * Opt-in `POST /authz/can` fallback endpoint. **Off by default.** When enabled,
113
+ * registers a controller that runs `gate.allows(ability, resource?)` for the
114
+ * current (context) user and returns `{ allowed: boolean }`. This is the
115
+ * last-resort path the codegen-emitted `can()` helper targets.
116
+ *
117
+ * - `true` → mount at the default path `authz/can`.
118
+ * - `string` → mount at that path (e.g. `'api/authz/can'`).
119
+ * - `false`/omitted → no endpoint is registered.
120
+ *
121
+ * Prefer the no-request paths (shared Inertia props, per-resource `can` maps)
122
+ * — this endpoint exists only for abilities not already hydrated on the client.
123
+ */
124
+ canEndpoint?: boolean | string;
125
+ /**
126
+ * Resource loaders keyed by the resource `type` name the client/codegen emits
127
+ * (e.g. `'Post'`). They close the per-instance gap in the {@link canEndpoint}
128
+ * fallback: when the endpoint receives `{ ability, resource: { type, id } }` and a
129
+ * loader is registered for `type`, it `await`s `loader(id)` and authorizes the REAL
130
+ * entity — so an instance-bound `@Policy` matches by constructor and its method runs
131
+ * with the loaded resource. A loader returning nullish is treated as "not found"
132
+ * (the endpoint denies). Types WITHOUT a loader keep the prior behavior (class-level
133
+ * / ad-hoc only; a resource-bound ability still denies). Opt-in: unset → unchanged.
134
+ *
135
+ * ```ts
136
+ * resourceLoaders: { Post: (id) => postRepo.findOneBy({ id: Number(id) }) }
137
+ * ```
138
+ */
139
+ resourceLoaders?: ResourceLoaderMap;
86
140
  }
87
141
  export interface AuthzModuleOptionsFactory {
88
142
  createAuthzOptions(): Promise<AuthzModuleOptions> | AuthzModuleOptions;
@@ -93,5 +147,18 @@ export interface AuthzModuleAsyncOptions {
93
147
  useClass?: Type<AuthzModuleOptionsFactory>;
94
148
  useFactory?: (...args: unknown[]) => Promise<AuthzModuleOptions> | AuthzModuleOptions;
95
149
  inject?: unknown[];
150
+ /**
151
+ * Opt-in `POST /authz/can` fallback endpoint (see {@link AuthzModuleOptions.canEndpoint}).
152
+ * Declared statically here because controllers are registered at module-definition
153
+ * time, before the async options factory resolves. Off by default.
154
+ */
155
+ canEndpoint?: boolean | string;
156
+ /**
157
+ * Resource loaders for the {@link canEndpoint} fallback
158
+ * (see {@link AuthzModuleOptions.resourceLoaders}). May also be returned from the
159
+ * async options factory; either way the endpoint reads them from the resolved
160
+ * options, so this declaration is for the rarer case of supplying them statically.
161
+ */
162
+ resourceLoaders?: ResourceLoaderMap;
96
163
  }
97
164
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D;;;GAGG;AACH,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC;AAE9B;;;GAGG;AACH,MAAM,MAAM,IAAI,GAAG,OAAO,CAAC;AAE3B;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,CAAC,EAAE,QAAQ,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAE3F;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG,CAC7B,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,MAAM,KACZ,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;AAExD,4EAA4E;AAC5E,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IACrD,MAAM,CAAC,EAAE,gBAAgB,CAAC;CAC3B,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,MAAM,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,CAAC,EAAE,QAAQ,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAErF;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,CAC3B,IAAI,EAAE,IAAI,EACV,OAAO,EAAE,MAAM,KACZ,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;AAExD,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC;IACvC;;;;;;;;;;OAUG;IACH,UAAU,CAAC,EAAE,cAAc,CAAC;IAC5B;;;;;;;;;;;;OAYG;IACH,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC,IAAI,GAAG,SAAS,CAAC,CAAC;IAC7E;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IACpC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,yBAAyB;IACxC,kBAAkB,IAAI,OAAO,CAAC,kBAAkB,CAAC,GAAG,kBAAkB,CAAC;CACxE;AAED,MAAM,WAAW,uBAAuB;IACtC,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC;IACpB,WAAW,CAAC,EAAE,IAAI,CAAC,yBAAyB,CAAC,CAAC;IAC9C,QAAQ,CAAC,EAAE,IAAI,CAAC,yBAAyB,CAAC,CAAC;IAC3C,UAAU,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC,kBAAkB,CAAC,GAAG,kBAAkB,CAAC;IACtF,MAAM,CAAC,EAAE,OAAO,EAAE,CAAC;CACpB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dudousxd/nestjs-authz",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "NestJS authorization — Laravel-style Gates & Policies, @Can guard, resource resolver (zero DB).",
5
5
  "license": "MIT",
6
6
  "repository": {