Skip to content

Strict Mode, Errors & Names

Access management is sensitive, so AccessControl never fails silently. This page covers how it surfaces problems and the knobs that control naming and error output.

policy.strict turns unknown names into loud errors instead of silent denials. It’s a boolean or a per‑key object.

new AccessControl(grants, {
policy: { strict: { actions: true, resources: true } }
});
keydefaulteffect
rolesonunknown role at check time → throws ROLE_NOT_FOUND
checksonan unverifiable own check (record/owner missing) → deny
actionsoffunknown action → throws UNKNOWN_ACTION (else silent deny)
resourcesoffunknown resource → throws UNKNOWN_RESOURCE (else deny)

strict: true turns all four on; strict: false, all off. The known sets come from the grants, plus CRUD (actions), plus anything you declared via setup() or the policy.actions / policy.resources allow‑lists.

A denial returns granted: false. A genuine fault throws an AccessControlError. Detect it and branch on its code — not its message.

import { AccessControl } from 'accesscontrol';
try {
ac.can(role).readAny('post');
} catch (err) {
if (AccessControl.isACError(err)) {
log(err.code, err.role, err.resource, err.action);
}
}

Every AccessControlError carries a stable err.code (the ErrorCode enum) — the part of the API you should branch on. Messages are redacted by default and may change wording.

The phase column tells you when a code can reach you: author codes throw while the policy is being built or loaded (grant chainers, the constructor, setGrants(), require(), restore()) — they mean the policy itself is bad, and they surface at startup, not per request. check codes throw while resolving a permission — the policy loaded fine, but this particular check couldn’t be answered.

codewhenphase
INVALID_NAMEempty/malformed nameboth
RESERVED_NAMEa reserved keyword (__proto__, prototype, constructor, _)both
INVALID_QUERYmalformed check query (IQueryInfo)check
INVALID_SETUPmalformed setup() vocabularyauthor
INVALID_GRANTinvalid grant rule / grants objectauthor
INVALID_ACTIONinvalid action name or possessionboth
ROLE_NOT_FOUNDreferenced role doesn’t existboth
INVALID_INHERITANCEself / cross / non‑existent inheritanceauthor
UNKNOWN_ACTION / UNKNOWN_RESOURCEstrict‑mode unknown namecheck
LOCKEDmutation attempted after lock()author
ASYNC_REQUIREDa { fn } condition was hit on the sync pathcheck
INVALID_CONDITIONmalformed / too‑deeply‑nested conditionauthor
UNKNOWN_CONDITION_FNunregistered custom function namecheck
REGEX_DISABLED / UNSAFE_REGEXmatches disabled, or an unsafe patterncheck
INVALID_DTREXPmalformed or over‑long during expressionauthor
DTREXP_NEVER_MATCHESa during expression that can never match (e.g. D30 M2)author
import { ErrorCode } from 'accesscontrol';
if (err.code === ErrorCode.ROLE_NOT_FOUND) { /* … */ }

The split is what makes tryCan() safe to use on the request path: check-phase throws are swallowed into a denial (the error event still fires for your logs), while author-phase throws happen where you want a crash — at load time, before any request is served. The full generated reference lives at API › ErrorCode.

Namespacing codes (engine.errorCodePrefix)

Section titled “Namespacing codes (engine.errorCodePrefix)”

Codes like INVALID_NAME or INVALID_QUERY are generic and may collide with your own system’s codes. Prefix every AC code to namespace them:

const ac = new AccessControl(grants, { engine: { errorCodePrefix: 'AC_' } });
// now err.code === 'AC_ROLE_NOT_FOUND'

By default (engine.safeErrors: true), error messages omit caller‑supplied values so request data doesn’t leak into logs. The values remain on the structured fields.

const e = grab(() => ac.can('ghost').readAny('post').granted);
e.message; // "Role not found." (redacted — safe to log)
e.role; // "ghost" (available programmatically)

Turn it off for verbose, developer‑friendly messages:

new AccessControl(grants, { engine: { safeErrors: false } });
// → "Role not found. Got: \"ghost\"."

See Security › Error messages.

Names (roles, resources, actions, groups, categories) are validated against a character set. The default is ASCII; opt into Unicode for i18n.

import { Charset } from 'accesscontrol';
new AccessControl(grants, { engine: { charset: Charset.UNICODE } });
ac.grant('café').readAny('café'); // allowed under UNICODE
valueallowednotes
Charset.ASCII (default)[A-Za-z0-9_-]rules out homograph attacks
Charset.UNICODEUnicode letters/digits + _ -⚠️ homograph risk