@lbstack/accessx 0.3.0 → 0.4.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.
@@ -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>[];
@@ -27,6 +27,11 @@ export function createEngine(config) {
27
27
  interval: interval,
28
28
  condition: condition,
29
29
  };
30
+ const updatePermissions = (list) => {
31
+ assignment.permissions.clear();
32
+ list.forEach((p) => assignment.permissions.set(p, assignment.condition || (() => true)));
33
+ notify();
34
+ };
30
35
  const updateFn = async (newKey) => {
31
36
  let list = [];
32
37
  if (typeof perms === "function") {
@@ -38,8 +43,7 @@ export function createEngine(config) {
38
43
  else {
39
44
  list = Array.isArray(perms) ? perms : [perms];
40
45
  }
41
- assignment.permissions.clear();
42
- list.forEach((p) => assignment.permissions.set(p, assignment.condition || (() => true)));
46
+ updatePermissions(list);
43
47
  if (newKey !== undefined) {
44
48
  assignment.lastInvalidateKey = newKey;
45
49
  }
@@ -49,12 +53,26 @@ export function createEngine(config) {
49
53
  ? await assignment.invalidateKeyFetcher()
50
54
  : assignment.invalidateKeyFetcher;
51
55
  }
52
- notify();
53
56
  };
54
- await updateFn();
57
+ // Immediate application for synchronous perms
58
+ if (typeof perms !== "function") {
59
+ const list = Array.isArray(perms) ? perms : [perms];
60
+ updatePermissions(list);
61
+ // Handle literal key synchronously
62
+ if (invalidateKey && typeof invalidateKey !== "function") {
63
+ assignment.lastInvalidateKey = invalidateKey;
64
+ }
65
+ }
55
66
  if (!roleAssignments.has(role))
56
67
  roleAssignments.set(role, []);
57
68
  roleAssignments.get(role).push(assignment);
69
+ // Still call updateFn to handle potential async invalidateKeyFetcher
70
+ // and initial perms fetch if it's a function.
71
+ const initialUpdate = updateFn();
72
+ if (typeof perms === "function" ||
73
+ typeof invalidateKey === "function") {
74
+ await initialUpdate;
75
+ }
58
76
  if (interval && interval > 0) {
59
77
  assignment.timer = setInterval(async () => {
60
78
  const currentKey = assignment.invalidateKeyFetcher
@@ -122,34 +140,50 @@ export function createEngine(config) {
122
140
  function resolvePermissions(role) {
123
141
  return getRolePermissions(role);
124
142
  }
125
- function can(role, permission, context) {
143
+ /**
144
+ * Check if a role has the specified permission.
145
+ * @param role The role or roles to check.
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) {
126
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
+ }
127
159
  return roles.some((r) => {
128
160
  const assignments = roleAssignments.get(r);
129
161
  if (!assignments)
130
162
  return false;
131
163
  return assignments.some((assignment) => {
132
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
+ };
133
176
  // 1. Global wildcard
134
- if (permsMap.has("*")) {
135
- const condition = permsMap.get("*");
136
- if (condition(context))
137
- return true;
138
- }
177
+ if (check("*"))
178
+ return true;
139
179
  // 2. Direct match
140
- if (permsMap.has(permission)) {
141
- const condition = permsMap.get(permission);
142
- if (condition(context))
143
- return true;
144
- }
180
+ if (check(permission))
181
+ return true;
145
182
  // 3. Resource wildcard
146
183
  const [resource] = permission.split(":");
147
184
  const resourceWildcard = `${resource}:*`;
148
- if (permsMap.has(resourceWildcard)) {
149
- const condition = permsMap.get(resourceWildcard);
150
- if (condition(context))
151
- return true;
152
- }
185
+ if (check(resourceWildcard))
186
+ return true;
153
187
  return false;
154
188
  });
155
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.0",
3
+ "version": "0.4.0",
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
@@ -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", "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.
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 }}>