@zudojs/permissions 1.0.0 → 1.2.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 (43) hide show
  1. package/README.md +76 -21
  2. package/dist/ability/ability.core.js +4 -24
  3. package/dist/cache/cache.actorDigest.d.ts +26 -0
  4. package/dist/cache/cache.actorDigest.js +90 -0
  5. package/dist/cache/cache.core.d.ts +9 -2
  6. package/dist/cache/cache.core.js +27 -5
  7. package/dist/cache/index.d.ts +1 -0
  8. package/dist/cache/index.js +1 -0
  9. package/dist/evaluator/authorizationEngine.d.ts +20 -3
  10. package/dist/evaluator/authorizationEngine.js +59 -73
  11. package/dist/evaluator/engineSupport/authorizationEngine.validation.d.ts +37 -0
  12. package/dist/evaluator/engineSupport/authorizationEngine.validation.js +90 -0
  13. package/dist/evaluator/engineSupport/evaluator.cacheKey.d.ts +24 -0
  14. package/dist/evaluator/engineSupport/evaluator.cacheKey.js +56 -0
  15. package/dist/evaluator/engineSupport/evaluator.observed.d.ts +18 -0
  16. package/dist/evaluator/engineSupport/evaluator.observed.js +49 -0
  17. package/dist/evaluator/engineSupport/index.d.ts +10 -0
  18. package/dist/evaluator/engineSupport/index.js +10 -0
  19. package/dist/evaluator/evaluator.core.js +36 -41
  20. package/dist/evaluator/evaluator.pipeline.d.ts +6 -0
  21. package/dist/evaluator/evaluator.pipeline.js +25 -3
  22. package/dist/http/httpMiddleware.core.d.ts +15 -5
  23. package/dist/http/httpMiddleware.core.js +18 -4
  24. package/dist/http/httpResource.helper.d.ts +30 -0
  25. package/dist/http/httpResource.helper.js +32 -0
  26. package/dist/http/httpTypes.d.ts +17 -4
  27. package/dist/http/index.d.ts +7 -5
  28. package/dist/http/index.js +6 -4
  29. package/dist/permission/index.d.ts +1 -1
  30. package/dist/permission/index.js +1 -1
  31. package/dist/permission/permission.core.d.ts +12 -0
  32. package/dist/permission/permission.core.js +20 -1
  33. package/dist/policy/policyRegistry.d.ts +12 -0
  34. package/dist/policy/policyRegistry.js +23 -2
  35. package/dist/role/roleRegistry.d.ts +18 -0
  36. package/dist/role/roleRegistry.js +38 -2
  37. package/dist/rule/rule.core.d.ts +14 -22
  38. package/dist/rule/rule.core.js +49 -86
  39. package/dist/rule/rule.pattern.d.ts +44 -0
  40. package/dist/rule/rule.pattern.js +72 -0
  41. package/dist/utils/utils.notifier.d.ts +19 -0
  42. package/dist/utils/utils.notifier.js +31 -0
  43. package/package.json +6 -10
@@ -3,42 +3,34 @@
3
3
  *
4
4
  * @module rule/rule
5
5
  */
6
+ import { ruleBearsOn } from "./rule.pattern.js";
7
+ export { ruleMatches, patternStrMatches } from "./rule.pattern.js";
6
8
  /**
7
- * Check if a rule matches a target permission.
8
- *
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}.
9
+ * Sort key for `priority` combining: higher priority first, and at equal
10
+ * priority a deny before an allow.
12
11
  */
13
- export function ruleMatches(rule, target) {
14
- return (patternListMatches(rule.resource, target.resource) &&
15
- patternListMatches(rule.action, target.action));
16
- }
17
- /** Check if any pattern in a rule field matches a target segment. */
18
- function patternListMatches(pattern, target) {
19
- if (Array.isArray(pattern)) {
20
- return pattern.some((entry) => patternStrMatches(entry, target));
21
- }
22
- return patternStrMatches(pattern, target);
12
+ function byPriority(a, b) {
13
+ const priorityA = a.priority ?? 0;
14
+ const priorityB = b.priority ?? 0;
15
+ if (priorityA !== priorityB)
16
+ return priorityB - priorityA;
17
+ if (a.effect === "deny" && b.effect !== "deny")
18
+ return -1;
19
+ if (b.effect === "deny" && a.effect !== "deny")
20
+ return 1;
21
+ return 0;
23
22
  }
24
- /**
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
31
- */
32
- export function patternStrMatches(pattern, target) {
33
- if (pattern === "*")
34
- return true;
35
- if (pattern === target)
36
- return true;
37
- if (pattern.endsWith(".*")) {
38
- const prefix = pattern.slice(0, -2);
39
- return target === prefix || target.startsWith(`${prefix}.`);
23
+ function combineApplicable(applicable, algorithm) {
24
+ if (applicable.length === 0)
25
+ return { allowed: false, applicable: [] };
26
+ if (algorithm === "deny-overrides") {
27
+ const deny = applicable.find((rule) => rule.effect === "deny");
28
+ if (deny)
29
+ return { allowed: false, matchedRule: deny, applicable };
30
+ return { allowed: true, matchedRule: applicable[0], applicable };
40
31
  }
41
- return false;
32
+ const top = [...applicable].sort(byPriority)[0];
33
+ return { allowed: top.effect === "allow", matchedRule: top, applicable };
42
34
  }
43
35
  /**
44
36
  * Evaluate a set of rules against a target permission.
@@ -48,9 +40,15 @@ export function patternStrMatches(pattern, target) {
48
40
  * has to mean "owners may", not "anyone may". Conditions are async, so this
49
41
  * function is too.
50
42
  *
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.
43
+ * A condition that cannot be evaluated fails closed, and what "closed" means
44
+ * depends on the rule's effect. An allow whose condition throws, or that has
45
+ * a condition but no context, does not apply. A *deny* in the same position
46
+ * does apply: treating it as unmet dropped the deny and let the role's allow
47
+ * win, so a missing resource or a blocklist service that was down granted
48
+ * the very access the deny existed to refuse.
49
+ *
50
+ * A deny also applies when its patterns merely overlap a wildcard target —
51
+ * `can(actor, "post:*")` is refused by a deny on `post:delete`.
54
52
  *
55
53
  * Default combining algorithm: `deny-overrides`. Any applicable deny wins,
56
54
  * whatever its priority.
@@ -59,78 +57,43 @@ export async function evaluateRules(rules, target, context, options) {
59
57
  const algorithm = options?.algorithm ?? "deny-overrides";
60
58
  const applicable = [];
61
59
  for (const rule of rules) {
62
- if (!ruleMatches(rule, target))
60
+ if (!ruleBearsOn(rule, target))
63
61
  continue;
64
62
  if (rule.condition) {
65
- if (!context)
63
+ const failClosed = rule.effect === "deny";
64
+ if (!context) {
65
+ if (failClosed)
66
+ applicable.push(rule);
66
67
  continue;
67
- let met = false;
68
+ }
69
+ let met;
68
70
  try {
69
71
  met = await rule.condition(context);
70
72
  }
71
73
  catch (error) {
72
74
  options?.onConditionError?.(rule, error);
73
- met = false;
75
+ met = failClosed;
74
76
  }
75
77
  if (!met)
76
78
  continue;
77
79
  }
78
80
  applicable.push(rule);
79
81
  }
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 };
82
+ return combineApplicable(applicable, algorithm);
103
83
  }
104
84
  /**
105
85
  * Evaluate rules that carry no conditions.
106
86
  *
107
87
  * 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.
88
+ * A conditional rule cannot be evaluated without awaiting it — use
89
+ * {@link evaluateRules} for those — so it fails closed the same way an
90
+ * unevaluable condition does there: a conditional allow is skipped, and a
91
+ * conditional deny applies.
110
92
  */
111
93
  export function evaluateRulesSync(rules, target, options) {
112
94
  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 };
121
- }
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;
127
- if (a.effect === "deny" && b.effect !== "deny")
128
- return -1;
129
- if (b.effect === "deny" && a.effect !== "deny")
130
- return 1;
131
- return 0;
132
- });
133
- const top = sorted[0];
134
- return { allowed: top.effect === "allow", matchedRule: top, applicable };
95
+ const applicable = rules.filter((rule) => ruleBearsOn(rule, target) &&
96
+ (rule.condition === undefined || rule.effect === "deny"));
97
+ return combineApplicable(applicable, algorithm);
135
98
  }
136
99
  //# sourceMappingURL=rule.core.js.map
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Pattern matching shared by rules, grants, denies and policies.
3
+ *
4
+ * @module rule/rule.pattern
5
+ */
6
+ import type { Permission, PermissionRule } from "../permissionTypes/index.js";
7
+ /**
8
+ * Check if a single pattern string matches a target, supporting wildcards.
9
+ *
10
+ * Two forms are supported, and they are the same two the permission matcher
11
+ * supports, so a pattern means the same thing wherever it is written:
12
+ * `*` — matches anything
13
+ * `billing.*` — matches `billing` and any `billing.…` namespace
14
+ */
15
+ export declare function patternStrMatches(pattern: string, target: string): boolean;
16
+ /**
17
+ * Whether two segment patterns can name a common segment.
18
+ *
19
+ * For a concrete `target` this is exactly {@link patternStrMatches}. For a
20
+ * wildcard target it is what a *deny* has to test: `can(actor, "post:*")`
21
+ * asks about everything under `post`, so a deny of `post:delete` bears on it
22
+ * although `post:delete` does not match `post:*` as a pattern.
23
+ */
24
+ export declare function patternsOverlap(a: string, b: string): boolean;
25
+ /**
26
+ * Check if a rule matches a target permission.
27
+ *
28
+ * Matching is about resource and action only. Whether the rule *applies* also
29
+ * depends on its condition, which needs a context and is therefore evaluated
30
+ * by `evaluateRules`.
31
+ */
32
+ export declare function ruleMatches(rule: PermissionRule, target: Permission): boolean;
33
+ /** Check if a rule's patterns overlap a (possibly wildcard) target. */
34
+ export declare function ruleOverlaps(rule: PermissionRule, target: Permission): boolean;
35
+ /**
36
+ * Whether a rule bears on a target: allows by matching, denies by overlap.
37
+ *
38
+ * An allow must cover the whole target to grant it; a deny needs only to
39
+ * touch it. For a concrete target the two are the same test.
40
+ */
41
+ export declare function ruleBearsOn(rule: PermissionRule, target: Permission): boolean;
42
+ /** Whether a permission names more than one concrete permission. */
43
+ export declare function isWildcardTarget(target: Permission): boolean;
44
+ //# sourceMappingURL=rule.pattern.d.ts.map
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Pattern matching shared by rules, grants, denies and policies.
3
+ *
4
+ * @module rule/rule.pattern
5
+ */
6
+ /**
7
+ * Check if a single pattern string matches a target, supporting wildcards.
8
+ *
9
+ * Two forms are supported, and they are the same two the permission matcher
10
+ * supports, so a pattern means the same thing wherever it is written:
11
+ * `*` — matches anything
12
+ * `billing.*` — matches `billing` and any `billing.…` namespace
13
+ */
14
+ export function patternStrMatches(pattern, target) {
15
+ if (pattern === "*")
16
+ return true;
17
+ if (pattern === target)
18
+ return true;
19
+ if (pattern.endsWith(".*")) {
20
+ const prefix = pattern.slice(0, -2);
21
+ return target === prefix || target.startsWith(`${prefix}.`);
22
+ }
23
+ return false;
24
+ }
25
+ /**
26
+ * Whether two segment patterns can name a common segment.
27
+ *
28
+ * For a concrete `target` this is exactly {@link patternStrMatches}. For a
29
+ * wildcard target it is what a *deny* has to test: `can(actor, "post:*")`
30
+ * asks about everything under `post`, so a deny of `post:delete` bears on it
31
+ * although `post:delete` does not match `post:*` as a pattern.
32
+ */
33
+ export function patternsOverlap(a, b) {
34
+ return patternStrMatches(a, b) || patternStrMatches(b, a);
35
+ }
36
+ function patternList(pattern) {
37
+ return Array.isArray(pattern)
38
+ ? pattern
39
+ : [pattern];
40
+ }
41
+ /**
42
+ * Check if a rule matches a target permission.
43
+ *
44
+ * Matching is about resource and action only. Whether the rule *applies* also
45
+ * depends on its condition, which needs a context and is therefore evaluated
46
+ * by `evaluateRules`.
47
+ */
48
+ export function ruleMatches(rule, target) {
49
+ return (patternList(rule.resource).some((entry) => patternStrMatches(entry, target.resource)) &&
50
+ patternList(rule.action).some((entry) => patternStrMatches(entry, target.action)));
51
+ }
52
+ /** Check if a rule's patterns overlap a (possibly wildcard) target. */
53
+ export function ruleOverlaps(rule, target) {
54
+ return (patternList(rule.resource).some((entry) => patternsOverlap(entry, target.resource)) &&
55
+ patternList(rule.action).some((entry) => patternsOverlap(entry, target.action)));
56
+ }
57
+ /**
58
+ * Whether a rule bears on a target: allows by matching, denies by overlap.
59
+ *
60
+ * An allow must cover the whole target to grant it; a deny needs only to
61
+ * touch it. For a concrete target the two are the same test.
62
+ */
63
+ export function ruleBearsOn(rule, target) {
64
+ return rule.effect === "deny"
65
+ ? ruleOverlaps(rule, target)
66
+ : ruleMatches(rule, target);
67
+ }
68
+ /** Whether a permission names more than one concrete permission. */
69
+ export function isWildcardTarget(target) {
70
+ return target.resource.includes("*") || target.action.includes("*");
71
+ }
72
+ //# sourceMappingURL=rule.pattern.js.map
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Minimal change notifier for the live registries.
3
+ *
4
+ * @module utils/utils.notifier
5
+ */
6
+ /** A set of change listeners. */
7
+ export interface ChangeNotifier {
8
+ /** Register a listener. Returns an unsubscribe function. */
9
+ subscribe(listener: () => void): () => void;
10
+ /**
11
+ * Call every listener. All of them run even if one throws; the first
12
+ * error is rethrown afterwards, so one broken subscriber cannot stop an
13
+ * engine further down the list from dropping its stale decisions.
14
+ */
15
+ notify(): void;
16
+ }
17
+ /** Create a {@link ChangeNotifier}. */
18
+ export declare function createChangeNotifier(): ChangeNotifier;
19
+ //# sourceMappingURL=utils.notifier.d.ts.map
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Minimal change notifier for the live registries.
3
+ *
4
+ * @module utils/utils.notifier
5
+ */
6
+ /** Create a {@link ChangeNotifier}. */
7
+ export function createChangeNotifier() {
8
+ const listeners = new Set();
9
+ return {
10
+ subscribe(listener) {
11
+ listeners.add(listener);
12
+ return () => {
13
+ listeners.delete(listener);
14
+ };
15
+ },
16
+ notify() {
17
+ let failure;
18
+ for (const listener of [...listeners]) {
19
+ try {
20
+ listener();
21
+ }
22
+ catch (error) {
23
+ failure ??= { error };
24
+ }
25
+ }
26
+ if (failure)
27
+ throw failure.error;
28
+ },
29
+ };
30
+ }
31
+ //# sourceMappingURL=utils.notifier.js.map
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "@zudojs/permissions",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Generic authorization engine with RBAC, ABAC, resource authorization, wildcards, role hierarchy, policies, and abilities.",
5
5
  "license": "MIT",
6
+ "author": {
7
+ "name": "Oluwayemi Oyinlola",
8
+ "url": "https://github.com/oyinlola-tech"
9
+ },
6
10
  "type": "module",
7
11
  "main": "./dist/index.js",
8
12
  "module": "./dist/index.js",
@@ -22,15 +26,7 @@
22
26
  "!dist/.tsbuildinfo"
23
27
  ],
24
28
  "dependencies": {
25
- "@zudojs/errors": "1.0.0"
26
- },
27
- "peerDependencies": {
28
- "@zudojs/http": "1.0.0"
29
- },
30
- "peerDependenciesMeta": {
31
- "@zudojs/http": {
32
- "optional": true
33
- }
29
+ "@zudojs/errors": "1.1.0"
34
30
  },
35
31
  "engines": {
36
32
  "node": ">=24.0.0"