@zudojs/permissions 0.1.0 → 1.1.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 +350 -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 +25 -5
  6. package/dist/cache/cache.core.js +77 -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 +156 -16
  14. package/dist/evaluator/evaluator.core.d.ts +28 -4
  15. package/dist/evaluator/evaluator.core.js +259 -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 +32 -24
  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 +25 -25
  58. package/dist/role/roleRegistry.js +46 -24
  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 +27 -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
@@ -3,62 +3,84 @@
3
3
  *
4
4
  * @module role/roleRegistry
5
5
  */
6
- import { DuplicateRoleError, InvalidRoleError, } from "../permissionErrors/index.js";
6
+ import { DuplicateRoleError, InvalidRoleError, RoleNotFoundError, } from "../permissionErrors/index.js";
7
+ import { isValidPermission } from "../permission/permission.core.js";
8
+ /**
9
+ * Copy a role definition so it no longer shares arrays with the caller.
10
+ *
11
+ * `Object.freeze({ ...definition })` froze the wrapper and kept the caller's
12
+ * `permissions`, `inherits` and `rules` arrays by reference — so pushing
13
+ * `"*:*"` onto an array *after* `define()` had validated it widened the role
14
+ * in silence.
15
+ */
16
+ export function freezeRoleDefinition(definition) {
17
+ return Object.freeze({
18
+ ...definition,
19
+ permissions: Object.freeze([...definition.permissions]),
20
+ ...(definition.inherits
21
+ ? { inherits: Object.freeze([...definition.inherits]) }
22
+ : {}),
23
+ ...(definition.rules ? { rules: Object.freeze([...definition.rules]) } : {}),
24
+ });
25
+ }
7
26
  /**
8
27
  * Create a role registry.
28
+ *
29
+ * Pass it straight to `createPermissionEngine({ roles: registry })`.
9
30
  */
10
31
  export function createRoleRegistry(options) {
11
32
  const roles = new Map();
12
33
  const allowOverride = options?.allowOverride ?? false;
34
+ const validatePermissions = options?.validatePermissions ?? true;
13
35
  return {
14
36
  /**
15
37
  * Register a role definition.
38
+ *
39
+ * A role with no permissions of its own is valid: a role that exists only
40
+ * to combine others through `inherits` is the normal way to build a
41
+ * hierarchy.
16
42
  */
17
43
  define(definition) {
18
44
  if (!definition.name || definition.name.trim() === "") {
19
45
  throw new InvalidRoleError("Role name cannot be empty");
20
46
  }
21
- if (!definition.permissions || definition.permissions.length === 0) {
22
- throw new InvalidRoleError(`Role "${definition.name}" must have at least one permission`);
47
+ if (!Array.isArray(definition.permissions)) {
48
+ throw new InvalidRoleError(`Role "${definition.name}" must declare a permissions array`);
49
+ }
50
+ if (validatePermissions) {
51
+ for (const permission of definition.permissions) {
52
+ if (!isValidPermission(permission)) {
53
+ throw new InvalidRoleError(`Role "${definition.name}" grants "${permission}", which is not a ` +
54
+ `valid "resource:action" permission`);
55
+ }
56
+ }
23
57
  }
24
58
  if (roles.has(definition.name) && !allowOverride) {
25
59
  throw new DuplicateRoleError(definition.name);
26
60
  }
27
- roles.set(definition.name, Object.freeze({ ...definition }));
61
+ roles.set(definition.name, freezeRoleDefinition(definition));
28
62
  },
29
- /**
30
- * Get a role by name.
31
- */
32
63
  get(name) {
33
64
  return roles.get(name);
34
65
  },
35
- /**
36
- * Check if a role exists.
37
- */
66
+ require(name) {
67
+ const role = roles.get(name);
68
+ if (!role)
69
+ throw new RoleNotFoundError(name);
70
+ return role;
71
+ },
38
72
  has(name) {
39
73
  return roles.has(name);
40
74
  },
41
- /**
42
- * Get all registered role names.
43
- */
44
75
  names() {
45
- return Array.from(roles.keys());
76
+ return [...roles.keys()];
46
77
  },
47
- /**
48
- * Get all registered role definitions.
49
- */
50
78
  all() {
51
- return Array.from(roles.values());
79
+ return [...roles.values()];
52
80
  },
53
- /**
54
- * Remove a role from the registry.
55
- */
56
81
  remove(name) {
57
82
  return roles.delete(name);
58
83
  },
59
- /**
60
- * Clear all registered roles.
61
- */
62
84
  clear() {
63
85
  roles.clear();
64
86
  },
@@ -3,6 +3,6 @@
3
3
  *
4
4
  * @module rule
5
5
  */
6
- export { ruleMatches, evaluateRules } from "./rule.core.js";
7
- export { compileRules, findMatchingRules } from "./ruleCompiler.js";
6
+ export { ruleMatches, patternStrMatches, evaluateRules, evaluateRulesSync, } from "./rule.core.js";
7
+ export { compileRules, findMatchingRules, type RuleIndex, } from "./ruleCompiler.js";
8
8
  //# sourceMappingURL=index.d.ts.map
@@ -3,6 +3,6 @@
3
3
  *
4
4
  * @module rule
5
5
  */
6
- export { ruleMatches, evaluateRules } from "./rule.core.js";
7
- export { compileRules, findMatchingRules } from "./ruleCompiler.js";
6
+ export { ruleMatches, patternStrMatches, evaluateRules, evaluateRulesSync, } from "./rule.core.js";
7
+ export { compileRules, findMatchingRules, } from "./ruleCompiler.js";
8
8
  //# sourceMappingURL=index.js.map
@@ -1,24 +1,53 @@
1
1
  /**
2
- * Rule matching and compilation for permission evaluation.
2
+ * Rule matching and evaluation for permission decisions.
3
3
  *
4
4
  * @module rule/rule
5
5
  */
6
- import type { PermissionRule, Permission } from "../permissionTypes/index.js";
6
+ import type { PermissionRule, Permission, PermissionContext, RuleCombiningAlgorithm, RuleEvaluation } from "../permissionTypes/index.js";
7
7
  /**
8
8
  * Check if a rule matches a target permission.
9
9
  *
10
- * A rule matches if its resource and action patterns match the target,
11
- * considering wildcard support.
10
+ * Matching is about resource and action only. Whether the rule *applies* also
11
+ * depends on its condition, which needs a context and is therefore evaluated
12
+ * by {@link evaluateRules}.
12
13
  */
13
14
  export declare function ruleMatches(rule: PermissionRule, target: Permission): boolean;
15
+ /**
16
+ * Check if a single pattern string matches a target, supporting wildcards.
17
+ *
18
+ * Two forms are supported, and they are the same two the permission matcher
19
+ * supports, so a pattern means the same thing wherever it is written:
20
+ * `*` — matches anything
21
+ * `billing.*` — matches `billing` and any `billing.…` namespace
22
+ */
23
+ export declare function patternStrMatches(pattern: string, target: string): boolean;
14
24
  /**
15
25
  * Evaluate a set of rules against a target permission.
16
26
  *
17
- * Returns the effective decision after applying deny-overrides and priority.
18
- * Default: deny.
27
+ * A rule with a condition applies only when that condition returns `true`,
28
+ * which is what makes ABAC work: `{ effect: "allow", condition: isOwner() }`
29
+ * has to mean "owners may", not "anyone may". Conditions are async, so this
30
+ * function is too.
31
+ *
32
+ * A condition that throws is treated as unmet — an authorization check fails
33
+ * closed. A rule that carries a condition when no context was supplied is
34
+ * skipped for the same reason.
35
+ *
36
+ * Default combining algorithm: `deny-overrides`. Any applicable deny wins,
37
+ * whatever its priority.
38
+ */
39
+ export declare function evaluateRules(rules: readonly PermissionRule[], target: Permission, context?: PermissionContext, options?: {
40
+ readonly algorithm?: RuleCombiningAlgorithm;
41
+ readonly onConditionError?: (rule: PermissionRule, error: unknown) => void;
42
+ }): Promise<RuleEvaluation>;
43
+ /**
44
+ * Evaluate rules that carry no conditions.
45
+ *
46
+ * Synchronous, for callers that build their own condition-free rule sets.
47
+ * Any rule with a condition is skipped, because there is no way to evaluate
48
+ * one without awaiting it — use {@link evaluateRules} for those.
19
49
  */
20
- export declare function evaluateRules(rules: readonly PermissionRule[], target: Permission): {
21
- readonly allowed: boolean;
22
- readonly matchedRule?: PermissionRule;
23
- };
50
+ export declare function evaluateRulesSync(rules: readonly PermissionRule[], target: Permission, options?: {
51
+ readonly algorithm?: RuleCombiningAlgorithm;
52
+ }): RuleEvaluation;
24
53
  //# sourceMappingURL=rule.core.d.ts.map
@@ -1,45 +1,39 @@
1
1
  /**
2
- * Rule matching and compilation for permission evaluation.
2
+ * Rule matching and evaluation for permission decisions.
3
3
  *
4
4
  * @module rule/rule
5
5
  */
6
6
  /**
7
7
  * Check if a rule matches a target permission.
8
8
  *
9
- * A rule matches if its resource and action patterns match the target,
10
- * considering wildcard support.
9
+ * Matching is about resource and action only. Whether the rule *applies* also
10
+ * depends on its condition, which needs a context and is therefore evaluated
11
+ * by {@link evaluateRules}.
11
12
  */
12
13
  export function ruleMatches(rule, target) {
13
- return (resourceMatches(rule.resource, target.resource) &&
14
- actionMatches(rule.action, target.action));
14
+ return (patternListMatches(rule.resource, target.resource) &&
15
+ patternListMatches(rule.action, target.action));
15
16
  }
16
- /**
17
- * Check if a rule's resource pattern matches a target resource.
18
- */
19
- function resourceMatches(pattern, target) {
17
+ /** Check if any pattern in a rule field matches a target segment. */
18
+ function patternListMatches(pattern, target) {
20
19
  if (Array.isArray(pattern)) {
21
- return pattern.some((p) => patternStrMatches(p, target));
22
- }
23
- return patternStrMatches(pattern, target);
24
- }
25
- /**
26
- * Check if a rule's action pattern matches a target action.
27
- */
28
- function actionMatches(pattern, target) {
29
- if (Array.isArray(pattern)) {
30
- return pattern.some((p) => patternStrMatches(p, target));
20
+ return pattern.some((entry) => patternStrMatches(entry, target));
31
21
  }
32
22
  return patternStrMatches(pattern, target);
33
23
  }
34
24
  /**
35
25
  * Check if a single pattern string matches a target, supporting wildcards.
26
+ *
27
+ * Two forms are supported, and they are the same two the permission matcher
28
+ * supports, so a pattern means the same thing wherever it is written:
29
+ * `*` — matches anything
30
+ * `billing.*` — matches `billing` and any `billing.…` namespace
36
31
  */
37
- function patternStrMatches(pattern, target) {
32
+ export function patternStrMatches(pattern, target) {
38
33
  if (pattern === "*")
39
34
  return true;
40
35
  if (pattern === target)
41
36
  return true;
42
- // Namespace wildcard: "billing.*" matches "billing.invoice"
43
37
  if (pattern.endsWith(".*")) {
44
38
  const prefix = pattern.slice(0, -2);
45
39
  return target === prefix || target.startsWith(`${prefix}.`);
@@ -49,21 +43,87 @@ function patternStrMatches(pattern, target) {
49
43
  /**
50
44
  * Evaluate a set of rules against a target permission.
51
45
  *
52
- * Returns the effective decision after applying deny-overrides and priority.
53
- * Default: deny.
46
+ * A rule with a condition applies only when that condition returns `true`,
47
+ * which is what makes ABAC work: `{ effect: "allow", condition: isOwner() }`
48
+ * has to mean "owners may", not "anyone may". Conditions are async, so this
49
+ * function is too.
50
+ *
51
+ * A condition that throws is treated as unmet — an authorization check fails
52
+ * closed. A rule that carries a condition when no context was supplied is
53
+ * skipped for the same reason.
54
+ *
55
+ * Default combining algorithm: `deny-overrides`. Any applicable deny wins,
56
+ * whatever its priority.
57
+ */
58
+ export async function evaluateRules(rules, target, context, options) {
59
+ const algorithm = options?.algorithm ?? "deny-overrides";
60
+ const applicable = [];
61
+ for (const rule of rules) {
62
+ if (!ruleMatches(rule, target))
63
+ continue;
64
+ if (rule.condition) {
65
+ if (!context)
66
+ continue;
67
+ let met = false;
68
+ try {
69
+ met = await rule.condition(context);
70
+ }
71
+ catch (error) {
72
+ options?.onConditionError?.(rule, error);
73
+ met = false;
74
+ }
75
+ if (!met)
76
+ continue;
77
+ }
78
+ applicable.push(rule);
79
+ }
80
+ if (applicable.length === 0) {
81
+ return { allowed: false, applicable: [] };
82
+ }
83
+ if (algorithm === "deny-overrides") {
84
+ const deny = applicable.find((rule) => rule.effect === "deny");
85
+ if (deny)
86
+ return { allowed: false, matchedRule: deny, applicable };
87
+ return { allowed: true, matchedRule: applicable[0], applicable };
88
+ }
89
+ const sorted = [...applicable].sort((a, b) => {
90
+ const priorityA = a.priority ?? 0;
91
+ const priorityB = b.priority ?? 0;
92
+ if (priorityA !== priorityB)
93
+ return priorityB - priorityA;
94
+ // Same priority: deny wins.
95
+ if (a.effect === "deny" && b.effect !== "deny")
96
+ return -1;
97
+ if (b.effect === "deny" && a.effect !== "deny")
98
+ return 1;
99
+ return 0;
100
+ });
101
+ const top = sorted[0];
102
+ return { allowed: top.effect === "allow", matchedRule: top, applicable };
103
+ }
104
+ /**
105
+ * Evaluate rules that carry no conditions.
106
+ *
107
+ * Synchronous, for callers that build their own condition-free rule sets.
108
+ * Any rule with a condition is skipped, because there is no way to evaluate
109
+ * one without awaiting it — use {@link evaluateRules} for those.
54
110
  */
55
- export function evaluateRules(rules, target) {
56
- const matching = rules.filter((r) => ruleMatches(r, target));
57
- if (matching.length === 0) {
58
- return { allowed: false };
111
+ export function evaluateRulesSync(rules, target, options) {
112
+ const algorithm = options?.algorithm ?? "deny-overrides";
113
+ const applicable = rules.filter((rule) => rule.condition === undefined && ruleMatches(rule, target));
114
+ if (applicable.length === 0)
115
+ return { allowed: false, applicable: [] };
116
+ if (algorithm === "deny-overrides") {
117
+ const deny = applicable.find((rule) => rule.effect === "deny");
118
+ if (deny)
119
+ return { allowed: false, matchedRule: deny, applicable };
120
+ return { allowed: true, matchedRule: applicable[0], applicable };
59
121
  }
60
- // Sort by priority descending; same priority: deny wins (deny has higher effective priority)
61
- const sorted = [...matching].sort((a, b) => {
62
- const priA = a.priority ?? 0;
63
- const priB = b.priority ?? 0;
64
- if (priA !== priB)
65
- return priB - priA;
66
- // Same priority: deny wins
122
+ const sorted = [...applicable].sort((a, b) => {
123
+ const priorityA = a.priority ?? 0;
124
+ const priorityB = b.priority ?? 0;
125
+ if (priorityA !== priorityB)
126
+ return priorityB - priorityA;
67
127
  if (a.effect === "deny" && b.effect !== "deny")
68
128
  return -1;
69
129
  if (b.effect === "deny" && a.effect !== "deny")
@@ -71,9 +131,6 @@ export function evaluateRules(rules, target) {
71
131
  return 0;
72
132
  });
73
133
  const top = sorted[0];
74
- return {
75
- allowed: top.effect === "allow",
76
- matchedRule: top,
77
- };
134
+ return { allowed: top.effect === "allow", matchedRule: top, applicable };
78
135
  }
79
136
  //# sourceMappingURL=rule.core.js.map
@@ -4,16 +4,22 @@
4
4
  * @module rule/ruleCompiler
5
5
  */
6
6
  import type { PermissionRule, Permission } from "../permissionTypes/index.js";
7
- /** Compiled rule index for O(1) lookup by resource:action. */
8
- interface RuleIndex {
7
+ /** Compiled rule index for fast lookup by resource:action. */
8
+ export interface RuleIndex {
9
9
  /** Exact matches: "post:update" → rules. */
10
10
  readonly exact: ReadonlyMap<string, readonly PermissionRule[]>;
11
- /** Resource wildcards: "post:*" → rules. */
11
+ /** Resource wildcards: "post:*" → rules, keyed by resource. */
12
12
  readonly resourceWildcard: ReadonlyMap<string, readonly PermissionRule[]>;
13
- /** Action wildcards: "*:read" → rules. */
13
+ /** Action wildcards: "*:read" → rules, keyed by action. */
14
14
  readonly actionWildcard: ReadonlyMap<string, readonly PermissionRule[]>;
15
15
  /** Global wildcards: "*:*" → rules. */
16
16
  readonly globalWildcard: readonly PermissionRule[];
17
+ /**
18
+ * Rules using a namespace wildcard (`billing.*`), which no exact key can
19
+ * index. Scanned linearly — small in practice, and leaving them out of the
20
+ * index entirely is what made them silently unmatchable.
21
+ */
22
+ readonly patterned: readonly PermissionRule[];
17
23
  }
18
24
  /**
19
25
  * Compile a set of rules into an optimized index.
@@ -21,7 +27,10 @@ interface RuleIndex {
21
27
  export declare function compileRules(rules: readonly PermissionRule[]): RuleIndex;
22
28
  /**
23
29
  * Find all rules from the compiled index that match a target permission.
30
+ *
31
+ * The result is de-duplicated: a rule listing several resources or actions is
32
+ * indexed under each, and returning it more than once would let one rule
33
+ * count twice when the decision is combined.
24
34
  */
25
35
  export declare function findMatchingRules(index: RuleIndex, target: Permission): readonly PermissionRule[];
26
- export {};
27
36
  //# sourceMappingURL=ruleCompiler.d.ts.map
@@ -3,6 +3,11 @@
3
3
  *
4
4
  * @module rule/ruleCompiler
5
5
  */
6
+ import { ruleMatches } from "./rule.core.js";
7
+ /** True when a pattern needs a linear scan rather than a map lookup. */
8
+ function isPatterned(pattern) {
9
+ return pattern !== "*" && pattern.endsWith(".*");
10
+ }
6
11
  /**
7
12
  * Compile a set of rules into an optimized index.
8
13
  */
@@ -11,63 +16,74 @@ export function compileRules(rules) {
11
16
  const resourceWildcard = new Map();
12
17
  const actionWildcard = new Map();
13
18
  const globalWildcard = [];
19
+ const patterned = [];
20
+ const push = (map, key, rule) => {
21
+ const bucket = map.get(key);
22
+ if (bucket)
23
+ bucket.push(rule);
24
+ else
25
+ map.set(key, [rule]);
26
+ };
14
27
  for (const rule of rules) {
15
28
  const resources = normalizeToArray(rule.resource);
16
29
  const actions = normalizeToArray(rule.action);
30
+ let indexed = false;
17
31
  for (const resource of resources) {
18
32
  for (const action of actions) {
33
+ if (isPatterned(resource) || isPatterned(action)) {
34
+ if (!indexed) {
35
+ patterned.push(rule);
36
+ indexed = true;
37
+ }
38
+ continue;
39
+ }
19
40
  if (resource === "*" && action === "*") {
20
41
  globalWildcard.push(rule);
21
42
  }
22
43
  else if (resource === "*") {
23
- const key = action;
24
- if (!actionWildcard.has(key))
25
- actionWildcard.set(key, []);
26
- actionWildcard.get(key).push(rule);
44
+ push(actionWildcard, action, rule);
27
45
  }
28
46
  else if (action === "*") {
29
- const key = resource;
30
- if (!resourceWildcard.has(key))
31
- resourceWildcard.set(key, []);
32
- resourceWildcard.get(key).push(rule);
47
+ push(resourceWildcard, resource, rule);
33
48
  }
34
49
  else {
35
- const key = `${resource}:${action}`;
36
- if (!exact.has(key))
37
- exact.set(key, []);
38
- exact.get(key).push(rule);
50
+ push(exact, `${resource}:${action}`, rule);
39
51
  }
40
52
  }
41
53
  }
42
54
  }
43
- const index = {
44
- exact,
45
- resourceWildcard,
46
- actionWildcard,
47
- globalWildcard,
48
- };
49
- return index;
55
+ return { exact, resourceWildcard, actionWildcard, globalWildcard, patterned };
50
56
  }
51
57
  /**
52
58
  * Find all rules from the compiled index that match a target permission.
59
+ *
60
+ * The result is de-duplicated: a rule listing several resources or actions is
61
+ * indexed under each, and returning it more than once would let one rule
62
+ * count twice when the decision is combined.
53
63
  */
54
64
  export function findMatchingRules(index, target) {
55
65
  const results = [];
56
- const key = `${target.resource}:${target.action}`;
66
+ const seen = new Set();
67
+ const add = (rules) => {
68
+ if (!rules)
69
+ return;
70
+ for (const rule of rules) {
71
+ if (seen.has(rule))
72
+ continue;
73
+ seen.add(rule);
74
+ results.push(rule);
75
+ }
76
+ };
57
77
  // 1. Exact matches
58
- const exactRules = index.exact.get(key);
59
- if (exactRules)
60
- results.push(...exactRules);
78
+ add(index.exact.get(`${target.resource}:${target.action}`));
61
79
  // 2. Resource wildcard: "post:*" matches "post:update"
62
- const resWildcard = index.resourceWildcard.get(target.resource);
63
- if (resWildcard)
64
- results.push(...resWildcard);
80
+ add(index.resourceWildcard.get(target.resource));
65
81
  // 3. Action wildcard: "*:read" matches "post:read"
66
- const actWildcard = index.actionWildcard.get(target.action);
67
- if (actWildcard)
68
- results.push(...actWildcard);
82
+ add(index.actionWildcard.get(target.action));
69
83
  // 4. Global wildcard: "*:*"
70
- results.push(...index.globalWildcard);
84
+ add(index.globalWildcard);
85
+ // 5. Namespace wildcards, which need a real match test
86
+ add(index.patterned.filter((rule) => ruleMatches(rule, target)));
71
87
  return results;
72
88
  }
73
89
  function normalizeToArray(value) {
@@ -6,6 +6,10 @@
6
6
  import type { PermissionActor } from "../permissionTypes/index.js";
7
7
  /**
8
8
  * Create a permission check cache key.
9
+ *
10
+ * @deprecated Use `permissionCacheKey` from `@zudojs/permissions`. Two key
11
+ * formats meant entries written with one were invisible to the invalidation
12
+ * that understood the other; this now delegates to the canonical builder.
9
13
  */
10
14
  export declare function createCacheKey(actorId: string, permission: string, resourceId?: string): string;
11
15
  /**
@@ -27,6 +31,10 @@ export declare function extractAction(permission: string): string;
27
31
  export declare function buildPermission(resource: string, action: string): string;
28
32
  /**
29
33
  * Create a quick actor object.
34
+ *
35
+ * @deprecated Use `createPermissionActor`. This used to build an actor whose
36
+ * role and permission arrays were not frozen, so two functions with the same
37
+ * job produced objects with different mutability.
30
38
  */
31
39
  export declare function createActor(id: string, options?: {
32
40
  readonly type?: string;
@@ -3,12 +3,18 @@
3
3
  *
4
4
  * @module utils/utils
5
5
  */
6
+ import { permissionCacheKey } from "../cache/cache.core.js";
7
+ import { createPermissionActor } from "../actor/actor.core.js";
8
+ import { formatPermission, parsePermissionSafe, } from "../permission/permission.core.js";
6
9
  /**
7
10
  * Create a permission check cache key.
11
+ *
12
+ * @deprecated Use `permissionCacheKey` from `@zudojs/permissions`. Two key
13
+ * formats meant entries written with one were invisible to the invalidation
14
+ * that understood the other; this now delegates to the canonical builder.
8
15
  */
9
16
  export function createCacheKey(actorId, permission, resourceId) {
10
- const base = `${actorId}:${permission}`;
11
- return resourceId ? `${base}:${resourceId}` : base;
17
+ return permissionCacheKey(actorId, permission, resourceId);
12
18
  }
13
19
  /**
14
20
  * Extract the resource type from a permission string.
@@ -17,8 +23,7 @@ export function createCacheKey(actorId, permission, resourceId) {
17
23
  * @example extractResource("billing.invoice:refund") → "billing.invoice"
18
24
  */
19
25
  export function extractResource(permission) {
20
- const lastColon = permission.lastIndexOf(":");
21
- return lastColon > 0 ? permission.slice(0, lastColon) : permission;
26
+ return parsePermissionSafe(permission)?.resource ?? permission;
22
27
  }
23
28
  /**
24
29
  * Extract the action from a permission string.
@@ -26,25 +31,22 @@ export function extractResource(permission) {
26
31
  * @example extractAction("post:update") → "update"
27
32
  */
28
33
  export function extractAction(permission) {
29
- const lastColon = permission.lastIndexOf(":");
30
- return lastColon > 0 ? permission.slice(lastColon + 1) : "";
34
+ return parsePermissionSafe(permission)?.action ?? "";
31
35
  }
32
36
  /**
33
37
  * Build a permission string from resource and action.
34
38
  */
35
39
  export function buildPermission(resource, action) {
36
- return `${resource}:${action}`;
40
+ return formatPermission(resource, action);
37
41
  }
38
42
  /**
39
43
  * Create a quick actor object.
44
+ *
45
+ * @deprecated Use `createPermissionActor`. This used to build an actor whose
46
+ * role and permission arrays were not frozen, so two functions with the same
47
+ * job produced objects with different mutability.
40
48
  */
41
49
  export function createActor(id, options) {
42
- return Object.freeze({
43
- id,
44
- type: options?.type,
45
- roles: options?.roles,
46
- permissions: options?.permissions,
47
- deniedPermissions: options?.deniedPermissions,
48
- });
50
+ return createPermissionActor(id, options);
49
51
  }
50
52
  //# sourceMappingURL=utils.helper.js.map