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.

codewhen
INVALID_NAMEempty/malformed name
RESERVED_NAMEa reserved keyword (__proto__, prototype, constructor, _)
INVALID_QUERYmalformed check query (IQueryInfo)
INVALID_SETUPmalformed setup() vocabulary
INVALID_GRANTinvalid grant rule / grants object
INVALID_ACTIONinvalid action name or possession
ROLE_NOT_FOUNDreferenced role doesn’t exist
INVALID_INHERITANCEself / cross / non‑existent inheritance
UNKNOWN_ACTION / UNKNOWN_RESOURCEstrict‑mode unknown name
LOCKEDmutation attempted after lock()
ASYNC_REQUIREDa { fn } condition was hit on the sync path
INVALID_CONDITIONmalformed / too‑deeply‑nested condition
UNKNOWN_CONDITION_FNunregistered custom function name
REGEX_DISABLED / UNSAFE_REGEXmatches disabled, or an unsafe pattern
import { ErrorCode } from 'accesscontrol';
if (err.code === ErrorCode.ROLE_NOT_FOUND) { /* … */ }

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