@zap-studio/permit 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -1
- package/{LICENSE.md → LICENSE} +1 -1
- package/README.md +5 -5
- package/bin/intent.js +4 -0
- package/dist/errors.d.mts.map +1 -1
- package/dist/errors.mjs +1 -1
- package/dist/helpers.d.mts.map +1 -1
- package/dist/helpers.mjs +2 -2
- package/dist/helpers.mjs.map +1 -1
- package/dist/index.d.mts +2 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1 -2
- package/dist/index.mjs.map +1 -1
- package/dist/types.d.mts +3 -4
- package/dist/types.d.mts.map +1 -1
- package/dist/types.mjs +1 -1
- package/package.json +41 -40
- package/skills/zap-permit-policy-authoring/SKILL.md +157 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @zap-studio/permit
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- fe60f55: Change `policy.can()` to use a single permission string plus the resource object.
|
|
8
|
+
|
|
9
|
+
`policy.can(ctx, "read", "post", post)` is replaced by
|
|
10
|
+
`policy.can(ctx, "post:read", post)`.
|
|
11
|
+
|
|
12
|
+
This is a breaking API change in the `0.x` line. Docs and examples now use the
|
|
13
|
+
new permission-string format consistently.
|
|
14
|
+
|
|
15
|
+
## 0.2.2
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- e26293e: Updated dependencies.
|
|
20
|
+
- @zap-studio/validation@0.3.2
|
|
21
|
+
|
|
3
22
|
## 0.2.1
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
|
@@ -7,7 +26,6 @@
|
|
|
7
26
|
- 5ea3d3b: Updated dependencies.
|
|
8
27
|
- @zap-studio/validation@0.3.1
|
|
9
28
|
|
|
10
|
-
|
|
11
29
|
## 0.2.0
|
|
12
30
|
|
|
13
31
|
### Changed
|
package/{LICENSE.md → LICENSE}
RENAMED
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",
|
|
76
|
-
await policy.can(ctx, "write",
|
|
77
|
-
await policy.can(ctx, "delete",
|
|
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
|
|
package/bin/intent.js
ADDED
package/dist/errors.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.mts","names":[],"sources":["../src/errors.ts"],"
|
|
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
package/dist/helpers.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.d.mts","names":[],"sources":["../src/helpers.ts"],"
|
|
1
|
+
{"version":3,"file":"helpers.d.mts","names":[],"sources":["../src/helpers.ts"],"mappings":";;AAsBA;;;;;;;;;;;;;;;;;;;;;iBAAgB,WAAA,CAAY,KAAA"}
|
package/dist/helpers.mjs
CHANGED
package/dist/helpers.mjs.map
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
|
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
|
/**
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"
|
|
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.
|
|
@@ -300,7 +299,7 @@ function mergePoliciesAny(...policies) {
|
|
|
300
299
|
return false;
|
|
301
300
|
} };
|
|
302
301
|
}
|
|
303
|
-
|
|
304
302
|
//#endregion
|
|
305
303
|
export { allow, and, collectInheritedRoles, createPolicy, deny, has, hasRole, mergePolicies, mergePoliciesAny, not, or, when };
|
|
304
|
+
|
|
306
305
|
//# sourceMappingURL=index.mjs.map
|
package/dist/index.mjs.map
CHANGED
|
@@ -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, \"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>(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 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(resourceType, resource);\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>(...policies: Policy<TContext, TResources, TActions>[]): 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>(...policies: Policy<TContext, TResources, TActions>[]): 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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"}
|
package/dist/types.d.mts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { StandardSchemaV1 } from "@
|
|
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,7 @@ 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
|
|
73
|
+
type InferAction<TActions extends Record<string, readonly string[]>, K extends keyof TActions> = TActions[K][number];
|
|
75
74
|
/**
|
|
76
75
|
* A function that determines whether a given action on a resource is allowed in a specific context.
|
|
77
76
|
*/
|
|
@@ -127,7 +126,7 @@ interface Policy<TContext extends Context, TResources extends Resources = Resour
|
|
|
127
126
|
/**
|
|
128
127
|
* Determines if the specified action is permitted on the resource in the given context.
|
|
129
128
|
*/
|
|
130
|
-
can<K
|
|
129
|
+
can<K extends keyof TResources & keyof TActions>(context: TContext, action: InferAction<TActions, K>, resourceType: K, resource: InferResource<TResources, K>): Promise<boolean>;
|
|
131
130
|
}
|
|
132
131
|
//#endregion
|
|
133
132
|
export { ActionPolicyMap, Actions, ConditionFn, Context, Decision, InferAction, InferResource, PermitConfig, Policy, PolicyFn, Resources, Role, RoleHierarchy, Rules };
|
package/dist/types.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.mts","names":[],"sources":["../src/types.ts"],"
|
|
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;;;;KAKD,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;;;;;;;;;;;AA7B9B;;;;UA+CiB,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;;;;;;;;;AA7CrC;;;;;;;UA+DiB,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,MAAA,EAAQ,WAAA,CAAY,QAAA,EAAU,CAAA,GAC9B,YAAA,EAAc,CAAA,EACd,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,52 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zap-studio/permit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"type": "module",
|
|
5
|
-
"license": "MIT",
|
|
3
|
+
"version": "0.3.0",
|
|
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
|
-
"
|
|
13
|
+
"rbac",
|
|
22
14
|
"standard-schema",
|
|
23
|
-
"
|
|
15
|
+
"typescript",
|
|
24
16
|
"valibot",
|
|
25
|
-
"
|
|
17
|
+
"zod"
|
|
26
18
|
],
|
|
27
|
-
"
|
|
28
|
-
|
|
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"
|
|
25
|
+
},
|
|
26
|
+
"bin": {
|
|
27
|
+
"intent": "./bin/intent.js"
|
|
29
28
|
},
|
|
30
29
|
"files": [
|
|
31
30
|
"dist",
|
|
32
31
|
"CHANGELOG.md",
|
|
33
|
-
"LICENSE
|
|
34
|
-
"README.md"
|
|
32
|
+
"LICENSE",
|
|
33
|
+
"README.md",
|
|
34
|
+
"skills",
|
|
35
|
+
"bin",
|
|
36
|
+
"!skills/_artifacts"
|
|
35
37
|
],
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
},
|
|
40
|
-
"devDependencies": {
|
|
41
|
-
"@types/node": "^25.0.2",
|
|
42
|
-
"@vitest/coverage-v8": "^4.0.15",
|
|
43
|
-
"tsdown": "^0.18.0",
|
|
44
|
-
"typescript": "^5.9.3",
|
|
45
|
-
"vitest": "^4.0.18",
|
|
46
|
-
"@zap-studio/vitest-config": "0.0.0",
|
|
47
|
-
"@zap-studio/typescript-config": "0.0.0",
|
|
48
|
-
"@zap-studio/tsdown-config": "0.0.0"
|
|
49
|
-
},
|
|
38
|
+
"type": "module",
|
|
39
|
+
"sideEffects": false,
|
|
40
|
+
"types": "./dist/index.d.mts",
|
|
50
41
|
"exports": {
|
|
51
42
|
".": "./dist/index.mjs",
|
|
52
43
|
"./errors": "./dist/errors.mjs",
|
|
@@ -54,13 +45,23 @@
|
|
|
54
45
|
"./types": "./dist/types.mjs",
|
|
55
46
|
"./package.json": "./package.json"
|
|
56
47
|
},
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public"
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"@zap-studio/validation": "0.3.2"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"typescript": "^6.0.2",
|
|
56
|
+
"vite-plus": "^0.1.15",
|
|
57
|
+
"@zap-studio/typescript": "0.0.0"
|
|
58
|
+
},
|
|
59
|
+
"engines": {
|
|
60
|
+
"node": ">=18.0.0"
|
|
61
|
+
},
|
|
60
62
|
"scripts": {
|
|
61
|
-
"build": "
|
|
62
|
-
"
|
|
63
|
-
"test": "
|
|
64
|
-
"test:watch": "vitest --watch"
|
|
63
|
+
"build": "vp pack",
|
|
64
|
+
"test": "vp test run",
|
|
65
|
+
"test:watch": "vp test watch"
|
|
65
66
|
}
|
|
66
67
|
}
|
|
@@ -0,0 +1,157 @@
|
|
|
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.
|