@zap-studio/permit 0.3.4 → 1.0.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 CHANGED
@@ -1,106 +1,112 @@
1
- ## @zap-studio/permit@0.3.4
1
+ # Changelog
2
2
 
3
- ### Tree-shakeable root re-exports
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.0.0]
8
+
9
+ ### Changed
10
+
11
+ - Clarified tree-shakeable design in the package description and README (no code change).
12
+
13
+ ### Removed
14
+
15
+ `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.
16
+
17
+ ### Fixed
18
+
19
+ 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.
20
+
21
+ ## [0.3.4]
22
+
23
+ ### Added
4
24
 
5
25
  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
26
 
7
27
  - 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
28
 
9
- ## @zap-studio/permit@0.3.3
29
+ ## [0.3.3]
10
30
 
11
- ### Migrate to ultracite lint/format
31
+ ### Changed
12
32
 
13
33
  Internal formatting and lint cleanup only. No public API or behavior change.
14
34
 
15
- # @zap-studio/permit
35
+ ## [0.3.2]
16
36
 
17
- ## 0.3.2
18
-
19
- ### Dependencies
37
+ ### Changed
20
38
 
21
39
  - Updated dependency `@zap-studio/validation` to `0.3.4`.
22
40
 
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.
41
+ ## [0.3.1]
29
42
 
30
43
  ### Changed
31
44
 
32
45
  - 5fa58b1: Reduced policy evaluation complexity by extracting permission parsing and merge strategy helpers without changing the `policy.can(...)` API.
33
46
  - abfda4b: Cleaned up public option typings by removing redundant `| undefined` unions from package types.
34
47
  - 9f31f87: Switched the package build to ESNext-aligned output and updated package tooling and publish metadata.
48
+ - d9ba7d1: Reworked the packaged permit skill content.
49
+ - Updated dependency `@zap-studio/validation` to `0.3.3`.
35
50
 
36
- ### Dependencies
51
+ ### Removed
37
52
 
38
- - Updated dependency `@zap-studio/validation` to `0.3.3`.
53
+ - d10d8c4: Removed the TanStack Intent-specific authoring helper from the package surface.
39
54
 
40
- ## 0.3.0
55
+ ## [0.3.0]
41
56
 
42
- ### Minor Changes
57
+ ### Changed
43
58
 
44
- - fe60f55: Change `policy.can()` to use a single permission string plus the resource object.
59
+ - fe60f55: **Breaking:** Changed `policy.can()` to use a single permission string plus the resource object.
45
60
 
46
61
  `policy.can(ctx, "read", "post", post)` is replaced by `policy.can(ctx, "post:read", post)`.
47
62
 
48
63
  This is a breaking API change in the `0.x` line. Docs and examples now use the new permission-string format consistently.
49
64
 
50
- ## 0.2.2
65
+ ## [0.2.2]
51
66
 
52
- ### Patch Changes
67
+ ### Changed
53
68
 
54
69
  - e26293e: Updated dependencies.
55
70
  - @zap-studio/validation@0.3.2
56
71
 
57
- ## 0.2.1
72
+ ## [0.2.1]
58
73
 
59
- ### Patch Changes
74
+ ### Changed
60
75
 
61
76
  - 5ea3d3b: Updated dependencies.
62
77
  - @zap-studio/validation@0.3.1
63
78
 
64
- ## 0.2.0
79
+ ## [0.2.0]
65
80
 
66
81
  ### Changed
67
82
 
68
83
  - f0f503e: Made policy evaluation asynchronous by default.
69
-
70
- ### Breaking Changes
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.
84
+ - f0f503e: **Breaking:** `policy.can(...)` now returns `Promise<boolean>`.
85
+ - 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.
86
+ - f0f503e: **Breaking:** The `Policy` interface changed so `can()` is async, and `mergePolicies`/`mergePoliciesAny` are async accordingly.
75
87
  - 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
88
  - f75b984: Updated dependency `@zap-studio/validation` to `0.3.0`.
80
89
 
81
- ## 0.1.3
90
+ ## [0.1.3]
82
91
 
83
- ### Dependencies
92
+ ### Changed
84
93
 
85
94
  - e4542bb: Updated dependency `@zap-studio/validation` to `0.2.1`.
86
95
 
87
- ## 0.1.2
96
+ ## [0.1.2]
88
97
 
89
98
  ### Changed
90
99
 
91
100
  - 2de8183: Adopted shared synchronous Standard Schema validator utilities from `@zap-studio/validation` for resource schema validation in `createPolicy`.
92
-
93
- ### Dependencies
94
-
95
101
  - 2de8183: Updated dependency `@zap-studio/validation` to `0.2.0`.
96
102
 
97
- ## 0.1.1
103
+ ## [0.1.1]
98
104
 
99
105
  ### Fixed
100
106
 
101
107
  - 907d903: Added runtime resource validation and fail-closed behavior, including deny-by-default when merges are invoked with no policies.
102
108
 
103
- ## 0.1.0
109
+ ## [0.1.0]
104
110
 
105
111
  ### Added
106
112
 
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2026 alexandretrotel
3
+ Copyright (c) 2026 Alexandre Trotel
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
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
- ## 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
5
+ Full documentation: [zapstudio.dev/permit](https://www.zapstudio.dev/permit)
13
6
 
14
7
  ## Installation
15
8
 
@@ -17,6 +10,19 @@ 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
+ - **Tree-shakeable** — policies and conditions are plain functions; unused exports are dropped by any modern bundler.
25
+
20
26
  ## Quick Start
21
27
 
22
28
  ```ts
@@ -24,113 +30,65 @@ import { z } from "zod";
24
30
  import { createPolicy, allow, deny, when } from "@zap-studio/permit";
25
31
  import type { Resources, Actions } from "@zap-studio/permit";
26
32
 
27
- // 1. Define your resource schemas
28
33
  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
- }),
34
+ post: z.object({ id: z.string(), authorId: z.string() }),
39
35
  } satisfies Resources;
40
36
 
41
- // 2. Define actions per resource
42
37
  const actions = {
43
38
  post: ["read", "write", "delete"],
44
- comment: ["read", "write"],
45
39
  } as const satisfies Actions<typeof resources>;
46
40
 
47
- // 3. Define your context type
48
- type AppContext = {
49
- user: { id: string; role: "guest" | "user" | "admin" };
50
- };
41
+ type AppContext = { user: { id: string } };
51
42
 
52
- // 4. Create your policy
53
43
  const policy = createPolicy<AppContext>({
54
44
  resources,
55
45
  actions,
56
46
  rules: {
57
47
  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
48
  read: allow(),
64
49
  write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
50
+ delete: deny(),
65
51
  },
66
52
  },
67
53
  });
68
54
 
69
- // 5. Check permissions
70
- const ctx: AppContext = { user: { id: "user-1", role: "user" } };
71
- const post = { id: "1", authorId: "user-1", visibility: "public" as const };
55
+ const ctx: AppContext = { user: { id: "user-1" } };
56
+ const post = { id: "1", authorId: "user-1" };
72
57
 
73
- await policy.can(ctx, "post:read", post); // true
74
- await policy.can(ctx, "post:write", post); // true (user is author)
75
- await policy.can(ctx, "post:delete", post); // false (always denied)
58
+ await policy.can(ctx, "post:write", post); // true, inferred as boolean
76
59
  ```
77
60
 
78
- ## API Reference
79
-
80
- ### Policy Builders
81
-
82
- #### `allow()`
61
+ ## Declarative Policies
83
62
 
84
- Returns a policy function that always allows the action.
63
+ Through `createPolicy(...)` with `allow()`, `deny()`, and `when(condition)`.
85
64
 
86
65
  ```ts
87
66
  rules: {
88
67
  post: {
89
- read: allow(), // Anyone can read
68
+ read: allow(),
69
+ delete: deny(),
70
+ write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
90
71
  },
91
72
  }
92
73
  ```
93
74
 
94
- #### `deny()`
75
+ ## Role Hierarchy Support
95
76
 
96
- Returns a policy function that always denies the action.
77
+ Via `hasRole(role, hierarchy?)`, with inheritance resolved by `collectInheritedRoles`.
97
78
 
98
79
  ```ts
99
- rules: {
100
- post: {
101
- delete: deny(), // No one can delete
102
- },
103
- }
104
- ```
80
+ const hierarchy = { guest: [], user: ["guest"], admin: ["user"] };
105
81
 
106
- #### `when(condition)`
107
-
108
- Returns a policy function that allows or denies based on a condition.
109
-
110
- ```ts
111
82
  rules: {
112
83
  post: {
113
- write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
84
+ read: when(hasRole("guest", hierarchy)), // admins and users inherit guest access
114
85
  },
115
86
  }
116
87
  ```
117
88
 
118
- ### Condition Combinators
119
-
120
- #### `and(...conditions)`
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)`
89
+ ## Composable Conditions
132
90
 
133
- Returns a condition that is true if any condition is true.
91
+ Via `and`, `or`, and `not`.
134
92
 
135
93
  ```ts
136
94
  const isOwnerOrAdmin = or(
@@ -139,132 +97,38 @@ const isOwnerOrAdmin = or(
139
97
  );
140
98
  ```
141
99
 
142
- #### `not(condition)`
143
-
144
- Returns a condition that negates another condition.
145
-
146
- ```ts
147
- const isNotOwner = not(
148
- (ctx, action, resource) => ctx.user.id === resource.authorId
149
- );
150
- ```
151
-
152
- ### Context Helpers
100
+ ## Policy Merging Strategies
153
101
 
154
- #### `has(key, value)`
155
-
156
- Checks if a context property equals a specific value.
102
+ Via `mergePoliciesAnd` and `mergePoliciesOr`.
157
103
 
158
104
  ```ts
159
- rules: {
160
- post: {
161
- write: when(has("role", "admin")),
162
- },
163
- }
105
+ const merged = mergePoliciesAnd(basePolicy, restrictivePolicy);
164
106
  ```
165
107
 
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`
108
+ ## Standard Schema Support
205
109
 
206
- Type helper for defining resource schemas with `satisfies`.
110
+ Works with Zod, Valibot, ArkType, or any compatible library.
207
111
 
208
112
  ```ts
113
+ // Zod, Valibot, ArkType, or any Standard Schema-compatible library
209
114
  const resources = {
210
115
  post: z.object({ id: z.string() }),
211
116
  } satisfies Resources;
212
117
  ```
213
118
 
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>`
119
+ ## Structured Errors
225
120
 
226
- Infers the TypeScript type for a specific resource.
121
+ `PolicyError` for invalid configuration or evaluation failures.
227
122
 
228
123
  ```ts
229
- type Post = InferResource<typeof resources, "post">;
230
- // { id: string }
231
- ```
124
+ import { PolicyError } from "@zap-studio/permit";
232
125
 
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;
126
+ try {
127
+ const policy = createPolicy(config);
128
+ await policy.can(ctx, "post:read", post);
129
+ } catch (error) {
130
+ if (error instanceof PolicyError) console.error(error.message);
131
+ }
268
132
  ```
269
133
 
270
134
  ## Runtime Support
@@ -278,3 +142,7 @@ const resources = {
278
142
  | Browsers | Latest evergreen (Chrome, Edge, Firefox, Safari) |
279
143
 
280
144
  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`).
145
+
146
+ ## License
147
+
148
+ MIT
@@ -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.
@@ -1 +1 @@
1
- {"version":3,"file":"conditions.d.ts","names":[],"sources":["../src/conditions.ts"],"mappings":";;;;;;;;;;;;;;;;;;cA+Ba,QAET,iBAAiB,SACjB,iCACA,0BACG,SAAS,UAAU,SAAS;;;;;;;;;;;;;;;;;cAoBtB,OAET,iBAAiB,SACjB,iCACA,0BACG,SAAS,UAAU,SAAS;;;;;;;;;;;;;;;;;cAoBtB,OAET,iBAAiB,SACjB,iCACA,qBAEA,WAAW,YAAY,UAAU,SAAS,eACzC,SAAS,UAAU,SAAS;;;;;;;;;;;;;;;;;;cAqBpB,MAET,iBAAiB,SACjB,iCACA,wBAEG,YAAY,YAAY,UAAU,SAAS,iBAC7C,YAAY,UAAU,SAAS;;;;;;;;;;;;;;;;;;cAqBvB,KAET,iBAAiB,SACjB,iCACA,wBAEG,YAAY,YAAY,UAAU,SAAS,iBAC7C,YAAY,UAAU,SAAS;;;;;;;;;;;;;;;cAkBvB,MAET,iBAAiB,SACjB,iCACA,qBAEA,WAAW,YAAY,UAAU,SAAS,eACzC,YAAY,UAAU,SAAS;;;;;;;;;;;;;cAgBvB,MACV,iBAAiB,SAAS,gBAAgB,UACzC,KAAK,GACL,OAAO,SAAS,OACf,YAAY;;;;;;;;;;;;;;;;;;cAqBJ,wBAAyB,cAAc,OAAO,MACzD,OAAO,SACP,WAAW,cAAc,WACxB,IAAI;;;;UAwBG;GAEN;IAAmB,MAAM,OAAO;KAChC,iCACA,qBAEA,MAAM,OACL,YAAY,UAAU,SAAS;GAEhC;IAAmB,MAAM,QAAQ;KACjC,iCACA,qBACA,cAAc,OAAO,MAErB,MAAM,OACN,WAAW,cAAc,SACxB,YAAY,UAAU,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA8BvB,SAAS"}
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"}
@@ -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 parents = hierarchy[role] ?? [];
140
- for (const parent of parents) add(parent);
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;
@@ -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 parents = hierarchy[role] ?? [];\n for (const parent of parents) {\n add(parent);\n }\n };\n\n for (const role of roles) {\n add(role);\n }\n return inherited;\n};\n\n/**\n * Call signatures for {@link hasRole}, preserving the with/without hierarchy overloads.\n */\ninterface HasRoleFn {\n <\n TContext extends { role: Role | Role[] },\n TAction extends string = string,\n TResource = unknown,\n >(\n role: Role\n ): ConditionFn<TContext, TAction, TResource>;\n <\n TContext extends { role: TRole | TRole[] },\n TAction extends string = string,\n TResource = unknown,\n TRole extends Role = Role,\n >(\n role: TRole,\n hierarchy: RoleHierarchy<TRole>\n ): ConditionFn<TContext, TAction, TResource>;\n}\n\n/**\n * Returns a condition function that checks if the user has a specific role.\n * Supports role hierarchy for inherited permissions.\n *\n * @example\n * ```ts\n * // Without hierarchy\n * rules: {\n * post: {\n * delete: when(hasRole(\"admin\")),\n * },\n * }\n *\n * // With hierarchy\n * const hierarchy = {\n * guest: [],\n * user: [\"guest\"],\n * admin: [\"user\"],\n * };\n *\n * rules: {\n * post: {\n * read: when(hasRole(\"guest\", hierarchy)), // Admins and users can also read\n * },\n * }\n * ```\n */\nexport const hasRole: HasRoleFn =\n (\n role: Role,\n hierarchy?: RoleHierarchy\n ): ConditionFn<{ role: Role | Role[] }> =>\n (context) => {\n const userRoles = Array.isArray(context.role)\n ? context.role\n : [context.role];\n\n if (hierarchy === undefined) {\n return userRoles.includes(role);\n }\n\n const inherited = collectInheritedRoles(userRoles, hierarchy);\n return inherited.has(role);\n };\n"],"mappings":";;;;;;;;;;;;;;;;;AA+BA,MAAa,oBAOT;;;;;;;;;;;;;;;;;AAkBJ,MAAa,mBAOT;;;;;;;;;;;;;;;;;AAkBJ,MAAa,QAMT,eAED,SAAS,QAAQ,aAChB,UAAU,SAAS,QAAQ,QAAQ,IAAI,UAAU;;;;;;;;;;;;;;;;;;AAmBrD,MAAa,OAMT,GAAG,gBAEJ,SAAS,QAAQ,aAChB,WAAW,OAAO,cAAc,UAAU,SAAS,QAAQ,QAAQ,CAAC;;;;;;;;;;;;;;;;;;AAmBxE,MAAa,MAMT,GAAG,gBAEJ,SAAS,QAAQ,aAChB,WAAW,MAAM,cAAc,UAAU,SAAS,QAAQ,QAAQ,CAAC;;;;;;;;;;;;;;;AAgBvE,MAAa,OAMT,eAED,SAAS,QAAQ,aAChB,CAAC,UAAU,SAAS,QAAQ,QAAQ;;;;;;;;;;;;;AAcxC,MAAa,OAET,KACA,WAED,YACC,QAAQ,SAAS;;;;;;;;;;;;;;;;;;AAmBrB,MAAa,yBACX,OACA,cACe;CACf,MAAM,4BAAY,IAAI,IAAW;CAEjC,MAAM,OAAO,SAAsB;EACjC,IAAI,UAAU,IAAI,IAAI,GACpB;EAGF,UAAU,IAAI,IAAI;EAClB,MAAM,UAAU,UAAU,SAAS,CAAC;EACpC,KAAK,MAAM,UAAU,SACnB,IAAI,MAAM;CAEd;CAEA,KAAK,MAAM,QAAQ,OACjB,IAAI,IAAI;CAEV,OAAO;AACT;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDA,MAAa,WAET,MACA,eAED,YAAY;CACX,MAAM,YAAY,MAAM,QAAQ,QAAQ,IAAI,IACxC,QAAQ,OACR,CAAC,QAAQ,IAAI;CAEjB,IAAI,cAAc,KAAA,GAChB,OAAO,UAAU,SAAS,IAAI;CAIhC,OADkB,sBAAsB,WAAW,SACpC,CAAC,CAAC,IAAI,IAAI;AAC3B"}
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
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","names":[],"sources":["../src/errors.ts"],"mappings":";;;;;;;;;;cAUa,oBAAoB;;;;;;EAM/B,YAAY"}
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
  /**
@@ -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":";;;;;;;;;;AAUA,IAAa,cAAb,cAAiC,MAAM;;;;;;CAMrC,YAAY,SAAiB;EAC3B,MAAM,OAAO;EACb,KAAK,OAAO;CACd;AACF"}
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 { assertNever } from "./helpers.js";
5
- import { createPolicy, mergePolicies, mergePoliciesAny } from "./policy.js";
6
- export { type ActionPolicyMap, type Actions, type ConditionFn, type Context, type Decision, type InferAction, type InferPermission, type InferResource, type PermitConfig, type Policy, PolicyError, type PolicyFn, type Resources, type Role, type RoleHierarchy, type Rules, allow, and, assertNever, collectInheritedRoles, createPolicy, deny, has, hasRole, mergePolicies, mergePoliciesAny, not, or, when };
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 { assertNever } from "./helpers.js";
4
- import { createPolicy, mergePolicies, mergePoliciesAny } from "./policy.js";
5
- export { PolicyError, allow, and, assertNever, collectInheritedRoles, createPolicy, deny, has, hasRole, mergePolicies, mergePoliciesAny, not, or, when };
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 using "deny-overrides" strategy.
61
- * If any policy denies, the merged policy denies. All must allow for the result to allow.
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 = mergePolicies(basePolicy, adminPolicy);
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 mergePolicies: <TContext extends Context, TResources extends Resources = Resources, TActions extends Actions<TResources> = Actions<TResources>>(...policies: Policy<TContext, TResources, TActions>[]) => Policy<TContext, TResources, TActions>;
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 using "allow-overrides" strategy.
75
- * If any policy allows, the merged policy allows. All must deny for the result to deny.
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 = mergePoliciesAny(guestPolicy, memberPolicy);
84
+ * const merged = mergePoliciesOr(guestPolicy, memberPolicy);
83
85
  * // If either policy allows, the action is permitted
84
86
  * ```
85
87
  */
86
- declare const mergePoliciesAny: <TContext extends Context, TResources extends Resources = Resources, TActions extends Actions<TResources> = Actions<TResources>>(...policies: Policy<TContext, TResources, TActions>[]) => Policy<TContext, TResources, TActions>;
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, mergePolicies, mergePoliciesAny };
90
+ export { createPolicy, mergePoliciesAnd, mergePoliciesOr };
89
91
  //# sourceMappingURL=policy.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"policy.d.ts","names":[],"sources":["../src/policy.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAyGa,eACX,iBAAiB,SACjB,mBAAmB,YAAY,WAC/B,iBAAiB,QAAQ,cAAc,QAAQ,aAE/C,QAAQ,aAAa,UAAU,YAAY,cAC1C,OAAO,UAAU,YAAY;;;;;;;;;;;;;;cA2InB,gBACX,iBAAiB,SACjB,mBAAmB,YAAY,WAC/B,iBAAiB,QAAQ,cAAc,QAAQ,gBAE5C,UAAU,OAAO,UAAU,YAAY,gBACzC,OAAO,UAAU,YAAY;;;;;;;;;;;;;;cAgBnB,mBACX,iBAAiB,SACjB,mBAAmB,YAAY,WAC/B,iBAAiB,QAAQ,cAAc,QAAQ,gBAE5C,UAAU,OAAO,UAAU,YAAY,gBACzC,OAAO,UAAU,YAAY"}
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;;;;;;;;;;;;;;;cAgJnB,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 segments).
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
@@ -101,7 +103,7 @@ const createPolicy = (config) => {
101
103
  validators.set(key, async (input) => await validator(input));
102
104
  }
103
105
  return { async can(context, permission, resource) {
104
- const parsedPermission = parsePermission(permission);
106
+ const parsedPermission = parsePermission(permission, actions);
105
107
  if (parsedPermission === null) return false;
106
108
  const { action, resourceType } = parsedPermission;
107
109
  if (!hasAllowedAction(resourceType, action)) return false;
@@ -112,42 +114,43 @@ const createPolicy = (config) => {
112
114
  };
113
115
  const mergePoliciesWithStrategy = (policies, strategy) => ({ async can(context, permission, resource) {
114
116
  if (policies.length === 0) return false;
115
- for (const policy of policies) {
116
- const allowed = await policy.can(context, permission, resource);
117
- if (strategy === "allow-overrides" && allowed) return true;
118
- if (strategy === "deny-overrides" && !allowed) return false;
119
- }
120
- return strategy === "deny-overrides";
117
+ const results = (await Promise.allSettled(policies.map(async (policy) => await policy.can(context, permission, resource)))).map((result) => {
118
+ if (result.status === "fulfilled") return result.value;
119
+ return false;
120
+ });
121
+ return strategy === "and" ? results.every(Boolean) : results.some(Boolean);
121
122
  } });
122
123
  /**
123
- * Merges multiple policies into one using "deny-overrides" strategy.
124
- * If any policy denies, the merged policy denies. All must allow for the result to allow.
124
+ * Merges multiple policies into one, requiring every policy to allow.
125
+ * If any policy denies, the merged policy denies. Policies are evaluated
126
+ * in parallel; every policy is invoked regardless of outcome.
125
127
  *
126
128
  * @example
127
129
  * ```ts
128
130
  * const basePolicy = createPolicy({ ... });
129
131
  * const adminPolicy = createPolicy({ ... });
130
132
  *
131
- * const merged = mergePolicies(basePolicy, adminPolicy);
133
+ * const merged = mergePoliciesAnd(basePolicy, adminPolicy);
132
134
  * // Both policies must allow for the action to be permitted
133
135
  * ```
134
136
  */
135
- const mergePolicies = (...policies) => mergePoliciesWithStrategy(policies, "deny-overrides");
137
+ const mergePoliciesAnd = (...policies) => mergePoliciesWithStrategy(policies, "and");
136
138
  /**
137
- * Merges multiple policies into one using "allow-overrides" strategy.
138
- * If any policy allows, the merged policy allows. All must deny for the result to deny.
139
+ * Merges multiple policies into one, requiring at least one policy to allow.
140
+ * If every policy denies, the merged policy denies. Policies are evaluated
141
+ * in parallel; every policy is invoked regardless of outcome.
139
142
  *
140
143
  * @example
141
144
  * ```ts
142
145
  * const guestPolicy = createPolicy({ ... });
143
146
  * const memberPolicy = createPolicy({ ... });
144
147
  *
145
- * const merged = mergePoliciesAny(guestPolicy, memberPolicy);
148
+ * const merged = mergePoliciesOr(guestPolicy, memberPolicy);
146
149
  * // If either policy allows, the action is permitted
147
150
  * ```
148
151
  */
149
- const mergePoliciesAny = (...policies) => mergePoliciesWithStrategy(policies, "allow-overrides");
152
+ const mergePoliciesOr = (...policies) => mergePoliciesWithStrategy(policies, "or");
150
153
  //#endregion
151
- export { createPolicy, mergePolicies, mergePoliciesAny };
154
+ export { createPolicy, mergePoliciesAnd, mergePoliciesOr };
152
155
 
153
156
  //# sourceMappingURL=policy.js.map
@@ -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 } = 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<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 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<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,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,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
@@ -4,15 +4,30 @@ import { StandardSchemaV1 } from "@zap-studio/validation";
4
4
  * Represents the possible outcomes of a policy decision.
5
5
  * - "allow": The action is permitted.
6
6
  * - "deny": The action is not permitted.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * const decision: Decision = "allow";
11
+ * ```
7
12
  */
8
13
  type Decision = "allow" | "deny";
9
14
  /**
10
15
  * Represents the context in which a policy decision is made.
11
16
  * Can include user information, environment, or any relevant data.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * type AppContext = Context<{ user: { id: string; role: string } }>;
21
+ * ```
12
22
  */
13
23
  type Context<TContext = unknown> = TContext;
14
24
  /**
15
25
  * Represents a role within the system.
26
+ *
27
+ * @example
28
+ * ```ts
29
+ * type AppRole = Role<"guest" | "user" | "admin">;
30
+ * ```
16
31
  */
17
32
  type Role<TRole extends string = string> = TRole;
18
33
  /**
@@ -64,12 +79,23 @@ type Resources<TResourceKey extends string = string> = Record<TResourceKey, Stan
64
79
  type Actions<TResources extends Resources> = { [K in keyof TResources]: readonly string[]; };
65
80
  /**
66
81
  * Infers the output type from a Standard Schema.
82
+ *
83
+ * @example
84
+ * ```ts
85
+ * type Post = InferResource<typeof resources, "post">;
86
+ * ```
67
87
  */
68
88
  type InferResource<TResources extends Resources, TResourceKey extends keyof TResources> = StandardSchemaV1.InferOutput<TResources[TResourceKey]>;
69
89
  /**
70
90
  * Infers the action union type for a specific resource.
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * type PostAction = InferAction<typeof resources, typeof actions, "post">;
95
+ * // "read" | "write" | "delete"
96
+ * ```
71
97
  */
72
- type InferAction<TActions extends Record<string, readonly string[]>, K extends keyof TActions> = TActions[K][number];
98
+ type InferAction<TResources extends Resources, TActions extends Actions<TResources>, K extends keyof TActions> = TActions[K][number];
73
99
  /**
74
100
  * Infers the permission-string union for all resource/action combinations.
75
101
  *
@@ -79,24 +105,76 @@ type InferAction<TActions extends Record<string, readonly string[]>, K extends k
79
105
  * // "post:read" | "post:write" | "comment:read"
80
106
  * ```
81
107
  */
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];
108
+ 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
109
  /**
84
110
  * A function that determines whether a given action on a resource is allowed in a specific context.
111
+ *
112
+ * @example
113
+ * ```ts
114
+ * const readPolicy: PolicyFn<AppContext, "read", Post> = (context, action, post) =>
115
+ * post.visibility === "public" ? "allow" : "deny";
116
+ * ```
85
117
  */
86
118
  type PolicyFn<TContext extends Context, TAction extends string = string, TResource = unknown> = (context: TContext, action: TAction, resource: TResource) => Decision;
87
119
  /**
88
120
  * A function that evaluates a condition for a given action and resource in a specific context.
121
+ *
122
+ * @example
123
+ * ```ts
124
+ * const isOwner: ConditionFn<AppContext, "write", Post> = (context, action, post) =>
125
+ * context.user.id === post.authorId;
126
+ * ```
89
127
  */
90
128
  type ConditionFn<TContext extends Context, TAction extends string = string, TResource = unknown> = (context: TContext, action: TAction, resource: TResource) => boolean;
129
+ /**
130
+ * Call signatures for {@link hasRole}, preserving the with/without hierarchy overloads.
131
+ */
132
+ interface HasRoleFn {
133
+ <TContext extends {
134
+ role: Role | Role[];
135
+ }, TAction extends string = string, TResource = unknown>(role: Role): ConditionFn<TContext, TAction, TResource>;
136
+ <TContext extends {
137
+ role: TRole | TRole[];
138
+ }, TAction extends string = string, TResource = unknown, TRole extends Role = Role>(role: TRole, hierarchy: RoleHierarchy<TRole>): ConditionFn<TContext, TAction, TResource>;
139
+ }
91
140
  /**
92
141
  * Maps actions to their corresponding policy functions for a specific resource.
142
+ *
143
+ * @example
144
+ * ```ts
145
+ * import type { ActionPolicyMap } from "@zap-studio/permit/types";
146
+ *
147
+ * type PostActions = "read" | "write" | "delete";
148
+ *
149
+ * const postPolicies: ActionPolicyMap<AppContext, PostActions, Post> = {
150
+ * read: (context, action, post) => "allow",
151
+ * write: (context, action, post) =>
152
+ * post.authorId === context.userId ? "allow" : "deny",
153
+ * };
154
+ * ```
93
155
  */
94
156
  type ActionPolicyMap<TContext extends Context, TAction extends string = string, TResource = unknown> = { [A in TAction]?: PolicyFn<TContext, A, TResource>; };
95
157
  /**
96
158
  * Defines the rules for each resource and action combination.
97
159
  * Each resource key maps to an object where each action key maps to a policy function.
160
+ *
161
+ * @example
162
+ * ```ts
163
+ * import type { Rules } from "@zap-studio/permit/types";
164
+ *
165
+ * const rules: Rules<AppContext, typeof resources, typeof actions> = {
166
+ * post: {
167
+ * read: (context, action, post) => "allow",
168
+ * write: (context, action, post) =>
169
+ * post.authorId === context.userId ? "allow" : "deny",
170
+ * },
171
+ * comment: {
172
+ * read: (context, action, comment) => "allow",
173
+ * },
174
+ * };
175
+ * ```
98
176
  */
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>>; };
177
+ 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
178
  /**
101
179
  * Configuration object for creating a permit policy.
102
180
  *
@@ -135,8 +213,8 @@ interface Policy<TContext extends Context, TResources extends Resources = Resour
135
213
  /**
136
214
  * Determines if the specified action is permitted on the resource in the given context.
137
215
  */
138
- can: <K extends keyof TResources & keyof TActions>(context: TContext, permission: `${K & string}:${InferAction<TActions, K> & string}`, resource: InferResource<TResources, K>) => Promise<boolean>;
216
+ can: <K extends keyof TResources & keyof TActions>(context: TContext, permission: `${K & string}:${InferAction<TResources, TActions, K> & string}`, resource: InferResource<TResources, K>) => Promise<boolean>;
139
217
  }
140
218
  //#endregion
141
- export { ActionPolicyMap, Actions, ConditionFn, Context, Decision, InferAction, InferPermission, InferResource, PermitConfig, Policy, PolicyFn, Resources, Role, RoleHierarchy, Rules };
219
+ export { ActionPolicyMap, Actions, ConditionFn, Context, Decision, HasRoleFn, InferAction, InferPermission, InferResource, PermitConfig, Policy, PolicyFn, Resources, Role, RoleHierarchy, Rules };
142
220
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;;;KAaY;;;;;KAMA,QAAQ,sBAAsB;;;;KAK9B,KAAK,iCAAiC;;;;;;;;;;;;;;;;KAiBtC,cAAc,cAAc,OAAO,QAAQ,OAAO,OAAO;;;;;;;;;;;;;;;;KAiBzD,UAAU,wCAAwC,OAC5D,cACA;;;;;;;;;;;;;;;KAiBU,QAAQ,mBAAmB,gBACpC,WAAW;;;;KAMF,cACV,mBAAmB,WACnB,2BAA2B,cACzB,iBAAiB,YAAY,WAAW;;;;KAKhC,YACV,iBAAiB,mCACjB,gBAAgB,YACd,SAAS;;;;;;;;;;KAWD,gBACV,mBAAmB,WACnB,iBAAiB,QAAQ,kBAExB,WAAW,mBACJ,cAAc,cAAc,YAAY,UAAU,uBACpD,mBAAmB;;;;KAKf,SACV,iBAAiB,SACjB,iCACA,wBACG,SAAS,UAAU,QAAQ,SAAS,UAAU,cAAc;;;;KAKrD,YACV,iBAAiB,SACjB,iCACA,wBACG,SAAS,UAAU,QAAQ,SAAS,UAAU;;;;KAKvC,gBACV,iBAAiB,SACjB,iCACA,0BAEC,KAAK,WAAW,SAAS,UAAU,GAAG;;;;;KAO7B,MACV,iBAAiB,SACjB,mBAAmB,YAAY,WAC/B,iBAAiB,QAAQ,cAAc,QAAQ,kBAE9C,WAAW,mBAAmB,WAAW,gBACxC,UACA,YAAY,UAAU,IACtB,cAAc,YAAY;;;;;;;;;;;;;;;UAkBb,aACf,iBAAiB,SACjB,mBAAmB,YAAY,WAC/B,iBAAiB,QAAQ,cAAc,QAAQ;EAE/C,SAAS;EACT,WAAW;EACX,OAAO,MAAM,UAAU,YAAY;;;;;;;;;;;;;;;;;UAkBpB,OACf,iBAAiB,SACjB,mBAAmB,YAAY,WAC/B,iBAAiB,QAAQ,cAAc,QAAQ;;;;EAK/C,MAAM,gBAAgB,mBAAmB,UACvC,SAAS,UACT,eAAe,cAAc,YAAY,UAAU,eACnD,UAAU,cAAc,YAAY,OACjC"}
1
+ {"version":3,"file":"types.d.ts","names":[],"sources":["../src/types.ts"],"mappings":";;;;;;;;;;;;KAkBY;;;;;;;;;;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;GAEb;IAAmB,MAAM,OAAO;KAChC,iCACA,qBAEA,MAAM,OACL,YAAY,UAAU,SAAS;GAEhC;IAAmB,MAAM,QAAQ;KACjC,iCACA,qBACA,cAAc,OAAO,MAErB,MAAM,OACN,WAAW,cAAc,SACxB,YAAY,UAAU,SAAS;;;;;;;;;;;;;;;;;;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;EAE/C,SAAS;EACT,WAAW;EACX,OAAO,MAAM,UAAU,YAAY;;;;;;;;;;;;;;;;;UAkBpB,OACf,iBAAiB,SACjB,mBAAmB,YAAY,WAC/B,iBAAiB,QAAQ,cAAc,QAAQ;;;;EAK/C,MAAM,gBAAgB,mBAAmB,UACvC,SAAS,UACT,eAAe,cAAc,YAAY,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": "0.3.4",
3
+ "version": "1.0.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,15 @@
45
44
  "access": "public"
46
45
  },
47
46
  "dependencies": {
48
- "@zap-studio/validation": "workspace:*"
47
+ "@zap-studio/validation": "1.0.0"
49
48
  },
50
49
  "devDependencies": {
51
- "@zap-studio/typescript": "workspace:*",
52
- "tsdown": "catalog:",
53
- "typescript": "catalog:",
54
- "vitest": "catalog:"
50
+ "tsdown": "^0.22.14",
51
+ "typescript": "^7.0.2",
52
+ "vitest": "^4.1.10",
53
+ "@zap-studio/typescript": "0.0.0"
55
54
  },
56
55
  "engines": {
57
56
  "node": ">=18.0.0"
58
57
  }
59
- }
58
+ }
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
@@ -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
@@ -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"}