| .. | ||
| dist-in | ||
| docs | ||
| src | ||
| tests | ||
| .gitignore | ||
| eslint.config.js | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
@polymech/acl
Role-based access control (RBAC) library inspired by Zend_ACL.
Source Repository: polymech/mono | Used in Filebrowser at https://service.polymech.info/.
Pure ESM, async/await, zero runtime dependencies (except pino for optional logging).
Supabase integration? See Supabase RLS Mirror — VFS Guide
Pure Postgres? See Postgres RLS — VFS Guide
Source Layout
src/
├── Acl.ts # Core ACL class
├── interfaces.ts # Type definitions & backend interface
├── index.ts # Barrel export
├── data/
│ ├── MemoryBackend.ts # In-memory backend (default)
│ └── FileBackend.ts # JSON file backend (dev/testing)
├── vfs/ # Virtual filesystem extensions
│ ├── AclVfsClient.ts # Guarded VFS bindings
│ ├── DecoratedVfsClient.ts # Extensible client wrapper
│ └── vfs-acl.ts # VFS permission settings loader
└── acl.test.ts # Functional tests (vitest)
Concepts
| Term | Description |
|---|---|
| User | An identifier (string or number) representing a subject |
| Role | A named group of permissions assigned to users |
| Resource | Something being protected (e.g. "posts", "settings") |
| Permission | An action on a resource (e.g. "read", "write", "delete") |
| Parent | Roles can inherit permissions from parent roles |
Wildcard "*" grants all permissions on a resource.
Quick Start
import { Acl, MemoryBackend } from '@polymech/acl';
const acl = new Acl(new MemoryBackend());
// Define permissions
await acl.allow('viewer', 'posts', 'read');
await acl.allow('editor', 'posts', ['read', 'write', 'delete']);
await acl.allow('admin', 'settings', '*');
// Assign roles to users
await acl.addUserRoles('alice', 'editor');
await acl.addUserRoles('bob', 'viewer');
// Check access
await acl.isAllowed('alice', 'posts', 'write'); // true
await acl.isAllowed('bob', 'posts', 'write'); // false
await acl.isAllowed('bob', 'posts', 'read'); // true
Role Hierarchy
Roles can inherit from parents. A child role gets all permissions of its ancestors.
await acl.allow('viewer', 'docs', 'read');
await acl.allow('editor', 'docs', 'write');
await acl.allow('admin', 'docs', 'admin');
// editor inherits from viewer, admin inherits from editor
await acl.addRoleParents('editor', 'viewer');
await acl.addRoleParents('admin', 'editor');
await acl.addUserRoles('carol', 'admin');
await acl.isAllowed('carol', 'docs', 'read'); // true (from viewer)
await acl.isAllowed('carol', 'docs', 'write'); // true (from editor)
await acl.isAllowed('carol', 'docs', 'admin'); // true (own)
Batch Grants
Use the array syntax to define complex permission sets at once:
await acl.allow([
{
roles: 'moderator',
allows: [
{ resources: 'posts', permissions: ['read', 'edit', 'flag'] },
{ resources: 'comments', permissions: ['read', 'delete'] },
],
},
{
roles: 'author',
allows: [
{ resources: 'posts', permissions: ['read', 'create'] },
],
},
]);
Querying Permissions
// All permissions a user has on given resources
const perms = await acl.allowedPermissions('alice', ['posts', 'settings']);
// → { posts: ['read', 'write', 'delete'], settings: [] }
// Which resources does a role have access to?
const resources = await acl.whatResources('editor');
// → { posts: ['read', 'write', 'delete'] }
// Which resources grant a specific permission?
const writable = await acl.whatResources('editor', 'write');
// → ['posts']
Removal
// Remove specific permissions
await acl.removeAllow('editor', 'posts', 'delete');
// Remove all permissions for a resource from a role
await acl.removeAllow('editor', 'posts');
// Remove a role entirely
await acl.removeRole('editor');
// Remove a resource from all roles
await acl.removeResource('posts');
// Remove a role from a user
await acl.removeUserRoles('alice', 'editor');
File Backend (Dev/Testing)
FileBackend extends MemoryBackend with JSON persistence:
import { Acl, FileBackend } from '@polymech/acl';
const backend = new FileBackend('./acl-data.json');
backend.read(); // load from disk
const acl = new Acl(backend);
await acl.allow('admin', 'all', '*');
await acl.addUserRoles('root', 'admin');
backend.write(); // persist to disk
Logging
Pass a pino logger as the second constructor argument:
import pino from 'pino';
import { Acl, MemoryBackend } from '@polymech/acl';
const logger = pino({ level: 'debug' });
const acl = new Acl(new MemoryBackend(), logger);
await acl.allow('admin', 'posts', '*');
// logs: { roles: ['admin'], resources: ['posts'], permissions: ['*'] } allow
Custom Backend
Implement the IBackend interface to plug in any storage:
import type { IBackend, Value, Values } from '@polymech/acl';
class RedisBackend implements IBackend<RedisTx> {
begin(): RedisTx { /* ... */ }
async end(tx: RedisTx): Promise<void> { /* ... */ }
async clean(): Promise<void> { /* ... */ }
async get(bucket: string, key: Value): Promise<string[]> { /* ... */ }
async union(bucket: string, keys: Value[]): Promise<string[]> { /* ... */ }
async unions(buckets: string[], keys: Value[]): Promise<Record<string, string[]>> { /* ... */ }
add(tx: RedisTx, bucket: string, key: Value, values: Values): void { /* ... */ }
del(tx: RedisTx, bucket: string, keys: Values): void { /* ... */ }
remove(tx: RedisTx, bucket: string, key: Value, values: Values): void { /* ... */ }
}
Real-World Example — VFS Per-User Folder Permissions
Fine-grained, path-scoped access control for a virtual filesystem where each user owns a folder and can grant specific permissions to others — individually or via groups.
How It Works
Each user's VFS folder contains a vfs-settings.json that declares who can do what:
{
"owner": "3bb4cfbf-318b-44d3-a9d3-35680e738421",
"groups": [
{
"name": "team",
"members": ["aaaaaaaa-...", "cccccccc-..."]
},
{
"name": "viewers",
"members": ["dddddddd-...", "ffffffff-..."]
}
],
"acl": [
{
"group": "team",
"path": "/shared",
"permissions": ["read", "write", "list", "mkdir", "delete"]
},
{
"group": "team",
"path": "/docs",
"permissions": ["read", "list"]
},
{
"group": "viewers",
"path": "/docs",
"permissions": ["read", "list"]
},
{
"userId": "ffffffff-...",
"path": "/shared",
"permissions": ["read", "list"]
}
]
}
- Groups bundle users — grant once, apply to all members.
- Direct user grants coexist with groups for individual overrides.
- Path scoping — grants apply to a specific folder and its children (
/shared,/docs). - The owner always gets
*on/(entire tree).
Permission Model
| Permission | Operations |
|---|---|
read |
stat, readfile, exists |
list |
readdir |
write |
writefile, mkfile |
mkdir |
mkdir |
delete |
rmfile, rmdir |
rename |
rename |
copy |
copy |
* |
All of the above — auto-granted to the folder owner |
Path-Scoped Resolution
When a user accesses /shared/reports/q1.pdf, the guard walks the resource chain upward:
vfs:<ownerId>:/shared/reports/q1.pdf ← most specific
vfs:<ownerId>:/shared/reports
vfs:<ownerId>:/shared ← team group matches here ✓
vfs:<ownerId>:/
Access is granted if any level allows the operation.
Setup Flow
sequenceDiagram
participant App
participant Bridge as VFS ACL Bridge
participant ACL as Acl Instance
App->>Bridge: loadVfsSettings acl, userDir
Bridge->>Bridge: Read vfs-settings.json
Bridge->>ACL: allow owner role, resource, wildcard
Bridge->>ACL: addUserRoles owner, owner role
loop Each group ACL entry
Bridge->>ACL: allow group role, resource, permissions
loop Each group member
Bridge->>ACL: addUserRoles member, group role
end
end
loop Each direct ACL entry
Bridge->>ACL: allow grant role, resource, permissions
Bridge->>ACL: addUserRoles grantee, grant role
end
Bridge-->>App: Return settings
Code Example
import { Acl, MemoryBackend, AclVfsClient, loadVfsSettings } from '@polymech/acl';
// 1. Create ACL and load settings from the user's folder
const acl = new Acl(new MemoryBackend());
const userDir = './data/vfs/3bb4cfbf-318b-44d3-a9d3-35680e738421';
await loadVfsSettings(acl, userDir);
// 2. Create a guarded client for a specific caller
const ownerId = '3bb4cfbf-318b-44d3-a9d3-35680e738421';
const callerId = 'aaaaaaaa-1111-2222-3333-bbbbbbbbbbbb'; // team member
const client = new AclVfsClient(acl, ownerId, callerId, {
root: userDir,
});
// 3. Allowed — team has read+list on /docs
const files = await client.readdir('docs'); // ✓ works
const exists = await client.exists('docs/readme.txt'); // ✓ works
// 4. Allowed — team has full access on /shared
await client.writefile('shared/notes.txt', 'hello'); // ✓ works
await client.mkdir('shared/reports'); // ✓ works
// 5. Denied — team has no access on /private
await client.readdir('private'); // ✗ throws EACCES
await client.writefile('private/x.txt', '...'); // ✗ throws EACCES
// 6. Denied — team has read-only on /docs, no write
await client.writefile('docs/hack.txt', '...'); // ✗ throws EACCES
Test Fixtures
tests/
├── vfs/root/
│ ├── 3bb4cfbf-…/
│ │ ├── vfs-settings.json # Path-scoped direct grants
│ │ ├── docs/readme.txt
│ │ ├── shared/data.txt
│ │ └── private/secret.txt
│ ├── groups-test/
│ │ └── vfs-settings.json # Group grants + mixed direct
│ ├── edge-cases/ # Edge case fixtures
│ │ └── vfs-settings.json
│ └── anon-test/ # Anonymous access fixtures
│ └── vfs-settings.json
├── vfs-acl.e2e.test.ts # 26 permission boundary tests
├── vfs-acl-fs.e2e.test.ts # 24 real filesystem tests
├── vfs-acl-paths.e2e.test.ts # 34 per-path nested tests
├── vfs-acl-groups.e2e.test.ts # 27 groups e2e tests
├── vfs-acl-edge.e2e.test.ts # 18 edge case tests
├── vfs-acl-decorated.e2e.test.ts # 15 decorated VFS tests
└── vfs-acl-anonymous.e2e.test.ts # 8 anonymous access tests
Scripts
npm run build # Compile TypeScript
npm run dev # Watch mode
npm run test:core # Core ACL tests (23)
npm run test:all # Full suite (175 tests)
npm run lint # ESLint
References
1. Virtual Filesystems and OS Integration
- libFUSE (Linux): https://github.com/libfuse/libfuse
- macFUSE (macOS): https://osxfuse.github.io/
- WinFSP (Windows): https://winfsp.dev/
- Dokany (Windows): https://dokan-dev.github.io/
- GVfs (GNOME): https://wiki.gnome.org/Projects/gvfs
- KIO (KDE): https://develop.kde.org/docs/kio/
- WebDAV RFC 4918: https://www.rfc-editor.org/rfc/rfc4918
- Rclone: https://rclone.org/
- IPFS/Kubo FUSE: https://docs.ipfs.tech/concepts/file-systems/#mfs
- SSHFS: https://github.com/libfuse/sshfs
2. Browser and PWA Primitives
- File System Access API: https://developer.mozilla.org/en-US/docs/Web/API/File_System_Access_API
- OPFS guide: https://web.dev/opfs/
- IndexedDB: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API
- Service Workers: https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API
- Background sync: https://web.dev/periodic-background-sync/
- WebRTC Data Channels: https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API
- WebTransport (QUIC): https://developer.mozilla.org/en-US/docs/Web/API/WebTransport_API
- BrowserFS: https://github.com/jvilk/BrowserFS
- LightningFS: https://github.com/isomorphic-git/lightning-fs
- IDBFS (Emscripten): https://emscripten.org/docs/api_reference/Filesystem-API.html
3. Decentralized Storage, Sync, and Collaboration
- IPFS: https://ipfs.tech/
- IPNS: https://docs.ipfs.tech/concepts/ipns/
- OrbitDB: https://orbitdb.org/
- Peergos: https://peergos.org/
- WNFS: https://github.com/fission-codes/wnfs
- Hypercore Protocol: https://hypercore-protocol.org/
- Hyperdrive: https://github.com/holepunchto/hyperdrive
- Hyperswarm: https://github.com/hyperswarm/hyperswarm
- Secure Scuttlebutt (SSB): https://scuttlebutt.nz/
- Manyverse: https://www.manyver.se/
- Tahoe-LAFS: https://tahoe-lafs.org/
- Syncthing: https://syncthing.net/
- Nextcloud: https://nextcloud.com/
- Solid pods: https://solidproject.org/
4. Local‑First Data Structures and Collaboration Engines
- Yjs: https://yjs.dev/
- Automerge: https://automerge.org/
- PouchDB: https://pouchdb.com/
- CouchDB: https://couchdb.apache.org/
- ElectricSQL: https://electric-sql.com/
- Replicache: https://replicache.dev/
5. Identity and Capability‑Based Access
- Decentralized Identifiers (DIDs): https://www.w3.org/TR/did-core/
- UCAN: https://ucan.xyz/
- WebAuthn/passkeys: https://www.w3.org/TR/webauthn-2/
- Matrix: https://matrix.org/
- Nostr: https://github.com/nostr-protocol/nostr
This wonderful package has been brought to you by the people who get shit done :)