@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
@@ -1,6 +1,9 @@
1
1
  /**
2
2
  * Condition combinators for composing authorization policies.
3
3
  *
4
+ * Conditions are evaluated by the rule engine: attach one to a
5
+ * `PermissionRule` and the rule applies only when it returns `true`.
6
+ *
4
7
  * @module conditions/conditions
5
8
  */
6
9
  /**
@@ -45,6 +48,22 @@ export function always() {
45
48
  export function never() {
46
49
  return () => false;
47
50
  }
51
+ /** Compares two identifiers that may differ in type across a boundary. */
52
+ function sameId(left, right) {
53
+ if (left === undefined || left === null)
54
+ return false;
55
+ if (right === undefined || right === null)
56
+ return false;
57
+ if (typeof left === typeof right)
58
+ return left === right;
59
+ // A numeric id from a database row against a string id from a token is the
60
+ // common case, and a strict comparison silently denies.
61
+ if ((typeof left === "string" || typeof left === "number") &&
62
+ (typeof right === "string" || typeof right === "number")) {
63
+ return String(left) === String(right);
64
+ }
65
+ return false;
66
+ }
48
67
  /**
49
68
  * Check that the actor owns the resource.
50
69
  *
@@ -55,24 +74,45 @@ export function isOwner(ownerField = "ownerId") {
55
74
  if (!context.resource || typeof context.resource !== "object")
56
75
  return false;
57
76
  const resource = context.resource;
58
- const ownerId = resource[ownerField];
59
- return ownerId === context.actor.id;
77
+ return sameId(resource[ownerField], context.actor.id);
60
78
  };
61
79
  }
62
80
  /**
63
81
  * Enforce tenant isolation — actor and resource must share the same tenant.
64
82
  *
65
- * @param actorTenantField - Field on actor metadata holding tenant ID. Defaults to "tenantId".
66
- * @param resourceTenantField - Field on resource holding tenant ID. Defaults to "tenantId".
83
+ * The actor's tenant is read from `context.metadata`, which is supplied per
84
+ * check through `AuthorizationOptions.metadata`:
85
+ *
86
+ * ```ts
87
+ * engine.check(actor, "invoice:read", invoice, {
88
+ * metadata: { tenantId: request.tenantId },
89
+ * });
90
+ * ```
91
+ *
92
+ * @param actorTenantField - Key in the context metadata holding the actor's tenant. Defaults to "tenantId".
93
+ * @param resourceTenantField - Field on the resource holding the tenant ID. Defaults to "tenantId".
67
94
  */
68
95
  export function tenantIsolation(actorTenantField = "tenantId", resourceTenantField = "tenantId") {
69
96
  return (context) => {
70
97
  const actorTenant = context.metadata?.get(actorTenantField);
98
+ if (actorTenant === undefined || actorTenant === null)
99
+ return false;
71
100
  if (!context.resource || typeof context.resource !== "object")
72
101
  return false;
73
102
  const resource = context.resource;
74
- const resourceTenant = resource[resourceTenantField];
75
- return actorTenant !== undefined && actorTenant === resourceTenant;
103
+ return sameId(actorTenant, resource[resourceTenantField]);
104
+ };
105
+ }
106
+ /** Check that a value in the context metadata equals an expected value. */
107
+ export function metadataEquals(key, expected) {
108
+ return (context) => context.metadata?.get(key) === expected;
109
+ }
110
+ /** Check that the resource field matches the expected value. */
111
+ export function resourceEquals(field, expected) {
112
+ return (context) => {
113
+ if (!context.resource || typeof context.resource !== "object")
114
+ return false;
115
+ return context.resource[field] === expected;
76
116
  };
77
117
  }
78
118
  //# sourceMappingURL=conditions.core.js.map
@@ -3,5 +3,5 @@
3
3
  *
4
4
  * @module conditions
5
5
  */
6
- export { allOf, anyOf, not, always, never, isOwner, tenantIsolation, } from "./conditions.core.js";
6
+ export { allOf, anyOf, not, always, never, isOwner, tenantIsolation, metadataEquals, resourceEquals, } from "./conditions.core.js";
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -3,5 +3,5 @@
3
3
  *
4
4
  * @module conditions
5
5
  */
6
- export { allOf, anyOf, not, always, never, isOwner, tenantIsolation, } from "./conditions.core.js";
6
+ export { allOf, anyOf, not, always, never, isOwner, tenantIsolation, metadataEquals, resourceEquals, } from "./conditions.core.js";
7
7
  //# sourceMappingURL=index.js.map
@@ -3,17 +3,55 @@
3
3
  *
4
4
  * @module evaluator/authorizationEngine
5
5
  */
6
- import type { PermissionActor, PermissionDecision, ExplainResult, PermissionPolicyDefinition, AuthorizationOptions } from "../permissionTypes/index.js";
7
- import type { RoleDefinition } from "../permissionTypes/index.js";
6
+ import type { PermissionActor, PermissionDecision, ExplainResult, PermissionRule, PermissionPolicyDefinition, PermissionCache, PermissionResolver, RoleResolver, RoleDefinition, RuleCombiningAlgorithm, AuthorizationOptions } from "../permissionTypes/index.js";
8
7
  import { type Ability } from "../ability/ability.core.js";
8
+ import type { PermissionEventEmitter } from "../observability/observability.core.js";
9
+ /** Anything the engine will accept as its source of roles. */
10
+ export interface RoleSource {
11
+ get(name: string): RoleDefinition | undefined;
12
+ }
13
+ /** Anything the engine will accept as its source of policies. */
14
+ export interface PolicySource {
15
+ names(): readonly string[];
16
+ get(name: string): PermissionPolicyDefinition | undefined;
17
+ }
9
18
  /** Configuration for the permission engine. */
10
19
  export interface PermissionEngineOptions {
11
- /** Roles available to the engine. */
12
- readonly roles?: readonly RoleDefinition[];
13
- /** Policies to evaluate during authorization. */
14
- readonly policies?: readonly PermissionPolicyDefinition[];
20
+ /**
21
+ * Roles available to the engine — an array, or a registry created with
22
+ * `createRoleRegistry()`.
23
+ */
24
+ readonly roles?: readonly RoleDefinition[] | RoleSource;
25
+ /**
26
+ * Policies to evaluate during authorization — an array, or a registry
27
+ * created with `createPolicyRegistry()`.
28
+ */
29
+ readonly policies?: readonly PermissionPolicyDefinition[] | PolicySource;
30
+ /** Static rules evaluated alongside the actor's permissions. */
31
+ readonly rules?: readonly PermissionRule[];
15
32
  /** Default timeout for async policy evaluation (ms). */
16
33
  readonly policyTimeout?: number;
34
+ /** How competing rules combine. Default: `"deny-overrides"`. */
35
+ readonly algorithm?: RuleCombiningAlgorithm;
36
+ /** Caches decisions. Create one with `createMemoryPermissionCache()`. */
37
+ readonly cache?: PermissionCache;
38
+ /** Time-to-live for cached decisions, in ms. */
39
+ readonly cacheTtlMs?: number;
40
+ /** Loads additional rules for an actor from an external source. */
41
+ readonly permissionResolver?: PermissionResolver;
42
+ /** Loads additional roles for an actor from an external source. */
43
+ readonly roleResolver?: RoleResolver;
44
+ /** Expands a permission into the permissions it implies. */
45
+ readonly expandImplied?: (permission: string) => readonly string[];
46
+ /** Emits an event for every completed check, including failures. */
47
+ readonly emitter?: PermissionEventEmitter;
48
+ /** Reports a failure authorization swallowed to stay fail-closed. */
49
+ readonly onError?: (error: unknown, source: string) => void;
50
+ /**
51
+ * Rejects malformed configuration at construction instead of letting a
52
+ * grant that can never match sit in the role table. Default: `true`.
53
+ */
54
+ readonly validateConfiguration?: boolean;
17
55
  }
18
56
  /**
19
57
  * The public permission engine API.
@@ -39,6 +77,10 @@ export interface PermissionEngine {
39
77
  * Create a pre-resolved Ability for an actor.
40
78
  */
41
79
  createAbility(actor: PermissionActor): Ability;
80
+ /** Drop cached decisions for one actor. */
81
+ invalidateActor(actorId: string): Promise<void>;
82
+ /** Re-read the role source, discarding the memoized lookups. */
83
+ invalidateRoles(): void;
42
84
  }
43
85
  /**
44
86
  * Create a permission engine.
@@ -3,47 +3,176 @@
3
3
  *
4
4
  * @module evaluator/authorizationEngine
5
5
  */
6
- import { evaluate, evaluateWithExplain } from "./evaluator.core.js";
6
+ import { evaluate, evaluateWithTrace } from "./evaluator.core.js";
7
7
  import { createAbility } from "../ability/ability.core.js";
8
- import { PermissionDeniedError } from "../permissionErrors/index.js";
8
+ import { PermissionDeniedError, InvalidRoleError, } from "../permissionErrors/index.js";
9
+ import { isValidPermission } from "../permission/permission.core.js";
10
+ import { memoizeRoleLookup } from "../role/roleHierarchy.js";
11
+ function isRoleSource(roles) {
12
+ return (roles !== undefined &&
13
+ !Array.isArray(roles) &&
14
+ typeof roles.get === "function");
15
+ }
16
+ function isPolicySource(policies) {
17
+ return (policies !== undefined &&
18
+ !Array.isArray(policies) &&
19
+ typeof policies.names === "function");
20
+ }
21
+ /** Rejects a role whose grants could never match. */
22
+ function validateRole(role) {
23
+ if (!role.name || role.name.trim() === "") {
24
+ throw new InvalidRoleError("Role name cannot be empty");
25
+ }
26
+ for (const permission of role.permissions) {
27
+ if (!isValidPermission(permission)) {
28
+ // A malformed grant can never match, so it is a silent no-op unless
29
+ // it is rejected here.
30
+ throw new InvalidRoleError(`Role "${role.name}" grants "${permission}", which is not a valid ` +
31
+ `"resource:action" permission`);
32
+ }
33
+ }
34
+ }
35
+ function validateRoles(roles) {
36
+ const seen = new Set();
37
+ for (const role of roles) {
38
+ validateRole(role);
39
+ if (seen.has(role.name)) {
40
+ throw new InvalidRoleError(`Role "${role.name}" is defined more than once`);
41
+ }
42
+ seen.add(role.name);
43
+ }
44
+ }
9
45
  /**
10
46
  * Create a permission engine.
11
47
  */
12
48
  export function createPermissionEngine(options) {
13
- const roleMap = new Map();
14
- if (options?.roles) {
15
- for (const role of options.roles) {
16
- roleMap.set(role.name, role);
17
- }
49
+ const validateConfiguration = options?.validateConfiguration ?? true;
50
+ let roleLookup;
51
+ let invalidateRoleCache;
52
+ if (isRoleSource(options?.roles)) {
53
+ const source = options.roles;
54
+ // A role source is looked up lazily, so `validateConfiguration` has to be
55
+ // applied lazily too. Checking only the array form left a registry-backed
56
+ // engine accepting grants that can never match — the exact silent no-op
57
+ // the option exists to prevent.
58
+ const memo = memoizeRoleLookup((name) => {
59
+ const role = source.get(name);
60
+ if (role && validateConfiguration)
61
+ validateRole(role);
62
+ return role;
63
+ });
64
+ roleLookup = memo.lookup;
65
+ invalidateRoleCache = memo.invalidate;
18
66
  }
19
- const evaluatorOptions = {
20
- getRole: (name) => roleMap.get(name),
21
- policies: options?.policies,
22
- policyTimeout: options?.policyTimeout,
67
+ else {
68
+ const list = (options?.roles ?? []);
69
+ if (validateConfiguration)
70
+ validateRoles(list);
71
+ const roleMap = new Map(list.map((role) => [role.name, role]));
72
+ roleLookup = (name) => roleMap.get(name);
73
+ invalidateRoleCache = () => { };
74
+ }
75
+ const policySource = isPolicySource(options?.policies)
76
+ ? options.policies
77
+ : undefined;
78
+ const policyList = policySource
79
+ ? undefined
80
+ : (options?.policies ?? []);
81
+ const resolvePolicies = () => {
82
+ if (!policySource)
83
+ return policyList ?? [];
84
+ return policySource
85
+ .names()
86
+ .map((name) => policySource.get(name))
87
+ .filter((policy) => policy !== undefined);
23
88
  };
89
+ const evaluatorOptions = () => ({
90
+ getRole: roleLookup,
91
+ policies: resolvePolicies(),
92
+ rules: options?.rules,
93
+ policyTimeout: options?.policyTimeout,
94
+ algorithm: options?.algorithm,
95
+ cache: options?.cache,
96
+ cacheTtlMs: options?.cacheTtlMs,
97
+ permissionResolver: options?.permissionResolver,
98
+ roleResolver: options?.roleResolver,
99
+ expandImplied: options?.expandImplied,
100
+ onError: options?.onError,
101
+ });
102
+ const emitter = options?.emitter;
103
+ /**
104
+ * Runs a check and emits an audit event whichever way it ends — including
105
+ * when it throws. An authorization trail that records only the successful
106
+ * paths is not a trail.
107
+ */
108
+ async function runCheck(actor, permission, resource, authOptions) {
109
+ const start = performance.now();
110
+ let decision;
111
+ let failure;
112
+ try {
113
+ decision = await evaluate(actor, permission, resource, evaluatorOptions(), authOptions);
114
+ return decision;
115
+ }
116
+ catch (error) {
117
+ failure = error;
118
+ throw error;
119
+ }
120
+ finally {
121
+ emitter?.emit({
122
+ actorId: actor.id,
123
+ permission,
124
+ resourceType: resourceTypeOf(resource),
125
+ allowed: decision?.allowed ?? false,
126
+ reason: decision?.reason ??
127
+ (failure instanceof Error ? `error:${failure.name}` : undefined),
128
+ durationMs: performance.now() - start,
129
+ errored: failure !== undefined,
130
+ });
131
+ }
132
+ }
24
133
  return {
25
134
  async can(actor, permission, resource, authOptions) {
26
- const decision = await evaluate(actor, permission, resource, evaluatorOptions, authOptions);
135
+ const decision = await runCheck(actor, permission, resource, authOptions);
27
136
  return decision.allowed;
28
137
  },
29
138
  async check(actor, permission, resource, authOptions) {
30
- return evaluate(actor, permission, resource, evaluatorOptions, authOptions);
139
+ return runCheck(actor, permission, resource, authOptions);
31
140
  },
32
141
  async authorize(actor, permission, resource, authOptions) {
33
- const decision = await evaluate(actor, permission, resource, evaluatorOptions, authOptions);
142
+ const decision = await runCheck(actor, permission, resource, authOptions);
34
143
  if (!decision.allowed) {
35
- throw new PermissionDeniedError(decision.reason ?? "Access denied", {
144
+ // The public message names nothing internal; the reason and the
145
+ // policy travel in metadata for the log.
146
+ throw new PermissionDeniedError(decision.publicReason ?? "Access denied", {
36
147
  actorId: actor.id,
37
148
  permission,
149
+ reason: decision.reason,
150
+ policy: decision.policy,
38
151
  });
39
152
  }
40
153
  },
41
154
  async explain(actor, permission, resource, authOptions) {
42
- return evaluateWithExplain(actor, permission, resource, evaluatorOptions, authOptions);
155
+ return evaluateWithTrace(actor, permission, resource, evaluatorOptions(), authOptions);
43
156
  },
44
157
  createAbility(actor) {
45
- return createAbility(actor, evaluatorOptions);
158
+ return createAbility(actor, evaluatorOptions(), emitter);
159
+ },
160
+ async invalidateActor(actorId) {
161
+ await options?.cache?.invalidateActor(actorId);
162
+ },
163
+ invalidateRoles() {
164
+ invalidateRoleCache();
46
165
  },
47
166
  };
48
167
  }
168
+ /** Best-effort resource type for an audit event. */
169
+ function resourceTypeOf(resource) {
170
+ if (typeof resource !== "object" || resource === null)
171
+ return undefined;
172
+ const record = resource;
173
+ if (typeof record.type === "string")
174
+ return record.type;
175
+ const name = record.constructor?.name;
176
+ return name && name !== "Object" ? name : undefined;
177
+ }
49
178
  //# sourceMappingURL=authorizationEngine.js.map
@@ -1,11 +1,35 @@
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 type { PermissionActor, PermissionDecision, AuthorizationOptions } from "../permissionTypes/index.js";
5
- import type { EvaluatorOptions } from "./evaluator.pipeline.js";
11
+ import type { PermissionActor, PermissionDecision, ExplainStep, ExplainResult, AuthorizationOptions } from "../permissionTypes/index.js";
12
+ import { type EvaluatorOptions } from "./evaluator.pipeline.js";
6
13
  export { evaluateWithExplain } from "./evaluator.explain.js";
14
+ /** Collects the steps that explain a decision. */
15
+ export interface TraceCollector {
16
+ push(step: ExplainStep): void;
17
+ }
7
18
  /**
8
19
  * Evaluate a single permission check and return a decision.
20
+ *
21
+ * Every failure path denies. An unparsable permission, an unknown role, a
22
+ * policy that throws or times out — all of them produce a denial with a
23
+ * reason, never an exception that some outer handler might turn into a pass.
24
+ * The one exception is cancellation, which throws `AuthorizationAbortedError`
25
+ * because the caller asked for the work to stop.
9
26
  */
10
- export declare function evaluate(actor: PermissionActor, permissionStr: string, resource: unknown, options: EvaluatorOptions, authOptions?: AuthorizationOptions): Promise<PermissionDecision>;
27
+ export declare function evaluate(actor: PermissionActor, permissionStr: string, resource: unknown, options: EvaluatorOptions, authOptions?: AuthorizationOptions, trace?: TraceCollector): Promise<PermissionDecision>;
28
+ /**
29
+ * Evaluate and collect the trace, in one pass.
30
+ *
31
+ * `check` and `explain` share this, so a trace can never describe a decision
32
+ * other than the one that was made.
33
+ */
34
+ export declare function evaluateWithTrace(actor: PermissionActor, permissionStr: string, resource: unknown, options: EvaluatorOptions, authOptions?: AuthorizationOptions): Promise<ExplainResult>;
11
35
  //# sourceMappingURL=evaluator.core.d.ts.map