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.
Strict mode (typo protection)
Section titled “Strict mode (typo protection)”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 } }});| key | default | effect |
|---|---|---|
roles | on | unknown role at check time → throws ROLE_NOT_FOUND |
checks | on | an unverifiable own check (record/owner missing) → deny |
actions | off | unknown action → throws UNKNOWN_ACTION (else silent deny) |
resources | off | unknown 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.
Errors never fail open
Section titled “Errors never fail open”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); }}Error codes
Section titled “Error codes”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.
| code | when |
|---|---|
INVALID_NAME | empty/malformed name |
RESERVED_NAME | a reserved keyword (__proto__, prototype, constructor, _) |
INVALID_QUERY | malformed check query (IQueryInfo) |
INVALID_SETUP | malformed setup() vocabulary |
INVALID_GRANT | invalid grant rule / grants object |
INVALID_ACTION | invalid action name or possession |
ROLE_NOT_FOUND | referenced role doesn’t exist |
INVALID_INHERITANCE | self / cross / non‑existent inheritance |
UNKNOWN_ACTION / UNKNOWN_RESOURCE | strict‑mode unknown name |
LOCKED | mutation attempted after lock() |
ASYNC_REQUIRED | a { fn } condition was hit on the sync path |
INVALID_CONDITION | malformed / too‑deeply‑nested condition |
UNKNOWN_CONDITION_FN | unregistered custom function name |
REGEX_DISABLED / UNSAFE_REGEX | matches 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'Safe error messages
Section titled “Safe error messages”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.
Charset
Section titled “Charset”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| value | allowed | notes |
|---|---|---|
Charset.ASCII (default) | [A-Za-z0-9_-] | rules out homograph attacks |
Charset.UNICODE | Unicode letters/digits + _ - | ⚠️ homograph risk |