@sarfarajey/rbac-merge
v1.0.0
Published
Salesforce-style RBAC: profile baseline + additive permission-set overrides. Generic over role and feature names. Zero dependencies.
Downloads
85
Maintainers
Readme
@sarfarajey/rbac-merge
Tiny RBAC primitive shaped after the Salesforce Profile + Permission Set model:
- A profile baseline — a static per-role table of granted capabilities, defined in code and shipped with the application.
- Permission set overrides — per-user, additive grants typically loaded from a database.
- Effective permissions = union of the two. Overrides can only add; never remove.
Generic over role and feature names. Zero dependencies.
Install
npm install @sarfarajey/rbac-mergeDefine your roles and features in code
import type { RoleBaseline } from '@sarfarajey/rbac-merge';
type Role = 'guest' | 'user' | 'admin';
type Feature = 'posts' | 'admin' | 'profile';
const baseline: RoleBaseline<Role, Feature> = {
guest: {
posts: ['view'],
},
user: {
posts: ['view', 'create'],
profile: ['view_own', 'edit_own'],
},
admin: {
posts: ['view', 'create', 'delete'],
profile: ['view_own', 'edit_own', 'view_others'],
admin: ['view_dashboard', 'manage_users'],
},
};Check a capability
import { hasPermission } from '@sarfarajey/rbac-merge';
hasPermission<Role, Feature>('user', 'posts', 'create', { baseline });
// → true
hasPermission<Role, Feature>('user', 'posts', 'delete', { baseline });
// → false
hasPermission<Role, Feature>(null, 'posts', 'view', {
baseline,
defaultRole: 'guest',
});
// → truePer-user overrides
A specific user may have extra grants stored in the database — pass them in:
const userOverrides = {
posts: ['delete'], // grant delete on posts
admin: ['view_dashboard'], // unlock the admin dashboard
};
hasPermission<Role, Feature>('user', 'posts', 'delete', {
baseline,
overrides: userOverrides,
});
// → true (granted via override)Overrides add only — they cannot remove capabilities the baseline grants.
Resolved map
import { getPermissions } from '@sarfarajey/rbac-merge';
getPermissions<Role, Feature>('user', { baseline, overrides: userOverrides });
// {
// posts: ['view', 'create', 'delete'],
// profile: ['view_own', 'edit_own'],
// admin: ['view_dashboard'],
// }Plain merge
If you already have the role's baseline in hand and just want the merge:
import { mergePermissions } from '@sarfarajey/rbac-merge';
mergePermissions(baseline.user, userOverrides);Security invariants worth knowing
Unknown role → denied. If you call
hasPermission('developer', ...)and'developer'is not in your baseline, it is denied. It does not silently fall back todefaultRole. Onlynull/undefinedusedefaultRole. This matters when role strings come from untrusted sources (cookies, JWT claims): an attacker writing a bogus role name can never inheritguest's permissions.Adding a capability to the baseline takes effect immediately. No need to migrate per-user override records when you add
posts.archiveto theadminbaseline — every admin gets it on the next deploy.Overrides cannot revoke. If you need to revoke a baseline capability, change the baseline. The data model is intentionally additive to avoid the "forgot to update the override" failure mode.
License
MIT
