@dosmike/hacs
v1.1.2
Published
Hierarchical Authorization Capability System for TypeScript and React.
Readme
HACS Node.js
Hierarchical Authorization Capability System helpers for TypeScript and React.
Install
npm install @dosmike/hacsReact is a peer dependency when using the React entry point.
Permission Model
Permissions are dot-separated keys. A broader grant applies to deeper permissions unless a more specific matching grant overrides it.
import { Permission, PermissionGrants, test } from '@dosmike/hacs';
const grants = PermissionGrants({
project: 'allow',
'project.delete': 'deny',
'project.delete.owner': 'allow',
});
test(grants, Permission('project.delete.owner')); // true
test(grants, Permission('project.delete.member')); // falseGrant values can be:
allow: permits the permission.deny: denies the permission.inherit: skips this grant and falls back to the next matching grant.
Boolean values are also accepted as aliases: true is allow, and false is
deny. undefined and null are treated as inherit.
The root grant key * matches every permission. If no grant matches, HACS
denies by default.
Core API
import {
test,
explain,
explainPermission,
Permission,
PermissionGrants,
mergeGrants,
resolvePermission,
} from '@dosmike/hacs';Permission strings are tagged with Permission(...) before checking. Grant
objects are normalized with PermissionGrants(...).
Permissions follow this syntax:
permission = node ( '.' node )*
node = [a-zA-Z0-9]+Grant keys use the same syntax, with one addition: * is allowed as the root
grant and matches every permission.
Typed Checked Permissions
Checked permissions can be constrained to a user-defined literal collection. This does not affect grant storage; it only tightens app-authored permission checks at compile time.
type AppPermission =
| 'project.read'
| 'project.delete.owner';
const permission = Permission<AppPermission>('project.delete.owner');
// Type error:
Permission<AppPermission>('project.delete.member');test(grants, permission)
Returns true when the most specific matching grant is allow.
test(PermissionGrants({ project: 'allow' }), Permission('project.update.owner'));resolvePermission(grants, permission)
Returns the matched grant, or null when no grant applies.
explain(grants, permission)
Returns a human-readable explanation of the permission decision.
explainPermission(grants, permission)
Returns a structured explanation object with the decision, matched grant, and considered grants.
mergeGrants(grantSets)
Combines grant sets in order. Later grants with the same key override earlier
grants unless the later value is inherit.
React API
import { Permission, PermissionGrants } from '@dosmike/hacs';
import { HACSProvider, If, useHACS } from '@dosmike/hacs/react';
function App() {
return (
<HACSProvider
grants={PermissionGrants({
project: 'allow',
'project.delete': 'deny',
})}
>
<ProjectActions />
</HACSProvider>
);
}
function ProjectActions() {
const { can } = useHACS(PermissionGrants({
'project.delete.owner': 'allow',
}));
return (
<If test={can(Permission('project.delete.owner'))}>
<button type="button">Delete project</button>
</If>
);
}HACSProvider accepts normalized grants or raw grant objects. useHACS()
checks against the nearest provider grants. useHACS(localGrants) merges the
local grants after the provider grants, so same-key local grants override the
provider value unless the local value is inherit.
Nested providers replace the context for their children. They do not automatically merge with outer providers.
The hook can also be typed with the same checked permission collection:
type AppPermission = 'project.read' | 'project.delete.owner';
const { can } = useHACS<AppPermission>();
can(Permission<AppPermission>('project.delete.owner'));Development
npm install
npm run typecheck
npm test
npm run buildThe package builds with tsup, tests with vitest, and exposes separate core
and React entry points.
