@dudousxd/nestjs-authz 0.5.1 → 0.6.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.
@@ -0,0 +1,48 @@
1
+ import { permissionSatisfied } from './permission-matcher.js';
2
+ /**
3
+ * A memo over a {@link PermissionProvider}'s `getPermissions(user)` so a user's
4
+ * granted-permission set is fetched AT MOST ONCE per logical scope (a single
5
+ * `allowsMany` batch, or — when wired to nestjs-context — a single HTTP request).
6
+ *
7
+ * Without it, every `gate.allows(...)` re-fetches the set (an N+1 within a list
8
+ * page, since each card runs its own check). The cache keys by the resolved
9
+ * `user` identity so two checks for the SAME user share one fetch while a
10
+ * different user still triggers its own.
11
+ *
12
+ * Only the `getPermissions` (full-set, wildcard-matched) path is memoized — the
13
+ * exact-match `hasPermission` fast path is left to the provider, which is free to
14
+ * memoize internally. A provider WITHOUT `getPermissions` makes this a no-op.
15
+ */
16
+ export class PermissionCache {
17
+ // Keyed by the user object identity. `Map` (not WeakMap) because a batch/request
18
+ // cache is short-lived and we also accept primitive ids/refs as keys.
19
+ byUser = new Map();
20
+ /**
21
+ * Resolve the user's granted-permission set via the provider, memoized for the
22
+ * cache's lifetime. Returns `undefined` when the provider can't list grants
23
+ * (no `getPermissions`, or it yielded nullish) so the caller falls back to the
24
+ * exact-match `hasPermission` path.
25
+ */
26
+ async getPermissions(provider, user) {
27
+ if (typeof provider.getPermissions !== 'function')
28
+ return undefined;
29
+ const cached = this.byUser.get(user);
30
+ if (cached)
31
+ return cached;
32
+ const pending = Promise.resolve(provider.getPermissions(user)).then((granted) => granted == null ? undefined : new Set(granted));
33
+ this.byUser.set(user, pending);
34
+ return pending;
35
+ }
36
+ /**
37
+ * True when the user's (memoized) granted set satisfies `ability` under the
38
+ * core's wildcard matcher. `undefined` when no set is available (caller should
39
+ * defer to `hasPermission`).
40
+ */
41
+ async satisfies(provider, user, ability) {
42
+ const set = await this.getPermissions(provider, user);
43
+ if (set === undefined)
44
+ return undefined;
45
+ return permissionSatisfied(set, ability);
46
+ }
47
+ }
48
+ //# sourceMappingURL=permission-cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission-cache.js","sourceRoot":"","sources":["../src/permission-cache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAI9D;;;;;;;;;;;;;GAaG;AACH,MAAM,OAAO,eAAe;IAC1B,iFAAiF;IACjF,sEAAsE;IACrD,MAAM,GAAG,IAAI,GAAG,EAAqD,CAAC;IAEvF;;;;;OAKG;IACH,KAAK,CAAC,cAAc,CAClB,QAA4B,EAC5B,IAAU;QAEV,IAAI,OAAO,QAAQ,CAAC,cAAc,KAAK,UAAU;YAAE,OAAO,SAAS,CAAC;QACpE,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACrC,IAAI,MAAM;YAAE,OAAO,MAAM,CAAC;QAC1B,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,EAAE,CAC9E,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,CAC/C,CAAC;QACF,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC/B,OAAO,OAAO,CAAC;IACjB,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,SAAS,CACb,QAA4B,EAC5B,IAAU,EACV,OAAe;QAEf,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;QACtD,IAAI,GAAG,KAAK,SAAS;YAAE,OAAO,SAAS,CAAC;QACxC,OAAO,mBAAmB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IAC3C,CAAC;CACF"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Laravel/spatie-style wildcard permission matching.
3
+ *
4
+ * A GRANTED permission (the left-hand side) may contain `*` wildcard segments;
5
+ * the REQUIRED ability (the check) is always a concrete, literal string. Matching
6
+ * is segment-based on the `.` separator:
7
+ *
8
+ * - exact string equality matches (`posts.update` ⇒ `posts.update`).
9
+ * - a trailing `*` segment matches ONE-OR-MORE remaining segments
10
+ * (`posts.*` ⇒ `posts.update`, `posts.update.draft`) — but not zero
11
+ * (`posts.*` does NOT match the bare `posts`).
12
+ * - a bare `*` grant matches anything.
13
+ * - `*` only matches at a segment boundary, so `posts.*` does NOT match
14
+ * `comments.update`.
15
+ *
16
+ * Only the GRANTED side is expanded — a `*` in the required ability is treated
17
+ * literally, since a check is never a pattern.
18
+ */
19
+ export declare function permissionMatches(granted: string, required: string): boolean;
20
+ /** True when ANY permission in the granted set satisfies the required ability. */
21
+ export declare function permissionSatisfied(granted: Iterable<string>, required: string): boolean;
22
+ //# sourceMappingURL=permission-matcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission-matcher.d.ts","sourceRoot":"","sources":["../src/permission-matcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CA0B5E;AAED,kFAAkF;AAClF,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAKxF"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Laravel/spatie-style wildcard permission matching.
3
+ *
4
+ * A GRANTED permission (the left-hand side) may contain `*` wildcard segments;
5
+ * the REQUIRED ability (the check) is always a concrete, literal string. Matching
6
+ * is segment-based on the `.` separator:
7
+ *
8
+ * - exact string equality matches (`posts.update` ⇒ `posts.update`).
9
+ * - a trailing `*` segment matches ONE-OR-MORE remaining segments
10
+ * (`posts.*` ⇒ `posts.update`, `posts.update.draft`) — but not zero
11
+ * (`posts.*` does NOT match the bare `posts`).
12
+ * - a bare `*` grant matches anything.
13
+ * - `*` only matches at a segment boundary, so `posts.*` does NOT match
14
+ * `comments.update`.
15
+ *
16
+ * Only the GRANTED side is expanded — a `*` in the required ability is treated
17
+ * literally, since a check is never a pattern.
18
+ */
19
+ export function permissionMatches(granted, required) {
20
+ if (granted === required)
21
+ return true;
22
+ if (granted === '*')
23
+ return true;
24
+ const grantedParts = granted.split('.');
25
+ const requiredParts = required.split('.');
26
+ for (let i = 0; i < grantedParts.length; i++) {
27
+ const g = grantedParts[i];
28
+ if (g === '*') {
29
+ // A trailing `*` matches one-or-more remaining required segments.
30
+ // (Only the final segment may be a wildcard; an interior `*` still works
31
+ // positionally but a trailing `*` is the idiomatic form.)
32
+ if (i === grantedParts.length - 1) {
33
+ return requiredParts.length > i;
34
+ }
35
+ // interior wildcard: this single segment is consumed; continue matching.
36
+ if (requiredParts[i] === undefined)
37
+ return false;
38
+ continue;
39
+ }
40
+ if (g !== requiredParts[i])
41
+ return false;
42
+ }
43
+ // All granted segments consumed and matched exactly → only a true match when
44
+ // the required ability has no extra trailing segments.
45
+ return grantedParts.length === requiredParts.length;
46
+ }
47
+ /** True when ANY permission in the granted set satisfies the required ability. */
48
+ export function permissionSatisfied(granted, required) {
49
+ for (const g of granted) {
50
+ if (permissionMatches(g, required))
51
+ return true;
52
+ }
53
+ return false;
54
+ }
55
+ //# sourceMappingURL=permission-matcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"permission-matcher.js","sourceRoot":"","sources":["../src/permission-matcher.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAAe,EAAE,QAAgB;IACjE,IAAI,OAAO,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACtC,IAAI,OAAO,KAAK,GAAG;QAAE,OAAO,IAAI,CAAC;IAEjC,MAAM,YAAY,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACxC,MAAM,aAAa,GAAG,QAAQ,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IAE1C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,YAAY,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC7C,MAAM,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC;QAC1B,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;YACd,kEAAkE;YAClE,yEAAyE;YACzE,0DAA0D;YAC1D,IAAI,CAAC,KAAK,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAClC,OAAO,aAAa,CAAC,MAAM,GAAG,CAAC,CAAC;YAClC,CAAC;YACD,yEAAyE;YACzE,IAAI,aAAa,CAAC,CAAC,CAAC,KAAK,SAAS;gBAAE,OAAO,KAAK,CAAC;YACjD,SAAS;QACX,CAAC;QACD,IAAI,CAAC,KAAK,aAAa,CAAC,CAAC,CAAC;YAAE,OAAO,KAAK,CAAC;IAC3C,CAAC;IAED,6EAA6E;IAC7E,uDAAuD;IACvD,OAAO,YAAY,CAAC,MAAM,KAAK,aAAa,CAAC,MAAM,CAAC;AACtD,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,mBAAmB,CAAC,OAAyB,EAAE,QAAgB;IAC7E,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,IAAI,iBAAiB,CAAC,CAAC,EAAE,QAAQ,CAAC;YAAE,OAAO,IAAI,CAAC;IAClD,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -18,8 +18,26 @@ import type { Resource, User } from './types.js';
18
18
  * when anonymous). `permission` is the ability name passed to `gate.allows(...)`.
19
19
  * `resource` is the dispatch target, when one was given; providers MAY ignore it —
20
20
  * model-less, named-ability grants (e.g. the typeorm RBAC adapter) do.
21
+ *
22
+ * ## Wildcard / hierarchical permissions
23
+ *
24
+ * To support Laravel/spatie-style wildcard grants (a granted `posts.*` satisfying a
25
+ * check for `posts.update`, or `*` satisfying anything), a provider may ALSO expose
26
+ * {@link PermissionProvider.getPermissions} — the user's full granted permission set.
27
+ * When present, the core applies segment-based wildcard matching against that set
28
+ * (see `permission-matcher.ts`), so wildcard semantics work regardless of adapter
29
+ * and without each adapter re-implementing the matching. `hasPermission` remains the
30
+ * exact-match fast path (and the sole contract for providers that don't list grants).
21
31
  */
22
32
  export interface PermissionProvider {
23
33
  hasPermission(user: User, permission: string, resource?: Resource): boolean | undefined | Promise<boolean | undefined>;
34
+ /**
35
+ * Optional: the full set of permission names granted to `user` (each possibly a
36
+ * wildcard pattern such as `posts.*` or `*`). When implemented, the core matches
37
+ * the checked ability against this set using segment-based wildcard semantics, so
38
+ * a granted `posts.*` satisfies `posts.update`. Return `undefined` to defer to
39
+ * `hasPermission` (e.g. for an anonymous/unmappable user).
40
+ */
41
+ getPermissions?(user: User): Iterable<string> | undefined | Promise<Iterable<string> | undefined>;
24
42
  }
25
43
  //# sourceMappingURL=permission-provider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"permission-provider.d.ts","sourceRoot":"","sources":["../src/permission-provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAEjD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,WAAW,kBAAkB;IACjC,aAAa,CACX,IAAI,EAAE,IAAI,EACV,UAAU,EAAE,MAAM,EAClB,QAAQ,CAAC,EAAE,QAAQ,GAClB,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;CACvD"}
1
+ {"version":3,"file":"permission-provider.d.ts","sourceRoot":"","sources":["../src/permission-provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAEjD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,WAAW,kBAAkB;IACjC,aAAa,CACX,IAAI,EAAE,IAAI,EACV,UAAU,EAAE,MAAM,EAClB,QAAQ,CAAC,EAAE,QAAQ,GAClB,OAAO,GAAG,SAAS,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IACtD;;;;;;OAMG;IACH,cAAc,CAAC,CAAC,IAAI,EAAE,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC,GAAG,SAAS,GAAG,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC;CACnG"}
@@ -0,0 +1,84 @@
1
+ import type { Resource, User } from './types.js';
2
+ /**
3
+ * ORM-neutral query-scope constraint (the `accessibleBy` / Pundit `policy_scope` /
4
+ * Cerbos `PlanResources` concept). Where a policy METHOD decides yes/no for a SINGLE
5
+ * resource, a policy SCOPE produces a constraint that filters a COLLECTION to the rows
6
+ * the user may access — applied at the DB layer instead of over-fetch-then-filter.
7
+ *
8
+ * The representation is a small, pure-data condition AST (no callbacks, fully
9
+ * serializable) so every ORM adapter can walk it and emit a parameterized,
10
+ * identifier-safe `WHERE`. Compare:
11
+ *
12
+ * - **CASL `accessibleBy`** — turns ability rules into an ORM `where` object.
13
+ * - **Pundit `policy_scope`** — a Scope class returning a narrowed relation.
14
+ * - **Cerbos Query Plan (`PlanResources`)** — returns `ALWAYS_ALLOWED` /
15
+ * `ALWAYS_DENIED` / a condition AST of operands. This AST mirrors that shape.
16
+ *
17
+ * Two terminal verdicts bracket the AST and keep it aligned with the Gate's
18
+ * deny-by-default + super-admin/before-grant semantics:
19
+ *
20
+ * - {@link ScopeAll} (`allow-all`) — the user sees every row (super-admin, a
21
+ * `before` grant, or a permission-provider grant). No `WHERE` is added.
22
+ * - {@link ScopeNone} (`deny-all`) — the user sees no rows (anonymous, or a policy
23
+ * with no `scope`/`viewAny`). The adapter emits an always-false predicate.
24
+ */
25
+ export type ScopeConstraint = ScopeAll | ScopeNone | ScopeNode;
26
+ /** Terminal: the user may access every row — no filter is applied. */
27
+ export interface ScopeAll {
28
+ kind: 'all';
29
+ }
30
+ /** Terminal: the user may access no rows — an always-false predicate is applied. */
31
+ export interface ScopeNone {
32
+ kind: 'none';
33
+ }
34
+ /** A non-terminal constraint: a single condition or a boolean group of them. */
35
+ export type ScopeNode = ScopeCondition | ScopeGroup;
36
+ /** Comparison operators a {@link ScopeCondition} may use. ORM-neutral; adapters map them. */
37
+ export type ScopeOperator = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'nin' | 'isNull' | 'isNotNull';
38
+ /**
39
+ * A leaf condition: `field <op> value`. `field` names a COLUMN on the scoped entity
40
+ * (validated as a safe identifier by the adapter). `value` is bound as a parameter
41
+ * (never interpolated). `isNull`/`isNotNull` ignore `value`.
42
+ */
43
+ export interface ScopeCondition {
44
+ kind: 'condition';
45
+ field: string;
46
+ op: ScopeOperator;
47
+ value?: unknown;
48
+ }
49
+ /** A boolean group: AND/OR over child nodes. An empty group is the group's identity. */
50
+ export interface ScopeGroup {
51
+ kind: 'and' | 'or';
52
+ nodes: ScopeNode[];
53
+ }
54
+ /** The allow-all terminal (super-admin / before-grant). */
55
+ export declare const scopeAll: ScopeAll;
56
+ /** The deny-all terminal (anonymous / no access). */
57
+ export declare const scopeNone: ScopeNone;
58
+ /** Build a leaf `field <op> value` condition. */
59
+ export declare function where(field: string, op: ScopeOperator, value?: unknown): ScopeCondition;
60
+ /** Shorthand for the common `field = value` (equality) condition. */
61
+ export declare function eq(field: string, value: unknown): ScopeCondition;
62
+ /** Combine nodes with AND. A single node is returned as-is; empty → allow-all. */
63
+ export declare function and(...nodes: ScopeNode[]): ScopeConstraint;
64
+ /** Combine nodes with OR. A single node is returned as-is; empty → deny-all. */
65
+ export declare function or(...nodes: ScopeNode[]): ScopeConstraint;
66
+ /**
67
+ * What a policy `scope` method may return:
68
+ * - a {@link ScopeConstraint} (terminal or AST),
69
+ * - `true` → allow-all (sugar for {@link scopeAll}),
70
+ * - `false`/`null`/`undefined` → deny-all (sugar for {@link scopeNone}).
71
+ *
72
+ * May be async.
73
+ */
74
+ export type ScopeResult = ScopeConstraint | boolean | null | undefined;
75
+ /**
76
+ * A policy scope method. Mirrors Pundit's `Scope#resolve`: given the current user,
77
+ * return the constraint that filters the entity's collection to the accessible rows.
78
+ * Conventionally named `scope` on a `@Policy` class (a `viewAny`-style fallback is
79
+ * also honored by the Gate when no `scope` is present).
80
+ */
81
+ export type ScopeMethod = (user: User, resource?: Resource) => ScopeResult | Promise<ScopeResult>;
82
+ /** Normalize a {@link ScopeResult} (incl. boolean/nullish sugar) into a {@link ScopeConstraint}. */
83
+ export declare function normalizeScope(result: ScopeResult): ScopeConstraint;
84
+ //# sourceMappingURL=scope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope.d.ts","sourceRoot":"","sources":["../src/scope.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAEjD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,MAAM,eAAe,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,CAAC;AAE/D,sEAAsE;AACtE,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,KAAK,CAAC;CACb;AAED,oFAAoF;AACpF,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,gFAAgF;AAChF,MAAM,MAAM,SAAS,GAAG,cAAc,GAAG,UAAU,CAAC;AAEpD,6FAA6F;AAC7F,MAAM,MAAM,aAAa,GACrB,IAAI,GACJ,IAAI,GACJ,IAAI,GACJ,KAAK,GACL,IAAI,GACJ,KAAK,GACL,IAAI,GACJ,KAAK,GACL,QAAQ,GACR,WAAW,CAAC;AAEhB;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,WAAW,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,aAAa,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,wFAAwF;AACxF,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,KAAK,GAAG,IAAI,CAAC;IACnB,KAAK,EAAE,SAAS,EAAE,CAAC;CACpB;AAED,2DAA2D;AAC3D,eAAO,MAAM,QAAQ,EAAE,QAA0B,CAAC;AAElD,qDAAqD;AACrD,eAAO,MAAM,SAAS,EAAE,SAA4B,CAAC;AAErD,iDAAiD;AACjD,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,aAAa,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,cAAc,CAIvF;AAED,qEAAqE;AACrE,wBAAgB,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,cAAc,CAEhE;AAED,kFAAkF;AAClF,wBAAgB,GAAG,CAAC,GAAG,KAAK,EAAE,SAAS,EAAE,GAAG,eAAe,CAI1D;AAED,gFAAgF;AAChF,wBAAgB,EAAE,CAAC,GAAG,KAAK,EAAE,SAAS,EAAE,GAAG,eAAe,CAIzD;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,WAAW,GAAG,eAAe,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC;AAEvE;;;;;GAKG;AACH,MAAM,MAAM,WAAW,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,QAAQ,CAAC,EAAE,QAAQ,KAAK,WAAW,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;AAElG,oGAAoG;AACpG,wBAAgB,cAAc,CAAC,MAAM,EAAE,WAAW,GAAG,eAAe,CAKnE"}
package/dist/scope.js ADDED
@@ -0,0 +1,41 @@
1
+ /** The allow-all terminal (super-admin / before-grant). */
2
+ export const scopeAll = { kind: 'all' };
3
+ /** The deny-all terminal (anonymous / no access). */
4
+ export const scopeNone = { kind: 'none' };
5
+ /** Build a leaf `field <op> value` condition. */
6
+ export function where(field, op, value) {
7
+ return op === 'isNull' || op === 'isNotNull'
8
+ ? { kind: 'condition', field, op }
9
+ : { kind: 'condition', field, op, value };
10
+ }
11
+ /** Shorthand for the common `field = value` (equality) condition. */
12
+ export function eq(field, value) {
13
+ return { kind: 'condition', field, op: 'eq', value };
14
+ }
15
+ /** Combine nodes with AND. A single node is returned as-is; empty → allow-all. */
16
+ export function and(...nodes) {
17
+ if (nodes.length === 0)
18
+ return scopeAll;
19
+ if (nodes.length === 1)
20
+ return nodes[0];
21
+ return { kind: 'and', nodes };
22
+ }
23
+ /** Combine nodes with OR. A single node is returned as-is; empty → deny-all. */
24
+ export function or(...nodes) {
25
+ if (nodes.length === 0)
26
+ return scopeNone;
27
+ if (nodes.length === 1)
28
+ return nodes[0];
29
+ return { kind: 'or', nodes };
30
+ }
31
+ /** Normalize a {@link ScopeResult} (incl. boolean/nullish sugar) into a {@link ScopeConstraint}. */
32
+ export function normalizeScope(result) {
33
+ if (result == null)
34
+ return scopeNone;
35
+ if (result === true)
36
+ return scopeAll;
37
+ if (result === false)
38
+ return scopeNone;
39
+ return result;
40
+ }
41
+ //# sourceMappingURL=scope.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scope.js","sourceRoot":"","sources":["../src/scope.ts"],"names":[],"mappings":"AAuEA,2DAA2D;AAC3D,MAAM,CAAC,MAAM,QAAQ,GAAa,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AAElD,qDAAqD;AACrD,MAAM,CAAC,MAAM,SAAS,GAAc,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAErD,iDAAiD;AACjD,MAAM,UAAU,KAAK,CAAC,KAAa,EAAE,EAAiB,EAAE,KAAe;IACrE,OAAO,EAAE,KAAK,QAAQ,IAAI,EAAE,KAAK,WAAW;QAC1C,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,EAAE;QAClC,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC;AAC9C,CAAC;AAED,qEAAqE;AACrE,MAAM,UAAU,EAAE,CAAC,KAAa,EAAE,KAAc;IAC9C,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AACvD,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,GAAG,CAAC,GAAG,KAAkB;IACvC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,QAAQ,CAAC;IACxC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,CAAC,CAAc,CAAC;IACrD,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAChC,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,EAAE,CAAC,GAAG,KAAkB;IACtC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IACzC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC,CAAC,CAAc,CAAC;IACrD,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;AAC/B,CAAC;AAoBD,oGAAoG;AACpG,MAAM,UAAU,cAAc,CAAC,MAAmB;IAChD,IAAI,MAAM,IAAI,IAAI;QAAE,OAAO,SAAS,CAAC;IACrC,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,QAAQ,CAAC;IACrC,IAAI,MAAM,KAAK,KAAK;QAAE,OAAO,SAAS,CAAC;IACvC,OAAO,MAAM,CAAC;AAChB,CAAC"}
package/dist/types.d.ts CHANGED
@@ -11,26 +11,62 @@ export type Resource = object;
11
11
  * is whatever the app's auth layer produced (or `undefined` when unauthenticated).
12
12
  */
13
13
  export type User = unknown;
14
+ /**
15
+ * The richer result a policy method / ad-hoc gate may return instead of a bare
16
+ * boolean: a verdict plus an optional human-readable `message` explaining a denial.
17
+ * The message is surfaced on the thrown `ForbiddenException` (so a 403 is debuggable)
18
+ * and published on the diagnostics decision. Returning a bare boolean is unchanged.
19
+ *
20
+ * Mirrors Laravel's `Response::allow()/deny('message')`.
21
+ */
22
+ export interface PolicyResponse {
23
+ allowed: boolean;
24
+ message?: string;
25
+ }
26
+ /**
27
+ * What a policy method / gate / hook may return: a bare boolean (unchanged) or the
28
+ * richer {@link PolicyResponse}. Hooks additionally allow `void`/`undefined` to mean
29
+ * "no opinion — fall through".
30
+ */
31
+ export type PolicyResult = boolean | PolicyResponse;
14
32
  /**
15
33
  * A policy method: decides a single ability for a `(user, resource?)` pair.
16
- * Class-level abilities (e.g. `create`) omit the resource. May be async.
34
+ * Class-level abilities (e.g. `create`) omit the resource. May be async. May return
35
+ * a bare boolean (unchanged) or a richer {@link PolicyResponse} with a deny message.
17
36
  */
18
- export type PolicyMethod = (user: User, resource?: Resource) => boolean | Promise<boolean>;
37
+ export type PolicyMethod = (user: User, resource?: Resource) => PolicyResult | Promise<PolicyResult>;
19
38
  /**
20
39
  * Optional per-policy `before` hook. Runs before the ability method.
21
40
  * - return `true` → allow (short-circuit)
22
41
  * - return `false` → deny (short-circuit)
23
42
  * - return `void`/`undefined` → fall through to the ability method
24
43
  */
25
- export type PolicyBeforeHook = (user: User, ability: string) => boolean | undefined | Promise<boolean | undefined>;
44
+ export type PolicyBeforeHook = (user: User, ability: string) => PolicyResult | undefined | Promise<PolicyResult | undefined>;
26
45
  /** A policy instance is an arbitrary object whose methods are abilities. */
27
46
  export type PolicyInstance = Record<string, unknown> & {
28
47
  before?: PolicyBeforeHook;
29
48
  };
30
49
  /**
31
50
  * An ad-hoc gate: a model-less ability resolved by name via `gate.define`.
51
+ * May return a bare boolean (unchanged) or a richer {@link PolicyResponse}.
32
52
  */
33
- export type GateFn = (user: User, resource?: Resource) => boolean | Promise<boolean>;
53
+ export type GateFn = (user: User, resource?: Resource) => PolicyResult | Promise<PolicyResult>;
54
+ /**
55
+ * Global `after` hook. Mirrors Laravel's `Gate::after()`: runs AFTER policy/gate
56
+ * resolution and may observe or override the decision.
57
+ *
58
+ * `result` is the verdict produced by the policy/gate (`true`/`false`), or
59
+ * `undefined` when the policy/gate returned a nullish value (it had "no opinion").
60
+ *
61
+ * Override semantics (Laravel-idiomatic): the hook's return value REPLACES the
62
+ * decision ONLY when the policy/gate produced no explicit verdict (`result` is
63
+ * `undefined`). When the policy/gate DID decide (returned a boolean), `after` is
64
+ * purely an observer and its return value is ignored — so it can never silently
65
+ * overturn an explicit allow/deny. Returning `void`/`undefined` always leaves the
66
+ * decision untouched. Like a policy, it may return a {@link PolicyResponse} to
67
+ * attach a deny message.
68
+ */
69
+ export type AfterHook = (user: User, ability: string, result: boolean | undefined, resource?: Resource) => PolicyResult | undefined | Promise<PolicyResult | undefined>;
34
70
  /**
35
71
  * A resource loader: turns an `id` into the REAL entity instance for a resource
36
72
  * `type`. Used by the opt-in `POST /authz/can` endpoint to rehydrate the client's
@@ -47,7 +83,7 @@ export type ResourceLoaderMap = Record<string, ResourceLoader>;
47
83
  * Global super-admin before-hook. Runs before any policy/gate; truthy → allow.
48
84
  * `void`/`undefined`/`false` falls through to normal resolution.
49
85
  */
50
- export type SuperAdminHook = (user: User, ability: string) => boolean | undefined | Promise<boolean | undefined>;
86
+ export type SuperAdminHook = (user: User, ability: string) => PolicyResult | undefined | Promise<PolicyResult | undefined>;
51
87
  export interface AuthzModuleOptions {
52
88
  /**
53
89
  * Policy classes to register explicitly. Each must be decorated with
@@ -67,6 +103,18 @@ export interface AuthzModuleOptions {
67
103
  * explicit `gate.forUser(entity)` path you receive exactly what you passed.
68
104
  */
69
105
  superAdmin?: SuperAdminHook;
106
+ /**
107
+ * Global `after` hook (Laravel's `Gate::after()`). Runs AFTER policy/gate
108
+ * resolution and may observe or override the decision. It can only OVERRIDE the
109
+ * verdict when the policy/gate produced no explicit result (returned nullish);
110
+ * when the policy/gate decided explicitly, `after` is observe-only and its return
111
+ * value is ignored. Returning `void`/`undefined` always leaves the decision
112
+ * untouched. See {@link AfterHook}.
113
+ *
114
+ * Receives the same (possibly un-hydrated) `user` as {@link superAdmin} on the
115
+ * context path — apply {@link resolveUser} if you need the full entity.
116
+ */
117
+ after?: AfterHook;
70
118
  /**
71
119
  * Optional hook to hydrate the full user entity from the context's
72
120
  * {@link UserRef} (`{ type, id }`) before policies / `before` / `superAdmin`
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
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.5.1",
3
+ "version": "0.6.0",
4
4
  "description": "NestJS authorization — Laravel-style Gates & Policies, @Can guard, resource resolver (zero DB).",
5
5
  "license": "MIT",
6
6
  "repository": {