@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/dist/client.js ADDED
@@ -0,0 +1,6 @@
1
+ import {
2
+ ForbiddenError
3
+ } from "./chunk-I35WLWUH.js";
4
+ export {
5
+ ForbiddenError
6
+ };
@@ -0,0 +1,126 @@
1
+ import { P as PermissionsConfig, a as Permissions, b as PermissionAction, D as DrizzleTable, W as WhereClause, G as Grant, c as PermissionDescriptor, d as PermissionCheckResult } from './client-CJBFS0IS.js';
2
+ export { C as CRUD_ACTIONS, e as CrudAction, F as ForbiddenError, f as GrantFn, g as getTableName } from './client-CJBFS0IS.js';
3
+
4
+ /**
5
+ * Creates a permission configuration that can be shared between server-side
6
+ * enforcement (`@cfast/db`) and client-side introspection (`@cfast/actions`).
7
+ *
8
+ * Supports two calling styles:
9
+ * - **Direct:** `definePermissions(config)` when no custom user type is needed.
10
+ * - **Curried:** `definePermissions<MyUser>()(config)` to get typed `where` clause user parameters.
11
+ *
12
+ * @param config - The permissions configuration with roles, grants, and optional hierarchy.
13
+ * @returns A {@link Permissions} object containing roles, raw grants, and hierarchy-expanded `resolvedGrants`.
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * import { definePermissions, grant } from "@cfast/permissions";
18
+ * import { eq } from "drizzle-orm";
19
+ * import { posts, comments } from "./schema";
20
+ *
21
+ * const permissions = definePermissions({
22
+ * roles: ["anonymous", "user", "admin"] as const,
23
+ * grants: {
24
+ * anonymous: [
25
+ * grant("read", posts, { where: (p) => eq(p.published, true) }),
26
+ * ],
27
+ * user: [
28
+ * grant("read", posts),
29
+ * grant("create", posts),
30
+ * grant("update", posts, { where: (p, u) => eq(p.authorId, u.id) }),
31
+ * ],
32
+ * admin: [grant("manage", "all")],
33
+ * },
34
+ * });
35
+ * ```
36
+ */
37
+ declare function definePermissions<TRoles extends readonly string[]>(config: PermissionsConfig<TRoles>): Permissions<TRoles>;
38
+ declare function definePermissions<TUser>(): <TRoles extends readonly string[]>(config: PermissionsConfig<TRoles, TUser>) => Permissions<TRoles>;
39
+
40
+ /**
41
+ * Declares that a role can perform an action on a subject, optionally restricted
42
+ * by a row-level `where` clause.
43
+ *
44
+ * Used inside the `grants` map of {@link definePermissions} to build permission rules.
45
+ * A grant without a `where` clause applies to all rows.
46
+ *
47
+ * @param action - The operation being permitted (`"read"`, `"create"`, `"update"`, `"delete"`, or `"manage"` for all four).
48
+ * @param subject - A Drizzle table reference, or `"all"` to apply to every table.
49
+ * @param options - Optional configuration.
50
+ * @param options.where - A Drizzle filter function `(columns, user) => SQL` that restricts which rows this grant covers.
51
+ * @returns A {@link Grant} object for use in a permissions configuration.
52
+ *
53
+ * @example
54
+ * ```typescript
55
+ * import { grant } from "@cfast/permissions";
56
+ * import { eq } from "drizzle-orm";
57
+ * import { posts } from "./schema";
58
+ *
59
+ * // Unrestricted read on all posts
60
+ * grant("read", posts);
61
+ *
62
+ * // Only allow updating own posts
63
+ * grant("update", posts, {
64
+ * where: (post, user) => eq(post.authorId, user.id),
65
+ * });
66
+ *
67
+ * // Full access to everything
68
+ * grant("manage", "all");
69
+ * ```
70
+ */
71
+ declare function grant(action: PermissionAction, subject: DrizzleTable | "all", options?: {
72
+ where?: WhereClause;
73
+ }): Grant;
74
+
75
+ /**
76
+ * Checks whether a role satisfies a set of permission descriptors.
77
+ *
78
+ * This is the low-level structural checking function. It determines whether a
79
+ * role has *any* matching grant for each descriptor (action + table), without
80
+ * evaluating row-level `where` clauses. Row-level enforcement happens at
81
+ * execution time in `@cfast/db`.
82
+ *
83
+ * @param role - The role to check (e.g., `"user"`, `"admin"`).
84
+ * @param permissions - The permissions object from {@link definePermissions}.
85
+ * @param descriptors - Array of permission descriptors to check against.
86
+ * @returns A {@link PermissionCheckResult} with `permitted`, `denied`, and `reasons`.
87
+ *
88
+ * @example
89
+ * ```typescript
90
+ * import { checkPermissions, definePermissions, grant } from "@cfast/permissions";
91
+ *
92
+ * const permissions = definePermissions({
93
+ * roles: ["user", "admin"] as const,
94
+ * grants: {
95
+ * user: [grant("read", posts), grant("create", posts)],
96
+ * admin: [grant("manage", "all")],
97
+ * },
98
+ * });
99
+ *
100
+ * const result = checkPermissions("user", permissions, [
101
+ * { action: "update", table: posts },
102
+ * ]);
103
+ * result.permitted; // false
104
+ * result.denied; // [{ action: "update", table: posts }]
105
+ * ```
106
+ */
107
+ declare function checkPermissions(role: string, permissions: Permissions, descriptors: PermissionDescriptor[]): PermissionCheckResult;
108
+
109
+ /**
110
+ * Resolves and merges grants for multiple roles into a single flat array.
111
+ *
112
+ * Looks up each role's pre-expanded grants (from hierarchy resolution) and
113
+ * deduplicates them by action + subject. When multiple grants share the same
114
+ * action + subject:
115
+ * - If **any** grant has no `where` clause, the merged grant is unrestricted.
116
+ * - If **all** grants have `where` clauses, they are OR-merged via Drizzle's `or()`.
117
+ *
118
+ * This is used when a user has multiple roles and their grants need to be combined.
119
+ *
120
+ * @param permissions - The permissions object from {@link definePermissions}.
121
+ * @param roles - Array of role names whose grants should be merged.
122
+ * @returns A deduplicated array of {@link Grant} objects with merged `where` clauses.
123
+ */
124
+ declare function resolveGrants(permissions: Permissions, roles: string[]): Grant[];
125
+
126
+ export { DrizzleTable, Grant, PermissionAction, PermissionCheckResult, PermissionDescriptor, Permissions, PermissionsConfig, WhereClause, checkPermissions, definePermissions, grant, resolveGrants };
package/dist/index.js ADDED
@@ -0,0 +1,170 @@
1
+ import {
2
+ CRUD_ACTIONS,
3
+ ForbiddenError,
4
+ getTableName
5
+ } from "./chunk-I35WLWUH.js";
6
+
7
+ // src/grant.ts
8
+ function grant(action, subject, options) {
9
+ return {
10
+ action,
11
+ subject,
12
+ where: options?.where
13
+ };
14
+ }
15
+
16
+ // src/define-permissions.ts
17
+ function buildPermissions(config) {
18
+ const { roles, hierarchy } = config;
19
+ const grantFn = grant;
20
+ const grants = typeof config.grants === "function" ? config.grants(grantFn) : config.grants;
21
+ const resolvedGrants = resolveHierarchy(roles, grants, hierarchy);
22
+ return {
23
+ roles,
24
+ grants,
25
+ resolvedGrants
26
+ };
27
+ }
28
+ function definePermissions(config) {
29
+ if (config === void 0) {
30
+ return (c) => buildPermissions(c);
31
+ }
32
+ return buildPermissions(config);
33
+ }
34
+ function resolveHierarchy(roles, grants, hierarchy) {
35
+ if (!hierarchy) {
36
+ return { ...grants };
37
+ }
38
+ const resolved = {};
39
+ const resolving = /* @__PURE__ */ new Set();
40
+ function resolve(role) {
41
+ if (resolved[role]) return resolved[role];
42
+ if (resolving.has(role)) {
43
+ throw new Error(
44
+ `Circular role hierarchy detected: '${role}' inherits from itself`
45
+ );
46
+ }
47
+ resolving.add(role);
48
+ const own = grants[role] ?? [];
49
+ const parents = hierarchy?.[role] ?? [];
50
+ const inherited = parents.flatMap((parent) => resolve(parent));
51
+ resolved[role] = [...inherited, ...own];
52
+ resolving.delete(role);
53
+ return resolved[role];
54
+ }
55
+ for (const role of roles) {
56
+ resolve(role);
57
+ }
58
+ return resolved;
59
+ }
60
+
61
+ // src/check.ts
62
+ function grantMatches(g, action, table) {
63
+ const actionOk = g.action === action || g.action === "manage";
64
+ const subjectOk = g.subject === "all" || g.subject === table || getTableName(g.subject) === getTableName(table);
65
+ return actionOk && subjectOk;
66
+ }
67
+ function hasGrantFor(grants, action, table) {
68
+ return grants.some((g) => grantMatches(g, action, table));
69
+ }
70
+ function hasManagePermission(grants, table) {
71
+ if (hasGrantFor(grants, "manage", table)) return true;
72
+ return CRUD_ACTIONS.every((action) => hasGrantFor(grants, action, table));
73
+ }
74
+ function checkPermissions(role, permissions, descriptors) {
75
+ const grants = permissions.resolvedGrants[role] ?? [];
76
+ const denied = [];
77
+ const reasons = [];
78
+ for (const descriptor of descriptors) {
79
+ let permitted;
80
+ if (descriptor.action === "manage") {
81
+ permitted = hasManagePermission(grants, descriptor.table);
82
+ } else {
83
+ permitted = hasGrantFor(grants, descriptor.action, descriptor.table);
84
+ }
85
+ if (!permitted) {
86
+ denied.push(descriptor);
87
+ reasons.push(
88
+ `Role '${role}' cannot ${descriptor.action} on '${getTableName(descriptor.table)}'`
89
+ );
90
+ }
91
+ }
92
+ return {
93
+ permitted: denied.length === 0,
94
+ denied,
95
+ reasons
96
+ };
97
+ }
98
+
99
+ // src/resolve-grants.ts
100
+ import { or } from "drizzle-orm";
101
+ function toSQLWrapper(value) {
102
+ return value;
103
+ }
104
+ function resolveGrants(permissions, roles) {
105
+ const allGrants = [];
106
+ for (const role of roles) {
107
+ const roleGrants = permissions.resolvedGrants[role];
108
+ if (roleGrants) {
109
+ allGrants.push(...roleGrants);
110
+ }
111
+ }
112
+ if (allGrants.length === 0) return [];
113
+ const groups = /* @__PURE__ */ new Map();
114
+ const subjectIds = /* @__PURE__ */ new Map();
115
+ let nextId = 0;
116
+ function getSubjectId(subject) {
117
+ let id = subjectIds.get(subject);
118
+ if (id === void 0) {
119
+ id = nextId++;
120
+ subjectIds.set(subject, id);
121
+ }
122
+ return id;
123
+ }
124
+ for (const g of allGrants) {
125
+ const key = `${g.action}:${getSubjectId(g.subject)}`;
126
+ let group = groups.get(key);
127
+ if (!group) {
128
+ group = { action: g.action, subject: g.subject, wheres: [] };
129
+ groups.set(key, group);
130
+ }
131
+ group.wheres.push(g.where);
132
+ }
133
+ const result = [];
134
+ for (const group of groups.values()) {
135
+ const hasUnrestricted = group.wheres.some((w) => w === void 0);
136
+ if (hasUnrestricted) {
137
+ result.push({ action: group.action, subject: group.subject });
138
+ } else {
139
+ const whereFns = group.wheres.filter(
140
+ (w) => w !== void 0
141
+ );
142
+ if (whereFns.length === 1) {
143
+ result.push({
144
+ action: group.action,
145
+ subject: group.subject,
146
+ where: whereFns[0]
147
+ });
148
+ } else {
149
+ const merged = (columns, user) => or(
150
+ ...whereFns.map((fn) => toSQLWrapper(fn(columns, user)))
151
+ );
152
+ result.push({
153
+ action: group.action,
154
+ subject: group.subject,
155
+ where: merged
156
+ });
157
+ }
158
+ }
159
+ }
160
+ return result;
161
+ }
162
+ export {
163
+ CRUD_ACTIONS,
164
+ ForbiddenError,
165
+ checkPermissions,
166
+ definePermissions,
167
+ getTableName,
168
+ grant,
169
+ resolveGrants
170
+ };
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@cfast/permissions",
3
+ "version": "0.0.1",
4
+ "description": "Isomorphic, composable permission system with Drizzle-native row-level access control",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/DanielMSchmidt/cfast.git",
9
+ "directory": "packages/permissions"
10
+ },
11
+ "type": "module",
12
+ "main": "dist/index.js",
13
+ "types": "dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "import": "./dist/index.js",
17
+ "types": "./dist/index.d.ts"
18
+ },
19
+ "./client": {
20
+ "import": "./dist/client.js",
21
+ "types": "./dist/client.d.ts"
22
+ }
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "sideEffects": false,
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "peerDependencies": {
32
+ "drizzle-orm": ">=0.35"
33
+ },
34
+ "devDependencies": {
35
+ "tsup": "^8",
36
+ "typescript": "^5.7",
37
+ "vitest": "^4.1.0"
38
+ },
39
+ "scripts": {
40
+ "build": "tsup src/index.ts src/client.ts --format esm --dts",
41
+ "dev": "tsup src/index.ts src/client.ts --format esm --dts --watch",
42
+ "typecheck": "tsc --noEmit",
43
+ "lint": "eslint src/",
44
+ "test": "vitest run"
45
+ }
46
+ }