@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 CHANGED
@@ -1,106 +1,118 @@
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.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
- ## @zap-studio/permit@0.3.3
35
+ ## [0.3.3]
10
36
 
11
- ### Migrate to ultracite lint/format
37
+ ### Changed
12
38
 
13
39
  Internal formatting and lint cleanup only. No public API or behavior change.
14
40
 
15
- # @zap-studio/permit
41
+ ## [0.3.2]
16
42
 
17
- ## 0.3.2
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
- ### Dependencies
57
+ ### Removed
37
58
 
38
- - Updated dependency `@zap-studio/validation` to `0.3.3`.
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
- ### Minor Changes
63
+ ### Changed
43
64
 
44
- - fe60f55: Change `policy.can()` to use a single permission string plus the resource object.
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
- ### Patch Changes
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
- ### Patch Changes
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
- ### 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.
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
- ### Dependencies
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
@@ -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,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
- // 3. Define your context type
48
- type AppContext = {
49
- user: { id: string; role: "guest" | "user" | "admin" };
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
- // 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 };
60
+ const ctx: AppContext = { user: { id: "user-1" } };
61
+ const post = { id: "1", authorId: "user-1" };
72
62
 
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)
63
+ await policy.can(ctx, "post:write", post); // true, inferred as boolean
76
64
  ```
77
65
 
78
- ## API Reference
79
-
80
- ### Policy Builders
66
+ ## Declarative Policies
81
67
 
82
- #### `allow()`
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(), // Anyone can read
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
- #### `deny()`
80
+ ## Role Hierarchy Support
95
81
 
96
- Returns a policy function that always denies the action.
82
+ Via `hasRole(role, hierarchy?)`, with inheritance resolved by `collectInheritedRoles`.
97
83
 
98
84
  ```ts
99
- rules: {
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
- write: when((ctx, action, resource) => ctx.user.id === resource.authorId),
89
+ read: when(hasRole("guest", hierarchy)), // admins and users inherit guest access
114
90
  },
115
91
  }
116
92
  ```
117
93
 
118
- ### Condition Combinators
94
+ ## Composable Conditions
119
95
 
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)`
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
- #### `not(condition)`
105
+ ## Policy Merging Strategies
143
106
 
144
- Returns a condition that negates another condition.
107
+ Via `mergePoliciesAnd` and `mergePoliciesOr`.
145
108
 
146
109
  ```ts
147
- const isNotOwner = not(
148
- (ctx, action, resource) => ctx.user.id === resource.authorId
149
- );
110
+ const merged = mergePoliciesAnd(basePolicy, restrictivePolicy);
150
111
  ```
151
112
 
152
- ### Context Helpers
153
-
154
- #### `has(key, value)`
155
-
156
- Checks if a context property equals a specific value.
157
-
158
- ```ts
159
- rules: {
160
- post: {
161
- write: when(has("role", "admin")),
162
- },
163
- }
164
- ```
165
-
166
- #### `hasRole(role, hierarchy?)`
167
-
168
- Checks if the user has a specific role, with optional hierarchy support.
169
-
170
- ```ts
171
- const hierarchy = {
172
- guest: [],
173
- user: ["guest"],
174
- admin: ["user"],
175
- };
176
-
177
- rules: {
178
- post: {
179
- read: when(hasRole("guest", hierarchy)), // Admins and users inherit guest permissions
180
- },
181
- }
182
- ```
183
-
184
- ### Policy Merging
185
-
186
- #### `mergePolicies(...policies)`
187
-
188
- Merges policies with "deny-overrides" strategy. All policies must allow for the action to be permitted.
189
-
190
- ```ts
191
- const merged = mergePolicies(basePolicy, restrictivePolicy);
192
- ```
193
-
194
- #### `mergePoliciesAny(...policies)`
195
-
196
- Merges policies with "allow-overrides" strategy. Any policy allowing is sufficient.
197
-
198
- ```ts
199
- const merged = mergePoliciesAny(guestPolicy, memberPolicy);
200
- ```
201
-
202
- ## Type Helpers
203
-
204
- ### `Resources`
113
+ ## Standard Schema Support
205
114
 
206
- Type helper for defining resource schemas with `satisfies`.
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
- ### `Actions<TResources>`
124
+ ## Structured Errors
215
125
 
216
- Type helper for defining actions with `satisfies`. Ensures action keys match resource keys.
126
+ `PolicyError` for invalid configuration or evaluation failures.
217
127
 
218
128
  ```ts
219
- const actions = {
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
- Infers the TypeScript type for a specific resource.
227
-
228
- ```ts
229
- type Post = InferResource<typeof resources, "post">;
230
- // { id: string }
231
- ```
232
-
233
- ### `InferAction<TActions, K>`
234
-
235
- Infers the action union type for a specific resource.
236
-
237
- ```ts
238
- type PostAction = InferAction<typeof actions, "post">;
239
- // "read" | "write"
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
- ## Standard Schema Support
139
+ ## Logging
243
140
 
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/)
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
- // 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;
144
+ import { ConsoleLogger } from "@zap-studio/logger";
145
+ import { createPolicy } from "@zap-studio/permit";
262
146
 
263
- // With ArkType
264
- import { type } from "arktype";
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
@@ -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;;;;;;;;;;;;;;;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 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
@@ -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
- console.warn(`Resource validation failed for ${String(resourceType)}: ${String(error)}`);
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
- return policyFn(context, action, resource) === "allow";
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
- console.warn(`Policy evaluation error for ${String(resourceType)}.${action}: ${String(error)}`);
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
- 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";
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 using "deny-overrides" strategy.
124
- * If any policy denies, the merged policy denies. All must allow for the result to allow.
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 = mergePolicies(basePolicy, adminPolicy);
149
+ * const merged = mergePoliciesAnd(basePolicy, adminPolicy);
132
150
  * // Both policies must allow for the action to be permitted
133
151
  * ```
134
152
  */
135
- const mergePolicies = (...policies) => mergePoliciesWithStrategy(policies, "deny-overrides");
153
+ const mergePoliciesAnd = (...policies) => mergePoliciesWithStrategy(policies, "and");
136
154
  /**
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.
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 = mergePoliciesAny(guestPolicy, memberPolicy);
164
+ * const merged = mergePoliciesOr(guestPolicy, memberPolicy);
146
165
  * // If either policy allows, the action is permitted
147
166
  * ```
148
167
  */
149
- const mergePoliciesAny = (...policies) => mergePoliciesWithStrategy(policies, "allow-overrides");
168
+ const mergePoliciesOr = (...policies) => mergePoliciesWithStrategy(policies, "or");
150
169
  //#endregion
151
- export { createPolicy, mergePolicies, mergePoliciesAny };
170
+ export { createPolicy, mergePoliciesAnd, mergePoliciesOr };
152
171
 
153
172
  //# 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, 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<TActions extends Record<string, readonly string[]>, K extends keyof TActions> = TActions[K][number];
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
@@ -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":";;;;;;;;;;;;;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": "0.3.4",
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/validation": "workspace:*"
47
+ "@zap-studio/logger": "1.0.0",
48
+ "@zap-studio/validation": "1.0.0"
49
49
  },
50
50
  "devDependencies": {
51
- "@zap-studio/typescript": "workspace:*",
52
- "tsdown": "catalog:",
53
- "typescript": "catalog:",
54
- "vitest": "catalog:"
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
@@ -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"}