@zap-studio/permit 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @zap-studio/permit
|
|
2
2
|
|
|
3
|
+
## 0.1.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [e4542bb]
|
|
8
|
+
- @zap-studio/validation@0.2.1
|
|
9
|
+
|
|
10
|
+
## 0.1.2
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- 2de8183: Add a reusable synchronous Standard Schema validator helper in `@zap-studio/validation` and refactor `@zap-studio/permit` to use it for resource schema validation in `createPolicy`.
|
|
15
|
+
- Updated dependencies [2de8183]
|
|
16
|
+
- @zap-studio/validation@0.2.0
|
|
17
|
+
|
|
3
18
|
## 0.1.1
|
|
4
19
|
|
|
5
20
|
### Patch Changes
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":"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"}
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { t as PolicyError } from "./errors-CgD70cJR.mjs";
|
|
2
|
+
import { createSyncStandardValidator } from "@zap-studio/validation";
|
|
2
3
|
|
|
3
4
|
//#region src/index.ts
|
|
4
5
|
/**
|
|
@@ -238,11 +239,8 @@ function createPolicy(config) {
|
|
|
238
239
|
for (const key of Object.keys(resources)) {
|
|
239
240
|
const schema = resources[key];
|
|
240
241
|
if (!schema) throw new PolicyError(`Missing schema for resource: ${String(key)}`);
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (result instanceof Promise) throw new PolicyError("Async schemas are not supported in createPolicy");
|
|
244
|
-
return result;
|
|
245
|
-
});
|
|
242
|
+
const validator = createSyncStandardValidator(schema);
|
|
243
|
+
validators.set(key, (input) => validator(input));
|
|
246
244
|
}
|
|
247
245
|
return { can(context, action, resourceType, resource) {
|
|
248
246
|
const allowedActions = actions[resourceType];
|
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 { 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 * policy.can(ctx, \"read\", \"post\", post); // true\n * 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) => StandardSchemaV1.Result<unknown>\n >();\n\n const getValidatedResource = <K extends keyof TResources>(\n resourceType: K,\n resource: InferResource<TResources, K>\n ): InferResource<TResources, K> | null => {\n const validator = validators.get(resourceType);\n if (!validator) {\n return null;\n }\n try {\n const result = 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 validators.set(key, (input: unknown) => {\n const result = schema[\"~standard\"].validate(input);\n if (result instanceof Promise) {\n throw new PolicyError(\n \"Async schemas are not supported in createPolicy\"\n );\n }\n return result;\n });\n }\n\n return {\n can<K extends keyof TResources & keyof TActions>(\n context: TContext,\n action: InferAction<TActions, K>,\n resourceType: K,\n resource: InferResource<TResources, K>\n ): 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 = 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>(\n ...policies: Policy<TContext, TResources, TActions>[]\n): Policy<TContext, TResources, TActions> {\n return {\n can<K extends keyof TResources & keyof TActions>(\n context: TContext,\n action: InferAction<TActions, K>,\n resourceType: K,\n resource: InferResource<TResources, K>\n ): boolean {\n if (!policies.length) {\n return false;\n }\n for (const policy of policies) {\n if (!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 can<K extends keyof TResources & keyof TActions>(\n context: TContext,\n action: InferAction<TActions, K>,\n resourceType: K,\n resource: InferResource<TResources, K>\n ): boolean {\n if (!policies.length) {\n return false;\n }\n return policies.some((policy) =>\n policy.can(context, action, resourceType, resource)\n );\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 \"@standard-schema/spec\";\nimport { createSyncStandardValidator } 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 * policy.can(ctx, \"read\", \"post\", post); // true\n * 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) => StandardSchemaV1.Result<unknown>\n >();\n\n const getValidatedResource = <K extends keyof TResources>(\n resourceType: K,\n resource: InferResource<TResources, K>\n ): InferResource<TResources, K> | null => {\n const validator = validators.get(resourceType);\n if (!validator) {\n return null;\n }\n try {\n const result = 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 = createSyncStandardValidator(schema);\n validators.set(key, (input: unknown) => validator(input));\n }\n\n return {\n can<K extends keyof TResources & keyof TActions>(\n context: TContext,\n action: InferAction<TActions, K>,\n resourceType: K,\n resource: InferResource<TResources, K>\n ): 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 = 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>(\n ...policies: Policy<TContext, TResources, TActions>[]\n): Policy<TContext, TResources, TActions> {\n return {\n can<K extends keyof TResources & keyof TActions>(\n context: TContext,\n action: InferAction<TActions, K>,\n resourceType: K,\n resource: InferResource<TResources, K>\n ): boolean {\n if (!policies.length) {\n return false;\n }\n for (const policy of policies) {\n if (!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 can<K extends keyof TResources & keyof TActions>(\n context: TContext,\n action: InferAction<TActions, K>,\n resourceType: K,\n resource: InferResource<TResources, K>\n ): boolean {\n if (!policies.length) {\n return false;\n }\n return policies.some((policy) =>\n policy.can(context, action, resourceType, resource)\n );\n },\n };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zap-studio/permit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -34,7 +34,8 @@
|
|
|
34
34
|
"README.md"
|
|
35
35
|
],
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@standard-schema/spec": "^1.1.0"
|
|
37
|
+
"@standard-schema/spec": "^1.1.0",
|
|
38
|
+
"@zap-studio/validation": "0.2.1"
|
|
38
39
|
},
|
|
39
40
|
"devDependencies": {
|
|
40
41
|
"@types/node": "^25.0.2",
|