ARBAC
This section answers: what does Aooth's authorization layer actually do, and which package gives me which piece of it? It covers @aooth/arbac-core (the zero-dependency engine) and @aooth/arbac (the batteries-included layer of builders, privilege factories, scope mergers, and codegen).
What ARBAC is
ARBAC stands for Advanced (or Attribute-aware) Role-Based Access Control. It keeps the familiar RBAC shape — a role grants permission to perform an action on a resource — and adds three things that classic RBAC libraries leave to userland:
- Wildcard matching. A single rule can target many resources or actions via
*(single dotted segment) and**(cross-segment) patterns. - Dynamic scopes. An allow rule may attach a
scope(userAttrs, userId)callback whose return value is treated as a data-level filter by the calling code. The engine never inspects the scope object — it just collects scopes from every matching allow rule into a UNION so the caller can OR them into a SQL/Mongo filter. - Deny-wins precedence. A matching
denyrule on any of the user's roles vetoes everyallowrule — regardless of specificity. There is no precedence weighting.
Together those three turn classic RBAC ("can Alice read articles?") into ARBAC ("can Alice read articles, and which articles?"). The engine answers both questions in one evaluate() call.
The package split
Two packages, one engine. The split is deliberate — the engine is tiny and has no dependencies, so you can embed it anywhere; the second package is where the ergonomics live.
| Package | Role |
|---|---|
@aooth/arbac-core | The evaluation engine. class Arbac, arbacPatternToRegex, and the rule/role/eval-result types. Zero dependencies. |
@aooth/arbac | Re-exports everything from arbac-core, then layers on defineRole(), definePrivilege(), the allowTable* family, scope-merge utilities, and the aoothjs-arbac-codegen CLI. Single dependency: @aooth/arbac-core. |
In practice
Almost every consumer installs @aooth/arbac only — it re-exports the core API, so you never need to import from arbac-core directly. The split exists so the engine stays embeddable.
Where to start
| If you want to… | Read |
|---|---|
| Understand the vocabulary and the allow/deny algorithm | Mental Model |
Use the engine directly with hand-rolled TArbacRole literals | Core Engine |
| Build roles with a chainable, generic-aware API | Builder API |
| Bundle related rules into reusable named units | Privilege Factories |
| UNION scopes from multiple roles at query time | Scope Merging |
| Generate TypeScript types from a roles array | Codegen |
Where the framework glue lives
@aooth/arbac is framework-agnostic. The Moost-specific layer — @ArbacAuthorize, the useArbac() composable, role-aware AsDbController, and the ArbacDbScope shape that ties projections + filters + Uniquery controls together — lives in @aooth/arbac-moost. See Moost Integration → ARBAC Authorize.
This section stays in the framework-agnostic layer. Everything you read here works the same whether you embed the engine in an Express app, a CLI, or a Moost project.
The 30-second example
// minimal
import { Arbac, defineRole } from "@aooth/arbac";
const editor = defineRole<{ dept: string }, { dept: string }>()
.id("editor")
.allow("articles", "read")
.allow("articles", "update", (attrs) => ({ dept: attrs.dept }))
.deny("articles", "publish")
.build();
const arbac = new Arbac();
arbac.registerRole(editor);
const result = await arbac.evaluate(
{ resource: "articles", action: "update" },
{ id: "u1", roles: ["editor"], attrs: { dept: "sales" } },
);
// → { allowed: true, scopes: [{ dept: 'sales' }] }Three things to notice already, before moving on to the Mental Model:
- The role declares what the user may do (read, update) and on which data (the scope filter).
- The engine returns one boolean and an array of scopes. The caller decides how to apply them to a query.
- A subsequent
.deny('articles', 'publish')would not be cancelled by any number of.allow()calls on the same resource/action.