@zap-studio/permit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/LICENSE.md +21 -0
- package/README.md +268 -0
- package/dist/errors.d.mts +11 -0
- package/dist/errors.d.mts.map +1 -0
- package/dist/errors.mjs +15 -0
- package/dist/errors.mjs.map +1 -0
- package/dist/helpers.d.mts +27 -0
- package/dist/helpers.d.mts.map +1 -0
- package/dist/helpers.mjs +30 -0
- package/dist/helpers.mjs.map +1 -0
- package/dist/index.d.mts +257 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +269 -0
- package/dist/index.mjs.map +1 -0
- package/dist/types-Bd4zClp0.d.mts +134 -0
- package/dist/types-Bd4zClp0.d.mts.map +1 -0
- package/dist/types.d.mts +2 -0
- package/dist/types.mjs +1 -0
- package/package.json +65 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# @zap-studio/permit
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 0627885: Initial release of @zap-studio/permit - a type-safe, declarative authorization library for TypeScript.
|
|
8
|
+
|
|
9
|
+
Features:
|
|
10
|
+
|
|
11
|
+
- Declarative policy creation with `createPolicy()`
|
|
12
|
+
- Policy rules: `allow()`, `deny()`, and `when()` for conditional access
|
|
13
|
+
- Condition combinators: `and()`, `or()`, `not()`, and `has()`
|
|
14
|
+
- Role-based access control with `hasRole()` and role hierarchies
|
|
15
|
+
- Policy merging with `mergePolicies()` (deny-overrides) and `mergePoliciesAny()` (allow-overrides)
|
|
16
|
+
- Standard Schema support (Zod, Valibot, ArkType, etc.)
|
|
17
|
+
- Full TypeScript support with type inference
|
|
18
|
+
- `PolicyError` class for authorization errors
|
|
19
|
+
- `assertNever()` helper for exhaustive type checking
|
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Alexandre Trotel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# @zap-studio/permit
|
|
2
|
+
|
|
3
|
+
A type-safe, declarative authorization library for TypeScript with [Standard Schema](https://standardschema.dev/) support.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Full type safety with TypeScript
|
|
8
|
+
- Standard Schema support (Zod, Valibot, ArkType, etc.)
|
|
9
|
+
- Declarative policy definitions
|
|
10
|
+
- Role hierarchy support
|
|
11
|
+
- Composable conditions (`and`, `or`, `not`)
|
|
12
|
+
- Policy merging strategies
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm add @zap-studio/permit
|
|
18
|
+
# or
|
|
19
|
+
npm install @zap-studio/permit
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { z } from "zod";
|
|
26
|
+
import { createPolicy, allow, deny, when } from "@zap-studio/permit";
|
|
27
|
+
import type { Resources, Actions } from "@zap-studio/permit/types";
|
|
28
|
+
|
|
29
|
+
// 1. Define your resource schemas
|
|
30
|
+
const resources = {
|
|
31
|
+
post: z.object({
|
|
32
|
+
id: z.string(),
|
|
33
|
+
authorId: z.string(),
|
|
34
|
+
visibility: z.enum(["public", "private"]),
|
|
35
|
+
}),
|
|
36
|
+
comment: z.object({
|
|
37
|
+
id: z.string(),
|
|
38
|
+
postId: z.string(),
|
|
39
|
+
authorId: z.string(),
|
|
40
|
+
}),
|
|
41
|
+
} satisfies Resources;
|
|
42
|
+
|
|
43
|
+
// 2. Define actions per resource
|
|
44
|
+
const actions = {
|
|
45
|
+
post: ["read", "write", "delete"],
|
|
46
|
+
comment: ["read", "write"],
|
|
47
|
+
} as const satisfies Actions<typeof resources>;
|
|
48
|
+
|
|
49
|
+
// 3. Define your context type
|
|
50
|
+
type AppContext = {
|
|
51
|
+
user: { id: string; role: "guest" | "user" | "admin" };
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// 4. Create your policy
|
|
55
|
+
const policy = createPolicy<AppContext>({
|
|
56
|
+
resources,
|
|
57
|
+
actions,
|
|
58
|
+
rules: {
|
|
59
|
+
post: {
|
|
60
|
+
read: when((ctx, action, resource) => resource.visibility === "public"),
|
|
61
|
+
write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
|
|
62
|
+
delete: deny(),
|
|
63
|
+
},
|
|
64
|
+
comment: {
|
|
65
|
+
read: allow(),
|
|
66
|
+
write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// 5. Check permissions
|
|
72
|
+
const ctx: AppContext = { user: { id: "user-1", role: "user" } };
|
|
73
|
+
const post = { id: "1", authorId: "user-1", visibility: "public" as const };
|
|
74
|
+
|
|
75
|
+
policy.can(ctx, "read", "post", post); // true
|
|
76
|
+
policy.can(ctx, "write", "post", post); // true (user is author)
|
|
77
|
+
policy.can(ctx, "delete", "post", post); // false (always denied)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## API Reference
|
|
81
|
+
|
|
82
|
+
### Policy Builders
|
|
83
|
+
|
|
84
|
+
#### `allow()`
|
|
85
|
+
|
|
86
|
+
Returns a policy function that always allows the action.
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
rules: {
|
|
90
|
+
post: {
|
|
91
|
+
read: allow(), // Anyone can read
|
|
92
|
+
},
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### `deny()`
|
|
97
|
+
|
|
98
|
+
Returns a policy function that always denies the action.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
rules: {
|
|
102
|
+
post: {
|
|
103
|
+
delete: deny(), // No one can delete
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
#### `when(condition)`
|
|
109
|
+
|
|
110
|
+
Returns a policy function that allows or denies based on a condition.
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
rules: {
|
|
114
|
+
post: {
|
|
115
|
+
write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
|
|
116
|
+
},
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Condition Combinators
|
|
121
|
+
|
|
122
|
+
#### `and(...conditions)`
|
|
123
|
+
|
|
124
|
+
Returns a condition that is true only if all conditions are true.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
const isOwnerAndPublished = and(
|
|
128
|
+
(ctx, action, resource) => ctx.user.id === resource.authorId,
|
|
129
|
+
(ctx, action, resource) => resource.status === "published"
|
|
130
|
+
);
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
#### `or(...conditions)`
|
|
134
|
+
|
|
135
|
+
Returns a condition that is true if any condition is true.
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
const isOwnerOrAdmin = or(
|
|
139
|
+
(ctx, action, resource) => ctx.user.id === resource.authorId,
|
|
140
|
+
(ctx, action, resource) => ctx.user.role === "admin"
|
|
141
|
+
);
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
#### `not(condition)`
|
|
145
|
+
|
|
146
|
+
Returns a condition that negates another condition.
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
const isNotOwner = not((ctx, action, resource) => ctx.user.id === resource.authorId);
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Context Helpers
|
|
153
|
+
|
|
154
|
+
#### `has(key, value)`
|
|
155
|
+
|
|
156
|
+
Checks if a context property equals a specific value.
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
rules: {
|
|
160
|
+
post: {
|
|
161
|
+
write: when(has("role", "admin")),
|
|
162
|
+
},
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
#### `hasRole(role, hierarchy?)`
|
|
167
|
+
|
|
168
|
+
Checks if the user has a specific role, with optional hierarchy support.
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
const hierarchy = {
|
|
172
|
+
guest: [],
|
|
173
|
+
user: ["guest"],
|
|
174
|
+
admin: ["user"],
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
rules: {
|
|
178
|
+
post: {
|
|
179
|
+
read: when(hasRole("guest", hierarchy)), // Admins and users inherit guest permissions
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Policy Merging
|
|
185
|
+
|
|
186
|
+
#### `mergePolicies(...policies)`
|
|
187
|
+
|
|
188
|
+
Merges policies with "deny-overrides" strategy. All policies must allow for the action to be permitted.
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
const merged = mergePolicies(basePolicy, restrictivePolicy);
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
#### `mergePoliciesAny(...policies)`
|
|
195
|
+
|
|
196
|
+
Merges policies with "allow-overrides" strategy. Any policy allowing is sufficient.
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
const merged = mergePoliciesAny(guestPolicy, memberPolicy);
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Type Helpers
|
|
203
|
+
|
|
204
|
+
### `Resources`
|
|
205
|
+
|
|
206
|
+
Type helper for defining resource schemas with `satisfies`.
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
const resources = {
|
|
210
|
+
post: z.object({ id: z.string() }),
|
|
211
|
+
} satisfies Resources;
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
### `Actions<TResources>`
|
|
215
|
+
|
|
216
|
+
Type helper for defining actions with `satisfies`. Ensures action keys match resource keys.
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
const actions = {
|
|
220
|
+
post: ["read", "write"],
|
|
221
|
+
} as const satisfies Actions<typeof resources>;
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### `InferResource<TResources, K>`
|
|
225
|
+
|
|
226
|
+
Infers the TypeScript type for a specific resource.
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
type Post = InferResource<typeof resources, "post">;
|
|
230
|
+
// { id: string }
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### `InferAction<TActions, K>`
|
|
234
|
+
|
|
235
|
+
Infers the action union type for a specific resource.
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
type PostAction = InferAction<typeof actions, "post">;
|
|
239
|
+
// "read" | "write"
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
## Standard Schema Support
|
|
243
|
+
|
|
244
|
+
This library uses [Standard Schema](https://standardschema.dev/) for resource validation, which means it works with any compatible schema library:
|
|
245
|
+
|
|
246
|
+
- [Zod](https://zod.dev/)
|
|
247
|
+
- [Valibot](https://valibot.dev/)
|
|
248
|
+
- [ArkType](https://arktype.io/)
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
// With Zod
|
|
252
|
+
import { z } from "zod";
|
|
253
|
+
const resources = {
|
|
254
|
+
post: z.object({ id: z.string() }),
|
|
255
|
+
} satisfies Resources;
|
|
256
|
+
|
|
257
|
+
// With Valibot
|
|
258
|
+
import * as v from "valibot";
|
|
259
|
+
const resources = {
|
|
260
|
+
post: v.object({ id: v.string() }),
|
|
261
|
+
} satisfies Resources;
|
|
262
|
+
|
|
263
|
+
// With ArkType
|
|
264
|
+
import { type } from "arktype";
|
|
265
|
+
const resources = {
|
|
266
|
+
post: type({ id: "string" }),
|
|
267
|
+
} satisfies Resources;
|
|
268
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.mts","names":[],"sources":["../src/errors.ts"],"sourcesContent":[],"mappings":";;AAIA;;;cAAa,WAAA,SAAoB,KAAA"}
|
package/dist/errors.mjs
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
//#region src/errors.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
|
+
var PolicyError = class extends Error {
|
|
7
|
+
constructor(message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = "PolicyError";
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
//#endregion
|
|
14
|
+
export { PolicyError };
|
|
15
|
+
//# sourceMappingURL=errors.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.d.mts","names":[],"sources":["../src/helpers.ts"],"sourcesContent":[],"mappings":";;AAsBA;;;;;;;;;;;;;;;;;;;;;iBAAgB,WAAA"}
|
package/dist/helpers.mjs
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
//#region src/helpers.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
|
+
function assertNever(value) {
|
|
25
|
+
throw new Error(`Unexpected value: ${value}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
//#endregion
|
|
29
|
+
export { assertNever };
|
|
30
|
+
//# sourceMappingURL=helpers.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"helpers.mjs","names":[],"sources":["../src/helpers.ts"],"sourcesContent":["/**\n * Ensures that a value of type `never` is actually never encountered at runtime.\n * This is useful for exhaustive checks on discriminated unions.\n *\n * @example\n * ```ts\n * type Action = 'read' | 'write'\n *\n * function performAction(action: Action) {\n * switch (action) {\n * case 'read':\n * console.log('Reading...')\n * break\n * case 'write':\n * console.log('Writing...')\n * break\n * default:\n * assertNever(action) // TypeScript will error if a new Action is added but not handled\n * }\n * }\n * ```\n */\nexport function assertNever(value: never): never {\n throw new Error(`Unexpected value: ${value}`);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAsBA,SAAgB,YAAY,OAAqB;AAC/C,OAAM,IAAI,MAAM,qBAAqB,QAAQ"}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { c as PermitConfig, d as Resources, f as Role, i as Context, l as Policy, n as Actions, p as RoleHierarchy, r as ConditionFn, u as PolicyFn } from "./types-Bd4zClp0.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/index.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Returns a policy function that always allows the action.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* const policy = createPolicy({
|
|
11
|
+
* resources,
|
|
12
|
+
* actions,
|
|
13
|
+
* rules: {
|
|
14
|
+
* post: {
|
|
15
|
+
* read: allow(), // Always allow reading posts
|
|
16
|
+
* },
|
|
17
|
+
* },
|
|
18
|
+
* });
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
declare function allow<TContext extends Context, TAction extends string = string, TResource = unknown>(): PolicyFn<TContext, TAction, TResource>;
|
|
22
|
+
/**
|
|
23
|
+
* Returns a policy function that always denies the action.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* const policy = createPolicy({
|
|
28
|
+
* resources,
|
|
29
|
+
* actions,
|
|
30
|
+
* rules: {
|
|
31
|
+
* post: {
|
|
32
|
+
* delete: deny(), // Never allow deleting posts
|
|
33
|
+
* },
|
|
34
|
+
* },
|
|
35
|
+
* });
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
declare function deny<TContext extends Context, TAction extends string = string, TResource = unknown>(): PolicyFn<TContext, TAction, TResource>;
|
|
39
|
+
/**
|
|
40
|
+
* Returns a policy function that allows or denies based on a condition.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```ts
|
|
44
|
+
* const policy = createPolicy({
|
|
45
|
+
* resources,
|
|
46
|
+
* actions,
|
|
47
|
+
* rules: {
|
|
48
|
+
* post: {
|
|
49
|
+
* write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
|
|
50
|
+
* },
|
|
51
|
+
* },
|
|
52
|
+
* });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
declare function when<TContext extends Context, TAction extends string = string, TResource = unknown>(condition: ConditionFn<TContext, TAction, TResource>): PolicyFn<TContext, TAction, TResource>;
|
|
56
|
+
/**
|
|
57
|
+
* Returns a condition function that returns `true` if all conditions are met.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* const isOwnerAndPublished = and(
|
|
62
|
+
* (ctx, action, resource) => ctx.user.id === resource.authorId,
|
|
63
|
+
* (ctx, action, resource) => resource.status === "published"
|
|
64
|
+
* );
|
|
65
|
+
*
|
|
66
|
+
* rules: {
|
|
67
|
+
* post: {
|
|
68
|
+
* delete: when(isOwnerAndPublished),
|
|
69
|
+
* },
|
|
70
|
+
* }
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
declare function and<TContext extends Context, TAction extends string = string, TResource = unknown>(...conditions: ConditionFn<TContext, TAction, TResource>[]): ConditionFn<TContext, TAction, TResource>;
|
|
74
|
+
/**
|
|
75
|
+
* Returns a condition function that returns `true` if any condition is met.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```ts
|
|
79
|
+
* const isOwnerOrAdmin = or(
|
|
80
|
+
* (ctx, action, resource) => ctx.user.id === resource.authorId,
|
|
81
|
+
* (ctx, action, resource) => ctx.user.role === "admin"
|
|
82
|
+
* );
|
|
83
|
+
*
|
|
84
|
+
* rules: {
|
|
85
|
+
* post: {
|
|
86
|
+
* write: when(isOwnerOrAdmin),
|
|
87
|
+
* },
|
|
88
|
+
* }
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
declare function or<TContext extends Context, TAction extends string = string, TResource = unknown>(...conditions: ConditionFn<TContext, TAction, TResource>[]): ConditionFn<TContext, TAction, TResource>;
|
|
92
|
+
/**
|
|
93
|
+
* Returns a condition function that negates another condition.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```ts
|
|
97
|
+
* const isNotOwner = not((ctx, action, resource) => ctx.user.id === resource.authorId);
|
|
98
|
+
*
|
|
99
|
+
* rules: {
|
|
100
|
+
* post: {
|
|
101
|
+
* like: when(isNotOwner), // Can only like posts you don't own
|
|
102
|
+
* },
|
|
103
|
+
* }
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
declare function not<TContext extends Context, TAction extends string = string, TResource = unknown>(condition: ConditionFn<TContext, TAction, TResource>): ConditionFn<TContext, TAction, TResource>;
|
|
107
|
+
/**
|
|
108
|
+
* Returns a condition function that checks if a context property equals a value.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```ts
|
|
112
|
+
* rules: {
|
|
113
|
+
* post: {
|
|
114
|
+
* write: when(has("role", "admin")), // Only admins can write
|
|
115
|
+
* },
|
|
116
|
+
* }
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
declare function has<TContext extends Context, K extends keyof TContext>(key: K, value: TContext[K]): ConditionFn<TContext>;
|
|
120
|
+
/**
|
|
121
|
+
* Collects all roles including inherited ones from a role hierarchy.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```ts
|
|
125
|
+
* type Role = "guest" | "user" | "admin";
|
|
126
|
+
*
|
|
127
|
+
* const hierarchy: RoleHierarchy<Role> = {
|
|
128
|
+
* guest: [],
|
|
129
|
+
* user: ["guest"],
|
|
130
|
+
* admin: ["user"],
|
|
131
|
+
* };
|
|
132
|
+
*
|
|
133
|
+
* collectInheritedRoles(["admin"], hierarchy);
|
|
134
|
+
* // Returns: Set { "admin", "user", "guest" }
|
|
135
|
+
* ```
|
|
136
|
+
*/
|
|
137
|
+
declare function collectInheritedRoles<TRole extends Role = Role>(roles: TRole[], hierarchy: RoleHierarchy<TRole>): Set<TRole>;
|
|
138
|
+
/**
|
|
139
|
+
* Returns a condition function that checks if the user has a specific role.
|
|
140
|
+
* Supports role hierarchy for inherited permissions.
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```ts
|
|
144
|
+
* // Without hierarchy
|
|
145
|
+
* rules: {
|
|
146
|
+
* post: {
|
|
147
|
+
* delete: when(hasRole("admin")),
|
|
148
|
+
* },
|
|
149
|
+
* }
|
|
150
|
+
*
|
|
151
|
+
* // With hierarchy
|
|
152
|
+
* const hierarchy = {
|
|
153
|
+
* guest: [],
|
|
154
|
+
* user: ["guest"],
|
|
155
|
+
* admin: ["user"],
|
|
156
|
+
* };
|
|
157
|
+
*
|
|
158
|
+
* rules: {
|
|
159
|
+
* post: {
|
|
160
|
+
* read: when(hasRole("guest", hierarchy)), // Admins and users can also read
|
|
161
|
+
* },
|
|
162
|
+
* }
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
declare function hasRole<TContext extends Context & {
|
|
166
|
+
role: Role | Role[];
|
|
167
|
+
}, TAction extends string = string, TResource = unknown>(role: Role): ConditionFn<TContext, TAction, TResource>;
|
|
168
|
+
declare function hasRole<TContext extends Context & {
|
|
169
|
+
role: TRole | TRole[];
|
|
170
|
+
}, TAction extends string = string, TResource = unknown, TRole extends Role = Role>(role: TRole, hierarchy: RoleHierarchy<TRole>): ConditionFn<TContext, TAction, TResource>;
|
|
171
|
+
/**
|
|
172
|
+
* Creates a type-safe policy from resource schemas, actions, and rules.
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* ```ts
|
|
176
|
+
* import { z } from "zod";
|
|
177
|
+
* import { createPolicy, allow, deny, when } from "@zap-studio/permit";
|
|
178
|
+
* import type { Resources, Actions } from "@zap-studio/permit/types";
|
|
179
|
+
*
|
|
180
|
+
* // Define resource schemas
|
|
181
|
+
* const resources = {
|
|
182
|
+
* post: z.object({
|
|
183
|
+
* id: z.string(),
|
|
184
|
+
* authorId: z.string(),
|
|
185
|
+
* visibility: z.enum(["public", "private"]),
|
|
186
|
+
* }),
|
|
187
|
+
* comment: z.object({
|
|
188
|
+
* id: z.string(),
|
|
189
|
+
* postId: z.string(),
|
|
190
|
+
* authorId: z.string(),
|
|
191
|
+
* }),
|
|
192
|
+
* } satisfies Resources;
|
|
193
|
+
*
|
|
194
|
+
* // Define actions per resource
|
|
195
|
+
* const actions = {
|
|
196
|
+
* post: ["read", "write", "delete"],
|
|
197
|
+
* comment: ["read", "write"],
|
|
198
|
+
* } as const satisfies Actions<typeof resources>;
|
|
199
|
+
*
|
|
200
|
+
* // Define context type
|
|
201
|
+
* type AppContext = { user: { id: string; role: string } };
|
|
202
|
+
*
|
|
203
|
+
* // Create the policy
|
|
204
|
+
* const policy = createPolicy<AppContext>({
|
|
205
|
+
* resources,
|
|
206
|
+
* actions,
|
|
207
|
+
* rules: {
|
|
208
|
+
* post: {
|
|
209
|
+
* read: when((ctx, action, resource) => resource.visibility === "public"),
|
|
210
|
+
* write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
|
|
211
|
+
* delete: deny(),
|
|
212
|
+
* },
|
|
213
|
+
* comment: {
|
|
214
|
+
* read: allow(),
|
|
215
|
+
* write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
|
|
216
|
+
* },
|
|
217
|
+
* },
|
|
218
|
+
* });
|
|
219
|
+
*
|
|
220
|
+
* // Check permissions
|
|
221
|
+
* const post = { id: "1", authorId: "user-1", visibility: "public" as const };
|
|
222
|
+
* policy.can(ctx, "read", "post", post); // true
|
|
223
|
+
* policy.can(ctx, "write", "post", post); // depends on ctx.user.id
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
declare function createPolicy<TContext extends Context, TResources extends Resources = Resources, TActions extends Actions<TResources> = Actions<TResources>>(config: PermitConfig<TContext, TResources, TActions>): Policy<TContext, TResources, TActions>;
|
|
227
|
+
/**
|
|
228
|
+
* Merges multiple policies into one using "deny-overrides" strategy.
|
|
229
|
+
* If any policy denies, the merged policy denies. All must allow for the result to allow.
|
|
230
|
+
*
|
|
231
|
+
* @example
|
|
232
|
+
* ```ts
|
|
233
|
+
* const basePolicy = createPolicy({ ... });
|
|
234
|
+
* const adminPolicy = createPolicy({ ... });
|
|
235
|
+
*
|
|
236
|
+
* const merged = mergePolicies(basePolicy, adminPolicy);
|
|
237
|
+
* // Both policies must allow for the action to be permitted
|
|
238
|
+
* ```
|
|
239
|
+
*/
|
|
240
|
+
declare function mergePolicies<TContext extends Context, TResources extends Resources = Resources, TActions extends Actions<TResources> = Actions<TResources>>(...policies: Policy<TContext, TResources, TActions>[]): Policy<TContext, TResources, TActions>;
|
|
241
|
+
/**
|
|
242
|
+
* Merges multiple policies into one using "allow-overrides" strategy.
|
|
243
|
+
* If any policy allows, the merged policy allows. All must deny for the result to deny.
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* ```ts
|
|
247
|
+
* const guestPolicy = createPolicy({ ... });
|
|
248
|
+
* const memberPolicy = createPolicy({ ... });
|
|
249
|
+
*
|
|
250
|
+
* const merged = mergePoliciesAny(guestPolicy, memberPolicy);
|
|
251
|
+
* // If either policy allows, the action is permitted
|
|
252
|
+
* ```
|
|
253
|
+
*/
|
|
254
|
+
declare function mergePoliciesAny<TContext extends Context, TResources extends Resources = Resources, TActions extends Actions<TResources> = Actions<TResources>>(...policies: Policy<TContext, TResources, TActions>[]): Policy<TContext, TResources, TActions>;
|
|
255
|
+
//#endregion
|
|
256
|
+
export { allow, and, collectInheritedRoles, createPolicy, deny, has, hasRole, mergePolicies, mergePoliciesAny, not, or, when };
|
|
257
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"sourcesContent":[],"mappings":";;;;;;AA8BA;;;;;;;AAwBA;;;;;;;AAwBgB,iBAhDA,KAgDA,CACG,iBAhDA,OAgDA,EAIM,gBAAA,MAAA,GAAA,MAAA,EAAU,YAAA,OAAA,CAAS,CAAA,CAAA,EAjDvC,QAiDuC,CAjD9B,QAiD8B,EAjDpB,OAiDoB,EAjDX,SAiDW,CAAA;;;;;;;AAuB5C;;;;;;;;;;AAMG,iBA1Da,IA0Db,CAsBH,iBA/EmB,OA+EH,EACG,gBAAA,MAAA,GAAA,MAAA,EAIU,YAAA,OAAA,CAAU,CAAA,CAAA,EAjFlC,QAiFkC,CAjFzB,QAiFyB,EAjFf,OAiFe,EAjFN,SAiFM,CAAA;;;;;;;;AAoBvC;;;;;;;;;AAMG,iBAvFa,IAuFb,CAAA,iBAtFgB,OAsFhB,EAgBH,gBAAgB,MAAA,GAAA,MAAA,EAAqB,YAAA,OAAA,CAAyB,CAAA,SAAA,EAlGjD,WAkGiD,CAlGrC,QAkGqC,EAlG3B,OAkG2B,EAlGlB,SAkGkB,CAAA,CAAA,EAjG3D,QAiG2D,CAjGlD,QAiGkD,EAjGxC,OAiGwC,EAjG/B,SAiG+B,CAAA;;;;;;;AAwB9D;;;;;;;;;AA6CA;;AACqC,iBAjJrB,GAiJqB,CAAO,iBAhJzB,OAgJyB,EAGpC,gBAAA,MAAA,GAAA,MAAA,EAAmB,YAAA,OAAA,CAAU,CAAA,GAAA,UAAA,EA/IpB,WA+IoB,CA/IR,QA+IQ,EA/IE,OA+IF,EA/IW,SA+IX,CAAA,EAAA,CAAA,EA9IlC,WA8IkC,CA9ItB,QA8IsB,EA9IZ,OA8IY,EA9IH,SA8IG,CAAA;;;;AAErC;;;;;;;;;;;;;;AAuFgB,iBAjNA,EAiNA,CACG,iBAjNA,OAiNA,EACE,gBAAA,MAAA,GAAA,MAAA,EAAY,YAAA,OAAA,CACN,CAAA,GAAA,UAAA,EA/MV,WA+MU,CA/ME,QA+MF,EA/MY,OA+MZ,EA/MqB,SA+MrB,CAAA,EAAA,CAAA,EA9MxB,WA8MwB,CA9MZ,QA8MY,EA9MF,OA8ME,EA9MO,SA8MP,CAAA;;;;;;;;;;;;;AAsC3B;;AAEqB,iBAnOL,GAmOK,CAAY,iBAlOd,OAkOc,EACN,gBAAA,MAAA,GAAA,MAAA,EAAR,YAAA,OAAA,CAA8B,CAAA,SAAA,EA/NpC,WA+NoC,CA/NxB,QA+NwB,EA/Nd,OA+Nc,EA/NL,SA+NK,CAAA,CAAA,EA9N9C,WA8N8C,CA9NlC,QA8NkC,EA9NxB,OA8NwB,EA9Nf,SA8Ne,CAAA;;;;;;;;;;;AAkCjD;;AAEqB,iBAlPL,GAkPK,CAAY,iBAlPI,OAkPJ,EACN,UAAA,MAnPmC,QAmPnC,CAAR,CAAA,GAAA,EAlPZ,CAkPY,EAAA,KAAA,EAjPV,QAiPU,CAjPD,CAiPC,CAAA,CAAA,EAhPhB,WAgPgB,CAhPJ,QAgPI,CAAA;;;;;;;;;;;;;;;;;;iBA3NH,oCAAoC,OAAO,aAClD,oBACI,cAAc,SACxB,IAAI;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA0CS,yBACG;QAAkB,OAAO;+DAGpC,OAAO,YAAY,UAAU,SAAS;iBAE9B,yBACG;QAAkB,QAAQ;uEAG7B,OAAO,YAEf,kBACK,cAAc,SACxB,YAAY,UAAU,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA+ElB,8BACG,4BACE,YAAY,4BACd,QAAQ,cAAc,QAAQ,qBAEvC,aAAa,UAAU,YAAY,YAC1C,OAAO,UAAU,YAAY;;;;;;;;;;;;;;iBAmChB,+BACG,4BACE,YAAY,4BACd,QAAQ,cAAc,QAAQ,0BAElC,OAAO,UAAU,YAAY,cACzC,OAAO,UAAU,YAAY;;;;;;;;;;;;;;iBA+BhB,kCACG,4BACE,YAAY,4BACd,QAAQ,cAAc,QAAQ,0BAElC,OAAO,UAAU,YAAY,cACzC,OAAO,UAAU,YAAY"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
//#region src/index.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
|
+
function allow() {
|
|
19
|
+
return () => "allow";
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Returns a policy function that always denies the action.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* const policy = createPolicy({
|
|
27
|
+
* resources,
|
|
28
|
+
* actions,
|
|
29
|
+
* rules: {
|
|
30
|
+
* post: {
|
|
31
|
+
* delete: deny(), // Never allow deleting posts
|
|
32
|
+
* },
|
|
33
|
+
* },
|
|
34
|
+
* });
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
function deny() {
|
|
38
|
+
return () => "deny";
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Returns a policy function that allows or denies based on a condition.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* const policy = createPolicy({
|
|
46
|
+
* resources,
|
|
47
|
+
* actions,
|
|
48
|
+
* rules: {
|
|
49
|
+
* post: {
|
|
50
|
+
* write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
|
|
51
|
+
* },
|
|
52
|
+
* },
|
|
53
|
+
* });
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
function when(condition) {
|
|
57
|
+
return (context, action, resource) => condition(context, action, resource) ? "allow" : "deny";
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Returns a condition function that returns `true` if all conditions are met.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```ts
|
|
64
|
+
* const isOwnerAndPublished = and(
|
|
65
|
+
* (ctx, action, resource) => ctx.user.id === resource.authorId,
|
|
66
|
+
* (ctx, action, resource) => resource.status === "published"
|
|
67
|
+
* );
|
|
68
|
+
*
|
|
69
|
+
* rules: {
|
|
70
|
+
* post: {
|
|
71
|
+
* delete: when(isOwnerAndPublished),
|
|
72
|
+
* },
|
|
73
|
+
* }
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
function and(...conditions) {
|
|
77
|
+
return (context, action, resource) => conditions.every((condition) => condition(context, action, resource));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Returns a condition function that returns `true` if any condition is met.
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```ts
|
|
84
|
+
* const isOwnerOrAdmin = or(
|
|
85
|
+
* (ctx, action, resource) => ctx.user.id === resource.authorId,
|
|
86
|
+
* (ctx, action, resource) => ctx.user.role === "admin"
|
|
87
|
+
* );
|
|
88
|
+
*
|
|
89
|
+
* rules: {
|
|
90
|
+
* post: {
|
|
91
|
+
* write: when(isOwnerOrAdmin),
|
|
92
|
+
* },
|
|
93
|
+
* }
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
function or(...conditions) {
|
|
97
|
+
return (context, action, resource) => conditions.some((condition) => condition(context, action, resource));
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Returns a condition function that negates another condition.
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```ts
|
|
104
|
+
* const isNotOwner = not((ctx, action, resource) => ctx.user.id === resource.authorId);
|
|
105
|
+
*
|
|
106
|
+
* rules: {
|
|
107
|
+
* post: {
|
|
108
|
+
* like: when(isNotOwner), // Can only like posts you don't own
|
|
109
|
+
* },
|
|
110
|
+
* }
|
|
111
|
+
* ```
|
|
112
|
+
*/
|
|
113
|
+
function not(condition) {
|
|
114
|
+
return (context, action, resource) => !condition(context, action, resource);
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Returns a condition function that checks if a context property equals a value.
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```ts
|
|
121
|
+
* rules: {
|
|
122
|
+
* post: {
|
|
123
|
+
* write: when(has("role", "admin")), // Only admins can write
|
|
124
|
+
* },
|
|
125
|
+
* }
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
function has(key, value) {
|
|
129
|
+
return (context) => context[key] === value;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Collects all roles including inherited ones from a role hierarchy.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* type Role = "guest" | "user" | "admin";
|
|
137
|
+
*
|
|
138
|
+
* const hierarchy: RoleHierarchy<Role> = {
|
|
139
|
+
* guest: [],
|
|
140
|
+
* user: ["guest"],
|
|
141
|
+
* admin: ["user"],
|
|
142
|
+
* };
|
|
143
|
+
*
|
|
144
|
+
* collectInheritedRoles(["admin"], hierarchy);
|
|
145
|
+
* // Returns: Set { "admin", "user", "guest" }
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
function collectInheritedRoles(roles, hierarchy) {
|
|
149
|
+
const inherited = /* @__PURE__ */ new Set();
|
|
150
|
+
function add(role) {
|
|
151
|
+
if (!inherited.has(role)) {
|
|
152
|
+
inherited.add(role);
|
|
153
|
+
(hierarchy[role] ?? []).forEach(add);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
roles.forEach(add);
|
|
157
|
+
return inherited;
|
|
158
|
+
}
|
|
159
|
+
function hasRole(role, hierarchy) {
|
|
160
|
+
return (context) => {
|
|
161
|
+
const userRoles = Array.isArray(context.role) ? context.role : [context.role];
|
|
162
|
+
if (!hierarchy) return userRoles.includes(role);
|
|
163
|
+
return collectInheritedRoles(userRoles, hierarchy).has(role);
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Creates a type-safe policy from resource schemas, actions, and rules.
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* ```ts
|
|
171
|
+
* import { z } from "zod";
|
|
172
|
+
* import { createPolicy, allow, deny, when } from "@zap-studio/permit";
|
|
173
|
+
* import type { Resources, Actions } from "@zap-studio/permit/types";
|
|
174
|
+
*
|
|
175
|
+
* // Define resource schemas
|
|
176
|
+
* const resources = {
|
|
177
|
+
* post: z.object({
|
|
178
|
+
* id: z.string(),
|
|
179
|
+
* authorId: z.string(),
|
|
180
|
+
* visibility: z.enum(["public", "private"]),
|
|
181
|
+
* }),
|
|
182
|
+
* comment: z.object({
|
|
183
|
+
* id: z.string(),
|
|
184
|
+
* postId: z.string(),
|
|
185
|
+
* authorId: z.string(),
|
|
186
|
+
* }),
|
|
187
|
+
* } satisfies Resources;
|
|
188
|
+
*
|
|
189
|
+
* // Define actions per resource
|
|
190
|
+
* const actions = {
|
|
191
|
+
* post: ["read", "write", "delete"],
|
|
192
|
+
* comment: ["read", "write"],
|
|
193
|
+
* } as const satisfies Actions<typeof resources>;
|
|
194
|
+
*
|
|
195
|
+
* // Define context type
|
|
196
|
+
* type AppContext = { user: { id: string; role: string } };
|
|
197
|
+
*
|
|
198
|
+
* // Create the policy
|
|
199
|
+
* const policy = createPolicy<AppContext>({
|
|
200
|
+
* resources,
|
|
201
|
+
* actions,
|
|
202
|
+
* rules: {
|
|
203
|
+
* post: {
|
|
204
|
+
* read: when((ctx, action, resource) => resource.visibility === "public"),
|
|
205
|
+
* write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
|
|
206
|
+
* delete: deny(),
|
|
207
|
+
* },
|
|
208
|
+
* comment: {
|
|
209
|
+
* read: allow(),
|
|
210
|
+
* write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
|
|
211
|
+
* },
|
|
212
|
+
* },
|
|
213
|
+
* });
|
|
214
|
+
*
|
|
215
|
+
* // Check permissions
|
|
216
|
+
* const post = { id: "1", authorId: "user-1", visibility: "public" as const };
|
|
217
|
+
* policy.can(ctx, "read", "post", post); // true
|
|
218
|
+
* policy.can(ctx, "write", "post", post); // depends on ctx.user.id
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
221
|
+
function createPolicy(config) {
|
|
222
|
+
const { rules } = config;
|
|
223
|
+
return { can(context, action, resourceType, resource) {
|
|
224
|
+
const policyFn = rules[resourceType][action];
|
|
225
|
+
if (!policyFn) return false;
|
|
226
|
+
return policyFn(context, action, resource) === "allow";
|
|
227
|
+
} };
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Merges multiple policies into one using "deny-overrides" strategy.
|
|
231
|
+
* If any policy denies, the merged policy denies. All must allow for the result to allow.
|
|
232
|
+
*
|
|
233
|
+
* @example
|
|
234
|
+
* ```ts
|
|
235
|
+
* const basePolicy = createPolicy({ ... });
|
|
236
|
+
* const adminPolicy = createPolicy({ ... });
|
|
237
|
+
*
|
|
238
|
+
* const merged = mergePolicies(basePolicy, adminPolicy);
|
|
239
|
+
* // Both policies must allow for the action to be permitted
|
|
240
|
+
* ```
|
|
241
|
+
*/
|
|
242
|
+
function mergePolicies(...policies) {
|
|
243
|
+
return { can(context, action, resourceType, resource) {
|
|
244
|
+
for (const policy of policies) if (!policy.can(context, action, resourceType, resource)) return false;
|
|
245
|
+
return true;
|
|
246
|
+
} };
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Merges multiple policies into one using "allow-overrides" strategy.
|
|
250
|
+
* If any policy allows, the merged policy allows. All must deny for the result to deny.
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* ```ts
|
|
254
|
+
* const guestPolicy = createPolicy({ ... });
|
|
255
|
+
* const memberPolicy = createPolicy({ ... });
|
|
256
|
+
*
|
|
257
|
+
* const merged = mergePoliciesAny(guestPolicy, memberPolicy);
|
|
258
|
+
* // If either policy allows, the action is permitted
|
|
259
|
+
* ```
|
|
260
|
+
*/
|
|
261
|
+
function mergePoliciesAny(...policies) {
|
|
262
|
+
return { can(context, action, resourceType, resource) {
|
|
263
|
+
return policies.some((policy) => policy.can(context, action, resourceType, resource));
|
|
264
|
+
} };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
//#endregion
|
|
268
|
+
export { allow, and, collectInheritedRoles, createPolicy, deny, has, hasRole, mergePolicies, mergePoliciesAny, not, or, when };
|
|
269
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["import 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 } = config;\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 resourceRules = rules[resourceType];\n const policyFn = resourceRules[action];\n\n if (!policyFn) {\n return false;\n }\n\n return policyFn(context, action, resource) === \"allow\";\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 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 return policies.some((policy) =>\n policy.can(context, action, resourceType, resource)\n );\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA8BA,SAAgB,QAI4B;AAC1C,cAAa;;;;;;;;;;;;;;;;;;AAmBf,SAAgB,OAI4B;AAC1C,cAAa;;;;;;;;;;;;;;;;;;AAmBf,SAAgB,KAKd,WACwC;AACxC,SAAQ,SAAS,QAAQ,aACvB,UAAU,SAAS,QAAQ,SAAS,GAAG,UAAU;;;;;;;;;;;;;;;;;;;AAoBrD,SAAgB,IAKd,GAAG,YACwC;AAC3C,SAAQ,SAAS,QAAQ,aACvB,WAAW,OAAO,cAAc,UAAU,SAAS,QAAQ,SAAS,CAAC;;;;;;;;;;;;;;;;;;;AAoBzE,SAAgB,GAKd,GAAG,YACwC;AAC3C,SAAQ,SAAS,QAAQ,aACvB,WAAW,MAAM,cAAc,UAAU,SAAS,QAAQ,SAAS,CAAC;;;;;;;;;;;;;;;;AAiBxE,SAAgB,IAKd,WAC2C;AAC3C,SAAQ,SAAS,QAAQ,aAAa,CAAC,UAAU,SAAS,QAAQ,SAAS;;;;;;;;;;;;;;AAe7E,SAAgB,IACd,KACA,OACuB;AACvB,SAAQ,YAAY,QAAQ,SAAS;;;;;;;;;;;;;;;;;;;AAoBvC,SAAgB,sBACd,OACA,WACY;CACZ,MAAM,4BAAY,IAAI,KAAY;CAElC,SAAS,IAAI,MAAa;AACxB,MAAI,CAAC,UAAU,IAAI,KAAK,EAAE;AACxB,aAAU,IAAI,KAAK;AAEnB,IADgB,UAAU,SAAS,EAAE,EAC7B,QAAQ,IAAI;;;AAIxB,OAAM,QAAQ,IAAI;AAClB,QAAO;;AA8CT,SAAgB,QAKd,MACA,WAC2C;AAC3C,SAAQ,YAAY;EAClB,MAAM,YAAY,MAAM,QAAQ,QAAQ,KAAK,GACzC,QAAQ,OACR,CAAC,QAAQ,KAAK;AAElB,MAAI,CAAC,UACH,QAAO,UAAU,SAAS,KAAK;AAIjC,SADkB,sBAAsB,WAAW,UAAU,CAC5C,IAAI,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2D9B,SAAgB,aAKd,QACwC;CACxC,MAAM,EAAE,UAAU;AAElB,QAAO,EACL,IACE,SACA,QACA,cACA,UACS;EAET,MAAM,WADgB,MAAM,cACG;AAE/B,MAAI,CAAC,SACH,QAAO;AAGT,SAAO,SAAS,SAAS,QAAQ,SAAS,KAAK;IAElD;;;;;;;;;;;;;;;AAgBH,SAAgB,cAKd,GAAG,UACqC;AACxC,QAAO,EACL,IACE,SACA,QACA,cACA,UACS;AACT,OAAK,MAAM,UAAU,SACnB,KAAI,CAAC,OAAO,IAAI,SAAS,QAAQ,cAAc,SAAS,CACtD,QAAO;AAGX,SAAO;IAEV;;;;;;;;;;;;;;;AAgBH,SAAgB,iBAKd,GAAG,UACqC;AACxC,QAAO,EACL,IACE,SACA,QACA,cACA,UACS;AACT,SAAO,SAAS,MAAM,WACpB,OAAO,IAAI,SAAS,QAAQ,cAAc,SAAS,CACpD;IAEJ"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
|
+
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Represents the possible outcomes of a policy decision.
|
|
7
|
+
* - "allow": The action is permitted.
|
|
8
|
+
* - "deny": The action is not permitted.
|
|
9
|
+
*/
|
|
10
|
+
type Decision = "allow" | "deny";
|
|
11
|
+
/**
|
|
12
|
+
* Represents the context in which a policy decision is made.
|
|
13
|
+
* Can include user information, environment, or any relevant data.
|
|
14
|
+
*/
|
|
15
|
+
type Context<TContext = unknown> = TContext;
|
|
16
|
+
/**
|
|
17
|
+
* Represents a role within the system.
|
|
18
|
+
*/
|
|
19
|
+
type Role<TRole extends string = string> = TRole;
|
|
20
|
+
/**
|
|
21
|
+
* Represents a role hierarchy within the system.
|
|
22
|
+
* Maps each role to an array of roles it inherits from.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* type Roles = "guest" | "user" | "admin";
|
|
27
|
+
*
|
|
28
|
+
* const hierarchy: RoleHierarchy<Roles> = {
|
|
29
|
+
* guest: [],
|
|
30
|
+
* user: ["guest"],
|
|
31
|
+
* admin: ["user"],
|
|
32
|
+
* };
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
type RoleHierarchy<TRole extends Role = Role> = Record<TRole, TRole[]>;
|
|
36
|
+
/**
|
|
37
|
+
* Type helper for defining resource schemas using Standard Schema.
|
|
38
|
+
* Use with `satisfies` to ensure type safety when defining resources.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```ts
|
|
42
|
+
* import { z } from "zod";
|
|
43
|
+
* import type { Resources } from "@zap-studio/permit/types";
|
|
44
|
+
*
|
|
45
|
+
* const resources = {
|
|
46
|
+
* post: z.object({ id: z.string(), authorId: z.string() }),
|
|
47
|
+
* comment: z.object({ id: z.string(), postId: z.string() }),
|
|
48
|
+
* } satisfies Resources;
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
type Resources<TResourceKey extends string = string> = Record<TResourceKey, StandardSchemaV1>;
|
|
52
|
+
/**
|
|
53
|
+
* Type helper for defining actions per resource.
|
|
54
|
+
* Use with `satisfies` to ensure keys match the resource definitions.
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* import type { Actions } from "@zap-studio/permit/types";
|
|
59
|
+
*
|
|
60
|
+
* const actions = {
|
|
61
|
+
* post: ["read", "write", "delete"],
|
|
62
|
+
* comment: ["read", "write"],
|
|
63
|
+
* } as const satisfies Actions<typeof resources>;
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
type Actions<TResources extends Resources> = { [K in keyof TResources]: readonly string[] };
|
|
67
|
+
/**
|
|
68
|
+
* Infers the output type from a Standard Schema.
|
|
69
|
+
*/
|
|
70
|
+
type InferResource<TResources extends Resources, TResourceKey extends keyof TResources> = StandardSchemaV1.InferOutput<TResources[TResourceKey]>;
|
|
71
|
+
/**
|
|
72
|
+
* Infers the action union type for a specific resource.
|
|
73
|
+
*/
|
|
74
|
+
type InferAction<TActions extends Record<string, readonly string[]>, K$1 extends keyof TActions> = TActions[K$1][number];
|
|
75
|
+
/**
|
|
76
|
+
* A function that determines whether a given action on a resource is allowed in a specific context.
|
|
77
|
+
*/
|
|
78
|
+
type PolicyFn<TContext extends Context, TAction extends string = string, TResource = unknown> = (context: TContext, action: TAction, resource: TResource) => Decision;
|
|
79
|
+
/**
|
|
80
|
+
* A function that evaluates a condition for a given action and resource in a specific context.
|
|
81
|
+
*/
|
|
82
|
+
type ConditionFn<TContext extends Context, TAction extends string = string, TResource = unknown> = (context: TContext, action: TAction, resource: TResource) => boolean;
|
|
83
|
+
/**
|
|
84
|
+
* Maps actions to their corresponding policy functions for a specific resource.
|
|
85
|
+
*/
|
|
86
|
+
type ActionPolicyMap<TContext extends Context, TAction extends string = string, TResource = unknown> = { [A in TAction]?: PolicyFn<TContext, A, TResource> };
|
|
87
|
+
/**
|
|
88
|
+
* Defines the rules for each resource and action combination.
|
|
89
|
+
* Each resource key maps to an object where each action key maps to a policy function.
|
|
90
|
+
*/
|
|
91
|
+
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>> };
|
|
92
|
+
/**
|
|
93
|
+
* Configuration object for creating a permit policy.
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* ```ts
|
|
97
|
+
* const config: PermitConfig<MyContext> = {
|
|
98
|
+
* resources,
|
|
99
|
+
* actions,
|
|
100
|
+
* rules: {
|
|
101
|
+
* post: { read: allow(), write: deny() },
|
|
102
|
+
* },
|
|
103
|
+
* };
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
type PermitConfig<TContext extends Context, TResources extends Resources = Resources, TActions extends Actions<TResources> = Actions<TResources>> = {
|
|
107
|
+
resources: TResources;
|
|
108
|
+
actions: TActions;
|
|
109
|
+
rules: Rules<TContext, TResources, TActions>;
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Represents a policy object that can evaluate permissions.
|
|
113
|
+
* The `can` method checks if a given action is permitted on a resource in a specific context.
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* ```ts
|
|
117
|
+
* const policy: Policy<MyContext> = createPolicy({
|
|
118
|
+
* resources,
|
|
119
|
+
* actions,
|
|
120
|
+
* rules: { ... },
|
|
121
|
+
* });
|
|
122
|
+
*
|
|
123
|
+
* policy.can(ctx, "read", "post", postData); // true or false
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
type Policy<TContext extends Context, TResources extends Resources = Resources, TActions extends Actions<TResources> = Actions<TResources>> = {
|
|
127
|
+
/**
|
|
128
|
+
* Determines if the specified action is permitted on the resource in the given context.
|
|
129
|
+
*/
|
|
130
|
+
can<K$1 extends keyof TResources & keyof TActions>(context: TContext, action: InferAction<TActions, K$1>, resourceType: K$1, resource: InferResource<TResources, K$1>): boolean;
|
|
131
|
+
};
|
|
132
|
+
//#endregion
|
|
133
|
+
export { Decision as a, PermitConfig as c, Resources as d, Role as f, Context as i, Policy as l, Rules as m, Actions as n, InferAction as o, RoleHierarchy as p, ConditionFn as r, InferResource as s, ActionPolicyMap as t, PolicyFn as u };
|
|
134
|
+
//# sourceMappingURL=types-Bd4zClp0.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types-Bd4zClp0.d.mts","names":[],"sources":["../src/types.ts"],"sourcesContent":[],"mappings":";;;;;;AAOA;AAMA;AAKA;AAiBY,KA5BA,QAAA,GA4BA,OAAA,GAAA,MAAA;;;;;AAA2C,KAtB3C,OAsB2C,CAAA,WAAA,OAAA,CAAA,GAtBb,QAsBa;;AAiBvD;;AAEE,KApCU,IAoCV,CAAA,cAAA,MAAA,GAAA,MAAA,CAAA,GApCgD,KAoChD;;;AAiBF;AAOA;;;;;;;AAQA;;;;;AAGa,KAtDD,aAsDC,CAAA,cAtD2B,IAsD3B,GAtDkC,IAsDlC,CAAA,GAtD0C,MAsD1C,CAtDiD,KAsDjD,EAtDwD,KAsDxD,EAAA,CAAA;AAKb;;;;;;;AASA;;;;;;AASA;;AAKQ,KAjEI,SAiEJ,CAAA,qBAAA,MAAA,GAAA,MAAA,CAAA,GAjEsD,MAiEtD,CAhEN,YAgEM,EA/DN,gBA+DM,CAAA;;;;;;AAOR;;;;;;;;;AAKiC,KA1DrB,OA0DqB,CAAA,mBA1DM,SA0DN,CAAA,GAAA,QAAA,MAzDnB,UAyDmB,GAAA,SAAA,MAAA,EAAA,EAAA;;;;AAE7B,KArDQ,aAqDR,CACc,mBArDG,SAqDH,EAAY,qBAAA,MApDD,UAoDC,CAA1B,GAnDA,gBAAA,CAAiB,WAmDjB,CAnD6B,UAmD7B,CAnDwC,YAmDxC,CAAA,CAAA;;;AAkBJ;AACmB,KAjEP,WAiEO,CACE,iBAjEF,MAiEE,CAAA,MAAA,EAAA,SAAA,MAAA,EAAA,CAAA,EAAY,YAAA,MAhEf,QAgEe,CACN,GAhEvB,QAgEuB,CAhEd,GAgEc,CAAA,CAAA,MAAA,CAAA;;;;AAEd,KA7DD,QA6DC,CACF,iBA7DQ,OA6DR,EACI,gBAAA,MAAA,GAAA,MAAA,EAAU,YAAA,OAAA,CAAY,GAAA,CAAA,OAAA,EA3DvB,QA2DuB,EAAA,MAAA,EA3DL,OA2DK,EAAA,QAAA,EA3Dc,SA2Dd,EAAA,GA3D4B,QA2D5B;;;AAkBrC;AACmB,KAzEP,WAyEO,CACE,iBAzEF,OAyEE,EAAY,gBAAA,MAAA,GAAA,MAAA,EACN,YAAA,OAAA,CAAR,GAAA,CAAA,OAAA,EAvEL,QAuEK,EAAA,MAAA,EAvEa,OAuEb,EAAA,QAAA,EAvEgC,SAuEhC,EAAA,GAAA,OAAA;;;;AAKsB,KAvE7B,eAuE6B,CAC5B,iBAvEM,OAuEN,EACW,gBAAA,MAAA,GAAA,MAAA,EAAU,YAAA,OAAA,CAAtB,GAAA,QApEJ,OAoEI,IApEO,QAoEP,CApEgB,QAoEhB,EApE0B,CAoE1B,EApE6B,SAoE7B,CAAA,EAAA;;;;;AAEE,KA/DF,KA+DE,kBA9DK,4BACE,YAAY,4BACd,QAAQ,cAAc,QAAQ,6BAEnC,mBAAmB,WAAW,gBACxC,UACA,YAAY,UAAU,IACtB,cAAc,YAAY;;;;;;;;;;;;;;;KAkBlB,8BACO,4BACE,YAAY,4BACd,QAAQ,cAAc,QAAQ;aAEpC;WACF;SACF,MAAM,UAAU,YAAY;;;;;;;;;;;;;;;;;KAkBzB,wBACO,4BACE,YAAY,4BACd,QAAQ,cAAc,QAAQ;;;;wBAK3B,mBAAmB,mBAC5B,kBACD,YAAY,UAAU,oBAChB,eACJ,cAAc,YAAY"}
|
package/dist/types.d.mts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { a as Decision, c as PermitConfig, d as Resources, f as Role, i as Context, l as Policy, m as Rules, n as Actions, o as InferAction, p as RoleHierarchy, r as ConditionFn, s as InferResource, t as ActionPolicyMap, u as PolicyFn } from "./types-Bd4zClp0.mjs";
|
|
2
|
+
export { ActionPolicyMap, Actions, ConditionFn, Context, Decision, InferAction, InferResource, PermitConfig, Policy, PolicyFn, Resources, Role, RoleHierarchy, Rules };
|
package/dist/types.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zap-studio/permit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"private": false,
|
|
7
|
+
"homepage": "https://www.zapstudio.dev/packages/permit",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/zap-studio/monorepo.git",
|
|
11
|
+
"directory": "packages/permit"
|
|
12
|
+
},
|
|
13
|
+
"description": "A type-safe, declarative authorization library for TypeScript with Standard Schema support",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"authorization",
|
|
16
|
+
"permissions",
|
|
17
|
+
"access-control",
|
|
18
|
+
"rbac",
|
|
19
|
+
"abac",
|
|
20
|
+
"policy",
|
|
21
|
+
"typescript",
|
|
22
|
+
"standard-schema",
|
|
23
|
+
"zod",
|
|
24
|
+
"valibot",
|
|
25
|
+
"arktype"
|
|
26
|
+
],
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"CHANGELOG.md",
|
|
33
|
+
"LICENSE.md",
|
|
34
|
+
"README.md"
|
|
35
|
+
],
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@standard-schema/spec": "^1.0.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^25.0.2",
|
|
41
|
+
"@vitest/coverage-v8": "^4.0.15",
|
|
42
|
+
"tsdown": "^0.18.0",
|
|
43
|
+
"typescript": "^5.9.3",
|
|
44
|
+
"vitest": "^4.0.15",
|
|
45
|
+
"@zap-studio/tsdown-config": "0.0.0",
|
|
46
|
+
"@zap-studio/typescript-config": "0.0.0",
|
|
47
|
+
"@zap-studio/vitest-config": "0.0.0"
|
|
48
|
+
},
|
|
49
|
+
"exports": {
|
|
50
|
+
".": "./dist/index.mjs",
|
|
51
|
+
"./errors": "./dist/errors.mjs",
|
|
52
|
+
"./helpers": "./dist/helpers.mjs",
|
|
53
|
+
"./types": "./dist/types.mjs",
|
|
54
|
+
"./package.json": "./package.json"
|
|
55
|
+
},
|
|
56
|
+
"main": "./dist/index.mjs",
|
|
57
|
+
"module": "./dist/index.mjs",
|
|
58
|
+
"types": "./dist/index.d.mts",
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "tsdown --config tsdown.config.ts",
|
|
61
|
+
"check": "tsc --noEmit",
|
|
62
|
+
"test": "vitest run",
|
|
63
|
+
"test:watch": "vitest --watch"
|
|
64
|
+
}
|
|
65
|
+
}
|