@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
package/README.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  Generic authorization engine with RBAC, ABAC, resource authorization, wildcards, role hierarchy, policies, and abilities.
4
4
 
5
+ <!-- zudo-docs:start -->
6
+
7
+ **Documentation:** [zudojs.oyinlola.site/docs/packages-permissions](https://zudojs.oyinlola.site/docs/packages-permissions) · **For AI agents:** [Markdown version](https://zudojs.oyinlola.site/docs/packages-permissions.md), [llms.txt](https://zudojs.oyinlola.site/llms.txt)
8
+
9
+ <!-- zudo-docs:end -->
10
+
5
11
  ## Installation
6
12
 
7
13
  ```bash
@@ -102,8 +108,19 @@ await engine.can(actor, "post:update", { ownerId: "user_1" }); // true
102
108
  await engine.can(actor, "post:update", { ownerId: "user_9" }); // false
103
109
  ```
104
110
 
105
- Conditions may be async. A condition that throws is treated as unmet — an
106
- authorization check fails closed.
111
+ Conditions may be async. A condition that cannot be evaluated fails closed,
112
+ and what "closed" means depends on the rule's effect:
113
+
114
+ - an **allow** whose condition throws (or that has a condition but no
115
+ context) does not apply;
116
+ - a **deny** whose condition throws **applies**. In the example above,
117
+ `locked-posts` throws when no resource is passed, and the check is denied —
118
+ treating the deny as unmet would let the role's allow win. The error is
119
+ reported through `onError`, and the decision is not cached.
120
+
121
+ A deny also bears on a wildcard check: `can(actor, "post:*")` is refused by a
122
+ deny on `post:delete`, whether it comes from `deniedPermissions`, a deny rule,
123
+ or a policy registered for `post:delete`.
107
124
 
108
125
  **Combining.** The default is `deny-overrides`: any applicable deny wins,
109
126
  whatever its priority. Pass `algorithm: "priority"` for highest-priority-wins,
@@ -118,13 +135,19 @@ Conditions read request-scoped facts from `context.metadata`, supplied per
118
135
  check:
119
136
 
120
137
  ```typescript
138
+ import { requireCurrentTenant } from "@zudojs/tenancy";
139
+
121
140
  await engine.can(actor, "invoice:read", invoice, {
122
- metadata: { tenantId: request.tenantId },
141
+ // The *verified* tenant — resolved and trust-checked by @zudojs/tenancy.
142
+ metadata: { tenantId: requireCurrentTenant().id },
123
143
  });
124
144
  ```
125
145
 
126
146
  `tenantIsolation()` compares that value against the resource's tenant, and
127
- denies when either is missing.
147
+ denies when either is missing. It is only as good as the value you pass: fill
148
+ `tenantId` from a source the client cannot choose (a verified token claim, or
149
+ the tenant `@zudojs/tenancy` resolved), **never from a request header** — a
150
+ caller would set the header to the resource's tenant and pass.
128
151
 
129
152
  ## Policies
130
153
 
@@ -151,8 +174,8 @@ const engine = createPermissionEngine({
151
174
 
152
175
  Policies run highest priority first and stop at the first denial. A policy
153
176
  that throws, or exceeds `policyTimeout`, denies. A denying policy always wins;
154
- an allowing one can grant access the rules did not, but never overrides a
155
- denial.
177
+ an allowing one can grant access the rules did not decide, but never overrides
178
+ a denial — from another policy or from a deny rule that applied.
156
179
 
157
180
  `policyTimeout: 0` means "expire immediately", not "no timeout" — omit it to
158
181
  disable.
@@ -176,7 +199,11 @@ const policies = createPolicyRegistry();
176
199
  const engine = createPermissionEngine({ roles, policies });
177
200
 
178
201
  roles.define({ name: "auditor", permissions: ["audit:read"] });
179
- engine.invalidateRoles(); // pick up the change
202
+ roles.remove("reader");
203
+ // both take effect on the next check: the engine subscribes to the registry
204
+
205
+ policies.define({ name: "lockdown", permissions: ["*:*"], evaluate: () => ({ allowed: false }) });
206
+ // enforced by the next check — through the engine or an existing Ability
180
207
 
181
208
  roles.require("auditor"); // throws RoleNotFoundError when unregistered
182
209
  ```
@@ -185,9 +212,17 @@ All three registries reject a duplicate name — re-registering a role or a
185
212
  policy is an authorization rule disappearing without a trace. Pass
186
213
  `{ allowOverride: true }` when replacement is what you mean.
187
214
 
188
- `validateConfiguration` (default `true`) applies to a registry as well as to
189
- an inline array: a role whose grant could never match is rejected when the
190
- engine looks it up, and the check denies.
215
+ The engine subscribes to a role or policy registry it is given. Every
216
+ `define`, `remove` and `clear` discards the memoized roles **and every cached
217
+ decision**, so revoking a role is not undone by a cache entry written before
218
+ the revocation. `engine.invalidateRoles()` does the same for a custom source.
219
+
220
+ `validateConfiguration` (default `true`) rejects a pattern that could never
221
+ match wherever it is written — a role grant, a role's rules, a static rule, or
222
+ a policy's `permissions` — because a malformed pattern in a deny or a lockdown
223
+ policy would fail open in silence. Arrays are checked at construction; a
224
+ registry checks on `define` (`InvalidRoleError` / `InvalidPermissionError`),
225
+ and a custom source is checked when the engine reads it.
191
226
 
192
227
  A permission registry records descriptions and implications:
193
228
 
@@ -223,9 +258,13 @@ await engine.can(actor, "post:read", post, { skipCache: true });
223
258
  await engine.invalidateActor("user_1");
224
259
  ```
225
260
 
226
- Keys include the actor, the permission and the resource id (from
227
- `resource.id`, or `options.resourceId`), so two resources never share one
228
- decision.
261
+ Keys include the actor id, a digest of everything else the actor carries
262
+ (`roles`, `permissions`, `type`, any other field a condition may read), the
263
+ permission and the resource id (from `resource.id`, or `options.resourceId`).
264
+ The same user id with different roles — an admin token in one tenant and a
265
+ viewer token in another, or a demoted token — never shares a decision. A `|`
266
+ inside an actor or resource id is escaped, so two different checks can never
267
+ share a key either.
229
268
 
230
269
  A check is cached only when the key can describe it completely:
231
270
 
@@ -235,7 +274,10 @@ A check is cached only when the key can describe it completely:
235
274
  - a check carrying `metadata` is **not cached**, because conditions such as
236
275
  `tenantIsolation()` read the tenant from there and it is not part of the
237
276
  key;
238
- - a decision produced by a policy marked `cacheable: false` is not stored;
277
+ - an actor carrying something the digest cannot describe (a function, a class
278
+ instance, a `Map`) is **not cached**;
279
+ - a decision produced by a policy marked `cacheable: false`, or forced by a
280
+ condition that threw, is not stored;
239
281
  - a TTL of `0` or less means "do not cache".
240
282
 
241
283
  `deniedPermissions` is evaluated before the cache is consulted, so a deny
@@ -264,7 +306,10 @@ Every failure denies:
264
306
  | --------------------------- | ------------------------------------------------------ |
265
307
  | Unknown role on the actor | denied; reported to `onError`; other roles still apply |
266
308
  | Malformed permission string | denied, `reason: "invalid_permission"` |
267
- | Condition throws | rule does not apply |
309
+ | Allow condition throws | the allow does not apply |
310
+ | Deny condition throws | denied, `reason: "rule_deny"`; reported to `onError` |
311
+ | Malformed rule/policy pattern | rejected at construction or `define` |
312
+ | A deny rule applies | denied, `reason: "rule_deny"`, even if a policy allows |
268
313
  | Policy throws or times out | denied, `reason: "policy_error:<name>"` |
269
314
  | Role inheritance cycle | denied; reported to `onError` |
270
315
  | Role source throws | denied; reported to `onError` |
@@ -319,8 +364,10 @@ emitter.on((event) => auditLog.write(event));
319
364
  const engine = createPermissionEngine({ roles, emitter });
320
365
  ```
321
366
 
322
- Every check emits — allowed, denied, and the ones that throw. A handler that
323
- throws cannot break authorization, but it is reported rather than swallowed.
367
+ Every check emits — allowed, denied, and the ones that throw — including
368
+ `explain()` on the engine and on an Ability, which make the same real
369
+ decision. A handler that throws cannot break authorization, but it is reported
370
+ rather than swallowed.
324
371
 
325
372
  ## HTTP middleware
326
373
 
@@ -329,10 +376,13 @@ import { authorize, createActorMiddleware } from "@zudojs/permissions";
329
376
 
330
377
  const guard = authorize(engine, "post:update", {
331
378
  extractActor: (context) => context.state.get("auth:user"),
332
- extractResource: (context) => loadPost(context.request.params.get("id")),
379
+ // May be async: it is awaited, and a loader that rejects denies (403).
380
+ extractResource: (context) => loadPost(context.request.getParam?.("id")),
381
+ // The tenant @zudojs/tenancy resolved and trust-checked — never a header.
333
382
  extractMetadata: (context) => ({
334
- tenantId: context.request.headers.get("x-tenant"),
383
+ tenantId: context.state.get<{ tenantId: string }>("tenancy:context")?.tenantId,
335
384
  }),
385
+ onError: (error, source) => logger.warn({ error, source }, "guard denied"),
336
386
  });
337
387
  ```
338
388
 
@@ -346,11 +396,16 @@ const guard = authorize(engine, "post:update", {
346
396
  decision.
347
397
  - `context.signal` is forwarded, so a client disconnect stops policy
348
398
  evaluation.
399
+ - `extractResource` is awaited. A loader that throws or rejects answers
400
+ **403** (`reason: "resource_error"`) and reports through `onError`; it never
401
+ lets the request through.
349
402
  - `createRequirePermissionsMiddleware(engine, permissions, { mode })` checks
350
403
  several permissions, short-circuiting on the first that decides the outcome.
404
+ An empty list denies in either mode.
351
405
 
352
- `@zudojs/http` is an optional peer dependency; the middleware types are
353
- mirrored locally so this package works without it.
406
+ The middleware composes with the real `@zudojs/http` pipeline without
407
+ depending on it: the HTTP types are mirrored structurally (headers, params and
408
+ query may be plain objects, as `@zudojs/http` provides them, or maps).
354
409
 
355
410
  ## Errors
356
411
 
@@ -5,33 +5,13 @@
5
5
  */
6
6
  import { evaluate, evaluateWithTrace } from "../evaluator/evaluator.core.js";
7
7
  import { PermissionDeniedError } from "../permissionErrors/index.js";
8
+ import { observed } from "../evaluator/engineSupport/index.js";
8
9
  /**
9
10
  * Create an Ability for an actor.
10
11
  */
11
12
  export function createAbility(actor, evaluatorOptions, emitter) {
12
- async function run(permission, resource, options) {
13
- const start = performance.now();
14
- let decision;
15
- let failure;
16
- try {
17
- decision = await evaluate(actor, permission, resource, evaluatorOptions, options);
18
- return decision;
19
- }
20
- catch (error) {
21
- failure = error;
22
- throw error;
23
- }
24
- finally {
25
- emitter?.emit({
26
- actorId: actor.id,
27
- permission,
28
- allowed: decision?.allowed ?? false,
29
- reason: decision?.reason ??
30
- (failure instanceof Error ? `error:${failure.name}` : undefined),
31
- durationMs: performance.now() - start,
32
- errored: failure !== undefined,
33
- });
34
- }
13
+ function run(permission, resource, options) {
14
+ return observed(emitter, actor, permission, resource, () => evaluate(actor, permission, resource, evaluatorOptions, options), (decision) => decision);
35
15
  }
36
16
  return {
37
17
  actor,
@@ -45,7 +25,7 @@ export function createAbility(actor, evaluatorOptions, emitter) {
45
25
  return run(permission, resource, options);
46
26
  },
47
27
  async explain(permission, resource, options) {
48
- return evaluateWithTrace(actor, permission, resource, evaluatorOptions, options);
28
+ return observed(emitter, actor, permission, resource, () => evaluateWithTrace(actor, permission, resource, evaluatorOptions, options), (result) => result.decision);
49
29
  },
50
30
  async authorize(permission, resource, options) {
51
31
  const decision = await run(permission, resource, options);
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Actor digest for the decision cache key.
3
+ *
4
+ * A decision depends on everything the actor carries, not only its id:
5
+ * `roles`, `permissions` and `type` travel on the actor object for each
6
+ * request (a tenant-scoped token, `auth.checkAccess`), and a condition may
7
+ * read any other field on it. Keying on the id alone let an admin decision
8
+ * made in one tenant answer for the same user as a viewer in another, and a
9
+ * demoted token keep its old grants until the entry expired.
10
+ *
11
+ * @module cache/cache.actorDigest
12
+ */
13
+ import type { PermissionActor } from "../permissionTypes/index.js";
14
+ /** Longest digest worth keying on; a longer one is not cached at all. */
15
+ export declare const MAX_ACTOR_DIGEST_LENGTH = 4096;
16
+ /**
17
+ * A stable description of everything an actor carries besides its id.
18
+ *
19
+ * Returns `undefined` when the actor holds something the digest cannot
20
+ * describe faithfully — a function, a class instance, a `Map`, nesting past
21
+ * six levels, or a digest longer than {@link MAX_ACTOR_DIGEST_LENGTH}. The
22
+ * engine does not cache such a decision rather than key it on a partial
23
+ * description.
24
+ */
25
+ export declare function actorCacheDigest(actor: PermissionActor): string | undefined;
26
+ //# sourceMappingURL=cache.actorDigest.d.ts.map
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Actor digest for the decision cache key.
3
+ *
4
+ * A decision depends on everything the actor carries, not only its id:
5
+ * `roles`, `permissions` and `type` travel on the actor object for each
6
+ * request (a tenant-scoped token, `auth.checkAccess`), and a condition may
7
+ * read any other field on it. Keying on the id alone let an admin decision
8
+ * made in one tenant answer for the same user as a viewer in another, and a
9
+ * demoted token keep its old grants until the entry expired.
10
+ *
11
+ * @module cache/cache.actorDigest
12
+ */
13
+ /** Longest digest worth keying on; a longer one is not cached at all. */
14
+ export const MAX_ACTOR_DIGEST_LENGTH = 4096;
15
+ /** Deepest nesting the digest will describe. */
16
+ const MAX_DEPTH = 6;
17
+ /** Marks a value the digest cannot describe faithfully. */
18
+ const UNDESCRIBABLE = Symbol("undescribable");
19
+ function isPlainRecord(value) {
20
+ const proto = Object.getPrototypeOf(value);
21
+ return proto === Object.prototype || proto === null;
22
+ }
23
+ function canonical(value, depth) {
24
+ if (depth > MAX_DEPTH)
25
+ return UNDESCRIBABLE;
26
+ if (value === null)
27
+ return "null";
28
+ switch (typeof value) {
29
+ case "string":
30
+ case "boolean":
31
+ return JSON.stringify(value);
32
+ case "number":
33
+ return Number.isFinite(value) ? JSON.stringify(value) : UNDESCRIBABLE;
34
+ case "object":
35
+ break;
36
+ default:
37
+ return UNDESCRIBABLE;
38
+ }
39
+ const object = value;
40
+ if (object instanceof Date) {
41
+ const time = object.getTime();
42
+ return Number.isNaN(time) ? UNDESCRIBABLE : `D${JSON.stringify(time)}`;
43
+ }
44
+ if (Array.isArray(object)) {
45
+ const parts = [];
46
+ for (const entry of object) {
47
+ const part = canonical(entry, depth + 1);
48
+ if (part === UNDESCRIBABLE)
49
+ return UNDESCRIBABLE;
50
+ parts.push(part);
51
+ }
52
+ return `[${parts.join(",")}]`;
53
+ }
54
+ if (!isPlainRecord(object))
55
+ return UNDESCRIBABLE;
56
+ return canonicalRecord(object, depth, []);
57
+ }
58
+ function canonicalRecord(record, depth, skip) {
59
+ const parts = [];
60
+ for (const key of Object.keys(record).sort()) {
61
+ if (skip.includes(key))
62
+ continue;
63
+ const entry = record[key];
64
+ if (entry === undefined)
65
+ continue;
66
+ const part = canonical(entry, depth + 1);
67
+ if (part === UNDESCRIBABLE)
68
+ return UNDESCRIBABLE;
69
+ parts.push(`${JSON.stringify(key)}:${part}`);
70
+ }
71
+ return `{${parts.join(",")}}`;
72
+ }
73
+ /**
74
+ * A stable description of everything an actor carries besides its id.
75
+ *
76
+ * Returns `undefined` when the actor holds something the digest cannot
77
+ * describe faithfully — a function, a class instance, a `Map`, nesting past
78
+ * six levels, or a digest longer than {@link MAX_ACTOR_DIGEST_LENGTH}. The
79
+ * engine does not cache such a decision rather than key it on a partial
80
+ * description.
81
+ */
82
+ export function actorCacheDigest(actor) {
83
+ if (typeof actor !== "object" || actor === null)
84
+ return undefined;
85
+ const digest = canonicalRecord(actor, 0, ["id"]);
86
+ if (digest === UNDESCRIBABLE)
87
+ return undefined;
88
+ return digest.length > MAX_ACTOR_DIGEST_LENGTH ? undefined : digest;
89
+ }
90
+ //# sourceMappingURL=cache.actorDigest.js.map
@@ -8,9 +8,16 @@ import type { PermissionCache } from "../permissionTypes/index.js";
8
8
  * Generate a cache key for a permission check.
9
9
  *
10
10
  * The actor id is delimited, so invalidating actor `1` cannot also clear
11
- * actors `10` and `123`.
11
+ * actors `10` and `123`, and a delimiter inside an id is escaped so two
12
+ * different (actor, permission, resource) triples can never share a key.
13
+ *
14
+ * `scope` carries whatever else the decision depends on — the engine passes
15
+ * a digest of the actor's roles, permissions and attributes, so the same id
16
+ * holding different grants never shares a key. It sits after the actor
17
+ * prefix behind a `~`, which no permission string can start with, so
18
+ * `invalidateActor` still finds every entry for the actor.
12
19
  */
13
- export declare function permissionCacheKey(actorId: string, permission: string, resourceId?: string): string;
20
+ export declare function permissionCacheKey(actorId: string, permission: string, resourceId?: string, scope?: string): string;
14
21
  /** Options for {@link createMemoryPermissionCache}. */
15
22
  export interface MemoryPermissionCacheOptions {
16
23
  /** Default TTL in milliseconds. Defaults to 60,000 (1 minute). */
@@ -5,19 +5,41 @@
5
5
  */
6
6
  /** Separates the actor id from the rest of the key. */
7
7
  const KEY_DELIMITER = "|";
8
+ /**
9
+ * Escape a caller-supplied segment so it cannot forge a delimiter.
10
+ *
11
+ * Actor and resource ids are opaque strings and may contain `|`. Without
12
+ * escaping, actor `u|post:read` checking `x:y` produced the same key as
13
+ * actor `u` checking `post:read` on resource `x:y` — one actor's cached
14
+ * decision answering for another. Ids without `|` or `\` are unchanged, so
15
+ * existing keys keep their shape.
16
+ */
17
+ function escapeSegment(segment) {
18
+ return segment.replace(/[\\|]/g, (char) => `\\${char}`);
19
+ }
8
20
  /** Prefix identifying an actor's cache entries. */
9
21
  function actorPrefix(actorId) {
10
- return `actor:${actorId}${KEY_DELIMITER}`;
22
+ return `actor:${escapeSegment(actorId)}${KEY_DELIMITER}`;
11
23
  }
12
24
  /**
13
25
  * Generate a cache key for a permission check.
14
26
  *
15
27
  * The actor id is delimited, so invalidating actor `1` cannot also clear
16
- * actors `10` and `123`.
28
+ * actors `10` and `123`, and a delimiter inside an id is escaped so two
29
+ * different (actor, permission, resource) triples can never share a key.
30
+ *
31
+ * `scope` carries whatever else the decision depends on — the engine passes
32
+ * a digest of the actor's roles, permissions and attributes, so the same id
33
+ * holding different grants never shares a key. It sits after the actor
34
+ * prefix behind a `~`, which no permission string can start with, so
35
+ * `invalidateActor` still finds every entry for the actor.
17
36
  */
18
- export function permissionCacheKey(actorId, permission, resourceId) {
19
- const base = `${actorPrefix(actorId)}${permission}`;
20
- return resourceId ? `${base}${KEY_DELIMITER}${resourceId}` : base;
37
+ export function permissionCacheKey(actorId, permission, resourceId, scope) {
38
+ const scoped = scope === undefined ? "" : `~${escapeSegment(scope)}${KEY_DELIMITER}`;
39
+ const base = `${actorPrefix(actorId)}${scoped}${permission}`;
40
+ return resourceId
41
+ ? `${base}${KEY_DELIMITER}${escapeSegment(resourceId)}`
42
+ : base;
21
43
  }
22
44
  const DEFAULT_TTL_MS = 60_000;
23
45
  const DEFAULT_MAX_ENTRIES = 10_000;
@@ -4,4 +4,5 @@
4
4
  * @module cache
5
5
  */
6
6
  export { createMemoryPermissionCache, permissionCacheKey, type MemoryPermissionCacheOptions, } from "./cache.core.js";
7
+ export { actorCacheDigest, MAX_ACTOR_DIGEST_LENGTH, } from "./cache.actorDigest.js";
7
8
  //# sourceMappingURL=index.d.ts.map
@@ -4,4 +4,5 @@
4
4
  * @module cache
5
5
  */
6
6
  export { createMemoryPermissionCache, permissionCacheKey, } from "./cache.core.js";
7
+ export { actorCacheDigest, MAX_ACTOR_DIGEST_LENGTH, } from "./cache.actorDigest.js";
7
8
  //# sourceMappingURL=index.js.map
@@ -6,14 +6,27 @@
6
6
  import type { PermissionActor, PermissionDecision, ExplainResult, PermissionRule, PermissionPolicyDefinition, PermissionCache, PermissionResolver, RoleResolver, RoleDefinition, RuleCombiningAlgorithm, AuthorizationOptions } from "../permissionTypes/index.js";
7
7
  import { type Ability } from "../ability/ability.core.js";
8
8
  import type { PermissionEventEmitter } from "../observability/observability.core.js";
9
- /** Anything the engine will accept as its source of roles. */
9
+ /**
10
+ * Anything the engine will accept as its source of roles.
11
+ *
12
+ * A source with `subscribe` (every `createRoleRegistry()`) is watched: a
13
+ * `define`, `remove` or `clear` discards the engine's memoized roles and
14
+ * every cached decision, so revoking a role takes effect on the next check.
15
+ */
10
16
  export interface RoleSource {
11
17
  get(name: string): RoleDefinition | undefined;
18
+ subscribe?(listener: () => void): () => void;
12
19
  }
13
- /** Anything the engine will accept as its source of policies. */
20
+ /**
21
+ * Anything the engine will accept as its source of policies.
22
+ *
23
+ * A source with `subscribe` (every `createPolicyRegistry()`) is watched the
24
+ * same way, so a policy added or removed is not bypassed by a cached decision.
25
+ */
14
26
  export interface PolicySource {
15
27
  names(): readonly string[];
16
28
  get(name: string): PermissionPolicyDefinition | undefined;
29
+ subscribe?(listener: () => void): () => void;
17
30
  }
18
31
  /** Configuration for the permission engine. */
19
32
  export interface PermissionEngineOptions {
@@ -79,7 +92,11 @@ export interface PermissionEngine {
79
92
  createAbility(actor: PermissionActor): Ability;
80
93
  /** Drop cached decisions for one actor. */
81
94
  invalidateActor(actorId: string): Promise<void>;
82
- /** Re-read the role source, discarding the memoized lookups. */
95
+ /**
96
+ * Re-read the role source, discarding the memoized lookups and every
97
+ * cached decision. Registries created with `createRoleRegistry()` and
98
+ * `createPolicyRegistry()` trigger this themselves on every change.
99
+ */
83
100
  invalidateRoles(): void;
84
101
  }
85
102
  /**