@zudojs/permissions 0.0.1 → 1.0.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 (156) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +345 -14
  3. package/dist/ability/ability.core.d.ts +2 -1
  4. package/dist/ability/ability.core.js +34 -9
  5. package/dist/cache/cache.core.d.ts +24 -5
  6. package/dist/cache/cache.core.js +62 -20
  7. package/dist/cache/index.d.ts +1 -1
  8. package/dist/conditions/conditions.core.d.ts +18 -2
  9. package/dist/conditions/conditions.core.js +46 -6
  10. package/dist/conditions/index.d.ts +1 -1
  11. package/dist/conditions/index.js +1 -1
  12. package/dist/evaluator/authorizationEngine.d.ts +48 -6
  13. package/dist/evaluator/authorizationEngine.js +146 -17
  14. package/dist/evaluator/evaluator.core.d.ts +28 -4
  15. package/dist/evaluator/evaluator.core.js +246 -39
  16. package/dist/evaluator/evaluator.explain.d.ts +6 -3
  17. package/dist/evaluator/evaluator.explain.js +8 -71
  18. package/dist/evaluator/evaluator.pipeline.d.ts +70 -9
  19. package/dist/evaluator/evaluator.pipeline.js +194 -49
  20. package/dist/evaluator/index.d.ts +4 -2
  21. package/dist/evaluator/index.js +4 -1
  22. package/dist/http/httpHelpers.d.ts +28 -12
  23. package/dist/http/httpHelpers.js +33 -6
  24. package/dist/http/httpMiddleware.core.d.ts +54 -13
  25. package/dist/http/httpMiddleware.core.js +72 -28
  26. package/dist/http/index.d.ts +7 -3
  27. package/dist/http/index.js +7 -3
  28. package/dist/index.d.ts +3 -2
  29. package/dist/index.js +2 -2
  30. package/dist/observability/index.d.ts +1 -1
  31. package/dist/observability/observability.core.d.ts +32 -12
  32. package/dist/observability/observability.core.js +41 -20
  33. package/dist/permission/index.d.ts +2 -2
  34. package/dist/permission/index.js +1 -1
  35. package/dist/permission/permission.core.d.ts +13 -2
  36. package/dist/permission/permission.core.js +29 -21
  37. package/dist/permission/permissionRegistry.d.ts +27 -33
  38. package/dist/permission/permissionRegistry.js +59 -41
  39. package/dist/permissionErrors/index.d.ts +1 -1
  40. package/dist/permissionErrors/index.js +1 -1
  41. package/dist/permissionErrors/permissionError.access.d.ts +24 -0
  42. package/dist/permissionErrors/permissionError.access.js +25 -5
  43. package/dist/permissionErrors/permissionError.types.d.ts +1 -1
  44. package/dist/permissionErrors/permissionError.types.js +1 -1
  45. package/dist/permissionTypes/index.d.ts +2 -2
  46. package/dist/permissionTypes/index.js +1 -1
  47. package/dist/permissionTypes/policyTypes.d.ts +35 -2
  48. package/dist/permissionTypes/ruleTypes.d.ts +38 -5
  49. package/dist/policy/index.d.ts +1 -1
  50. package/dist/policy/index.js +1 -1
  51. package/dist/policy/policyRegistry.d.ts +16 -23
  52. package/dist/policy/policyRegistry.js +26 -23
  53. package/dist/role/index.d.ts +2 -2
  54. package/dist/role/index.js +1 -1
  55. package/dist/role/roleHierarchy.d.ts +34 -4
  56. package/dist/role/roleHierarchy.js +48 -23
  57. package/dist/role/roleRegistry.d.ts +16 -25
  58. package/dist/role/roleRegistry.js +27 -23
  59. package/dist/rule/index.d.ts +2 -2
  60. package/dist/rule/index.js +2 -2
  61. package/dist/rule/rule.core.d.ts +39 -10
  62. package/dist/rule/rule.core.js +95 -38
  63. package/dist/rule/ruleCompiler.d.ts +14 -5
  64. package/dist/rule/ruleCompiler.js +46 -30
  65. package/dist/utils/utils.helper.d.ts +8 -0
  66. package/dist/utils/utils.helper.js +16 -14
  67. package/package.json +23 -16
  68. package/dist/.tsbuildinfo +0 -1
  69. package/dist/ability/ability.core.d.ts.map +0 -1
  70. package/dist/ability/ability.core.js.map +0 -1
  71. package/dist/ability/index.d.ts.map +0 -1
  72. package/dist/ability/index.js.map +0 -1
  73. package/dist/actor/actor.core.d.ts.map +0 -1
  74. package/dist/actor/actor.core.js.map +0 -1
  75. package/dist/actor/index.d.ts.map +0 -1
  76. package/dist/actor/index.js.map +0 -1
  77. package/dist/cache/cache.core.d.ts.map +0 -1
  78. package/dist/cache/cache.core.js.map +0 -1
  79. package/dist/cache/index.d.ts.map +0 -1
  80. package/dist/cache/index.js.map +0 -1
  81. package/dist/conditions/conditions.core.d.ts.map +0 -1
  82. package/dist/conditions/conditions.core.js.map +0 -1
  83. package/dist/conditions/index.d.ts.map +0 -1
  84. package/dist/conditions/index.js.map +0 -1
  85. package/dist/evaluator/authorizationEngine.d.ts.map +0 -1
  86. package/dist/evaluator/authorizationEngine.js.map +0 -1
  87. package/dist/evaluator/evaluator.core.d.ts.map +0 -1
  88. package/dist/evaluator/evaluator.core.js.map +0 -1
  89. package/dist/evaluator/evaluator.explain.d.ts.map +0 -1
  90. package/dist/evaluator/evaluator.explain.js.map +0 -1
  91. package/dist/evaluator/evaluator.pipeline.d.ts.map +0 -1
  92. package/dist/evaluator/evaluator.pipeline.js.map +0 -1
  93. package/dist/evaluator/index.d.ts.map +0 -1
  94. package/dist/evaluator/index.js.map +0 -1
  95. package/dist/http/httpHelpers.d.ts.map +0 -1
  96. package/dist/http/httpHelpers.js.map +0 -1
  97. package/dist/http/httpMiddleware.core.d.ts.map +0 -1
  98. package/dist/http/httpMiddleware.core.js.map +0 -1
  99. package/dist/http/httpTypes.d.ts.map +0 -1
  100. package/dist/http/httpTypes.js.map +0 -1
  101. package/dist/http/index.d.ts.map +0 -1
  102. package/dist/http/index.js.map +0 -1
  103. package/dist/index.d.ts.map +0 -1
  104. package/dist/index.js.map +0 -1
  105. package/dist/observability/index.d.ts.map +0 -1
  106. package/dist/observability/index.js.map +0 -1
  107. package/dist/observability/observability.core.d.ts.map +0 -1
  108. package/dist/observability/observability.core.js.map +0 -1
  109. package/dist/permission/index.d.ts.map +0 -1
  110. package/dist/permission/index.js.map +0 -1
  111. package/dist/permission/permission.core.d.ts.map +0 -1
  112. package/dist/permission/permission.core.js.map +0 -1
  113. package/dist/permission/permissionRegistry.d.ts.map +0 -1
  114. package/dist/permission/permissionRegistry.js.map +0 -1
  115. package/dist/permissionErrors/index.d.ts.map +0 -1
  116. package/dist/permissionErrors/index.js.map +0 -1
  117. package/dist/permissionErrors/permissionError.access.d.ts.map +0 -1
  118. package/dist/permissionErrors/permissionError.access.js.map +0 -1
  119. package/dist/permissionErrors/permissionError.base.d.ts.map +0 -1
  120. package/dist/permissionErrors/permissionError.base.js.map +0 -1
  121. package/dist/permissionErrors/permissionError.types.d.ts.map +0 -1
  122. package/dist/permissionErrors/permissionError.types.js.map +0 -1
  123. package/dist/permissionErrors/permissionError.validation.d.ts.map +0 -1
  124. package/dist/permissionErrors/permissionError.validation.js.map +0 -1
  125. package/dist/permissionTypes/index.d.ts.map +0 -1
  126. package/dist/permissionTypes/index.js.map +0 -1
  127. package/dist/permissionTypes/permissionActor.d.ts.map +0 -1
  128. package/dist/permissionTypes/permissionActor.js.map +0 -1
  129. package/dist/permissionTypes/policyTypes.d.ts.map +0 -1
  130. package/dist/permissionTypes/policyTypes.js.map +0 -1
  131. package/dist/permissionTypes/ruleTypes.d.ts.map +0 -1
  132. package/dist/permissionTypes/ruleTypes.js.map +0 -1
  133. package/dist/policy/index.d.ts.map +0 -1
  134. package/dist/policy/index.js.map +0 -1
  135. package/dist/policy/policyRegistry.d.ts.map +0 -1
  136. package/dist/policy/policyRegistry.js.map +0 -1
  137. package/dist/resolvers/index.d.ts.map +0 -1
  138. package/dist/resolvers/index.js.map +0 -1
  139. package/dist/resolvers/resolvers.core.d.ts.map +0 -1
  140. package/dist/resolvers/resolvers.core.js.map +0 -1
  141. package/dist/role/index.d.ts.map +0 -1
  142. package/dist/role/index.js.map +0 -1
  143. package/dist/role/roleHierarchy.d.ts.map +0 -1
  144. package/dist/role/roleHierarchy.js.map +0 -1
  145. package/dist/role/roleRegistry.d.ts.map +0 -1
  146. package/dist/role/roleRegistry.js.map +0 -1
  147. package/dist/rule/index.d.ts.map +0 -1
  148. package/dist/rule/index.js.map +0 -1
  149. package/dist/rule/rule.core.d.ts.map +0 -1
  150. package/dist/rule/rule.core.js.map +0 -1
  151. package/dist/rule/ruleCompiler.d.ts.map +0 -1
  152. package/dist/rule/ruleCompiler.js.map +0 -1
  153. package/dist/utils/index.d.ts.map +0 -1
  154. package/dist/utils/index.js.map +0 -1
  155. package/dist/utils/utils.helper.d.ts.map +0 -1
  156. package/dist/utils/utils.helper.js.map +0 -1
@@ -4,12 +4,27 @@
4
4
  import { PermissionError } from "./permissionError.base.js";
5
5
  /**
6
6
  * Access denied — the actor is not authorized.
7
+ *
8
+ * `AuthorizationError` exposes its body to the client, so the identifying
9
+ * details go in `internal` rather than `metadata`: `actorId` is an internal
10
+ * identifier and a policy name describes the shape of the authorization
11
+ * model, neither of which belongs in a 403 body.
7
12
  */
8
13
  export declare class PermissionDeniedError extends PermissionError {
14
+ /** Detail for logs and audit trails. Never serialized to a client. */
15
+ readonly details: {
16
+ readonly actorId?: string;
17
+ readonly permission?: string;
18
+ readonly resourceType?: string;
19
+ readonly reason?: string;
20
+ readonly policy?: string;
21
+ };
9
22
  constructor(message?: string, options?: {
10
23
  readonly actorId?: string;
11
24
  readonly permission?: string;
12
25
  readonly resourceType?: string;
26
+ readonly reason?: string;
27
+ readonly policy?: string;
13
28
  });
14
29
  }
15
30
  /**
@@ -36,4 +51,13 @@ export declare class RoleNotFoundError extends PermissionError {
36
51
  export declare class DuplicateRoleError extends PermissionError {
37
52
  constructor(role: string);
38
53
  }
54
+ /**
55
+ * A duplicate policy was registered.
56
+ *
57
+ * Silently replacing a policy is how an authorization rule disappears without
58
+ * anyone noticing, so the registry rejects it unless override is asked for.
59
+ */
60
+ export declare class DuplicatePolicyError extends PermissionError {
61
+ constructor(policy: string);
62
+ }
39
63
  //# sourceMappingURL=permissionError.access.d.ts.map
@@ -5,17 +5,23 @@ import { ErrorCode } from "@zudojs/errors";
5
5
  import { PermissionError } from "./permissionError.base.js";
6
6
  /**
7
7
  * Access denied — the actor is not authorized.
8
+ *
9
+ * `AuthorizationError` exposes its body to the client, so the identifying
10
+ * details go in `internal` rather than `metadata`: `actorId` is an internal
11
+ * identifier and a policy name describes the shape of the authorization
12
+ * model, neither of which belongs in a 403 body.
8
13
  */
9
14
  export class PermissionDeniedError extends PermissionError {
15
+ /** Detail for logs and audit trails. Never serialized to a client. */
16
+ details;
10
17
  constructor(message = "Access denied", options) {
11
18
  super(message, {
12
19
  code: ErrorCode.ACCESS_DENIED,
13
- metadata: {
14
- actorId: options?.actorId,
15
- permission: options?.permission,
16
- resourceType: options?.resourceType,
17
- },
20
+ // Only the permission name, which the caller already knows because
21
+ // they attempted it.
22
+ metadata: { permission: options?.permission },
18
23
  });
24
+ this.details = Object.freeze({ ...options });
19
25
  }
20
26
  }
21
27
  /**
@@ -62,4 +68,18 @@ export class DuplicateRoleError extends PermissionError {
62
68
  });
63
69
  }
64
70
  }
71
+ /**
72
+ * A duplicate policy was registered.
73
+ *
74
+ * Silently replacing a policy is how an authorization rule disappears without
75
+ * anyone noticing, so the registry rejects it unless override is asked for.
76
+ */
77
+ export class DuplicatePolicyError extends PermissionError {
78
+ constructor(policy) {
79
+ super(`Duplicate policy: ${policy}`, {
80
+ code: ErrorCode.CONFLICT,
81
+ metadata: { policy },
82
+ });
83
+ }
84
+ }
65
85
  //# sourceMappingURL=permissionError.access.js.map
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Permission error types — re-exports from focused files.
3
3
  */
4
- export { PermissionDeniedError, PermissionNotFoundError, DuplicatePermissionError, RoleNotFoundError, DuplicateRoleError, } from "./permissionError.access.js";
4
+ export { PermissionDeniedError, PermissionNotFoundError, DuplicatePermissionError, RoleNotFoundError, DuplicateRoleError, DuplicatePolicyError, } from "./permissionError.access.js";
5
5
  export { InvalidPermissionError, InvalidRoleError, CircularRoleInheritanceError, PolicyError, PolicyTimeoutError, PermissionResolverError, AuthorizationAbortedError, } from "./permissionError.validation.js";
6
6
  //# sourceMappingURL=permissionError.types.d.ts.map
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Permission error types — re-exports from focused files.
3
3
  */
4
- export { PermissionDeniedError, PermissionNotFoundError, DuplicatePermissionError, RoleNotFoundError, DuplicateRoleError, } from "./permissionError.access.js";
4
+ export { PermissionDeniedError, PermissionNotFoundError, DuplicatePermissionError, RoleNotFoundError, DuplicateRoleError, DuplicatePolicyError, } from "./permissionError.access.js";
5
5
  export { InvalidPermissionError, InvalidRoleError, CircularRoleInheritanceError, PolicyError, PolicyTimeoutError, PermissionResolverError, AuthorizationAbortedError, } from "./permissionError.validation.js";
6
6
  //# sourceMappingURL=permissionError.types.js.map
@@ -1,9 +1,9 @@
1
1
  /**
2
- * Core transaction type definitions.
2
+ * Core permission type definitions.
3
3
  *
4
4
  * @module permissionTypes
5
5
  */
6
6
  export { type PermissionActor, type Permission, type PermissionString, } from "./permissionActor.js";
7
- export { type RuleEffect, type PermissionRule, type PermissionConditionFn, type PermissionContext, type PermissionDecision, } from "./ruleTypes.js";
7
+ export { type RuleEffect, type RuleCombiningAlgorithm, type RuleEvaluation, type PermissionRule, type PermissionConditionFn, type PermissionContext, type PermissionDecision, } from "./ruleTypes.js";
8
8
  export { type RoleDefinition, type PermissionResolver, type RoleResolver, type PermissionCache, type PermissionPolicyDefinition, type ExplainStep, type ExplainResult, type AuthorizationOptions, } from "./policyTypes.js";
9
9
  //# sourceMappingURL=index.d.ts.map
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Core transaction type definitions.
2
+ * Core permission type definitions.
3
3
  *
4
4
  * @module permissionTypes
5
5
  */
@@ -9,10 +9,15 @@ import type { PermissionRule, PermissionContext, PermissionDecision } from "./ru
9
9
  export interface RoleDefinition {
10
10
  /** Role name (e.g. "admin", "editor"). */
11
11
  readonly name: string;
12
- /** Permissions granted by this role. */
12
+ /**
13
+ * Permissions granted by this role. May be empty for a role that exists
14
+ * only to combine others through {@link RoleDefinition.inherits}.
15
+ */
13
16
  readonly permissions: readonly string[];
14
17
  /** Roles this role inherits from. */
15
18
  readonly inherits?: readonly string[];
19
+ /** Rules granted by this role, for ABAC beyond a permission string. */
20
+ readonly rules?: readonly PermissionRule[];
16
21
  /** Role description. */
17
22
  readonly description?: string;
18
23
  /** Whether this is a system role (cannot be deleted). */
@@ -34,18 +39,30 @@ export interface PermissionCache {
34
39
  }): Promise<void>;
35
40
  delete(key: string): Promise<void>;
36
41
  invalidateActor(actorId: string): Promise<void>;
42
+ /** Drops every entry. */
43
+ clear?(): Promise<void>;
37
44
  }
38
45
  /** A named authorization policy. */
39
46
  export interface PermissionPolicyDefinition {
40
47
  readonly name: string;
48
+ /**
49
+ * Permissions this policy applies to. Wildcards are honoured, so
50
+ * `["post:*"]` covers `post:update` — the same matching grants use.
51
+ */
41
52
  readonly permissions: readonly string[];
53
+ /**
54
+ * Whether a decision from this policy may be cached. A policy that reads
55
+ * mutable state outside the actor and resource should set this to `false`.
56
+ * Default: `true`.
57
+ */
42
58
  readonly cacheable?: boolean;
59
+ /** Higher priority policies are evaluated first. Default: 0. */
43
60
  readonly priority?: number;
44
61
  evaluate(context: PermissionContext): PermissionDecision | Promise<PermissionDecision>;
45
62
  }
46
63
  /** A step in an explain trace. */
47
64
  export interface ExplainStep {
48
- readonly type: "role" | "permission" | "rule" | "policy" | "deny";
65
+ readonly type: "role" | "permission" | "rule" | "policy" | "condition" | "cache" | "deny";
49
66
  readonly detail: string;
50
67
  readonly matched: boolean;
51
68
  }
@@ -53,10 +70,26 @@ export interface ExplainStep {
53
70
  export interface ExplainResult {
54
71
  readonly allowed: boolean;
55
72
  readonly steps: readonly ExplainStep[];
73
+ /** The decision the steps explain. */
74
+ readonly decision: PermissionDecision;
56
75
  }
57
76
  /** Options for authorization checks. */
58
77
  export interface AuthorizationOptions {
78
+ /**
79
+ * Cancels the check. Evaluation aborts with an
80
+ * `AuthorizationAbortedError`; it does not silently allow.
81
+ */
59
82
  readonly signal?: AbortSignal;
83
+ /** Overrides the engine's policy timeout, in milliseconds. */
60
84
  readonly policyTimeout?: number;
85
+ /**
86
+ * Request-scoped metadata made available to conditions and policies as
87
+ * `PermissionContext.metadata` — tenant, IP, time of day.
88
+ */
89
+ readonly metadata?: ReadonlyMap<string, unknown> | Record<string, unknown>;
90
+ /** Skips the decision cache for this check. */
91
+ readonly skipCache?: boolean;
92
+ /** Identifies the resource for caching. Falls back to `resource.id`. */
93
+ readonly resourceId?: string;
61
94
  }
62
95
  //# sourceMappingURL=policyTypes.d.ts.map
@@ -6,6 +6,15 @@
6
6
  import type { PermissionActor, Permission } from "./permissionActor.js";
7
7
  /** Whether a rule allows or denies access. */
8
8
  export type RuleEffect = "allow" | "deny";
9
+ /**
10
+ * How competing rules are combined into one decision.
11
+ *
12
+ * - `"deny-overrides"` (the default) — any applicable deny wins, whatever its
13
+ * priority. This is the safe reading, and the one the documentation has
14
+ * always described.
15
+ * - `"priority"` — the highest-priority rule wins; deny breaks a tie.
16
+ */
17
+ export type RuleCombiningAlgorithm = "deny-overrides" | "priority";
9
18
  /** A permission rule that grants or denies access under conditions. */
10
19
  export interface PermissionRule {
11
20
  /** Allow or deny. */
@@ -14,9 +23,12 @@ export interface PermissionRule {
14
23
  readonly action: string | readonly string[];
15
24
  /** Resource(s) this rule applies to. */
16
25
  readonly resource: string | readonly string[];
17
- /** Optional condition function for ABAC. */
26
+ /**
27
+ * Optional condition for ABAC. A rule whose condition returns `false` does
28
+ * not apply — neither to allow nor to deny.
29
+ */
18
30
  readonly condition?: PermissionConditionFn;
19
- /** Rule priority — higher wins. Same priority: deny wins. */
31
+ /** Rule priority — higher wins under the `"priority"` algorithm. */
20
32
  readonly priority?: number;
21
33
  /** Human-readable rule name for debugging. */
22
34
  readonly name?: string;
@@ -27,24 +39,45 @@ export type PermissionConditionFn = (context: PermissionContext) => boolean | Pr
27
39
  export interface PermissionContext {
28
40
  /** The actor requesting access. */
29
41
  readonly actor: PermissionActor;
30
- /** The permission string being checked (e.g. "post:update"). */
42
+ /** The permission being checked, parsed into resource and action. */
31
43
  readonly permission: Permission;
32
44
  /** The target resource (optional). */
33
45
  readonly resource?: unknown;
34
- /** Arbitrary metadata (IP, tenant, timestamp, etc.). */
46
+ /**
47
+ * Arbitrary metadata (IP, tenant, timestamp, etc.), supplied through
48
+ * `AuthorizationOptions.metadata`. Conditions such as `tenantIsolation`
49
+ * read the actor's tenant from here.
50
+ */
35
51
  readonly metadata?: ReadonlyMap<string, unknown>;
52
+ /** Cancels long-running conditions and policies. */
53
+ readonly signal?: AbortSignal;
36
54
  }
37
55
  /** Result of an authorization check. */
38
56
  export interface PermissionDecision {
39
57
  /** Whether the action is allowed. */
40
58
  readonly allowed: boolean;
41
- /** Human-readable reason for the decision. */
59
+ /**
60
+ * Why the decision came out this way, as a stable machine-readable code.
61
+ *
62
+ * Internal by design: it names policies and rules, so it belongs in a log,
63
+ * not in a response body. {@link PermissionDecision.publicReason} is the
64
+ * one safe to return to a caller.
65
+ */
42
66
  readonly reason?: string;
43
67
  /** Name of the policy/rule that produced the decision. */
44
68
  readonly policy?: string;
45
69
  /** The matched permission string. */
46
70
  readonly matchedPermission?: string;
71
+ /** A caller-safe message, free of policy and rule names. */
72
+ readonly publicReason?: string;
47
73
  /** Additional decision metadata. */
48
74
  readonly metadata?: unknown;
49
75
  }
76
+ /** The outcome of evaluating a rule set. */
77
+ export interface RuleEvaluation {
78
+ readonly allowed: boolean;
79
+ readonly matchedRule?: PermissionRule;
80
+ /** Rules that matched the target and whose condition passed. */
81
+ readonly applicable: readonly PermissionRule[];
82
+ }
50
83
  //# sourceMappingURL=ruleTypes.d.ts.map
@@ -3,5 +3,5 @@
3
3
  *
4
4
  * @module policy
5
5
  */
6
- export { createPolicyRegistry } from "./policyRegistry.js";
6
+ export { createPolicyRegistry, type PolicyRegistry, type PolicyRegistryOptions, } from "./policyRegistry.js";
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -3,5 +3,5 @@
3
3
  *
4
4
  * @module policy
5
5
  */
6
- export { createPolicyRegistry } from "./policyRegistry.js";
6
+ export { createPolicyRegistry, } from "./policyRegistry.js";
7
7
  //# sourceMappingURL=index.js.map
@@ -4,33 +4,26 @@
4
4
  * @module policy/policyRegistry
5
5
  */
6
6
  import type { PermissionPolicyDefinition } from "../permissionTypes/index.js";
7
- /**
8
- * Create a policy registry.
9
- */
10
- export declare function createPolicyRegistry(): {
11
- /**
12
- * Register a policy.
13
- */
7
+ /** Options for the policy registry. */
8
+ export interface PolicyRegistryOptions {
9
+ /** Allow overwriting an existing policy. Defaults to false. */
10
+ readonly allowOverride?: boolean;
11
+ }
12
+ /** A policy registry, usable directly as the engine's `policies` source. */
13
+ export interface PolicyRegistry {
14
14
  define(definition: PermissionPolicyDefinition): void;
15
- /**
16
- * Get a policy by name.
17
- */
18
15
  get(name: string): PermissionPolicyDefinition | undefined;
19
- /**
20
- * Get all policies that apply to a given permission string.
21
- */
16
+ has(name: string): boolean;
22
17
  forPermission(permission: string): readonly PermissionPolicyDefinition[];
23
- /**
24
- * Get all registered policy names.
25
- */
26
18
  names(): readonly string[];
27
- /**
28
- * Remove a policy.
29
- */
19
+ all(): readonly PermissionPolicyDefinition[];
30
20
  remove(name: string): boolean;
31
- /**
32
- * Clear all policies.
33
- */
34
21
  clear(): void;
35
- };
22
+ }
23
+ /**
24
+ * Create a policy registry.
25
+ *
26
+ * Pass it straight to `createPermissionEngine({ policies: registry })`.
27
+ */
28
+ export declare function createPolicyRegistry(options?: PolicyRegistryOptions): PolicyRegistry;
36
29
  //# sourceMappingURL=policyRegistry.d.ts.map
@@ -3,52 +3,55 @@
3
3
  *
4
4
  * @module policy/policyRegistry
5
5
  */
6
+ import { DuplicatePolicyError } from "../permissionErrors/index.js";
7
+ import { selectPolicies } from "../evaluator/evaluator.pipeline.js";
6
8
  /**
7
9
  * Create a policy registry.
10
+ *
11
+ * Pass it straight to `createPermissionEngine({ policies: registry })`.
8
12
  */
9
- export function createPolicyRegistry() {
13
+ export function createPolicyRegistry(options) {
10
14
  const policies = new Map();
15
+ const allowOverride = options?.allowOverride ?? false;
11
16
  return {
12
17
  /**
13
18
  * Register a policy.
19
+ *
20
+ * Re-registering a name is rejected unless `allowOverride` was set: a
21
+ * second `define("owner-only", …)` used to replace the first in silence,
22
+ * which is an authorization rule vanishing without a trace.
14
23
  */
15
24
  define(definition) {
25
+ if (policies.has(definition.name) && !allowOverride) {
26
+ throw new DuplicatePolicyError(definition.name);
27
+ }
16
28
  policies.set(definition.name, Object.freeze({ ...definition }));
17
29
  },
18
- /**
19
- * Get a policy by name.
20
- */
21
30
  get(name) {
22
31
  return policies.get(name);
23
32
  },
33
+ has(name) {
34
+ return policies.has(name);
35
+ },
24
36
  /**
25
- * Get all policies that apply to a given permission string.
37
+ * Policies that apply to a permission, highest priority first.
38
+ *
39
+ * Delegates to the evaluator's own selection, so this and the engine can
40
+ * never disagree about which policies cover a permission — they were two
41
+ * copies of the same filter-and-sort before.
26
42
  */
27
43
  forPermission(permission) {
28
- const results = [];
29
- for (const [, policy] of policies) {
30
- if (policy.permissions.includes(permission)) {
31
- results.push(policy);
32
- }
33
- }
34
- // Sort by priority descending
35
- return results.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
44
+ return selectPolicies([...policies.values()], permission);
36
45
  },
37
- /**
38
- * Get all registered policy names.
39
- */
40
46
  names() {
41
- return Array.from(policies.keys());
47
+ return [...policies.keys()];
48
+ },
49
+ all() {
50
+ return [...policies.values()];
42
51
  },
43
- /**
44
- * Remove a policy.
45
- */
46
52
  remove(name) {
47
53
  return policies.delete(name);
48
54
  },
49
- /**
50
- * Clear all policies.
51
- */
52
55
  clear() {
53
56
  policies.clear();
54
57
  },
@@ -3,6 +3,6 @@
3
3
  *
4
4
  * @module role
5
5
  */
6
- export { createRoleRegistry, type RoleRegistryOptions, } from "./roleRegistry.js";
7
- export { resolveRolePermissions } from "./roleHierarchy.js";
6
+ export { createRoleRegistry, type RoleRegistry, type RoleRegistryOptions, } from "./roleRegistry.js";
7
+ export { resolveRolePermissions, memoizeRoleLookup, type RoleResolution, type RoleResolutionOptions, } from "./roleHierarchy.js";
8
8
  //# sourceMappingURL=index.d.ts.map
@@ -4,5 +4,5 @@
4
4
  * @module role
5
5
  */
6
6
  export { createRoleRegistry, } from "./roleRegistry.js";
7
- export { resolveRolePermissions } from "./roleHierarchy.js";
7
+ export { resolveRolePermissions, memoizeRoleLookup, } from "./roleHierarchy.js";
8
8
  //# sourceMappingURL=index.js.map
@@ -3,13 +3,43 @@
3
3
  *
4
4
  * @module role/roleHierarchy
5
5
  */
6
- import type { RoleDefinition } from "../permissionTypes/index.js";
6
+ import type { PermissionRule, RoleDefinition } from "../permissionTypes/index.js";
7
+ /** What a set of roles grants, once inheritance is followed. */
8
+ export interface RoleResolution {
9
+ readonly permissions: readonly string[];
10
+ readonly rules: readonly PermissionRule[];
11
+ /** Role names the lookup could not find. */
12
+ readonly unknownRoles: readonly string[];
13
+ }
14
+ /** Options for {@link resolveRolePermissions}. */
15
+ export interface RoleResolutionOptions {
16
+ /**
17
+ * Called for each role the lookup cannot find.
18
+ *
19
+ * Reporting rather than throwing is deliberate: a stale role name in a
20
+ * token is a data problem, and an authorization check that throws hands the
21
+ * outcome to an outer error handler instead of denying.
22
+ */
23
+ readonly onUnknownRole?: (name: string) => void;
24
+ }
7
25
  /**
8
- * Resolve all permissions for a set of role names, following inheritance chains.
26
+ * Resolve everything a set of roles grants, following inheritance chains.
9
27
  *
10
28
  * @param roleNames - Direct role names assigned to the actor.
11
29
  * @param getRole - Function to look up a role definition by name.
12
- * @returns Deduplicated list of all permission strings.
30
+ * @returns Deduplicated permissions and rules, plus any unresolved names.
31
+ * @throws CircularRoleInheritanceError when the graph contains a cycle
13
32
  */
14
- export declare function resolveRolePermissions(roleNames: readonly string[], getRole: (name: string) => RoleDefinition | undefined): readonly string[];
33
+ export declare function resolveRolePermissions(roleNames: readonly string[], getRole: (name: string) => RoleDefinition | undefined, options?: RoleResolutionOptions): RoleResolution;
34
+ /**
35
+ * Wrap a role lookup with a memo, so a deep hierarchy is walked once per
36
+ * distinct role rather than on every authorization check.
37
+ *
38
+ * The cache is invalidated by `invalidate()`; a registry that changes at
39
+ * runtime should call it.
40
+ */
41
+ export declare function memoizeRoleLookup(getRole: (name: string) => RoleDefinition | undefined): {
42
+ readonly lookup: (name: string) => RoleDefinition | undefined;
43
+ readonly invalidate: () => void;
44
+ };
15
45
  //# sourceMappingURL=roleHierarchy.d.ts.map
@@ -3,46 +3,71 @@
3
3
  *
4
4
  * @module role/roleHierarchy
5
5
  */
6
- import { CircularRoleInheritanceError, RoleNotFoundError, } from "../permissionErrors/index.js";
6
+ import { CircularRoleInheritanceError } from "../permissionErrors/index.js";
7
7
  /**
8
- * Resolve all permissions for a set of role names, following inheritance chains.
8
+ * Resolve everything a set of roles grants, following inheritance chains.
9
9
  *
10
10
  * @param roleNames - Direct role names assigned to the actor.
11
11
  * @param getRole - Function to look up a role definition by name.
12
- * @returns Deduplicated list of all permission strings.
12
+ * @returns Deduplicated permissions and rules, plus any unresolved names.
13
+ * @throws CircularRoleInheritanceError when the graph contains a cycle
13
14
  */
14
- export function resolveRolePermissions(roleNames, getRole) {
15
+ export function resolveRolePermissions(roleNames, getRole, options) {
15
16
  const permissions = new Set();
17
+ const rules = [];
18
+ const unknownRoles = [];
16
19
  const visited = new Set();
17
20
  for (const name of roleNames) {
18
- collectPermissions(name, getRole, permissions, visited, []);
21
+ collect(name, getRole, permissions, rules, unknownRoles, visited, []);
19
22
  }
20
- return Array.from(permissions);
21
- }
22
- /**
23
- * Collect permissions from a role and its ancestors, detecting cycles.
24
- */
25
- function collectPermissions(roleName, getRole, permissions, visited, chain) {
26
- const role = getRole(roleName);
27
- if (!role) {
28
- throw new RoleNotFoundError(roleName);
23
+ if (options?.onUnknownRole) {
24
+ for (const name of unknownRoles)
25
+ options.onUnknownRole(name);
29
26
  }
30
- // Cycle detection — check before adding to visited
27
+ return { permissions: [...permissions], rules, unknownRoles };
28
+ }
29
+ function collect(roleName, getRole, permissions, rules, unknownRoles, visited, chain) {
30
+ // Cycle detection comes first: a role that appears twice in the current
31
+ // chain is a cycle even if it has already been visited on another branch.
31
32
  if (chain.includes(roleName)) {
32
33
  throw new CircularRoleInheritanceError([...chain, roleName]);
33
34
  }
34
- // Skip if already fully processed (not in current chain)
35
35
  if (visited.has(roleName))
36
36
  return;
37
+ const role = getRole(roleName);
38
+ if (!role) {
39
+ if (!unknownRoles.includes(roleName))
40
+ unknownRoles.push(roleName);
41
+ return;
42
+ }
37
43
  visited.add(roleName);
38
44
  const newChain = [...chain, roleName];
39
- for (const perm of role.permissions) {
40
- permissions.add(perm);
41
- }
42
- if (role.inherits) {
43
- for (const parent of role.inherits) {
44
- collectPermissions(parent, getRole, permissions, visited, newChain);
45
- }
45
+ for (const permission of role.permissions)
46
+ permissions.add(permission);
47
+ if (role.rules)
48
+ rules.push(...role.rules);
49
+ for (const parent of role.inherits ?? []) {
50
+ collect(parent, getRole, permissions, rules, unknownRoles, visited, newChain);
46
51
  }
47
52
  }
53
+ /**
54
+ * Wrap a role lookup with a memo, so a deep hierarchy is walked once per
55
+ * distinct role rather than on every authorization check.
56
+ *
57
+ * The cache is invalidated by `invalidate()`; a registry that changes at
58
+ * runtime should call it.
59
+ */
60
+ export function memoizeRoleLookup(getRole) {
61
+ const cache = new Map();
62
+ return {
63
+ lookup: (name) => {
64
+ if (cache.has(name))
65
+ return cache.get(name);
66
+ const role = getRole(name);
67
+ cache.set(name, role);
68
+ return role;
69
+ },
70
+ invalidate: () => cache.clear(),
71
+ };
72
+ }
48
73
  //# sourceMappingURL=roleHierarchy.js.map
@@ -8,38 +8,29 @@ import type { RoleDefinition } from "../permissionTypes/index.js";
8
8
  export interface RoleRegistryOptions {
9
9
  /** Allow overwriting existing roles. Defaults to false. */
10
10
  readonly allowOverride?: boolean;
11
- }
12
- /**
13
- * Create a role registry.
14
- */
15
- export declare function createRoleRegistry(options?: RoleRegistryOptions): {
16
11
  /**
17
- * Register a role definition.
12
+ * Reject permission strings that are not `resource:action`. Defaults to
13
+ * true — a malformed grant can never match, so accepting one registers a
14
+ * permission that silently does nothing.
18
15
  */
16
+ readonly validatePermissions?: boolean;
17
+ }
18
+ /** A role registry, usable directly as the engine's `roles` source. */
19
+ export interface RoleRegistry {
19
20
  define(definition: RoleDefinition): void;
20
- /**
21
- * Get a role by name.
22
- */
23
21
  get(name: string): RoleDefinition | undefined;
24
- /**
25
- * Check if a role exists.
26
- */
22
+ /** Like {@link RoleRegistry.get}, but throws when the role is unregistered. */
23
+ require(name: string): RoleDefinition;
27
24
  has(name: string): boolean;
28
- /**
29
- * Get all registered role names.
30
- */
31
25
  names(): readonly string[];
32
- /**
33
- * Get all registered role definitions.
34
- */
35
26
  all(): readonly RoleDefinition[];
36
- /**
37
- * Remove a role from the registry.
38
- */
39
27
  remove(name: string): boolean;
40
- /**
41
- * Clear all registered roles.
42
- */
43
28
  clear(): void;
44
- };
29
+ }
30
+ /**
31
+ * Create a role registry.
32
+ *
33
+ * Pass it straight to `createPermissionEngine({ roles: registry })`.
34
+ */
35
+ export declare function createRoleRegistry(options?: RoleRegistryOptions): RoleRegistry;
45
36
  //# sourceMappingURL=roleRegistry.d.ts.map