Functional Hierarchical Access Control Layer (ACL)
npm install @fimbul-works/aclA functional, type-safe Access Control Layer (ACL) library for TypeScript/JavaScript applications.



- 🔒 Fine-grained permissions with hierarchical pattern matching
- 🌟 Wildcard support - Use * to match any segment or resource
- ⚡ High performance - Optimized permission checking with automatic sorting
- 🔀 Functional API - Immutable operations, predictable behavior
- 📝 Type-safe - Full TypeScript support with comprehensive types
- 💾 Persistence ready - Serialize/deserialize for databases and JWTs
- 🛠️ Developer tools - Debugging, validation, and permission math utilities
- 🎯 Zero dependencies - Lightweight and easy to adopt
``bash`
npm install @fimbul-works/aclor
yarn add @fimbul-works/aclor
pnpm install @fimbul-works/acl
`typescript
import {
createPermissions,
checkPermission,
serializePermissions,
deserializePermissions
} from "@fimbul-works/acl";
// Define permissions
const permissions = createPermissions([
["allow", "discussion.*.read"], // Allow reading any discussion
["allow", "user.1234.*"], // Allow user 1234 to do anything
["deny", "admin.*"], // Deny all admin access
["allow", "*.public.view"], // Allow viewing public resources
]);
// Check permissions
checkPermission(permissions, "discussion.abc.read"); // true
checkPermission(permissions, "admin.panel"); // false
checkPermission(permissions, "user.1234.edit"); // true
// Serialize for storage (databases, JWTs, etc.)
const serialized = serializePermissions(permissions);
// "deny:admin.,allow:.public.view,allow:discussion..read,allow:user.1234."
// Deserialize back to permissions
const restored = deserializePermissions(serialized);
`
Permissions use dot-separated patterns with wildcards:
- * - Root access (matches everything)user.*
- - Matches any resource under user namespace (e.g., user.read, user.profile.edit)*.public
- - Matches resources ending with .public (e.g., user.public, admin.public)user.*.read
- - Matches user read operations (e.g., user.1234.read, user.profile.read)discussion.1234.moderate
- - Exact match only
Permissions are evaluated in this order:
1. Deny overrides Allow - A deny always wins over an allow
2. Root access - * patterns have special handling
3. Specific over General - More specific patterns take precedence
`typescript
const permissions = createPermissions([
["allow", "*"], // Allow everything
["deny", "admin.*"], // But deny admin
]);
checkPermission(permissions, "user.read"); // true (root allow)
checkPermission(permissions, "admin.panel"); // false (deny wins)
`
`typescript
import {
createPermission,
createPermissions,
addPermission,
removePermission
} from "@fimbul-works/acl";
// Create single permission
const perm = createPermission("allow", "user.*.read");
// Create multiple permissions
const permissions = createPermissions([
["allow", "discussion.*.read"],
["deny", "admin.*"],
]);
// Add permission (returns new array)
const updated = addPermission(permissions, "allow", "user.*.write");
// Remove permission
const filtered = removePermission(permissions, "deny", "admin.*");
`
`typescript
import {
checkPermission,
checkPermissions,
getMatchingPermissions,
hasRootAccess
} from "@fimbul-works/acl";
// Check single resource
checkPermission(permissions, "user.1234.read"); // boolean
// Check multiple resources (batch)
const results = checkPermissions(permissions, [
"user.1234.read",
"admin.panel",
"discussion.abc.read",
]);
// [true, false, true]
// Get all matching permissions (for debugging)
const matches = getMatchingPermissions(permissions, "discussion.abc.read");
// Check for root access
hasRootAccess(permissions); // boolean
`
`typescript
import {
mergePermissions,
sortPermissions,
filterPermissionsByType
} from "@fimbul-works/acl";
// Merge multiple permission arrays (with deduplication)
const userPerms = createPermissions([["allow", "user.*.read"]]);
const adminPerms = createPermissions([["allow", "*"]]);
const merged = mergePermissions(userPerms, adminPerms);
// Sort by precedence (deny before allow, general before specific)
const sorted = sortPermissions(permissions);
// Filter by type
const allowPerms = filterPermissionsByType(permissions, "allow");
const denyPerms = filterPermissionsByType(permissions, "deny");
`
`typescript
import {
serializePermissions,
deserializePermissions
} from "@fimbul-works/acl";
// Serialize to string
const data = serializePermissions(permissions);
// "deny:admin.,allow:user..read"
// Deserialize from string
const restored = deserializePermissions(data);
`
`typescript
import {
intersectPermissions,
subtractPermissions
} from "@fimbul-works/acl";
// Find common permissions across multiple arrays
const common = intersectPermissions(roleA, roleB, roleC);
// Subtract permissions (what's in A but not in B)
const diff = subtractPermissions(adminPerms, userPerms);
`
`typescript
import {
validatePermissionPattern,
expandPermissions
} from "@fimbul-works/acl";
// Validate pattern without throwing
const validation = validatePermissionPattern("user.*.read");
// { valid: true }
const bad = validatePermissionPattern("");
// { valid: false, error: "Permission pattern cannot be empty" }
// Expand wildcards to see what they match
const expanded = expandPermissions(permissions, [
"user.1234.read",
"admin.panel",
"discussion.abc.read",
]);
// [
// { permission: {...}, matches: ["user.1234.read"] },
// { permission: {...}, matches: ["admin.panel"] }
// ]
`
`typescript
// Define role-based permissions
const roles = {
guest: createPermissions([
["allow", "*.public.view"],
]),
user: createPermissions([
["allow", "user.*.read"],
["allow", "user.*.edit"],
["allow", "discussion.*.read"],
["allow", "discussion.*.comment"],
["deny", "user.*.delete"],
]),
moderator: createPermissions([
["allow", "discussion.*.read"],
["allow", "discussion.*.moderate"],
["allow", "discussion.*.delete"],
["allow", "user.*.read"],
["allow", "user.*.suspend"],
["deny", "admin.*"],
]),
admin: createPermissions([
["allow", "*"], // Root access
]),
};
// Check user permissions
function canAccess(userRole: keyof typeof roles, resource: string): boolean {
return checkPermission(roles[userRole], resource);
}
canAccess("user", "user.1234.edit"); // true
canAccess("user", "user.1234.delete"); // false
canAccess("moderator", "discussion.123.delete"); // true
canAccess("guest", "admin.panel"); // false
`
`typescript
import jwt from "jsonwebtoken";
import { createPermissions, serializePermissions, deserializePermissions } from "@fimbul-works/acl";
// When creating JWT
const token = jwt.sign({
sub: "user123",
permissions: serializePermissions(createPermissions([
["allow", "user.*.read"],
["allow", "user.1234.*"],
])),
}, "secret");
// When verifying JWT
const decoded = jwt.verify(token, "secret") as { permissions: string };
const permissions = deserializePermissions(decoded.permissions);
if (checkPermission(permissions, "user.1234.edit")) {
// Grant access
}
`
`typescript
import { createPermissions, serializePermissions, deserializePermissions } from "@fimbul-works/acl";
// Save to database
await db.users.update({
where: { id: 123 },
data: {
permissions: serializePermissions(userPermissions),
},
});
// Load from database
const user = await db.users.findUnique({ where: { id: 123 } });
const permissions = deserializePermissions(user.permissions);
`
`typescript
import { mergePermissions, checkPermission } from "@fimbul-works/acl";
// User can have multiple roles
const userRoles = [
createPermissions([["allow", "content.*.read"]]), // Editor
createPermissions([["allow", "content.*.publish"]]), // Publisher
createPermissions([["deny", "content.*.delete"]]), // Cannot delete
];
// Merge all roles
const merged = mergePermissions(...userRoles);
checkPermission(merged, "content.123.read"); // true
checkPermission(merged, "content.123.publish"); // true
checkPermission(merged, "content.123.delete"); // false (deny wins)
`
The library is optimized for performance:
- Automatic sorting - Permissions are sorted by precedence for early-exit optimization
- O(n) complexity - Linear time permission checking
- Efficient pattern matching - Pre-split pattern segments
- Batch operations - Check multiple resources in one call
Benchmark with 1000 permissions:
`typescriptresource.${i}.action
// Checking 100 resources
const start = Date.now();
for (let i = 0; i < 100; i++) {
checkPermission(permissions, );${Date.now() - start}ms
}
console.log(); // Typically < 10ms`
- createPermission(type, pattern) - Create single permissioncreatePermissions(rules)
- - Create multiple permissionsaddPermission(permissions, type, pattern)
- - Add permissionremovePermission(permissions, type, pattern)
- - Remove permissioncheckPermission(permissions, resource)
- - Check single resourcecheckPermissions(permissions, resources)
- - Check multiple resourcesmergePermissions(...arrays)
- - Merge and deduplicatesortPermissions(permissions)
- - Sort by precedencehasRootAccess(permissions)
- - Check for root accessgetMatchingPermissions(permissions, resource)
- - Get all matches
- serializePermissions(permissions) - Convert to stringdeserializePermissions(data)
- - Parse from stringintersectPermissions(...arrays)
- - Find common permissionssubtractPermissions(base, remove)
- - Remove permissionsfilterPermissionsByType(permissions, type)
- - Filter by typeexpandPermissions(permissions, resources)
- - Show pattern matchesvalidatePermissionPattern(pattern)
- - Validate without throwing
This library is written in TypeScript and provides excellent type inference:
`typescript
import { Permission, PermissionType } from "@fimbul-works/acl";
const type: PermissionType = "allow"; // "allow" | "deny"
const permission: Permission = {
type: "allow",
pattern: "user.*.read",
parts: ["user", "*", "read"],
isRoot: false,
};
``
MIT License - See LICENSE file for details.
Contributions are welcome! Please feel free to submit a Pull Request.
---
Built with ⚡ by FimbulWorks