@zap-studio/permit 0.3.2 → 0.3.3

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.mjs CHANGED
@@ -17,9 +17,7 @@ import { createStandardValidator } from "@zap-studio/validation";
17
17
  * });
18
18
  * ```
19
19
  */
20
- function allow() {
21
- return () => "allow";
22
- }
20
+ const allow = () => () => "allow";
23
21
  /**
24
22
  * Returns a policy function that always denies the action.
25
23
  *
@@ -36,9 +34,7 @@ function allow() {
36
34
  * });
37
35
  * ```
38
36
  */
39
- function deny() {
40
- return () => "deny";
41
- }
37
+ const deny = () => () => "deny";
42
38
  /**
43
39
  * Returns a policy function that allows or denies based on a condition.
44
40
  *
@@ -55,9 +51,7 @@ function deny() {
55
51
  * });
56
52
  * ```
57
53
  */
58
- function when(condition) {
59
- return (context, action, resource) => condition(context, action, resource) ? "allow" : "deny";
60
- }
54
+ const when = (condition) => (context, action, resource) => condition(context, action, resource) ? "allow" : "deny";
61
55
  /**
62
56
  * Returns a condition function that returns `true` if all conditions are met.
63
57
  *
@@ -75,9 +69,7 @@ function when(condition) {
75
69
  * }
76
70
  * ```
77
71
  */
78
- function and(...conditions) {
79
- return (context, action, resource) => conditions.every((condition) => condition(context, action, resource));
80
- }
72
+ const and = (...conditions) => (context, action, resource) => conditions.every((condition) => condition(context, action, resource));
81
73
  /**
82
74
  * Returns a condition function that returns `true` if any condition is met.
83
75
  *
@@ -95,9 +87,7 @@ function and(...conditions) {
95
87
  * }
96
88
  * ```
97
89
  */
98
- function or(...conditions) {
99
- return (context, action, resource) => conditions.some((condition) => condition(context, action, resource));
100
- }
90
+ const or = (...conditions) => (context, action, resource) => conditions.some((condition) => condition(context, action, resource));
101
91
  /**
102
92
  * Returns a condition function that negates another condition.
103
93
  *
@@ -112,9 +102,7 @@ function or(...conditions) {
112
102
  * }
113
103
  * ```
114
104
  */
115
- function not(condition) {
116
- return (context, action, resource) => !condition(context, action, resource);
117
- }
105
+ const not = (condition) => (context, action, resource) => !condition(context, action, resource);
118
106
  /**
119
107
  * Returns a condition function that checks if a context property equals a value.
120
108
  *
@@ -127,9 +115,7 @@ function not(condition) {
127
115
  * }
128
116
  * ```
129
117
  */
130
- function has(key, value) {
131
- return (context) => context[key] === value;
132
- }
118
+ const has = (key, value) => (context) => context[key] === value;
133
119
  /**
134
120
  * Collects all roles including inherited ones from a role hierarchy.
135
121
  *
@@ -147,24 +133,61 @@ function has(key, value) {
147
133
  * // Returns: Set { "admin", "user", "guest" }
148
134
  * ```
149
135
  */
150
- function collectInheritedRoles(roles, hierarchy) {
136
+ const collectInheritedRoles = (roles, hierarchy) => {
151
137
  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);
138
+ const add = (role) => {
139
+ if (inherited.has(role)) return;
140
+ inherited.add(role);
141
+ const parents = hierarchy[role] ?? [];
142
+ for (const parent of parents) add(parent);
143
+ };
144
+ for (const role of roles) add(role);
159
145
  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);
146
+ };
147
+ /**
148
+ * Returns a condition function that checks if the user has a specific role.
149
+ * Supports role hierarchy for inherited permissions.
150
+ *
151
+ * @example
152
+ * ```ts
153
+ * // Without hierarchy
154
+ * rules: {
155
+ * post: {
156
+ * delete: when(hasRole("admin")),
157
+ * },
158
+ * }
159
+ *
160
+ * // With hierarchy
161
+ * const hierarchy = {
162
+ * guest: [],
163
+ * user: ["guest"],
164
+ * admin: ["user"],
165
+ * };
166
+ *
167
+ * rules: {
168
+ * post: {
169
+ * read: when(hasRole("guest", hierarchy)), // Admins and users can also read
170
+ * },
171
+ * }
172
+ * ```
173
+ */
174
+ const hasRole = (role, hierarchy) => (context) => {
175
+ const userRoles = Array.isArray(context.role) ? context.role : [context.role];
176
+ if (hierarchy === void 0) return userRoles.includes(role);
177
+ return collectInheritedRoles(userRoles, hierarchy).has(role);
178
+ };
179
+ /**
180
+ * Splits a typed `resource:action` permission string into its parts.
181
+ * Returns `null` when the string is malformed (missing/empty part or extra segments).
182
+ */
183
+ const parsePermission = (permission) => {
184
+ const [resourceTypeValue, actionValue, ...rest] = permission.split(":");
185
+ if (resourceTypeValue === void 0 || resourceTypeValue.length === 0 || actionValue === void 0 || actionValue.length === 0 || rest.length > 0) return null;
186
+ return {
187
+ action: actionValue,
188
+ resourceType: resourceTypeValue
166
189
  };
167
- }
190
+ };
168
191
  /**
169
192
  * Creates a type-safe policy from resource schemas, actions, and rules.
170
193
  *
@@ -220,12 +243,12 @@ function hasRole(role, hierarchy) {
220
243
  * await policy.can(ctx, "post:write", post); // depends on ctx.user.id
221
244
  * ```
222
245
  */
223
- function createPolicy(config) {
246
+ const createPolicy = (config) => {
224
247
  const { rules, resources, actions } = config;
225
248
  const validators = /* @__PURE__ */ new Map();
226
249
  const getValidatedResource = async (resourceType, resource) => {
227
250
  const validator = validators.get(resourceType);
228
- if (!validator) return null;
251
+ if (validator === void 0) return null;
229
252
  try {
230
253
  const result = await validator(resource);
231
254
  if (result.issues) return null;
@@ -235,41 +258,42 @@ function createPolicy(config) {
235
258
  return null;
236
259
  }
237
260
  };
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
261
  const hasAllowedAction = (resourceType, action) => actions[resourceType]?.includes(action) ?? false;
247
262
  const evaluatePolicy = (context, resourceType, action, resource) => {
248
263
  const policyFn = rules[resourceType]?.[action];
249
- if (!policyFn) return false;
264
+ if (policyFn === void 0) return false;
250
265
  try {
251
266
  return policyFn(context, action, resource) === "allow";
252
267
  } catch (error) {
253
- console.warn(`Policy evaluation error for ${String(resourceType)}.${String(action)}: ${String(error)}`);
268
+ console.warn(`Policy evaluation error for ${String(resourceType)}.${action}: ${String(error)}`);
254
269
  return false;
255
270
  }
256
271
  };
257
272
  for (const key of Object.keys(resources)) {
258
273
  const schema = resources[key];
259
- if (!schema) throw new PolicyError(`Missing schema for resource: ${String(key)}`);
274
+ if (schema === void 0) throw new PolicyError(`Missing schema for resource: ${String(key)}`);
260
275
  const validator = createStandardValidator(schema);
261
- validators.set(key, async (input) => validator(input));
276
+ validators.set(key, async (input) => await validator(input));
262
277
  }
263
278
  return { async can(context, permission, resource) {
264
279
  const parsedPermission = parsePermission(permission);
265
- if (!parsedPermission) return false;
280
+ if (parsedPermission === null) return false;
266
281
  const { action, resourceType } = parsedPermission;
267
282
  if (!hasAllowedAction(resourceType, action)) return false;
268
283
  const validatedResource = await getValidatedResource(resourceType, resource);
269
- if (!validatedResource) return false;
284
+ if (validatedResource === null) return false;
270
285
  return evaluatePolicy(context, resourceType, action, validatedResource);
271
286
  } };
272
- }
287
+ };
288
+ const mergePoliciesWithStrategy = (policies, strategy) => ({ async can(context, permission, resource) {
289
+ if (policies.length === 0) return false;
290
+ for (const policy of policies) {
291
+ const allowed = await policy.can(context, permission, resource);
292
+ if (strategy === "allow-overrides" && allowed) return true;
293
+ if (strategy === "deny-overrides" && !allowed) return false;
294
+ }
295
+ return strategy === "deny-overrides";
296
+ } });
273
297
  /**
274
298
  * Merges multiple policies into one using "deny-overrides" strategy.
275
299
  * If any policy denies, the merged policy denies. All must allow for the result to allow.
@@ -283,9 +307,7 @@ function createPolicy(config) {
283
307
  * // Both policies must allow for the action to be permitted
284
308
  * ```
285
309
  */
286
- function mergePolicies(...policies) {
287
- return mergePoliciesWithStrategy(policies, "deny-overrides");
288
- }
310
+ const mergePolicies = (...policies) => mergePoliciesWithStrategy(policies, "deny-overrides");
289
311
  /**
290
312
  * Merges multiple policies into one using "allow-overrides" strategy.
291
313
  * If any policy allows, the merged policy allows. All must deny for the result to deny.
@@ -299,20 +321,7 @@ function mergePolicies(...policies) {
299
321
  * // If either policy allows, the action is permitted
300
322
  * ```
301
323
  */
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
- }
324
+ const mergePoliciesAny = (...policies) => mergePoliciesWithStrategy(policies, "allow-overrides");
316
325
  //#endregion
317
326
  export { allow, and, collectInheritedRoles, createPolicy, deny, has, hasRole, mergePolicies, mergePoliciesAny, not, or, when };
318
327
 
@@ -1 +1 @@
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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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * Policy composition and evaluation utilities.\n *\n * @module @zap-studio/permit\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 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 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 parents = hierarchy[role] ?? [];\n for (const parent of parents) {\n add(parent);\n }\n };\n\n for (const role of roles) {\n add(role);\n }\n return inherited;\n};\n\n/**\n * Call signatures for {@link hasRole}, preserving the with/without hierarchy overloads.\n */\ninterface HasRoleFn {\n <\n TContext extends { role: Role | Role[] },\n TAction extends string = string,\n TResource = unknown,\n >(\n role: Role\n ): ConditionFn<TContext, TAction, TResource>;\n <\n TContext extends { role: TRole | TRole[] },\n TAction extends string = string,\n TResource = unknown,\n TRole extends Role = Role,\n >(\n role: TRole,\n hierarchy: RoleHierarchy<TRole>\n ): ConditionFn<TContext, TAction, TResource>;\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\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 segments).\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<TActions, K> & string}`\n): { action: InferAction<TActions, K>; resourceType: K } | null => {\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 ) {\n return null;\n }\n\n return {\n action: actionValue,\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Parsed permission strings are constrained by the typed permission template.\n resourceType: resourceTypeValue as K,\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<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 === 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<TActions, K> & string}`,\n resource: InferResource<TResources, K>\n ): Promise<boolean> {\n const parsedPermission = parsePermission<TResources, TActions, K>(\n permission\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: \"allow-overrides\" | \"deny-overrides\"\n): Policy<TContext, TResources, TActions> => ({\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 === 0) {\n return false;\n }\n for (const policy of policies) {\n // oxlint-disable-next-line no-await-in-loop -- Policies must evaluate sequentially to preserve short-circuit semantics.\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/**\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 const mergePolicies = <\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, \"deny-overrides\");\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 const mergePoliciesAny = <\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, \"allow-overrides\");\n"],"mappings":"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"}
package/dist/types.d.mts CHANGED
@@ -1,142 +1,141 @@
1
1
  import { StandardSchemaV1 } from "@zap-studio/validation";
2
-
3
2
  //#region src/types.d.ts
4
3
  /**
5
- * Represents the possible outcomes of a policy decision.
6
- * - "allow": The action is permitted.
7
- * - "deny": The action is not permitted.
8
- */
4
+ * Represents the possible outcomes of a policy decision.
5
+ * - "allow": The action is permitted.
6
+ * - "deny": The action is not permitted.
7
+ */
9
8
  type Decision = "allow" | "deny";
10
9
  /**
11
- * Represents the context in which a policy decision is made.
12
- * Can include user information, environment, or any relevant data.
13
- */
10
+ * Represents the context in which a policy decision is made.
11
+ * Can include user information, environment, or any relevant data.
12
+ */
14
13
  type Context<TContext = unknown> = TContext;
15
14
  /**
16
- * Represents a role within the system.
17
- */
15
+ * Represents a role within the system.
16
+ */
18
17
  type Role<TRole extends string = string> = TRole;
19
18
  /**
20
- * Represents a role hierarchy within the system.
21
- * Maps each role to an array of roles it inherits from.
22
- *
23
- * @example
24
- * ```ts
25
- * type Roles = "guest" | "user" | "admin";
26
- *
27
- * const hierarchy: RoleHierarchy<Roles> = {
28
- * guest: [],
29
- * user: ["guest"],
30
- * admin: ["user"],
31
- * };
32
- * ```
33
- */
19
+ * Represents a role hierarchy within the system.
20
+ * Maps each role to an array of roles it inherits from.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * type Roles = "guest" | "user" | "admin";
25
+ *
26
+ * const hierarchy: RoleHierarchy<Roles> = {
27
+ * guest: [],
28
+ * user: ["guest"],
29
+ * admin: ["user"],
30
+ * };
31
+ * ```
32
+ */
34
33
  type RoleHierarchy<TRole extends Role = Role> = Record<TRole, TRole[]>;
35
34
  /**
36
- * Type helper for defining resource schemas using Standard Schema.
37
- * Use with `satisfies` to ensure type safety when defining resources.
38
- *
39
- * @example
40
- * ```ts
41
- * import { z } from "zod";
42
- * import type { Resources } from "@zap-studio/permit/types";
43
- *
44
- * const resources = {
45
- * post: z.object({ id: z.string(), authorId: z.string() }),
46
- * comment: z.object({ id: z.string(), postId: z.string() }),
47
- * } satisfies Resources;
48
- * ```
49
- */
35
+ * Type helper for defining resource schemas using Standard Schema.
36
+ * Use with `satisfies` to ensure type safety when defining resources.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * import { z } from "zod";
41
+ * import type { Resources } from "@zap-studio/permit/types";
42
+ *
43
+ * const resources = {
44
+ * post: z.object({ id: z.string(), authorId: z.string() }),
45
+ * comment: z.object({ id: z.string(), postId: z.string() }),
46
+ * } satisfies Resources;
47
+ * ```
48
+ */
50
49
  type Resources<TResourceKey extends string = string> = Record<TResourceKey, StandardSchemaV1>;
51
50
  /**
52
- * Type helper for defining actions per resource.
53
- * Use with `satisfies` to ensure keys match the resource definitions.
54
- *
55
- * @example
56
- * ```ts
57
- * import type { Actions } from "@zap-studio/permit/types";
58
- *
59
- * const actions = {
60
- * post: ["read", "write", "delete"],
61
- * comment: ["read", "write"],
62
- * } as const satisfies Actions<typeof resources>;
63
- * ```
64
- */
65
- type Actions<TResources extends Resources> = { [K in keyof TResources]: readonly string[] };
51
+ * Type helper for defining actions per resource.
52
+ * Use with `satisfies` to ensure keys match the resource definitions.
53
+ *
54
+ * @example
55
+ * ```ts
56
+ * import type { Actions } from "@zap-studio/permit/types";
57
+ *
58
+ * const actions = {
59
+ * post: ["read", "write", "delete"],
60
+ * comment: ["read", "write"],
61
+ * } as const satisfies Actions<typeof resources>;
62
+ * ```
63
+ */
64
+ type Actions<TResources extends Resources> = { [K in keyof TResources]: readonly string[]; };
66
65
  /**
67
- * Infers the output type from a Standard Schema.
68
- */
66
+ * Infers the output type from a Standard Schema.
67
+ */
69
68
  type InferResource<TResources extends Resources, TResourceKey extends keyof TResources> = StandardSchemaV1.InferOutput<TResources[TResourceKey]>;
70
69
  /**
71
- * Infers the action union type for a specific resource.
72
- */
70
+ * Infers the action union type for a specific resource.
71
+ */
73
72
  type InferAction<TActions extends Record<string, readonly string[]>, K extends keyof TActions> = TActions[K][number];
74
73
  /**
75
- * Infers the permission-string union for all resource/action combinations.
76
- *
77
- * @example
78
- * ```ts
79
- * type Permission = InferPermission<typeof resources, typeof actions>;
80
- * // "post:read" | "post:write" | "comment:read"
81
- * ```
82
- */
83
- type InferPermission<TResources extends Resources, TActions extends Actions<TResources>> = { [K in keyof TResources & keyof TActions]: `${K & string}:${InferAction<TActions, K> & string}` }[keyof TResources & keyof TActions];
74
+ * Infers the permission-string union for all resource/action combinations.
75
+ *
76
+ * @example
77
+ * ```ts
78
+ * type Permission = InferPermission<typeof resources, typeof actions>;
79
+ * // "post:read" | "post:write" | "comment:read"
80
+ * ```
81
+ */
82
+ type InferPermission<TResources extends Resources, TActions extends Actions<TResources>> = { [K in keyof TResources & keyof TActions]: `${K & string}:${InferAction<TActions, K> & string}`; }[keyof TResources & keyof TActions];
84
83
  /**
85
- * A function that determines whether a given action on a resource is allowed in a specific context.
86
- */
84
+ * A function that determines whether a given action on a resource is allowed in a specific context.
85
+ */
87
86
  type PolicyFn<TContext extends Context, TAction extends string = string, TResource = unknown> = (context: TContext, action: TAction, resource: TResource) => Decision;
88
87
  /**
89
- * A function that evaluates a condition for a given action and resource in a specific context.
90
- */
88
+ * A function that evaluates a condition for a given action and resource in a specific context.
89
+ */
91
90
  type ConditionFn<TContext extends Context, TAction extends string = string, TResource = unknown> = (context: TContext, action: TAction, resource: TResource) => boolean;
92
91
  /**
93
- * Maps actions to their corresponding policy functions for a specific resource.
94
- */
95
- type ActionPolicyMap<TContext extends Context, TAction extends string = string, TResource = unknown> = { [A in TAction]?: PolicyFn<TContext, A, TResource> };
92
+ * Maps actions to their corresponding policy functions for a specific resource.
93
+ */
94
+ type ActionPolicyMap<TContext extends Context, TAction extends string = string, TResource = unknown> = { [A in TAction]?: PolicyFn<TContext, A, TResource>; };
96
95
  /**
97
- * Defines the rules for each resource and action combination.
98
- * Each resource key maps to an object where each action key maps to a policy function.
99
- */
100
- type Rules<TContext extends Context, TResources extends Resources = Resources, TActions extends Actions<TResources> = Actions<TResources>> = { [K in keyof TResources & keyof TActions]: ActionPolicyMap<TContext, InferAction<TActions, K>, InferResource<TResources, K>> };
96
+ * Defines the rules for each resource and action combination.
97
+ * Each resource key maps to an object where each action key maps to a policy function.
98
+ */
99
+ type Rules<TContext extends Context, TResources extends Resources = Resources, TActions extends Actions<TResources> = Actions<TResources>> = { [K in keyof TResources & keyof TActions]: ActionPolicyMap<TContext, InferAction<TActions, K>, InferResource<TResources, K>>; };
101
100
  /**
102
- * Configuration object for creating a permit policy.
103
- *
104
- * @example
105
- * ```ts
106
- * const config: PermitConfig<MyContext> = {
107
- * resources,
108
- * actions,
109
- * rules: {
110
- * post: { read: allow(), write: deny() },
111
- * },
112
- * };
113
- * ```
114
- */
101
+ * Configuration object for creating a permit policy.
102
+ *
103
+ * @example
104
+ * ```ts
105
+ * const config: PermitConfig<MyContext> = {
106
+ * resources,
107
+ * actions,
108
+ * rules: {
109
+ * post: { read: allow(), write: deny() },
110
+ * },
111
+ * };
112
+ * ```
113
+ */
115
114
  interface PermitConfig<TContext extends Context, TResources extends Resources = Resources, TActions extends Actions<TResources> = Actions<TResources>> {
116
115
  actions: TActions;
117
116
  resources: TResources;
118
117
  rules: Rules<TContext, TResources, TActions>;
119
118
  }
120
119
  /**
121
- * Represents a policy object that can evaluate permissions.
122
- * The `can` method checks if a given action is permitted on a resource in a specific context.
123
- *
124
- * @example
125
- * ```ts
126
- * const policy: Policy<MyContext> = createPolicy({
127
- * resources,
128
- * actions,
129
- * rules: { ... },
130
- * });
131
- *
132
- * await policy.can(ctx, "post:read", postData); // true or false
133
- * ```
134
- */
120
+ * Represents a policy object that can evaluate permissions.
121
+ * The `can` method checks if a given action is permitted on a resource in a specific context.
122
+ *
123
+ * @example
124
+ * ```ts
125
+ * const policy: Policy<MyContext> = createPolicy({
126
+ * resources,
127
+ * actions,
128
+ * rules: { ... },
129
+ * });
130
+ *
131
+ * await policy.can(ctx, "post:read", postData); // true or false
132
+ * ```
133
+ */
135
134
  interface Policy<TContext extends Context, TResources extends Resources = Resources, TActions extends Actions<TResources> = Actions<TResources>> {
136
135
  /**
137
- * Determines if the specified action is permitted on the resource in the given context.
138
- */
139
- can<K extends keyof TResources & keyof TActions>(context: TContext, permission: `${K & string}:${InferAction<TActions, K> & string}`, resource: InferResource<TResources, K>): Promise<boolean>;
136
+ * Determines if the specified action is permitted on the resource in the given context.
137
+ */
138
+ can: <K extends keyof TResources & keyof TActions>(context: TContext, permission: `${K & string}:${InferAction<TActions, K> & string}`, resource: InferResource<TResources, K>) => Promise<boolean>;
140
139
  }
141
140
  //#endregion
142
141
  export { ActionPolicyMap, Actions, ConditionFn, Context, Decision, InferAction, InferPermission, InferResource, PermitConfig, Policy, PolicyFn, Resources, Role, RoleHierarchy, Rules };
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;AAOA;;;KAAY,QAAA;;AAMZ;;;KAAY,OAAA,uBAA8B,QAAA;;AAK1C;;KAAY,IAAA,kCAAsC,KAAA;;;AAiBlD;;;;;;;;;;;;;KAAY,aAAA,eAA4B,IAAA,GAAO,IAAA,IAAQ,MAAA,CAAO,KAAA,EAAO,KAAA;;;;;AAiBrE;;;;;;;;;;;KAAY,SAAA,yCAAkD,MAAA,CAC5D,YAAA,EACA,gBAAA;;;AAiBF;;;;;;;;;;AAOA;;KAPY,OAAA,oBAA2B,SAAA,kBACzB,UAAA;;;;KAMF,aAAA,oBACS,SAAA,6BACQ,UAAA,IACzB,gBAAA,CAAiB,WAAA,CAAY,UAAA,CAAW,YAAA;;;;KAKhC,WAAA,kBACO,MAAA,6CACD,QAAA,IACd,QAAA,CAAS,CAAA;;;;AAHb;;;;;;KAcY,eAAA,oBAAmC,SAAA,mBAA4B,OAAA,CAAQ,UAAA,mBACrE,UAAA,SAAmB,QAAA,MAAc,CAAA,aAAc,WAAA,CAAY,QAAA,EAAU,CAAA,qBAC3E,UAAA,SAAmB,QAAA;;;;KAKf,QAAA,kBACO,OAAA,2DAGd,OAAA,EAAS,QAAA,EAAU,MAAA,EAAQ,OAAA,EAAS,QAAA,EAAU,SAAA,KAAc,QAAA;;;;KAKrD,WAAA,kBACO,OAAA,2DAGd,OAAA,EAAS,QAAA,EAAU,MAAA,EAAQ,OAAA,EAAS,QAAA,EAAU,SAAA;;;;KAKvC,eAAA,kBACO,OAAA,kEAIX,OAAA,IAAW,QAAA,CAAS,QAAA,EAAU,CAAA,EAAG,SAAA;;;;;KAO7B,KAAA,kBACO,OAAA,qBACE,SAAA,GAAY,SAAA,mBACd,OAAA,CAAQ,UAAA,IAAc,OAAA,CAAQ,UAAA,mBAEnC,UAAA,SAAmB,QAAA,GAAW,eAAA,CACxC,QAAA,EACA,WAAA,CAAY,QAAA,EAAU,CAAA,GACtB,aAAA,CAAc,UAAA,EAAY,CAAA;;;;;;;AAtC9B;;;;;;;;UAwDiB,YAAA,kBACE,OAAA,qBACE,SAAA,GAAY,SAAA,mBACd,OAAA,CAAQ,UAAA,IAAc,OAAA,CAAQ,UAAA;EAE/C,OAAA,EAAS,QAAA;EACT,SAAA,EAAW,UAAA;EACX,KAAA,EAAO,KAAA,CAAM,QAAA,EAAU,UAAA,EAAY,QAAA;AAAA;;;;;;;AAtDrC;;;;;;;;;UAwEiB,MAAA,kBACE,OAAA,qBACE,SAAA,GAAY,SAAA,mBACd,OAAA,CAAQ,UAAA,IAAc,OAAA,CAAQ,UAAA;;;;EAK/C,GAAA,iBAAoB,UAAA,SAAmB,QAAA,EACrC,OAAA,EAAS,QAAA,EACT,UAAA,KAAe,CAAA,aAAc,WAAA,CAAY,QAAA,EAAU,CAAA,cACnD,QAAA,EAAU,aAAA,CAAc,UAAA,EAAY,CAAA,IACnC,OAAA;AAAA"}
1
+ {"version":3,"file":"types.d.mts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;;;KAaY;;;;;KAMA,QAAQ,sBAAsB;;;;KAK9B,KAAK,iCAAiC;;;;;;;;;;;;;;;;KAiBtC,cAAc,cAAc,OAAO,QAAQ,OAAO,OAAO;;;;;;;;;;;;;;;;KAiBzD,UAAU,wCAAwC,OAC5D,cACA;;;;;;;;;;;;;;;KAiBU,QAAQ,mBAAmB,gBACpC,WAAW;;;;KAMF,cACV,mBAAmB,WACnB,2BAA2B,cACzB,iBAAiB,YAAY,WAAW;;;;KAKhC,YACV,iBAAiB,mCACjB,gBAAgB,YACd,SAAS;;;;;;;;;;KAWD,gBACV,mBAAmB,WACnB,iBAAiB,QAAQ,kBAExB,WAAW,mBACJ,cAAc,cAAc,YAAY,UAAU,uBACpD,mBAAmB;;;;KAKf,SACV,iBAAiB,SACjB,iCACA,wBACG,SAAS,UAAU,QAAQ,SAAS,UAAU,cAAc;;;;KAKrD,YACV,iBAAiB,SACjB,iCACA,wBACG,SAAS,UAAU,QAAQ,SAAS,UAAU;;;;KAKvC,gBACV,iBAAiB,SACjB,iCACA,0BAEC,KAAK,WAAW,SAAS,UAAU,GAAG;;;;;KAO7B,MACV,iBAAiB,SACjB,mBAAmB,YAAY,WAC/B,iBAAiB,QAAQ,cAAc,QAAQ,kBAE9C,WAAW,mBAAmB,WAAW,gBACxC,UACA,YAAY,UAAU,IACtB,cAAc,YAAY;;;;;;;;;;;;;;;UAkBb,aACf,iBAAiB,SACjB,mBAAmB,YAAY,WAC/B,iBAAiB,QAAQ,cAAc,QAAQ;EAE/C,SAAS;EACT,WAAW;EACX,OAAO,MAAM,UAAU,YAAY;;;;;;;;;;;;;;;;;UAkBpB,OACf,iBAAiB,SACjB,mBAAmB,YAAY,WAC/B,iBAAiB,QAAQ,cAAc,QAAQ;;;;EAK/C,MAAM,gBAAgB,mBAAmB,UACvC,SAAS,UACT,eAAe,cAAc,YAAY,UAAU,eACnD,UAAU,cAAc,YAAY,OACjC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zap-studio/permit",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "private": false,
5
5
  "description": "A type-safe, declarative authorization library for TypeScript with Standard Schema support",
6
6
  "keywords": [
@@ -43,19 +43,18 @@
43
43
  "access": "public"
44
44
  },
45
45
  "dependencies": {
46
- "@zap-studio/validation": "0.3.4"
46
+ "@zap-studio/validation": "0.3.5"
47
47
  },
48
48
  "devDependencies": {
49
- "typescript": "^6.0.3",
50
- "vite-plus": "^0.1.19",
49
+ "tsdown": "^0.22.4",
50
+ "typescript": "^7.0.2",
51
+ "vitest": "^4.1.10",
51
52
  "@zap-studio/typescript": "0.0.0"
52
53
  },
53
54
  "engines": {
54
55
  "node": ">=18.0.0"
55
56
  },
56
57
  "scripts": {
57
- "build": "vp pack",
58
- "test": "vp test run",
59
- "test:watch": "vp test watch"
58
+ "build": "tsdown --config ./tsdown.config.ts"
60
59
  }
61
60
  }