@cfast/permissions 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Daniel Schmidt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,321 @@
1
+ # @cfast/permissions
2
+
3
+ **Define permissions once. Enforce everywhere. No duplication between what you check and what you execute.**
4
+
5
+ `@cfast/permissions` is an isomorphic, Drizzle-native permission system that brings application-level row-level security to Cloudflare D1. It draws inspiration from [CASL](https://casl.js.org/)'s `can(action, subject)` mental model but goes further: permissions are not just boolean checks, they're Drizzle `where` clauses that filter data at the query level.
6
+
7
+ This is the foundation of the cfast permission story. You define your permissions here. `@cfast/db` enforces them as lazy operations. `@cfast/actions` composes them across multi-step workflows. The same permission definitions that guard your database queries also tell your UI which buttons to show — with zero duplication.
8
+
9
+ ## Design Goals
10
+
11
+ - **Isomorphic.** The same permission definitions work on client and server. No duplication, no drift.
12
+ - **Drizzle-native.** Permissions compile down to Drizzle `where` clauses. They don't sit alongside your queries, they *become* your queries.
13
+ - **Type-safe.** Roles, actions, and subjects are all type-checked. If you misspell a permission, TypeScript tells you.
14
+ - **D1-first.** Cloudflare D1 (SQLite) has no native RLS. This library provides application-level RLS with the same guarantees.
15
+ - **Zero boilerplate.** You never write a permission check and then repeat the same logic in your query. The operation *is* the permission declaration.
16
+
17
+ ## API
18
+
19
+ ### `definePermissions(config)`
20
+
21
+ Creates a permission configuration that can be shared between `@cfast/db` (server-side enforcement) and `@cfast/actions` (client-side introspection).
22
+
23
+ ```typescript
24
+ import { definePermissions, grant } from "@cfast/permissions";
25
+ import { eq } from "drizzle-orm";
26
+ import { posts, comments, users, auditLogs } from "./schema";
27
+
28
+ export const permissions = definePermissions({
29
+ roles: ["anonymous", "user", "editor", "admin"] as const,
30
+
31
+ grants: {
32
+ anonymous: [
33
+ grant("read", posts, { where: (post) => eq(post.published, true) }),
34
+ grant("read", comments),
35
+ ],
36
+
37
+ user: [
38
+ grant("read", posts, { where: (post) => eq(post.published, true) }),
39
+ grant("create", posts),
40
+ grant("update", posts, { where: (post, user) => eq(post.authorId, user.id) }),
41
+ grant("delete", posts, { where: (post, user) => eq(post.authorId, user.id) }),
42
+ grant("create", comments),
43
+ grant("delete", comments, { where: (comment, user) => eq(comment.authorId, user.id) }),
44
+ ],
45
+
46
+ editor: [
47
+ grant("read", posts),
48
+ grant("update", posts),
49
+ grant("create", posts),
50
+ grant("delete", posts),
51
+ grant("manage", comments),
52
+ ],
53
+
54
+ admin: [
55
+ grant("manage", "all"),
56
+ ],
57
+ },
58
+ });
59
+ ```
60
+
61
+ **Parameters:**
62
+
63
+ | Field | Type | Description |
64
+ |---|---|---|
65
+ | `roles` | `readonly string[]` | All roles in your application, declared with `as const` for type inference. |
66
+ | `grants` | `Record<Role, Grant[]>` | A map from role to an array of `grant()` calls. Every role must be represented. |
67
+ | `hierarchy` | `Partial<Record<Role, Role[]>>` | Optional. Declares which roles inherit from which. Not every role needs an entry. See [Role Hierarchy](#role-hierarchy). |
68
+
69
+ **Returns:** A `Permissions` object that you pass to `createDb()` and can import on the client.
70
+
71
+ ### `grant(action, subject, options?)`
72
+
73
+ Declares that a role can perform `action` on `subject`, optionally restricted by a `where` clause.
74
+
75
+ **Parameters:**
76
+
77
+ | Field | Type | Description |
78
+ |---|---|---|
79
+ | `action` | `"read" \| "create" \| "update" \| "delete" \| "manage"` | The operation being permitted. `"manage"` is shorthand for all four CRUD actions. |
80
+ | `subject` | `Table \| "all"` | A Drizzle table reference, or `"all"` to apply to every table. |
81
+ | `options.where` | `(columns, user) => SQL \| undefined` | Optional. A Drizzle filter expression that restricts which rows this grant applies to. Compiles to a SQL `WHERE` clause at query time. |
82
+
83
+ **Action semantics:**
84
+
85
+ | Action | Maps to | Used by |
86
+ |---|---|---|
87
+ | `"read"` | `SELECT` queries | `db.query()` — adds the `where` clause to filter results |
88
+ | `"create"` | `INSERT` statements | `db.insert()` — boolean check before execution |
89
+ | `"update"` | `UPDATE` statements | `db.update()` — boolean check + optional row-level `where` |
90
+ | `"delete"` | `DELETE` statements | `db.delete()` — boolean check + optional row-level `where` |
91
+ | `"manage"` | All of the above | Shorthand for granting full CRUD access |
92
+
93
+ **The `where` clause:**
94
+
95
+ The `where` function receives two arguments:
96
+
97
+ 1. **`row`** — A reference to the table's columns. Use this to build Drizzle filter expressions.
98
+ 2. **`user`** — The current user object (from `@cfast/auth`). Use this for ownership checks.
99
+
100
+ ```typescript
101
+ // Only allow users to update their own posts
102
+ grant("update", posts, {
103
+ where: (post, user) => eq(post.authorId, user.id),
104
+ });
105
+
106
+ // Only allow reading published posts
107
+ grant("read", posts, {
108
+ where: (post) => eq(post.published, true),
109
+ });
110
+ ```
111
+
112
+ A `grant` without a `where` clause means the permission applies to all rows:
113
+
114
+ ```typescript
115
+ // Editors can read all posts, regardless of published status
116
+ grant("read", posts),
117
+ ```
118
+
119
+ **How `where` clauses are applied:**
120
+
121
+ - For `"read"` grants: the `where` clause is automatically appended to every `SELECT` query on that table. If the user has multiple read grants on the same table (e.g., from role hierarchy), they are `OR`'d together.
122
+ - For `"update"` and `"delete"` grants: the `where` clause is checked against the target rows. If the mutation affects rows outside the permitted set, a `ForbiddenError` is thrown.
123
+ - For `"create"` grants: `where` is not applicable (there's no existing row to filter). Create grants are boolean — you either can or can't.
124
+
125
+ ### `PermissionDescriptor`
126
+
127
+ The structural representation of a permission requirement. This is what `Operation.permissions` returns (see `@cfast/db`).
128
+
129
+ ```typescript
130
+ type PermissionDescriptor = {
131
+ action: "read" | "create" | "update" | "delete" | "manage";
132
+ table: Table;
133
+ };
134
+ ```
135
+
136
+ Permission descriptors are **structural, not value-dependent**. They describe *what kind* of operation is being performed on *which table*, not *which specific rows*. This is what makes it possible to inspect permissions without providing concrete parameter values.
137
+
138
+ ```typescript
139
+ // This operation's permissions can be inspected without knowing the postId:
140
+ const updatePost = db.update(posts)
141
+ .set({ published: true })
142
+ .where(eq(posts.id, sql.placeholder("postId")));
143
+
144
+ updatePost.permissions;
145
+ // → [{ action: "update", table: posts }]
146
+ // No postId needed to know this requires "update" on "posts"
147
+ ```
148
+
149
+ ### `checkPermissions(role, permissions, descriptors)`
150
+
151
+ Checks whether a role satisfies a set of permission descriptors. Returns a result object with details about which permissions passed and which failed.
152
+
153
+ ```typescript
154
+ import { checkPermissions } from "@cfast/permissions";
155
+
156
+ const result = checkPermissions("user", permissions, [
157
+ { action: "update", table: posts },
158
+ { action: "create", table: auditLogs },
159
+ ]);
160
+
161
+ result.permitted; // boolean — true only if ALL descriptors are satisfied
162
+ result.denied; // PermissionDescriptor[] — which ones failed
163
+ result.reasons; // string[] — human-readable reasons for each denial
164
+ ```
165
+
166
+ This is the low-level checking function. Most users will never call it directly — `Operation.run()` calls it internally, and `@cfast/actions` uses it to pre-compute `permitted` booleans for the client.
167
+
168
+ **When you might use it directly:**
169
+
170
+ - Building custom middleware that needs to check permissions outside of a database operation
171
+ - Admin UIs that display permission matrices
172
+ - Testing: asserting that a role has the expected permissions
173
+
174
+ ### Role Hierarchy
175
+
176
+ Roles can inherit from other roles to avoid repetition:
177
+
178
+ ```typescript
179
+ export const permissions = definePermissions({
180
+ roles: ["anonymous", "user", "editor", "admin"] as const,
181
+
182
+ hierarchy: {
183
+ user: ["anonymous"], // users can do everything anonymous can
184
+ editor: ["user"], // editors can do everything users can
185
+ admin: ["editor"], // admins can do everything editors can
186
+ },
187
+
188
+ grants: {
189
+ anonymous: [
190
+ grant("read", posts, { where: (post) => eq(post.published, true) }),
191
+ ],
192
+
193
+ // Only define the *additional* permissions per role
194
+ user: [
195
+ grant("create", posts),
196
+ grant("update", posts, { where: (post, user) => eq(post.authorId, user.id) }),
197
+ grant("delete", posts, { where: (post, user) => eq(post.authorId, user.id) }),
198
+ ],
199
+
200
+ editor: [
201
+ // Editors inherit "read published" from anonymous (via user),
202
+ // but this unrestricted grant takes precedence
203
+ grant("read", posts),
204
+ grant("update", posts),
205
+ grant("delete", posts),
206
+ ],
207
+
208
+ admin: [
209
+ grant("manage", "all"),
210
+ ],
211
+ },
212
+ });
213
+ ```
214
+
215
+ **Resolution rules:**
216
+
217
+ 1. A role's effective grants = its own grants + all grants from roles it inherits from (recursively).
218
+ 2. When multiple grants apply to the same action+table, their `where` clauses are `OR`'d. This means a more permissive grant always wins — if an editor inherits `read posts WHERE published = true` from user but also has `read posts` (no filter), the editor sees all posts.
219
+ 3. `grant("manage", "all")` on any role in the hierarchy means that role can do everything. Period.
220
+ 4. Circular hierarchies are detected at runtime and throw an `Error` (e.g., `"Circular role hierarchy detected: 'editor' inherits from itself"`).
221
+
222
+ ### `ForbiddenError`
223
+
224
+ Thrown when a permission check fails during `Operation.run()`.
225
+
226
+ ```typescript
227
+ import { ForbiddenError } from "@cfast/permissions";
228
+
229
+ try {
230
+ await deletePostOp.run({ postId: "abc" });
231
+ } catch (err) {
232
+ if (err instanceof ForbiddenError) {
233
+ err.action; // "delete"
234
+ err.table; // posts table reference
235
+ err.role; // "user"
236
+ err.message; // "Role 'user' cannot delete on 'posts'"
237
+ err.descriptors; // the full list of PermissionDescriptor[] that were checked
238
+ }
239
+ }
240
+ ```
241
+
242
+ `ForbiddenError` extends `Error`. It has a `toJSON()` method, making it JSON-serializable so it can cross the server/client boundary in action responses.
243
+
244
+ ### `CRUD_ACTIONS`
245
+
246
+ A readonly array of the four CRUD action strings, useful for iteration:
247
+
248
+ ```typescript
249
+ import { CRUD_ACTIONS } from "@cfast/permissions";
250
+
251
+ CRUD_ACTIONS; // ["read", "create", "update", "delete"]
252
+ ```
253
+
254
+ ### Client Entrypoint
255
+
256
+ Import from `@cfast/permissions/client` in client bundles to avoid pulling in server-only code (like `definePermissions`, `grant`, and `checkPermissions`). The client entrypoint exports only types and the `ForbiddenError` class:
257
+
258
+ ```typescript
259
+ import { ForbiddenError } from "@cfast/permissions/client";
260
+ import type { PermissionAction, CrudAction, PermissionDescriptor, PermissionCheckResult } from "@cfast/permissions/client";
261
+ ```
262
+
263
+ ## How Permissions Flow Through the System
264
+
265
+ ```
266
+ definePermissions() @cfast/permissions (isomorphic)
267
+
268
+
269
+ createDb({ permissions }) @cfast/db (server)
270
+
271
+
272
+ db.query / db.update / ... returns Operation
273
+
274
+ ├─► .permissions → PermissionDescriptor[] (structural, no values needed)
275
+
276
+ └─► .run(params) → checkPermissions() → execute via Drizzle prepared statement
277
+
278
+ ├─► Success → returns query results
279
+ └─► Denied → throws ForbiddenError
280
+
281
+ createAction({ operations }) @cfast/actions
282
+
283
+ ├─► Server: calls .run() which checks + executes
284
+ └─► Client: server pre-computes .permitted boolean from .permissions
285
+ ```
286
+
287
+ ### Key insight: permissions are structural
288
+
289
+ The permission system has two layers:
290
+
291
+ 1. **Structural layer** (`PermissionDescriptor`) — "does this role have *any* grant for `update` on `posts`?" This is what `.permissions` exposes. It can be checked without concrete values and is what the client uses for UI adaptation.
292
+
293
+ 2. **Row-level layer** (`where` clauses) — "does this role's grant for `update` on `posts` include *this specific row*?" This is checked at execution time inside `.run()` when concrete parameter values are available.
294
+
295
+ The structural layer enables composition and client-side introspection. The row-level layer provides the actual security enforcement.
296
+
297
+ ## Architecture
298
+
299
+ ```
300
+ @cfast/permissions (isomorphic, ~3KB)
301
+ ├── definePermissions() — configuration
302
+ ├── grant() — grant builder
303
+ ├── checkPermissions() — structural permission checking
304
+ ├── PermissionDescriptor — structural type
305
+ ├── ForbiddenError — error class
306
+ ├── Role hierarchy resolution — flattens inherited grants
307
+
308
+ ├── Server (used by @cfast/db):
309
+ │ └── Compiles where clauses to Drizzle SQL expressions
310
+
311
+ └── Client (used by @cfast/actions):
312
+ └── PermissionDescriptor is JSON-serializable for server→client transfer
313
+ ```
314
+
315
+ The isomorphic core has no server-only dependencies. The Drizzle query compilation (turning `where` functions into actual SQL) lives in `@cfast/db`, so the client bundle never includes it.
316
+
317
+ ## Integration
318
+
319
+ - **`@cfast/db`** — Consumes `permissions` in `createDb()`. Every operation returned by the db is permission-aware. See the `@cfast/db` README for the full Operation API.
320
+ - **`@cfast/actions`** — Actions define their operations, and the framework extracts permission descriptors for client-side introspection. See the `@cfast/actions` README.
321
+ - **`@cfast/admin`** — Admin CRUD operations go through the same permission system. An admin sees all rows. A moderator sees what the moderator role allows.
@@ -0,0 +1,32 @@
1
+ // src/errors.ts
2
+ function getTableName(table) {
3
+ return table._?.name ?? "unknown";
4
+ }
5
+ var ForbiddenError = class extends Error {
6
+ action;
7
+ table;
8
+ role;
9
+ descriptors;
10
+ constructor(options) {
11
+ const tableName = getTableName(options.table);
12
+ super(`Role '${options.role}' cannot ${options.action} on '${tableName}'`);
13
+ this.name = "ForbiddenError";
14
+ this.action = options.action;
15
+ this.table = options.table;
16
+ this.role = options.role;
17
+ this.descriptors = options.descriptors ?? [];
18
+ }
19
+ toJSON() {
20
+ return {
21
+ name: this.name,
22
+ message: this.message,
23
+ action: this.action,
24
+ table: getTableName(this.table),
25
+ role: this.role
26
+ };
27
+ }
28
+ };
29
+
30
+ export {
31
+ ForbiddenError
32
+ };
@@ -0,0 +1,54 @@
1
+ // src/types.ts
2
+ var DRIZZLE_NAME_SYMBOL = /* @__PURE__ */ Symbol.for("drizzle:Name");
3
+ function getTableName(table) {
4
+ const name = Reflect.get(table, DRIZZLE_NAME_SYMBOL);
5
+ return typeof name === "string" ? name : "unknown";
6
+ }
7
+ var CRUD_ACTIONS = ["read", "create", "update", "delete"];
8
+
9
+ // src/errors.ts
10
+ var ForbiddenError = class extends Error {
11
+ /** The action that was denied (e.g., `"delete"`). */
12
+ action;
13
+ /** The Drizzle table the action targeted. */
14
+ table;
15
+ /** The role that lacked the permission, or `undefined` if not specified. */
16
+ role;
17
+ /** The full list of permission descriptors that were checked. */
18
+ descriptors;
19
+ /**
20
+ * Creates a new `ForbiddenError`.
21
+ *
22
+ * @param options - The action, table, and optional role/descriptors for the error.
23
+ */
24
+ constructor(options) {
25
+ const tableName = getTableName(options.table);
26
+ const msg = options.role ? `Role '${options.role}' cannot ${options.action} on '${tableName}'` : `Cannot ${options.action} on '${tableName}'`;
27
+ super(msg);
28
+ this.name = "ForbiddenError";
29
+ this.action = options.action;
30
+ this.table = options.table;
31
+ this.role = options.role;
32
+ this.descriptors = options.descriptors ?? [];
33
+ }
34
+ /**
35
+ * Serializes the error to a JSON-safe object for server-to-client transfer.
36
+ *
37
+ * @returns A plain object with `name`, `message`, `action`, `table`, and `role` fields.
38
+ */
39
+ toJSON() {
40
+ return {
41
+ name: this.name,
42
+ message: this.message,
43
+ action: this.action,
44
+ table: getTableName(this.table),
45
+ role: this.role
46
+ };
47
+ }
48
+ };
49
+
50
+ export {
51
+ getTableName,
52
+ CRUD_ACTIONS,
53
+ ForbiddenError
54
+ };
@@ -0,0 +1,53 @@
1
+ // src/types.ts
2
+ var DRIZZLE_NAME_SYMBOL = /* @__PURE__ */ Symbol.for("drizzle:Name");
3
+ function getTableName(table) {
4
+ return table[DRIZZLE_NAME_SYMBOL] ?? "unknown";
5
+ }
6
+ var CRUD_ACTIONS = ["read", "create", "update", "delete"];
7
+
8
+ // src/errors.ts
9
+ var ForbiddenError = class extends Error {
10
+ /** The action that was denied (e.g., `"delete"`). */
11
+ action;
12
+ /** The Drizzle table the action targeted. */
13
+ table;
14
+ /** The role that lacked the permission, or `undefined` if not specified. */
15
+ role;
16
+ /** The full list of permission descriptors that were checked. */
17
+ descriptors;
18
+ /**
19
+ * Creates a new `ForbiddenError`.
20
+ *
21
+ * @param options - The action, table, and optional role/descriptors for the error.
22
+ */
23
+ constructor(options) {
24
+ const tableName = getTableName(options.table);
25
+ const msg = options.role ? `Role '${options.role}' cannot ${options.action} on '${tableName}'` : `Cannot ${options.action} on '${tableName}'`;
26
+ super(msg);
27
+ this.name = "ForbiddenError";
28
+ this.action = options.action;
29
+ this.table = options.table;
30
+ this.role = options.role;
31
+ this.descriptors = options.descriptors ?? [];
32
+ }
33
+ /**
34
+ * Serializes the error to a JSON-safe object for server-to-client transfer.
35
+ *
36
+ * @returns A plain object with `name`, `message`, `action`, `table`, and `role` fields.
37
+ */
38
+ toJSON() {
39
+ return {
40
+ name: this.name,
41
+ message: this.message,
42
+ action: this.action,
43
+ table: getTableName(this.table),
44
+ role: this.role
45
+ };
46
+ }
47
+ };
48
+
49
+ export {
50
+ getTableName,
51
+ CRUD_ACTIONS,
52
+ ForbiddenError
53
+ };
@@ -0,0 +1,39 @@
1
+ // src/types.ts
2
+ var DRIZZLE_NAME_SYMBOL = /* @__PURE__ */ Symbol.for("drizzle:Name");
3
+ function getTableName(table) {
4
+ return table[DRIZZLE_NAME_SYMBOL] ?? "unknown";
5
+ }
6
+ var CRUD_ACTIONS = ["read", "create", "update", "delete"];
7
+
8
+ // src/errors.ts
9
+ var ForbiddenError = class extends Error {
10
+ action;
11
+ table;
12
+ role;
13
+ descriptors;
14
+ constructor(options) {
15
+ const tableName = getTableName(options.table);
16
+ const msg = options.role ? `Role '${options.role}' cannot ${options.action} on '${tableName}'` : `Cannot ${options.action} on '${tableName}'`;
17
+ super(msg);
18
+ this.name = "ForbiddenError";
19
+ this.action = options.action;
20
+ this.table = options.table;
21
+ this.role = options.role;
22
+ this.descriptors = options.descriptors ?? [];
23
+ }
24
+ toJSON() {
25
+ return {
26
+ name: this.name,
27
+ message: this.message,
28
+ action: this.action,
29
+ table: getTableName(this.table),
30
+ role: this.role
31
+ };
32
+ }
33
+ };
34
+
35
+ export {
36
+ getTableName,
37
+ CRUD_ACTIONS,
38
+ ForbiddenError
39
+ };
@@ -0,0 +1,33 @@
1
+ // src/errors.ts
2
+ function getTableName(table) {
3
+ return table._?.name ?? "unknown";
4
+ }
5
+ var ForbiddenError = class extends Error {
6
+ action;
7
+ table;
8
+ role;
9
+ descriptors;
10
+ constructor(options) {
11
+ const tableName = getTableName(options.table);
12
+ const msg = options.role ? `Role '${options.role}' cannot ${options.action} on '${tableName}'` : `Cannot ${options.action} on '${tableName}'`;
13
+ super(msg);
14
+ this.name = "ForbiddenError";
15
+ this.action = options.action;
16
+ this.table = options.table;
17
+ this.role = options.role;
18
+ this.descriptors = options.descriptors ?? [];
19
+ }
20
+ toJSON() {
21
+ return {
22
+ name: this.name,
23
+ message: this.message,
24
+ action: this.action,
25
+ table: getTableName(this.table),
26
+ role: this.role
27
+ };
28
+ }
29
+ };
30
+
31
+ export {
32
+ ForbiddenError
33
+ };
@@ -0,0 +1,38 @@
1
+ // src/types.ts
2
+ function getTableName(table) {
3
+ return table._?.name ?? "unknown";
4
+ }
5
+ var CRUD_ACTIONS = ["read", "create", "update", "delete"];
6
+
7
+ // src/errors.ts
8
+ var ForbiddenError = class extends Error {
9
+ action;
10
+ table;
11
+ role;
12
+ descriptors;
13
+ constructor(options) {
14
+ const tableName = getTableName(options.table);
15
+ const msg = options.role ? `Role '${options.role}' cannot ${options.action} on '${tableName}'` : `Cannot ${options.action} on '${tableName}'`;
16
+ super(msg);
17
+ this.name = "ForbiddenError";
18
+ this.action = options.action;
19
+ this.table = options.table;
20
+ this.role = options.role;
21
+ this.descriptors = options.descriptors ?? [];
22
+ }
23
+ toJSON() {
24
+ return {
25
+ name: this.name,
26
+ message: this.message,
27
+ action: this.action,
28
+ table: getTableName(this.table),
29
+ role: this.role
30
+ };
31
+ }
32
+ };
33
+
34
+ export {
35
+ getTableName,
36
+ CRUD_ACTIONS,
37
+ ForbiddenError
38
+ };
@@ -0,0 +1,62 @@
1
+ type DrizzleTable = {
2
+ _: {
3
+ name: string;
4
+ };
5
+ };
6
+ type DrizzleSQL = {
7
+ getSQL(): unknown;
8
+ };
9
+ type PermissionAction = "read" | "create" | "update" | "delete" | "manage";
10
+ type CrudAction = Exclude<PermissionAction, "manage">;
11
+ declare const CRUD_ACTIONS: readonly CrudAction[];
12
+ type WhereClause = (columns: Record<string, unknown>, user: any) => DrizzleSQL | undefined;
13
+ type Grant = {
14
+ action: PermissionAction;
15
+ subject: DrizzleTable | "all";
16
+ where?: WhereClause;
17
+ };
18
+ type GrantFn<TUser> = (action: PermissionAction, subject: DrizzleTable | "all", options?: {
19
+ where?: (columns: Record<string, unknown>, user: TUser) => DrizzleSQL | undefined;
20
+ }) => Grant;
21
+ type PermissionDescriptor = {
22
+ action: PermissionAction;
23
+ table: DrizzleTable;
24
+ };
25
+ type PermissionCheckResult = {
26
+ permitted: boolean;
27
+ denied: PermissionDescriptor[];
28
+ reasons: string[];
29
+ };
30
+ type PermissionsConfig<TRoles extends readonly string[], TUser = unknown> = {
31
+ roles: TRoles;
32
+ grants: Record<TRoles[number], Grant[]> | ((grant: GrantFn<TUser>) => Record<TRoles[number], Grant[]>);
33
+ hierarchy?: Partial<Record<TRoles[number], TRoles[number][]>>;
34
+ };
35
+ type Permissions<TRoles extends readonly string[] = readonly string[]> = {
36
+ roles: TRoles;
37
+ grants: Record<TRoles[number], Grant[]>;
38
+ resolvedGrants: Record<TRoles[number], Grant[]>;
39
+ };
40
+
41
+ type ForbiddenErrorOptions = {
42
+ action: PermissionAction;
43
+ table: DrizzleTable;
44
+ role?: string;
45
+ descriptors?: PermissionDescriptor[];
46
+ };
47
+ declare class ForbiddenError extends Error {
48
+ readonly action: PermissionAction;
49
+ readonly table: DrizzleTable;
50
+ readonly role: string | undefined;
51
+ readonly descriptors: PermissionDescriptor[];
52
+ constructor(options: ForbiddenErrorOptions);
53
+ toJSON(): {
54
+ name: string;
55
+ message: string;
56
+ action: PermissionAction;
57
+ table: string;
58
+ role: string | undefined;
59
+ };
60
+ }
61
+
62
+ export { CRUD_ACTIONS as C, type DrizzleTable as D, ForbiddenError as F, type Grant as G, type PermissionsConfig as P, type WhereClause as W, type Permissions as a, type PermissionAction as b, type PermissionDescriptor as c, type PermissionCheckResult as d, type CrudAction as e, type GrantFn as f };