Changelog
All notable changes to this project will be documented in this file. The format is based on Keep a Changelog and this project adheres to Semantic Versioning.
3.0.0 (2026-06-14)
Section titled “3.0.0 (2026-06-14)”The release that turns AccessControl from RBAC-with-attribute-filtering into a
full policy engine — landing the capabilities most requested since v2:
attribute conditions (ABAC), real ownership enforcement, custom (non-CRUD)
actions, deny-overrides, mandatory restriction gates, role groups & resource
categories, async checks, an audit event stream, one-call serialization, and a
hardened authorization path. The everyday API (grant/deny, can,
createAny/readOwn/…, permission.granted / .attributes / .filter()) is
unchanged — see MIGRATION for the few breaking points and
WHATS-NEW for worked examples.
- Conditions (ABAC) —
.where(). Attach a declarative condition that decides whether a grant applies at check time, written as readable string sugar or canonical JSON. Operators:==!=>>=<<=,in,contains,matches,startsWith,endsWith,before/after/between, andcidr; combine with{ and, or, not }. The time helper$.now.*is auto-injected (overridable for deterministic tests). (fixes #35, #41) - Per-check context. Supply condition data via
can(role, context), the fluent.with(context), orcheck({ context }); ambient defaults vianew AccessControl(grants, { context }). - Object-form checks —
check()/checkAsync(). Resolve anIQueryInfo({ role, resource, action, context }) straight to aPermission— the programmatic alternative to the fluentcan(...).readAny(...)chain. - Permission result carries
granted/grantedAsync,attributes,roles,resource,action,possession(the resolved possession —anywhen anownquery is satisfied via ananygrant), andfilter(data)to strip a payload (object or array of objects) to the allowed attributes. (fixes #23, #36) - Enforced ownership.
ownpermissions now actually verify the record belongs to the requester. Configure once withpolicy.ownerFieldor a custompolicy.owner(ctx)resolver; backward-compatible (with no resolver configured,ownkeeps v2 behavior). A blanketanygrant still satisfiesownvia the cascade. (fixes #14, #24) - Custom (non-CRUD) actions —
.action()/.do(). Any action name works with the full possession / ownership / condition machinery (e.g.action('publish:own', 'article'))..do()is the one sanctioned alias and also covers CRUD. (fixes #87, #46) - Mandatory gates —
.require(). Independent restriction gates that can only narrow access:granted = (a grant matches) AND (every applicable gate passes). Scoped globally, percategory(), or perresource(). Exported/imported viagetRequirements(). - Role groups & resource categories (
/). Declare a vocabulary withsetup({ roles, resources, actions }), then grant to a group or category once and reach every member dynamically — a bounded, collision-free alternative to*(media/photo ≠ legal/photo). Introspect withgroup(),category(),getGroups(),getCategories(),getVocabulary(); manage withremoveGroup(),removeCategory(). (fixes #58, #103) - Async checks & custom functions. Register logic with
defineCondition(name, fn)and reference it from a grant or gate as{ fn, args }(stays JSON-serializable). Resolve viagrantedAsync/checkAsync; declarative checks stay synchronous. - Events & audit hooks. A built-in, dependency-free emitter:
accessfires on every resolved check (granted and denied, with a denialreason),changetracks policy edits,errorreports faults. Listeners are observational and isolated — a throwing listener never breaks a check.on()/off()/once(), with theAccessControlEventenum. - One-call serialization —
snapshot()/restore().snapshot()captures grants + gates + vocabulary as one plain-JSON object;restore()rebuilds the model exactly (a full replace through the validatedsetGrants()/setup()/require()paths). SurvivesJSON.stringify/ aJSONBcolumn. - Serialization primitives.
getGrantsList()(DB-friendly flat rows) ⇄getGrants()(object form),getRequirements()(gates), andgetVocabulary()(thesetup()input). The constructor accepts both object and list forms. - Structured options —
new AccessControl(grants, { engine, policy, context }). Three buckets:engine(mechanics & security —pathPrefix,allowRegex,charset,safeErrors),policy(your model —ownerField/owner,strictwithroles/checks/actions/resourcesswitches, plus action & resource allow-lists), andcontext(ambient condition data). - Policy lifecycle —
lock()/reset(). Freeze a finalized model against further edits, or clear it for a clean rebuild. - Introspection & management.
getActions()(all actions, or scoped to a role incl. inherited — fixes #33),getGroups()/getCategories(),hasGroup()/hasCategory()/hasRole()/hasResource(),getInheritedRolesOf(),removeGroup()/removeCategory()/removeRoles()/removeResources(). - New named exports.
Charset,ErrorCode,AccessControlEvent, and theAccessControlErrorclass, alongsideAction/Possession— now real enums. (fixes #90)
Changed
Section titled “Changed”- Grants model shape. Possession is now a field and each action maps to an
array of rules (
{ possession, attributes, effect?, condition? }), so a single action can carry conditions, deny rules, and multiple coexisting rules — none of which the v2 attribute-list value had a slot for. Re-export once (getGrants()/getGrantsList()) to migrate persisted v2 data.// v2 — possession fused into the key; value is just an attribute list (one rule):{ user: { post: { 'read:any': ['*'] } } }// v3 — action key → array of rules; possession/effect/condition are fields,// so this is now representable (and v2 simply could not store it):{ user: { post: { read: [{ possession: 'any', attributes: ['*', '!secret'] },{ possession: 'own', attributes: ['*'], condition: '$.post.status != "locked"' },{ effect: 'deny', possession: 'any', attributes: ['ssn'] }] } } } - Inheritance in the flat list travels as
$extendrows ({ role, $extend: [...] }). The programmaticextend()/extendRole()form is unchanged. - Name handling is case-preserving and charset-validated (
[A-Za-z0-9_-]).Adminandadminare now distinct;:/$, spaces and dots are reserved and rejected. Opt into international names withengine.charset: Charset.UNICODE. getGrants()/getGrantsList()/getRequirements()return frozen deep copies. Mutate the model through the builder API instead.strict.rolesdefaults on (throws on an unknown role at check time, as v2 did); setpolicy.strict.roles = falsefor lenient behavior.
Removed
Section titled “Removed”- Breaking: the default export — use the named
import { AccessControl } from 'accesscontrol'. - Breaking: static getters (
Action,Possession,Error, …) on theAccessControlclass — use the respective named imports. - Breaking: redundant method aliases (
allow(),reject(),query(),inherit(), …), in favor of canonical names — the one intentional alias kept is.do(). (fixes #25)
- Inheritance override / deny-overrides. An explicit
denynow restricts inherited grants too — deny always wins. Grants are purely additive (a smaller child grant no longer shrinks an inherited one);denydoes not cascade across possession (deny create:anystill leavescreate:own). (fixes #34) utils.getUnionAttrsOfRoles()no longer throws when a (flattened/extended) role does not define the queried resource.utils.getCrossExtendingRole()now returnsnull(instead offalse) when no cross-inheritance is found.
Security
Section titled “Security”tryCan()— fail-closed checks. Identical tocan()but never throws: an invalid query, astrictviolation, or a custom/async condition hit on the sync path all resolve togranted: false. Prefer it on the request path so a thrown error can’t become an accidental allow.strict.checksdefaults on — if ownership is configured but the record is missing from the context, the check denies (secure by default).- Prototype-pollution-safe. The gadget names
__proto__,prototypeandconstructorare rejected; any name colliding with an inherited member (toString, …) is treated as plain data, never a prototype member. - Opt-in, ReDoS-guarded regex. The
matchesoperator is off by default (engine.allowRegex); when enabled, patterns are screened for catastrophic backtracking and condition nesting depth is bounded. - Redacted errors with stable codes. Every
AccessControlErrorcarries a machine-readableErrorCode;engine.safeErrors(default on) keeps caller-supplied values out of messages (they remain onerr.role/ etc.). - ASCII-by-default charset (homograph-safe), with
Charset.UNICODEas an explicit opt-in.
Dev & environment
Section titled “Dev & environment”- (Dev) Breaking: ESM-only and requires Node.js v20+; the CommonJS build is removed (stay on v2 for CJS).
- (Dev) Ships an
exportsmap — only the package root (accesscontrol) andpackage.jsonare importable; internal paths are no longer reachable. - (Dev) Now built on
notationv3 (NotationGlob.union,Notation#filter). (fixes #96) - (Dev) Modernized toolchain: TypeScript 6, ESM-only build via
tsc(no bundler), Vitest + istanbul coverage (from Jest/ts-jest), Biome lint + format (from ESLint/TSLint), GitHub Actions CI (from Travis), and shared config viatsconfig-oy/biome-config-oy. - (Dev) Runs on Deno (
import … from 'npm:accesscontrol') and Bun in addition to Node.js ≥ 20 — a consequence of the pure-ESM, no-Node-builtins build. (fixes #106)
v2.3.0 (2021-05-10)
Section titled “v2.3.0 (2021-05-10)”IGrants,IGrantsItem,IGrantsList,IGrantsListItemtypes.
Changed
Section titled “Changed”coverallsandnotationdependencies.
package-lock.jsonerrors.
v2.2.1 (2018-02-24)
Section titled “v2.2.1 (2018-02-24)”- An issue with attribute filtering caused by the core dependency Notation. Now fixed and updated.
- (Dev) Updated dev-dependencies to latest versions. Removed yarn.
v2.2.0 (2017-11-25)
Section titled “v2.2.0 (2017-11-25)”This release greatly improves stability!
- An issue where action and possession of a permission query is not pre-normalized. Only
#permission()method was affected. - An issue where it would throw even if
$extendwas used properly in the initial grants model, passed to the constructor or#setGrants(). Fixes issue #22. - A memory leak (leading to “maximum call stack” error) occurs while processing role hierarchy.
- An issue where role validation would incorrectly return
truein a specific case.
Changed
Section titled “Changed”#lock()to throw a meaningful error if not successful.#hasRole()and#hasResource()methods to also accept a string array (to check for multiple at once), in addition tostring(single).- Various chain methods to throw when explicit invalid values are passed. e.g.
ac.grant()...will not throw (omitted parameter allowed) butac.grant(undefined)...will throw. This mitigates the chance of passing an unset variable by mistake. - Various revisions, optimizations and clean-up.
- (Dev) Migrated tests to Jest. Refactored tests to TypeScript. Removed Jasmine and dependencies.
- (Dev) Adapted
yarn. Enabled test coverage viajest. Addedcoverallssupport. - (Dev) Added moooore tests. Revised code style. Improved coverage.
v2.0.0 (2017-10-05)
Section titled “v2.0.0 (2017-10-05)”Changed
Section titled “Changed”- Breaking: Cross role inheritance is no more allowed. Fixes issue #18.
- Breaking: Grants model cannot be emptied any more by omitting the parameter (e.g.
#setGrants()) or passingnull,undefined. This will throw. You need to either, explicitly call#reset()or set grants to an empty object ({}) in order to reset/empty grants safely. - Breaking: Renamed
#access()to#query(). This is an alias for#can()method. AccessControlto throw if any reserved keywords are used (i.e. for role, resource names) such as"$","$extend".
- An issue where deeper inherited roles (more than 1 level) would not be taken into account while querying for permissions. Fixes issue #17.
- A mutation issue occurred when resource attributes are unioned. (Notation issue #2).
- An issue with unioned attributes (when a role extends another and attributes (globs) are unioned for querying permissions). Fixes issue #19 (Notation issue #3).
AccessControl#lock()method that freezes the underlying grants model and disables all functionality for modifying it. This is useful when you want to restrict any changes. Any attempts to modify (such as#setGrants(),#grant(),#deny(), etc) will throw after grants are locked. There is nounlock()method. It’s like you lock the door and swallow the key. :yum:AccessControl#isLockedbooleanproperty.AccessControl#getInheritedRolesOf()convenience method.- The ability to detect invalid grants object passed to
AccessControlinstance. In order to prevent silent, future errors and mistakes;AccessControlnow thoroughly inspects the grants object passed to constructor or#setGrants()method; and throws immediately if it has an invalid structure or configuration. - The ability to parse comma-separated attributes. You can now use this, in addition to string arrays; for defining resource attributes.
v1.5.4 (2017-09-22)
Section titled “v1.5.4 (2017-09-22)”- An issue where the static method
AccessControl.filter()does not return the filtered data properly. Fixes issue #16.
v1.5.3 (2017-08-25)
Section titled “v1.5.3 (2017-08-25)”Changed
Section titled “Changed”- Errors are now thrown with more meaningful messages.
v1.5.2 (2017-07-02)
Section titled “v1.5.2 (2017-07-02)”- An issue where the grants were not processed into the inner grants model structure; if an array is passed to
AccessControlconstructor; instead of using.setGrants(). Fixes issue #10.
v1.5.1 (2017-05-24)
Section titled “v1.5.1 (2017-05-24)”- TS import issue. Use
import { AccessControl } from 'accesscontrol'in TypeScript projects.
v1.5.0 (2017-03-08)
Section titled “v1.5.0 (2017-03-08)”Changed
Section titled “Changed”- Migrated whole code base to TypeScript.
- You could grant permissions for multiple roles at once. Now, you can also grant permissions for multiple resources at the same time. This is very handy when you permit all attributes of the resources; e.g.
ac.grant(['admin', 'superadmin']).readAny(['account', 'video'], ['*']). The caveat is that the resources (most probably) have different attributes; so you can either permit all, or only common attributes (e.g.['id', 'name']). - Extending a role with a non-existent role will now throw.
- More strict validation checks. It will now throw on invalid information passed for both grants and permission checks. This helps prevent typos, unintended permission checks, etc…
- A bug where checking permission with multiple roles would mutate the permission attributes. Fixes issue #2.
- A mutation issue when an access definition object (
IAccessInfoinstead of role(s)) passed to.grant()or.deny()methods.
v1.0.1 (2016-11-09)
Section titled “v1.0.1 (2016-11-09)”- A syntax issue that throws when permission filter is called. Fixes issue #1.
- (Dev) added filter test.
v1.0.0 (2016-09-10)
Section titled “v1.0.0 (2016-09-10)”- initial release.