@zap-studio/permit 0.3.2 → 0.3.4

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.
package/dist/index.d.mts DELETED
@@ -1,256 +0,0 @@
1
- import { Actions, ConditionFn, Context, PermitConfig, Policy, PolicyFn, Resources, Role, RoleHierarchy } from "./types.mjs";
2
-
3
- //#region src/index.d.ts
4
- /**
5
- * Returns a policy function that always allows the action.
6
- *
7
- * @example
8
- * ```ts
9
- * const policy = createPolicy({
10
- * resources,
11
- * actions,
12
- * rules: {
13
- * post: {
14
- * read: allow(), // Always allow reading posts
15
- * },
16
- * },
17
- * });
18
- * ```
19
- */
20
- declare function allow<TContext extends Context, TAction extends string = string, TResource = unknown>(): PolicyFn<TContext, TAction, TResource>;
21
- /**
22
- * Returns a policy function that always denies the action.
23
- *
24
- * @example
25
- * ```ts
26
- * const policy = createPolicy({
27
- * resources,
28
- * actions,
29
- * rules: {
30
- * post: {
31
- * delete: deny(), // Never allow deleting posts
32
- * },
33
- * },
34
- * });
35
- * ```
36
- */
37
- declare function deny<TContext extends Context, TAction extends string = string, TResource = unknown>(): PolicyFn<TContext, TAction, TResource>;
38
- /**
39
- * Returns a policy function that allows or denies based on a condition.
40
- *
41
- * @example
42
- * ```ts
43
- * const policy = createPolicy({
44
- * resources,
45
- * actions,
46
- * rules: {
47
- * post: {
48
- * write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
49
- * },
50
- * },
51
- * });
52
- * ```
53
- */
54
- declare function when<TContext extends Context, TAction extends string = string, TResource = unknown>(condition: ConditionFn<TContext, TAction, TResource>): PolicyFn<TContext, TAction, TResource>;
55
- /**
56
- * Returns a condition function that returns `true` if all conditions are met.
57
- *
58
- * @example
59
- * ```ts
60
- * const isOwnerAndPublished = and(
61
- * (ctx, action, resource) => ctx.user.id === resource.authorId,
62
- * (ctx, action, resource) => resource.status === "published"
63
- * );
64
- *
65
- * rules: {
66
- * post: {
67
- * delete: when(isOwnerAndPublished),
68
- * },
69
- * }
70
- * ```
71
- */
72
- declare function and<TContext extends Context, TAction extends string = string, TResource = unknown>(...conditions: ConditionFn<TContext, TAction, TResource>[]): ConditionFn<TContext, TAction, TResource>;
73
- /**
74
- * Returns a condition function that returns `true` if any condition is met.
75
- *
76
- * @example
77
- * ```ts
78
- * const isOwnerOrAdmin = or(
79
- * (ctx, action, resource) => ctx.user.id === resource.authorId,
80
- * (ctx, action, resource) => ctx.user.role === "admin"
81
- * );
82
- *
83
- * rules: {
84
- * post: {
85
- * write: when(isOwnerOrAdmin),
86
- * },
87
- * }
88
- * ```
89
- */
90
- declare function or<TContext extends Context, TAction extends string = string, TResource = unknown>(...conditions: ConditionFn<TContext, TAction, TResource>[]): ConditionFn<TContext, TAction, TResource>;
91
- /**
92
- * Returns a condition function that negates another condition.
93
- *
94
- * @example
95
- * ```ts
96
- * const isNotOwner = not((ctx, action, resource) => ctx.user.id === resource.authorId);
97
- *
98
- * rules: {
99
- * post: {
100
- * like: when(isNotOwner), // Can only like posts you don't own
101
- * },
102
- * }
103
- * ```
104
- */
105
- declare function not<TContext extends Context, TAction extends string = string, TResource = unknown>(condition: ConditionFn<TContext, TAction, TResource>): ConditionFn<TContext, TAction, TResource>;
106
- /**
107
- * Returns a condition function that checks if a context property equals a value.
108
- *
109
- * @example
110
- * ```ts
111
- * rules: {
112
- * post: {
113
- * write: when(has("role", "admin")), // Only admins can write
114
- * },
115
- * }
116
- * ```
117
- */
118
- declare function has<TContext extends Context, K extends keyof TContext>(key: K, value: TContext[K]): ConditionFn<TContext>;
119
- /**
120
- * Collects all roles including inherited ones from a role hierarchy.
121
- *
122
- * @example
123
- * ```ts
124
- * type Role = "guest" | "user" | "admin";
125
- *
126
- * const hierarchy: RoleHierarchy<Role> = {
127
- * guest: [],
128
- * user: ["guest"],
129
- * admin: ["user"],
130
- * };
131
- *
132
- * collectInheritedRoles(["admin"], hierarchy);
133
- * // Returns: Set { "admin", "user", "guest" }
134
- * ```
135
- */
136
- declare function collectInheritedRoles<TRole extends Role = Role>(roles: TRole[], hierarchy: RoleHierarchy<TRole>): Set<TRole>;
137
- /**
138
- * Returns a condition function that checks if the user has a specific role.
139
- * Supports role hierarchy for inherited permissions.
140
- *
141
- * @example
142
- * ```ts
143
- * // Without hierarchy
144
- * rules: {
145
- * post: {
146
- * delete: when(hasRole("admin")),
147
- * },
148
- * }
149
- *
150
- * // With hierarchy
151
- * const hierarchy = {
152
- * guest: [],
153
- * user: ["guest"],
154
- * admin: ["user"],
155
- * };
156
- *
157
- * rules: {
158
- * post: {
159
- * read: when(hasRole("guest", hierarchy)), // Admins and users can also read
160
- * },
161
- * }
162
- * ```
163
- */
164
- declare function hasRole<TContext extends {
165
- role: Role | Role[];
166
- }, TAction extends string = string, TResource = unknown>(role: Role): ConditionFn<TContext, TAction, TResource>;
167
- declare function hasRole<TContext extends {
168
- role: TRole | TRole[];
169
- }, TAction extends string = string, TResource = unknown, TRole extends Role = Role>(role: TRole, hierarchy: RoleHierarchy<TRole>): ConditionFn<TContext, TAction, TResource>;
170
- /**
171
- * Creates a type-safe policy from resource schemas, actions, and rules.
172
- *
173
- * @example
174
- * ```ts
175
- * import { z } from "zod";
176
- * import { createPolicy, allow, deny, when } from "@zap-studio/permit";
177
- * import type { Resources, Actions } from "@zap-studio/permit/types";
178
- *
179
- * // Define resource schemas
180
- * const resources = {
181
- * post: z.object({
182
- * id: z.string(),
183
- * authorId: z.string(),
184
- * visibility: z.enum(["public", "private"]),
185
- * }),
186
- * comment: z.object({
187
- * id: z.string(),
188
- * postId: z.string(),
189
- * authorId: z.string(),
190
- * }),
191
- * } satisfies Resources;
192
- *
193
- * // Define actions per resource
194
- * const actions = {
195
- * post: ["read", "write", "delete"],
196
- * comment: ["read", "write"],
197
- * } as const satisfies Actions<typeof resources>;
198
- *
199
- * // Define context type
200
- * type AppContext = { user: { id: string; role: string } };
201
- *
202
- * // Create the policy
203
- * const policy = createPolicy<AppContext>({
204
- * resources,
205
- * actions,
206
- * rules: {
207
- * post: {
208
- * read: when((ctx, action, resource) => resource.visibility === "public"),
209
- * write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
210
- * delete: deny(),
211
- * },
212
- * comment: {
213
- * read: allow(),
214
- * write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
215
- * },
216
- * },
217
- * });
218
- *
219
- * // Check permissions
220
- * const post = { id: "1", authorId: "user-1", visibility: "public" as const };
221
- * await policy.can(ctx, "post:read", post); // true
222
- * await policy.can(ctx, "post:write", post); // depends on ctx.user.id
223
- * ```
224
- */
225
- declare function createPolicy<TContext extends Context, TResources extends Resources = Resources, TActions extends Actions<TResources> = Actions<TResources>>(config: PermitConfig<TContext, TResources, TActions>): Policy<TContext, TResources, TActions>;
226
- /**
227
- * Merges multiple policies into one using "deny-overrides" strategy.
228
- * If any policy denies, the merged policy denies. All must allow for the result to allow.
229
- *
230
- * @example
231
- * ```ts
232
- * const basePolicy = createPolicy({ ... });
233
- * const adminPolicy = createPolicy({ ... });
234
- *
235
- * const merged = mergePolicies(basePolicy, adminPolicy);
236
- * // Both policies must allow for the action to be permitted
237
- * ```
238
- */
239
- declare function mergePolicies<TContext extends Context, TResources extends Resources = Resources, TActions extends Actions<TResources> = Actions<TResources>>(...policies: Policy<TContext, TResources, TActions>[]): Policy<TContext, TResources, TActions>;
240
- /**
241
- * Merges multiple policies into one using "allow-overrides" strategy.
242
- * If any policy allows, the merged policy allows. All must deny for the result to deny.
243
- *
244
- * @example
245
- * ```ts
246
- * const guestPolicy = createPolicy({ ... });
247
- * const memberPolicy = createPolicy({ ... });
248
- *
249
- * const merged = mergePoliciesAny(guestPolicy, memberPolicy);
250
- * // If either policy allows, the action is permitted
251
- * ```
252
- */
253
- declare function mergePoliciesAny<TContext extends Context, TResources extends Resources = Resources, TActions extends Actions<TResources> = Actions<TResources>>(...policies: Policy<TContext, TResources, TActions>[]): Policy<TContext, TResources, TActions>;
254
- //#endregion
255
- export { allow, and, collectInheritedRoles, createPolicy, deny, has, hasRole, mergePolicies, mergePoliciesAny, not, or, when };
256
- //# sourceMappingURL=index.d.mts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":"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"}
package/dist/index.mjs DELETED
@@ -1,319 +0,0 @@
1
- import { PolicyError } from "./errors.mjs";
2
- import { createStandardValidator } from "@zap-studio/validation";
3
- //#region src/index.ts
4
- /**
5
- * Returns a policy function that always allows the action.
6
- *
7
- * @example
8
- * ```ts
9
- * const policy = createPolicy({
10
- * resources,
11
- * actions,
12
- * rules: {
13
- * post: {
14
- * read: allow(), // Always allow reading posts
15
- * },
16
- * },
17
- * });
18
- * ```
19
- */
20
- function allow() {
21
- return () => "allow";
22
- }
23
- /**
24
- * Returns a policy function that always denies the action.
25
- *
26
- * @example
27
- * ```ts
28
- * const policy = createPolicy({
29
- * resources,
30
- * actions,
31
- * rules: {
32
- * post: {
33
- * delete: deny(), // Never allow deleting posts
34
- * },
35
- * },
36
- * });
37
- * ```
38
- */
39
- function deny() {
40
- return () => "deny";
41
- }
42
- /**
43
- * Returns a policy function that allows or denies based on a condition.
44
- *
45
- * @example
46
- * ```ts
47
- * const policy = createPolicy({
48
- * resources,
49
- * actions,
50
- * rules: {
51
- * post: {
52
- * write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
53
- * },
54
- * },
55
- * });
56
- * ```
57
- */
58
- function when(condition) {
59
- return (context, action, resource) => condition(context, action, resource) ? "allow" : "deny";
60
- }
61
- /**
62
- * Returns a condition function that returns `true` if all conditions are met.
63
- *
64
- * @example
65
- * ```ts
66
- * const isOwnerAndPublished = and(
67
- * (ctx, action, resource) => ctx.user.id === resource.authorId,
68
- * (ctx, action, resource) => resource.status === "published"
69
- * );
70
- *
71
- * rules: {
72
- * post: {
73
- * delete: when(isOwnerAndPublished),
74
- * },
75
- * }
76
- * ```
77
- */
78
- function and(...conditions) {
79
- return (context, action, resource) => conditions.every((condition) => condition(context, action, resource));
80
- }
81
- /**
82
- * Returns a condition function that returns `true` if any condition is met.
83
- *
84
- * @example
85
- * ```ts
86
- * const isOwnerOrAdmin = or(
87
- * (ctx, action, resource) => ctx.user.id === resource.authorId,
88
- * (ctx, action, resource) => ctx.user.role === "admin"
89
- * );
90
- *
91
- * rules: {
92
- * post: {
93
- * write: when(isOwnerOrAdmin),
94
- * },
95
- * }
96
- * ```
97
- */
98
- function or(...conditions) {
99
- return (context, action, resource) => conditions.some((condition) => condition(context, action, resource));
100
- }
101
- /**
102
- * Returns a condition function that negates another condition.
103
- *
104
- * @example
105
- * ```ts
106
- * const isNotOwner = not((ctx, action, resource) => ctx.user.id === resource.authorId);
107
- *
108
- * rules: {
109
- * post: {
110
- * like: when(isNotOwner), // Can only like posts you don't own
111
- * },
112
- * }
113
- * ```
114
- */
115
- function not(condition) {
116
- return (context, action, resource) => !condition(context, action, resource);
117
- }
118
- /**
119
- * Returns a condition function that checks if a context property equals a value.
120
- *
121
- * @example
122
- * ```ts
123
- * rules: {
124
- * post: {
125
- * write: when(has("role", "admin")), // Only admins can write
126
- * },
127
- * }
128
- * ```
129
- */
130
- function has(key, value) {
131
- return (context) => context[key] === value;
132
- }
133
- /**
134
- * Collects all roles including inherited ones from a role hierarchy.
135
- *
136
- * @example
137
- * ```ts
138
- * type Role = "guest" | "user" | "admin";
139
- *
140
- * const hierarchy: RoleHierarchy<Role> = {
141
- * guest: [],
142
- * user: ["guest"],
143
- * admin: ["user"],
144
- * };
145
- *
146
- * collectInheritedRoles(["admin"], hierarchy);
147
- * // Returns: Set { "admin", "user", "guest" }
148
- * ```
149
- */
150
- function collectInheritedRoles(roles, hierarchy) {
151
- const inherited = /* @__PURE__ */ new Set();
152
- function add(role) {
153
- if (!inherited.has(role)) {
154
- inherited.add(role);
155
- (hierarchy[role] ?? []).forEach(add);
156
- }
157
- }
158
- roles.forEach(add);
159
- return inherited;
160
- }
161
- function hasRole(role, hierarchy) {
162
- return (context) => {
163
- const userRoles = Array.isArray(context.role) ? context.role : [context.role];
164
- if (!hierarchy) return userRoles.includes(role);
165
- return collectInheritedRoles(userRoles, hierarchy).has(role);
166
- };
167
- }
168
- /**
169
- * Creates a type-safe policy from resource schemas, actions, and rules.
170
- *
171
- * @example
172
- * ```ts
173
- * import { z } from "zod";
174
- * import { createPolicy, allow, deny, when } from "@zap-studio/permit";
175
- * import type { Resources, Actions } from "@zap-studio/permit/types";
176
- *
177
- * // Define resource schemas
178
- * const resources = {
179
- * post: z.object({
180
- * id: z.string(),
181
- * authorId: z.string(),
182
- * visibility: z.enum(["public", "private"]),
183
- * }),
184
- * comment: z.object({
185
- * id: z.string(),
186
- * postId: z.string(),
187
- * authorId: z.string(),
188
- * }),
189
- * } satisfies Resources;
190
- *
191
- * // Define actions per resource
192
- * const actions = {
193
- * post: ["read", "write", "delete"],
194
- * comment: ["read", "write"],
195
- * } as const satisfies Actions<typeof resources>;
196
- *
197
- * // Define context type
198
- * type AppContext = { user: { id: string; role: string } };
199
- *
200
- * // Create the policy
201
- * const policy = createPolicy<AppContext>({
202
- * resources,
203
- * actions,
204
- * rules: {
205
- * post: {
206
- * read: when((ctx, action, resource) => resource.visibility === "public"),
207
- * write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
208
- * delete: deny(),
209
- * },
210
- * comment: {
211
- * read: allow(),
212
- * write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
213
- * },
214
- * },
215
- * });
216
- *
217
- * // Check permissions
218
- * const post = { id: "1", authorId: "user-1", visibility: "public" as const };
219
- * await policy.can(ctx, "post:read", post); // true
220
- * await policy.can(ctx, "post:write", post); // depends on ctx.user.id
221
- * ```
222
- */
223
- function createPolicy(config) {
224
- const { rules, resources, actions } = config;
225
- const validators = /* @__PURE__ */ new Map();
226
- const getValidatedResource = async (resourceType, resource) => {
227
- const validator = validators.get(resourceType);
228
- if (!validator) return null;
229
- try {
230
- const result = await validator(resource);
231
- if (result.issues) return null;
232
- return result.value;
233
- } catch (error) {
234
- console.warn(`Resource validation failed for ${String(resourceType)}: ${String(error)}`);
235
- return null;
236
- }
237
- };
238
- const parsePermission = (permission) => {
239
- const [resourceTypeValue, actionValue, ...rest] = permission.split(":");
240
- if (!resourceTypeValue || !actionValue || rest.length > 0) return null;
241
- return {
242
- action: actionValue,
243
- resourceType: resourceTypeValue
244
- };
245
- };
246
- const hasAllowedAction = (resourceType, action) => actions[resourceType]?.includes(action) ?? false;
247
- const evaluatePolicy = (context, resourceType, action, resource) => {
248
- const policyFn = rules[resourceType]?.[action];
249
- if (!policyFn) return false;
250
- try {
251
- return policyFn(context, action, resource) === "allow";
252
- } catch (error) {
253
- console.warn(`Policy evaluation error for ${String(resourceType)}.${String(action)}: ${String(error)}`);
254
- return false;
255
- }
256
- };
257
- for (const key of Object.keys(resources)) {
258
- const schema = resources[key];
259
- if (!schema) throw new PolicyError(`Missing schema for resource: ${String(key)}`);
260
- const validator = createStandardValidator(schema);
261
- validators.set(key, async (input) => validator(input));
262
- }
263
- return { async can(context, permission, resource) {
264
- const parsedPermission = parsePermission(permission);
265
- if (!parsedPermission) return false;
266
- const { action, resourceType } = parsedPermission;
267
- if (!hasAllowedAction(resourceType, action)) return false;
268
- const validatedResource = await getValidatedResource(resourceType, resource);
269
- if (!validatedResource) return false;
270
- return evaluatePolicy(context, resourceType, action, validatedResource);
271
- } };
272
- }
273
- /**
274
- * Merges multiple policies into one using "deny-overrides" strategy.
275
- * If any policy denies, the merged policy denies. All must allow for the result to allow.
276
- *
277
- * @example
278
- * ```ts
279
- * const basePolicy = createPolicy({ ... });
280
- * const adminPolicy = createPolicy({ ... });
281
- *
282
- * const merged = mergePolicies(basePolicy, adminPolicy);
283
- * // Both policies must allow for the action to be permitted
284
- * ```
285
- */
286
- function mergePolicies(...policies) {
287
- return mergePoliciesWithStrategy(policies, "deny-overrides");
288
- }
289
- /**
290
- * Merges multiple policies into one using "allow-overrides" strategy.
291
- * If any policy allows, the merged policy allows. All must deny for the result to deny.
292
- *
293
- * @example
294
- * ```ts
295
- * const guestPolicy = createPolicy({ ... });
296
- * const memberPolicy = createPolicy({ ... });
297
- *
298
- * const merged = mergePoliciesAny(guestPolicy, memberPolicy);
299
- * // If either policy allows, the action is permitted
300
- * ```
301
- */
302
- function mergePoliciesAny(...policies) {
303
- return mergePoliciesWithStrategy(policies, "allow-overrides");
304
- }
305
- function mergePoliciesWithStrategy(policies, strategy) {
306
- return { async can(context, permission, resource) {
307
- if (!policies.length) return false;
308
- for (const policy of policies) {
309
- const allowed = await policy.can(context, permission, resource);
310
- if (strategy === "allow-overrides" && allowed) return true;
311
- if (strategy === "deny-overrides" && !allowed) return false;
312
- }
313
- return strategy === "deny-overrides";
314
- } };
315
- }
316
- //#endregion
317
- export { allow, and, collectInheritedRoles, createPolicy, deny, has, hasRole, mergePolicies, mergePoliciesAny, not, or, when };
318
-
319
- //# sourceMappingURL=index.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@zap-studio/validation\";\nimport { createStandardValidator } from \"@zap-studio/validation\";\n\nimport { PolicyError } from \"./errors.js\";\nimport type {\n Actions,\n ConditionFn,\n Context,\n InferAction,\n InferResource,\n PermitConfig,\n Policy,\n PolicyFn,\n Resources,\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 function allow<\n TContext extends Context,\n TAction extends string = string,\n TResource = unknown,\n>(): PolicyFn<TContext, TAction, TResource> {\n return () => \"allow\";\n}\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 function deny<\n TContext extends Context,\n TAction extends string = string,\n TResource = unknown,\n>(): PolicyFn<TContext, TAction, TResource> {\n return () => \"deny\";\n}\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 function when<\n TContext extends Context,\n TAction extends string = string,\n TResource = unknown,\n>(condition: ConditionFn<TContext, TAction, TResource>): PolicyFn<TContext, TAction, TResource> {\n return (context, action, resource) => (condition(context, action, resource) ? \"allow\" : \"deny\");\n}\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 function and<TContext extends Context, TAction extends string = string, TResource = unknown>(\n ...conditions: ConditionFn<TContext, TAction, TResource>[]\n): ConditionFn<TContext, TAction, TResource> {\n return (context, action, resource) =>\n conditions.every((condition) => condition(context, action, resource));\n}\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 function or<TContext extends Context, TAction extends string = string, TResource = unknown>(\n ...conditions: ConditionFn<TContext, TAction, TResource>[]\n): ConditionFn<TContext, TAction, TResource> {\n return (context, action, resource) =>\n conditions.some((condition) => condition(context, action, resource));\n}\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 function not<TContext extends Context, TAction extends string = string, TResource = unknown>(\n condition: ConditionFn<TContext, TAction, TResource>,\n): ConditionFn<TContext, TAction, TResource> {\n return (context, action, resource) => !condition(context, action, resource);\n}\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 function has<TContext extends Context, K extends keyof TContext>(\n key: K,\n value: TContext[K],\n): ConditionFn<TContext> {\n return (context) => context[key] === value;\n}\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 function collectInheritedRoles<TRole extends Role = Role>(\n roles: TRole[],\n hierarchy: RoleHierarchy<TRole>,\n): Set<TRole> {\n const inherited = new Set<TRole>();\n\n function add(role: TRole) {\n if (!inherited.has(role)) {\n inherited.add(role);\n const parents = hierarchy[role] ?? [];\n parents.forEach(add); // recursively add parent roles\n }\n }\n\n roles.forEach(add);\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 function hasRole<\n TContext extends { role: Role | Role[] },\n TAction extends string = string,\n TResource = unknown,\n>(role: Role): ConditionFn<TContext, TAction, TResource>;\n\nexport function hasRole<\n TContext extends { role: TRole | TRole[] },\n TAction extends string = string,\n TResource = unknown,\n TRole extends Role = Role,\n>(role: TRole, hierarchy: RoleHierarchy<TRole>): ConditionFn<TContext, TAction, TResource>;\n\nexport function hasRole<\n TContext extends { role: Role | Role[] },\n TAction extends string = string,\n TResource = unknown,\n>(role: Role, hierarchy?: RoleHierarchy<Role>): ConditionFn<TContext, TAction, TResource> {\n return (context) => {\n const userRoles = Array.isArray(context.role) ? context.role : [context.role];\n\n if (!hierarchy) {\n return userRoles.includes(role);\n }\n\n const inherited = collectInheritedRoles(userRoles, hierarchy);\n return inherited.has(role);\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 function createPolicy<\n TContext extends Context,\n TResources extends Resources = Resources,\n TActions extends Actions<TResources> = Actions<TResources>,\n>(config: PermitConfig<TContext, TResources, TActions>): 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) {\n return null;\n }\n try {\n const result = await validator(resource);\n if (result.issues) {\n return null;\n }\n return result.value as InferResource<TResources, K>;\n } catch (error) {\n console.warn(`Resource validation failed for ${String(resourceType)}: ${String(error)}`);\n return null;\n }\n };\n\n const parsePermission = <K extends keyof TResources & keyof TActions>(\n permission: `${K & string}:${InferAction<TActions, K> & string}`,\n ): { action: InferAction<TActions, K>; resourceType: K } | null => {\n const [resourceTypeValue, actionValue, ...rest] = permission.split(\":\");\n if (!resourceTypeValue || !actionValue || rest.length > 0) {\n return null;\n }\n\n return {\n action: actionValue as InferAction<TActions, K>,\n resourceType: resourceTypeValue as K,\n };\n };\n\n const hasAllowedAction = <K extends keyof TResources & keyof TActions>(\n resourceType: K,\n action: InferAction<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<TActions, K>,\n resource: InferResource<TResources, K>,\n ): boolean => {\n const policyFn = rules[resourceType]?.[action];\n if (!policyFn) {\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)}.${String(action)}: ${String(error)}`,\n );\n return false;\n }\n };\n\n for (const key of Object.keys(resources) as Array<keyof TResources>) {\n const schema = resources[key];\n if (!schema) {\n throw new PolicyError(`Missing schema for resource: ${String(key)}`);\n }\n const validator = createStandardValidator(schema);\n validators.set(key, async (input: unknown) => validator(input));\n }\n\n return {\n async can<K extends keyof TResources & keyof TActions>(\n context: TContext,\n permission: `${K & string}:${InferAction<TActions, K> & string}`,\n resource: InferResource<TResources, K>,\n ): Promise<boolean> {\n const parsedPermission = parsePermission(permission);\n if (!parsedPermission) {\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(resourceType, resource);\n if (!validatedResource) {\n return false;\n }\n\n return evaluatePolicy(context, resourceType, action, validatedResource);\n },\n };\n}\n\n/**\n * Merges multiple policies into one using \"deny-overrides\" strategy.\n * If any policy denies, the merged policy denies. All must allow for the result to allow.\n *\n * @example\n * ```ts\n * const basePolicy = createPolicy({ ... });\n * const adminPolicy = createPolicy({ ... });\n *\n * const merged = mergePolicies(basePolicy, adminPolicy);\n * // Both policies must allow for the action to be permitted\n * ```\n */\nexport function mergePolicies<\n TContext extends Context,\n TResources extends Resources = Resources,\n TActions extends Actions<TResources> = Actions<TResources>,\n>(...policies: Policy<TContext, TResources, TActions>[]): Policy<TContext, TResources, TActions> {\n return mergePoliciesWithStrategy(policies, \"deny-overrides\");\n}\n\n/**\n * Merges multiple policies into one using \"allow-overrides\" strategy.\n * If any policy allows, the merged policy allows. All must deny for the result to deny.\n *\n * @example\n * ```ts\n * const guestPolicy = createPolicy({ ... });\n * const memberPolicy = createPolicy({ ... });\n *\n * const merged = mergePoliciesAny(guestPolicy, memberPolicy);\n * // If either policy allows, the action is permitted\n * ```\n */\nexport function mergePoliciesAny<\n TContext extends Context,\n TResources extends Resources = Resources,\n TActions extends Actions<TResources> = Actions<TResources>,\n>(...policies: Policy<TContext, TResources, TActions>[]): Policy<TContext, TResources, TActions> {\n return mergePoliciesWithStrategy(policies, \"allow-overrides\");\n}\n\nfunction 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: \"allow-overrides\" | \"deny-overrides\",\n): Policy<TContext, TResources, TActions> {\n return {\n async can<K extends keyof TResources & keyof TActions>(\n context: TContext,\n permission: `${K & string}:${InferAction<TActions, K> & string}`,\n resource: InferResource<TResources, K>,\n ): Promise<boolean> {\n if (!policies.length) {\n return false;\n }\n for (const policy of policies) {\n const allowed = await policy.can(context, permission, resource);\n\n if (strategy === \"allow-overrides\" && allowed) {\n return true;\n }\n if (strategy === \"deny-overrides\" && !allowed) {\n return false;\n }\n }\n return strategy === \"deny-overrides\";\n },\n };\n}\n"],"mappings":"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"}