explain is the auditability surface of Alfiz. It runs the same evaluation as can, producing an identical boolean outcome, but returns the full working: which grant rows matched, which revoke rows suppressed them, what the computed object closure was, and whether the result was reached through ancestor implication rather than a direct grant match. Use explain when debugging unexpected access decisions, building audit UIs, or powering “why can you see this?” transparency features in your product.
client.explain(principal, key, scope?)
PrincipalRef
required
Identifies who is being explained. Either
{ userId: string } for a human
user or { serviceId: string } for a machine principal.LooseKey<K>
required
A concrete permission key declared in your catalog. Unlike
canAny, you
cannot pass a wildcard pattern here. explain always explains a specific
leaf. The key is verified against the catalog before evaluation; an unknown
key raises UnknownPermissionError.ScopeId
Optional. The scope instance to explain the decision at, for example
docs.doc:abc123. Omit for a global-scope explanation. When provided,
the object closure (scope → ancestors → *) is included in the return
value.Return value
boolean
true when the principal currently holds the key at the given scope.
identical to what client.can(principal, key, scope) would return.GrantRow[]
The unexpired grant rows that would allow this key at this scope. Each
GrantRow carries its id, subject, scope, pattern or roleId,
optional expiresAt, and the provenance record describing who created
the grant and how. Empty when allowed is false and no suppression is
involved.RevokeRow[]
Personal revoke rows that suppress the access. When
matchedRevokes is
non-empty, allowed is always false, because revokes always win, regardless
of how many matching grants exist. Each RevokeRow carries its id,
userId, pattern, scope, and provenance.ScopeId[]
The resolved ancestor chain of the requested scope:
[scope, ...ancestors, "*"]. For a global-scope check this is ["*"]. This is the set against
which grants and revokes were matched. It tells you exactly which ancestor
scopes were in scope for the evaluation.boolean
Whether the principal was active at evaluation time. An inactive principal
always evaluates to no access (
allowed: false) even if matchedGrants
is non-empty.boolean
true when access was granted through §7.5 ancestor implication rather
than a direct grant match. This happens when a permission leaf is declared
with impliedOnAncestors: true and the principal holds a grant of that
leaf at a descendant scope of the requested scope. When implied is
true, matchedGrants will be empty (the direct match found nothing) but
allowed is true. The grants responsible are reported in impliedBy.GrantRow[]
The grants at a descendant scope that produced an implied allow. Empty
unless
implied is true. matchedGrants keeps its exact meaning: rows
matching at the requested scope, so this is where “which grant, and at
which scope, implied this?” is answered.When to use explain
explain is a diagnostic and auditability tool, not a gate. Do not replace can with explain on hot paths, because explain does the same evaluation work and returns more data, which means it allocates more objects and is more expensive to call at high frequency.
Use explain for:
- Debugging during development. Unexpected
trueorfalsefromcanin tests. - Audit log UIs. Displaying “why does this user have access to this document?” alongside the grant history.
- “Why can you see this?” transparency features. Showing users which of their roles granted them access.
- Access review tooling. Surface the grant provenance chain to reviewers.
- Automated tests. Asserting that a specific grant row (by id or pattern) is the one conferring a permission.
explain draws from the same caches as can. If you need a fresh
explanation that bypasses the cache, take a snapshot with { fresh: true }
and call snap.explain(key, scope) instead.