@ahmed109/gatekeeper-core 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.
package/dist/index.cjs CHANGED
@@ -25,15 +25,19 @@ __export(index_exports, {
25
25
  canAll: () => canAll,
26
26
  canAny: () => canAny,
27
27
  createSubject: () => createSubject,
28
+ defineRoleHierarchy: () => defineRoleHierarchy,
28
29
  defineRolePermissions: () => defineRolePermissions,
29
30
  evaluatePermission: () => evaluatePermission,
31
+ explain: () => explain,
30
32
  fromJwtClaims: () => fromJwtClaims,
31
33
  fromPermissions: () => fromPermissions,
32
34
  fromRoles: () => fromRoles,
33
35
  getUserEffectivePermissions: () => getUserEffectivePermissions,
34
36
  isFeatureEnabled: () => isFeatureEnabled,
35
37
  normalizeFeatureFlags: () => normalizeFeatureFlags,
36
- parseEvaluateOptions: () => parseEvaluateOptions
38
+ parseEvaluateOptions: () => parseEvaluateOptions,
39
+ permissionMatches: () => permissionMatches,
40
+ resolveInheritedRoles: () => resolveInheritedRoles
37
41
  });
38
42
  module.exports = __toCommonJS(index_exports);
39
43
 
@@ -55,13 +59,28 @@ function defineRolePermissions(customMap, baseMap = DEFAULT_ROLE_PERMISSIONS) {
55
59
  }
56
60
  return merged;
57
61
  }
62
+ function defineRoleHierarchy(hierarchy) {
63
+ return Object.fromEntries(
64
+ Object.entries(hierarchy).map(([role, parents]) => [role, [...parents]])
65
+ );
66
+ }
67
+ function resolveInheritedRoles(roles, hierarchy) {
68
+ const resolved = /* @__PURE__ */ new Set();
69
+ const visit = (role) => {
70
+ if (resolved.has(role)) return;
71
+ resolved.add(role);
72
+ for (const parent of hierarchy?.[role] ?? []) visit(parent);
73
+ };
74
+ for (const role of roles) visit(role);
75
+ return [...resolved];
76
+ }
58
77
 
59
78
  // src/evaluator.ts
60
79
  function parseEvaluateOptions(options) {
61
80
  if (!options || typeof options !== "object") {
62
81
  return {};
63
82
  }
64
- if ("roleMap" in options || "context" in options || "policy" in options) {
83
+ if ("roleMap" in options || "roleHierarchy" in options || "context" in options || "policy" in options) {
65
84
  return options;
66
85
  }
67
86
  const values = Object.values(options);
@@ -86,7 +105,7 @@ function getUserEffectivePermissions(subject, roleMapOrOptions) {
86
105
  if (subject.role) {
87
106
  rolesToEvaluate.add(subject.role);
88
107
  }
89
- for (const role of rolesToEvaluate) {
108
+ for (const role of resolveInheritedRoles(rolesToEvaluate, opts.roleHierarchy)) {
90
109
  const rolePerms = roleMap[role];
91
110
  if (rolePerms && Array.isArray(rolePerms)) {
92
111
  for (const perm of rolePerms) {
@@ -101,6 +120,9 @@ function getUserEffectivePermissions(subject, roleMapOrOptions) {
101
120
  }
102
121
  return permissions;
103
122
  }
123
+ function permissionMatches(granted, requested) {
124
+ return granted === requested || granted.endsWith(".*") && requested.startsWith(granted.slice(0, -1));
125
+ }
104
126
  function can(subject, permission, options) {
105
127
  if (!subject || !permission) {
106
128
  return false;
@@ -113,7 +135,7 @@ function can(subject, permission, options) {
113
135
  }
114
136
  }
115
137
  const effective = getUserEffectivePermissions(subject, options !== void 0 ? opts : void 0);
116
- return effective.has(permission);
138
+ return [...effective].some((granted) => permissionMatches(granted, permission));
117
139
  }
118
140
  function canAll(subject, permissions, options) {
119
141
  if (!subject || !permissions.length) {
@@ -147,20 +169,33 @@ function evaluatePermission(subject, permission, options) {
147
169
  }
148
170
  }
149
171
  const effective = getUserEffectivePermissions(subject, options !== void 0 ? opts : void 0);
150
- const allowed = effective.has(permission);
172
+ const matchedPermission = [...effective].find((granted) => permissionMatches(granted, permission));
173
+ const allowed = matchedPermission !== void 0;
151
174
  if (allowed) {
152
175
  return {
153
176
  allowed: true,
154
- reason: "Permission granted."
177
+ reason: "Permission granted.",
178
+ matchedPermission,
179
+ source: subject.permissions?.includes(matchedPermission) ? "direct-permission" : "role-permission",
180
+ evaluatedRoles: resolveInheritedRoles(
181
+ [...subject.roles ?? [], ...subject.role ? [subject.role] : []],
182
+ opts.roleHierarchy
183
+ )
155
184
  };
156
185
  }
157
186
  const roleDesc = subject.role ?? (subject.roles?.length ? subject.roles.join(", ") : "none");
158
187
  return {
159
188
  allowed: false,
160
189
  reason: `User with role '${roleDesc}' lacks required permission '${permission}'.`,
161
- missingPermissions: [permission]
190
+ missingPermissions: [permission],
191
+ source: "none",
192
+ evaluatedRoles: resolveInheritedRoles(
193
+ [...subject.roles ?? [], ...subject.role ? [subject.role] : []],
194
+ opts.roleHierarchy
195
+ )
162
196
  };
163
197
  }
198
+ var explain = evaluatePermission;
164
199
  function normalizeFeatureFlags(flags) {
165
200
  if (!flags) {
166
201
  return {};
@@ -366,13 +401,17 @@ function createSubject(source, mapping) {
366
401
  canAll,
367
402
  canAny,
368
403
  createSubject,
404
+ defineRoleHierarchy,
369
405
  defineRolePermissions,
370
406
  evaluatePermission,
407
+ explain,
371
408
  fromJwtClaims,
372
409
  fromPermissions,
373
410
  fromRoles,
374
411
  getUserEffectivePermissions,
375
412
  isFeatureEnabled,
376
413
  normalizeFeatureFlags,
377
- parseEvaluateOptions
414
+ parseEvaluateOptions,
415
+ permissionMatches,
416
+ resolveInheritedRoles
378
417
  });
package/dist/index.d.cts CHANGED
@@ -45,6 +45,8 @@ type AuthorizationPolicy = (subject: AuthorizationSubject, permission: Permissio
45
45
  */
46
46
  interface EvaluateOptions {
47
47
  roleMap?: RolePermissionsMap;
48
+ /** A role can inherit permissions from one or more parent roles. */
49
+ roleHierarchy?: RoleHierarchy;
48
50
  context?: Record<string, unknown>;
49
51
  policy?: AuthorizationPolicy;
50
52
  }
@@ -65,11 +67,18 @@ interface AuthorizationResult {
65
67
  allowed: boolean;
66
68
  reason?: string;
67
69
  missingPermissions?: Permission[];
70
+ /** Roles considered after resolving inheritance. */
71
+ evaluatedRoles?: Role[];
72
+ /** How the decision was reached. */
73
+ source?: 'policy' | 'direct-permission' | 'role-permission' | 'none';
74
+ matchedPermission?: Permission;
68
75
  }
69
76
  /**
70
77
  * Mapping from roles to their granted permissions.
71
78
  */
72
79
  type RolePermissionsMap = Record<Role, readonly Permission[] | Permission[]>;
80
+ /** Maps a role to the roles whose permissions it inherits. */
81
+ type RoleHierarchy = Record<Role, readonly Role[] | Role[]>;
73
82
 
74
83
  /**
75
84
  * Default role-to-permissions map for Gatekeeper.
@@ -83,6 +92,10 @@ declare const DEFAULT_ROLE_PERMISSIONS: RolePermissionsMap;
83
92
  * Creates a merged role-to-permissions map by extending or overriding the defaults.
84
93
  */
85
94
  declare function defineRolePermissions(customMap?: Partial<RolePermissionsMap>, baseMap?: RolePermissionsMap): RolePermissionsMap;
95
+ /** Creates a defensive copy of role inheritance declarations. */
96
+ declare function defineRoleHierarchy(hierarchy: RoleHierarchy): RoleHierarchy;
97
+ /** Resolves all inherited roles safely, including cyclic declarations. */
98
+ declare function resolveInheritedRoles(roles: Iterable<Role>, hierarchy?: RoleHierarchy): Role[];
86
99
 
87
100
  /**
88
101
  * Normalizes either a legacy RolePermissionsMap or a modern EvaluateOptions object.
@@ -97,6 +110,8 @@ declare function parseEvaluateOptions(options?: RoleMapOrOptions): EvaluateOptio
97
110
  * Deduplicates permissions and denies safely if no roles or permissions exist.
98
111
  */
99
112
  declare function getUserEffectivePermissions(subject: AuthorizationSubject | null | undefined, roleMapOrOptions?: RoleMapOrOptions): Set<Permission>;
113
+ /** Supports exact permissions and namespace wildcards such as `billing.*`. */
114
+ declare function permissionMatches(granted: Permission, requested: Permission): boolean;
100
115
  /**
101
116
  * Evaluates whether a subject has a specific permission.
102
117
  * Supports scoped policy checks and legacy roleMap options.
@@ -119,6 +134,8 @@ declare function canAny(subject: AuthorizationSubject | null | undefined, permis
119
134
  * Evaluates access with detailed diagnostic reasons.
120
135
  */
121
136
  declare function evaluatePermission(subject: AuthorizationSubject | null | undefined, permission: Permission, options?: RoleMapOrOptions): AuthorizationResult;
137
+ /** Alias for evaluatePermission, optimized for audit/debug UI naming. */
138
+ declare const explain: typeof evaluatePermission;
122
139
  /**
123
140
  * Normalizes feature flag inputs (either an array of FeatureFlag or a record of boolean flags)
124
141
  * into a standard Record<string, boolean>.
@@ -239,4 +256,4 @@ interface SubjectMappingConfig<TSource> {
239
256
  */
240
257
  declare function createSubject<TSource>(source: TSource, mapping: SubjectMappingConfig<TSource> | ((source: TSource) => AuthorizationSubject)): AuthorizationSubject;
241
258
 
242
- export { type AuthorizationPolicy, type AuthorizationResult, type AuthorizationSubject, DEFAULT_ROLE_PERMISSIONS, type EvaluateOptions, type FeatureFlag, type FromPermissionsInput, type FromRolesInput, type GatekeeperUser, type JwtClaimsOptions, type Permission, type Role, type RoleMapOrOptions, type RolePermissionsMap, type StandardPermission, type StandardRole, type SubjectMappingConfig, can, canAll, canAny, createSubject, defineRolePermissions, evaluatePermission, fromJwtClaims, fromPermissions, fromRoles, getUserEffectivePermissions, isFeatureEnabled, normalizeFeatureFlags, parseEvaluateOptions };
259
+ export { type AuthorizationPolicy, type AuthorizationResult, type AuthorizationSubject, DEFAULT_ROLE_PERMISSIONS, type EvaluateOptions, type FeatureFlag, type FromPermissionsInput, type FromRolesInput, type GatekeeperUser, type JwtClaimsOptions, type Permission, type Role, type RoleHierarchy, type RoleMapOrOptions, type RolePermissionsMap, type StandardPermission, type StandardRole, type SubjectMappingConfig, can, canAll, canAny, createSubject, defineRoleHierarchy, defineRolePermissions, evaluatePermission, explain, fromJwtClaims, fromPermissions, fromRoles, getUserEffectivePermissions, isFeatureEnabled, normalizeFeatureFlags, parseEvaluateOptions, permissionMatches, resolveInheritedRoles };
package/dist/index.d.ts CHANGED
@@ -45,6 +45,8 @@ type AuthorizationPolicy = (subject: AuthorizationSubject, permission: Permissio
45
45
  */
46
46
  interface EvaluateOptions {
47
47
  roleMap?: RolePermissionsMap;
48
+ /** A role can inherit permissions from one or more parent roles. */
49
+ roleHierarchy?: RoleHierarchy;
48
50
  context?: Record<string, unknown>;
49
51
  policy?: AuthorizationPolicy;
50
52
  }
@@ -65,11 +67,18 @@ interface AuthorizationResult {
65
67
  allowed: boolean;
66
68
  reason?: string;
67
69
  missingPermissions?: Permission[];
70
+ /** Roles considered after resolving inheritance. */
71
+ evaluatedRoles?: Role[];
72
+ /** How the decision was reached. */
73
+ source?: 'policy' | 'direct-permission' | 'role-permission' | 'none';
74
+ matchedPermission?: Permission;
68
75
  }
69
76
  /**
70
77
  * Mapping from roles to their granted permissions.
71
78
  */
72
79
  type RolePermissionsMap = Record<Role, readonly Permission[] | Permission[]>;
80
+ /** Maps a role to the roles whose permissions it inherits. */
81
+ type RoleHierarchy = Record<Role, readonly Role[] | Role[]>;
73
82
 
74
83
  /**
75
84
  * Default role-to-permissions map for Gatekeeper.
@@ -83,6 +92,10 @@ declare const DEFAULT_ROLE_PERMISSIONS: RolePermissionsMap;
83
92
  * Creates a merged role-to-permissions map by extending or overriding the defaults.
84
93
  */
85
94
  declare function defineRolePermissions(customMap?: Partial<RolePermissionsMap>, baseMap?: RolePermissionsMap): RolePermissionsMap;
95
+ /** Creates a defensive copy of role inheritance declarations. */
96
+ declare function defineRoleHierarchy(hierarchy: RoleHierarchy): RoleHierarchy;
97
+ /** Resolves all inherited roles safely, including cyclic declarations. */
98
+ declare function resolveInheritedRoles(roles: Iterable<Role>, hierarchy?: RoleHierarchy): Role[];
86
99
 
87
100
  /**
88
101
  * Normalizes either a legacy RolePermissionsMap or a modern EvaluateOptions object.
@@ -97,6 +110,8 @@ declare function parseEvaluateOptions(options?: RoleMapOrOptions): EvaluateOptio
97
110
  * Deduplicates permissions and denies safely if no roles or permissions exist.
98
111
  */
99
112
  declare function getUserEffectivePermissions(subject: AuthorizationSubject | null | undefined, roleMapOrOptions?: RoleMapOrOptions): Set<Permission>;
113
+ /** Supports exact permissions and namespace wildcards such as `billing.*`. */
114
+ declare function permissionMatches(granted: Permission, requested: Permission): boolean;
100
115
  /**
101
116
  * Evaluates whether a subject has a specific permission.
102
117
  * Supports scoped policy checks and legacy roleMap options.
@@ -119,6 +134,8 @@ declare function canAny(subject: AuthorizationSubject | null | undefined, permis
119
134
  * Evaluates access with detailed diagnostic reasons.
120
135
  */
121
136
  declare function evaluatePermission(subject: AuthorizationSubject | null | undefined, permission: Permission, options?: RoleMapOrOptions): AuthorizationResult;
137
+ /** Alias for evaluatePermission, optimized for audit/debug UI naming. */
138
+ declare const explain: typeof evaluatePermission;
122
139
  /**
123
140
  * Normalizes feature flag inputs (either an array of FeatureFlag or a record of boolean flags)
124
141
  * into a standard Record<string, boolean>.
@@ -239,4 +256,4 @@ interface SubjectMappingConfig<TSource> {
239
256
  */
240
257
  declare function createSubject<TSource>(source: TSource, mapping: SubjectMappingConfig<TSource> | ((source: TSource) => AuthorizationSubject)): AuthorizationSubject;
241
258
 
242
- export { type AuthorizationPolicy, type AuthorizationResult, type AuthorizationSubject, DEFAULT_ROLE_PERMISSIONS, type EvaluateOptions, type FeatureFlag, type FromPermissionsInput, type FromRolesInput, type GatekeeperUser, type JwtClaimsOptions, type Permission, type Role, type RoleMapOrOptions, type RolePermissionsMap, type StandardPermission, type StandardRole, type SubjectMappingConfig, can, canAll, canAny, createSubject, defineRolePermissions, evaluatePermission, fromJwtClaims, fromPermissions, fromRoles, getUserEffectivePermissions, isFeatureEnabled, normalizeFeatureFlags, parseEvaluateOptions };
259
+ export { type AuthorizationPolicy, type AuthorizationResult, type AuthorizationSubject, DEFAULT_ROLE_PERMISSIONS, type EvaluateOptions, type FeatureFlag, type FromPermissionsInput, type FromRolesInput, type GatekeeperUser, type JwtClaimsOptions, type Permission, type Role, type RoleHierarchy, type RoleMapOrOptions, type RolePermissionsMap, type StandardPermission, type StandardRole, type SubjectMappingConfig, can, canAll, canAny, createSubject, defineRoleHierarchy, defineRolePermissions, evaluatePermission, explain, fromJwtClaims, fromPermissions, fromRoles, getUserEffectivePermissions, isFeatureEnabled, normalizeFeatureFlags, parseEvaluateOptions, permissionMatches, resolveInheritedRoles };
package/dist/index.js CHANGED
@@ -16,13 +16,28 @@ function defineRolePermissions(customMap, baseMap = DEFAULT_ROLE_PERMISSIONS) {
16
16
  }
17
17
  return merged;
18
18
  }
19
+ function defineRoleHierarchy(hierarchy) {
20
+ return Object.fromEntries(
21
+ Object.entries(hierarchy).map(([role, parents]) => [role, [...parents]])
22
+ );
23
+ }
24
+ function resolveInheritedRoles(roles, hierarchy) {
25
+ const resolved = /* @__PURE__ */ new Set();
26
+ const visit = (role) => {
27
+ if (resolved.has(role)) return;
28
+ resolved.add(role);
29
+ for (const parent of hierarchy?.[role] ?? []) visit(parent);
30
+ };
31
+ for (const role of roles) visit(role);
32
+ return [...resolved];
33
+ }
19
34
 
20
35
  // src/evaluator.ts
21
36
  function parseEvaluateOptions(options) {
22
37
  if (!options || typeof options !== "object") {
23
38
  return {};
24
39
  }
25
- if ("roleMap" in options || "context" in options || "policy" in options) {
40
+ if ("roleMap" in options || "roleHierarchy" in options || "context" in options || "policy" in options) {
26
41
  return options;
27
42
  }
28
43
  const values = Object.values(options);
@@ -47,7 +62,7 @@ function getUserEffectivePermissions(subject, roleMapOrOptions) {
47
62
  if (subject.role) {
48
63
  rolesToEvaluate.add(subject.role);
49
64
  }
50
- for (const role of rolesToEvaluate) {
65
+ for (const role of resolveInheritedRoles(rolesToEvaluate, opts.roleHierarchy)) {
51
66
  const rolePerms = roleMap[role];
52
67
  if (rolePerms && Array.isArray(rolePerms)) {
53
68
  for (const perm of rolePerms) {
@@ -62,6 +77,9 @@ function getUserEffectivePermissions(subject, roleMapOrOptions) {
62
77
  }
63
78
  return permissions;
64
79
  }
80
+ function permissionMatches(granted, requested) {
81
+ return granted === requested || granted.endsWith(".*") && requested.startsWith(granted.slice(0, -1));
82
+ }
65
83
  function can(subject, permission, options) {
66
84
  if (!subject || !permission) {
67
85
  return false;
@@ -74,7 +92,7 @@ function can(subject, permission, options) {
74
92
  }
75
93
  }
76
94
  const effective = getUserEffectivePermissions(subject, options !== void 0 ? opts : void 0);
77
- return effective.has(permission);
95
+ return [...effective].some((granted) => permissionMatches(granted, permission));
78
96
  }
79
97
  function canAll(subject, permissions, options) {
80
98
  if (!subject || !permissions.length) {
@@ -108,20 +126,33 @@ function evaluatePermission(subject, permission, options) {
108
126
  }
109
127
  }
110
128
  const effective = getUserEffectivePermissions(subject, options !== void 0 ? opts : void 0);
111
- const allowed = effective.has(permission);
129
+ const matchedPermission = [...effective].find((granted) => permissionMatches(granted, permission));
130
+ const allowed = matchedPermission !== void 0;
112
131
  if (allowed) {
113
132
  return {
114
133
  allowed: true,
115
- reason: "Permission granted."
134
+ reason: "Permission granted.",
135
+ matchedPermission,
136
+ source: subject.permissions?.includes(matchedPermission) ? "direct-permission" : "role-permission",
137
+ evaluatedRoles: resolveInheritedRoles(
138
+ [...subject.roles ?? [], ...subject.role ? [subject.role] : []],
139
+ opts.roleHierarchy
140
+ )
116
141
  };
117
142
  }
118
143
  const roleDesc = subject.role ?? (subject.roles?.length ? subject.roles.join(", ") : "none");
119
144
  return {
120
145
  allowed: false,
121
146
  reason: `User with role '${roleDesc}' lacks required permission '${permission}'.`,
122
- missingPermissions: [permission]
147
+ missingPermissions: [permission],
148
+ source: "none",
149
+ evaluatedRoles: resolveInheritedRoles(
150
+ [...subject.roles ?? [], ...subject.role ? [subject.role] : []],
151
+ opts.roleHierarchy
152
+ )
123
153
  };
124
154
  }
155
+ var explain = evaluatePermission;
125
156
  function normalizeFeatureFlags(flags) {
126
157
  if (!flags) {
127
158
  return {};
@@ -326,13 +357,17 @@ export {
326
357
  canAll,
327
358
  canAny,
328
359
  createSubject,
360
+ defineRoleHierarchy,
329
361
  defineRolePermissions,
330
362
  evaluatePermission,
363
+ explain,
331
364
  fromJwtClaims,
332
365
  fromPermissions,
333
366
  fromRoles,
334
367
  getUserEffectivePermissions,
335
368
  isFeatureEnabled,
336
369
  normalizeFeatureFlags,
337
- parseEvaluateOptions
370
+ parseEvaluateOptions,
371
+ permissionMatches,
372
+ resolveInheritedRoles
338
373
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ahmed109/gatekeeper-core",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Pure framework-independent authorization and feature-flag evaluator for TypeScript",
5
5
  "keywords": [
6
6
  "authorization",