@zap-studio/permit 0.2.2 → 0.3.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # @zap-studio/permit
2
2
 
3
+ ## 0.3.1
4
+
5
+ ### Fixed
6
+
7
+ - d10d8c4: Removed the TanStack Intent-specific authoring helper from the package surface.
8
+ - d9ba7d1: Reworked the packaged permit skill content.
9
+
10
+ ### Changed
11
+
12
+ - 5fa58b1: Reduced policy evaluation complexity by extracting permission parsing and merge strategy helpers without changing the `policy.can(...)` API.
13
+ - abfda4b: Cleaned up public option typings by removing redundant `| undefined` unions from package types.
14
+ - 9f31f87: Switched the package build to ESNext-aligned output and updated package tooling and publish metadata.
15
+
16
+ ### Dependencies
17
+
18
+ - Updated dependency `@zap-studio/validation` to `0.3.3`.
19
+
20
+ ## 0.3.0
21
+
22
+ ### Minor Changes
23
+
24
+ - fe60f55: Change `policy.can()` to use a single permission string plus the resource object.
25
+
26
+ `policy.can(ctx, "read", "post", post)` is replaced by
27
+ `policy.can(ctx, "post:read", post)`.
28
+
29
+ This is a breaking API change in the `0.x` line. Docs and examples now use the
30
+ new permission-string format consistently.
31
+
3
32
  ## 0.2.2
4
33
 
5
34
  ### Patch Changes
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Alexandre Trotel
3
+ Copyright (c) 2026 Alexandre Trotel
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -72,9 +72,9 @@ const policy = createPolicy<AppContext>({
72
72
  const ctx: AppContext = { user: { id: "user-1", role: "user" } };
73
73
  const post = { id: "1", authorId: "user-1", visibility: "public" as const };
74
74
 
75
- await policy.can(ctx, "read", "post", post); // true
76
- await policy.can(ctx, "write", "post", post); // true (user is author)
77
- await policy.can(ctx, "delete", "post", post); // false (always denied)
75
+ await policy.can(ctx, "post:read", post); // true
76
+ await policy.can(ctx, "post:write", post); // true (user is author)
77
+ await policy.can(ctx, "post:delete", post); // false (always denied)
78
78
  ```
79
79
 
80
80
  ## API Reference
@@ -126,7 +126,7 @@ Returns a condition that is true only if all conditions are true.
126
126
  ```ts
127
127
  const isOwnerAndPublished = and(
128
128
  (ctx, action, resource) => ctx.user.id === resource.authorId,
129
- (ctx, action, resource) => resource.status === "published"
129
+ (ctx, action, resource) => resource.status === "published",
130
130
  );
131
131
  ```
132
132
 
@@ -137,7 +137,7 @@ Returns a condition that is true if any condition is true.
137
137
  ```ts
138
138
  const isOwnerOrAdmin = or(
139
139
  (ctx, action, resource) => ctx.user.id === resource.authorId,
140
- (ctx, action, resource) => ctx.user.role === "admin"
140
+ (ctx, action, resource) => ctx.user.role === "admin",
141
141
  );
142
142
  ```
143
143
 
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.mts","names":[],"sources":["../src/errors.ts"],"sourcesContent":[],"mappings":";;AAIA;;;cAAa,WAAA,SAAoB,KAAA"}
1
+ {"version":3,"file":"errors.d.mts","names":[],"sources":["../src/errors.ts"],"mappings":";;AAIA;;;cAAa,WAAA,SAAoB,KAAA;EAC/B,WAAA,CAAY,OAAA;AAAA"}
package/dist/errors.mjs CHANGED
@@ -9,7 +9,7 @@ var PolicyError = class extends Error {
9
9
  this.name = "PolicyError";
10
10
  }
11
11
  };
12
-
13
12
  //#endregion
14
13
  export { PolicyError };
14
+
15
15
  //# sourceMappingURL=errors.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.d.mts","names":[],"sources":["../src/helpers.ts"],"sourcesContent":[],"mappings":";;AAsBA;;;;;;;;;;;;;;;;;;;;;iBAAgB,WAAA"}
1
+ {"version":3,"file":"helpers.d.mts","names":[],"sources":["../src/helpers.ts"],"mappings":";;AAsBA;;;;;;;;;;;;;;;;;;;;;iBAAgB,WAAA,CAAY,KAAA"}
package/dist/helpers.mjs CHANGED
@@ -22,9 +22,9 @@
22
22
  * ```
23
23
  */
24
24
  function assertNever(value) {
25
- throw new Error(`Unexpected value: ${value}`);
25
+ throw new Error(`Unexpected value: ${String(value)}`);
26
26
  }
27
-
28
27
  //#endregion
29
28
  export { assertNever };
29
+
30
30
  //# sourceMappingURL=helpers.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.mjs","names":[],"sources":["../src/helpers.ts"],"sourcesContent":["/**\n * Ensures that a value of type `never` is actually never encountered at runtime.\n * This is useful for exhaustive checks on discriminated unions.\n *\n * @example\n * ```ts\n * type Action = 'read' | 'write'\n *\n * function performAction(action: Action) {\n * switch (action) {\n * case 'read':\n * console.log('Reading...')\n * break\n * case 'write':\n * console.log('Writing...')\n * break\n * default:\n * assertNever(action) // TypeScript will error if a new Action is added but not handled\n * }\n * }\n * ```\n */\nexport function assertNever(value: never): never {\n throw new Error(`Unexpected value: ${value}`);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,YAAY,OAAqB;AAC/C,OAAM,IAAI,MAAM,qBAAqB,QAAQ"}
1
+ {"version":3,"file":"helpers.mjs","names":[],"sources":["../src/helpers.ts"],"sourcesContent":["/**\n * Ensures that a value of type `never` is actually never encountered at runtime.\n * This is useful for exhaustive checks on discriminated unions.\n *\n * @example\n * ```ts\n * type Action = 'read' | 'write'\n *\n * function performAction(action: Action) {\n * switch (action) {\n * case 'read':\n * console.log('Reading...')\n * break\n * case 'write':\n * console.log('Writing...')\n * break\n * default:\n * assertNever(action) // TypeScript will error if a new Action is added but not handled\n * }\n * }\n * ```\n */\nexport function assertNever(value: never): never {\n throw new Error(`Unexpected value: ${String(value)}`);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,YAAY,OAAqB;AAC/C,OAAM,IAAI,MAAM,qBAAqB,OAAO,MAAM,GAAG"}
package/dist/index.d.mts CHANGED
@@ -1,7 +1,6 @@
1
1
  import { Actions, ConditionFn, Context, PermitConfig, Policy, PolicyFn, Resources, Role, RoleHierarchy } from "./types.mjs";
2
2
 
3
3
  //#region src/index.d.ts
4
-
5
4
  /**
6
5
  * Returns a policy function that always allows the action.
7
6
  *
@@ -162,10 +161,10 @@ declare function collectInheritedRoles<TRole extends Role = Role>(roles: TRole[]
162
161
  * }
163
162
  * ```
164
163
  */
165
- declare function hasRole<TContext extends Context & {
164
+ declare function hasRole<TContext extends {
166
165
  role: Role | Role[];
167
166
  }, TAction extends string = string, TResource = unknown>(role: Role): ConditionFn<TContext, TAction, TResource>;
168
- declare function hasRole<TContext extends Context & {
167
+ declare function hasRole<TContext extends {
169
168
  role: TRole | TRole[];
170
169
  }, TAction extends string = string, TResource = unknown, TRole extends Role = Role>(role: TRole, hierarchy: RoleHierarchy<TRole>): ConditionFn<TContext, TAction, TResource>;
171
170
  /**
@@ -219,8 +218,8 @@ declare function hasRole<TContext extends Context & {
219
218
  *
220
219
  * // Check permissions
221
220
  * const post = { id: "1", authorId: "user-1", visibility: "public" as const };
222
- * await policy.can(ctx, "read", "post", post); // true
223
- * await policy.can(ctx, "write", "post", post); // depends on ctx.user.id
221
+ * await policy.can(ctx, "post:read", post); // true
222
+ * await policy.can(ctx, "post:write", post); // depends on ctx.user.id
224
223
  * ```
225
224
  */
226
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>;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":"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"}
package/dist/index.mjs CHANGED
@@ -1,6 +1,5 @@
1
1
  import { PolicyError } from "./errors.mjs";
2
2
  import { createStandardValidator } from "@zap-studio/validation";
3
-
4
3
  //#region src/index.ts
5
4
  /**
6
5
  * Returns a policy function that always allows the action.
@@ -217,8 +216,8 @@ function hasRole(role, hierarchy) {
217
216
  *
218
217
  * // Check permissions
219
218
  * const post = { id: "1", authorId: "user-1", visibility: "public" as const };
220
- * await policy.can(ctx, "read", "post", post); // true
221
- * await policy.can(ctx, "write", "post", post); // depends on ctx.user.id
219
+ * await policy.can(ctx, "post:read", post); // true
220
+ * await policy.can(ctx, "post:write", post); // depends on ctx.user.id
222
221
  * ```
223
222
  */
224
223
  function createPolicy(config) {
@@ -236,28 +235,39 @@ function createPolicy(config) {
236
235
  return null;
237
236
  }
238
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
+ };
239
257
  for (const key of Object.keys(resources)) {
240
258
  const schema = resources[key];
241
259
  if (!schema) throw new PolicyError(`Missing schema for resource: ${String(key)}`);
242
260
  const validator = createStandardValidator(schema);
243
261
  validators.set(key, async (input) => validator(input));
244
262
  }
245
- return { async can(context, action, resourceType, resource) {
246
- const allowedActions = actions[resourceType];
247
- if (!allowedActions) return false;
248
- if (!allowedActions.includes(action)) return false;
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;
249
268
  const validatedResource = await getValidatedResource(resourceType, resource);
250
269
  if (!validatedResource) return false;
251
- const resourceRules = rules[resourceType];
252
- if (!resourceRules) return false;
253
- const policyFn = resourceRules[action];
254
- if (!policyFn) return false;
255
- try {
256
- return policyFn(context, action, validatedResource) === "allow";
257
- } catch (error) {
258
- console.warn(`Policy evaluation error for ${String(resourceType)}.${String(action)}: ${String(error)}`);
259
- return false;
260
- }
270
+ return evaluatePolicy(context, resourceType, action, validatedResource);
261
271
  } };
262
272
  }
263
273
  /**
@@ -274,11 +284,7 @@ function createPolicy(config) {
274
284
  * ```
275
285
  */
276
286
  function mergePolicies(...policies) {
277
- return { async can(context, action, resourceType, resource) {
278
- if (!policies.length) return false;
279
- for (const policy of policies) if (!await policy.can(context, action, resourceType, resource)) return false;
280
- return true;
281
- } };
287
+ return mergePoliciesWithStrategy(policies, "deny-overrides");
282
288
  }
283
289
  /**
284
290
  * Merges multiple policies into one using "allow-overrides" strategy.
@@ -294,13 +300,20 @@ function mergePolicies(...policies) {
294
300
  * ```
295
301
  */
296
302
  function mergePoliciesAny(...policies) {
297
- return { async can(context, action, resourceType, resource) {
303
+ return mergePoliciesWithStrategy(policies, "allow-overrides");
304
+ }
305
+ function mergePoliciesWithStrategy(policies, strategy) {
306
+ return { async can(context, permission, resource) {
298
307
  if (!policies.length) return false;
299
- for (const policy of policies) if (await policy.can(context, action, resourceType, resource)) return true;
300
- 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";
301
314
  } };
302
315
  }
303
-
304
316
  //#endregion
305
317
  export { allow, and, collectInheritedRoles, createPolicy, deny, has, hasRole, mergePolicies, mergePoliciesAny, not, or, when };
318
+
306
319
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { createStandardValidator } from \"@zap-studio/validation\";\nimport { PolicyError } from \"./errors\";\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\";\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>(\n condition: ConditionFn<TContext, TAction, TResource>\n): PolicyFn<TContext, TAction, TResource> {\n return (context, action, resource) =>\n 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<\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 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<\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 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<\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 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 Context & { 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 Context & { 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\nexport function hasRole<\n TContext extends Context & { role: Role | Role[] },\n TAction extends string = string,\n TResource = unknown,\n>(\n role: Role,\n hierarchy?: RoleHierarchy<Role>\n): ConditionFn<TContext, TAction, TResource> {\n return (context) => {\n const userRoles = Array.isArray(context.role)\n ? context.role\n : [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, \"read\", \"post\", post); // true\n * await policy.can(ctx, \"write\", \"post\", 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>(\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) {\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(\n `Resource validation failed for ${String(resourceType)}: ${String(error)}`\n );\n return null;\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 action: InferAction<TActions, K>,\n resourceType: K,\n resource: InferResource<TResources, K>\n ): Promise<boolean> {\n const allowedActions = actions[resourceType];\n if (!allowedActions) {\n return false;\n }\n if (!allowedActions.includes(action)) {\n return false;\n }\n\n const validatedResource = await getValidatedResource(\n resourceType,\n resource\n );\n if (!validatedResource) {\n return false;\n }\n\n const resourceRules = rules[resourceType];\n if (!resourceRules) {\n return false;\n }\n\n const policyFn = resourceRules[action];\n\n if (!policyFn) {\n return false;\n }\n\n try {\n return policyFn(context, action, validatedResource) === \"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}\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>(\n ...policies: Policy<TContext, TResources, TActions>[]\n): Policy<TContext, TResources, TActions> {\n return {\n async can<K extends keyof TResources & keyof TActions>(\n context: TContext,\n action: InferAction<TActions, K>,\n resourceType: K,\n resource: InferResource<TResources, K>\n ): Promise<boolean> {\n if (!policies.length) {\n return false;\n }\n for (const policy of policies) {\n if (!(await policy.can(context, action, resourceType, resource))) {\n return false;\n }\n }\n return true;\n },\n };\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>(\n ...policies: Policy<TContext, TResources, TActions>[]\n): Policy<TContext, TResources, TActions> {\n return {\n async can<K extends keyof TResources & keyof TActions>(\n context: TContext,\n action: InferAction<TActions, K>,\n resourceType: K,\n resource: InferResource<TResources, K>\n ): Promise<boolean> {\n if (!policies.length) {\n return false;\n }\n for (const policy of policies) {\n if (await policy.can(context, action, resourceType, resource)) {\n return true;\n }\n }\n return false;\n },\n };\n}\n"],"mappings":"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"}
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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"}
package/dist/types.d.mts CHANGED
@@ -1,7 +1,6 @@
1
- import { StandardSchemaV1 } from "@standard-schema/spec";
1
+ import { StandardSchemaV1 } from "@zap-studio/validation";
2
2
 
3
3
  //#region src/types.d.ts
4
-
5
4
  /**
6
5
  * Represents the possible outcomes of a policy decision.
7
6
  * - "allow": The action is permitted.
@@ -71,7 +70,17 @@ type InferResource<TResources extends Resources, TResourceKey extends keyof TRes
71
70
  /**
72
71
  * Infers the action union type for a specific resource.
73
72
  */
74
- type InferAction<TActions extends Record<string, readonly string[]>, K$1 extends keyof TActions> = TActions[K$1][number];
73
+ type InferAction<TActions extends Record<string, readonly string[]>, K extends keyof TActions> = TActions[K][number];
74
+ /**
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];
75
84
  /**
76
85
  * A function that determines whether a given action on a resource is allowed in a specific context.
77
86
  */
@@ -120,15 +129,15 @@ interface PermitConfig<TContext extends Context, TResources extends Resources =
120
129
  * rules: { ... },
121
130
  * });
122
131
  *
123
- * await policy.can(ctx, "read", "post", postData); // true or false
132
+ * await policy.can(ctx, "post:read", postData); // true or false
124
133
  * ```
125
134
  */
126
135
  interface Policy<TContext extends Context, TResources extends Resources = Resources, TActions extends Actions<TResources> = Actions<TResources>> {
127
136
  /**
128
137
  * Determines if the specified action is permitted on the resource in the given context.
129
138
  */
130
- can<K$1 extends keyof TResources & keyof TActions>(context: TContext, action: InferAction<TActions, K$1>, resourceType: K$1, resource: InferResource<TResources, K$1>): Promise<boolean>;
139
+ can<K extends keyof TResources & keyof TActions>(context: TContext, permission: `${K & string}:${InferAction<TActions, K> & string}`, resource: InferResource<TResources, K>): Promise<boolean>;
131
140
  }
132
141
  //#endregion
133
- export { ActionPolicyMap, Actions, ConditionFn, Context, Decision, InferAction, InferResource, PermitConfig, Policy, PolicyFn, Resources, Role, RoleHierarchy, Rules };
142
+ export { ActionPolicyMap, Actions, ConditionFn, Context, Decision, InferAction, InferPermission, InferResource, PermitConfig, Policy, PolicyFn, Resources, Role, RoleHierarchy, Rules };
134
143
  //# sourceMappingURL=types.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.mts","names":[],"sources":["../src/types.ts"],"sourcesContent":[],"mappings":";;;;;;AAOA;AAMA;AAKA;AAiBY,KA5BA,QAAA,GA4BA,OAAA,GAAA,MAAA;;;;;AAA2C,KAtB3C,OAsB2C,CAAA,WAAA,OAAA,CAAA,GAtBb,QAsBa;;AAiBvD;;AAEE,KApCU,IAoCV,CAAA,cAAA,MAAA,GAAA,MAAA,CAAA,GApCgD,KAoChD;;;AAiBF;AAOA;;;;;;;AAQA;;;;;AAGa,KAtDD,aAsDC,CAAA,cAtD2B,IAsD3B,GAtDkC,IAsDlC,CAAA,GAtD0C,MAsD1C,CAtDiD,KAsDjD,EAtDwD,KAsDxD,EAAA,CAAA;AAKb;;;;;;;AASA;;;;;;AASA;;AAKQ,KAjEI,SAiEJ,CAAA,qBAAA,MAAA,GAAA,MAAA,CAAA,GAjEsD,MAiEtD,CAhEN,YAgEM,EA/DN,gBA+DM,CAAA;;;;;;AAOR;;;;;;;;;AAKiC,KA1DrB,OA0DqB,CAAA,mBA1DM,SA0DN,CAAA,GAAA,QAAA,MAzDnB,UAyDmB,GAAA,SAAA,MAAA,EAAA,EAAA;;;;AAE7B,KArDQ,aAqDR,CACc,mBArDG,SAqDH,EAAY,qBAAA,MApDD,UAoDC,CAA1B,GAnDA,gBAAA,CAAiB,WAmDjB,CAnD6B,UAmD7B,CAnDwC,YAmDxC,CAAA,CAAA;;;AAkBJ;AACmB,KAjEP,WAiEO,CACE,iBAjEF,MAiEE,CAAA,MAAA,EAAA,SAAA,MAAA,EAAA,CAAA,EAAY,YAAA,MAhEf,QAgEe,CACN,GAhEvB,QAgEuB,CAhEd,GAgEc,CAAA,CAAA,MAAA,CAAA;;;;AAEhB,KA7DC,QA6DD,CACE,iBA7DM,OA6DN,EACE,gBAAA,MAAA,GAAA,MAAA,EAAU,YAAA,OAAA,CAAY,GAAA,CAAA,OAAA,EA3DvB,QA2DuB,EAAA,MAAA,EA3DL,OA2DK,EAAA,QAAA,EA3Dc,SA2Dd,EAAA,GA3D4B,QA2D5B;;;AAkBrC;AACmB,KAzEP,WAyEO,CACE,iBAzEF,OAyEE,EAAY,gBAAA,MAAA,GAAA,MAAA,EACN,YAAA,OAAA,CAAR,GAAA,CAAA,OAAA,EAvEL,QAuEK,EAAA,MAAA,EAvEa,OAuEb,EAAA,QAAA,EAvEgC,SAuEhC,EAAA,GAAA,OAAA;;;;AAKsB,KAvE7B,eAuE6B,CAC5B,iBAvEM,OAuEN,EACW,gBAAA,MAAA,GAAA,MAAA,EAAU,YAAA,OAAA,CAAtB,GAAA,QApEJ,OAoEI,IApEO,QAoEP,CApEgB,QAoEhB,EApE0B,CAoE1B,EApE6B,SAoE7B,CAAA,EAAA;;;;;AAGP,KAhEO,KAgEP,CAAA,iBA/Dc,OA+Dd,qBA9DgB,YAAY,4BACd,QAAQ,cAAc,QAAQ,6BAEnC,mBAAmB,WAAW,gBACxC,UACA,YAAY,UAAU,IACtB,cAAc,YAAY;;;;;;;;;;;;;;;UAkBb,8BACE,4BACE,YAAY,4BACd,QAAQ,cAAc,QAAQ;WAEtC;aACE;SACJ,MAAM,UAAU,YAAY;;;;;;;;;;;;;;;;;UAkBpB,wBACE,4BACE,YAAY,4BACd,QAAQ,cAAc,QAAQ;;;;wBAK3B,mBAAmB,mBAC5B,kBACD,YAAY,UAAU,oBAChB,eACJ,cAAc,YAAY,OACnC"}
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"}
package/dist/types.mjs CHANGED
@@ -1 +1 @@
1
- export { };
1
+ export {};
package/package.json CHANGED
@@ -1,55 +1,37 @@
1
1
  {
2
2
  "name": "@zap-studio/permit",
3
- "version": "0.2.2",
4
- "type": "module",
5
- "license": "MIT",
3
+ "version": "0.3.1",
6
4
  "private": false,
7
- "homepage": "https://www.zapstudio.dev/packages/permit",
8
- "repository": {
9
- "type": "git",
10
- "url": "https://github.com/zap-studio/monorepo.git",
11
- "directory": "packages/permit"
12
- },
13
5
  "description": "A type-safe, declarative authorization library for TypeScript with Standard Schema support",
14
6
  "keywords": [
7
+ "abac",
8
+ "access-control",
9
+ "arktype",
15
10
  "authorization",
16
11
  "permissions",
17
- "access-control",
18
- "rbac",
19
- "abac",
20
12
  "policy",
21
- "typescript",
13
+ "rbac",
22
14
  "standard-schema",
23
- "zod",
15
+ "typescript",
24
16
  "valibot",
25
- "arktype"
17
+ "zod"
26
18
  ],
27
- "publishConfig": {
28
- "access": "public"
19
+ "homepage": "https://www.zapstudio.dev/packages/permit",
20
+ "license": "MIT",
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/zap-studio/monorepo.git",
24
+ "directory": "packages/permit"
29
25
  },
30
26
  "files": [
31
27
  "dist",
32
28
  "CHANGELOG.md",
33
29
  "LICENSE",
34
- "README.md",
35
- "skills",
36
- "bin",
37
- "!skills/_artifacts"
30
+ "README.md"
38
31
  ],
39
- "dependencies": {
40
- "@standard-schema/spec": "^1.1.0",
41
- "@zap-studio/validation": "0.3.2"
42
- },
43
- "devDependencies": {
44
- "@types/node": "^25.0.2",
45
- "@vitest/coverage-v8": "^4.0.15",
46
- "tsdown": "^0.18.0",
47
- "typescript": "^5.9.3",
48
- "vitest": "^4.0.18",
49
- "@zap-studio/vitest-config": "0.0.0",
50
- "@zap-studio/typescript-config": "0.0.0",
51
- "@zap-studio/tsdown-config": "0.0.0"
52
- },
32
+ "type": "module",
33
+ "sideEffects": false,
34
+ "types": "./dist/index.d.mts",
53
35
  "exports": {
54
36
  ".": "./dist/index.mjs",
55
37
  "./errors": "./dist/errors.mjs",
@@ -57,16 +39,23 @@
57
39
  "./types": "./dist/types.mjs",
58
40
  "./package.json": "./package.json"
59
41
  },
60
- "main": "./dist/index.mjs",
61
- "module": "./dist/index.mjs",
62
- "types": "./dist/index.d.mts",
63
- "bin": {
64
- "intent": "./bin/intent.js"
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "dependencies": {
46
+ "@zap-studio/validation": "0.3.3"
47
+ },
48
+ "devDependencies": {
49
+ "typescript": "^6.0.3",
50
+ "vite-plus": "^0.1.19",
51
+ "@zap-studio/typescript": "0.0.0"
52
+ },
53
+ "engines": {
54
+ "node": ">=18.0.0"
65
55
  },
66
56
  "scripts": {
67
- "build": "tsdown --config tsdown.config.ts",
68
- "typecheck": "tsc --noEmit",
69
- "test": "vitest run",
70
- "test:watch": "vitest --watch"
57
+ "build": "vp pack",
58
+ "test": "vp test run",
59
+ "test:watch": "vp test watch"
71
60
  }
72
61
  }
package/bin/intent.js DELETED
@@ -1,4 +0,0 @@
1
- #!/usr/bin/env node
2
- // Auto-generated by @tanstack/intent setup
3
- // Exposes the intent end-user CLI for consumers of this library.
4
- await import("@tanstack/intent/intent-library");
@@ -1,157 +0,0 @@
1
- ---
2
- name: zap-permit-policy-authoring
3
- description: >
4
- Author typed authorization policies with @zap-studio/permit using
5
- createPolicy, allow/deny/when, condition combinators, has/hasRole,
6
- and mergePolicies vs mergePoliciesAny decision strategies.
7
- type: core
8
- library: '@zap-studio/permit'
9
- library_version: '0.2.1'
10
- sources:
11
- - 'zap-studio/monorepo:packages/permit/README.md'
12
- - 'zap-studio/monorepo:packages/permit/src/index.ts'
13
- - 'zap-studio/monorepo:packages/permit/src/types.ts'
14
- ---
15
-
16
- # @zap-studio/permit — Policy Authoring
17
-
18
- ## Setup
19
-
20
- ```ts
21
- import { z } from 'zod';
22
- import { createPolicy, allow, when } from '@zap-studio/permit';
23
- import type { Resources, Actions } from '@zap-studio/permit/types';
24
-
25
- const resources = {
26
- post: z.object({ id: z.string(), authorId: z.string() }),
27
- } satisfies Resources;
28
-
29
- const actions = {
30
- post: ['read', 'write'],
31
- } as const satisfies Actions<typeof resources>;
32
-
33
- type AppContext = { user: { id: string; role: 'user' | 'admin' } };
34
-
35
- const policy = createPolicy<AppContext>({
36
- resources,
37
- actions,
38
- rules: {
39
- post: {
40
- read: allow(),
41
- write: when((ctx, _action, post) => ctx.user.id === post.authorId),
42
- },
43
- },
44
- });
45
- ```
46
-
47
- ## Core Patterns
48
-
49
- ### Compose conditions with `and`, `or`, and `not`
50
-
51
- ```ts
52
- import { when, and, or, not } from '@zap-studio/permit';
53
-
54
- const canEdit = when(
55
- and(
56
- (ctx, _action, post) => ctx.user.id === post.authorId,
57
- not((_ctx, _action, post) => post.locked === true),
58
- ),
59
- );
60
-
61
- const canRead = when(
62
- or(
63
- (_ctx, _action, post) => post.visibility === 'public',
64
- (ctx, _action, post) => ctx.user.id === post.authorId,
65
- ),
66
- );
67
- ```
68
-
69
- ### Add role inheritance checks with `hasRole`
70
-
71
- ```ts
72
- import { when, hasRole } from '@zap-studio/permit';
73
-
74
- const hierarchy = {
75
- guest: [],
76
- user: ['guest'],
77
- admin: ['user'],
78
- } as const;
79
-
80
- const adminOnly = when(hasRole('admin', hierarchy));
81
- ```
82
-
83
- ### Choose merge strategy explicitly
84
-
85
- ```ts
86
- import { mergePolicies, mergePoliciesAny } from '@zap-studio/permit';
87
-
88
- const strict = mergePolicies(basePolicy, tenantPolicy); // all must allow
89
- const permissive = mergePoliciesAny(basePolicy, temporaryOverridePolicy); // any can allow
90
- ```
91
-
92
- ## Common Mistakes
93
-
94
- ### HIGH Using action missing from actions map
95
-
96
- Wrong:
97
-
98
- ```ts
99
- await policy.can(ctx, 'publish', 'post', post);
100
- ```
101
-
102
- Correct:
103
-
104
- ```ts
105
- const actions = {
106
- post: ['read', 'write', 'publish'],
107
- } as const;
108
-
109
- await policy.can(ctx, 'publish', 'post', post);
110
- ```
111
-
112
- `can()` first checks `actions[resourceType]`; missing actions always resolve to `false`.
113
-
114
- Source: zap-studio/monorepo:packages/permit/src/index.ts
115
-
116
- ### HIGH Assuming invalid resources still hit rule function
117
-
118
- Wrong:
119
-
120
- ```ts
121
- await policy.can(ctx, 'write', 'post', { id: 123 } as any);
122
- ```
123
-
124
- Correct:
125
-
126
- ```ts
127
- await policy.can(ctx, 'write', 'post', {
128
- id: '123',
129
- authorId: ctx.user.id,
130
- });
131
- ```
132
-
133
- Resource validation runs before policy evaluation; invalid payloads short-circuit to deny.
134
-
135
- Source: zap-studio/monorepo:packages/permit/src/index.ts
136
-
137
- ### MEDIUM Expecting `mergePoliciesAny` to enforce deny-overrides
138
-
139
- Wrong:
140
-
141
- ```ts
142
- const merged = mergePoliciesAny(basePolicy, restrictivePolicy);
143
- // expecting restrictivePolicy to always win
144
- ```
145
-
146
- Correct:
147
-
148
- ```ts
149
- const merged = mergePolicies(basePolicy, restrictivePolicy);
150
- // deny-overrides behavior
151
- ```
152
-
153
- `mergePoliciesAny` returns allowed when any policy allows; use `mergePolicies` for strict composition.
154
-
155
- Source: zap-studio/monorepo:packages/permit/src/index.ts
156
-
157
- See also: zap-validation-standard-schema/SKILL.md — invalid resource payload behavior.