@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
@@ -1,57 +1,277 @@
1
1
  /**
2
- * Core permission evaluator — resolves actor permissions and evaluates rules + policies.
2
+ * Core permission evaluator.
3
+ *
4
+ * One evaluation path, with an optional trace collector. `check` and
5
+ * `explain` used to be ninety duplicated lines kept in sync by hand — in the
6
+ * one pair of functions that must agree, since `explain` exists to say why
7
+ * `check` decided what it did.
8
+ *
9
+ * @module evaluator/evaluator
3
10
  */
4
- import { parsePermission, matches } from "../permission/permission.core.js";
11
+ import { parsePermissionSafe, matches } from "../permission/permission.core.js";
12
+ import { InvalidPermissionError } from "../permissionErrors/index.js";
5
13
  import { compileRules, findMatchingRules } from "../rule/ruleCompiler.js";
6
14
  import { evaluateRules } from "../rule/rule.core.js";
7
- import { resolveActorPermissions, evaluatePolicies, } from "./evaluator.pipeline.js";
15
+ import { assertNotAborted, evaluatePolicies, resolveActorGrants, toMetadataMap, } from "./evaluator.pipeline.js";
16
+ import { permissionCacheKey } from "../cache/cache.core.js";
8
17
  export { evaluateWithExplain } from "./evaluator.explain.js";
18
+ /** A public message that names nothing internal. */
19
+ const PUBLIC_DENIED = "Access denied";
20
+ function denied(reason, extra) {
21
+ return Object.freeze({
22
+ allowed: false,
23
+ reason,
24
+ publicReason: PUBLIC_DENIED,
25
+ ...extra,
26
+ });
27
+ }
9
28
  /**
10
29
  * Evaluate a single permission check and return a decision.
30
+ *
31
+ * Every failure path denies. An unparsable permission, an unknown role, a
32
+ * policy that throws or times out — all of them produce a denial with a
33
+ * reason, never an exception that some outer handler might turn into a pass.
34
+ * The one exception is cancellation, which throws `AuthorizationAbortedError`
35
+ * because the caller asked for the work to stop.
11
36
  */
12
- export async function evaluate(actor, permissionStr, resource, options, authOptions) {
13
- const permission = parsePermission(permissionStr);
14
- if (actor.deniedPermissions) {
15
- for (const denied of actor.deniedPermissions) {
16
- if (denied === permissionStr ||
17
- denied === `${permission.resource}:*` ||
18
- denied === "*:*") {
19
- return Object.freeze({
20
- allowed: false,
21
- reason: "explicit_deny",
22
- matchedPermission: denied,
37
+ export async function evaluate(actor, permissionStr, resource, options, authOptions, trace) {
38
+ const signal = authOptions?.signal;
39
+ assertNotAborted(signal);
40
+ const permission = parsePermissionSafe(permissionStr);
41
+ if (!permission) {
42
+ trace?.push({
43
+ type: "deny",
44
+ detail: `Malformed permission: ${permissionStr}`,
45
+ matched: false,
46
+ });
47
+ return denied("invalid_permission");
48
+ }
49
+ /* ── Explicit denies ─────────────────────────────────────────────────── */
50
+ // Denies are checked before the cache is consulted. `deniedPermissions`
51
+ // travels on the actor object, which is per-request data the cache key
52
+ // knows nothing about, so a cached allow from an earlier call would
53
+ // otherwise be served to a caller who has since denied the permission.
54
+ for (const deny of actor.deniedPermissions ?? []) {
55
+ // Denies go through the same matcher as grants. Comparing them as exact
56
+ // strings meant `*:delete` was ignored while `*:delete` as a grant was
57
+ // honoured — an asymmetry a deny list cannot survive.
58
+ if (!parsePermissionSafe(deny)) {
59
+ // A malformed deny can never match, so it would silently grant what it
60
+ // was written to forbid. Report it rather than dropping it in silence.
61
+ options.onError?.(new InvalidPermissionError(deny), "Actor.deniedPermissions");
62
+ trace?.push({
63
+ type: "deny",
64
+ detail: `Malformed deny ignored: ${deny}`,
65
+ matched: false,
66
+ });
67
+ continue;
68
+ }
69
+ if (matches(deny, permissionStr)) {
70
+ trace?.push({
71
+ type: "deny",
72
+ detail: `Explicit deny: ${deny}`,
73
+ matched: true,
74
+ });
75
+ return denied("explicit_deny", { matchedPermission: deny });
76
+ }
77
+ }
78
+ /* ── Cache ───────────────────────────────────────────────────────────── */
79
+ const resourceId = authOptions?.resourceId ?? resourceIdOf(resource);
80
+ // Only a decision the key can fully describe may be cached.
81
+ //
82
+ // A resource with no derivable id is the sharp case: the key would collapse
83
+ // to `actor|permission`, so an allow for `{ ownerId: "ada" }` would answer
84
+ // for `{ ownerId: "bob" }` on the next call. Request metadata is the other:
85
+ // `tenantIsolation()` reads the tenant from it, and it is not part of the
86
+ // key, so a decision made for one tenant must not answer for another.
87
+ const keyable = (resource === undefined || resourceId !== undefined) &&
88
+ !hasMetadata(authOptions?.metadata);
89
+ const cacheKey = options.cache && authOptions?.skipCache !== true && keyable
90
+ ? permissionCacheKey(actor.id, permissionStr, resourceId)
91
+ : undefined;
92
+ if (options.cache && cacheKey) {
93
+ try {
94
+ const cached = await options.cache.get(cacheKey);
95
+ if (cached) {
96
+ trace?.push({
97
+ type: "cache",
98
+ detail: `Cache hit: ${cached.allowed ? "allow" : "deny"}`,
99
+ matched: cached.allowed,
23
100
  });
101
+ return cached;
24
102
  }
25
103
  }
104
+ catch (error) {
105
+ options.onError?.(error, "PermissionCache.get");
106
+ }
26
107
  }
27
- const allPermissions = resolveActorPermissions(actor, options);
28
- const matchingPermissions = allPermissions.filter((p) => matches(p, permissionStr));
29
- const rules = matchingPermissions.map((p) => {
30
- const parsed = parsePermission(p);
31
- return {
108
+ /* ── Grants ──────────────────────────────────────────────────────────── */
109
+ const grants = await resolveActorGrants(actor, options, signal);
110
+ assertNotAborted(signal);
111
+ for (const role of grants.unknownRoles) {
112
+ options.onError?.(new Error(`Unknown role "${role}" for actor "${actor.id}"`), "RoleHierarchy");
113
+ trace?.push({
114
+ type: "role",
115
+ detail: `Unknown role: ${role}`,
116
+ matched: false,
117
+ });
118
+ }
119
+ if (trace) {
120
+ for (const role of actor.roles ?? []) {
121
+ trace.push({
122
+ type: "role",
123
+ detail: `Role: ${role}`,
124
+ matched: !grants.unknownRoles.includes(role),
125
+ });
126
+ }
127
+ }
128
+ const matchedPermissions = grants.permissions.filter((granted) => matches(granted, permissionStr));
129
+ if (trace) {
130
+ // Only the grants that bear on this decision. Listing every permission
131
+ // the actor holds turns a trace into a dump of their whole grant set.
132
+ for (const granted of matchedPermissions) {
133
+ trace.push({
134
+ type: "permission",
135
+ detail: `Permission: ${granted}`,
136
+ matched: true,
137
+ });
138
+ }
139
+ if (matchedPermissions.length === 0) {
140
+ trace.push({
141
+ type: "permission",
142
+ detail: `No permission matched "${permissionStr}" among ${grants.permissions.length} granted`,
143
+ matched: false,
144
+ });
145
+ }
146
+ }
147
+ /* ── Rules ───────────────────────────────────────────────────────────── */
148
+ const context = {
149
+ actor,
150
+ permission,
151
+ resource,
152
+ metadata: toMetadataMap(authOptions?.metadata),
153
+ signal,
154
+ };
155
+ const rules = [
156
+ ...matchedPermissions.map((granted) => ({
32
157
  effect: "allow",
33
- action: parsed.action,
34
- resource: parsed.resource,
35
- };
158
+ resource: parsePermissionSafe(granted)?.resource ?? granted,
159
+ action: parsePermissionSafe(granted)?.action ?? "*",
160
+ })),
161
+ ...grants.rules,
162
+ ];
163
+ const ruleResult = await evaluateRules(findMatchingRules(compileRules(rules), permission), permission, context, {
164
+ algorithm: options.algorithm,
165
+ onConditionError: (rule, error) => {
166
+ options.onError?.(error, `RuleCondition.${rule.name ?? "unnamed"}`);
167
+ },
168
+ });
169
+ if (trace) {
170
+ for (const rule of ruleResult.applicable) {
171
+ trace.push({
172
+ type: "rule",
173
+ detail: `Rule ${rule.effect}: ${rule.name ?? `${String(rule.resource)}:${String(rule.action)}`}`,
174
+ matched: true,
175
+ });
176
+ }
177
+ }
178
+ /* ── Policies ────────────────────────────────────────────────────────── */
179
+ const outcome = await evaluatePolicies(context, options.policies ?? [], options, authOptions);
180
+ if (trace) {
181
+ for (const name of outcome.evaluated) {
182
+ trace.push({
183
+ type: "policy",
184
+ detail: `Policy: ${name}`,
185
+ matched: outcome.decision?.allowed ?? true,
186
+ });
187
+ }
188
+ }
189
+ const decision = combine(ruleResult, outcome.decision, permissionStr);
190
+ /* ── Cache write ─────────────────────────────────────────────────────── */
191
+ if (options.cache && cacheKey && outcome.cacheable) {
192
+ try {
193
+ await options.cache.set(cacheKey, decision, {
194
+ ttl: options.cacheTtlMs,
195
+ });
196
+ }
197
+ catch (error) {
198
+ options.onError?.(error, "PermissionCache.set");
199
+ }
200
+ }
201
+ return decision;
202
+ }
203
+ /**
204
+ * Combine the rule outcome with the policy outcome.
205
+ *
206
+ * A denying policy always wins. An allowing policy can grant access the rules
207
+ * did not, which is what makes a policy an ABAC escape hatch rather than a
208
+ * filter — but it can never override a denial, and that includes a denial
209
+ * the *rules* produced. "The rules did not allow" covers two cases: no rule
210
+ * matched, and a deny rule matched. Treating them alike let an allowing
211
+ * policy for `post:*` cancel a `deny post:update` rule — the exact inversion
212
+ * of `deny-overrides`.
213
+ */
214
+ function combine(ruleResult, policyDecision, permissionStr) {
215
+ if (policyDecision && !policyDecision.allowed)
216
+ return policyDecision;
217
+ const denyRule = !ruleResult.allowed && ruleResult.matchedRule?.effect === "deny"
218
+ ? ruleResult.matchedRule
219
+ : undefined;
220
+ if (denyRule) {
221
+ return denied("rule_deny", {
222
+ matchedPermission: permissionStr,
223
+ ...(denyRule.name ? { policy: denyRule.name } : {}),
224
+ });
225
+ }
226
+ if (ruleResult.allowed) {
227
+ return Object.freeze({
228
+ allowed: true,
229
+ reason: "role_permission",
230
+ matchedPermission: permissionStr,
231
+ ...(policyDecision?.policy ? { policy: policyDecision.policy } : {}),
232
+ });
233
+ }
234
+ if (policyDecision?.allowed)
235
+ return policyDecision;
236
+ return denied("no_matching_rule");
237
+ }
238
+ /** True when the caller supplied request metadata the cache key cannot carry. */
239
+ function hasMetadata(metadata) {
240
+ if (!metadata)
241
+ return false;
242
+ if (metadata instanceof Map)
243
+ return metadata.size > 0;
244
+ return Object.keys(metadata).length > 0;
245
+ }
246
+ /** Best-effort resource identity for the cache key. */
247
+ function resourceIdOf(resource) {
248
+ if (typeof resource !== "object" || resource === null)
249
+ return undefined;
250
+ const id = resource.id;
251
+ if (typeof id === "string")
252
+ return id;
253
+ if (typeof id === "number")
254
+ return String(id);
255
+ return undefined;
256
+ }
257
+ /**
258
+ * Evaluate and collect the trace, in one pass.
259
+ *
260
+ * `check` and `explain` share this, so a trace can never describe a decision
261
+ * other than the one that was made.
262
+ */
263
+ export async function evaluateWithTrace(actor, permissionStr, resource, options, authOptions) {
264
+ const steps = [];
265
+ const decision = await evaluate(actor, permissionStr, resource, options, authOptions, { push: (step) => steps.push(step) });
266
+ steps.push({
267
+ type: decision.allowed ? "permission" : "deny",
268
+ detail: `Decision: ${decision.allowed ? "allow" : "deny"} (${decision.reason ?? "unknown"})`,
269
+ matched: decision.allowed,
36
270
  });
37
- const compiled = compileRules(rules);
38
- const matching = findMatchingRules(compiled, permission);
39
- const ruleResult = evaluateRules(matching, permission);
40
- if (!ruleResult.allowed) {
41
- const ctx = { actor, permission, resource };
42
- const policyResult = await evaluatePolicies(ctx, options.policies ?? [], authOptions);
43
- if (policyResult !== null)
44
- return policyResult;
45
- return Object.freeze({ allowed: false, reason: "no_matching_rule" });
46
- }
47
- const ctx = { actor, permission, resource };
48
- const policyResult = await evaluatePolicies(ctx, options.policies ?? [], authOptions);
49
- if (policyResult !== null)
50
- return policyResult;
51
271
  return Object.freeze({
52
- allowed: true,
53
- reason: "role_permission",
54
- matchedPermission: permissionStr,
272
+ allowed: decision.allowed,
273
+ steps: Object.freeze(steps),
274
+ decision,
55
275
  });
56
276
  }
57
277
  //# sourceMappingURL=evaluator.core.js.map
@@ -1,12 +1,15 @@
1
1
  /**
2
- * Permission evaluator with full explain trace.
2
+ * Permission evaluator with a full explain trace.
3
3
  *
4
- * Records each evaluation step for debugging and auditing.
4
+ * A thin alias over {@link evaluateWithTrace}: the trace is produced by the
5
+ * same pass that produces the decision, so the two cannot disagree.
6
+ *
7
+ * @module evaluator/evaluator.explain
5
8
  */
6
9
  import type { PermissionActor, ExplainResult, AuthorizationOptions } from "../permissionTypes/index.js";
7
10
  import type { EvaluatorOptions } from "./evaluator.pipeline.js";
8
11
  /**
9
- * Evaluate with full explain trace.
12
+ * Evaluate with a full explain trace.
10
13
  */
11
14
  export declare function evaluateWithExplain(actor: PermissionActor, permissionStr: string, resource: unknown, options: EvaluatorOptions, authOptions?: AuthorizationOptions): Promise<ExplainResult>;
12
15
  //# sourceMappingURL=evaluator.explain.d.ts.map
@@ -1,79 +1,16 @@
1
1
  /**
2
- * Permission evaluator with full explain trace.
2
+ * Permission evaluator with a full explain trace.
3
3
  *
4
- * Records each evaluation step for debugging and auditing.
4
+ * A thin alias over {@link evaluateWithTrace}: the trace is produced by the
5
+ * same pass that produces the decision, so the two cannot disagree.
6
+ *
7
+ * @module evaluator/evaluator.explain
5
8
  */
6
- import { parsePermission, matches } from "../permission/permission.core.js";
7
- import { compileRules, findMatchingRules } from "../rule/ruleCompiler.js";
8
- import { evaluateRules } from "../rule/rule.core.js";
9
- import { resolveActorPermissions, evaluatePolicies, } from "./evaluator.pipeline.js";
9
+ import { evaluateWithTrace } from "./evaluator.core.js";
10
10
  /**
11
- * Evaluate with full explain trace.
11
+ * Evaluate with a full explain trace.
12
12
  */
13
13
  export async function evaluateWithExplain(actor, permissionStr, resource, options, authOptions) {
14
- const permission = parsePermission(permissionStr);
15
- const steps = [];
16
- if (actor.deniedPermissions) {
17
- for (const denied of actor.deniedPermissions) {
18
- if (denied === permissionStr ||
19
- denied === `${permission.resource}:*` ||
20
- denied === "*:*") {
21
- steps.push({
22
- type: "deny",
23
- detail: `Explicit deny: ${denied}`,
24
- matched: true,
25
- });
26
- return Object.freeze({ allowed: false, steps });
27
- }
28
- }
29
- }
30
- const allPermissions = resolveActorPermissions(actor, options);
31
- if (actor.roles) {
32
- for (const role of actor.roles) {
33
- steps.push({ type: "role", detail: `Role: ${role}`, matched: true });
34
- }
35
- }
36
- const matchingPermissions = allPermissions.filter((p) => matches(p, permissionStr));
37
- const rules = matchingPermissions.map((p) => {
38
- const parsed = parsePermission(p);
39
- return {
40
- effect: "allow",
41
- action: parsed.action,
42
- resource: parsed.resource,
43
- };
44
- });
45
- const compiled = compileRules(rules);
46
- const matching = findMatchingRules(compiled, permission);
47
- const ruleResult = evaluateRules(matching, permission);
48
- for (const perm of allPermissions) {
49
- const matched = matches(perm, permissionStr);
50
- steps.push({ type: "permission", detail: `Permission: ${perm}`, matched });
51
- }
52
- if (!ruleResult.allowed) {
53
- const ctx = { actor, permission, resource };
54
- const policyResult = await evaluatePolicies(ctx, options.policies ?? [], authOptions);
55
- if (policyResult !== null) {
56
- steps.push({
57
- type: "policy",
58
- detail: `Policy: ${policyResult.reason ?? "unknown"}`,
59
- matched: policyResult.allowed,
60
- });
61
- return Object.freeze({ allowed: policyResult.allowed, steps });
62
- }
63
- steps.push({ type: "deny", detail: "No matching rule", matched: false });
64
- return Object.freeze({ allowed: false, steps });
65
- }
66
- const ctx = { actor, permission, resource };
67
- const policyResult = await evaluatePolicies(ctx, options.policies ?? [], authOptions);
68
- if (policyResult !== null) {
69
- steps.push({
70
- type: "policy",
71
- detail: `Policy: ${policyResult.reason ?? "unknown"}`,
72
- matched: policyResult.allowed,
73
- });
74
- return Object.freeze({ allowed: policyResult.allowed, steps });
75
- }
76
- steps.push({ type: "permission", detail: "Rule matched", matched: true });
77
- return Object.freeze({ allowed: true, steps });
14
+ return evaluateWithTrace(actor, permissionStr, resource, options, authOptions);
78
15
  }
79
16
  //# sourceMappingURL=evaluator.explain.js.map
@@ -3,26 +3,87 @@
3
3
  *
4
4
  * @module evaluator/evaluator.pipeline
5
5
  */
6
- import type { PermissionActor, PermissionContext, PermissionDecision, PermissionPolicyDefinition, AuthorizationOptions } from "../permissionTypes/index.js";
6
+ import type { PermissionActor, PermissionContext, PermissionDecision, PermissionPolicyDefinition, PermissionRule, PermissionCache, PermissionResolver, RoleDefinition, RoleResolver, RuleCombiningAlgorithm, AuthorizationOptions } from "../permissionTypes/index.js";
7
7
  /** Configuration for the evaluator. */
8
8
  export interface EvaluatorOptions {
9
9
  /** Function to look up a role definition by name. */
10
- readonly getRole?: (name: string) => {
11
- readonly name: string;
12
- readonly permissions: readonly string[];
13
- readonly inherits?: readonly string[];
14
- } | undefined;
10
+ readonly getRole?: (name: string) => RoleDefinition | undefined;
15
11
  /** Registered policies. */
16
12
  readonly policies?: readonly PermissionPolicyDefinition[];
13
+ /** Static rules, evaluated alongside the actor's permissions. */
14
+ readonly rules?: readonly PermissionRule[];
17
15
  /** Default timeout for async policy evaluation (ms). */
18
16
  readonly policyTimeout?: number;
17
+ /** How competing rules combine. Default: `"deny-overrides"`. */
18
+ readonly algorithm?: RuleCombiningAlgorithm;
19
+ /** Decision cache. */
20
+ readonly cache?: PermissionCache;
21
+ /** Time-to-live for cached decisions, in ms. */
22
+ readonly cacheTtlMs?: number;
23
+ /** Loads additional rules for an actor from an external source. */
24
+ readonly permissionResolver?: PermissionResolver;
25
+ /** Loads additional roles for an actor from an external source. */
26
+ readonly roleResolver?: RoleResolver;
27
+ /**
28
+ * Expands a permission into the ones it implies, so that granting
29
+ * `post:admin` can carry `post:read` with it.
30
+ */
31
+ readonly expandImplied?: (permission: string) => readonly string[];
32
+ /** Reports a failure that authorization swallowed to stay fail-closed. */
33
+ readonly onError?: (error: unknown, source: string) => void;
19
34
  }
35
+ /** The permissions and rules an actor holds, once everything is resolved. */
36
+ export interface ResolvedGrants {
37
+ readonly permissions: readonly string[];
38
+ readonly rules: readonly PermissionRule[];
39
+ /** Roles that could not be resolved. Their permissions are absent. */
40
+ readonly unknownRoles: readonly string[];
41
+ }
42
+ /** Throws if the caller has cancelled the check. */
43
+ export declare function assertNotAborted(signal?: AbortSignal): void;
44
+ /** Normalizes caller-supplied metadata into the map conditions read. */
45
+ export declare function toMetadataMap(metadata: AuthorizationOptions["metadata"]): ReadonlyMap<string, unknown> | undefined;
20
46
  /**
21
- * Resolve all permissions for an actor (direct + role-based).
47
+ * Resolve everything an actor holds: direct permissions, role permissions
48
+ * (following inheritance), resolver-supplied roles and rules.
49
+ *
50
+ * A role the lookup cannot find is reported rather than thrown. An unknown
51
+ * role in a token is a data problem, and turning every request from that
52
+ * actor into an exception hands the decision to whichever error handler
53
+ * happens to be installed — which may well be more permissive than a denial.
54
+ */
55
+ export declare function resolveActorGrants(actor: PermissionActor, options: EvaluatorOptions, signal?: AbortSignal): Promise<ResolvedGrants>;
56
+ /**
57
+ * Resolve all permission strings for an actor (direct + role-based).
58
+ *
59
+ * Kept for callers that only need the strings; the engine uses
60
+ * {@link resolveActorGrants}, which also carries rules and unknown roles.
22
61
  */
23
62
  export declare function resolveActorPermissions(actor: PermissionActor, options: EvaluatorOptions): readonly string[];
63
+ /** Policies that apply to a permission, most important first. */
64
+ export declare function selectPolicies(policies: readonly PermissionPolicyDefinition[], permissionStr: string): readonly PermissionPolicyDefinition[];
65
+ /** The outcome of running the applicable policies. */
66
+ export interface PolicyOutcome {
67
+ /** `null` when no policy applied. */
68
+ readonly decision: PermissionDecision | null;
69
+ /** Whether every policy that ran allows the result to be cached. */
70
+ readonly cacheable: boolean;
71
+ readonly evaluated: readonly string[];
72
+ }
24
73
  /**
25
- * Evaluate policies for a context. Returns null if no policy matched.
74
+ * Evaluate policies for a context.
75
+ *
76
+ * Policies are evaluated highest priority first and short-circuit on the
77
+ * first denial. A policy that throws or times out denies — an authorization
78
+ * check that cannot complete must not fall through to "allowed".
79
+ */
80
+ export declare function evaluatePolicies(context: PermissionContext, policies: readonly PermissionPolicyDefinition[], options: EvaluatorOptions, authOptions?: AuthorizationOptions): Promise<PolicyOutcome>;
81
+ /**
82
+ * Run a promise with an optional timeout.
83
+ *
84
+ * `0` means "expire immediately", not "no timeout" — treating a falsy value
85
+ * as "disabled" silently turned `policyTimeout: 0` into no timeout at all.
86
+ * Pass `undefined` to disable.
26
87
  */
27
- export declare function evaluatePolicies(context: PermissionContext, policies: readonly PermissionPolicyDefinition[], authOptions?: AuthorizationOptions): Promise<PermissionDecision | null>;
88
+ export declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number | undefined, policyName?: string): Promise<T>;
28
89
  //# sourceMappingURL=evaluator.pipeline.d.ts.map