@zap-studio/permit 0.3.3 → 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.
@@ -0,0 +1,180 @@
1
+ //#region src/conditions.ts
2
+ /**
3
+ * Returns a policy function that always allows the action.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * const policy = createPolicy({
8
+ * resources,
9
+ * actions,
10
+ * rules: {
11
+ * post: {
12
+ * read: allow(), // Always allow reading posts
13
+ * },
14
+ * },
15
+ * });
16
+ * ```
17
+ */
18
+ const allow = () => () => "allow";
19
+ /**
20
+ * Returns a policy function that always denies the action.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * const policy = createPolicy({
25
+ * resources,
26
+ * actions,
27
+ * rules: {
28
+ * post: {
29
+ * delete: deny(), // Never allow deleting posts
30
+ * },
31
+ * },
32
+ * });
33
+ * ```
34
+ */
35
+ const deny = () => () => "deny";
36
+ /**
37
+ * Returns a policy function that allows or denies based on a condition.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * const policy = createPolicy({
42
+ * resources,
43
+ * actions,
44
+ * rules: {
45
+ * post: {
46
+ * write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
47
+ * },
48
+ * },
49
+ * });
50
+ * ```
51
+ */
52
+ const when = (condition) => (context, action, resource) => condition(context, action, resource) ? "allow" : "deny";
53
+ /**
54
+ * Returns a condition function that returns `true` if all conditions are met.
55
+ *
56
+ * @example
57
+ * ```ts
58
+ * const isOwnerAndPublished = and(
59
+ * (ctx, action, resource) => ctx.user.id === resource.authorId,
60
+ * (ctx, action, resource) => resource.status === "published"
61
+ * );
62
+ *
63
+ * rules: {
64
+ * post: {
65
+ * delete: when(isOwnerAndPublished),
66
+ * },
67
+ * }
68
+ * ```
69
+ */
70
+ const and = (...conditions) => (context, action, resource) => conditions.every((condition) => condition(context, action, resource));
71
+ /**
72
+ * Returns a condition function that returns `true` if any condition is met.
73
+ *
74
+ * @example
75
+ * ```ts
76
+ * const isOwnerOrAdmin = or(
77
+ * (ctx, action, resource) => ctx.user.id === resource.authorId,
78
+ * (ctx, action, resource) => ctx.user.role === "admin"
79
+ * );
80
+ *
81
+ * rules: {
82
+ * post: {
83
+ * write: when(isOwnerOrAdmin),
84
+ * },
85
+ * }
86
+ * ```
87
+ */
88
+ const or = (...conditions) => (context, action, resource) => conditions.some((condition) => condition(context, action, resource));
89
+ /**
90
+ * Returns a condition function that negates another condition.
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * const isNotOwner = not((ctx, action, resource) => ctx.user.id === resource.authorId);
95
+ *
96
+ * rules: {
97
+ * post: {
98
+ * like: when(isNotOwner), // Can only like posts you don't own
99
+ * },
100
+ * }
101
+ * ```
102
+ */
103
+ const not = (condition) => (context, action, resource) => !condition(context, action, resource);
104
+ /**
105
+ * Returns a condition function that checks if a context property equals a value.
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * rules: {
110
+ * post: {
111
+ * write: when(has("role", "admin")), // Only admins can write
112
+ * },
113
+ * }
114
+ * ```
115
+ */
116
+ const has = (key, value) => (context) => context[key] === value;
117
+ /**
118
+ * Collects all roles including inherited ones from a role hierarchy.
119
+ *
120
+ * @example
121
+ * ```ts
122
+ * type Role = "guest" | "user" | "admin";
123
+ *
124
+ * const hierarchy: RoleHierarchy<Role> = {
125
+ * guest: [],
126
+ * user: ["guest"],
127
+ * admin: ["user"],
128
+ * };
129
+ *
130
+ * collectInheritedRoles(["admin"], hierarchy);
131
+ * // Returns: Set { "admin", "user", "guest" }
132
+ * ```
133
+ */
134
+ const collectInheritedRoles = (roles, hierarchy) => {
135
+ const inherited = /* @__PURE__ */ new Set();
136
+ const add = (role) => {
137
+ if (inherited.has(role)) return;
138
+ inherited.add(role);
139
+ const baseRoles = hierarchy[role] ?? [];
140
+ for (const baseRole of baseRoles) add(baseRole);
141
+ };
142
+ for (const role of roles) add(role);
143
+ return inherited;
144
+ };
145
+ /**
146
+ * Returns a condition function that checks if the user has a specific role.
147
+ * Supports role hierarchy for inherited permissions.
148
+ *
149
+ * @example
150
+ * ```ts
151
+ * // Without hierarchy
152
+ * rules: {
153
+ * post: {
154
+ * delete: when(hasRole("admin")),
155
+ * },
156
+ * }
157
+ *
158
+ * // With hierarchy
159
+ * const hierarchy = {
160
+ * guest: [],
161
+ * user: ["guest"],
162
+ * admin: ["user"],
163
+ * };
164
+ *
165
+ * rules: {
166
+ * post: {
167
+ * read: when(hasRole("guest", hierarchy)), // Admins and users can also read
168
+ * },
169
+ * }
170
+ * ```
171
+ */
172
+ const hasRole = (role, hierarchy) => (context) => {
173
+ const userRoles = Array.isArray(context.role) ? context.role : [context.role];
174
+ if (hierarchy === void 0) return userRoles.includes(role);
175
+ return collectInheritedRoles(userRoles, hierarchy).has(role);
176
+ };
177
+ //#endregion
178
+ export { allow, and, collectInheritedRoles, deny, has, hasRole, not, or, when };
179
+
180
+ //# sourceMappingURL=conditions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conditions.js","names":[],"sources":["../src/conditions.ts"],"sourcesContent":["/**\n * Policy and condition combinators: `allow`, `deny`, `when`, boolean\n * composition, and role helpers.\n *\n * @module @zap-studio/permit/conditions\n */\n\nimport type {\n ConditionFn,\n Context,\n HasRoleFn,\n PolicyFn,\n Role,\n RoleHierarchy,\n} from \"./types.js\";\n\n/**\n * Returns a policy function that always allows the action.\n *\n * @example\n * ```ts\n * const policy = createPolicy({\n * resources,\n * actions,\n * rules: {\n * post: {\n * read: allow(), // Always allow reading posts\n * },\n * },\n * });\n * ```\n */\nexport const allow =\n <\n TContext extends Context,\n TAction extends string = string,\n TResource = unknown,\n >(): PolicyFn<TContext, TAction, TResource> =>\n () =>\n \"allow\";\n\n/**\n * Returns a policy function that always denies the action.\n *\n * @example\n * ```ts\n * const policy = createPolicy({\n * resources,\n * actions,\n * rules: {\n * post: {\n * delete: deny(), // Never allow deleting posts\n * },\n * },\n * });\n * ```\n */\nexport const deny =\n <\n TContext extends Context,\n TAction extends string = string,\n TResource = unknown,\n >(): PolicyFn<TContext, TAction, TResource> =>\n () =>\n \"deny\";\n\n/**\n * Returns a policy function that allows or denies based on a condition.\n *\n * @example\n * ```ts\n * const policy = createPolicy({\n * resources,\n * actions,\n * rules: {\n * post: {\n * write: when((ctx, action, resource) => ctx.user.id === resource.authorId),\n * },\n * },\n * });\n * ```\n */\nexport const when =\n <\n TContext extends Context,\n TAction extends string = string,\n TResource = unknown,\n >(\n condition: ConditionFn<TContext, TAction, TResource>\n ): PolicyFn<TContext, TAction, TResource> =>\n (context, action, resource) =>\n condition(context, action, resource) ? \"allow\" : \"deny\";\n\n/**\n * Returns a condition function that returns `true` if all conditions are met.\n *\n * @example\n * ```ts\n * const isOwnerAndPublished = and(\n * (ctx, action, resource) => ctx.user.id === resource.authorId,\n * (ctx, action, resource) => resource.status === \"published\"\n * );\n *\n * rules: {\n * post: {\n * delete: when(isOwnerAndPublished),\n * },\n * }\n * ```\n */\nexport const and =\n <\n TContext extends Context,\n TAction extends string = string,\n TResource = unknown,\n >(\n ...conditions: ConditionFn<TContext, TAction, TResource>[]\n ): ConditionFn<TContext, TAction, TResource> =>\n (context, action, resource) =>\n conditions.every((condition) => condition(context, action, resource));\n\n/**\n * Returns a condition function that returns `true` if any condition is met.\n *\n * @example\n * ```ts\n * const isOwnerOrAdmin = or(\n * (ctx, action, resource) => ctx.user.id === resource.authorId,\n * (ctx, action, resource) => ctx.user.role === \"admin\"\n * );\n *\n * rules: {\n * post: {\n * write: when(isOwnerOrAdmin),\n * },\n * }\n * ```\n */\nexport const or =\n <\n TContext extends Context,\n TAction extends string = string,\n TResource = unknown,\n >(\n ...conditions: ConditionFn<TContext, TAction, TResource>[]\n ): ConditionFn<TContext, TAction, TResource> =>\n (context, action, resource) =>\n conditions.some((condition) => condition(context, action, resource));\n\n/**\n * Returns a condition function that negates another condition.\n *\n * @example\n * ```ts\n * const isNotOwner = not((ctx, action, resource) => ctx.user.id === resource.authorId);\n *\n * rules: {\n * post: {\n * like: when(isNotOwner), // Can only like posts you don't own\n * },\n * }\n * ```\n */\nexport const not =\n <\n TContext extends Context,\n TAction extends string = string,\n TResource = unknown,\n >(\n condition: ConditionFn<TContext, TAction, TResource>\n ): ConditionFn<TContext, TAction, TResource> =>\n (context, action, resource) =>\n !condition(context, action, resource);\n\n/**\n * Returns a condition function that checks if a context property equals a value.\n *\n * @example\n * ```ts\n * rules: {\n * post: {\n * write: when(has(\"role\", \"admin\")), // Only admins can write\n * },\n * }\n * ```\n */\nexport const has =\n <TContext extends Context, K extends keyof TContext>(\n key: K,\n value: TContext[K]\n ): ConditionFn<TContext> =>\n (context) =>\n context[key] === value;\n\n/**\n * Collects all roles including inherited ones from a role hierarchy.\n *\n * @example\n * ```ts\n * type Role = \"guest\" | \"user\" | \"admin\";\n *\n * const hierarchy: RoleHierarchy<Role> = {\n * guest: [],\n * user: [\"guest\"],\n * admin: [\"user\"],\n * };\n *\n * collectInheritedRoles([\"admin\"], hierarchy);\n * // Returns: Set { \"admin\", \"user\", \"guest\" }\n * ```\n */\nexport const collectInheritedRoles = <TRole extends Role = Role>(\n roles: TRole[],\n hierarchy: RoleHierarchy<TRole>\n): Set<TRole> => {\n const inherited = new Set<TRole>();\n\n const add = (role: TRole): void => {\n if (inherited.has(role)) {\n return;\n }\n\n inherited.add(role);\n const baseRoles = hierarchy[role] ?? [];\n for (const baseRole of baseRoles) {\n add(baseRole);\n }\n };\n\n for (const role of roles) {\n add(role);\n }\n return inherited;\n};\n\n/**\n * Returns a condition function that checks if the user has a specific role.\n * Supports role hierarchy for inherited permissions.\n *\n * @example\n * ```ts\n * // Without hierarchy\n * rules: {\n * post: {\n * delete: when(hasRole(\"admin\")),\n * },\n * }\n *\n * // With hierarchy\n * const hierarchy = {\n * guest: [],\n * user: [\"guest\"],\n * admin: [\"user\"],\n * };\n *\n * rules: {\n * post: {\n * read: when(hasRole(\"guest\", hierarchy)), // Admins and users can also read\n * },\n * }\n * ```\n */\nexport const hasRole: HasRoleFn =\n (\n role: Role,\n hierarchy?: RoleHierarchy\n ): ConditionFn<{ role: Role | Role[] }> =>\n (context) => {\n const userRoles = Array.isArray(context.role)\n ? context.role\n : [context.role];\n\n if (hierarchy === undefined) {\n return userRoles.includes(role);\n }\n\n const inherited = collectInheritedRoles(userRoles, hierarchy);\n return inherited.has(role);\n };\n"],"mappings":";;;;;;;;;;;;;;;;;AAgCA,MAAa,oBAOT;;;;;;;;;;;;;;;;;AAkBJ,MAAa,mBAOT;;;;;;;;;;;;;;;;;AAkBJ,MAAa,QAMT,eAED,SAAS,QAAQ,aAChB,UAAU,SAAS,QAAQ,QAAQ,IAAI,UAAU;;;;;;;;;;;;;;;;;;AAmBrD,MAAa,OAMT,GAAG,gBAEJ,SAAS,QAAQ,aAChB,WAAW,OAAO,cAAc,UAAU,SAAS,QAAQ,QAAQ,CAAC;;;;;;;;;;;;;;;;;;AAmBxE,MAAa,MAMT,GAAG,gBAEJ,SAAS,QAAQ,aAChB,WAAW,MAAM,cAAc,UAAU,SAAS,QAAQ,QAAQ,CAAC;;;;;;;;;;;;;;;AAgBvE,MAAa,OAMT,eAED,SAAS,QAAQ,aAChB,CAAC,UAAU,SAAS,QAAQ,QAAQ;;;;;;;;;;;;;AAcxC,MAAa,OAET,KACA,WAED,YACC,QAAQ,SAAS;;;;;;;;;;;;;;;;;;AAmBrB,MAAa,yBACX,OACA,cACe;CACf,MAAM,4BAAY,IAAI,IAAW;CAEjC,MAAM,OAAO,SAAsB;EACjC,IAAI,UAAU,IAAI,IAAI,GACpB;EAGF,UAAU,IAAI,IAAI;EAClB,MAAM,YAAY,UAAU,SAAS,CAAC;EACtC,KAAK,MAAM,YAAY,WACrB,IAAI,QAAQ;CAEhB;CAEA,KAAK,MAAM,QAAQ,OACjB,IAAI,IAAI;CAEV,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,MAAa,WAET,MACA,eAED,YAAY;CACX,MAAM,YAAY,MAAM,QAAQ,QAAQ,IAAI,IACxC,QAAQ,OACR,CAAC,QAAQ,IAAI;CAEjB,IAAI,cAAc,KAAA,GAChB,OAAO,UAAU,SAAS,IAAI;CAIhC,OADkB,sBAAsB,WAAW,SACpC,CAAC,CAAC,IAAI,IAAI;AAC3B"}
@@ -7,6 +7,20 @@
7
7
  /**
8
8
  * Represents an error that occurs during policy evaluation or enforcement.
9
9
  * Use this error to indicate issues related to policy logic, configuration, or execution.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { PolicyError } from "@zap-studio/permit";
14
+ *
15
+ * try {
16
+ * const policy = createPolicy(config);
17
+ * await policy.can(ctx, "post:read", post);
18
+ * } catch (error) {
19
+ * if (error instanceof PolicyError) {
20
+ * console.error("Invalid policy configuration:", error.message);
21
+ * }
22
+ * }
23
+ * ```
10
24
  */
11
25
  declare class PolicyError extends Error {
12
26
  /**
@@ -18,4 +32,4 @@ declare class PolicyError extends Error {
18
32
  }
19
33
  //#endregion
20
34
  export { PolicyError };
21
- //# sourceMappingURL=errors.d.mts.map
35
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","names":[],"sources":["../src/errors.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;cAwBa,oBAAoB;;;;;;EAM/B,YAAY"}
@@ -7,6 +7,20 @@
7
7
  /**
8
8
  * Represents an error that occurs during policy evaluation or enforcement.
9
9
  * Use this error to indicate issues related to policy logic, configuration, or execution.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { PolicyError } from "@zap-studio/permit";
14
+ *
15
+ * try {
16
+ * const policy = createPolicy(config);
17
+ * await policy.can(ctx, "post:read", post);
18
+ * } catch (error) {
19
+ * if (error instanceof PolicyError) {
20
+ * console.error("Invalid policy configuration:", error.message);
21
+ * }
22
+ * }
23
+ * ```
10
24
  */
11
25
  var PolicyError = class extends Error {
12
26
  /**
@@ -22,4 +36,4 @@ var PolicyError = class extends Error {
22
36
  //#endregion
23
37
  export { PolicyError };
24
38
 
25
- //# sourceMappingURL=errors.mjs.map
39
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * Error primitives for policy evaluation and configuration failures.\n *\n * @module @zap-studio/permit/errors\n */\n\n/**\n * Represents an error that occurs during policy evaluation or enforcement.\n * Use this error to indicate issues related to policy logic, configuration, or execution.\n *\n * @example\n * ```ts\n * import { PolicyError } from \"@zap-studio/permit\";\n *\n * try {\n * const policy = createPolicy(config);\n * await policy.can(ctx, \"post:read\", post);\n * } catch (error) {\n * if (error instanceof PolicyError) {\n * console.error(\"Invalid policy configuration:\", error.message);\n * }\n * }\n * ```\n */\nexport class PolicyError extends Error {\n /**\n * Creates a policy error with a human-readable message.\n *\n * @param message - Error message describing the policy failure.\n */\n constructor(message: string) {\n super(message);\n this.name = \"PolicyError\";\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAwBA,IAAa,cAAb,cAAiC,MAAM;;;;;;CAMrC,YAAY,SAAiB;EAC3B,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF"}
@@ -0,0 +1,5 @@
1
+ import { ActionPolicyMap, Actions, ConditionFn, Context, Decision, InferAction, InferPermission, InferResource, PermitConfig, Policy, PolicyFn, Resources, Role, RoleHierarchy, Rules } from "./types.js";
2
+ import { allow, and, collectInheritedRoles, deny, has, hasRole, not, or, when } from "./conditions.js";
3
+ import { PolicyError } from "./errors.js";
4
+ import { createPolicy, mergePoliciesAnd, mergePoliciesOr } from "./policy.js";
5
+ export { type ActionPolicyMap, type Actions, type ConditionFn, type Context, type Decision, type InferAction, type InferPermission, type InferResource, type PermitConfig, type Policy, PolicyError, type PolicyFn, type Resources, type Role, type RoleHierarchy, type Rules, allow, and, collectInheritedRoles, createPolicy, deny, has, hasRole, mergePoliciesAnd, mergePoliciesOr, not, or, when };
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ import { allow, and, collectInheritedRoles, deny, has, hasRole, not, or, when } from "./conditions.js";
2
+ import { PolicyError } from "./errors.js";
3
+ import { createPolicy, mergePoliciesAnd, mergePoliciesOr } from "./policy.js";
4
+ export { PolicyError, allow, and, collectInheritedRoles, createPolicy, deny, has, hasRole, mergePoliciesAnd, mergePoliciesOr, not, or, when };
@@ -0,0 +1,91 @@
1
+ import { Actions, Context, PermitConfig, Policy, Resources } from "./types.js";
2
+ //#region src/policy.d.ts
3
+ /**
4
+ * Creates a type-safe policy from resource schemas, actions, and rules.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * import { z } from "zod";
9
+ * import { createPolicy, allow, deny, when } from "@zap-studio/permit";
10
+ * import type { Resources, Actions } from "@zap-studio/permit/types";
11
+ *
12
+ * // Define resource schemas
13
+ * const resources = {
14
+ * post: z.object({
15
+ * id: z.string(),
16
+ * authorId: z.string(),
17
+ * visibility: z.enum(["public", "private"]),
18
+ * }),
19
+ * comment: z.object({
20
+ * id: z.string(),
21
+ * postId: z.string(),
22
+ * authorId: z.string(),
23
+ * }),
24
+ * } satisfies Resources;
25
+ *
26
+ * // Define actions per resource
27
+ * const actions = {
28
+ * post: ["read", "write", "delete"],
29
+ * comment: ["read", "write"],
30
+ * } as const satisfies Actions<typeof resources>;
31
+ *
32
+ * // Define context type
33
+ * type AppContext = { user: { id: string; role: string } };
34
+ *
35
+ * // Create the policy
36
+ * const policy = createPolicy<AppContext>({
37
+ * resources,
38
+ * actions,
39
+ * rules: {
40
+ * post: {
41
+ * read: when((ctx, action, resource) => resource.visibility === "public"),
42
+ * write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
43
+ * delete: deny(),
44
+ * },
45
+ * comment: {
46
+ * read: allow(),
47
+ * write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
48
+ * },
49
+ * },
50
+ * });
51
+ *
52
+ * // Check permissions
53
+ * const post = { id: "1", authorId: "user-1", visibility: "public" as const };
54
+ * await policy.can(ctx, "post:read", post); // true
55
+ * await policy.can(ctx, "post:write", post); // depends on ctx.user.id
56
+ * ```
57
+ */
58
+ declare const createPolicy: <TContext extends Context, TResources extends Resources = Resources, TActions extends Actions<TResources> = Actions<TResources>>(config: PermitConfig<TContext, TResources, TActions>) => Policy<TContext, TResources, TActions>;
59
+ /**
60
+ * Merges multiple policies into one, requiring every policy to allow.
61
+ * If any policy denies, the merged policy denies. Policies are evaluated
62
+ * in parallel; every policy is invoked regardless of outcome.
63
+ *
64
+ * @example
65
+ * ```ts
66
+ * const basePolicy = createPolicy({ ... });
67
+ * const adminPolicy = createPolicy({ ... });
68
+ *
69
+ * const merged = mergePoliciesAnd(basePolicy, adminPolicy);
70
+ * // Both policies must allow for the action to be permitted
71
+ * ```
72
+ */
73
+ declare const mergePoliciesAnd: <TContext extends Context, TResources extends Resources = Resources, TActions extends Actions<TResources> = Actions<TResources>>(...policies: Policy<TContext, TResources, TActions>[]) => Policy<TContext, TResources, TActions>;
74
+ /**
75
+ * Merges multiple policies into one, requiring at least one policy to allow.
76
+ * If every policy denies, the merged policy denies. Policies are evaluated
77
+ * in parallel; every policy is invoked regardless of outcome.
78
+ *
79
+ * @example
80
+ * ```ts
81
+ * const guestPolicy = createPolicy({ ... });
82
+ * const memberPolicy = createPolicy({ ... });
83
+ *
84
+ * const merged = mergePoliciesOr(guestPolicy, memberPolicy);
85
+ * // If either policy allows, the action is permitted
86
+ * ```
87
+ */
88
+ declare const mergePoliciesOr: <TContext extends Context, TResources extends Resources = Resources, TActions extends Actions<TResources> = Actions<TResources>>(...policies: Policy<TContext, TResources, TActions>[]) => Policy<TContext, TResources, TActions>;
89
+ //#endregion
90
+ export { createPolicy, mergePoliciesAnd, mergePoliciesOr };
91
+ //# sourceMappingURL=policy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.d.ts","names":[],"sources":["../src/policy.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA8Ga,eACX,iBAAiB,SACjB,mBAAmB,YAAY,WAC/B,iBAAiB,QAAQ,cAAc,QAAQ,aAE/C,QAAQ,aAAa,UAAU,YAAY,cAC1C,OAAO,UAAU,YAAY;;;;;;;;;;;;;;;cAgJnB,mBACX,iBAAiB,SACjB,mBAAmB,YAAY,WAC/B,iBAAiB,QAAQ,cAAc,QAAQ,gBAE5C,UAAU,OAAO,UAAU,YAAY,gBACzC,OAAO,UAAU,YAAY;;;;;;;;;;;;;;;cAiBnB,kBACX,iBAAiB,SACjB,mBAAmB,YAAY,WAC/B,iBAAiB,QAAQ,cAAc,QAAQ,gBAE5C,UAAU,OAAO,UAAU,YAAY,gBACzC,OAAO,UAAU,YAAY"}
package/dist/policy.js ADDED
@@ -0,0 +1,156 @@
1
+ import { PolicyError } from "./errors.js";
2
+ import { createStandardValidator } from "@zap-studio/validation";
3
+ //#region src/policy.ts
4
+ /**
5
+ * Splits a typed `resource:action` permission string into its parts.
6
+ * Returns `null` when the string is malformed (missing/empty part or extra
7
+ * segments) or `resourceType` is not one of `actions`' keys.
8
+ */
9
+ const parsePermission = (permission, actions) => {
10
+ const isValidResourceKey = (value) => Object.keys(actions).includes(value);
11
+ const [resourceTypeValue, actionValue, ...rest] = permission.split(":");
12
+ if (resourceTypeValue === void 0 || resourceTypeValue.length === 0 || actionValue === void 0 || actionValue.length === 0 || rest.length > 0 || !isValidResourceKey(resourceTypeValue)) return null;
13
+ return {
14
+ action: actionValue,
15
+ resourceType: resourceTypeValue
16
+ };
17
+ };
18
+ /**
19
+ * Creates a type-safe policy from resource schemas, actions, and rules.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * import { z } from "zod";
24
+ * import { createPolicy, allow, deny, when } from "@zap-studio/permit";
25
+ * import type { Resources, Actions } from "@zap-studio/permit/types";
26
+ *
27
+ * // Define resource schemas
28
+ * const resources = {
29
+ * post: z.object({
30
+ * id: z.string(),
31
+ * authorId: z.string(),
32
+ * visibility: z.enum(["public", "private"]),
33
+ * }),
34
+ * comment: z.object({
35
+ * id: z.string(),
36
+ * postId: z.string(),
37
+ * authorId: z.string(),
38
+ * }),
39
+ * } satisfies Resources;
40
+ *
41
+ * // Define actions per resource
42
+ * const actions = {
43
+ * post: ["read", "write", "delete"],
44
+ * comment: ["read", "write"],
45
+ * } as const satisfies Actions<typeof resources>;
46
+ *
47
+ * // Define context type
48
+ * type AppContext = { user: { id: string; role: string } };
49
+ *
50
+ * // Create the policy
51
+ * const policy = createPolicy<AppContext>({
52
+ * resources,
53
+ * actions,
54
+ * rules: {
55
+ * post: {
56
+ * read: when((ctx, action, resource) => resource.visibility === "public"),
57
+ * write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
58
+ * delete: deny(),
59
+ * },
60
+ * comment: {
61
+ * read: allow(),
62
+ * write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
63
+ * },
64
+ * },
65
+ * });
66
+ *
67
+ * // Check permissions
68
+ * const post = { id: "1", authorId: "user-1", visibility: "public" as const };
69
+ * await policy.can(ctx, "post:read", post); // true
70
+ * await policy.can(ctx, "post:write", post); // depends on ctx.user.id
71
+ * ```
72
+ */
73
+ const createPolicy = (config) => {
74
+ const { rules, resources, actions } = config;
75
+ const validators = /* @__PURE__ */ new Map();
76
+ const getValidatedResource = async (resourceType, resource) => {
77
+ const validator = validators.get(resourceType);
78
+ if (validator === void 0) return null;
79
+ try {
80
+ const result = await validator(resource);
81
+ if (result.issues) return null;
82
+ return result.value;
83
+ } catch (error) {
84
+ console.warn(`Resource validation failed for ${String(resourceType)}: ${String(error)}`);
85
+ return null;
86
+ }
87
+ };
88
+ const hasAllowedAction = (resourceType, action) => actions[resourceType]?.includes(action) ?? false;
89
+ const evaluatePolicy = (context, resourceType, action, resource) => {
90
+ const policyFn = rules[resourceType]?.[action];
91
+ if (policyFn === void 0) return false;
92
+ try {
93
+ return policyFn(context, action, resource) === "allow";
94
+ } catch (error) {
95
+ console.warn(`Policy evaluation error for ${String(resourceType)}.${action}: ${String(error)}`);
96
+ return false;
97
+ }
98
+ };
99
+ for (const key of Object.keys(resources)) {
100
+ const schema = resources[key];
101
+ if (schema === void 0) throw new PolicyError(`Missing schema for resource: ${String(key)}`);
102
+ const validator = createStandardValidator(schema);
103
+ validators.set(key, async (input) => await validator(input));
104
+ }
105
+ return { async can(context, permission, resource) {
106
+ const parsedPermission = parsePermission(permission, actions);
107
+ if (parsedPermission === null) return false;
108
+ const { action, resourceType } = parsedPermission;
109
+ if (!hasAllowedAction(resourceType, action)) return false;
110
+ const validatedResource = await getValidatedResource(resourceType, resource);
111
+ if (validatedResource === null) return false;
112
+ return evaluatePolicy(context, resourceType, action, validatedResource);
113
+ } };
114
+ };
115
+ const mergePoliciesWithStrategy = (policies, strategy) => ({ async can(context, permission, resource) {
116
+ if (policies.length === 0) return false;
117
+ const results = (await Promise.allSettled(policies.map(async (policy) => await policy.can(context, permission, resource)))).map((result) => {
118
+ if (result.status === "fulfilled") return result.value;
119
+ return false;
120
+ });
121
+ return strategy === "and" ? results.every(Boolean) : results.some(Boolean);
122
+ } });
123
+ /**
124
+ * Merges multiple policies into one, requiring every policy to allow.
125
+ * If any policy denies, the merged policy denies. Policies are evaluated
126
+ * in parallel; every policy is invoked regardless of outcome.
127
+ *
128
+ * @example
129
+ * ```ts
130
+ * const basePolicy = createPolicy({ ... });
131
+ * const adminPolicy = createPolicy({ ... });
132
+ *
133
+ * const merged = mergePoliciesAnd(basePolicy, adminPolicy);
134
+ * // Both policies must allow for the action to be permitted
135
+ * ```
136
+ */
137
+ const mergePoliciesAnd = (...policies) => mergePoliciesWithStrategy(policies, "and");
138
+ /**
139
+ * Merges multiple policies into one, requiring at least one policy to allow.
140
+ * If every policy denies, the merged policy denies. Policies are evaluated
141
+ * in parallel; every policy is invoked regardless of outcome.
142
+ *
143
+ * @example
144
+ * ```ts
145
+ * const guestPolicy = createPolicy({ ... });
146
+ * const memberPolicy = createPolicy({ ... });
147
+ *
148
+ * const merged = mergePoliciesOr(guestPolicy, memberPolicy);
149
+ * // If either policy allows, the action is permitted
150
+ * ```
151
+ */
152
+ const mergePoliciesOr = (...policies) => mergePoliciesWithStrategy(policies, "or");
153
+ //#endregion
154
+ export { createPolicy, mergePoliciesAnd, mergePoliciesOr };
155
+
156
+ //# sourceMappingURL=policy.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"policy.js","names":[],"sources":["../src/policy.ts"],"sourcesContent":["/**\n * Policy creation and composition: `createPolicy`, `mergePoliciesAnd`, and\n * `mergePoliciesOr`.\n *\n * @module @zap-studio/permit/policy\n */\n\nimport type { StandardSchemaV1 } from \"@zap-studio/validation\";\nimport { createStandardValidator } from \"@zap-studio/validation\";\n\nimport { PolicyError } from \"./errors.js\";\nimport type {\n Actions,\n Context,\n InferAction,\n InferResource,\n PermitConfig,\n Policy,\n Resources,\n} from \"./types.js\";\n\n/**\n * Splits a typed `resource:action` permission string into its parts.\n * Returns `null` when the string is malformed (missing/empty part or extra\n * segments) or `resourceType` is not one of `actions`' keys.\n */\nconst parsePermission = <\n TResources extends Resources,\n TActions extends Actions<TResources>,\n K extends keyof TResources & keyof TActions,\n>(\n permission: `${K & string}:${InferAction<TResources, TActions, K> & string}`,\n actions: TActions\n): { action: InferAction<TResources, TActions, K>; resourceType: K } | null => {\n const isValidResourceKey = (value: string): value is K & string =>\n Object.keys(actions).includes(value);\n\n const [resourceTypeValue, actionValue, ...rest] = permission.split(\":\");\n if (\n resourceTypeValue === undefined ||\n resourceTypeValue.length === 0 ||\n actionValue === undefined ||\n actionValue.length === 0 ||\n rest.length > 0 ||\n !isValidResourceKey(resourceTypeValue)\n ) {\n return null;\n }\n\n return {\n action: actionValue,\n resourceType: resourceTypeValue,\n };\n};\n\n/**\n * Creates a type-safe policy from resource schemas, actions, and rules.\n *\n * @example\n * ```ts\n * import { z } from \"zod\";\n * import { createPolicy, allow, deny, when } from \"@zap-studio/permit\";\n * import type { Resources, Actions } from \"@zap-studio/permit/types\";\n *\n * // Define resource schemas\n * const resources = {\n * post: z.object({\n * id: z.string(),\n * authorId: z.string(),\n * visibility: z.enum([\"public\", \"private\"]),\n * }),\n * comment: z.object({\n * id: z.string(),\n * postId: z.string(),\n * authorId: z.string(),\n * }),\n * } satisfies Resources;\n *\n * // Define actions per resource\n * const actions = {\n * post: [\"read\", \"write\", \"delete\"],\n * comment: [\"read\", \"write\"],\n * } as const satisfies Actions<typeof resources>;\n *\n * // Define context type\n * type AppContext = { user: { id: string; role: string } };\n *\n * // Create the policy\n * const policy = createPolicy<AppContext>({\n * resources,\n * actions,\n * rules: {\n * post: {\n * read: when((ctx, action, resource) => resource.visibility === \"public\"),\n * write: when((ctx, action, resource) => ctx.user.id === resource.authorId),\n * delete: deny(),\n * },\n * comment: {\n * read: allow(),\n * write: when((ctx, action, resource) => ctx.user.id === resource.authorId),\n * },\n * },\n * });\n *\n * // Check permissions\n * const post = { id: \"1\", authorId: \"user-1\", visibility: \"public\" as const };\n * await policy.can(ctx, \"post:read\", post); // true\n * await policy.can(ctx, \"post:write\", post); // depends on ctx.user.id\n * ```\n */\nexport const createPolicy = <\n TContext extends Context,\n TResources extends Resources = Resources,\n TActions extends Actions<TResources> = Actions<TResources>,\n>(\n config: PermitConfig<TContext, TResources, TActions>\n): Policy<TContext, TResources, TActions> => {\n const { rules, resources, actions } = config;\n const validators = new Map<\n keyof TResources,\n (input: unknown) => Promise<StandardSchemaV1.Result<unknown>>\n >();\n\n const getValidatedResource = async <K extends keyof TResources>(\n resourceType: K,\n resource: InferResource<TResources, K>\n ): Promise<InferResource<TResources, K> | null> => {\n const validator = validators.get(resourceType);\n if (validator === undefined) {\n return null;\n }\n try {\n const result = await validator(resource);\n if (result.issues) {\n return null;\n }\n return result.value;\n } catch (error) {\n console.warn(\n `Resource validation failed for ${String(resourceType)}: ${String(error)}`\n );\n return null;\n }\n };\n\n const hasAllowedAction = <K extends keyof TResources & keyof TActions>(\n resourceType: K,\n action: InferAction<TResources, TActions, K>\n ): boolean => actions[resourceType]?.includes(action) ?? false;\n\n const evaluatePolicy = <K extends keyof TResources & keyof TActions>(\n context: TContext,\n resourceType: K,\n action: InferAction<TResources, TActions, K>,\n resource: InferResource<TResources, K>\n ): boolean => {\n const policyFn = rules[resourceType]?.[action];\n if (policyFn === undefined) {\n return false;\n }\n\n try {\n return policyFn(context, action, resource) === \"allow\";\n } catch (error) {\n console.warn(\n `Policy evaluation error for ${String(resourceType)}.${action}: ${String(error)}`\n );\n return false;\n }\n };\n\n for (const key of Object.keys(resources) as (keyof TResources)[]) {\n const schema = resources[key];\n if (schema === undefined) {\n throw new PolicyError(`Missing schema for resource: ${String(key)}`);\n }\n const validator = createStandardValidator(schema);\n validators.set(key, async (input: unknown) => await validator(input));\n }\n\n return {\n async can<K extends keyof TResources & keyof TActions>(\n context: TContext,\n permission: `${K & string}:${InferAction<TResources, TActions, K> & string}`,\n resource: InferResource<TResources, K>\n ): Promise<boolean> {\n const parsedPermission = parsePermission<TResources, TActions, K>(\n permission,\n actions\n );\n if (parsedPermission === null) {\n return false;\n }\n\n const { action, resourceType } = parsedPermission;\n if (!hasAllowedAction(resourceType, action)) {\n return false;\n }\n\n const validatedResource = await getValidatedResource(\n resourceType,\n resource\n );\n if (validatedResource === null) {\n return false;\n }\n\n return evaluatePolicy(context, resourceType, action, validatedResource);\n },\n };\n};\n\nconst mergePoliciesWithStrategy = <\n TContext extends Context,\n TResources extends Resources = Resources,\n TActions extends Actions<TResources> = Actions<TResources>,\n>(\n policies: Policy<TContext, TResources, TActions>[],\n strategy: \"and\" | \"or\"\n): Policy<TContext, TResources, TActions> => ({\n async can<K extends keyof TResources & keyof TActions>(\n context: TContext,\n permission: `${K & string}:${InferAction<TResources, TActions, K> & string}`,\n resource: InferResource<TResources, K>\n ): Promise<boolean> {\n if (policies.length === 0) {\n return false;\n }\n\n const settled = await Promise.allSettled(\n policies.map(\n async (policy) => await policy.can(context, permission, resource)\n )\n );\n\n const results = settled.map((result) => {\n if (result.status === \"fulfilled\") {\n return result.value;\n }\n return false;\n });\n\n return strategy === \"and\" ? results.every(Boolean) : results.some(Boolean);\n },\n});\n\n/**\n * Merges multiple policies into one, requiring every policy to allow.\n * If any policy denies, the merged policy denies. Policies are evaluated\n * in parallel; every policy is invoked regardless of outcome.\n *\n * @example\n * ```ts\n * const basePolicy = createPolicy({ ... });\n * const adminPolicy = createPolicy({ ... });\n *\n * const merged = mergePoliciesAnd(basePolicy, adminPolicy);\n * // Both policies must allow for the action to be permitted\n * ```\n */\nexport const mergePoliciesAnd = <\n TContext extends Context,\n TResources extends Resources = Resources,\n TActions extends Actions<TResources> = Actions<TResources>,\n>(\n ...policies: Policy<TContext, TResources, TActions>[]\n): Policy<TContext, TResources, TActions> =>\n mergePoliciesWithStrategy(policies, \"and\");\n\n/**\n * Merges multiple policies into one, requiring at least one policy to allow.\n * If every policy denies, the merged policy denies. Policies are evaluated\n * in parallel; every policy is invoked regardless of outcome.\n *\n * @example\n * ```ts\n * const guestPolicy = createPolicy({ ... });\n * const memberPolicy = createPolicy({ ... });\n *\n * const merged = mergePoliciesOr(guestPolicy, memberPolicy);\n * // If either policy allows, the action is permitted\n * ```\n */\nexport const mergePoliciesOr = <\n TContext extends Context,\n TResources extends Resources = Resources,\n TActions extends Actions<TResources> = Actions<TResources>,\n>(\n ...policies: Policy<TContext, TResources, TActions>[]\n): Policy<TContext, TResources, TActions> =>\n mergePoliciesWithStrategy(policies, \"or\");\n"],"mappings":";;;;;;;;AA0BA,MAAM,mBAKJ,YACA,YAC6E;CAC7E,MAAM,sBAAsB,UAC1B,OAAO,KAAK,OAAO,CAAC,CAAC,SAAS,KAAK;CAErC,MAAM,CAAC,mBAAmB,aAAa,GAAG,QAAQ,WAAW,MAAM,GAAG;CACtE,IACE,sBAAsB,KAAA,KACtB,kBAAkB,WAAW,KAC7B,gBAAgB,KAAA,KAChB,YAAY,WAAW,KACvB,KAAK,SAAS,KACd,CAAC,mBAAmB,iBAAiB,GAErC,OAAO;CAGT,OAAO;EACL,QAAQ;EACR,cAAc;CAChB;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyDA,MAAa,gBAKX,WAC2C;CAC3C,MAAM,EAAE,OAAO,WAAW,YAAY;CACtC,MAAM,6BAAa,IAAI,IAGrB;CAEF,MAAM,uBAAuB,OAC3B,cACA,aACiD;EACjD,MAAM,YAAY,WAAW,IAAI,YAAY;EAC7C,IAAI,cAAc,KAAA,GAChB,OAAO;EAET,IAAI;GACF,MAAM,SAAS,MAAM,UAAU,QAAQ;GACvC,IAAI,OAAO,QACT,OAAO;GAET,OAAO,OAAO;EAChB,SAAS,OAAO;GACd,QAAQ,KACN,kCAAkC,OAAO,YAAY,EAAE,IAAI,OAAO,KAAK,GACzE;GACA,OAAO;EACT;CACF;CAEA,MAAM,oBACJ,cACA,WACY,QAAQ,aAAa,EAAE,SAAS,MAAM,KAAK;CAEzD,MAAM,kBACJ,SACA,cACA,QACA,aACY;EACZ,MAAM,WAAW,MAAM,aAAa,GAAG;EACvC,IAAI,aAAa,KAAA,GACf,OAAO;EAGT,IAAI;GACF,OAAO,SAAS,SAAS,QAAQ,QAAQ,MAAM;EACjD,SAAS,OAAO;GACd,QAAQ,KACN,+BAA+B,OAAO,YAAY,EAAE,GAAG,OAAO,IAAI,OAAO,KAAK,GAChF;GACA,OAAO;EACT;CACF;CAEA,KAAK,MAAM,OAAO,OAAO,KAAK,SAAS,GAA2B;EAChE,MAAM,SAAS,UAAU;EACzB,IAAI,WAAW,KAAA,GACb,MAAM,IAAI,YAAY,gCAAgC,OAAO,GAAG,GAAG;EAErE,MAAM,YAAY,wBAAwB,MAAM;EAChD,WAAW,IAAI,KAAK,OAAO,UAAmB,MAAM,UAAU,KAAK,CAAC;CACtE;CAEA,OAAO,EACL,MAAM,IACJ,SACA,YACA,UACkB;EAClB,MAAM,mBAAmB,gBACvB,YACA,OACF;EACA,IAAI,qBAAqB,MACvB,OAAO;EAGT,MAAM,EAAE,QAAQ,iBAAiB;EACjC,IAAI,CAAC,iBAAiB,cAAc,MAAM,GACxC,OAAO;EAGT,MAAM,oBAAoB,MAAM,qBAC9B,cACA,QACF;EACA,IAAI,sBAAsB,MACxB,OAAO;EAGT,OAAO,eAAe,SAAS,cAAc,QAAQ,iBAAiB;CACxE,EACF;AACF;AAEA,MAAM,6BAKJ,UACA,cAC4C,EAC5C,MAAM,IACJ,SACA,YACA,UACkB;CAClB,IAAI,SAAS,WAAW,GACtB,OAAO;CAST,MAAM,WAAU,MANM,QAAQ,WAC5B,SAAS,IACP,OAAO,WAAW,MAAM,OAAO,IAAI,SAAS,YAAY,QAAQ,CAClE,CACF,EAAA,CAEwB,KAAK,WAAW;EACtC,IAAI,OAAO,WAAW,aACpB,OAAO,OAAO;EAEhB,OAAO;CACT,CAAC;CAED,OAAO,aAAa,QAAQ,QAAQ,MAAM,OAAO,IAAI,QAAQ,KAAK,OAAO;AAC3E,EACF;;;;;;;;;;;;;;;AAgBA,MAAa,oBAKX,GAAG,aAEH,0BAA0B,UAAU,KAAK;;;;;;;;;;;;;;;AAgB3C,MAAa,mBAKX,GAAG,aAEH,0BAA0B,UAAU,IAAI"}