@lbstack/accessx 0.1.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/access-control-ui.d.ts +11 -0
- package/dist/access-control-ui.js +23 -0
- package/dist/access-control.d.ts +23 -0
- package/dist/access-control.js +54 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/src/core/access-control.d.ts +23 -0
- package/dist/src/core/access-control.js +85 -0
- package/dist/src/core/ui/access-control-ui.d.ts +12 -0
- package/dist/src/core/ui/access-control-ui.js +23 -0
- package/dist/src/types/index.d.ts +10 -0
- package/dist/src/types/index.js +1 -0
- package/dist/types.d.ts +9 -0
- package/dist/types.js +1 -0
- package/package.json +30 -0
- package/readme.md +315 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { PermissionType } from "./types";
|
|
3
|
+
export declare function createAccessUI(): {
|
|
4
|
+
useCan: (granted: PermissionType[], permission: PermissionType) => boolean;
|
|
5
|
+
Can: ({ permissions, permission, children, }: {
|
|
6
|
+
permissions: PermissionType[];
|
|
7
|
+
permission: PermissionType;
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
}) => React.ReactNode;
|
|
10
|
+
canDo: (granted: PermissionType[], permission: PermissionType) => boolean;
|
|
11
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export function createAccessUI() {
|
|
3
|
+
function useCan(granted, permission) {
|
|
4
|
+
return React.useMemo(() => canDo(granted, permission), [granted, permission]);
|
|
5
|
+
}
|
|
6
|
+
function Can({ permissions, permission, children, }) {
|
|
7
|
+
return canDo(permissions, permission) ? children : null;
|
|
8
|
+
}
|
|
9
|
+
// โ
Function-level permission checker
|
|
10
|
+
function canDo(granted, permission) {
|
|
11
|
+
if (granted.includes("*")) {
|
|
12
|
+
console.warn(`Global '*' permission used: allowing all permissions!`);
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
if (granted.includes(permission))
|
|
16
|
+
return true;
|
|
17
|
+
const [resource] = permission.split(":");
|
|
18
|
+
if (granted.includes(`${resource}:*`))
|
|
19
|
+
return true;
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
return { useCan, Can, canDo };
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Resource } from "./types";
|
|
2
|
+
export declare function createAccess<const R extends readonly string[], const A extends readonly string[], const Res extends readonly Resource[]>(config: {
|
|
3
|
+
roles: R;
|
|
4
|
+
actions: A;
|
|
5
|
+
resources: Res;
|
|
6
|
+
}): {
|
|
7
|
+
roles: R;
|
|
8
|
+
actions: A;
|
|
9
|
+
resources: Res;
|
|
10
|
+
permissions: {
|
|
11
|
+
key: "*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`;
|
|
12
|
+
resource: Resource;
|
|
13
|
+
action: string;
|
|
14
|
+
}[];
|
|
15
|
+
permissionKeys: ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`)[];
|
|
16
|
+
allow: (role: R[number], permission: ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`) | ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`)[]) => void;
|
|
17
|
+
can: (granted: ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`)[], required: "*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`) => boolean;
|
|
18
|
+
resolvePermissions: (role: R[number]) => ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`)[];
|
|
19
|
+
normalizePermissions: (raw: string[]) => ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`)[];
|
|
20
|
+
assignPermissions: (role: R[number], perms: ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`) | ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`)[]) => void;
|
|
21
|
+
getRolePermissions: (role: R[number]) => ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`)[];
|
|
22
|
+
hasPermission: (granted: ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`)[], required: "*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`) => boolean;
|
|
23
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export function createAccess(config) {
|
|
2
|
+
const permissions = config.resources.flatMap((resource) => config.actions.map((action) => ({
|
|
3
|
+
key: `${resource.key}:${action}`,
|
|
4
|
+
resource,
|
|
5
|
+
action,
|
|
6
|
+
})));
|
|
7
|
+
const permissionKeys = permissions.map((p) => p.key);
|
|
8
|
+
const rolePermissions = new Map();
|
|
9
|
+
function assignPermissions(role, perms) {
|
|
10
|
+
const list = Array.isArray(perms) ? perms : [perms];
|
|
11
|
+
if (!rolePermissions.has(role))
|
|
12
|
+
rolePermissions.set(role, new Set());
|
|
13
|
+
list.forEach((p) => rolePermissions.get(role).add(p));
|
|
14
|
+
}
|
|
15
|
+
function getRolePermissions(role) {
|
|
16
|
+
return Array.from(rolePermissions.get(role) ?? []);
|
|
17
|
+
}
|
|
18
|
+
function allow(role, permission) {
|
|
19
|
+
assignPermissions(role, permission);
|
|
20
|
+
}
|
|
21
|
+
function resolvePermissions(role) {
|
|
22
|
+
return getRolePermissions(role);
|
|
23
|
+
}
|
|
24
|
+
function hasPermission(granted, required) {
|
|
25
|
+
if (granted.includes("*")) {
|
|
26
|
+
console.warn("Global '*' permission used: allowing all permissions!");
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
if (granted.includes(required))
|
|
30
|
+
return true;
|
|
31
|
+
const [resource] = required.split(":");
|
|
32
|
+
if (granted.includes(`${resource}:*`))
|
|
33
|
+
return true;
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
function normalizePermissions(raw) {
|
|
37
|
+
const valid = new Set(permissionKeys);
|
|
38
|
+
return raw.filter((p) => valid.has(p));
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
roles: config.roles,
|
|
42
|
+
actions: config.actions,
|
|
43
|
+
resources: config.resources,
|
|
44
|
+
permissions,
|
|
45
|
+
permissionKeys,
|
|
46
|
+
allow,
|
|
47
|
+
can: hasPermission,
|
|
48
|
+
resolvePermissions,
|
|
49
|
+
normalizePermissions,
|
|
50
|
+
assignPermissions,
|
|
51
|
+
getRolePermissions,
|
|
52
|
+
hasPermission,
|
|
53
|
+
};
|
|
54
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ConditionFn, Resource } from "../types";
|
|
2
|
+
export declare function createAccess<const R extends readonly string[], const A extends readonly string[], const Res extends readonly Resource[]>(config: {
|
|
3
|
+
roles: R;
|
|
4
|
+
actions: A;
|
|
5
|
+
resources: Res;
|
|
6
|
+
}): {
|
|
7
|
+
roles: R;
|
|
8
|
+
actions: A;
|
|
9
|
+
resources: Res;
|
|
10
|
+
permissions: {
|
|
11
|
+
key: "*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`;
|
|
12
|
+
resource: Resource;
|
|
13
|
+
action: string;
|
|
14
|
+
}[];
|
|
15
|
+
permissionKeys: ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`)[];
|
|
16
|
+
allow: (role: R[number], permission: ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`) | ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`)[], condition?: ConditionFn) => void;
|
|
17
|
+
can: (role: R[number], permission: "*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`, context?: any) => boolean;
|
|
18
|
+
hasPermission: (granted: ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`)[], required: "*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`, context?: any) => boolean;
|
|
19
|
+
resolvePermissions: (role: R[number]) => ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`)[];
|
|
20
|
+
normalizePermissions: (raw: string[]) => ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`)[];
|
|
21
|
+
assignPermissions: (role: R[number], perms: ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`) | ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`)[], condition?: ConditionFn) => void;
|
|
22
|
+
getRolePermissions: (role: R[number]) => ("*" | `${Res[number]["key"]}:${A[number]}` | `${Res[number]["key"]}:*`)[];
|
|
23
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export function createAccess(config) {
|
|
2
|
+
const permissions = config.resources.flatMap((resource) => config.actions.map((action) => ({
|
|
3
|
+
key: `${resource.key}:${action}`,
|
|
4
|
+
resource,
|
|
5
|
+
action,
|
|
6
|
+
})));
|
|
7
|
+
const permissionKeys = permissions.map((p) => p.key);
|
|
8
|
+
// Store perms with optional conditions
|
|
9
|
+
const rolePermissions = new Map();
|
|
10
|
+
function assignPermissions(role, perms, condition) {
|
|
11
|
+
const list = Array.isArray(perms) ? perms : [perms];
|
|
12
|
+
if (!rolePermissions.has(role))
|
|
13
|
+
rolePermissions.set(role, new Map());
|
|
14
|
+
const permsMap = rolePermissions.get(role);
|
|
15
|
+
list.forEach((p) => permsMap.set(p, condition || (() => true)));
|
|
16
|
+
}
|
|
17
|
+
function getRolePermissions(role) {
|
|
18
|
+
const permsMap = rolePermissions.get(role);
|
|
19
|
+
return permsMap ? Array.from(permsMap.keys()) : [];
|
|
20
|
+
}
|
|
21
|
+
function allow(role, permission, condition) {
|
|
22
|
+
assignPermissions(role, permission, condition);
|
|
23
|
+
}
|
|
24
|
+
function resolvePermissions(role) {
|
|
25
|
+
return getRolePermissions(role);
|
|
26
|
+
}
|
|
27
|
+
function hasPermission(granted, required, context) {
|
|
28
|
+
// This is a static check for local lists.
|
|
29
|
+
// For ABAC, we need the actual definition with conditions.
|
|
30
|
+
// If 'granted' is just string[], we can't check conditions unless we have the role definition.
|
|
31
|
+
if (granted.includes("*"))
|
|
32
|
+
return true;
|
|
33
|
+
if (granted.includes(required))
|
|
34
|
+
return true;
|
|
35
|
+
const [resource] = required.split(":");
|
|
36
|
+
if (granted.includes(`${resource}:*`))
|
|
37
|
+
return true;
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
// New: Role-aware permission check (supports ABAC)
|
|
41
|
+
function can(role, permission, context) {
|
|
42
|
+
const permsMap = rolePermissions.get(role);
|
|
43
|
+
if (!permsMap)
|
|
44
|
+
return false;
|
|
45
|
+
// 1. Global wildcard
|
|
46
|
+
if (permsMap.has("*")) {
|
|
47
|
+
const condition = permsMap.get("*");
|
|
48
|
+
if (condition(context))
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
// 2. Direct match
|
|
52
|
+
if (permsMap.has(permission)) {
|
|
53
|
+
const condition = permsMap.get(permission);
|
|
54
|
+
if (condition(context))
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
// 3. Resource wildcard
|
|
58
|
+
const [resource] = permission.split(":");
|
|
59
|
+
const resourceWildcard = `${resource}:*`;
|
|
60
|
+
if (permsMap.has(resourceWildcard)) {
|
|
61
|
+
const condition = permsMap.get(resourceWildcard);
|
|
62
|
+
if (condition(context))
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
function normalizePermissions(raw) {
|
|
68
|
+
const valid = new Set(permissionKeys);
|
|
69
|
+
return raw.filter((p) => valid.has(p));
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
roles: config.roles,
|
|
73
|
+
actions: config.actions,
|
|
74
|
+
resources: config.resources,
|
|
75
|
+
permissions,
|
|
76
|
+
permissionKeys,
|
|
77
|
+
allow,
|
|
78
|
+
can, // Role-based check (ABAC supported)
|
|
79
|
+
hasPermission, // Static list check (Legacy/Simple)
|
|
80
|
+
resolvePermissions,
|
|
81
|
+
normalizePermissions,
|
|
82
|
+
assignPermissions,
|
|
83
|
+
getRolePermissions,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { PermissionType } from "../../types";
|
|
3
|
+
export declare function createAccessUI(): {
|
|
4
|
+
useCan: (granted: PermissionType[], permission: PermissionType, context?: any) => boolean;
|
|
5
|
+
Can: ({ permissions, permission, context, children, }: {
|
|
6
|
+
permissions: PermissionType[];
|
|
7
|
+
permission: PermissionType;
|
|
8
|
+
context?: any;
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
}) => React.ReactNode;
|
|
11
|
+
canDo: (granted: PermissionType[], permission: PermissionType, context?: any) => boolean;
|
|
12
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export function createAccessUI() {
|
|
3
|
+
function useCan(granted, permission, context) {
|
|
4
|
+
return React.useMemo(() => canDo(granted, permission, context), [granted, permission, context]);
|
|
5
|
+
}
|
|
6
|
+
function Can({ permissions, permission, context, children, }) {
|
|
7
|
+
return canDo(permissions, permission, context) ? children : null;
|
|
8
|
+
}
|
|
9
|
+
// โ
Function-level permission checker
|
|
10
|
+
// Note: This is a static list check. If conditions are needed on frontend,
|
|
11
|
+
// the frontend list should contain the logic or be checked against a role-aware engine.
|
|
12
|
+
function canDo(granted, permission, context) {
|
|
13
|
+
if (granted.includes("*"))
|
|
14
|
+
return true;
|
|
15
|
+
if (granted.includes(permission))
|
|
16
|
+
return true;
|
|
17
|
+
const [resource] = permission.split(":");
|
|
18
|
+
if (granted.includes(`${resource}:*`))
|
|
19
|
+
return true;
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
return { useCan, Can, canDo };
|
|
23
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type RoleType = string;
|
|
2
|
+
export type ActionType = string;
|
|
3
|
+
export type ResourceKeyType = string;
|
|
4
|
+
export type PermissionType = `${ResourceKeyType}:${ActionType}` | `${ResourceKeyType}:*` | "*";
|
|
5
|
+
export type ConditionFn<TContext = any> = (context: TContext) => boolean;
|
|
6
|
+
export interface Resource {
|
|
7
|
+
name: string;
|
|
8
|
+
key: ResourceKeyType;
|
|
9
|
+
description?: string;
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type RoleType = string;
|
|
2
|
+
export type ActionType = string;
|
|
3
|
+
export type ResourceKeyType = string;
|
|
4
|
+
export type PermissionType = `${ResourceKeyType}:${ActionType}` | `${ResourceKeyType}:*` | "*";
|
|
5
|
+
export interface Resource {
|
|
6
|
+
name: string;
|
|
7
|
+
key: ResourceKeyType;
|
|
8
|
+
description?: string;
|
|
9
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lbstack/accessx",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A role & resource based access control system with end to end type safety.",
|
|
5
|
+
"license": "ISC",
|
|
6
|
+
"author": "",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"module": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"test": "jest",
|
|
18
|
+
"prepublishOnly": "npm run build"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/jest": "^29.5.11",
|
|
22
|
+
"@types/react": "^18.2.48",
|
|
23
|
+
"jest": "^29.7.0",
|
|
24
|
+
"ts-jest": "^29.1.1",
|
|
25
|
+
"typescript": "^5.3.3"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"react": ">=16.8.0"
|
|
29
|
+
}
|
|
30
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# @lbstack/accessx
|
|
2
|
+
|
|
3
|
+
A **TypeScript-first RBAC permission engine** with **automatic permission generation**, designed to be the **single source of truth** for:
|
|
4
|
+
|
|
5
|
+
- Backend authorization
|
|
6
|
+
- Frontend UI access control
|
|
7
|
+
- Database permission storage
|
|
8
|
+
- Admin permission management
|
|
9
|
+
|
|
10
|
+
No manual permission strings.
|
|
11
|
+
No role leakage to frontend.
|
|
12
|
+
Full autocomplete everywhere.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## โจ Key Features
|
|
17
|
+
|
|
18
|
+
- ๐ **Automatic permission generation**
|
|
19
|
+
- ๐ง **Strong TypeScript inference & autocomplete**
|
|
20
|
+
- ๐๏ธ **Single initialization โ use everywhere**
|
|
21
|
+
- ๐ฅ๏ธ Backend (Express / Nest / Hono)
|
|
22
|
+
- ๐จ Frontend (React hooks & components)
|
|
23
|
+
- ๐๏ธ Database-friendly permission keys
|
|
24
|
+
- ๐ฆ Package & framework agnostic
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## ๐งฉ Core Concept
|
|
29
|
+
|
|
30
|
+
You define:
|
|
31
|
+
|
|
32
|
+
- **Roles**
|
|
33
|
+
- **Actions**
|
|
34
|
+
- **Resources (modules)**
|
|
35
|
+
|
|
36
|
+
The package automatically generates permissions in this format:
|
|
37
|
+
|
|
38
|
+
RESOURCE_KEY:ACTION
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
|
|
42
|
+
BLOGS:CREATE
|
|
43
|
+
BLOGS:READ
|
|
44
|
+
USER:DELETE
|
|
45
|
+
|
|
46
|
+
These permission keys are:
|
|
47
|
+
|
|
48
|
+
- Stored in DB
|
|
49
|
+
- Sent to frontend after login
|
|
50
|
+
- Used in UI & API checks
|
|
51
|
+
- Fully type-safe
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## ๐ฆ Installation
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm install @lbstack/accessx
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
or
|
|
62
|
+
|
|
63
|
+
pnpm add @lbstack/accessx
|
|
64
|
+
|
|
65
|
+
๐ Initialization (Single Source of Truth)
|
|
66
|
+
import { createAccess } from "@lbstack/accessx";
|
|
67
|
+
|
|
68
|
+
export const access = createAccess({
|
|
69
|
+
roles: ["ADMIN", "EDITOR", "CUSTOMER"] as const,
|
|
70
|
+
|
|
71
|
+
actions: ["CREATE", "READ", "UPDATE", "DELETE"] as const,
|
|
72
|
+
|
|
73
|
+
resources: [
|
|
74
|
+
{
|
|
75
|
+
name: "Users",
|
|
76
|
+
key: "USER",
|
|
77
|
+
description: "User management",
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "Blogs",
|
|
81
|
+
key: "BLOGS",
|
|
82
|
+
description: "Blog posts",
|
|
83
|
+
},
|
|
84
|
+
] as const,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
โ ๏ธ as const is required for TypeScript autocomplete.
|
|
89
|
+
|
|
90
|
+
๐ Automatically Generated Permissions
|
|
91
|
+
access.permissionKeys
|
|
92
|
+
|
|
93
|
+
[
|
|
94
|
+
"USER:CREATE",
|
|
95
|
+
"USER:READ",
|
|
96
|
+
"USER:UPDATE",
|
|
97
|
+
"USER:DELETE",
|
|
98
|
+
"BLOGS:CREATE",
|
|
99
|
+
"BLOGS:READ",
|
|
100
|
+
"BLOGS:UPDATE",
|
|
101
|
+
"BLOGS:DELETE",
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
No permission strings are written manually.
|
|
106
|
+
|
|
107
|
+
๐๏ธ Database Usage
|
|
108
|
+
Seed permissions table
|
|
109
|
+
await db.permissions.insertMany(access.permissions);
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
Each permission contains metadata:
|
|
113
|
+
|
|
114
|
+
{
|
|
115
|
+
key: "BLOGS:CREATE",
|
|
116
|
+
resource: { name, key, description },
|
|
117
|
+
action: "CREATE"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
๐ Backend Usage
|
|
121
|
+
Assign permissions to roles
|
|
122
|
+
access.allow("ADMIN", "USER:DELETE");
|
|
123
|
+
access.allow("EDITOR", "BLOGS:CREATE");
|
|
124
|
+
|
|
125
|
+
Check permission (Service / Controller)
|
|
126
|
+
access.can(user.role, "BLOGS:UPDATE");
|
|
127
|
+
|
|
128
|
+
Normalize permissions from DB
|
|
129
|
+
const permissionsFromDb = ["BLOGS:READ", "USER:DELETE"];
|
|
130
|
+
|
|
131
|
+
const permissions = access.normalizePermissions(permissionsFromDb);
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
Ensures only valid generated permissions are used.
|
|
135
|
+
|
|
136
|
+
Login Response
|
|
137
|
+
res.json({
|
|
138
|
+
user,
|
|
139
|
+
permissions: access.resolvePermissions(user.role),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
๐จ Frontend Usage (React)
|
|
143
|
+
|
|
144
|
+
Frontend never uses roles.
|
|
145
|
+
It only receives resolved permissions.
|
|
146
|
+
|
|
147
|
+
useCan Hook
|
|
148
|
+
const canEdit = access.useCan(
|
|
149
|
+
auth.permissions,
|
|
150
|
+
"BLOGS:UPDATE"
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
<button disabled={!canEdit}>Edit</button>
|
|
154
|
+
|
|
155
|
+
<Can /> Component
|
|
156
|
+
<access.Can
|
|
157
|
+
permissions={auth.permissions}
|
|
158
|
+
permission="USER:DELETE"
|
|
159
|
+
>
|
|
160
|
+
<DeleteUserButton />
|
|
161
|
+
</access.Can>
|
|
162
|
+
|
|
163
|
+
๐ง Type Safety & Autocomplete
|
|
164
|
+
|
|
165
|
+
Invalid permission โ โ TypeScript error
|
|
166
|
+
|
|
167
|
+
Invalid resource/action โ โ TypeScript error
|
|
168
|
+
|
|
169
|
+
IDE auto-suggests valid permissions everywhere
|
|
170
|
+
|
|
171
|
+
// โ Invalid
|
|
172
|
+
"BLOGS:PUBLISH"
|
|
173
|
+
|
|
174
|
+
// โ
Valid
|
|
175
|
+
"BLOGS:CREATE"
|
|
176
|
+
|
|
177
|
+
๐๏ธ API Reference
|
|
178
|
+
Metadata
|
|
179
|
+
access.roles
|
|
180
|
+
access.actions
|
|
181
|
+
access.resources
|
|
182
|
+
access.permissions
|
|
183
|
+
access.permissionKeys
|
|
184
|
+
|
|
185
|
+
Backend
|
|
186
|
+
access.allow(role, permission)
|
|
187
|
+
access.can(role, permission)
|
|
188
|
+
access.resolvePermissions(role)
|
|
189
|
+
access.normalizePermissions(raw)
|
|
190
|
+
|
|
191
|
+
Frontend
|
|
192
|
+
access.useCan(permissions, permission)
|
|
193
|
+
<access.Can />
|
|
194
|
+
|
|
195
|
+
๐ Comparison with Keycloak
|
|
196
|
+
|
|
197
|
+
This package is an application-level authorization engine, not a full IAM system.
|
|
198
|
+
|
|
199
|
+
Area @accessx/core Keycloak
|
|
200
|
+
Identity โ โ
|
|
201
|
+
RBAC Core โ
โ
|
|
202
|
+
Type Safety โ
โ
|
|
203
|
+
Frontend Hooks โ
โ
|
|
204
|
+
Performance โ
โ
|
|
205
|
+
Admin UI โ โ
|
|
206
|
+
Multi-App SSO โ โ
|
|
207
|
+
Runtime Policy Change โ โ
|
|
208
|
+
Enterprise Compliance โ โ
|
|
209
|
+
Ops Overhead Low High
|
|
210
|
+
Strengths
|
|
211
|
+
|
|
212
|
+
Excellent developer experience (TypeScript-first)
|
|
213
|
+
|
|
214
|
+
Frontend hooks and React components
|
|
215
|
+
|
|
216
|
+
High performance and low latency
|
|
217
|
+
|
|
218
|
+
Embedded, framework-agnostic, zero ops
|
|
219
|
+
|
|
220
|
+
Weaknesses Compared to Keycloak
|
|
221
|
+
|
|
222
|
+
No identity or login management
|
|
223
|
+
|
|
224
|
+
No multi-application SSO
|
|
225
|
+
|
|
226
|
+
No admin UI or delegation
|
|
227
|
+
|
|
228
|
+
Permissions mostly static (redeploy needed for new resources/actions)
|
|
229
|
+
|
|
230
|
+
No ABAC / advanced policy language
|
|
231
|
+
|
|
232
|
+
No enterprise audit / compliance tooling
|
|
233
|
+
|
|
234
|
+
Ideal Usage
|
|
235
|
+
Keycloak (Authentication + Identity)
|
|
236
|
+
โ
|
|
237
|
+
JWT / Claims
|
|
238
|
+
โ
|
|
239
|
+
@accessx/core (Authorization + UI permissions)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
This is complementary, not a replacement for Keycloak.
|
|
243
|
+
|
|
244
|
+
๐ Why Use @accessx/core?
|
|
245
|
+
|
|
246
|
+
Zero manual permission creation
|
|
247
|
+
|
|
248
|
+
DB, backend & frontend always in sync
|
|
249
|
+
|
|
250
|
+
Enterprise-grade RBAC foundation for apps
|
|
251
|
+
|
|
252
|
+
Scales to ABAC, multi-role, multi-tenant systems
|
|
253
|
+
|
|
254
|
+
## ๐ง ABAC (Attribute-Based Access Control)
|
|
255
|
+
|
|
256
|
+
You can define dynamic permissions based on context (e.g., user ID, ownership checking).
|
|
257
|
+
|
|
258
|
+
### 1. Define with Conditions
|
|
259
|
+
```typescript
|
|
260
|
+
access.allow("USER", "BLOG:UPDATE", (context: { user: any; post: any }) => {
|
|
261
|
+
return context.post.authorId === context.user.id;
|
|
262
|
+
});
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### 2. Check with Context
|
|
266
|
+
The `can` method accepts an optional context object as the third argument.
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
const canEdit = access.can("USER", "BLOG:UPDATE", { user, post });
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### 3. Frontend Usage
|
|
273
|
+
React components also support `context`.
|
|
274
|
+
|
|
275
|
+
```tsx
|
|
276
|
+
<Can permissions={myPermissions} permission="BLOG:UPDATE" context={{ user, post }}>
|
|
277
|
+
<button>Edit Post</button>
|
|
278
|
+
</Can>
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## ๐งช Testing
|
|
282
|
+
|
|
283
|
+
The package includes a comprehensive test suite using Jest.
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
npm test
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
## ๐ฃ๏ธ Roadmap
|
|
290
|
+
|
|
291
|
+
Wildcard permissions (BLOGS:*)
|
|
292
|
+
|
|
293
|
+
Multi-role users
|
|
294
|
+
|
|
295
|
+
Attribute-based access control (ABAC)
|
|
296
|
+
|
|
297
|
+
Permission groups
|
|
298
|
+
|
|
299
|
+
JWT permission compression
|
|
300
|
+
|
|
301
|
+
CLI generator
|
|
302
|
+
|
|
303
|
+
๐ License
|
|
304
|
+
|
|
305
|
+
MIT
|
|
306
|
+
|
|
307
|
+
๐ก Inspiration
|
|
308
|
+
|
|
309
|
+
Zanzibar (Google)
|
|
310
|
+
|
|
311
|
+
Auth0 / Keycloak permission models
|
|
312
|
+
|
|
313
|
+
CASL & OPA (simplified DX)
|
|
314
|
+
|
|
315
|
+
One definition. One truth. Everywhere. ๐โจ
|