@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 +21 -0
- package/README.md +321 -0
- package/dist/chunk-FVAAEIAE.js +32 -0
- package/dist/chunk-I35WLWUH.js +54 -0
- package/dist/chunk-IPYPD2CZ.js +53 -0
- package/dist/chunk-PNHVTXCZ.js +39 -0
- package/dist/chunk-YYYHMPTS.js +33 -0
- package/dist/chunk-ZTQJZJFW.js +38 -0
- package/dist/client-8HHp1rPO.d.ts +62 -0
- package/dist/client-BBcryLDZ.d.ts +61 -0
- package/dist/client-BKD8jH5P.d.ts +56 -0
- package/dist/client-CJBFS0IS.d.ts +197 -0
- package/dist/client-ChpyEV1r.d.ts +62 -0
- package/dist/client-CuB6igvw.d.ts +195 -0
- package/dist/client-DduKFZmk.d.ts +59 -0
- package/dist/client-FbortuK3.d.ts +53 -0
- package/dist/client.d.ts +1 -0
- package/dist/client.js +6 -0
- package/dist/index.d.ts +126 -0
- package/dist/index.js +170 -0
- package/package.json +46 -0
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 };
|