@lbstack/accessx 0.3.1 → 0.4.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.
@@ -29,7 +29,7 @@ export declare function createAccess<const R extends readonly string[], const A
29
29
  }[];
30
30
  permissionKeys: import("./engine").PermissionKey<Res, A>[];
31
31
  allow: (role: R[number], permission: import("./engine").PermissionKey<Res, A> | import("./engine").PermissionKey<Res, A>[], condition?: import("../types").ConditionFn) => void;
32
- can: (role: R[number] | R[number][], permission: import("./engine").PermissionKey<Res, A>, context?: any) => boolean;
32
+ can: (role: R[number] | R[number][], permission: import("./engine").PermissionKey<Res, A>, contextOrValidator?: any | (() => boolean)) => boolean;
33
33
  resolvePermissions: (role: R[number]) => import("./engine").PermissionKey<Res, A>[];
34
34
  normalizePermissions: (raw: string[]) => import("./engine").PermissionKey<Res, A>[];
35
35
  getRolePermissions: (role: R[number]) => import("./engine").PermissionKey<Res, A>[];
@@ -15,7 +15,7 @@ export declare function createEngine<const R extends readonly string[], const A
15
15
  }[];
16
16
  permissionKeys: PermissionKey<Res, A>[];
17
17
  allow: (role: R[number], permission: PermissionKey<Res, A> | PermissionKey<Res, A>[], condition?: ConditionFn) => void;
18
- can: (role: R[number] | R[number][], permission: PermissionKey<Res, A>, context?: any) => boolean;
18
+ can: (role: R[number] | R[number][], permission: PermissionKey<Res, A>, contextOrValidator?: any | (() => boolean)) => boolean;
19
19
  resolvePermissions: (role: R[number]) => PermissionKey<Res, A>[];
20
20
  normalizePermissions: (raw: string[]) => PermissionKey<Res, A>[];
21
21
  getRolePermissions: (role: R[number]) => PermissionKey<Res, A>[];
@@ -140,34 +140,50 @@ export function createEngine(config) {
140
140
  function resolvePermissions(role) {
141
141
  return getRolePermissions(role);
142
142
  }
143
- function can(role, permission, context) {
143
+ /**
144
+ * Check if a role has the specified permission.
145
+ * @param role The role or roles to check (e.g. `user.role` from session).
146
+ * @param permission The permission to check.
147
+ * @param contextOrValidator
148
+ * - (Recommended) A callback function `() => boolean` that returns true if allowed.
149
+ * - (Deprecated) A context object to be passed to the stored condition.
150
+ */
151
+ function can(role, permission, contextOrValidator) {
144
152
  const roles = Array.isArray(role) ? role : [role];
153
+ // If a validator function is provided, it MUST return true.
154
+ if (typeof contextOrValidator === "function") {
155
+ if (!contextOrValidator()) {
156
+ return false;
157
+ }
158
+ }
145
159
  return roles.some((r) => {
146
160
  const assignments = roleAssignments.get(r);
147
161
  if (!assignments)
148
162
  return false;
149
163
  return assignments.some((assignment) => {
150
164
  const permsMap = assignment.permissions;
165
+ // Helper to check a specific permission key in the map
166
+ const check = (key) => {
167
+ if (permsMap.has(key)) {
168
+ const storedCondition = permsMap.get(key);
169
+ // pass the context (or function) to stored condition
170
+ // If stored condition checks context, and we passed a function, it might fail/throw.
171
+ // Ideally legacy stored conditions are used with legacy context objects.
172
+ return storedCondition(contextOrValidator);
173
+ }
174
+ return false;
175
+ };
151
176
  // 1. Global wildcard
152
- if (permsMap.has("*")) {
153
- const condition = permsMap.get("*");
154
- if (condition(context))
155
- return true;
156
- }
177
+ if (check("*"))
178
+ return true;
157
179
  // 2. Direct match
158
- if (permsMap.has(permission)) {
159
- const condition = permsMap.get(permission);
160
- if (condition(context))
161
- return true;
162
- }
180
+ if (check(permission))
181
+ return true;
163
182
  // 3. Resource wildcard
164
183
  const [resource] = permission.split(":");
165
184
  const resourceWildcard = `${resource}:*`;
166
- if (permsMap.has(resourceWildcard)) {
167
- const condition = permsMap.get(resourceWildcard);
168
- if (condition(context))
169
- return true;
170
- }
185
+ if (check(resourceWildcard))
186
+ return true;
171
187
  return false;
172
188
  });
173
189
  });
package/llm.txt ADDED
@@ -0,0 +1,94 @@
1
+ # @lbstack/accessx
2
+
3
+ A **TypeScript-first RBAC permission engine** with **automatic permission generation**, designed to be the **single source of truth** for Backend authorization, Frontend UI access control, Database permission storage, and Admin permission management.
4
+
5
+ ## ✨ Key Features
6
+ - 🔐 Automatic permission generation
7
+ - 🧠 Strong TypeScript inference & autocomplete
8
+ - 🏗️ Single initialization – use everywhere (Backend & Frontend)
9
+ - 🗄️ Database-friendly permission keys
10
+ - 📦 Package & framework agnostic
11
+
12
+ ## 🧩 Core Concept
13
+ Define Roles, Actions, and Resources. The engine generates permissions as `RESOURCE_KEY:ACTION`.
14
+ Example: `BLOGS:CREATE`, `USER:DELETE`.
15
+
16
+ ## 📦 Installation
17
+ ```bash
18
+ npm install @lbstack/accessx
19
+ # or
20
+ pnpm add @lbstack/accessx
21
+ ```
22
+
23
+ ## 🚀 Initialization
24
+ ```typescript
25
+ import { createAccess } from "@lbstack/accessx";
26
+
27
+ export const access = createAccess({
28
+ roles: ["ADMIN", "EDITOR", "CUSTOMER"] as const,
29
+ actions: ["CREATE", "READ", "UPDATE", "DELETE"] as const,
30
+ resources: [
31
+ { name: "Users", key: "USER", description: "User management" },
32
+ { name: "Blogs", key: "BLOGS", description: "Blog posts" },
33
+ ] as const,
34
+ });
35
+ ```
36
+
37
+ ## 🔑 Assigning Permissions
38
+
39
+ ### 1. Manual Assignment (Static)
40
+ ```typescript
41
+ access.allow("ADMIN", "USER:DELETE");
42
+ access.allow("EDITOR", ["BLOGS:CREATE", "BLOGS:READ"]);
43
+ ```
44
+
45
+ ### 2. Dynamic Assignment (Database + Cache)
46
+ ```typescript
47
+ await access.assignPermissions("ADMIN",
48
+ async () => {
49
+ const permissions = await db.query("SELECT key FROM permissions WHERE role = 'ADMIN'");
50
+ return permissions.map(p => p.key);
51
+ },
52
+ {
53
+ invalidateKey: async () => await redis.get("perms:admin:version"),
54
+ interval: 60000
55
+ }
56
+ );
57
+ ```
58
+
59
+ ## 🔄 Manual Refresh & Sync
60
+ ```typescript
61
+ await access.refresh(); // Refresh all
62
+ await access.refresh("ADMIN"); // Refresh specific role
63
+ ```
64
+
65
+ ## 🎨 Frontend Usage (React)
66
+
67
+ ### useCan Hook
68
+ ```tsx
69
+ const canEdit = access.useCan("EDITOR", "BLOGS:UPDATE");
70
+ ```
71
+
72
+ ### usePermissions Hook
73
+ ```tsx
74
+ const { permissions, loading, refresh } = access.usePermissions(async () => {
75
+ const res = await api.get("/my-permissions");
76
+ return res.data;
77
+ });
78
+ ```
79
+
80
+ ### <Can /> Component
81
+ ```tsx
82
+ <access.Can role="ADMIN" permission="USER:DELETE">
83
+ <DeleteButton />
84
+ </access.Can>
85
+ ```
86
+
87
+ ## 🧠 ABAC (Attribute-Based Access Control)
88
+ ```typescript
89
+ access.allow("USER", "BLOG:UPDATE", (context) => {
90
+ return context.post.authorId === context.user.id;
91
+ });
92
+
93
+ const canEdit = access.can("USER", "BLOG:UPDATE", { user, post });
94
+ ```
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@lbstack/accessx",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "A role & resource based access control system with end to end type safety.",
5
- "license": "ISC",
5
+ "license": "MIT",
6
6
  "author": "@lbstack",
7
7
  "type": "module",
8
8
  "main": "./dist/index.js",
@@ -10,7 +10,8 @@
10
10
  "types": "./dist/index.d.ts",
11
11
  "files": [
12
12
  "dist",
13
- "README.md"
13
+ "README.md",
14
+ "llm.txt"
14
15
  ],
15
16
  "scripts": {
16
17
  "build": "tsc",
@@ -29,5 +30,15 @@
29
30
  },
30
31
  "peerDependencies": {
31
32
  "react": ">=16.8.0"
32
- }
33
+ },
34
+ "keywords": [
35
+ "rbac",
36
+ "access-control",
37
+ "authorization",
38
+ "permissions",
39
+ "role-based",
40
+ "resource-based",
41
+ "typescript",
42
+ "react"
43
+ ]
33
44
  }
package/readme.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # @lbstack/accessx
2
2
 
3
+ [📄 llm.txt (AI Friendly)](./llm.txt)
4
+
3
5
  A **TypeScript-first RBAC permission engine** with **automatic permission generation**, designed to be the **single source of truth** for:
4
6
 
5
7
  - Backend authorization
@@ -153,7 +155,7 @@ access.allow("ADMIN", "USER:DELETE");
153
155
  // Multiple permissions
154
156
  access.allow("EDITOR", ["BLOGS:CREATE", "BLOGS:READ", "BLOGS:UPDATE"]);
155
157
 
156
- // With custom ABAC conditions
158
+ // With custom ABAC conditions (DEPRECATED: Use Runtime Validation instead)
157
159
  access.allow("USER", "BLOG:UPDATE", (context) => {
158
160
  return context.post.authorId === context.user.id;
159
161
  });
@@ -296,24 +298,45 @@ Scales to ABAC, multi-role, multi-tenant systems
296
298
 
297
299
  ## 🧠 ABAC (Attribute-Based Access Control)
298
300
 
299
- You can define dynamic permissions based on context (e.g., user ID, ownership checking).
301
+ You can define dynamic permissions based on context.
302
+
303
+ ### 1. Runtime Validation (Recommended) ✨
304
+
305
+ Pass a validator function directly to `access.can`. This function is executed at runtime.
300
306
 
301
- ### 1. Define with Conditions
302
307
  ```typescript
303
- access.allow("USER", "BLOG:UPDATE", (context: { user: any; post: any }) => {
308
+ const isAllowed = access.can(user.role, "BLOG:UPDATE", () => {
309
+ // Your logic here
310
+ return post.authorId === user.id;
311
+ });
312
+ ```
313
+
314
+ This approach allows you to keep your permissions stored as pure data (strings) in your database while keeping the complex validation logic in your application code. Use `user.role` dynamically from your session/token.
315
+
316
+ ### 2. Stored Conditions (Deprecated ⚠️)
317
+
318
+ > [!WARNING]
319
+ > Defining conditions during assignment is deprecated and may be removed in future versions. Please use Runtime Validation instead.
320
+
321
+ Defining conditions during assignment:
322
+
323
+ ```typescript
324
+ // DEPRECATED
325
+ access.allow("USER", "BLOG:UPDATE", (context) => {
304
326
  return context.post.authorId === context.user.id;
305
327
  });
306
328
  ```
307
329
 
308
- ### 2. Check with Context
309
- The `can` method accepts an optional context object as the third argument.
330
+ Checking with context object:
310
331
 
311
332
  ```typescript
333
+ // DEPRECATED
312
334
  const canEdit = access.can("USER", "BLOG:UPDATE", { user, post });
313
335
  ```
314
336
 
315
337
  ### 3. Frontend Usage
316
- React components also support `context`.
338
+
339
+ React components support the new pattern via the `validator` prop (requires update to React components, currently supports context object):
317
340
 
318
341
  ```tsx
319
342
  <Can permissions={myPermissions} permission="BLOG:UPDATE" context={{ user, post }}>