@zap-studio/permit 0.3.4 → 1.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 +53 -41
- package/LICENSE +1 -1
- package/README.md +63 -176
- package/dist/conditions.d.ts +1 -12
- package/dist/conditions.d.ts.map +1 -1
- package/dist/conditions.js +2 -2
- package/dist/conditions.js.map +1 -1
- package/dist/errors.d.ts +14 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +14 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +2 -3
- package/dist/index.js +2 -3
- package/dist/policy.d.ts +11 -9
- package/dist/policy.d.ts.map +1 -1
- package/dist/policy.js +42 -23
- package/dist/policy.js.map +1 -1
- package/dist/types.d.ts +115 -5
- package/dist/types.d.ts.map +1 -1
- package/package.json +9 -9
- package/dist/helpers.d.ts +0 -32
- package/dist/helpers.d.ts.map +0 -1
- package/dist/helpers.js +0 -35
- package/dist/helpers.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,106 +1,118 @@
|
|
|
1
|
-
|
|
1
|
+
# Changelog
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
|
+
|
|
7
|
+
## [1.1.0]
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
`createPolicy(...)` gains an optional `logger?: Logger` option (from `@zap-studio/logger`). When provided, it logs allow decisions at `debug` and deny decisions at `info`. Internal-error warnings (resource validation and policy evaluation errors) route through the logger's `warn` instead of `console.warn` when a logger is provided; without one, they still print via `console.warn` as before. See [Logging](https://www.zapstudio.dev/permit/logging).
|
|
12
|
+
|
|
13
|
+
## [1.0.0]
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Clarified tree-shakeable design in the package description and README (no code change).
|
|
18
|
+
|
|
19
|
+
### Removed
|
|
20
|
+
|
|
21
|
+
`assertNever` is no longer exported — it had no internal consumers and existed only as a generic exhaustiveness-check convenience. Use your own `never`-typed helper if you relied on it.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
Neither merge strategy short-circuits anymore. All policies now run concurrently via `Promise.allSettled`, and every policy is invoked regardless of outcome, for both `mergePoliciesSome` (allow) and `mergePoliciesEvery` (deny). A rejecting policy no longer sinks the whole check — it's treated as a deny (`false`) and logged with `console.warn`, consistent with how `createPolicy` handles internal validation/evaluation errors.
|
|
26
|
+
|
|
27
|
+
## [0.3.4]
|
|
28
|
+
|
|
29
|
+
### Added
|
|
4
30
|
|
|
5
31
|
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
32
|
|
|
7
33
|
- 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
34
|
|
|
9
|
-
##
|
|
35
|
+
## [0.3.3]
|
|
10
36
|
|
|
11
|
-
###
|
|
37
|
+
### Changed
|
|
12
38
|
|
|
13
39
|
Internal formatting and lint cleanup only. No public API or behavior change.
|
|
14
40
|
|
|
15
|
-
|
|
41
|
+
## [0.3.2]
|
|
16
42
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
### Dependencies
|
|
43
|
+
### Changed
|
|
20
44
|
|
|
21
45
|
- Updated dependency `@zap-studio/validation` to `0.3.4`.
|
|
22
46
|
|
|
23
|
-
## 0.3.1
|
|
24
|
-
|
|
25
|
-
### Fixed
|
|
26
|
-
|
|
27
|
-
- d10d8c4: Removed the TanStack Intent-specific authoring helper from the package surface.
|
|
28
|
-
- d9ba7d1: Reworked the packaged permit skill content.
|
|
47
|
+
## [0.3.1]
|
|
29
48
|
|
|
30
49
|
### Changed
|
|
31
50
|
|
|
32
51
|
- 5fa58b1: Reduced policy evaluation complexity by extracting permission parsing and merge strategy helpers without changing the `policy.can(...)` API.
|
|
33
52
|
- abfda4b: Cleaned up public option typings by removing redundant `| undefined` unions from package types.
|
|
34
53
|
- 9f31f87: Switched the package build to ESNext-aligned output and updated package tooling and publish metadata.
|
|
54
|
+
- d9ba7d1: Reworked the packaged permit skill content.
|
|
55
|
+
- Updated dependency `@zap-studio/validation` to `0.3.3`.
|
|
35
56
|
|
|
36
|
-
###
|
|
57
|
+
### Removed
|
|
37
58
|
|
|
38
|
-
-
|
|
59
|
+
- d10d8c4: Removed the TanStack Intent-specific authoring helper from the package surface.
|
|
39
60
|
|
|
40
|
-
## 0.3.0
|
|
61
|
+
## [0.3.0]
|
|
41
62
|
|
|
42
|
-
###
|
|
63
|
+
### Changed
|
|
43
64
|
|
|
44
|
-
- fe60f55:
|
|
65
|
+
- fe60f55: **Breaking:** Changed `policy.can()` to use a single permission string plus the resource object.
|
|
45
66
|
|
|
46
67
|
`policy.can(ctx, "read", "post", post)` is replaced by `policy.can(ctx, "post:read", post)`.
|
|
47
68
|
|
|
48
69
|
This is a breaking API change in the `0.x` line. Docs and examples now use the new permission-string format consistently.
|
|
49
70
|
|
|
50
|
-
## 0.2.2
|
|
71
|
+
## [0.2.2]
|
|
51
72
|
|
|
52
|
-
###
|
|
73
|
+
### Changed
|
|
53
74
|
|
|
54
75
|
- e26293e: Updated dependencies.
|
|
55
76
|
- @zap-studio/validation@0.3.2
|
|
56
77
|
|
|
57
|
-
## 0.2.1
|
|
78
|
+
## [0.2.1]
|
|
58
79
|
|
|
59
|
-
###
|
|
80
|
+
### Changed
|
|
60
81
|
|
|
61
82
|
- 5ea3d3b: Updated dependencies.
|
|
62
83
|
- @zap-studio/validation@0.3.1
|
|
63
84
|
|
|
64
|
-
## 0.2.0
|
|
85
|
+
## [0.2.0]
|
|
65
86
|
|
|
66
87
|
### Changed
|
|
67
88
|
|
|
68
89
|
- f0f503e: Made policy evaluation asynchronous by default.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
- f0f503e: `policy.can(...)` now returns `Promise<boolean>`.
|
|
73
|
-
- f0f503e: `createPolicy()` now uses async-safe Standard Schema validation for resource schemas (including async resource schemas); this is not a separate `Policy` schema API.
|
|
74
|
-
- f0f503e: The `Policy` interface changed so `can()` is async, and `mergePolicies`/`mergePoliciesAny` are async accordingly.
|
|
90
|
+
- f0f503e: **Breaking:** `policy.can(...)` now returns `Promise<boolean>`.
|
|
91
|
+
- f0f503e: **Breaking:** `createPolicy()` now uses async-safe Standard Schema validation for resource schemas (including async resource schemas); this is not a separate `Policy` schema API.
|
|
92
|
+
- f0f503e: **Breaking:** The `Policy` interface changed so `can()` is async, and `mergePolicies`/`mergePoliciesAny` are async accordingly.
|
|
75
93
|
- Action required: callers must `await policy.can(...)` and handle `mergePolicies`/`mergePoliciesAny` as async operations; also account for async-safe resource schema validation in `createPolicy()`.
|
|
76
|
-
|
|
77
|
-
### Dependencies
|
|
78
|
-
|
|
79
94
|
- f75b984: Updated dependency `@zap-studio/validation` to `0.3.0`.
|
|
80
95
|
|
|
81
|
-
## 0.1.3
|
|
96
|
+
## [0.1.3]
|
|
82
97
|
|
|
83
|
-
###
|
|
98
|
+
### Changed
|
|
84
99
|
|
|
85
100
|
- e4542bb: Updated dependency `@zap-studio/validation` to `0.2.1`.
|
|
86
101
|
|
|
87
|
-
## 0.1.2
|
|
102
|
+
## [0.1.2]
|
|
88
103
|
|
|
89
104
|
### Changed
|
|
90
105
|
|
|
91
106
|
- 2de8183: Adopted shared synchronous Standard Schema validator utilities from `@zap-studio/validation` for resource schema validation in `createPolicy`.
|
|
92
|
-
|
|
93
|
-
### Dependencies
|
|
94
|
-
|
|
95
107
|
- 2de8183: Updated dependency `@zap-studio/validation` to `0.2.0`.
|
|
96
108
|
|
|
97
|
-
## 0.1.1
|
|
109
|
+
## [0.1.1]
|
|
98
110
|
|
|
99
111
|
### Fixed
|
|
100
112
|
|
|
101
113
|
- 907d903: Added runtime resource validation and fail-closed behavior, including deny-by-default when merges are invoked with no policies.
|
|
102
114
|
|
|
103
|
-
## 0.1.0
|
|
115
|
+
## [0.1.0]
|
|
104
116
|
|
|
105
117
|
### Added
|
|
106
118
|
|
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -2,14 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A type-safe, declarative authorization library for TypeScript with [Standard Schema](https://standardschema.dev/) support.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
|
5
|
+
Full documentation: [zapstudio.dev/permit](https://www.zapstudio.dev/permit)
|
|
13
6
|
|
|
14
7
|
## Installation
|
|
15
8
|
|
|
@@ -17,120 +10,90 @@ A type-safe, declarative authorization library for TypeScript with [Standard Sch
|
|
|
17
10
|
npm install @zap-studio/permit
|
|
18
11
|
```
|
|
19
12
|
|
|
13
|
+
You also need a schema library that implements [Standard Schema](https://standardschema.dev/), such as Zod, Valibot, or ArkType.
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- **Full type safety** — actions, resources, and permissions are inferred from your schemas and `satisfies` declarations.
|
|
18
|
+
- **Standard Schema support** via `Resources` — works with Zod, Valibot, ArkType, or any compatible library.
|
|
19
|
+
- **Declarative policies** through `createPolicy(...)` with `allow()`, `deny()`, and `when(condition)`.
|
|
20
|
+
- **Role hierarchy support** via `hasRole(role, hierarchy?)`, with inheritance resolved by `collectInheritedRoles`.
|
|
21
|
+
- **Composable conditions** via `and`, `or`, and `not`.
|
|
22
|
+
- **Policy merging strategies** via `mergePoliciesAnd` and `mergePoliciesOr`.
|
|
23
|
+
- **Structured errors** with `PolicyError` for invalid configuration or evaluation failures.
|
|
24
|
+
- **Optional logging** through `createPolicy({ logger })` ([`@zap-studio/logger`](https://www.npmjs.com/package/@zap-studio/logger)) — omit it and there's zero added logging overhead.
|
|
25
|
+
- **Tree-shakeable** — policies and conditions are plain functions; unused exports are dropped by any modern bundler.
|
|
26
|
+
|
|
20
27
|
## Quick Start
|
|
21
28
|
|
|
22
29
|
```ts
|
|
23
30
|
import { z } from "zod";
|
|
31
|
+
import { ConsoleLogger } from "@zap-studio/logger";
|
|
24
32
|
import { createPolicy, allow, deny, when } from "@zap-studio/permit";
|
|
25
33
|
import type { Resources, Actions } from "@zap-studio/permit";
|
|
26
34
|
|
|
27
|
-
// 1. Define your resource schemas
|
|
28
35
|
const resources = {
|
|
29
|
-
post: z.object({
|
|
30
|
-
id: z.string(),
|
|
31
|
-
authorId: z.string(),
|
|
32
|
-
visibility: z.enum(["public", "private"]),
|
|
33
|
-
}),
|
|
34
|
-
comment: z.object({
|
|
35
|
-
id: z.string(),
|
|
36
|
-
postId: z.string(),
|
|
37
|
-
authorId: z.string(),
|
|
38
|
-
}),
|
|
36
|
+
post: z.object({ id: z.string(), authorId: z.string() }),
|
|
39
37
|
} satisfies Resources;
|
|
40
38
|
|
|
41
|
-
// 2. Define actions per resource
|
|
42
39
|
const actions = {
|
|
43
40
|
post: ["read", "write", "delete"],
|
|
44
|
-
comment: ["read", "write"],
|
|
45
41
|
} as const satisfies Actions<typeof resources>;
|
|
46
42
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
};
|
|
43
|
+
type AppContext = { user: { id: string } };
|
|
44
|
+
|
|
45
|
+
const logger = new ConsoleLogger({ minLevel: "debug" });
|
|
51
46
|
|
|
52
|
-
// 4. Create your policy
|
|
53
47
|
const policy = createPolicy<AppContext>({
|
|
54
48
|
resources,
|
|
55
49
|
actions,
|
|
56
50
|
rules: {
|
|
57
51
|
post: {
|
|
58
|
-
read: when((ctx, action, resource) => resource.visibility === "public"),
|
|
59
|
-
write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
|
|
60
|
-
delete: deny(),
|
|
61
|
-
},
|
|
62
|
-
comment: {
|
|
63
52
|
read: allow(),
|
|
64
53
|
write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
|
|
54
|
+
delete: deny(),
|
|
65
55
|
},
|
|
66
56
|
},
|
|
57
|
+
logger,
|
|
67
58
|
});
|
|
68
59
|
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
const post = { id: "1", authorId: "user-1", visibility: "public" as const };
|
|
60
|
+
const ctx: AppContext = { user: { id: "user-1" } };
|
|
61
|
+
const post = { id: "1", authorId: "user-1" };
|
|
72
62
|
|
|
73
|
-
await policy.can(ctx, "post:
|
|
74
|
-
await policy.can(ctx, "post:write", post); // true (user is author)
|
|
75
|
-
await policy.can(ctx, "post:delete", post); // false (always denied)
|
|
63
|
+
await policy.can(ctx, "post:write", post); // true, inferred as boolean
|
|
76
64
|
```
|
|
77
65
|
|
|
78
|
-
##
|
|
79
|
-
|
|
80
|
-
### Policy Builders
|
|
66
|
+
## Declarative Policies
|
|
81
67
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
Returns a policy function that always allows the action.
|
|
68
|
+
Through `createPolicy(...)` with `allow()`, `deny()`, and `when(condition)`.
|
|
85
69
|
|
|
86
70
|
```ts
|
|
87
71
|
rules: {
|
|
88
72
|
post: {
|
|
89
|
-
read: allow(),
|
|
73
|
+
read: allow(),
|
|
74
|
+
delete: deny(),
|
|
75
|
+
write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
|
|
90
76
|
},
|
|
91
77
|
}
|
|
92
78
|
```
|
|
93
79
|
|
|
94
|
-
|
|
80
|
+
## Role Hierarchy Support
|
|
95
81
|
|
|
96
|
-
|
|
82
|
+
Via `hasRole(role, hierarchy?)`, with inheritance resolved by `collectInheritedRoles`.
|
|
97
83
|
|
|
98
84
|
```ts
|
|
99
|
-
|
|
100
|
-
post: {
|
|
101
|
-
delete: deny(), // No one can delete
|
|
102
|
-
},
|
|
103
|
-
}
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
#### `when(condition)`
|
|
107
|
-
|
|
108
|
-
Returns a policy function that allows or denies based on a condition.
|
|
85
|
+
const hierarchy = { guest: [], user: ["guest"], admin: ["user"] };
|
|
109
86
|
|
|
110
|
-
```ts
|
|
111
87
|
rules: {
|
|
112
88
|
post: {
|
|
113
|
-
|
|
89
|
+
read: when(hasRole("guest", hierarchy)), // admins and users inherit guest access
|
|
114
90
|
},
|
|
115
91
|
}
|
|
116
92
|
```
|
|
117
93
|
|
|
118
|
-
|
|
94
|
+
## Composable Conditions
|
|
119
95
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
Returns a condition that is true only if all conditions are true.
|
|
123
|
-
|
|
124
|
-
```ts
|
|
125
|
-
const isOwnerAndPublished = and(
|
|
126
|
-
(ctx, action, resource) => ctx.user.id === resource.authorId,
|
|
127
|
-
(ctx, action, resource) => resource.status === "published"
|
|
128
|
-
);
|
|
129
|
-
```
|
|
130
|
-
|
|
131
|
-
#### `or(...conditions)`
|
|
132
|
-
|
|
133
|
-
Returns a condition that is true if any condition is true.
|
|
96
|
+
Via `and`, `or`, and `not`.
|
|
134
97
|
|
|
135
98
|
```ts
|
|
136
99
|
const isOwnerOrAdmin = or(
|
|
@@ -139,134 +102,54 @@ const isOwnerOrAdmin = or(
|
|
|
139
102
|
);
|
|
140
103
|
```
|
|
141
104
|
|
|
142
|
-
|
|
105
|
+
## Policy Merging Strategies
|
|
143
106
|
|
|
144
|
-
|
|
107
|
+
Via `mergePoliciesAnd` and `mergePoliciesOr`.
|
|
145
108
|
|
|
146
109
|
```ts
|
|
147
|
-
const
|
|
148
|
-
(ctx, action, resource) => ctx.user.id === resource.authorId
|
|
149
|
-
);
|
|
110
|
+
const merged = mergePoliciesAnd(basePolicy, restrictivePolicy);
|
|
150
111
|
```
|
|
151
112
|
|
|
152
|
-
|
|
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`
|
|
113
|
+
## Standard Schema Support
|
|
205
114
|
|
|
206
|
-
|
|
115
|
+
Works with Zod, Valibot, ArkType, or any compatible library.
|
|
207
116
|
|
|
208
117
|
```ts
|
|
118
|
+
// Zod, Valibot, ArkType, or any Standard Schema-compatible library
|
|
209
119
|
const resources = {
|
|
210
120
|
post: z.object({ id: z.string() }),
|
|
211
121
|
} satisfies Resources;
|
|
212
122
|
```
|
|
213
123
|
|
|
214
|
-
|
|
124
|
+
## Structured Errors
|
|
215
125
|
|
|
216
|
-
|
|
126
|
+
`PolicyError` for invalid configuration or evaluation failures.
|
|
217
127
|
|
|
218
128
|
```ts
|
|
219
|
-
|
|
220
|
-
post: ["read", "write"],
|
|
221
|
-
} as const satisfies Actions<typeof resources>;
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
### `InferResource<TResources, K>`
|
|
129
|
+
import { PolicyError } from "@zap-studio/permit";
|
|
225
130
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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"
|
|
131
|
+
try {
|
|
132
|
+
const policy = createPolicy(config);
|
|
133
|
+
await policy.can(ctx, "post:read", post);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (error instanceof PolicyError) console.error(error.message);
|
|
136
|
+
}
|
|
240
137
|
```
|
|
241
138
|
|
|
242
|
-
##
|
|
139
|
+
## Logging
|
|
243
140
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
- [Zod](https://zod.dev/)
|
|
247
|
-
- [Valibot](https://valibot.dev/)
|
|
248
|
-
- [ArkType](https://arktype.io/)
|
|
141
|
+
Pass a `logger?: Logger` from [`@zap-studio/logger`](https://www.npmjs.com/package/@zap-studio/logger) to `createPolicy(...)` to observe allow/deny decisions. Omit it and only the pre-existing internal-error warnings still print, unchanged.
|
|
249
142
|
|
|
250
143
|
```ts
|
|
251
|
-
|
|
252
|
-
import {
|
|
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;
|
|
144
|
+
import { ConsoleLogger } from "@zap-studio/logger";
|
|
145
|
+
import { createPolicy } from "@zap-studio/permit";
|
|
262
146
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const resources = {
|
|
266
|
-
post: type({ id: "string" }),
|
|
267
|
-
} satisfies Resources;
|
|
147
|
+
const logger = new ConsoleLogger({ minLevel: "debug" });
|
|
148
|
+
const policy = createPolicy({ resources, actions, rules, logger });
|
|
268
149
|
```
|
|
269
150
|
|
|
151
|
+
Allow decisions log at `debug`, deny decisions log at `info`. Resource validation and policy evaluation errors log at `warn` through the logger when one is provided, instead of `console.warn`.
|
|
152
|
+
|
|
270
153
|
## Runtime Support
|
|
271
154
|
|
|
272
155
|
| Runtime | Minimum version |
|
|
@@ -278,3 +161,7 @@ const resources = {
|
|
|
278
161
|
| Browsers | Latest evergreen (Chrome, Edge, Firefox, Safari) |
|
|
279
162
|
|
|
280
163
|
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`).
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
MIT
|
package/dist/conditions.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConditionFn, Context, PolicyFn, Role, RoleHierarchy } from "./types.js";
|
|
1
|
+
import { ConditionFn, Context, HasRoleFn, PolicyFn, Role, RoleHierarchy } from "./types.js";
|
|
2
2
|
//#region src/conditions.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Returns a policy function that always allows the action.
|
|
@@ -133,17 +133,6 @@ declare const has: <TContext extends Context, K extends keyof TContext>(key: K,
|
|
|
133
133
|
* ```
|
|
134
134
|
*/
|
|
135
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
136
|
/**
|
|
148
137
|
* Returns a condition function that checks if the user has a specific role.
|
|
149
138
|
* Supports role hierarchy for inherited permissions.
|
package/dist/conditions.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"conditions.d.ts","names":[],"sources":["../src/conditions.ts"],"mappings":";;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"conditions.d.ts","names":[],"sources":["../src/conditions.ts"],"mappings":";;;;;;;;;;;;;;;;;;cAgCa,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;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAgDM,SAAS"}
|
package/dist/conditions.js
CHANGED
|
@@ -136,8 +136,8 @@ const collectInheritedRoles = (roles, hierarchy) => {
|
|
|
136
136
|
const add = (role) => {
|
|
137
137
|
if (inherited.has(role)) return;
|
|
138
138
|
inherited.add(role);
|
|
139
|
-
const
|
|
140
|
-
for (const
|
|
139
|
+
const baseRoles = hierarchy[role] ?? [];
|
|
140
|
+
for (const baseRole of baseRoles) add(baseRole);
|
|
141
141
|
};
|
|
142
142
|
for (const role of roles) add(role);
|
|
143
143
|
return inherited;
|
package/dist/conditions.js.map
CHANGED
|
@@ -1 +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
|
|
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 HasRoleFn,\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 baseRoles = hierarchy[role] ?? [];\n for (const baseRole of baseRoles) {\n add(baseRole);\n }\n };\n\n for (const role of roles) {\n add(role);\n }\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 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":";;;;;;;;;;;;;;;;;AAgCA,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,YAAY,UAAU,SAAS,CAAC;EACtC,KAAK,MAAM,YAAY,WACrB,IAAI,QAAQ;CAEhB;CAEA,KAAK,MAAM,QAAQ,OACjB,IAAI,IAAI;CAEV,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,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
CHANGED
|
@@ -7,6 +7,20 @@
|
|
|
7
7
|
/**
|
|
8
8
|
* Represents an error that occurs during policy evaluation or enforcement.
|
|
9
9
|
* Use this error to indicate issues related to policy logic, configuration, or execution.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { PolicyError } from "@zap-studio/permit";
|
|
14
|
+
*
|
|
15
|
+
* try {
|
|
16
|
+
* const policy = createPolicy(config);
|
|
17
|
+
* await policy.can(ctx, "post:read", post);
|
|
18
|
+
* } catch (error) {
|
|
19
|
+
* if (error instanceof PolicyError) {
|
|
20
|
+
* console.error("Invalid policy configuration:", error.message);
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
10
24
|
*/
|
|
11
25
|
declare class PolicyError extends Error {
|
|
12
26
|
/**
|
package/dist/errors.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.d.ts","names":[],"sources":["../src/errors.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"errors.d.ts","names":[],"sources":["../src/errors.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;cAwBa,oBAAoB;;;;;;EAM/B,YAAY"}
|
package/dist/errors.js
CHANGED
|
@@ -7,6 +7,20 @@
|
|
|
7
7
|
/**
|
|
8
8
|
* Represents an error that occurs during policy evaluation or enforcement.
|
|
9
9
|
* Use this error to indicate issues related to policy logic, configuration, or execution.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { PolicyError } from "@zap-studio/permit";
|
|
14
|
+
*
|
|
15
|
+
* try {
|
|
16
|
+
* const policy = createPolicy(config);
|
|
17
|
+
* await policy.can(ctx, "post:read", post);
|
|
18
|
+
* } catch (error) {
|
|
19
|
+
* if (error instanceof PolicyError) {
|
|
20
|
+
* console.error("Invalid policy configuration:", error.message);
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
10
24
|
*/
|
|
11
25
|
var PolicyError = class extends Error {
|
|
12
26
|
/**
|
package/dist/errors.js.map
CHANGED
|
@@ -1 +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":"
|
|
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 *\n * @example\n * ```ts\n * import { PolicyError } from \"@zap-studio/permit\";\n *\n * try {\n * const policy = createPolicy(config);\n * await policy.can(ctx, \"post:read\", post);\n * } catch (error) {\n * if (error instanceof PolicyError) {\n * console.error(\"Invalid policy configuration:\", error.message);\n * }\n * }\n * ```\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":";;;;;;;;;;;;;;;;;;;;;;;;AAwBA,IAAa,cAAb,cAAiC,MAAM;;;;;;CAMrC,YAAY,SAAiB;EAC3B,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { ActionPolicyMap, Actions, ConditionFn, Context, Decision, InferAction, InferPermission, InferResource, PermitConfig, Policy, PolicyFn, Resources, Role, RoleHierarchy, Rules } from "./types.js";
|
|
2
2
|
import { allow, and, collectInheritedRoles, deny, has, hasRole, not, or, when } from "./conditions.js";
|
|
3
3
|
import { PolicyError } from "./errors.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
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 };
|
|
4
|
+
import { createPolicy, mergePoliciesAnd, mergePoliciesOr } from "./policy.js";
|
|
5
|
+
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, collectInheritedRoles, createPolicy, deny, has, hasRole, mergePoliciesAnd, mergePoliciesOr, not, or, when };
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { allow, and, collectInheritedRoles, deny, has, hasRole, not, or, when } from "./conditions.js";
|
|
2
2
|
import { PolicyError } from "./errors.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
export { PolicyError, allow, and, assertNever, collectInheritedRoles, createPolicy, deny, has, hasRole, mergePolicies, mergePoliciesAny, not, or, when };
|
|
3
|
+
import { createPolicy, mergePoliciesAnd, mergePoliciesOr } from "./policy.js";
|
|
4
|
+
export { PolicyError, allow, and, collectInheritedRoles, createPolicy, deny, has, hasRole, mergePoliciesAnd, mergePoliciesOr, not, or, when };
|
package/dist/policy.d.ts
CHANGED
|
@@ -57,33 +57,35 @@ import { Actions, Context, PermitConfig, Policy, Resources } from "./types.js";
|
|
|
57
57
|
*/
|
|
58
58
|
declare const createPolicy: <TContext extends Context, TResources extends Resources = Resources, TActions extends Actions<TResources> = Actions<TResources>>(config: PermitConfig<TContext, TResources, TActions>) => Policy<TContext, TResources, TActions>;
|
|
59
59
|
/**
|
|
60
|
-
* Merges multiple policies into one
|
|
61
|
-
* If any policy denies, the merged policy denies.
|
|
60
|
+
* Merges multiple policies into one, requiring every policy to allow.
|
|
61
|
+
* If any policy denies, the merged policy denies. Policies are evaluated
|
|
62
|
+
* in parallel; every policy is invoked regardless of outcome.
|
|
62
63
|
*
|
|
63
64
|
* @example
|
|
64
65
|
* ```ts
|
|
65
66
|
* const basePolicy = createPolicy({ ... });
|
|
66
67
|
* const adminPolicy = createPolicy({ ... });
|
|
67
68
|
*
|
|
68
|
-
* const merged =
|
|
69
|
+
* const merged = mergePoliciesAnd(basePolicy, adminPolicy);
|
|
69
70
|
* // Both policies must allow for the action to be permitted
|
|
70
71
|
* ```
|
|
71
72
|
*/
|
|
72
|
-
declare const
|
|
73
|
+
declare const mergePoliciesAnd: <TContext extends Context, TResources extends Resources = Resources, TActions extends Actions<TResources> = Actions<TResources>>(...policies: Policy<TContext, TResources, TActions>[]) => Policy<TContext, TResources, TActions>;
|
|
73
74
|
/**
|
|
74
|
-
* Merges multiple policies into one
|
|
75
|
-
* If
|
|
75
|
+
* Merges multiple policies into one, requiring at least one policy to allow.
|
|
76
|
+
* If every policy denies, the merged policy denies. Policies are evaluated
|
|
77
|
+
* in parallel; every policy is invoked regardless of outcome.
|
|
76
78
|
*
|
|
77
79
|
* @example
|
|
78
80
|
* ```ts
|
|
79
81
|
* const guestPolicy = createPolicy({ ... });
|
|
80
82
|
* const memberPolicy = createPolicy({ ... });
|
|
81
83
|
*
|
|
82
|
-
* const merged =
|
|
84
|
+
* const merged = mergePoliciesOr(guestPolicy, memberPolicy);
|
|
83
85
|
* // If either policy allows, the action is permitted
|
|
84
86
|
* ```
|
|
85
87
|
*/
|
|
86
|
-
declare const
|
|
88
|
+
declare const mergePoliciesOr: <TContext extends Context, TResources extends Resources = Resources, TActions extends Actions<TResources> = Actions<TResources>>(...policies: Policy<TContext, TResources, TActions>[]) => Policy<TContext, TResources, TActions>;
|
|
87
89
|
//#endregion
|
|
88
|
-
export { createPolicy,
|
|
90
|
+
export { createPolicy, mergePoliciesAnd, mergePoliciesOr };
|
|
89
91
|
//# sourceMappingURL=policy.d.ts.map
|
package/dist/policy.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policy.d.ts","names":[],"sources":["../src/policy.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"policy.d.ts","names":[],"sources":["../src/policy.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA8Ga,eACX,iBAAiB,SACjB,mBAAmB,YAAY,WAC/B,iBAAiB,QAAQ,cAAc,QAAQ,aAE/C,QAAQ,aAAa,UAAU,YAAY,cAC1C,OAAO,UAAU,YAAY;;;;;;;;;;;;;;;cAoKnB,mBACX,iBAAiB,SACjB,mBAAmB,YAAY,WAC/B,iBAAiB,QAAQ,cAAc,QAAQ,gBAE5C,UAAU,OAAO,UAAU,YAAY,gBACzC,OAAO,UAAU,YAAY;;;;;;;;;;;;;;;cAiBnB,kBACX,iBAAiB,SACjB,mBAAmB,YAAY,WAC/B,iBAAiB,QAAQ,cAAc,QAAQ,gBAE5C,UAAU,OAAO,UAAU,YAAY,gBACzC,OAAO,UAAU,YAAY"}
|
package/dist/policy.js
CHANGED
|
@@ -3,11 +3,13 @@ import { createStandardValidator } from "@zap-studio/validation";
|
|
|
3
3
|
//#region src/policy.ts
|
|
4
4
|
/**
|
|
5
5
|
* Splits a typed `resource:action` permission string into its parts.
|
|
6
|
-
* Returns `null` when the string is malformed (missing/empty part or extra
|
|
6
|
+
* Returns `null` when the string is malformed (missing/empty part or extra
|
|
7
|
+
* segments) or `resourceType` is not one of `actions`' keys.
|
|
7
8
|
*/
|
|
8
|
-
const parsePermission = (permission) => {
|
|
9
|
+
const parsePermission = (permission, actions) => {
|
|
10
|
+
const isValidResourceKey = (value) => Object.keys(actions).includes(value);
|
|
9
11
|
const [resourceTypeValue, actionValue, ...rest] = permission.split(":");
|
|
10
|
-
if (resourceTypeValue === void 0 || resourceTypeValue.length === 0 || actionValue === void 0 || actionValue.length === 0 || rest.length > 0) return null;
|
|
12
|
+
if (resourceTypeValue === void 0 || resourceTypeValue.length === 0 || actionValue === void 0 || actionValue.length === 0 || rest.length > 0 || !isValidResourceKey(resourceTypeValue)) return null;
|
|
11
13
|
return {
|
|
12
14
|
action: actionValue,
|
|
13
15
|
resourceType: resourceTypeValue
|
|
@@ -69,7 +71,7 @@ const parsePermission = (permission) => {
|
|
|
69
71
|
* ```
|
|
70
72
|
*/
|
|
71
73
|
const createPolicy = (config) => {
|
|
72
|
-
const { rules, resources, actions } = config;
|
|
74
|
+
const { rules, resources, actions, logger } = config;
|
|
73
75
|
const validators = /* @__PURE__ */ new Map();
|
|
74
76
|
const getValidatedResource = async (resourceType, resource) => {
|
|
75
77
|
const validator = validators.get(resourceType);
|
|
@@ -79,7 +81,10 @@ const createPolicy = (config) => {
|
|
|
79
81
|
if (result.issues) return null;
|
|
80
82
|
return result.value;
|
|
81
83
|
} catch (error) {
|
|
82
|
-
|
|
84
|
+
logger?.warn(`Resource validation failed for ${String(resourceType)}: ${String(error)}`, {
|
|
85
|
+
error,
|
|
86
|
+
resourceType: String(resourceType)
|
|
87
|
+
});
|
|
83
88
|
return null;
|
|
84
89
|
}
|
|
85
90
|
};
|
|
@@ -88,9 +93,22 @@ const createPolicy = (config) => {
|
|
|
88
93
|
const policyFn = rules[resourceType]?.[action];
|
|
89
94
|
if (policyFn === void 0) return false;
|
|
90
95
|
try {
|
|
91
|
-
|
|
96
|
+
const allowed = policyFn(context, action, resource) === "allow";
|
|
97
|
+
if (allowed) logger?.debug("permission allowed", {
|
|
98
|
+
action,
|
|
99
|
+
resourceType: String(resourceType)
|
|
100
|
+
});
|
|
101
|
+
else logger?.info("permission denied", {
|
|
102
|
+
action,
|
|
103
|
+
resourceType: String(resourceType)
|
|
104
|
+
});
|
|
105
|
+
return allowed;
|
|
92
106
|
} catch (error) {
|
|
93
|
-
|
|
107
|
+
logger?.warn(`Policy evaluation error for ${String(resourceType)}.${action}: ${String(error)}`, {
|
|
108
|
+
action,
|
|
109
|
+
error,
|
|
110
|
+
resourceType: String(resourceType)
|
|
111
|
+
});
|
|
94
112
|
return false;
|
|
95
113
|
}
|
|
96
114
|
};
|
|
@@ -101,7 +119,7 @@ const createPolicy = (config) => {
|
|
|
101
119
|
validators.set(key, async (input) => await validator(input));
|
|
102
120
|
}
|
|
103
121
|
return { async can(context, permission, resource) {
|
|
104
|
-
const parsedPermission = parsePermission(permission);
|
|
122
|
+
const parsedPermission = parsePermission(permission, actions);
|
|
105
123
|
if (parsedPermission === null) return false;
|
|
106
124
|
const { action, resourceType } = parsedPermission;
|
|
107
125
|
if (!hasAllowedAction(resourceType, action)) return false;
|
|
@@ -112,42 +130,43 @@ const createPolicy = (config) => {
|
|
|
112
130
|
};
|
|
113
131
|
const mergePoliciesWithStrategy = (policies, strategy) => ({ async can(context, permission, resource) {
|
|
114
132
|
if (policies.length === 0) return false;
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return strategy === "deny-overrides";
|
|
133
|
+
const results = (await Promise.allSettled(policies.map(async (policy) => await policy.can(context, permission, resource)))).map((result) => {
|
|
134
|
+
if (result.status === "fulfilled") return result.value;
|
|
135
|
+
return false;
|
|
136
|
+
});
|
|
137
|
+
return strategy === "and" ? results.every(Boolean) : results.some(Boolean);
|
|
121
138
|
} });
|
|
122
139
|
/**
|
|
123
|
-
* Merges multiple policies into one
|
|
124
|
-
* If any policy denies, the merged policy denies.
|
|
140
|
+
* Merges multiple policies into one, requiring every policy to allow.
|
|
141
|
+
* If any policy denies, the merged policy denies. Policies are evaluated
|
|
142
|
+
* in parallel; every policy is invoked regardless of outcome.
|
|
125
143
|
*
|
|
126
144
|
* @example
|
|
127
145
|
* ```ts
|
|
128
146
|
* const basePolicy = createPolicy({ ... });
|
|
129
147
|
* const adminPolicy = createPolicy({ ... });
|
|
130
148
|
*
|
|
131
|
-
* const merged =
|
|
149
|
+
* const merged = mergePoliciesAnd(basePolicy, adminPolicy);
|
|
132
150
|
* // Both policies must allow for the action to be permitted
|
|
133
151
|
* ```
|
|
134
152
|
*/
|
|
135
|
-
const
|
|
153
|
+
const mergePoliciesAnd = (...policies) => mergePoliciesWithStrategy(policies, "and");
|
|
136
154
|
/**
|
|
137
|
-
* Merges multiple policies into one
|
|
138
|
-
* If
|
|
155
|
+
* Merges multiple policies into one, requiring at least one policy to allow.
|
|
156
|
+
* If every policy denies, the merged policy denies. Policies are evaluated
|
|
157
|
+
* in parallel; every policy is invoked regardless of outcome.
|
|
139
158
|
*
|
|
140
159
|
* @example
|
|
141
160
|
* ```ts
|
|
142
161
|
* const guestPolicy = createPolicy({ ... });
|
|
143
162
|
* const memberPolicy = createPolicy({ ... });
|
|
144
163
|
*
|
|
145
|
-
* const merged =
|
|
164
|
+
* const merged = mergePoliciesOr(guestPolicy, memberPolicy);
|
|
146
165
|
* // If either policy allows, the action is permitted
|
|
147
166
|
* ```
|
|
148
167
|
*/
|
|
149
|
-
const
|
|
168
|
+
const mergePoliciesOr = (...policies) => mergePoliciesWithStrategy(policies, "or");
|
|
150
169
|
//#endregion
|
|
151
|
-
export { createPolicy,
|
|
170
|
+
export { createPolicy, mergePoliciesAnd, mergePoliciesOr };
|
|
152
171
|
|
|
153
172
|
//# sourceMappingURL=policy.js.map
|
package/dist/policy.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policy.js","names":[],"sources":["../src/policy.ts"],"sourcesContent":["/**\n * Policy creation and composition: `createPolicy`, `mergePolicies`, and\n * `mergePoliciesAny`.\n *\n * @module @zap-studio/permit/policy\n */\n\nimport type { StandardSchemaV1 } from \"@zap-studio/validation\";\nimport { createStandardValidator } from \"@zap-studio/validation\";\n\nimport { PolicyError } from \"./errors.js\";\nimport type {\n Actions,\n Context,\n InferAction,\n InferResource,\n PermitConfig,\n Policy,\n Resources,\n} from \"./types.js\";\n\n/**\n * Splits a typed `resource:action` permission string into its parts.\n * Returns `null` when the string is malformed (missing/empty part or extra segments).\n */\nconst parsePermission = <\n TResources extends Resources,\n TActions extends Actions<TResources>,\n K extends keyof TResources & keyof TActions,\n>(\n permission: `${K & string}:${InferAction<TActions, K> & string}`\n): { action: InferAction<TActions, K>; resourceType: K } | null => {\n const [resourceTypeValue, actionValue, ...rest] = permission.split(\":\");\n if (\n resourceTypeValue === undefined ||\n resourceTypeValue.length === 0 ||\n actionValue === undefined ||\n actionValue.length === 0 ||\n rest.length > 0\n ) {\n return null;\n }\n\n return {\n action: actionValue,\n // oxlint-disable-next-line typescript/no-unsafe-type-assertion -- Parsed permission strings are constrained by the typed permission template.\n resourceType: resourceTypeValue as K,\n };\n};\n\n/**\n * Creates a type-safe policy from resource schemas, actions, and rules.\n *\n * @example\n * ```ts\n * import { z } from \"zod\";\n * import { createPolicy, allow, deny, when } from \"@zap-studio/permit\";\n * import type { Resources, Actions } from \"@zap-studio/permit/types\";\n *\n * // Define resource schemas\n * const resources = {\n * post: z.object({\n * id: z.string(),\n * authorId: z.string(),\n * visibility: z.enum([\"public\", \"private\"]),\n * }),\n * comment: z.object({\n * id: z.string(),\n * postId: z.string(),\n * authorId: z.string(),\n * }),\n * } satisfies Resources;\n *\n * // Define actions per resource\n * const actions = {\n * post: [\"read\", \"write\", \"delete\"],\n * comment: [\"read\", \"write\"],\n * } as const satisfies Actions<typeof resources>;\n *\n * // Define context type\n * type AppContext = { user: { id: string; role: string } };\n *\n * // Create the policy\n * const policy = createPolicy<AppContext>({\n * resources,\n * actions,\n * rules: {\n * post: {\n * read: when((ctx, action, resource) => resource.visibility === \"public\"),\n * write: when((ctx, action, resource) => ctx.user.id === resource.authorId),\n * delete: deny(),\n * },\n * comment: {\n * read: allow(),\n * write: when((ctx, action, resource) => ctx.user.id === resource.authorId),\n * },\n * },\n * });\n *\n * // Check permissions\n * const post = { id: \"1\", authorId: \"user-1\", visibility: \"public\" as const };\n * await policy.can(ctx, \"post:read\", post); // true\n * await policy.can(ctx, \"post:write\", post); // depends on ctx.user.id\n * ```\n */\nexport const createPolicy = <\n TContext extends Context,\n TResources extends Resources = Resources,\n TActions extends Actions<TResources> = Actions<TResources>,\n>(\n config: PermitConfig<TContext, TResources, TActions>\n): Policy<TContext, TResources, TActions> => {\n const { rules, resources, actions } = config;\n const validators = new Map<\n keyof TResources,\n (input: unknown) => Promise<StandardSchemaV1.Result<unknown>>\n >();\n\n const getValidatedResource = async <K extends keyof TResources>(\n resourceType: K,\n resource: InferResource<TResources, K>\n ): Promise<InferResource<TResources, K> | null> => {\n const validator = validators.get(resourceType);\n if (validator === undefined) {\n return null;\n }\n try {\n const result = await validator(resource);\n if (result.issues) {\n return null;\n }\n return result.value;\n } catch (error) {\n console.warn(\n `Resource validation failed for ${String(resourceType)}: ${String(error)}`\n );\n return null;\n }\n };\n\n const hasAllowedAction = <K extends keyof TResources & keyof TActions>(\n resourceType: K,\n action: InferAction<TActions, K>\n ): boolean => actions[resourceType]?.includes(action) ?? false;\n\n const evaluatePolicy = <K extends keyof TResources & keyof TActions>(\n context: TContext,\n resourceType: K,\n action: InferAction<TActions, K>,\n resource: InferResource<TResources, K>\n ): boolean => {\n const policyFn = rules[resourceType]?.[action];\n if (policyFn === undefined) {\n return false;\n }\n\n try {\n return policyFn(context, action, resource) === \"allow\";\n } catch (error) {\n console.warn(\n `Policy evaluation error for ${String(resourceType)}.${action}: ${String(error)}`\n );\n return false;\n }\n };\n\n for (const key of Object.keys(resources) as (keyof TResources)[]) {\n const schema = resources[key];\n if (schema === undefined) {\n throw new PolicyError(`Missing schema for resource: ${String(key)}`);\n }\n const validator = createStandardValidator(schema);\n validators.set(key, async (input: unknown) => await validator(input));\n }\n\n return {\n async can<K extends keyof TResources & keyof TActions>(\n context: TContext,\n permission: `${K & string}:${InferAction<TActions, K> & string}`,\n resource: InferResource<TResources, K>\n ): Promise<boolean> {\n const parsedPermission = parsePermission<TResources, TActions, K>(\n permission\n );\n if (parsedPermission === null) {\n return false;\n }\n\n const { action, resourceType } = parsedPermission;\n if (!hasAllowedAction(resourceType, action)) {\n return false;\n }\n\n const validatedResource = await getValidatedResource(\n resourceType,\n resource\n );\n if (validatedResource === null) {\n return false;\n }\n\n return evaluatePolicy(context, resourceType, action, validatedResource);\n },\n };\n};\n\nconst mergePoliciesWithStrategy = <\n TContext extends Context,\n TResources extends Resources = Resources,\n TActions extends Actions<TResources> = Actions<TResources>,\n>(\n policies: Policy<TContext, TResources, TActions>[],\n strategy: \"allow-overrides\" | \"deny-overrides\"\n): Policy<TContext, TResources, TActions> => ({\n async can<K extends keyof TResources & keyof TActions>(\n context: TContext,\n permission: `${K & string}:${InferAction<TActions, K> & string}`,\n resource: InferResource<TResources, K>\n ): Promise<boolean> {\n if (policies.length === 0) {\n return false;\n }\n for (const policy of policies) {\n // oxlint-disable-next-line no-await-in-loop -- Policies must evaluate sequentially to preserve short-circuit semantics.\n const allowed = await policy.can(context, permission, resource);\n\n if (strategy === \"allow-overrides\" && allowed) {\n return true;\n }\n if (strategy === \"deny-overrides\" && !allowed) {\n return false;\n }\n }\n return strategy === \"deny-overrides\";\n },\n});\n\n/**\n * Merges multiple policies into one using \"deny-overrides\" strategy.\n * If any policy denies, the merged policy denies. All must allow for the result to allow.\n *\n * @example\n * ```ts\n * const basePolicy = createPolicy({ ... });\n * const adminPolicy = createPolicy({ ... });\n *\n * const merged = mergePolicies(basePolicy, adminPolicy);\n * // Both policies must allow for the action to be permitted\n * ```\n */\nexport const mergePolicies = <\n TContext extends Context,\n TResources extends Resources = Resources,\n TActions extends Actions<TResources> = Actions<TResources>,\n>(\n ...policies: Policy<TContext, TResources, TActions>[]\n): Policy<TContext, TResources, TActions> =>\n mergePoliciesWithStrategy(policies, \"deny-overrides\");\n\n/**\n * Merges multiple policies into one using \"allow-overrides\" strategy.\n * If any policy allows, the merged policy allows. All must deny for the result to deny.\n *\n * @example\n * ```ts\n * const guestPolicy = createPolicy({ ... });\n * const memberPolicy = createPolicy({ ... });\n *\n * const merged = mergePoliciesAny(guestPolicy, memberPolicy);\n * // If either policy allows, the action is permitted\n * ```\n */\nexport const mergePoliciesAny = <\n TContext extends Context,\n TResources extends Resources = Resources,\n TActions extends Actions<TResources> = Actions<TResources>,\n>(\n ...policies: Policy<TContext, TResources, TActions>[]\n): Policy<TContext, TResources, TActions> =>\n mergePoliciesWithStrategy(policies, \"allow-overrides\");\n"],"mappings":";;;;;;;AAyBA,MAAM,mBAKJ,eACiE;CACjE,MAAM,CAAC,mBAAmB,aAAa,GAAG,QAAQ,WAAW,MAAM,GAAG;CACtE,IACE,sBAAsB,KAAA,KACtB,kBAAkB,WAAW,KAC7B,gBAAgB,KAAA,KAChB,YAAY,WAAW,KACvB,KAAK,SAAS,GAEd,OAAO;CAGT,OAAO;EACL,QAAQ;EAER,cAAc;CAChB;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyDA,MAAa,gBAKX,WAC2C;CAC3C,MAAM,EAAE,OAAO,WAAW,YAAY;CACtC,MAAM,6BAAa,IAAI,IAGrB;CAEF,MAAM,uBAAuB,OAC3B,cACA,aACiD;EACjD,MAAM,YAAY,WAAW,IAAI,YAAY;EAC7C,IAAI,cAAc,KAAA,GAChB,OAAO;EAET,IAAI;GACF,MAAM,SAAS,MAAM,UAAU,QAAQ;GACvC,IAAI,OAAO,QACT,OAAO;GAET,OAAO,OAAO;EAChB,SAAS,OAAO;GACd,QAAQ,KACN,kCAAkC,OAAO,YAAY,EAAE,IAAI,OAAO,KAAK,GACzE;GACA,OAAO;EACT;CACF;CAEA,MAAM,oBACJ,cACA,WACY,QAAQ,aAAa,EAAE,SAAS,MAAM,KAAK;CAEzD,MAAM,kBACJ,SACA,cACA,QACA,aACY;EACZ,MAAM,WAAW,MAAM,aAAa,GAAG;EACvC,IAAI,aAAa,KAAA,GACf,OAAO;EAGT,IAAI;GACF,OAAO,SAAS,SAAS,QAAQ,QAAQ,MAAM;EACjD,SAAS,OAAO;GACd,QAAQ,KACN,+BAA+B,OAAO,YAAY,EAAE,GAAG,OAAO,IAAI,OAAO,KAAK,GAChF;GACA,OAAO;EACT;CACF;CAEA,KAAK,MAAM,OAAO,OAAO,KAAK,SAAS,GAA2B;EAChE,MAAM,SAAS,UAAU;EACzB,IAAI,WAAW,KAAA,GACb,MAAM,IAAI,YAAY,gCAAgC,OAAO,GAAG,GAAG;EAErE,MAAM,YAAY,wBAAwB,MAAM;EAChD,WAAW,IAAI,KAAK,OAAO,UAAmB,MAAM,UAAU,KAAK,CAAC;CACtE;CAEA,OAAO,EACL,MAAM,IACJ,SACA,YACA,UACkB;EAClB,MAAM,mBAAmB,gBACvB,UACF;EACA,IAAI,qBAAqB,MACvB,OAAO;EAGT,MAAM,EAAE,QAAQ,iBAAiB;EACjC,IAAI,CAAC,iBAAiB,cAAc,MAAM,GACxC,OAAO;EAGT,MAAM,oBAAoB,MAAM,qBAC9B,cACA,QACF;EACA,IAAI,sBAAsB,MACxB,OAAO;EAGT,OAAO,eAAe,SAAS,cAAc,QAAQ,iBAAiB;CACxE,EACF;AACF;AAEA,MAAM,6BAKJ,UACA,cAC4C,EAC5C,MAAM,IACJ,SACA,YACA,UACkB;CAClB,IAAI,SAAS,WAAW,GACtB,OAAO;CAET,KAAK,MAAM,UAAU,UAAU;EAE7B,MAAM,UAAU,MAAM,OAAO,IAAI,SAAS,YAAY,QAAQ;EAE9D,IAAI,aAAa,qBAAqB,SACpC,OAAO;EAET,IAAI,aAAa,oBAAoB,CAAC,SACpC,OAAO;CAEX;CACA,OAAO,aAAa;AACtB,EACF;;;;;;;;;;;;;;AAeA,MAAa,iBAKX,GAAG,aAEH,0BAA0B,UAAU,gBAAgB;;;;;;;;;;;;;;AAetD,MAAa,oBAKX,GAAG,aAEH,0BAA0B,UAAU,iBAAiB"}
|
|
1
|
+
{"version":3,"file":"policy.js","names":[],"sources":["../src/policy.ts"],"sourcesContent":["/**\n * Policy creation and composition: `createPolicy`, `mergePoliciesAnd`, and\n * `mergePoliciesOr`.\n *\n * @module @zap-studio/permit/policy\n */\n\nimport type { StandardSchemaV1 } from \"@zap-studio/validation\";\nimport { createStandardValidator } from \"@zap-studio/validation\";\n\nimport { PolicyError } from \"./errors.js\";\nimport type {\n Actions,\n Context,\n InferAction,\n InferResource,\n PermitConfig,\n Policy,\n Resources,\n} from \"./types.js\";\n\n/**\n * Splits a typed `resource:action` permission string into its parts.\n * Returns `null` when the string is malformed (missing/empty part or extra\n * segments) or `resourceType` is not one of `actions`' keys.\n */\nconst parsePermission = <\n TResources extends Resources,\n TActions extends Actions<TResources>,\n K extends keyof TResources & keyof TActions,\n>(\n permission: `${K & string}:${InferAction<TResources, TActions, K> & string}`,\n actions: TActions\n): { action: InferAction<TResources, TActions, K>; resourceType: K } | null => {\n const isValidResourceKey = (value: string): value is K & string =>\n Object.keys(actions).includes(value);\n\n const [resourceTypeValue, actionValue, ...rest] = permission.split(\":\");\n if (\n resourceTypeValue === undefined ||\n resourceTypeValue.length === 0 ||\n actionValue === undefined ||\n actionValue.length === 0 ||\n rest.length > 0 ||\n !isValidResourceKey(resourceTypeValue)\n ) {\n return null;\n }\n\n return {\n action: actionValue,\n resourceType: resourceTypeValue,\n };\n};\n\n/**\n * Creates a type-safe policy from resource schemas, actions, and rules.\n *\n * @example\n * ```ts\n * import { z } from \"zod\";\n * import { createPolicy, allow, deny, when } from \"@zap-studio/permit\";\n * import type { Resources, Actions } from \"@zap-studio/permit/types\";\n *\n * // Define resource schemas\n * const resources = {\n * post: z.object({\n * id: z.string(),\n * authorId: z.string(),\n * visibility: z.enum([\"public\", \"private\"]),\n * }),\n * comment: z.object({\n * id: z.string(),\n * postId: z.string(),\n * authorId: z.string(),\n * }),\n * } satisfies Resources;\n *\n * // Define actions per resource\n * const actions = {\n * post: [\"read\", \"write\", \"delete\"],\n * comment: [\"read\", \"write\"],\n * } as const satisfies Actions<typeof resources>;\n *\n * // Define context type\n * type AppContext = { user: { id: string; role: string } };\n *\n * // Create the policy\n * const policy = createPolicy<AppContext>({\n * resources,\n * actions,\n * rules: {\n * post: {\n * read: when((ctx, action, resource) => resource.visibility === \"public\"),\n * write: when((ctx, action, resource) => ctx.user.id === resource.authorId),\n * delete: deny(),\n * },\n * comment: {\n * read: allow(),\n * write: when((ctx, action, resource) => ctx.user.id === resource.authorId),\n * },\n * },\n * });\n *\n * // Check permissions\n * const post = { id: \"1\", authorId: \"user-1\", visibility: \"public\" as const };\n * await policy.can(ctx, \"post:read\", post); // true\n * await policy.can(ctx, \"post:write\", post); // depends on ctx.user.id\n * ```\n */\nexport const createPolicy = <\n TContext extends Context,\n TResources extends Resources = Resources,\n TActions extends Actions<TResources> = Actions<TResources>,\n>(\n config: PermitConfig<TContext, TResources, TActions>\n): Policy<TContext, TResources, TActions> => {\n const { rules, resources, actions, logger } = config;\n const validators = new Map<\n keyof TResources,\n (input: unknown) => Promise<StandardSchemaV1.Result<unknown>>\n >();\n\n const getValidatedResource = async <K extends keyof TResources>(\n resourceType: K,\n resource: InferResource<TResources, K>\n ): Promise<InferResource<TResources, K> | null> => {\n const validator = validators.get(resourceType);\n if (validator === undefined) {\n return null;\n }\n try {\n const result = await validator(resource);\n if (result.issues) {\n return null;\n }\n return result.value;\n } catch (error) {\n logger?.warn(\n `Resource validation failed for ${String(resourceType)}: ${String(error)}`,\n { error, resourceType: String(resourceType) }\n );\n return null;\n }\n };\n\n const hasAllowedAction = <K extends keyof TResources & keyof TActions>(\n resourceType: K,\n action: InferAction<TResources, TActions, K>\n ): boolean => actions[resourceType]?.includes(action) ?? false;\n\n const evaluatePolicy = <K extends keyof TResources & keyof TActions>(\n context: TContext,\n resourceType: K,\n action: InferAction<TResources, TActions, K>,\n resource: InferResource<TResources, K>\n ): boolean => {\n const policyFn = rules[resourceType]?.[action];\n if (policyFn === undefined) {\n return false;\n }\n\n try {\n const allowed = policyFn(context, action, resource) === \"allow\";\n\n if (allowed) {\n logger?.debug(\"permission allowed\", {\n action,\n resourceType: String(resourceType),\n });\n } else {\n logger?.info(\"permission denied\", {\n action,\n resourceType: String(resourceType),\n });\n }\n\n return allowed;\n } catch (error) {\n logger?.warn(\n `Policy evaluation error for ${String(resourceType)}.${action}: ${String(error)}`,\n {\n action,\n error,\n resourceType: String(resourceType),\n }\n );\n return false;\n }\n };\n\n for (const key of Object.keys(resources) as (keyof TResources)[]) {\n const schema = resources[key];\n if (schema === undefined) {\n throw new PolicyError(`Missing schema for resource: ${String(key)}`);\n }\n const validator = createStandardValidator(schema);\n validators.set(key, async (input: unknown) => await validator(input));\n }\n\n return {\n async can<K extends keyof TResources & keyof TActions>(\n context: TContext,\n permission: `${K & string}:${InferAction<TResources, TActions, K> & string}`,\n resource: InferResource<TResources, K>\n ): Promise<boolean> {\n const parsedPermission = parsePermission<TResources, TActions, K>(\n permission,\n actions\n );\n if (parsedPermission === null) {\n return false;\n }\n\n const { action, resourceType } = parsedPermission;\n if (!hasAllowedAction(resourceType, action)) {\n return false;\n }\n\n const validatedResource = await getValidatedResource(\n resourceType,\n resource\n );\n if (validatedResource === null) {\n return false;\n }\n\n return evaluatePolicy(context, resourceType, action, validatedResource);\n },\n };\n};\n\nconst mergePoliciesWithStrategy = <\n TContext extends Context,\n TResources extends Resources = Resources,\n TActions extends Actions<TResources> = Actions<TResources>,\n>(\n policies: Policy<TContext, TResources, TActions>[],\n strategy: \"and\" | \"or\"\n): Policy<TContext, TResources, TActions> => ({\n async can<K extends keyof TResources & keyof TActions>(\n context: TContext,\n permission: `${K & string}:${InferAction<TResources, TActions, K> & string}`,\n resource: InferResource<TResources, K>\n ): Promise<boolean> {\n if (policies.length === 0) {\n return false;\n }\n\n const settled = await Promise.allSettled(\n policies.map(\n async (policy) => await policy.can(context, permission, resource)\n )\n );\n\n const results = settled.map((result) => {\n if (result.status === \"fulfilled\") {\n return result.value;\n }\n return false;\n });\n\n return strategy === \"and\" ? results.every(Boolean) : results.some(Boolean);\n },\n});\n\n/**\n * Merges multiple policies into one, requiring every policy to allow.\n * If any policy denies, the merged policy denies. Policies are evaluated\n * in parallel; every policy is invoked regardless of outcome.\n *\n * @example\n * ```ts\n * const basePolicy = createPolicy({ ... });\n * const adminPolicy = createPolicy({ ... });\n *\n * const merged = mergePoliciesAnd(basePolicy, adminPolicy);\n * // Both policies must allow for the action to be permitted\n * ```\n */\nexport const mergePoliciesAnd = <\n TContext extends Context,\n TResources extends Resources = Resources,\n TActions extends Actions<TResources> = Actions<TResources>,\n>(\n ...policies: Policy<TContext, TResources, TActions>[]\n): Policy<TContext, TResources, TActions> =>\n mergePoliciesWithStrategy(policies, \"and\");\n\n/**\n * Merges multiple policies into one, requiring at least one policy to allow.\n * If every policy denies, the merged policy denies. Policies are evaluated\n * in parallel; every policy is invoked regardless of outcome.\n *\n * @example\n * ```ts\n * const guestPolicy = createPolicy({ ... });\n * const memberPolicy = createPolicy({ ... });\n *\n * const merged = mergePoliciesOr(guestPolicy, memberPolicy);\n * // If either policy allows, the action is permitted\n * ```\n */\nexport const mergePoliciesOr = <\n TContext extends Context,\n TResources extends Resources = Resources,\n TActions extends Actions<TResources> = Actions<TResources>,\n>(\n ...policies: Policy<TContext, TResources, TActions>[]\n): Policy<TContext, TResources, TActions> =>\n mergePoliciesWithStrategy(policies, \"or\");\n"],"mappings":";;;;;;;;AA0BA,MAAM,mBAKJ,YACA,YAC6E;CAC7E,MAAM,sBAAsB,UAC1B,OAAO,KAAK,OAAO,CAAC,CAAC,SAAS,KAAK;CAErC,MAAM,CAAC,mBAAmB,aAAa,GAAG,QAAQ,WAAW,MAAM,GAAG;CACtE,IACE,sBAAsB,KAAA,KACtB,kBAAkB,WAAW,KAC7B,gBAAgB,KAAA,KAChB,YAAY,WAAW,KACvB,KAAK,SAAS,KACd,CAAC,mBAAmB,iBAAiB,GAErC,OAAO;CAGT,OAAO;EACL,QAAQ;EACR,cAAc;CAChB;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAyDA,MAAa,gBAKX,WAC2C;CAC3C,MAAM,EAAE,OAAO,WAAW,SAAS,WAAW;CAC9C,MAAM,6BAAa,IAAI,IAGrB;CAEF,MAAM,uBAAuB,OAC3B,cACA,aACiD;EACjD,MAAM,YAAY,WAAW,IAAI,YAAY;EAC7C,IAAI,cAAc,KAAA,GAChB,OAAO;EAET,IAAI;GACF,MAAM,SAAS,MAAM,UAAU,QAAQ;GACvC,IAAI,OAAO,QACT,OAAO;GAET,OAAO,OAAO;EAChB,SAAS,OAAO;GACd,QAAQ,KACN,kCAAkC,OAAO,YAAY,EAAE,IAAI,OAAO,KAAK,KACvE;IAAE;IAAO,cAAc,OAAO,YAAY;GAAE,CAC9C;GACA,OAAO;EACT;CACF;CAEA,MAAM,oBACJ,cACA,WACY,QAAQ,aAAa,EAAE,SAAS,MAAM,KAAK;CAEzD,MAAM,kBACJ,SACA,cACA,QACA,aACY;EACZ,MAAM,WAAW,MAAM,aAAa,GAAG;EACvC,IAAI,aAAa,KAAA,GACf,OAAO;EAGT,IAAI;GACF,MAAM,UAAU,SAAS,SAAS,QAAQ,QAAQ,MAAM;GAExD,IAAI,SACF,QAAQ,MAAM,sBAAsB;IAClC;IACA,cAAc,OAAO,YAAY;GACnC,CAAC;QAED,QAAQ,KAAK,qBAAqB;IAChC;IACA,cAAc,OAAO,YAAY;GACnC,CAAC;GAGH,OAAO;EACT,SAAS,OAAO;GACd,QAAQ,KACN,+BAA+B,OAAO,YAAY,EAAE,GAAG,OAAO,IAAI,OAAO,KAAK,KAC9E;IACE;IACA;IACA,cAAc,OAAO,YAAY;GACnC,CACF;GACA,OAAO;EACT;CACF;CAEA,KAAK,MAAM,OAAO,OAAO,KAAK,SAAS,GAA2B;EAChE,MAAM,SAAS,UAAU;EACzB,IAAI,WAAW,KAAA,GACb,MAAM,IAAI,YAAY,gCAAgC,OAAO,GAAG,GAAG;EAErE,MAAM,YAAY,wBAAwB,MAAM;EAChD,WAAW,IAAI,KAAK,OAAO,UAAmB,MAAM,UAAU,KAAK,CAAC;CACtE;CAEA,OAAO,EACL,MAAM,IACJ,SACA,YACA,UACkB;EAClB,MAAM,mBAAmB,gBACvB,YACA,OACF;EACA,IAAI,qBAAqB,MACvB,OAAO;EAGT,MAAM,EAAE,QAAQ,iBAAiB;EACjC,IAAI,CAAC,iBAAiB,cAAc,MAAM,GACxC,OAAO;EAGT,MAAM,oBAAoB,MAAM,qBAC9B,cACA,QACF;EACA,IAAI,sBAAsB,MACxB,OAAO;EAGT,OAAO,eAAe,SAAS,cAAc,QAAQ,iBAAiB;CACxE,EACF;AACF;AAEA,MAAM,6BAKJ,UACA,cAC4C,EAC5C,MAAM,IACJ,SACA,YACA,UACkB;CAClB,IAAI,SAAS,WAAW,GACtB,OAAO;CAST,MAAM,WAAU,MANM,QAAQ,WAC5B,SAAS,IACP,OAAO,WAAW,MAAM,OAAO,IAAI,SAAS,YAAY,QAAQ,CAClE,CACF,EAAA,CAEwB,KAAK,WAAW;EACtC,IAAI,OAAO,WAAW,aACpB,OAAO,OAAO;EAEhB,OAAO;CACT,CAAC;CAED,OAAO,aAAa,QAAQ,QAAQ,MAAM,OAAO,IAAI,QAAQ,KAAK,OAAO;AAC3E,EACF;;;;;;;;;;;;;;;AAgBA,MAAa,oBAKX,GAAG,aAEH,0BAA0B,UAAU,KAAK;;;;;;;;;;;;;;;AAgB3C,MAAa,mBAKX,GAAG,aAEH,0BAA0B,UAAU,IAAI"}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,18 +1,34 @@
|
|
|
1
1
|
import { StandardSchemaV1 } from "@zap-studio/validation";
|
|
2
|
+
import { Logger } from "@zap-studio/logger";
|
|
2
3
|
//#region src/types.d.ts
|
|
3
4
|
/**
|
|
4
5
|
* Represents the possible outcomes of a policy decision.
|
|
5
6
|
* - "allow": The action is permitted.
|
|
6
7
|
* - "deny": The action is not permitted.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* const decision: Decision = "allow";
|
|
12
|
+
* ```
|
|
7
13
|
*/
|
|
8
14
|
type Decision = "allow" | "deny";
|
|
9
15
|
/**
|
|
10
16
|
* Represents the context in which a policy decision is made.
|
|
11
17
|
* Can include user information, environment, or any relevant data.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* type AppContext = Context<{ user: { id: string; role: string } }>;
|
|
22
|
+
* ```
|
|
12
23
|
*/
|
|
13
24
|
type Context<TContext = unknown> = TContext;
|
|
14
25
|
/**
|
|
15
26
|
* Represents a role within the system.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* type AppRole = Role<"guest" | "user" | "admin">;
|
|
31
|
+
* ```
|
|
16
32
|
*/
|
|
17
33
|
type Role<TRole extends string = string> = TRole;
|
|
18
34
|
/**
|
|
@@ -64,12 +80,23 @@ type Resources<TResourceKey extends string = string> = Record<TResourceKey, Stan
|
|
|
64
80
|
type Actions<TResources extends Resources> = { [K in keyof TResources]: readonly string[]; };
|
|
65
81
|
/**
|
|
66
82
|
* Infers the output type from a Standard Schema.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* type Post = InferResource<typeof resources, "post">;
|
|
87
|
+
* ```
|
|
67
88
|
*/
|
|
68
89
|
type InferResource<TResources extends Resources, TResourceKey extends keyof TResources> = StandardSchemaV1.InferOutput<TResources[TResourceKey]>;
|
|
69
90
|
/**
|
|
70
91
|
* Infers the action union type for a specific resource.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* ```ts
|
|
95
|
+
* type PostAction = InferAction<typeof resources, typeof actions, "post">;
|
|
96
|
+
* // "read" | "write" | "delete"
|
|
97
|
+
* ```
|
|
71
98
|
*/
|
|
72
|
-
type InferAction<
|
|
99
|
+
type InferAction<TResources extends Resources, TActions extends Actions<TResources>, K extends keyof TActions> = TActions[K][number];
|
|
73
100
|
/**
|
|
74
101
|
* Infers the permission-string union for all resource/action combinations.
|
|
75
102
|
*
|
|
@@ -79,24 +106,88 @@ type InferAction<TActions extends Record<string, readonly string[]>, K extends k
|
|
|
79
106
|
* // "post:read" | "post:write" | "comment:read"
|
|
80
107
|
* ```
|
|
81
108
|
*/
|
|
82
|
-
type InferPermission<TResources extends Resources, TActions extends Actions<TResources>> = { [K in keyof TResources & keyof TActions]: `${K & string}:${InferAction<TActions, K> & string}`; }[keyof TResources & keyof TActions];
|
|
109
|
+
type InferPermission<TResources extends Resources, TActions extends Actions<TResources>> = { [K in keyof TResources & keyof TActions]: `${K & string}:${InferAction<TResources, TActions, K> & string}`; }[keyof TResources & keyof TActions];
|
|
83
110
|
/**
|
|
84
111
|
* A function that determines whether a given action on a resource is allowed in a specific context.
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```ts
|
|
115
|
+
* const readPolicy: PolicyFn<AppContext, "read", Post> = (context, action, post) =>
|
|
116
|
+
* post.visibility === "public" ? "allow" : "deny";
|
|
117
|
+
* ```
|
|
85
118
|
*/
|
|
86
119
|
type PolicyFn<TContext extends Context, TAction extends string = string, TResource = unknown> = (context: TContext, action: TAction, resource: TResource) => Decision;
|
|
87
120
|
/**
|
|
88
121
|
* A function that evaluates a condition for a given action and resource in a specific context.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```ts
|
|
125
|
+
* const isOwner: ConditionFn<AppContext, "write", Post> = (context, action, post) =>
|
|
126
|
+
* context.user.id === post.authorId;
|
|
127
|
+
* ```
|
|
89
128
|
*/
|
|
90
129
|
type ConditionFn<TContext extends Context, TAction extends string = string, TResource = unknown> = (context: TContext, action: TAction, resource: TResource) => boolean;
|
|
130
|
+
/**
|
|
131
|
+
* Call signatures for {@link hasRole}, preserving the with/without hierarchy overloads.
|
|
132
|
+
*/
|
|
133
|
+
interface HasRoleFn {
|
|
134
|
+
/**
|
|
135
|
+
* Checks membership in `role` only, with no inherited roles.
|
|
136
|
+
*
|
|
137
|
+
* @param role - Role the context's `role` (or `role[]`) must include.
|
|
138
|
+
*/
|
|
139
|
+
<TContext extends {
|
|
140
|
+
role: Role | Role[];
|
|
141
|
+
}, TAction extends string = string, TResource = unknown>(role: Role): ConditionFn<TContext, TAction, TResource>;
|
|
142
|
+
/**
|
|
143
|
+
* Checks membership in `role`, treating any role that inherits from it
|
|
144
|
+
* (per `hierarchy`) as also satisfying the check.
|
|
145
|
+
*
|
|
146
|
+
* @param role - Role the context's `role` (or `role[]`) must include or inherit.
|
|
147
|
+
* @param hierarchy - Maps each role to the roles it inherits from.
|
|
148
|
+
*/
|
|
149
|
+
<TContext extends {
|
|
150
|
+
role: TRole | TRole[];
|
|
151
|
+
}, TAction extends string = string, TResource = unknown, TRole extends Role = Role>(role: TRole, hierarchy: RoleHierarchy<TRole>): ConditionFn<TContext, TAction, TResource>;
|
|
152
|
+
}
|
|
91
153
|
/**
|
|
92
154
|
* Maps actions to their corresponding policy functions for a specific resource.
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```ts
|
|
158
|
+
* import type { ActionPolicyMap } from "@zap-studio/permit/types";
|
|
159
|
+
*
|
|
160
|
+
* type PostActions = "read" | "write" | "delete";
|
|
161
|
+
*
|
|
162
|
+
* const postPolicies: ActionPolicyMap<AppContext, PostActions, Post> = {
|
|
163
|
+
* read: (context, action, post) => "allow",
|
|
164
|
+
* write: (context, action, post) =>
|
|
165
|
+
* post.authorId === context.userId ? "allow" : "deny",
|
|
166
|
+
* };
|
|
167
|
+
* ```
|
|
93
168
|
*/
|
|
94
169
|
type ActionPolicyMap<TContext extends Context, TAction extends string = string, TResource = unknown> = { [A in TAction]?: PolicyFn<TContext, A, TResource>; };
|
|
95
170
|
/**
|
|
96
171
|
* Defines the rules for each resource and action combination.
|
|
97
172
|
* Each resource key maps to an object where each action key maps to a policy function.
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* ```ts
|
|
176
|
+
* import type { Rules } from "@zap-studio/permit/types";
|
|
177
|
+
*
|
|
178
|
+
* const rules: Rules<AppContext, typeof resources, typeof actions> = {
|
|
179
|
+
* post: {
|
|
180
|
+
* read: (context, action, post) => "allow",
|
|
181
|
+
* write: (context, action, post) =>
|
|
182
|
+
* post.authorId === context.userId ? "allow" : "deny",
|
|
183
|
+
* },
|
|
184
|
+
* comment: {
|
|
185
|
+
* read: (context, action, comment) => "allow",
|
|
186
|
+
* },
|
|
187
|
+
* };
|
|
188
|
+
* ```
|
|
98
189
|
*/
|
|
99
|
-
type Rules<TContext extends Context, TResources extends Resources = Resources, TActions extends Actions<TResources> = Actions<TResources>> = { [K in keyof TResources & keyof TActions]: ActionPolicyMap<TContext, InferAction<TActions, K>, InferResource<TResources, K>>; };
|
|
190
|
+
type Rules<TContext extends Context, TResources extends Resources = Resources, TActions extends Actions<TResources> = Actions<TResources>> = { [K in keyof TResources & keyof TActions]: ActionPolicyMap<TContext, InferAction<TResources, TActions, K>, InferResource<TResources, K>>; };
|
|
100
191
|
/**
|
|
101
192
|
* Configuration object for creating a permit policy.
|
|
102
193
|
*
|
|
@@ -112,8 +203,27 @@ type Rules<TContext extends Context, TResources extends Resources = Resources, T
|
|
|
112
203
|
* ```
|
|
113
204
|
*/
|
|
114
205
|
interface PermitConfig<TContext extends Context, TResources extends Resources = Resources, TActions extends Actions<TResources> = Actions<TResources>> {
|
|
206
|
+
/**
|
|
207
|
+
* Allowed actions per resource type. Determines which `resource:action`
|
|
208
|
+
* permission strings are valid to check with `Policy.can(...)`.
|
|
209
|
+
*/
|
|
115
210
|
actions: TActions;
|
|
211
|
+
/**
|
|
212
|
+
* Optional logger for policy evaluation internals. When omitted, nothing
|
|
213
|
+
* is logged.
|
|
214
|
+
*
|
|
215
|
+
* Logs allow decisions at `debug` and deny decisions at `info`.
|
|
216
|
+
*/
|
|
217
|
+
logger?: Logger;
|
|
218
|
+
/**
|
|
219
|
+
* Standard Schema resource definitions, keyed by resource type. Each
|
|
220
|
+
* resource is validated against its schema before rules are evaluated.
|
|
221
|
+
*/
|
|
116
222
|
resources: TResources;
|
|
223
|
+
/**
|
|
224
|
+
* Policy functions for each resource/action combination, deciding
|
|
225
|
+
* `"allow"` or `"deny"` for a given context and resource.
|
|
226
|
+
*/
|
|
117
227
|
rules: Rules<TContext, TResources, TActions>;
|
|
118
228
|
}
|
|
119
229
|
/**
|
|
@@ -135,8 +245,8 @@ interface Policy<TContext extends Context, TResources extends Resources = Resour
|
|
|
135
245
|
/**
|
|
136
246
|
* Determines if the specified action is permitted on the resource in the given context.
|
|
137
247
|
*/
|
|
138
|
-
can: <K extends keyof TResources & keyof TActions>(context: TContext, permission: `${K & string}:${InferAction<TActions, K> & string}`, resource: InferResource<TResources, K>) => Promise<boolean>;
|
|
248
|
+
can: <K extends keyof TResources & keyof TActions>(context: TContext, permission: `${K & string}:${InferAction<TResources, TActions, K> & string}`, resource: InferResource<TResources, K>) => Promise<boolean>;
|
|
139
249
|
}
|
|
140
250
|
//#endregion
|
|
141
|
-
export { ActionPolicyMap, Actions, ConditionFn, Context, Decision, InferAction, InferPermission, InferResource, PermitConfig, Policy, PolicyFn, Resources, Role, RoleHierarchy, Rules };
|
|
251
|
+
export { ActionPolicyMap, Actions, ConditionFn, Context, Decision, HasRoleFn, InferAction, InferPermission, InferResource, PermitConfig, Policy, PolicyFn, Resources, Role, RoleHierarchy, Rules };
|
|
142
252
|
//# sourceMappingURL=types.d.ts.map
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;;;;;;;;;KAmBY;;;;;;;;;;KAWA,QAAQ,sBAAsB;;;;;;;;;KAU9B,KAAK,iCAAiC;;;;;;;;;;;;;;;;KAiBtC,cAAc,cAAc,OAAO,QAAQ,OAAO,OAAO;;;;;;;;;;;;;;;;KAiBzD,UAAU,wCAAwC,OAC5D,cACA;;;;;;;;;;;;;;;KAiBU,QAAQ,mBAAmB,gBACpC,WAAW;;;;;;;;;KAWF,cACV,mBAAmB,WACnB,2BAA2B,cACzB,iBAAiB,YAAY,WAAW;;;;;;;;;;KAWhC,YACV,mBAAmB,WACnB,iBAAiB,QAAQ,aACzB,gBAAgB,YACd,SAAS;;;;;;;;;;KAWD,gBACV,mBAAmB,WACnB,iBAAiB,QAAQ,kBAGvB,WAAW,mBAAmB,cAC1B,cAAc,YAAY,YAAY,UAAU,uBAChD,mBAAmB;;;;;;;;;;KAWf,SACV,iBAAiB,SACjB,iCACA,wBACG,SAAS,UAAU,QAAQ,SAAS,UAAU,cAAc;;;;;;;;;;KAWrD,YACV,iBAAiB,SACjB,iCACA,wBACG,SAAS,UAAU,QAAQ,SAAS,UAAU;;;;UAKlC;;;;;;GAOb;IAAmB,MAAM,OAAO;KAChC,iCACA,qBAEA,MAAM,OACL,YAAY,UAAU,SAAS;;;;;;;;GAShC;IAAmB,MAAM,QAAQ;KACjC,iCACA,qBACA,cAAc,OAAO,MAErB,MAAM,OACN,WAAW,cAAc,SACxB,YAAY,UAAU,SAAS;;;;;;;;;;;;;;;;;;KAmBxB,gBACV,iBAAiB,SACjB,iCACA,0BAEC,KAAK,WAAW,SAAS,UAAU,GAAG;;;;;;;;;;;;;;;;;;;;;KAuB7B,MACV,iBAAiB,SACjB,mBAAmB,YAAY,WAC/B,iBAAiB,QAAQ,cAAc,QAAQ,kBAE9C,WAAW,mBAAmB,WAAW,gBACxC,UACA,YAAY,YAAY,UAAU,IAClC,cAAc,YAAY;;;;;;;;;;;;;;;UAkBb,aACf,iBAAiB,SACjB,mBAAmB,YAAY,WAC/B,iBAAiB,QAAQ,cAAc,QAAQ;;;;;EAM/C,SAAS;;;;;;;EAOT,SAAS;;;;;EAKT,WAAW;;;;;EAKX,OAAO,MAAM,UAAU,YAAY;;;;;;;;;;;;;;;;;UAkBpB,OACf,iBAAiB,SACjB,mBAAmB,YAAY,WAC/B,iBAAiB,QAAQ,cAAc,QAAQ;;;;EAK/C,MAAM,gBAAgB,mBAAmB,UACvC,SAAS,UACT,eAAe,cAAc,YAAY,YAAY,UAAU,eAC/D,UAAU,cAAc,YAAY,OACjC"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zap-studio/permit",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"private": false,
|
|
5
|
-
"description": "A type-safe, declarative authorization library for TypeScript with Standard Schema support",
|
|
5
|
+
"description": "A type-safe, declarative, tree-shakeable authorization library for TypeScript with Standard Schema support",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"abac",
|
|
8
8
|
"access-control",
|
|
@@ -36,7 +36,6 @@
|
|
|
36
36
|
".": "./dist/index.js",
|
|
37
37
|
"./conditions": "./dist/conditions.js",
|
|
38
38
|
"./errors": "./dist/errors.js",
|
|
39
|
-
"./helpers": "./dist/helpers.js",
|
|
40
39
|
"./policy": "./dist/policy.js",
|
|
41
40
|
"./types": "./dist/types.js",
|
|
42
41
|
"./package.json": "./package.json"
|
|
@@ -45,15 +44,16 @@
|
|
|
45
44
|
"access": "public"
|
|
46
45
|
},
|
|
47
46
|
"dependencies": {
|
|
48
|
-
"@zap-studio/
|
|
47
|
+
"@zap-studio/logger": "1.0.0",
|
|
48
|
+
"@zap-studio/validation": "1.0.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
51
|
+
"tsdown": "^0.22.14",
|
|
52
|
+
"typescript": "^7.0.2",
|
|
53
|
+
"vitest": "^4.1.10",
|
|
54
|
+
"@zap-studio/typescript": "0.0.0"
|
|
55
55
|
},
|
|
56
56
|
"engines": {
|
|
57
57
|
"node": ">=18.0.0"
|
|
58
58
|
}
|
|
59
|
-
}
|
|
59
|
+
}
|
package/dist/helpers.d.ts
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
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
|
package/dist/helpers.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.d.ts","names":[],"sources":["../src/helpers.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;cA4Ba,cAAe"}
|
package/dist/helpers.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
//#region src/helpers.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
|
-
const assertNever = (value) => {
|
|
30
|
-
throw new Error(`Unexpected value: ${String(value)}`);
|
|
31
|
-
};
|
|
32
|
-
//#endregion
|
|
33
|
-
export { assertNever };
|
|
34
|
-
|
|
35
|
-
//# sourceMappingURL=helpers.js.map
|
package/dist/helpers.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
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"}
|