@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/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## @zap-studio/permit@0.3.4
|
|
2
|
+
|
|
3
|
+
### Tree-shakeable root re-exports
|
|
4
|
+
|
|
5
|
+
The package root now re-exports the full public API, so everything can be imported from `@zap-studio/permit` directly, including `PolicyError`, `assertNever`, and all public types. All exports are side-effect free and tree-shakeable; granular subpath imports keep working.
|
|
6
|
+
|
|
7
|
+
- The implementation moved out of the entrypoint into two new subpaths: `./conditions` (`allow`, `deny`, `when`, `and`, `or`, `not`, `has`, `hasRole`, `collectInheritedRoles`) and `./policy` (`createPolicy`, `mergePolicies`, `mergePoliciesAny`).
|
|
8
|
+
|
|
9
|
+
## @zap-studio/permit@0.3.3
|
|
10
|
+
|
|
11
|
+
### Migrate to ultracite lint/format
|
|
12
|
+
|
|
13
|
+
Internal formatting and lint cleanup only. No public API or behavior change.
|
|
14
|
+
|
|
1
15
|
# @zap-studio/permit
|
|
2
16
|
|
|
3
17
|
## 0.3.2
|
|
@@ -29,11 +43,9 @@
|
|
|
29
43
|
|
|
30
44
|
- fe60f55: Change `policy.can()` to use a single permission string plus the resource object.
|
|
31
45
|
|
|
32
|
-
`policy.can(ctx, "read", "post", post)` is replaced by
|
|
33
|
-
`policy.can(ctx, "post:read", post)`.
|
|
46
|
+
`policy.can(ctx, "read", "post", post)` is replaced by `policy.can(ctx, "post:read", post)`.
|
|
34
47
|
|
|
35
|
-
This is a breaking API change in the `0.x` line. Docs and examples now use the
|
|
36
|
-
new permission-string format consistently.
|
|
48
|
+
This is a breaking API change in the `0.x` line. Docs and examples now use the new permission-string format consistently.
|
|
37
49
|
|
|
38
50
|
## 0.2.2
|
|
39
51
|
|
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -14,8 +14,6 @@ A type-safe, declarative authorization library for TypeScript with [Standard Sch
|
|
|
14
14
|
## Installation
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
pnpm add @zap-studio/permit
|
|
18
|
-
# or
|
|
19
17
|
npm install @zap-studio/permit
|
|
20
18
|
```
|
|
21
19
|
|
|
@@ -24,7 +22,7 @@ npm install @zap-studio/permit
|
|
|
24
22
|
```ts
|
|
25
23
|
import { z } from "zod";
|
|
26
24
|
import { createPolicy, allow, deny, when } from "@zap-studio/permit";
|
|
27
|
-
import type { Resources, Actions } from "@zap-studio/permit
|
|
25
|
+
import type { Resources, Actions } from "@zap-studio/permit";
|
|
28
26
|
|
|
29
27
|
// 1. Define your resource schemas
|
|
30
28
|
const resources = {
|
|
@@ -126,7 +124,7 @@ Returns a condition that is true only if all conditions are true.
|
|
|
126
124
|
```ts
|
|
127
125
|
const isOwnerAndPublished = and(
|
|
128
126
|
(ctx, action, resource) => ctx.user.id === resource.authorId,
|
|
129
|
-
(ctx, action, resource) => resource.status === "published"
|
|
127
|
+
(ctx, action, resource) => resource.status === "published"
|
|
130
128
|
);
|
|
131
129
|
```
|
|
132
130
|
|
|
@@ -137,7 +135,7 @@ Returns a condition that is true if any condition is true.
|
|
|
137
135
|
```ts
|
|
138
136
|
const isOwnerOrAdmin = or(
|
|
139
137
|
(ctx, action, resource) => ctx.user.id === resource.authorId,
|
|
140
|
-
(ctx, action, resource) => ctx.user.role === "admin"
|
|
138
|
+
(ctx, action, resource) => ctx.user.role === "admin"
|
|
141
139
|
);
|
|
142
140
|
```
|
|
143
141
|
|
|
@@ -146,7 +144,9 @@ const isOwnerOrAdmin = or(
|
|
|
146
144
|
Returns a condition that negates another condition.
|
|
147
145
|
|
|
148
146
|
```ts
|
|
149
|
-
const isNotOwner = not(
|
|
147
|
+
const isNotOwner = not(
|
|
148
|
+
(ctx, action, resource) => ctx.user.id === resource.authorId
|
|
149
|
+
);
|
|
150
150
|
```
|
|
151
151
|
|
|
152
152
|
### Context Helpers
|
|
@@ -266,3 +266,15 @@ const resources = {
|
|
|
266
266
|
post: type({ id: "string" }),
|
|
267
267
|
} satisfies Resources;
|
|
268
268
|
```
|
|
269
|
+
|
|
270
|
+
## Runtime Support
|
|
271
|
+
|
|
272
|
+
| Runtime | Minimum version |
|
|
273
|
+
| ------------------ | ------------------------------------------------ |
|
|
274
|
+
| Node.js | 18.0.0 |
|
|
275
|
+
| Bun | 1.0.0 |
|
|
276
|
+
| Deno | 1.42 |
|
|
277
|
+
| Cloudflare Workers | Any current release |
|
|
278
|
+
| Browsers | Latest evergreen (Chrome, Edge, Firefox, Safari) |
|
|
279
|
+
|
|
280
|
+
The package ships standard ESM only and uses no runtime-specific APIs. Deno 1.42 is the first release that can install packages from JSR (`deno add jsr:@zap-studio/permit`).
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { ConditionFn, Context, PolicyFn, Role, RoleHierarchy } from "./types.js";
|
|
2
|
+
//#region src/conditions.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Returns a policy function that always allows the action.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* const policy = createPolicy({
|
|
9
|
+
* resources,
|
|
10
|
+
* actions,
|
|
11
|
+
* rules: {
|
|
12
|
+
* post: {
|
|
13
|
+
* read: allow(), // Always allow reading posts
|
|
14
|
+
* },
|
|
15
|
+
* },
|
|
16
|
+
* });
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
declare const allow: <TContext extends Context, TAction extends string = string, TResource = unknown>() => PolicyFn<TContext, TAction, TResource>;
|
|
20
|
+
/**
|
|
21
|
+
* Returns a policy function that always denies the action.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* const policy = createPolicy({
|
|
26
|
+
* resources,
|
|
27
|
+
* actions,
|
|
28
|
+
* rules: {
|
|
29
|
+
* post: {
|
|
30
|
+
* delete: deny(), // Never allow deleting posts
|
|
31
|
+
* },
|
|
32
|
+
* },
|
|
33
|
+
* });
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
declare const deny: <TContext extends Context, TAction extends string = string, TResource = unknown>() => PolicyFn<TContext, TAction, TResource>;
|
|
37
|
+
/**
|
|
38
|
+
* Returns a policy function that allows or denies based on a condition.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* const policy = createPolicy({
|
|
43
|
+
* resources,
|
|
44
|
+
* actions,
|
|
45
|
+
* rules: {
|
|
46
|
+
* post: {
|
|
47
|
+
* write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
|
|
48
|
+
* },
|
|
49
|
+
* },
|
|
50
|
+
* });
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
declare const when: <TContext extends Context, TAction extends string = string, TResource = unknown>(condition: ConditionFn<TContext, TAction, TResource>) => PolicyFn<TContext, TAction, TResource>;
|
|
54
|
+
/**
|
|
55
|
+
* Returns a condition function that returns `true` if all conditions are met.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* const isOwnerAndPublished = and(
|
|
60
|
+
* (ctx, action, resource) => ctx.user.id === resource.authorId,
|
|
61
|
+
* (ctx, action, resource) => resource.status === "published"
|
|
62
|
+
* );
|
|
63
|
+
*
|
|
64
|
+
* rules: {
|
|
65
|
+
* post: {
|
|
66
|
+
* delete: when(isOwnerAndPublished),
|
|
67
|
+
* },
|
|
68
|
+
* }
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
declare const and: <TContext extends Context, TAction extends string = string, TResource = unknown>(...conditions: ConditionFn<TContext, TAction, TResource>[]) => ConditionFn<TContext, TAction, TResource>;
|
|
72
|
+
/**
|
|
73
|
+
* Returns a condition function that returns `true` if any condition is met.
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```ts
|
|
77
|
+
* const isOwnerOrAdmin = or(
|
|
78
|
+
* (ctx, action, resource) => ctx.user.id === resource.authorId,
|
|
79
|
+
* (ctx, action, resource) => ctx.user.role === "admin"
|
|
80
|
+
* );
|
|
81
|
+
*
|
|
82
|
+
* rules: {
|
|
83
|
+
* post: {
|
|
84
|
+
* write: when(isOwnerOrAdmin),
|
|
85
|
+
* },
|
|
86
|
+
* }
|
|
87
|
+
* ```
|
|
88
|
+
*/
|
|
89
|
+
declare const or: <TContext extends Context, TAction extends string = string, TResource = unknown>(...conditions: ConditionFn<TContext, TAction, TResource>[]) => ConditionFn<TContext, TAction, TResource>;
|
|
90
|
+
/**
|
|
91
|
+
* Returns a condition function that negates another condition.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```ts
|
|
95
|
+
* const isNotOwner = not((ctx, action, resource) => ctx.user.id === resource.authorId);
|
|
96
|
+
*
|
|
97
|
+
* rules: {
|
|
98
|
+
* post: {
|
|
99
|
+
* like: when(isNotOwner), // Can only like posts you don't own
|
|
100
|
+
* },
|
|
101
|
+
* }
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
declare const not: <TContext extends Context, TAction extends string = string, TResource = unknown>(condition: ConditionFn<TContext, TAction, TResource>) => ConditionFn<TContext, TAction, TResource>;
|
|
105
|
+
/**
|
|
106
|
+
* Returns a condition function that checks if a context property equals a value.
|
|
107
|
+
*
|
|
108
|
+
* @example
|
|
109
|
+
* ```ts
|
|
110
|
+
* rules: {
|
|
111
|
+
* post: {
|
|
112
|
+
* write: when(has("role", "admin")), // Only admins can write
|
|
113
|
+
* },
|
|
114
|
+
* }
|
|
115
|
+
* ```
|
|
116
|
+
*/
|
|
117
|
+
declare const has: <TContext extends Context, K extends keyof TContext>(key: K, value: TContext[K]) => ConditionFn<TContext>;
|
|
118
|
+
/**
|
|
119
|
+
* Collects all roles including inherited ones from a role hierarchy.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts
|
|
123
|
+
* type Role = "guest" | "user" | "admin";
|
|
124
|
+
*
|
|
125
|
+
* const hierarchy: RoleHierarchy<Role> = {
|
|
126
|
+
* guest: [],
|
|
127
|
+
* user: ["guest"],
|
|
128
|
+
* admin: ["user"],
|
|
129
|
+
* };
|
|
130
|
+
*
|
|
131
|
+
* collectInheritedRoles(["admin"], hierarchy);
|
|
132
|
+
* // Returns: Set { "admin", "user", "guest" }
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
declare const collectInheritedRoles: <TRole extends Role = Role>(roles: TRole[], hierarchy: RoleHierarchy<TRole>) => Set<TRole>;
|
|
136
|
+
/**
|
|
137
|
+
* Call signatures for {@link hasRole}, preserving the with/without hierarchy overloads.
|
|
138
|
+
*/
|
|
139
|
+
interface HasRoleFn {
|
|
140
|
+
<TContext extends {
|
|
141
|
+
role: Role | Role[];
|
|
142
|
+
}, TAction extends string = string, TResource = unknown>(role: Role): ConditionFn<TContext, TAction, TResource>;
|
|
143
|
+
<TContext extends {
|
|
144
|
+
role: TRole | TRole[];
|
|
145
|
+
}, TAction extends string = string, TResource = unknown, TRole extends Role = Role>(role: TRole, hierarchy: RoleHierarchy<TRole>): ConditionFn<TContext, TAction, TResource>;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Returns a condition function that checks if the user has a specific role.
|
|
149
|
+
* Supports role hierarchy for inherited permissions.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```ts
|
|
153
|
+
* // Without hierarchy
|
|
154
|
+
* rules: {
|
|
155
|
+
* post: {
|
|
156
|
+
* delete: when(hasRole("admin")),
|
|
157
|
+
* },
|
|
158
|
+
* }
|
|
159
|
+
*
|
|
160
|
+
* // With hierarchy
|
|
161
|
+
* const hierarchy = {
|
|
162
|
+
* guest: [],
|
|
163
|
+
* user: ["guest"],
|
|
164
|
+
* admin: ["user"],
|
|
165
|
+
* };
|
|
166
|
+
*
|
|
167
|
+
* rules: {
|
|
168
|
+
* post: {
|
|
169
|
+
* read: when(hasRole("guest", hierarchy)), // Admins and users can also read
|
|
170
|
+
* },
|
|
171
|
+
* }
|
|
172
|
+
* ```
|
|
173
|
+
*/
|
|
174
|
+
declare const hasRole: HasRoleFn;
|
|
175
|
+
//#endregion
|
|
176
|
+
export { allow, and, collectInheritedRoles, deny, has, hasRole, not, or, when };
|
|
177
|
+
//# sourceMappingURL=conditions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conditions.d.ts","names":[],"sources":["../src/conditions.ts"],"mappings":";;;;;;;;;;;;;;;;;;cA+Ba,QAET,iBAAiB,SACjB,iCACA,0BACG,SAAS,UAAU,SAAS;;;;;;;;;;;;;;;;;cAoBtB,OAET,iBAAiB,SACjB,iCACA,0BACG,SAAS,UAAU,SAAS;;;;;;;;;;;;;;;;;cAoBtB,OAET,iBAAiB,SACjB,iCACA,qBAEA,WAAW,YAAY,UAAU,SAAS,eACzC,SAAS,UAAU,SAAS;;;;;;;;;;;;;;;;;;cAqBpB,MAET,iBAAiB,SACjB,iCACA,wBAEG,YAAY,YAAY,UAAU,SAAS,iBAC7C,YAAY,UAAU,SAAS;;;;;;;;;;;;;;;;;;cAqBvB,KAET,iBAAiB,SACjB,iCACA,wBAEG,YAAY,YAAY,UAAU,SAAS,iBAC7C,YAAY,UAAU,SAAS;;;;;;;;;;;;;;;cAkBvB,MAET,iBAAiB,SACjB,iCACA,qBAEA,WAAW,YAAY,UAAU,SAAS,eACzC,YAAY,UAAU,SAAS;;;;;;;;;;;;;cAgBvB,MACV,iBAAiB,SAAS,gBAAgB,UACzC,KAAK,GACL,OAAO,SAAS,OACf,YAAY;;;;;;;;;;;;;;;;;;cAqBJ,wBAAyB,cAAc,OAAO,MACzD,OAAO,SACP,WAAW,cAAc,WACxB,IAAI;;;;UAwBG;GAEN;IAAmB,MAAM,OAAO;KAChC,iCACA,qBAEA,MAAM,OACL,YAAY,UAAU,SAAS;GAEhC;IAAmB,MAAM,QAAQ;KACjC,iCACA,qBACA,cAAc,OAAO,MAErB,MAAM,OACN,WAAW,cAAc,SACxB,YAAY,UAAU,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA8BvB,SAAS"}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
//#region src/conditions.ts
|
|
2
|
+
/**
|
|
3
|
+
* Returns a policy function that always allows the action.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* ```ts
|
|
7
|
+
* const policy = createPolicy({
|
|
8
|
+
* resources,
|
|
9
|
+
* actions,
|
|
10
|
+
* rules: {
|
|
11
|
+
* post: {
|
|
12
|
+
* read: allow(), // Always allow reading posts
|
|
13
|
+
* },
|
|
14
|
+
* },
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
const allow = () => () => "allow";
|
|
19
|
+
/**
|
|
20
|
+
* Returns a policy function that always denies the action.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* const policy = createPolicy({
|
|
25
|
+
* resources,
|
|
26
|
+
* actions,
|
|
27
|
+
* rules: {
|
|
28
|
+
* post: {
|
|
29
|
+
* delete: deny(), // Never allow deleting posts
|
|
30
|
+
* },
|
|
31
|
+
* },
|
|
32
|
+
* });
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
const deny = () => () => "deny";
|
|
36
|
+
/**
|
|
37
|
+
* Returns a policy function that allows or denies based on a condition.
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```ts
|
|
41
|
+
* const policy = createPolicy({
|
|
42
|
+
* resources,
|
|
43
|
+
* actions,
|
|
44
|
+
* rules: {
|
|
45
|
+
* post: {
|
|
46
|
+
* write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
|
|
47
|
+
* },
|
|
48
|
+
* },
|
|
49
|
+
* });
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
const when = (condition) => (context, action, resource) => condition(context, action, resource) ? "allow" : "deny";
|
|
53
|
+
/**
|
|
54
|
+
* Returns a condition function that returns `true` if all conditions are met.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* const isOwnerAndPublished = and(
|
|
59
|
+
* (ctx, action, resource) => ctx.user.id === resource.authorId,
|
|
60
|
+
* (ctx, action, resource) => resource.status === "published"
|
|
61
|
+
* );
|
|
62
|
+
*
|
|
63
|
+
* rules: {
|
|
64
|
+
* post: {
|
|
65
|
+
* delete: when(isOwnerAndPublished),
|
|
66
|
+
* },
|
|
67
|
+
* }
|
|
68
|
+
* ```
|
|
69
|
+
*/
|
|
70
|
+
const and = (...conditions) => (context, action, resource) => conditions.every((condition) => condition(context, action, resource));
|
|
71
|
+
/**
|
|
72
|
+
* Returns a condition function that returns `true` if any condition is met.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* const isOwnerOrAdmin = or(
|
|
77
|
+
* (ctx, action, resource) => ctx.user.id === resource.authorId,
|
|
78
|
+
* (ctx, action, resource) => ctx.user.role === "admin"
|
|
79
|
+
* );
|
|
80
|
+
*
|
|
81
|
+
* rules: {
|
|
82
|
+
* post: {
|
|
83
|
+
* write: when(isOwnerOrAdmin),
|
|
84
|
+
* },
|
|
85
|
+
* }
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
const or = (...conditions) => (context, action, resource) => conditions.some((condition) => condition(context, action, resource));
|
|
89
|
+
/**
|
|
90
|
+
* Returns a condition function that negates another condition.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* const isNotOwner = not((ctx, action, resource) => ctx.user.id === resource.authorId);
|
|
95
|
+
*
|
|
96
|
+
* rules: {
|
|
97
|
+
* post: {
|
|
98
|
+
* like: when(isNotOwner), // Can only like posts you don't own
|
|
99
|
+
* },
|
|
100
|
+
* }
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
const not = (condition) => (context, action, resource) => !condition(context, action, resource);
|
|
104
|
+
/**
|
|
105
|
+
* Returns a condition function that checks if a context property equals a value.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```ts
|
|
109
|
+
* rules: {
|
|
110
|
+
* post: {
|
|
111
|
+
* write: when(has("role", "admin")), // Only admins can write
|
|
112
|
+
* },
|
|
113
|
+
* }
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
const has = (key, value) => (context) => context[key] === value;
|
|
117
|
+
/**
|
|
118
|
+
* Collects all roles including inherited ones from a role hierarchy.
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* ```ts
|
|
122
|
+
* type Role = "guest" | "user" | "admin";
|
|
123
|
+
*
|
|
124
|
+
* const hierarchy: RoleHierarchy<Role> = {
|
|
125
|
+
* guest: [],
|
|
126
|
+
* user: ["guest"],
|
|
127
|
+
* admin: ["user"],
|
|
128
|
+
* };
|
|
129
|
+
*
|
|
130
|
+
* collectInheritedRoles(["admin"], hierarchy);
|
|
131
|
+
* // Returns: Set { "admin", "user", "guest" }
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
const collectInheritedRoles = (roles, hierarchy) => {
|
|
135
|
+
const inherited = /* @__PURE__ */ new Set();
|
|
136
|
+
const add = (role) => {
|
|
137
|
+
if (inherited.has(role)) return;
|
|
138
|
+
inherited.add(role);
|
|
139
|
+
const parents = hierarchy[role] ?? [];
|
|
140
|
+
for (const parent of parents) add(parent);
|
|
141
|
+
};
|
|
142
|
+
for (const role of roles) add(role);
|
|
143
|
+
return inherited;
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* Returns a condition function that checks if the user has a specific role.
|
|
147
|
+
* Supports role hierarchy for inherited permissions.
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```ts
|
|
151
|
+
* // Without hierarchy
|
|
152
|
+
* rules: {
|
|
153
|
+
* post: {
|
|
154
|
+
* delete: when(hasRole("admin")),
|
|
155
|
+
* },
|
|
156
|
+
* }
|
|
157
|
+
*
|
|
158
|
+
* // With hierarchy
|
|
159
|
+
* const hierarchy = {
|
|
160
|
+
* guest: [],
|
|
161
|
+
* user: ["guest"],
|
|
162
|
+
* admin: ["user"],
|
|
163
|
+
* };
|
|
164
|
+
*
|
|
165
|
+
* rules: {
|
|
166
|
+
* post: {
|
|
167
|
+
* read: when(hasRole("guest", hierarchy)), // Admins and users can also read
|
|
168
|
+
* },
|
|
169
|
+
* }
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
const hasRole = (role, hierarchy) => (context) => {
|
|
173
|
+
const userRoles = Array.isArray(context.role) ? context.role : [context.role];
|
|
174
|
+
if (hierarchy === void 0) return userRoles.includes(role);
|
|
175
|
+
return collectInheritedRoles(userRoles, hierarchy).has(role);
|
|
176
|
+
};
|
|
177
|
+
//#endregion
|
|
178
|
+
export { allow, and, collectInheritedRoles, deny, has, hasRole, not, or, when };
|
|
179
|
+
|
|
180
|
+
//# sourceMappingURL=conditions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"conditions.js","names":[],"sources":["../src/conditions.ts"],"sourcesContent":["/**\n * Policy and condition combinators: `allow`, `deny`, `when`, boolean\n * composition, and role helpers.\n *\n * @module @zap-studio/permit/conditions\n */\n\nimport type {\n ConditionFn,\n Context,\n PolicyFn,\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 const allow =\n <\n TContext extends Context,\n TAction extends string = string,\n TResource = unknown,\n >(): PolicyFn<TContext, TAction, TResource> =>\n () =>\n \"allow\";\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 const deny =\n <\n TContext extends Context,\n TAction extends string = string,\n TResource = unknown,\n >(): PolicyFn<TContext, TAction, TResource> =>\n () =>\n \"deny\";\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 const when =\n <\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 (context, action, resource) =>\n condition(context, action, resource) ? \"allow\" : \"deny\";\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 const and =\n <\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 (context, action, resource) =>\n conditions.every((condition) => condition(context, action, resource));\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 const or =\n <\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 (context, action, resource) =>\n conditions.some((condition) => condition(context, action, resource));\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 const not =\n <\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 (context, action, resource) =>\n !condition(context, action, resource);\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 const has =\n <TContext extends Context, K extends keyof TContext>(\n key: K,\n value: TContext[K]\n ): ConditionFn<TContext> =>\n (context) =>\n context[key] === value;\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 const collectInheritedRoles = <TRole extends Role = Role>(\n roles: TRole[],\n hierarchy: RoleHierarchy<TRole>\n): Set<TRole> => {\n const inherited = new Set<TRole>();\n\n const add = (role: TRole): void => {\n if (inherited.has(role)) {\n return;\n }\n\n inherited.add(role);\n const parents = hierarchy[role] ?? [];\n for (const parent of parents) {\n add(parent);\n }\n };\n\n for (const role of roles) {\n add(role);\n }\n return inherited;\n};\n\n/**\n * Call signatures for {@link hasRole}, preserving the with/without hierarchy overloads.\n */\ninterface HasRoleFn {\n <\n TContext extends { role: Role | Role[] },\n TAction extends string = string,\n TResource = unknown,\n >(\n role: Role\n ): ConditionFn<TContext, TAction, TResource>;\n <\n TContext extends { 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}\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 const hasRole: HasRoleFn =\n (\n role: Role,\n hierarchy?: RoleHierarchy\n ): ConditionFn<{ role: Role | Role[] }> =>\n (context) => {\n const userRoles = Array.isArray(context.role)\n ? context.role\n : [context.role];\n\n if (hierarchy === undefined) {\n return userRoles.includes(role);\n }\n\n const inherited = collectInheritedRoles(userRoles, hierarchy);\n return inherited.has(role);\n };\n"],"mappings":";;;;;;;;;;;;;;;;;AA+BA,MAAa,oBAOT;;;;;;;;;;;;;;;;;AAkBJ,MAAa,mBAOT;;;;;;;;;;;;;;;;;AAkBJ,MAAa,QAMT,eAED,SAAS,QAAQ,aAChB,UAAU,SAAS,QAAQ,QAAQ,IAAI,UAAU;;;;;;;;;;;;;;;;;;AAmBrD,MAAa,OAMT,GAAG,gBAEJ,SAAS,QAAQ,aAChB,WAAW,OAAO,cAAc,UAAU,SAAS,QAAQ,QAAQ,CAAC;;;;;;;;;;;;;;;;;;AAmBxE,MAAa,MAMT,GAAG,gBAEJ,SAAS,QAAQ,aAChB,WAAW,MAAM,cAAc,UAAU,SAAS,QAAQ,QAAQ,CAAC;;;;;;;;;;;;;;;AAgBvE,MAAa,OAMT,eAED,SAAS,QAAQ,aAChB,CAAC,UAAU,SAAS,QAAQ,QAAQ;;;;;;;;;;;;;AAcxC,MAAa,OAET,KACA,WAED,YACC,QAAQ,SAAS;;;;;;;;;;;;;;;;;;AAmBrB,MAAa,yBACX,OACA,cACe;CACf,MAAM,4BAAY,IAAI,IAAW;CAEjC,MAAM,OAAO,SAAsB;EACjC,IAAI,UAAU,IAAI,IAAI,GACpB;EAGF,UAAU,IAAI,IAAI;EAClB,MAAM,UAAU,UAAU,SAAS,CAAC;EACpC,KAAK,MAAM,UAAU,SACnB,IAAI,MAAM;CAEd;CAEA,KAAK,MAAM,QAAQ,OACjB,IAAI,IAAI;CAEV,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDA,MAAa,WAET,MACA,eAED,YAAY;CACX,MAAM,YAAY,MAAM,QAAQ,QAAQ,IAAI,IACxC,QAAQ,OACR,CAAC,QAAQ,IAAI;CAEjB,IAAI,cAAc,KAAA,GAChB,OAAO,UAAU,SAAS,IAAI;CAIhC,OADkB,sBAAsB,WAAW,SACpC,CAAC,CAAC,IAAI,IAAI;AAC3B"}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//#region src/errors.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Error primitives for policy evaluation and configuration failures.
|
|
4
|
+
*
|
|
5
|
+
* @module @zap-studio/permit/errors
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Represents an error that occurs during policy evaluation or enforcement.
|
|
9
|
+
* Use this error to indicate issues related to policy logic, configuration, or execution.
|
|
10
|
+
*/
|
|
11
|
+
declare class PolicyError extends Error {
|
|
12
|
+
/**
|
|
13
|
+
* Creates a policy error with a human-readable message.
|
|
14
|
+
*
|
|
15
|
+
* @param message - Error message describing the policy failure.
|
|
16
|
+
*/
|
|
17
|
+
constructor(message: string);
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { PolicyError };
|
|
21
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","names":[],"sources":["../src/errors.ts"],"mappings":";;;;;;;;;;cAUa,oBAAoB;;;;;;EAM/B,YAAY"}
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
//#region src/errors.ts
|
|
2
2
|
/**
|
|
3
|
+
* Error primitives for policy evaluation and configuration failures.
|
|
4
|
+
*
|
|
5
|
+
* @module @zap-studio/permit/errors
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
3
8
|
* Represents an error that occurs during policy evaluation or enforcement.
|
|
4
9
|
* Use this error to indicate issues related to policy logic, configuration, or execution.
|
|
5
10
|
*/
|
|
6
11
|
var PolicyError = class extends Error {
|
|
12
|
+
/**
|
|
13
|
+
* Creates a policy error with a human-readable message.
|
|
14
|
+
*
|
|
15
|
+
* @param message - Error message describing the policy failure.
|
|
16
|
+
*/
|
|
7
17
|
constructor(message) {
|
|
8
18
|
super(message);
|
|
9
19
|
this.name = "PolicyError";
|
|
@@ -12,4 +22,4 @@ var PolicyError = class extends Error {
|
|
|
12
22
|
//#endregion
|
|
13
23
|
export { PolicyError };
|
|
14
24
|
|
|
15
|
-
//# sourceMappingURL=errors.
|
|
25
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * Error primitives for policy evaluation and configuration failures.\n *\n * @module @zap-studio/permit/errors\n */\n\n/**\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 /**\n * Creates a policy error with a human-readable message.\n *\n * @param message - Error message describing the policy failure.\n */\n constructor(message: string) {\n super(message);\n this.name = \"PolicyError\";\n }\n}\n"],"mappings":";;;;;;;;;;AAUA,IAAa,cAAb,cAAiC,MAAM;;;;;;CAMrC,YAAY,SAAiB;EAC3B,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/helpers.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Helper utilities for permit consumers.
|
|
4
|
+
*
|
|
5
|
+
* @module @zap-studio/permit/helpers
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Ensures that a value of type `never` is actually never encountered at runtime.
|
|
9
|
+
* This is useful for exhaustive checks on discriminated unions.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* type Action = 'read' | 'write'
|
|
14
|
+
*
|
|
15
|
+
* function performAction(action: Action) {
|
|
16
|
+
* switch (action) {
|
|
17
|
+
* case 'read':
|
|
18
|
+
* console.log('Reading...')
|
|
19
|
+
* break
|
|
20
|
+
* case 'write':
|
|
21
|
+
* console.log('Writing...')
|
|
22
|
+
* break
|
|
23
|
+
* default:
|
|
24
|
+
* assertNever(action) // TypeScript will error if a new Action is added but not handled
|
|
25
|
+
* }
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
declare const assertNever: (value: never) => never;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { assertNever };
|
|
32
|
+
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","names":[],"sources":["../src/helpers.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;cA4Ba,cAAe"}
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
//#region src/helpers.ts
|
|
2
2
|
/**
|
|
3
|
+
* Helper utilities for permit consumers.
|
|
4
|
+
*
|
|
5
|
+
* @module @zap-studio/permit/helpers
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
3
8
|
* Ensures that a value of type `never` is actually never encountered at runtime.
|
|
4
9
|
* This is useful for exhaustive checks on discriminated unions.
|
|
5
10
|
*
|
|
@@ -21,10 +26,10 @@
|
|
|
21
26
|
* }
|
|
22
27
|
* ```
|
|
23
28
|
*/
|
|
24
|
-
|
|
29
|
+
const assertNever = (value) => {
|
|
25
30
|
throw new Error(`Unexpected value: ${String(value)}`);
|
|
26
|
-
}
|
|
31
|
+
};
|
|
27
32
|
//#endregion
|
|
28
33
|
export { assertNever };
|
|
29
34
|
|
|
30
|
-
//# sourceMappingURL=helpers.
|
|
35
|
+
//# sourceMappingURL=helpers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.js","names":[],"sources":["../src/helpers.ts"],"sourcesContent":["/**\n * Helper utilities for permit consumers.\n *\n * @module @zap-studio/permit/helpers\n */\n\n/**\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 const assertNever = (value: never): never => {\n throw new Error(`Unexpected value: ${String(value)}`);\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,MAAa,eAAe,UAAwB;CAClD,MAAM,IAAI,MAAM,qBAAqB,OAAO,KAAK,GAAG;AACtD"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { ActionPolicyMap, Actions, ConditionFn, Context, Decision, InferAction, InferPermission, InferResource, PermitConfig, Policy, PolicyFn, Resources, Role, RoleHierarchy, Rules } from "./types.js";
|
|
2
|
+
import { allow, and, collectInheritedRoles, deny, has, hasRole, not, or, when } from "./conditions.js";
|
|
3
|
+
import { PolicyError } from "./errors.js";
|
|
4
|
+
import { assertNever } from "./helpers.js";
|
|
5
|
+
import { createPolicy, mergePolicies, mergePoliciesAny } from "./policy.js";
|
|
6
|
+
export { type ActionPolicyMap, type Actions, type ConditionFn, type Context, type Decision, type InferAction, type InferPermission, type InferResource, type PermitConfig, type Policy, PolicyError, type PolicyFn, type Resources, type Role, type RoleHierarchy, type Rules, allow, and, assertNever, collectInheritedRoles, createPolicy, deny, has, hasRole, mergePolicies, mergePoliciesAny, not, or, when };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { allow, and, collectInheritedRoles, deny, has, hasRole, not, or, when } from "./conditions.js";
|
|
2
|
+
import { PolicyError } from "./errors.js";
|
|
3
|
+
import { assertNever } from "./helpers.js";
|
|
4
|
+
import { createPolicy, mergePolicies, mergePoliciesAny } from "./policy.js";
|
|
5
|
+
export { PolicyError, allow, and, assertNever, collectInheritedRoles, createPolicy, deny, has, hasRole, mergePolicies, mergePoliciesAny, not, or, when };
|