Skip to content

Events & Auditing

AccessControl has a small, dependency‑free event emitter. The headline event is access, which fires on every resolved check (granted and denied) — your ready‑made audit log.

ac.on('access', (e) => audit(e));
ac.on('change', (e) => log(e.type));
ac.on('error', (e) => report(e.error));
ac.on('access', (e) => {
// {
// roles, resource, category, action, possession,
// granted, attributes, reason, context, timestamp
// }
audit(e);
});

reason explains a denial — one of:

reasonmeaning
no_grantno matching grant for the role/resource/action
condition_faileda .where() condition didn’t hold
ownership_failedan own rule couldn’t verify ownership
require_faileda .require() gate didn’t pass
ac.on('access', (e) => {
if (!e.granted) metrics.increment(`denied.${e.reason}`);
});

Fires when the model is mutated (grant, deny, extend, set_grants, setup, require, remove, reset, lock). The payload carries the type and a detail:

ac.on('change', (e) => log(`policy ${e.type}`, e.detail));

Fires when a check or operation throws (carries the AccessControlError). It fires even under tryCan(), where the check itself returns granted: false — so you still observe the fault.

ac.on('error', (e) => report(e.error)); // e.error is an AccessControlError
const onAccess = (e) => audit(e);
ac.on('access', onAccess);
ac.once('change', (e) => init(e)); // auto-removes after the first event
ac.off('access', onAccess); // remove one
ac.off('access'); // remove all 'access' listeners