@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,11 +1,15 @@
1
1
  /**
2
2
  * HTTP middleware adapter for @zudojs/permissions.
3
3
  *
4
- * Requires @zudojs/http as a peer dependency.
4
+ * The HTTP types are mirrored locally in `httpTypes.ts` so this package has no
5
+ * hard dependency on @zudojs/http, which is an optional peer. The mirror is
6
+ * structural: anything satisfying the real `HttpMiddlewareContext` satisfies
7
+ * the local one, so the middleware composes with the real pipeline. Keep the
8
+ * two in step when @zudojs/http changes — nothing here can check it for you.
5
9
  *
6
10
  * @module http
7
11
  */
8
- export { createActorMiddleware, createRequirePermissionMiddleware, authorize, createRequirePermissionsMiddleware, ACTOR_STATE_KEY, DECISION_STATE_KEY, type AuthorizeMiddlewareOptions, type RequirePermissionMiddlewareOptions, } from "./httpMiddleware.core.js";
9
- export { createForbiddenResponse, createJsonResponse, type DeniedResponseOptions, } from "./httpHelpers.js";
12
+ export { createActorMiddleware, createRequirePermissionMiddleware, authorize, createRequirePermissionsMiddleware, ACTOR_STATE_KEY, DECISION_STATE_KEY, DECISIONS_STATE_KEY, type AuthorizeMiddlewareOptions, type ActorMiddlewareOptions, type RequirePermissionMiddlewareOptions, type RequirePermissionsMiddlewareOptions, } from "./httpMiddleware.core.js";
13
+ export { createForbiddenResponse, createUnauthorizedResponse, createJsonResponse, type DeniedResponseOptions, type PermissionHttpResponse, } from "./httpHelpers.js";
10
14
  export type { HttpMiddleware, HttpMiddlewareContext, HttpRequestContext, HttpResponseContext, HttpMiddlewareState, } from "./httpTypes.js";
11
15
  //# sourceMappingURL=index.d.ts.map
@@ -1,10 +1,14 @@
1
1
  /**
2
2
  * HTTP middleware adapter for @zudojs/permissions.
3
3
  *
4
- * Requires @zudojs/http as a peer dependency.
4
+ * The HTTP types are mirrored locally in `httpTypes.ts` so this package has no
5
+ * hard dependency on @zudojs/http, which is an optional peer. The mirror is
6
+ * structural: anything satisfying the real `HttpMiddlewareContext` satisfies
7
+ * the local one, so the middleware composes with the real pipeline. Keep the
8
+ * two in step when @zudojs/http changes — nothing here can check it for you.
5
9
  *
6
10
  * @module http
7
11
  */
8
- export { createActorMiddleware, createRequirePermissionMiddleware, authorize, createRequirePermissionsMiddleware, ACTOR_STATE_KEY, DECISION_STATE_KEY, } from "./httpMiddleware.core.js";
9
- export { createForbiddenResponse, createJsonResponse, } from "./httpHelpers.js";
12
+ export { createActorMiddleware, createRequirePermissionMiddleware, authorize, createRequirePermissionsMiddleware, ACTOR_STATE_KEY, DECISION_STATE_KEY, DECISIONS_STATE_KEY, } from "./httpMiddleware.core.js";
13
+ export { createForbiddenResponse, createUnauthorizedResponse, createJsonResponse, } from "./httpHelpers.js";
10
14
  //# sourceMappingURL=index.js.map
package/dist/index.d.ts CHANGED
@@ -4,7 +4,8 @@
4
4
  * Authorization engine for the Zudojs framework.
5
5
  *
6
6
  * Supports RBAC, ABAC, resource policies, wildcards, role hierarchy,
7
- * condition combinators, ability compilation, and explain mode.
7
+ * condition combinators, ability compilation, decision caching, audit events
8
+ * and explain mode.
8
9
  *
9
10
  * @module @zudojs/permissions
10
11
  */
@@ -23,5 +24,5 @@ export * from "./cache/index.js";
23
24
  export * from "./observability/index.js";
24
25
  export * from "./utils/index.js";
25
26
  export * from "./http/index.js";
26
- export { createPermissionEngine, type PermissionEngine, type PermissionEngineOptions, } from "./evaluator/authorizationEngine.js";
27
+ export type { RoleSource, PolicySource, } from "./evaluator/authorizationEngine.js";
27
28
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -4,7 +4,8 @@
4
4
  * Authorization engine for the Zudojs framework.
5
5
  *
6
6
  * Supports RBAC, ABAC, resource policies, wildcards, role hierarchy,
7
- * condition combinators, ability compilation, and explain mode.
7
+ * condition combinators, ability compilation, decision caching, audit events
8
+ * and explain mode.
8
9
  *
9
10
  * @module @zudojs/permissions
10
11
  */
@@ -23,5 +24,4 @@ export * from "./cache/index.js";
23
24
  export * from "./observability/index.js";
24
25
  export * from "./utils/index.js";
25
26
  export * from "./http/index.js";
26
- export { createPermissionEngine, } from "./evaluator/authorizationEngine.js";
27
27
  //# sourceMappingURL=index.js.map
@@ -3,5 +3,5 @@
3
3
  *
4
4
  * @module observability
5
5
  */
6
- export { createPermissionEventEmitter, withObservability, type PermissionCheckEvent, type PermissionEventHandler, } from "./observability.core.js";
6
+ export { createPermissionEventEmitter, withObservability, type PermissionCheckEvent, type PermissionEventHandler, type PermissionEventEmitter, type PermissionEventEmitterOptions, } from "./observability.core.js";
7
7
  //# sourceMappingURL=index.d.ts.map
@@ -12,30 +12,50 @@ export interface PermissionCheckEvent {
12
12
  readonly permission: string;
13
13
  /** Resource type (if available). */
14
14
  readonly resourceType?: string;
15
- /** Whether allowed. */
15
+ /** Whether allowed. A check that threw is recorded as not allowed. */
16
16
  readonly allowed: boolean;
17
- /** Decision reason. */
17
+ /** Decision reason, or `error:<Name>` when the check threw. */
18
18
  readonly reason?: string;
19
19
  /** Evaluation duration in ms. */
20
20
  readonly durationMs: number;
21
+ /** Whether the check ended in an exception rather than a decision. */
22
+ readonly errored?: boolean;
21
23
  }
22
24
  /** Handler for permission events. */
23
25
  export type PermissionEventHandler = (event: PermissionCheckEvent) => void;
24
- /**
25
- * Create a permission event emitter.
26
- */
27
- export declare function createPermissionEventEmitter(): {
28
- /** Register an event handler. */
26
+ /** Emits authorization audit events. */
27
+ export interface PermissionEventEmitter {
28
+ /** Register a handler. Returns an unsubscribe function. */
29
29
  on(handler: PermissionEventHandler): () => void;
30
30
  /** Emit a permission check event. */
31
31
  emit(event: PermissionCheckEvent): void;
32
- };
32
+ /** Number of registered handlers. */
33
+ readonly size: number;
34
+ }
35
+ /** Options for {@link createPermissionEventEmitter}. */
36
+ export interface PermissionEventEmitterOptions {
37
+ /**
38
+ * Reports a handler that threw.
39
+ *
40
+ * Handler errors are swallowed so a broken audit sink cannot break
41
+ * authorization — but a sink that is silently failing is worse than one
42
+ * that is loudly failing, so this is where it surfaces.
43
+ */
44
+ readonly onHandlerError?: (error: unknown, event: PermissionCheckEvent) => void;
45
+ }
46
+ /**
47
+ * Create a permission event emitter.
48
+ */
49
+ export declare function createPermissionEventEmitter(options?: PermissionEventEmitterOptions): PermissionEventEmitter;
33
50
  /**
34
51
  * Wrap a permission check with observability.
35
52
  *
36
- * @param emitter - The event emitter.
37
- * @param fn - The permission check function.
38
- * @returns Wrapped function that emits events.
53
+ * The event is emitted from a `finally`, so a check that throws is recorded
54
+ * too — the exception paths are exactly the ones an audit trail must not
55
+ * miss.
56
+ *
57
+ * Prefer passing an emitter to `createPermissionEngine({ emitter })`, which
58
+ * instruments every check the engine makes.
39
59
  */
40
- export declare function withObservability<T extends (...args: readonly unknown[]) => Promise<PermissionDecision>>(emitter: ReturnType<typeof createPermissionEventEmitter>, fn: T): T;
60
+ export declare function withObservability<TArgs extends readonly unknown[]>(emitter: PermissionEventEmitter, fn: (...args: TArgs) => Promise<PermissionDecision>): (...args: TArgs) => Promise<PermissionDecision>;
41
61
  //# sourceMappingURL=observability.core.d.ts.map
@@ -6,49 +6,70 @@
6
6
  /**
7
7
  * Create a permission event emitter.
8
8
  */
9
- export function createPermissionEventEmitter() {
9
+ export function createPermissionEventEmitter(options) {
10
10
  const handlers = new Set();
11
11
  return {
12
- /** Register an event handler. */
13
12
  on(handler) {
14
13
  handlers.add(handler);
15
14
  return () => {
16
15
  handlers.delete(handler);
17
16
  };
18
17
  },
19
- /** Emit a permission check event. */
20
18
  emit(event) {
21
19
  for (const handler of handlers) {
22
20
  try {
23
21
  handler(event);
24
22
  }
25
- catch {
26
- // Swallow handler errors to prevent breaking authorization
23
+ catch (error) {
24
+ options?.onHandlerError?.(error, event);
27
25
  }
28
26
  }
29
27
  },
28
+ get size() {
29
+ return handlers.size;
30
+ },
30
31
  };
31
32
  }
32
33
  /**
33
34
  * Wrap a permission check with observability.
34
35
  *
35
- * @param emitter - The event emitter.
36
- * @param fn - The permission check function.
37
- * @returns Wrapped function that emits events.
36
+ * The event is emitted from a `finally`, so a check that throws is recorded
37
+ * too — the exception paths are exactly the ones an audit trail must not
38
+ * miss.
39
+ *
40
+ * Prefer passing an emitter to `createPermissionEngine({ emitter })`, which
41
+ * instruments every check the engine makes.
38
42
  */
39
43
  export function withObservability(emitter, fn) {
40
- return (async (...args) => {
44
+ return async (...args) => {
41
45
  const start = performance.now();
42
- const decision = await fn(...args);
43
- const durationMs = performance.now() - start;
44
- emitter.emit({
45
- actorId: args[0]?.id ?? "unknown",
46
- permission: args[1] ?? "unknown",
47
- allowed: decision.allowed,
48
- reason: decision.reason,
49
- durationMs,
50
- });
51
- return decision;
52
- });
46
+ let decision;
47
+ let failure;
48
+ try {
49
+ decision = await fn(...args);
50
+ return decision;
51
+ }
52
+ catch (error) {
53
+ failure = error;
54
+ throw error;
55
+ }
56
+ finally {
57
+ const actor = args[0];
58
+ const resource = args[2];
59
+ emitter.emit({
60
+ actorId: actor?.id ?? "unknown",
61
+ permission: typeof args[1] === "string" ? args[1] : "unknown",
62
+ resourceType: typeof resource === "object" && resource !== null
63
+ ? (resource.type ??
64
+ resource.constructor?.name)
65
+ : undefined,
66
+ allowed: decision?.allowed ?? false,
67
+ reason: decision?.reason ??
68
+ (failure instanceof Error ? `error:${failure.name}` : undefined),
69
+ durationMs: performance.now() - start,
70
+ errored: failure !== undefined,
71
+ });
72
+ }
73
+ };
53
74
  }
54
75
  //# sourceMappingURL=observability.core.js.map
@@ -3,6 +3,6 @@
3
3
  *
4
4
  * @module permission
5
5
  */
6
- export { parsePermission, isValidPermission, matches, matchesPermission, } from "./permission.core.js";
7
- export { createPermissionRegistry, type PermissionRegistryOptions, } from "./permissionRegistry.js";
6
+ export { parsePermission, parsePermissionSafe, isValidPermission, matches, matchesPermission, formatPermission, } from "./permission.core.js";
7
+ export { createPermissionRegistry, type PermissionRegistry, type PermissionRegistryOptions, type RegisteredPermission, } from "./permissionRegistry.js";
8
8
  //# sourceMappingURL=index.d.ts.map
@@ -3,6 +3,6 @@
3
3
  *
4
4
  * @module permission
5
5
  */
6
- export { parsePermission, isValidPermission, matches, matchesPermission, } from "./permission.core.js";
6
+ export { parsePermission, parsePermissionSafe, isValidPermission, matches, matchesPermission, formatPermission, } from "./permission.core.js";
7
7
  export { createPermissionRegistry, } from "./permissionRegistry.js";
8
8
  //# sourceMappingURL=index.js.map
@@ -9,6 +9,7 @@ import type { Permission } from "../permissionTypes/index.js";
9
9
  *
10
10
  * @example parsePermission("post:update") → { resource: "post", action: "update" }
11
11
  * @example parsePermission("billing.invoice:refund") → { resource: "billing.invoice", action: "refund" }
12
+ * @throws InvalidPermissionError if the string is not a valid permission
12
13
  */
13
14
  export declare function parsePermission(permission: string): Permission;
14
15
  /**
@@ -16,15 +17,25 @@ export declare function parsePermission(permission: string): Permission;
16
17
  */
17
18
  export declare function isValidPermission(permission: string): boolean;
18
19
  /**
19
- * Check if a permission matches a target permission, supporting wildcards.
20
+ * Check if a permission pattern matches a target permission.
21
+ *
22
+ * Uses the same wildcard rules as the rule engine, so `billing.*:read`
23
+ * means the same thing whether it is written as a grant, a deny, or a rule.
20
24
  *
21
25
  * @example matches("post:*", "post:update") → true
22
26
  * @example matches("*:*", "anything:goes") → true
27
+ * @example matches("billing.*:read", "billing.invoice:read") → true
23
28
  * @example matches("post:read", "post:update") → false
24
29
  */
25
30
  export declare function matches(pattern: string, target: string): boolean;
26
31
  /**
27
- * Check if a permission string matches a structured Permission object.
32
+ * Check if a permission pattern matches a structured Permission object.
28
33
  */
29
34
  export declare function matchesPermission(pattern: string, permission: Permission): boolean;
35
+ /**
36
+ * Parse a permission string, returning null on invalid format instead of throwing.
37
+ */
38
+ export declare function parsePermissionSafe(permission: string): Permission | null;
39
+ /** Build a permission string from a resource and an action. */
40
+ export declare function formatPermission(resource: string, action: string): string;
30
41
  //# sourceMappingURL=permission.core.d.ts.map
@@ -4,22 +4,29 @@
4
4
  * @module permission/permission
5
5
  */
6
6
  import { InvalidPermissionError } from "../permissionErrors/index.js";
7
- /** Regex for valid permission format: resource:action (supports namespaces and wildcards). */
8
- const PERMISSION_REGEX = /^[a-zA-Z0-9._*-]+:[a-zA-Z0-9._*-]+$/;
7
+ import { patternStrMatches } from "../rule/rule.core.js";
8
+ /**
9
+ * Valid permission format: `resource:action`.
10
+ *
11
+ * A segment is either a bare `*`, a namespace wildcard such as `billing.*`,
12
+ * or a literal name. A partial wildcard like `post*` is rejected: the matcher
13
+ * has no meaning for it, so accepting one produced a grant that silently
14
+ * never matched anything.
15
+ */
16
+ const SEGMENT = String.raw `(?:\*|[a-zA-Z0-9._-]+(?:\.\*)?)`;
17
+ const PERMISSION_REGEX = new RegExp(`^${SEGMENT}:${SEGMENT}$`);
9
18
  /**
10
19
  * Parse a permission string into a structured Permission.
11
20
  *
12
21
  * @example parsePermission("post:update") → { resource: "post", action: "update" }
13
22
  * @example parsePermission("billing.invoice:refund") → { resource: "billing.invoice", action: "refund" }
23
+ * @throws InvalidPermissionError if the string is not a valid permission
14
24
  */
15
25
  export function parsePermission(permission) {
16
- if (!PERMISSION_REGEX.test(permission)) {
26
+ const parsed = parsePermissionSafe(permission);
27
+ if (!parsed)
17
28
  throw new InvalidPermissionError(permission);
18
- }
19
- const lastColon = permission.lastIndexOf(":");
20
- const resource = permission.slice(0, lastColon);
21
- const action = permission.slice(lastColon + 1);
22
- return Object.freeze({ resource, action });
29
+ return Object.freeze(parsed);
23
30
  }
24
31
  /**
25
32
  * Check if a permission string is valid without throwing.
@@ -28,40 +35,37 @@ export function isValidPermission(permission) {
28
35
  return PERMISSION_REGEX.test(permission);
29
36
  }
30
37
  /**
31
- * Check if a permission matches a target permission, supporting wildcards.
38
+ * Check if a permission pattern matches a target permission.
39
+ *
40
+ * Uses the same wildcard rules as the rule engine, so `billing.*:read`
41
+ * means the same thing whether it is written as a grant, a deny, or a rule.
32
42
  *
33
43
  * @example matches("post:*", "post:update") → true
34
44
  * @example matches("*:*", "anything:goes") → true
45
+ * @example matches("billing.*:read", "billing.invoice:read") → true
35
46
  * @example matches("post:read", "post:update") → false
36
47
  */
37
48
  export function matches(pattern, target) {
38
- if (pattern === "*:*")
39
- return true;
40
49
  const patternParsed = parsePermissionSafe(pattern);
41
50
  const targetParsed = parsePermissionSafe(target);
42
51
  if (!patternParsed || !targetParsed)
43
52
  return false;
44
- const resourceMatch = patternParsed.resource === "*" ||
45
- patternParsed.resource === targetParsed.resource;
46
- const actionMatch = patternParsed.action === "*" ||
47
- patternParsed.action === targetParsed.action;
48
- return resourceMatch && actionMatch;
53
+ return matchesPermission(pattern, targetParsed);
49
54
  }
50
55
  /**
51
- * Check if a permission string matches a structured Permission object.
56
+ * Check if a permission pattern matches a structured Permission object.
52
57
  */
53
58
  export function matchesPermission(pattern, permission) {
54
59
  const parsed = parsePermissionSafe(pattern);
55
60
  if (!parsed)
56
61
  return false;
57
- const resourceMatch = parsed.resource === "*" || parsed.resource === permission.resource;
58
- const actionMatch = parsed.action === "*" || parsed.action === permission.action;
59
- return resourceMatch && actionMatch;
62
+ return (patternStrMatches(parsed.resource, permission.resource) &&
63
+ patternStrMatches(parsed.action, permission.action));
60
64
  }
61
65
  /**
62
66
  * Parse a permission string, returning null on invalid format instead of throwing.
63
67
  */
64
- function parsePermissionSafe(permission) {
68
+ export function parsePermissionSafe(permission) {
65
69
  if (!PERMISSION_REGEX.test(permission))
66
70
  return null;
67
71
  const lastColon = permission.lastIndexOf(":");
@@ -69,4 +73,8 @@ function parsePermissionSafe(permission) {
69
73
  const action = permission.slice(lastColon + 1);
70
74
  return { resource, action };
71
75
  }
76
+ /** Build a permission string from a resource and an action. */
77
+ export function formatPermission(resource, action) {
78
+ return `${resource}:${action}`;
79
+ }
72
80
  //# sourceMappingURL=permission.core.js.map
@@ -5,7 +5,8 @@
5
5
  */
6
6
  import type { Permission } from "../permissionTypes/index.js";
7
7
  /** A registered permission with optional metadata. */
8
- interface RegisteredPermission {
8
+ export interface RegisteredPermission {
9
+ readonly key: string;
9
10
  readonly parsed: Permission;
10
11
  readonly description?: string;
11
12
  readonly implies?: readonly string[];
@@ -15,49 +16,42 @@ export interface PermissionRegistryOptions {
15
16
  /** Allow overwriting existing permissions. Defaults to false. */
16
17
  readonly allowOverride?: boolean;
17
18
  }
18
- /**
19
- * Create a permission registry.
20
- */
21
- export declare function createPermissionRegistry(options?: PermissionRegistryOptions): {
22
- /**
23
- * Register a permission by string or structured object.
24
- */
25
- define(permission: string | Permission, opts?: {
19
+ /** A registry of the permissions an application defines. */
20
+ export interface PermissionRegistry {
21
+ define(permission: string | Permission, options?: {
26
22
  readonly description?: string;
27
23
  readonly implies?: readonly string[];
28
24
  }): void;
29
- /**
30
- * Look up a registered permission by string.
31
- */
32
25
  get(permission: string): Permission | undefined;
33
- /**
34
- * Get the full registered permission entry.
35
- */
26
+ /** Like {@link PermissionRegistry.get}, but throws when unregistered. */
27
+ require(permission: string): Permission;
36
28
  getEntry(permission: string): RegisteredPermission | undefined;
37
- /**
38
- * Check if a permission is registered.
39
- */
40
29
  has(permission: string): boolean;
41
- /**
42
- * Get all registered permission strings.
43
- */
44
30
  all(): readonly string[];
45
- /**
46
- * Get permissions that match a pattern.
47
- */
48
31
  match(pattern: string): readonly Permission[];
49
- /**
50
- * Get permissions implied by a given permission.
51
- */
52
32
  getImplied(permission: string): readonly string[];
53
33
  /**
54
- * Remove a permission from the registry.
34
+ * Every permission implied by `permission`, following chains and stopping
35
+ * at cycles. This is what `createPermissionEngine({ expandImplied })`
36
+ * wants: `post:admin implies post:read` only affects a decision if
37
+ * something expands it.
55
38
  */
39
+ expandImplied(permission: string): readonly string[];
56
40
  remove(permission: string): boolean;
57
- /**
58
- * Clear all registered permissions.
59
- */
60
41
  clear(): void;
61
- };
62
- export {};
42
+ }
43
+ /**
44
+ * Create a permission registry.
45
+ *
46
+ * ```ts
47
+ * const permissions = createPermissionRegistry();
48
+ * permissions.define("post:admin", { implies: ["post:read", "post:write"] });
49
+ *
50
+ * const engine = createPermissionEngine({
51
+ * roles,
52
+ * expandImplied: (permission) => permissions.expandImplied(permission),
53
+ * });
54
+ * ```
55
+ */
56
+ export declare function createPermissionRegistry(options?: PermissionRegistryOptions): PermissionRegistry;
63
57
  //# sourceMappingURL=permissionRegistry.d.ts.map
@@ -3,83 +3,101 @@
3
3
  *
4
4
  * @module permission/permissionRegistry
5
5
  */
6
- import { DuplicatePermissionError, PermissionNotFoundError, } from "../permissionErrors/index.js";
7
- import { parsePermission, matchesPermission } from "./permission.core.js";
6
+ import { DuplicatePermissionError, InvalidPermissionError, PermissionNotFoundError, } from "../permissionErrors/index.js";
7
+ import { formatPermission, isValidPermission, matchesPermission, parsePermission, } from "./permission.core.js";
8
8
  /**
9
9
  * Create a permission registry.
10
+ *
11
+ * ```ts
12
+ * const permissions = createPermissionRegistry();
13
+ * permissions.define("post:admin", { implies: ["post:read", "post:write"] });
14
+ *
15
+ * const engine = createPermissionEngine({
16
+ * roles,
17
+ * expandImplied: (permission) => permissions.expandImplied(permission),
18
+ * });
19
+ * ```
10
20
  */
11
21
  export function createPermissionRegistry(options) {
12
22
  const permissions = new Map();
13
23
  const allowOverride = options?.allowOverride ?? false;
24
+ function keyOf(permission) {
25
+ if (typeof permission === "string") {
26
+ // Validate strings and structured values alike; a structured value
27
+ // skipping validation let `{ resource: "a b", action: "" }` in.
28
+ parsePermission(permission);
29
+ return permission;
30
+ }
31
+ const key = formatPermission(permission.resource, permission.action);
32
+ if (!isValidPermission(key))
33
+ throw new InvalidPermissionError(key);
34
+ return key;
35
+ }
14
36
  return {
15
- /**
16
- * Register a permission by string or structured object.
17
- */
18
- define(permission, opts) {
19
- const parsed = typeof permission === "string"
20
- ? parsePermission(permission)
21
- : permission;
22
- const key = `${parsed.resource}:${parsed.action}`;
37
+ define(permission, defineOptions) {
38
+ const key = keyOf(permission);
39
+ const parsed = parsePermission(key);
23
40
  if (permissions.has(key) && !allowOverride) {
24
41
  throw new DuplicatePermissionError(key);
25
42
  }
43
+ for (const implied of defineOptions?.implies ?? []) {
44
+ if (!isValidPermission(implied)) {
45
+ throw new InvalidPermissionError(implied);
46
+ }
47
+ }
26
48
  permissions.set(key, {
27
- parsed: Object.freeze(parsed),
28
- description: opts?.description,
29
- implies: opts?.implies,
49
+ key,
50
+ parsed,
51
+ description: defineOptions?.description,
52
+ implies: defineOptions?.implies
53
+ ? Object.freeze([...defineOptions.implies])
54
+ : undefined,
30
55
  });
31
56
  },
32
- /**
33
- * Look up a registered permission by string.
34
- */
35
57
  get(permission) {
36
58
  return permissions.get(permission)?.parsed;
37
59
  },
38
- /**
39
- * Get the full registered permission entry.
40
- */
60
+ require(permission) {
61
+ const entry = permissions.get(permission);
62
+ if (!entry)
63
+ throw new PermissionNotFoundError(permission);
64
+ return entry.parsed;
65
+ },
41
66
  getEntry(permission) {
42
67
  return permissions.get(permission);
43
68
  },
44
- /**
45
- * Check if a permission is registered.
46
- */
47
69
  has(permission) {
48
70
  return permissions.has(permission);
49
71
  },
50
- /**
51
- * Get all registered permission strings.
52
- */
53
72
  all() {
54
- return Array.from(permissions.keys());
73
+ return [...permissions.keys()];
55
74
  },
56
- /**
57
- * Get permissions that match a pattern.
58
- */
59
75
  match(pattern) {
60
76
  const results = [];
61
- for (const [, entry] of permissions) {
62
- if (matchesPermission(pattern, entry.parsed)) {
77
+ for (const entry of permissions.values()) {
78
+ if (matchesPermission(pattern, entry.parsed))
63
79
  results.push(entry.parsed);
64
- }
65
80
  }
66
81
  return results;
67
82
  },
68
- /**
69
- * Get permissions implied by a given permission.
70
- */
71
83
  getImplied(permission) {
72
84
  return permissions.get(permission)?.implies ?? [];
73
85
  },
74
- /**
75
- * Remove a permission from the registry.
76
- */
86
+ expandImplied(permission) {
87
+ const expanded = new Set();
88
+ const queue = [...(permissions.get(permission)?.implies ?? [])];
89
+ while (queue.length > 0) {
90
+ const next = queue.shift();
91
+ if (next === permission || expanded.has(next))
92
+ continue;
93
+ expanded.add(next);
94
+ queue.push(...(permissions.get(next)?.implies ?? []));
95
+ }
96
+ return [...expanded];
97
+ },
77
98
  remove(permission) {
78
99
  return permissions.delete(permission);
79
100
  },
80
- /**
81
- * Clear all registered permissions.
82
- */
83
101
  clear() {
84
102
  permissions.clear();
85
103
  },
@@ -4,5 +4,5 @@
4
4
  * @module permissionErrors
5
5
  */
6
6
  export { PermissionError } from "./permissionError.base.js";
7
- export { PermissionDeniedError, PermissionNotFoundError, DuplicatePermissionError, RoleNotFoundError, DuplicateRoleError, InvalidPermissionError, InvalidRoleError, CircularRoleInheritanceError, PolicyError, PolicyTimeoutError, PermissionResolverError, AuthorizationAbortedError, } from "./permissionError.types.js";
7
+ export { PermissionDeniedError, PermissionNotFoundError, DuplicatePermissionError, RoleNotFoundError, DuplicateRoleError, DuplicatePolicyError, InvalidPermissionError, InvalidRoleError, CircularRoleInheritanceError, PolicyError, PolicyTimeoutError, PermissionResolverError, AuthorizationAbortedError, } from "./permissionError.types.js";
8
8
  //# sourceMappingURL=index.d.ts.map
@@ -4,5 +4,5 @@
4
4
  * @module permissionErrors
5
5
  */
6
6
  export { PermissionError } from "./permissionError.base.js";
7
- export { PermissionDeniedError, PermissionNotFoundError, DuplicatePermissionError, RoleNotFoundError, DuplicateRoleError, InvalidPermissionError, InvalidRoleError, CircularRoleInheritanceError, PolicyError, PolicyTimeoutError, PermissionResolverError, AuthorizationAbortedError, } from "./permissionError.types.js";
7
+ export { PermissionDeniedError, PermissionNotFoundError, DuplicatePermissionError, RoleNotFoundError, DuplicateRoleError, DuplicatePolicyError, InvalidPermissionError, InvalidRoleError, CircularRoleInheritanceError, PolicyError, PolicyTimeoutError, PermissionResolverError, AuthorizationAbortedError, } from "./permissionError.types.js";
8
8
  //# sourceMappingURL=index.js.map