@zap-studio/permit 0.3.2 → 0.3.4
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 +16 -4
- package/LICENSE +1 -1
- package/README.md +18 -6
- package/dist/conditions.d.ts +177 -0
- package/dist/conditions.d.ts.map +1 -0
- package/dist/conditions.js +180 -0
- package/dist/conditions.js.map +1 -0
- package/dist/errors.d.ts +21 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/{errors.mjs → errors.js} +11 -1
- package/dist/errors.js.map +1 -0
- package/dist/helpers.d.ts +32 -0
- package/dist/helpers.d.ts.map +1 -0
- package/dist/{helpers.mjs → helpers.js} +8 -3
- package/dist/helpers.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/dist/policy.d.ts +89 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +153 -0
- package/dist/policy.js.map +1 -0
- package/dist/types.d.ts +142 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +0 -0
- package/package.json +14 -16
- package/dist/errors.d.mts +0 -11
- package/dist/errors.d.mts.map +0 -1
- package/dist/errors.mjs.map +0 -1
- package/dist/helpers.d.mts +0 -27
- package/dist/helpers.d.mts.map +0 -1
- package/dist/helpers.mjs.map +0 -1
- package/dist/index.d.mts +0 -256
- package/dist/index.d.mts.map +0 -1
- package/dist/index.mjs +0 -319
- package/dist/index.mjs.map +0 -1
- package/dist/types.d.mts +0 -143
- package/dist/types.d.mts.map +0 -1
- package/dist/types.mjs +0 -1
package/dist/policy.d.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Actions, Context, PermitConfig, Policy, Resources } from "./types.js";
|
|
2
|
+
//#region src/policy.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Creates a type-safe policy from resource schemas, actions, and rules.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { z } from "zod";
|
|
9
|
+
* import { createPolicy, allow, deny, when } from "@zap-studio/permit";
|
|
10
|
+
* import type { Resources, Actions } from "@zap-studio/permit/types";
|
|
11
|
+
*
|
|
12
|
+
* // Define resource schemas
|
|
13
|
+
* const resources = {
|
|
14
|
+
* post: z.object({
|
|
15
|
+
* id: z.string(),
|
|
16
|
+
* authorId: z.string(),
|
|
17
|
+
* visibility: z.enum(["public", "private"]),
|
|
18
|
+
* }),
|
|
19
|
+
* comment: z.object({
|
|
20
|
+
* id: z.string(),
|
|
21
|
+
* postId: z.string(),
|
|
22
|
+
* authorId: z.string(),
|
|
23
|
+
* }),
|
|
24
|
+
* } satisfies Resources;
|
|
25
|
+
*
|
|
26
|
+
* // Define actions per resource
|
|
27
|
+
* const actions = {
|
|
28
|
+
* post: ["read", "write", "delete"],
|
|
29
|
+
* comment: ["read", "write"],
|
|
30
|
+
* } as const satisfies Actions<typeof resources>;
|
|
31
|
+
*
|
|
32
|
+
* // Define context type
|
|
33
|
+
* type AppContext = { user: { id: string; role: string } };
|
|
34
|
+
*
|
|
35
|
+
* // Create the policy
|
|
36
|
+
* const policy = createPolicy<AppContext>({
|
|
37
|
+
* resources,
|
|
38
|
+
* actions,
|
|
39
|
+
* rules: {
|
|
40
|
+
* post: {
|
|
41
|
+
* read: when((ctx, action, resource) => resource.visibility === "public"),
|
|
42
|
+
* write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
|
|
43
|
+
* delete: deny(),
|
|
44
|
+
* },
|
|
45
|
+
* comment: {
|
|
46
|
+
* read: allow(),
|
|
47
|
+
* write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
|
|
48
|
+
* },
|
|
49
|
+
* },
|
|
50
|
+
* });
|
|
51
|
+
*
|
|
52
|
+
* // Check permissions
|
|
53
|
+
* const post = { id: "1", authorId: "user-1", visibility: "public" as const };
|
|
54
|
+
* await policy.can(ctx, "post:read", post); // true
|
|
55
|
+
* await policy.can(ctx, "post:write", post); // depends on ctx.user.id
|
|
56
|
+
* ```
|
|
57
|
+
*/
|
|
58
|
+
declare const createPolicy: <TContext extends Context, TResources extends Resources = Resources, TActions extends Actions<TResources> = Actions<TResources>>(config: PermitConfig<TContext, TResources, TActions>) => Policy<TContext, TResources, TActions>;
|
|
59
|
+
/**
|
|
60
|
+
* Merges multiple policies into one using "deny-overrides" strategy.
|
|
61
|
+
* If any policy denies, the merged policy denies. All must allow for the result to allow.
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```ts
|
|
65
|
+
* const basePolicy = createPolicy({ ... });
|
|
66
|
+
* const adminPolicy = createPolicy({ ... });
|
|
67
|
+
*
|
|
68
|
+
* const merged = mergePolicies(basePolicy, adminPolicy);
|
|
69
|
+
* // Both policies must allow for the action to be permitted
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
declare const mergePolicies: <TContext extends Context, TResources extends Resources = Resources, TActions extends Actions<TResources> = Actions<TResources>>(...policies: Policy<TContext, TResources, TActions>[]) => Policy<TContext, TResources, TActions>;
|
|
73
|
+
/**
|
|
74
|
+
* Merges multiple policies into one using "allow-overrides" strategy.
|
|
75
|
+
* If any policy allows, the merged policy allows. All must deny for the result to deny.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```ts
|
|
79
|
+
* const guestPolicy = createPolicy({ ... });
|
|
80
|
+
* const memberPolicy = createPolicy({ ... });
|
|
81
|
+
*
|
|
82
|
+
* const merged = mergePoliciesAny(guestPolicy, memberPolicy);
|
|
83
|
+
* // If either policy allows, the action is permitted
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
declare const mergePoliciesAny: <TContext extends Context, TResources extends Resources = Resources, TActions extends Actions<TResources> = Actions<TResources>>(...policies: Policy<TContext, TResources, TActions>[]) => Policy<TContext, TResources, TActions>;
|
|
87
|
+
//#endregion
|
|
88
|
+
export { createPolicy, mergePolicies, mergePoliciesAny };
|
|
89
|
+
//# sourceMappingURL=policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy.d.ts","names":[],"sources":["../src/policy.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAyGa,eACX,iBAAiB,SACjB,mBAAmB,YAAY,WAC/B,iBAAiB,QAAQ,cAAc,QAAQ,aAE/C,QAAQ,aAAa,UAAU,YAAY,cAC1C,OAAO,UAAU,YAAY;;;;;;;;;;;;;;cA2InB,gBACX,iBAAiB,SACjB,mBAAmB,YAAY,WAC/B,iBAAiB,QAAQ,cAAc,QAAQ,gBAE5C,UAAU,OAAO,UAAU,YAAY,gBACzC,OAAO,UAAU,YAAY;;;;;;;;;;;;;;cAgBnB,mBACX,iBAAiB,SACjB,mBAAmB,YAAY,WAC/B,iBAAiB,QAAQ,cAAc,QAAQ,gBAE5C,UAAU,OAAO,UAAU,YAAY,gBACzC,OAAO,UAAU,YAAY"}
|
package/dist/policy.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { PolicyError } from "./errors.js";
|
|
2
|
+
import { createStandardValidator } from "@zap-studio/validation";
|
|
3
|
+
//#region src/policy.ts
|
|
4
|
+
/**
|
|
5
|
+
* Splits a typed `resource:action` permission string into its parts.
|
|
6
|
+
* Returns `null` when the string is malformed (missing/empty part or extra segments).
|
|
7
|
+
*/
|
|
8
|
+
const parsePermission = (permission) => {
|
|
9
|
+
const [resourceTypeValue, actionValue, ...rest] = permission.split(":");
|
|
10
|
+
if (resourceTypeValue === void 0 || resourceTypeValue.length === 0 || actionValue === void 0 || actionValue.length === 0 || rest.length > 0) return null;
|
|
11
|
+
return {
|
|
12
|
+
action: actionValue,
|
|
13
|
+
resourceType: resourceTypeValue
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Creates a type-safe policy from resource schemas, actions, and rules.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* import { z } from "zod";
|
|
22
|
+
* import { createPolicy, allow, deny, when } from "@zap-studio/permit";
|
|
23
|
+
* import type { Resources, Actions } from "@zap-studio/permit/types";
|
|
24
|
+
*
|
|
25
|
+
* // Define resource schemas
|
|
26
|
+
* const resources = {
|
|
27
|
+
* post: z.object({
|
|
28
|
+
* id: z.string(),
|
|
29
|
+
* authorId: z.string(),
|
|
30
|
+
* visibility: z.enum(["public", "private"]),
|
|
31
|
+
* }),
|
|
32
|
+
* comment: z.object({
|
|
33
|
+
* id: z.string(),
|
|
34
|
+
* postId: z.string(),
|
|
35
|
+
* authorId: z.string(),
|
|
36
|
+
* }),
|
|
37
|
+
* } satisfies Resources;
|
|
38
|
+
*
|
|
39
|
+
* // Define actions per resource
|
|
40
|
+
* const actions = {
|
|
41
|
+
* post: ["read", "write", "delete"],
|
|
42
|
+
* comment: ["read", "write"],
|
|
43
|
+
* } as const satisfies Actions<typeof resources>;
|
|
44
|
+
*
|
|
45
|
+
* // Define context type
|
|
46
|
+
* type AppContext = { user: { id: string; role: string } };
|
|
47
|
+
*
|
|
48
|
+
* // Create the policy
|
|
49
|
+
* const policy = createPolicy<AppContext>({
|
|
50
|
+
* resources,
|
|
51
|
+
* actions,
|
|
52
|
+
* rules: {
|
|
53
|
+
* post: {
|
|
54
|
+
* read: when((ctx, action, resource) => resource.visibility === "public"),
|
|
55
|
+
* write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
|
|
56
|
+
* delete: deny(),
|
|
57
|
+
* },
|
|
58
|
+
* comment: {
|
|
59
|
+
* read: allow(),
|
|
60
|
+
* write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
|
|
61
|
+
* },
|
|
62
|
+
* },
|
|
63
|
+
* });
|
|
64
|
+
*
|
|
65
|
+
* // Check permissions
|
|
66
|
+
* const post = { id: "1", authorId: "user-1", visibility: "public" as const };
|
|
67
|
+
* await policy.can(ctx, "post:read", post); // true
|
|
68
|
+
* await policy.can(ctx, "post:write", post); // depends on ctx.user.id
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
const createPolicy = (config) => {
|
|
72
|
+
const { rules, resources, actions } = config;
|
|
73
|
+
const validators = /* @__PURE__ */ new Map();
|
|
74
|
+
const getValidatedResource = async (resourceType, resource) => {
|
|
75
|
+
const validator = validators.get(resourceType);
|
|
76
|
+
if (validator === void 0) return null;
|
|
77
|
+
try {
|
|
78
|
+
const result = await validator(resource);
|
|
79
|
+
if (result.issues) return null;
|
|
80
|
+
return result.value;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.warn(`Resource validation failed for ${String(resourceType)}: ${String(error)}`);
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const hasAllowedAction = (resourceType, action) => actions[resourceType]?.includes(action) ?? false;
|
|
87
|
+
const evaluatePolicy = (context, resourceType, action, resource) => {
|
|
88
|
+
const policyFn = rules[resourceType]?.[action];
|
|
89
|
+
if (policyFn === void 0) return false;
|
|
90
|
+
try {
|
|
91
|
+
return policyFn(context, action, resource) === "allow";
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.warn(`Policy evaluation error for ${String(resourceType)}.${action}: ${String(error)}`);
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
for (const key of Object.keys(resources)) {
|
|
98
|
+
const schema = resources[key];
|
|
99
|
+
if (schema === void 0) throw new PolicyError(`Missing schema for resource: ${String(key)}`);
|
|
100
|
+
const validator = createStandardValidator(schema);
|
|
101
|
+
validators.set(key, async (input) => await validator(input));
|
|
102
|
+
}
|
|
103
|
+
return { async can(context, permission, resource) {
|
|
104
|
+
const parsedPermission = parsePermission(permission);
|
|
105
|
+
if (parsedPermission === null) return false;
|
|
106
|
+
const { action, resourceType } = parsedPermission;
|
|
107
|
+
if (!hasAllowedAction(resourceType, action)) return false;
|
|
108
|
+
const validatedResource = await getValidatedResource(resourceType, resource);
|
|
109
|
+
if (validatedResource === null) return false;
|
|
110
|
+
return evaluatePolicy(context, resourceType, action, validatedResource);
|
|
111
|
+
} };
|
|
112
|
+
};
|
|
113
|
+
const mergePoliciesWithStrategy = (policies, strategy) => ({ async can(context, permission, resource) {
|
|
114
|
+
if (policies.length === 0) return false;
|
|
115
|
+
for (const policy of policies) {
|
|
116
|
+
const allowed = await policy.can(context, permission, resource);
|
|
117
|
+
if (strategy === "allow-overrides" && allowed) return true;
|
|
118
|
+
if (strategy === "deny-overrides" && !allowed) return false;
|
|
119
|
+
}
|
|
120
|
+
return strategy === "deny-overrides";
|
|
121
|
+
} });
|
|
122
|
+
/**
|
|
123
|
+
* Merges multiple policies into one using "deny-overrides" strategy.
|
|
124
|
+
* If any policy denies, the merged policy denies. All must allow for the result to allow.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```ts
|
|
128
|
+
* const basePolicy = createPolicy({ ... });
|
|
129
|
+
* const adminPolicy = createPolicy({ ... });
|
|
130
|
+
*
|
|
131
|
+
* const merged = mergePolicies(basePolicy, adminPolicy);
|
|
132
|
+
* // Both policies must allow for the action to be permitted
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
const mergePolicies = (...policies) => mergePoliciesWithStrategy(policies, "deny-overrides");
|
|
136
|
+
/**
|
|
137
|
+
* Merges multiple policies into one using "allow-overrides" strategy.
|
|
138
|
+
* If any policy allows, the merged policy allows. All must deny for the result to deny.
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```ts
|
|
142
|
+
* const guestPolicy = createPolicy({ ... });
|
|
143
|
+
* const memberPolicy = createPolicy({ ... });
|
|
144
|
+
*
|
|
145
|
+
* const merged = mergePoliciesAny(guestPolicy, memberPolicy);
|
|
146
|
+
* // If either policy allows, the action is permitted
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
const mergePoliciesAny = (...policies) => mergePoliciesWithStrategy(policies, "allow-overrides");
|
|
150
|
+
//#endregion
|
|
151
|
+
export { createPolicy, mergePolicies, mergePoliciesAny };
|
|
152
|
+
|
|
153
|
+
//# sourceMappingURL=policy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy.js","names":[],"sources":["../src/policy.ts"],"sourcesContent":["/**\n * Policy creation and composition: `createPolicy`, `mergePolicies`, and\n * `mergePoliciesAny`.\n *\n * @module @zap-studio/permit/policy\n */\n\nimport type { StandardSchemaV1 } from \"@zap-studio/validation\";\nimport { createStandardValidator } from \"@zap-studio/validation\";\n\nimport { PolicyError } from \"./errors.js\";\nimport type {\n Actions,\n Context,\n InferAction,\n InferResource,\n PermitConfig,\n Policy,\n Resources,\n} from \"./types.js\";\n\n/**\n * Splits a typed `resource:action` permission string into its parts.\n * Returns `null` when the string is malformed (missing/empty part or extra segments).\n */\nconst parsePermission = <\n TResources extends Resources,\n TActions extends Actions<TResources>,\n K extends keyof TResources & keyof TActions,\n>(\n permission: `${K & string}:${InferAction<TActions, K> & string}`\n): { action: InferAction<TActions, K>; resourceType: K } | null => {\n const [resourceTypeValue, actionValue, ...rest] = permission.split(\":\");\n if (\n resourceTypeValue === undefined ||\n resourceTypeValue.length === 0 ||\n actionValue === undefined ||\n actionValue.length === 0 ||\n rest.length > 0\n ) {\n return null;\n }\n\n return {\n action: actionValue,\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Parsed permission strings are constrained by the typed permission template.\n resourceType: resourceTypeValue as K,\n };\n};\n\n/**\n * Creates a type-safe policy from resource schemas, actions, and rules.\n *\n * @example\n * ```ts\n * import { z } from \"zod\";\n * import { createPolicy, allow, deny, when } from \"@zap-studio/permit\";\n * import type { Resources, Actions } from \"@zap-studio/permit/types\";\n *\n * // Define resource schemas\n * const resources = {\n * post: z.object({\n * id: z.string(),\n * authorId: z.string(),\n * visibility: z.enum([\"public\", \"private\"]),\n * }),\n * comment: z.object({\n * id: z.string(),\n * postId: z.string(),\n * authorId: z.string(),\n * }),\n * } satisfies Resources;\n *\n * // Define actions per resource\n * const actions = {\n * post: [\"read\", \"write\", \"delete\"],\n * comment: [\"read\", \"write\"],\n * } as const satisfies Actions<typeof resources>;\n *\n * // Define context type\n * type AppContext = { user: { id: string; role: string } };\n *\n * // Create the policy\n * const policy = createPolicy<AppContext>({\n * resources,\n * actions,\n * rules: {\n * post: {\n * read: when((ctx, action, resource) => resource.visibility === \"public\"),\n * write: when((ctx, action, resource) => ctx.user.id === resource.authorId),\n * delete: deny(),\n * },\n * comment: {\n * read: allow(),\n * write: when((ctx, action, resource) => ctx.user.id === resource.authorId),\n * },\n * },\n * });\n *\n * // Check permissions\n * const post = { id: \"1\", authorId: \"user-1\", visibility: \"public\" as const };\n * await policy.can(ctx, \"post:read\", post); // true\n * await policy.can(ctx, \"post:write\", post); // depends on ctx.user.id\n * ```\n */\nexport const createPolicy = <\n TContext extends Context,\n TResources extends Resources = Resources,\n TActions extends Actions<TResources> = Actions<TResources>,\n>(\n config: PermitConfig<TContext, TResources, TActions>\n): Policy<TContext, TResources, TActions> => {\n const { rules, resources, actions } = config;\n const validators = new Map<\n keyof TResources,\n (input: unknown) => Promise<StandardSchemaV1.Result<unknown>>\n >();\n\n const getValidatedResource = async <K extends keyof TResources>(\n resourceType: K,\n resource: InferResource<TResources, K>\n ): Promise<InferResource<TResources, K> | null> => {\n const validator = validators.get(resourceType);\n if (validator === undefined) {\n return null;\n }\n try {\n const result = await validator(resource);\n if (result.issues) {\n return null;\n }\n return result.value;\n } catch (error) {\n console.warn(\n `Resource validation failed for ${String(resourceType)}: ${String(error)}`\n );\n return null;\n }\n };\n\n const hasAllowedAction = <K extends keyof TResources & keyof TActions>(\n resourceType: K,\n action: InferAction<TActions, K>\n ): boolean => actions[resourceType]?.includes(action) ?? false;\n\n const evaluatePolicy = <K extends keyof TResources & keyof TActions>(\n context: TContext,\n resourceType: K,\n action: InferAction<TActions, K>,\n resource: InferResource<TResources, K>\n ): boolean => {\n const policyFn = rules[resourceType]?.[action];\n if (policyFn === undefined) {\n return false;\n }\n\n try {\n return policyFn(context, action, resource) === \"allow\";\n } catch (error) {\n console.warn(\n `Policy evaluation error for ${String(resourceType)}.${action}: ${String(error)}`\n );\n return false;\n }\n };\n\n for (const key of Object.keys(resources) as (keyof TResources)[]) {\n const schema = resources[key];\n if (schema === undefined) {\n throw new PolicyError(`Missing schema for resource: ${String(key)}`);\n }\n const validator = createStandardValidator(schema);\n validators.set(key, async (input: unknown) => await validator(input));\n }\n\n return {\n async can<K extends keyof TResources & keyof TActions>(\n context: TContext,\n permission: `${K & string}:${InferAction<TActions, K> & string}`,\n resource: InferResource<TResources, K>\n ): Promise<boolean> {\n const parsedPermission = parsePermission<TResources, TActions, K>(\n permission\n );\n if (parsedPermission === null) {\n return false;\n }\n\n const { action, resourceType } = parsedPermission;\n if (!hasAllowedAction(resourceType, action)) {\n return false;\n }\n\n const validatedResource = await getValidatedResource(\n resourceType,\n resource\n );\n if (validatedResource === null) {\n return false;\n }\n\n return evaluatePolicy(context, resourceType, action, validatedResource);\n },\n };\n};\n\nconst mergePoliciesWithStrategy = <\n TContext extends Context,\n TResources extends Resources = Resources,\n TActions extends Actions<TResources> = Actions<TResources>,\n>(\n policies: Policy<TContext, TResources, TActions>[],\n strategy: \"allow-overrides\" | \"deny-overrides\"\n): Policy<TContext, TResources, TActions> => ({\n async can<K extends keyof TResources & keyof TActions>(\n context: TContext,\n permission: `${K & string}:${InferAction<TActions, K> & string}`,\n resource: InferResource<TResources, K>\n ): Promise<boolean> {\n if (policies.length === 0) {\n return false;\n }\n for (const policy of policies) {\n // oxlint-disable-next-line no-await-in-loop -- Policies must evaluate sequentially to preserve short-circuit semantics.\n const allowed = await policy.can(context, permission, resource);\n\n if (strategy === \"allow-overrides\" && allowed) {\n return true;\n }\n if (strategy === \"deny-overrides\" && !allowed) {\n return false;\n }\n }\n return strategy === \"deny-overrides\";\n },\n});\n\n/**\n * Merges multiple policies into one using \"deny-overrides\" strategy.\n * If any policy denies, the merged policy denies. All must allow for the result to allow.\n *\n * @example\n * ```ts\n * const basePolicy = createPolicy({ ... });\n * const adminPolicy = createPolicy({ ... });\n *\n * const merged = mergePolicies(basePolicy, adminPolicy);\n * // Both policies must allow for the action to be permitted\n * ```\n */\nexport const mergePolicies = <\n TContext extends Context,\n TResources extends Resources = Resources,\n TActions extends Actions<TResources> = Actions<TResources>,\n>(\n ...policies: Policy<TContext, TResources, TActions>[]\n): Policy<TContext, TResources, TActions> =>\n mergePoliciesWithStrategy(policies, \"deny-overrides\");\n\n/**\n * Merges multiple policies into one using \"allow-overrides\" strategy.\n * If any policy allows, the merged policy allows. All must deny for the result to deny.\n *\n * @example\n * ```ts\n * const guestPolicy = createPolicy({ ... });\n * const memberPolicy = createPolicy({ ... });\n *\n * const merged = mergePoliciesAny(guestPolicy, memberPolicy);\n * // If either policy allows, the action is permitted\n * ```\n */\nexport const mergePoliciesAny = <\n TContext extends Context,\n TResources extends Resources = Resources,\n TActions extends Actions<TResources> = Actions<TResources>,\n>(\n ...policies: Policy<TContext, TResources, TActions>[]\n): Policy<TContext, TResources, TActions> =>\n mergePoliciesWithStrategy(policies, \"allow-overrides\");\n"],"mappings":";;;;;;;AAyBA,MAAM,mBAKJ,eACiE;CACjE,MAAM,CAAC,mBAAmB,aAAa,GAAG,QAAQ,WAAW,MAAM,GAAG;CACtE,IACE,sBAAsB,KAAA,KACtB,kBAAkB,WAAW,KAC7B,gBAAgB,KAAA,KAChB,YAAY,WAAW,KACvB,KAAK,SAAS,GAEd,OAAO;CAGT,OAAO;EACL,QAAQ;EAER,cAAc;CAChB;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyDA,MAAa,gBAKX,WAC2C;CAC3C,MAAM,EAAE,OAAO,WAAW,YAAY;CACtC,MAAM,6BAAa,IAAI,IAGrB;CAEF,MAAM,uBAAuB,OAC3B,cACA,aACiD;EACjD,MAAM,YAAY,WAAW,IAAI,YAAY;EAC7C,IAAI,cAAc,KAAA,GAChB,OAAO;EAET,IAAI;GACF,MAAM,SAAS,MAAM,UAAU,QAAQ;GACvC,IAAI,OAAO,QACT,OAAO;GAET,OAAO,OAAO;EAChB,SAAS,OAAO;GACd,QAAQ,KACN,kCAAkC,OAAO,YAAY,EAAE,IAAI,OAAO,KAAK,GACzE;GACA,OAAO;EACT;CACF;CAEA,MAAM,oBACJ,cACA,WACY,QAAQ,aAAa,EAAE,SAAS,MAAM,KAAK;CAEzD,MAAM,kBACJ,SACA,cACA,QACA,aACY;EACZ,MAAM,WAAW,MAAM,aAAa,GAAG;EACvC,IAAI,aAAa,KAAA,GACf,OAAO;EAGT,IAAI;GACF,OAAO,SAAS,SAAS,QAAQ,QAAQ,MAAM;EACjD,SAAS,OAAO;GACd,QAAQ,KACN,+BAA+B,OAAO,YAAY,EAAE,GAAG,OAAO,IAAI,OAAO,KAAK,GAChF;GACA,OAAO;EACT;CACF;CAEA,KAAK,MAAM,OAAO,OAAO,KAAK,SAAS,GAA2B;EAChE,MAAM,SAAS,UAAU;EACzB,IAAI,WAAW,KAAA,GACb,MAAM,IAAI,YAAY,gCAAgC,OAAO,GAAG,GAAG;EAErE,MAAM,YAAY,wBAAwB,MAAM;EAChD,WAAW,IAAI,KAAK,OAAO,UAAmB,MAAM,UAAU,KAAK,CAAC;CACtE;CAEA,OAAO,EACL,MAAM,IACJ,SACA,YACA,UACkB;EAClB,MAAM,mBAAmB,gBACvB,UACF;EACA,IAAI,qBAAqB,MACvB,OAAO;EAGT,MAAM,EAAE,QAAQ,iBAAiB;EACjC,IAAI,CAAC,iBAAiB,cAAc,MAAM,GACxC,OAAO;EAGT,MAAM,oBAAoB,MAAM,qBAC9B,cACA,QACF;EACA,IAAI,sBAAsB,MACxB,OAAO;EAGT,OAAO,eAAe,SAAS,cAAc,QAAQ,iBAAiB;CACxE,EACF;AACF;AAEA,MAAM,6BAKJ,UACA,cAC4C,EAC5C,MAAM,IACJ,SACA,YACA,UACkB;CAClB,IAAI,SAAS,WAAW,GACtB,OAAO;CAET,KAAK,MAAM,UAAU,UAAU;EAE7B,MAAM,UAAU,MAAM,OAAO,IAAI,SAAS,YAAY,QAAQ;EAE9D,IAAI,aAAa,qBAAqB,SACpC,OAAO;EAET,IAAI,aAAa,oBAAoB,CAAC,SACpC,OAAO;CAEX;CACA,OAAO,aAAa;AACtB,EACF;;;;;;;;;;;;;;AAeA,MAAa,iBAKX,GAAG,aAEH,0BAA0B,UAAU,gBAAgB;;;;;;;;;;;;;;AAetD,MAAa,oBAKX,GAAG,aAEH,0BAA0B,UAAU,iBAAiB"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { StandardSchemaV1 } from "@zap-studio/validation";
|
|
2
|
+
//#region src/types.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Represents the possible outcomes of a policy decision.
|
|
5
|
+
* - "allow": The action is permitted.
|
|
6
|
+
* - "deny": The action is not permitted.
|
|
7
|
+
*/
|
|
8
|
+
type Decision = "allow" | "deny";
|
|
9
|
+
/**
|
|
10
|
+
* Represents the context in which a policy decision is made.
|
|
11
|
+
* Can include user information, environment, or any relevant data.
|
|
12
|
+
*/
|
|
13
|
+
type Context<TContext = unknown> = TContext;
|
|
14
|
+
/**
|
|
15
|
+
* Represents a role within the system.
|
|
16
|
+
*/
|
|
17
|
+
type Role<TRole extends string = string> = TRole;
|
|
18
|
+
/**
|
|
19
|
+
* Represents a role hierarchy within the system.
|
|
20
|
+
* Maps each role to an array of roles it inherits from.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* type Roles = "guest" | "user" | "admin";
|
|
25
|
+
*
|
|
26
|
+
* const hierarchy: RoleHierarchy<Roles> = {
|
|
27
|
+
* guest: [],
|
|
28
|
+
* user: ["guest"],
|
|
29
|
+
* admin: ["user"],
|
|
30
|
+
* };
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
type RoleHierarchy<TRole extends Role = Role> = Record<TRole, TRole[]>;
|
|
34
|
+
/**
|
|
35
|
+
* Type helper for defining resource schemas using Standard Schema.
|
|
36
|
+
* Use with `satisfies` to ensure type safety when defining resources.
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```ts
|
|
40
|
+
* import { z } from "zod";
|
|
41
|
+
* import type { Resources } from "@zap-studio/permit/types";
|
|
42
|
+
*
|
|
43
|
+
* const resources = {
|
|
44
|
+
* post: z.object({ id: z.string(), authorId: z.string() }),
|
|
45
|
+
* comment: z.object({ id: z.string(), postId: z.string() }),
|
|
46
|
+
* } satisfies Resources;
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
type Resources<TResourceKey extends string = string> = Record<TResourceKey, StandardSchemaV1>;
|
|
50
|
+
/**
|
|
51
|
+
* Type helper for defining actions per resource.
|
|
52
|
+
* Use with `satisfies` to ensure keys match the resource definitions.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```ts
|
|
56
|
+
* import type { Actions } from "@zap-studio/permit/types";
|
|
57
|
+
*
|
|
58
|
+
* const actions = {
|
|
59
|
+
* post: ["read", "write", "delete"],
|
|
60
|
+
* comment: ["read", "write"],
|
|
61
|
+
* } as const satisfies Actions<typeof resources>;
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
type Actions<TResources extends Resources> = { [K in keyof TResources]: readonly string[]; };
|
|
65
|
+
/**
|
|
66
|
+
* Infers the output type from a Standard Schema.
|
|
67
|
+
*/
|
|
68
|
+
type InferResource<TResources extends Resources, TResourceKey extends keyof TResources> = StandardSchemaV1.InferOutput<TResources[TResourceKey]>;
|
|
69
|
+
/**
|
|
70
|
+
* Infers the action union type for a specific resource.
|
|
71
|
+
*/
|
|
72
|
+
type InferAction<TActions extends Record<string, readonly string[]>, K extends keyof TActions> = TActions[K][number];
|
|
73
|
+
/**
|
|
74
|
+
* Infers the permission-string union for all resource/action combinations.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```ts
|
|
78
|
+
* type Permission = InferPermission<typeof resources, typeof actions>;
|
|
79
|
+
* // "post:read" | "post:write" | "comment:read"
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
type InferPermission<TResources extends Resources, TActions extends Actions<TResources>> = { [K in keyof TResources & keyof TActions]: `${K & string}:${InferAction<TActions, K> & string}`; }[keyof TResources & keyof TActions];
|
|
83
|
+
/**
|
|
84
|
+
* A function that determines whether a given action on a resource is allowed in a specific context.
|
|
85
|
+
*/
|
|
86
|
+
type PolicyFn<TContext extends Context, TAction extends string = string, TResource = unknown> = (context: TContext, action: TAction, resource: TResource) => Decision;
|
|
87
|
+
/**
|
|
88
|
+
* A function that evaluates a condition for a given action and resource in a specific context.
|
|
89
|
+
*/
|
|
90
|
+
type ConditionFn<TContext extends Context, TAction extends string = string, TResource = unknown> = (context: TContext, action: TAction, resource: TResource) => boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Maps actions to their corresponding policy functions for a specific resource.
|
|
93
|
+
*/
|
|
94
|
+
type ActionPolicyMap<TContext extends Context, TAction extends string = string, TResource = unknown> = { [A in TAction]?: PolicyFn<TContext, A, TResource>; };
|
|
95
|
+
/**
|
|
96
|
+
* Defines the rules for each resource and action combination.
|
|
97
|
+
* Each resource key maps to an object where each action key maps to a policy function.
|
|
98
|
+
*/
|
|
99
|
+
type Rules<TContext extends Context, TResources extends Resources = Resources, TActions extends Actions<TResources> = Actions<TResources>> = { [K in keyof TResources & keyof TActions]: ActionPolicyMap<TContext, InferAction<TActions, K>, InferResource<TResources, K>>; };
|
|
100
|
+
/**
|
|
101
|
+
* Configuration object for creating a permit policy.
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* const config: PermitConfig<MyContext> = {
|
|
106
|
+
* resources,
|
|
107
|
+
* actions,
|
|
108
|
+
* rules: {
|
|
109
|
+
* post: { read: allow(), write: deny() },
|
|
110
|
+
* },
|
|
111
|
+
* };
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
interface PermitConfig<TContext extends Context, TResources extends Resources = Resources, TActions extends Actions<TResources> = Actions<TResources>> {
|
|
115
|
+
actions: TActions;
|
|
116
|
+
resources: TResources;
|
|
117
|
+
rules: Rules<TContext, TResources, TActions>;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Represents a policy object that can evaluate permissions.
|
|
121
|
+
* The `can` method checks if a given action is permitted on a resource in a specific context.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```ts
|
|
125
|
+
* const policy: Policy<MyContext> = createPolicy({
|
|
126
|
+
* resources,
|
|
127
|
+
* actions,
|
|
128
|
+
* rules: { ... },
|
|
129
|
+
* });
|
|
130
|
+
*
|
|
131
|
+
* await policy.can(ctx, "post:read", postData); // true or false
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
interface Policy<TContext extends Context, TResources extends Resources = Resources, TActions extends Actions<TResources> = Actions<TResources>> {
|
|
135
|
+
/**
|
|
136
|
+
* Determines if the specified action is permitted on the resource in the given context.
|
|
137
|
+
*/
|
|
138
|
+
can: <K extends keyof TResources & keyof TActions>(context: TContext, permission: `${K & string}:${InferAction<TActions, K> & string}`, resource: InferResource<TResources, K>) => Promise<boolean>;
|
|
139
|
+
}
|
|
140
|
+
//#endregion
|
|
141
|
+
export { ActionPolicyMap, Actions, ConditionFn, Context, Decision, InferAction, InferPermission, InferResource, PermitConfig, Policy, PolicyFn, Resources, Role, RoleHierarchy, Rules };
|
|
142
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;;;KAaY;;;;;KAMA,QAAQ,sBAAsB;;;;KAK9B,KAAK,iCAAiC;;;;;;;;;;;;;;;;KAiBtC,cAAc,cAAc,OAAO,QAAQ,OAAO,OAAO;;;;;;;;;;;;;;;;KAiBzD,UAAU,wCAAwC,OAC5D,cACA;;;;;;;;;;;;;;;KAiBU,QAAQ,mBAAmB,gBACpC,WAAW;;;;KAMF,cACV,mBAAmB,WACnB,2BAA2B,cACzB,iBAAiB,YAAY,WAAW;;;;KAKhC,YACV,iBAAiB,mCACjB,gBAAgB,YACd,SAAS;;;;;;;;;;KAWD,gBACV,mBAAmB,WACnB,iBAAiB,QAAQ,kBAExB,WAAW,mBACJ,cAAc,cAAc,YAAY,UAAU,uBACpD,mBAAmB;;;;KAKf,SACV,iBAAiB,SACjB,iCACA,wBACG,SAAS,UAAU,QAAQ,SAAS,UAAU,cAAc;;;;KAKrD,YACV,iBAAiB,SACjB,iCACA,wBACG,SAAS,UAAU,QAAQ,SAAS,UAAU;;;;KAKvC,gBACV,iBAAiB,SACjB,iCACA,0BAEC,KAAK,WAAW,SAAS,UAAU,GAAG;;;;;KAO7B,MACV,iBAAiB,SACjB,mBAAmB,YAAY,WAC/B,iBAAiB,QAAQ,cAAc,QAAQ,kBAE9C,WAAW,mBAAmB,WAAW,gBACxC,UACA,YAAY,UAAU,IACtB,cAAc,YAAY;;;;;;;;;;;;;;;UAkBb,aACf,iBAAiB,SACjB,mBAAmB,YAAY,WAC/B,iBAAiB,QAAQ,cAAc,QAAQ;EAE/C,SAAS;EACT,WAAW;EACX,OAAO,MAAM,UAAU,YAAY;;;;;;;;;;;;;;;;;UAkBpB,OACf,iBAAiB,SACjB,mBAAmB,YAAY,WAC/B,iBAAiB,QAAQ,cAAc,QAAQ;;;;EAK/C,MAAM,gBAAgB,mBAAmB,UACvC,SAAS,UACT,eAAe,cAAc,YAAY,UAAU,eACnD,UAAU,cAAc,YAAY,OACjC"}
|
package/dist/types.js
ADDED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zap-studio/permit",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "A type-safe, declarative authorization library for TypeScript with Standard Schema support",
|
|
6
6
|
"keywords": [
|
|
@@ -31,31 +31,29 @@
|
|
|
31
31
|
],
|
|
32
32
|
"type": "module",
|
|
33
33
|
"sideEffects": false,
|
|
34
|
-
"types": "./dist/index.d.
|
|
34
|
+
"types": "./dist/index.d.ts",
|
|
35
35
|
"exports": {
|
|
36
|
-
".": "./dist/index.
|
|
37
|
-
"./
|
|
38
|
-
"./
|
|
39
|
-
"./
|
|
36
|
+
".": "./dist/index.js",
|
|
37
|
+
"./conditions": "./dist/conditions.js",
|
|
38
|
+
"./errors": "./dist/errors.js",
|
|
39
|
+
"./helpers": "./dist/helpers.js",
|
|
40
|
+
"./policy": "./dist/policy.js",
|
|
41
|
+
"./types": "./dist/types.js",
|
|
40
42
|
"./package.json": "./package.json"
|
|
41
43
|
},
|
|
42
44
|
"publishConfig": {
|
|
43
45
|
"access": "public"
|
|
44
46
|
},
|
|
45
47
|
"dependencies": {
|
|
46
|
-
"@zap-studio/validation": "
|
|
48
|
+
"@zap-studio/validation": "workspace:*"
|
|
47
49
|
},
|
|
48
50
|
"devDependencies": {
|
|
49
|
-
"typescript": "
|
|
50
|
-
"
|
|
51
|
-
"
|
|
51
|
+
"@zap-studio/typescript": "workspace:*",
|
|
52
|
+
"tsdown": "catalog:",
|
|
53
|
+
"typescript": "catalog:",
|
|
54
|
+
"vitest": "catalog:"
|
|
52
55
|
},
|
|
53
56
|
"engines": {
|
|
54
57
|
"node": ">=18.0.0"
|
|
55
|
-
},
|
|
56
|
-
"scripts": {
|
|
57
|
-
"build": "vp pack",
|
|
58
|
-
"test": "vp test run",
|
|
59
|
-
"test:watch": "vp test watch"
|
|
60
58
|
}
|
|
61
|
-
}
|
|
59
|
+
}
|
package/dist/errors.d.mts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
//#region src/errors.d.ts
|
|
2
|
-
/**
|
|
3
|
-
* Represents an error that occurs during policy evaluation or enforcement.
|
|
4
|
-
* Use this error to indicate issues related to policy logic, configuration, or execution.
|
|
5
|
-
*/
|
|
6
|
-
declare class PolicyError extends Error {
|
|
7
|
-
constructor(message: string);
|
|
8
|
-
}
|
|
9
|
-
//#endregion
|
|
10
|
-
export { PolicyError };
|
|
11
|
-
//# sourceMappingURL=errors.d.mts.map
|
package/dist/errors.d.mts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"errors.mjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * Represents an error that occurs during policy evaluation or enforcement.\n * Use this error to indicate issues related to policy logic, configuration, or execution.\n */\nexport class PolicyError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"PolicyError\";\n }\n}\n"],"mappings":";;;;;AAIA,IAAa,cAAb,cAAiC,MAAM;CACrC,YAAY,SAAiB;AAC3B,QAAM,QAAQ;AACd,OAAK,OAAO"}
|
package/dist/helpers.d.mts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
//#region src/helpers.d.ts
|
|
2
|
-
/**
|
|
3
|
-
* Ensures that a value of type `never` is actually never encountered at runtime.
|
|
4
|
-
* This is useful for exhaustive checks on discriminated unions.
|
|
5
|
-
*
|
|
6
|
-
* @example
|
|
7
|
-
* ```ts
|
|
8
|
-
* type Action = 'read' | 'write'
|
|
9
|
-
*
|
|
10
|
-
* function performAction(action: Action) {
|
|
11
|
-
* switch (action) {
|
|
12
|
-
* case 'read':
|
|
13
|
-
* console.log('Reading...')
|
|
14
|
-
* break
|
|
15
|
-
* case 'write':
|
|
16
|
-
* console.log('Writing...')
|
|
17
|
-
* break
|
|
18
|
-
* default:
|
|
19
|
-
* assertNever(action) // TypeScript will error if a new Action is added but not handled
|
|
20
|
-
* }
|
|
21
|
-
* }
|
|
22
|
-
* ```
|
|
23
|
-
*/
|
|
24
|
-
declare function assertNever(value: never): never;
|
|
25
|
-
//#endregion
|
|
26
|
-
export { assertNever };
|
|
27
|
-
//# sourceMappingURL=helpers.d.mts.map
|
package/dist/helpers.d.mts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.d.mts","names":[],"sources":["../src/helpers.ts"],"mappings":";;AAsBA;;;;;;;;;;;;;;;;;;;;;iBAAgB,WAAA,CAAY,KAAA"}
|
package/dist/helpers.mjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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"}
|