@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.
- package/dist/src/core/access-control.d.ts +1 -1
- package/dist/src/core/engine.d.ts +1 -1
- package/dist/src/core/engine.js +54 -20
- package/llm.txt +94 -0
- package/package.json +15 -4
- package/readme.md +29 -6
|
@@ -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>,
|
|
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>,
|
|
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>[];
|
package/dist/src/core/engine.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
135
|
-
|
|
136
|
-
if (condition(context))
|
|
137
|
-
return true;
|
|
138
|
-
}
|
|
177
|
+
if (check("*"))
|
|
178
|
+
return true;
|
|
139
179
|
// 2. Direct match
|
|
140
|
-
if (
|
|
141
|
-
|
|
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 (
|
|
149
|
-
|
|
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
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "A role & resource based access control system with end to end type safety.",
|
|
5
|
-
"license": "
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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 }}>
|