@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 +29 -0
- package/LICENSE +1 -1
- package/README.md +5 -5
- 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 +4 -5
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +39 -26
- package/dist/index.mjs.map +1 -1
- package/dist/types.d.mts +15 -6
- package/dist/types.d.mts.map +1 -1
- package/dist/types.mjs +1 -1
- package/package.json +33 -44
- package/bin/intent.js +0 -4
- package/skills/zap-permit-policy-authoring/SKILL.md +0 -157
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
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/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
|
/**
|
|
@@ -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",
|
|
223
|
-
* await policy.can(ctx, "write",
|
|
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>;
|
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.
|
|
@@ -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",
|
|
221
|
-
* await policy.can(ctx, "write",
|
|
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,
|
|
246
|
-
const
|
|
247
|
-
if (!
|
|
248
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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)
|
|
300
|
-
|
|
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
|
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, \"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 "@
|
|
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
|
|
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",
|
|
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
|
|
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
|
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;;;;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.
|
|
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
|
-
"
|
|
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"
|
|
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
|
-
"
|
|
40
|
-
|
|
41
|
-
|
|
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
|
-
"
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
"
|
|
64
|
-
"
|
|
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": "
|
|
68
|
-
"
|
|
69
|
-
"test": "
|
|
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,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.
|