This page follows a single usePermission call from the first render to the rendered verdict, naming every function and every fork along the way. It’s the runtime companion to the Architecture overview.
The whole path
Step by step
Render & denied seed
The hook initialises toDENIED_LOADING({ allowed:false, loading:true }). First paint is a denial — no allow window exists. (hooks.ts)Subject short-circuit (
usePermission)
If the provider has nosubject, the effect setsDENIED_FINALand returns without any network call. (hooks.ts)Stable effect key
The effect depends oncanonicalJson(query)— a sorted, recursive serialisation — not the object reference, so identical queries don’t refetch. It callssetState(DENIED_LOADING)again before fetching. (hooks.ts)client.check— subject guard
checkdenies'no-subject'ifquery.subject.idis falsy, before building anything. (client.ts)Serialise to the wire payload
toPayloadmaps the query to the exact server shape:subject(withtypedefaulted to'user'),permission, explicitnulls fororganization/application/resource,context(default{}),current_aal(default'aal1'),explain. (client.ts)Cache lookup (skipped for
explain)
When the cache is enabled and the query isn’t anexplain,checklooks upcacheKey(payload)(canonical JSON). A fresh hit short-circuits the network. (client.ts+cache.ts)The request, with a deadline
requestJsonPOSTs JSON withAccept/Content-Typeand (if a token is set)Authorization: Bearer. AnAbortControllerfires aftertimeoutMs(default 2000). Non-2xx, an abort, a thrown fetch, or unparseable JSON all yieldundefined; withretries > 0, idempotent network errors retry. (client.ts)Transport failure → deny
checkturnsundefinedintodeny('transport'). (client.ts+decision.ts)Normalise the body
decisionFromBodyunwraps the{ data }envelope and reads each field with safe defaults (allowedonly on literaltrue, etc.). (decision.ts)Cache the real verdict
On a real (non-transport) decision with the cache on and notexplain,cache.setstores it — flushing the whole cache first ifpolicyVersionincreased. (cache.ts)Reduce & map to state
Back in the hook,isGranted(decision)(allowed && !requiresStepUp) becomesallowed;requiresStepUpis surfaced;loadingisfalse. Thecancelledguard drops the update if the query re-keyed or the component unmounted. (hooks.ts+decision.ts)
Where each deny comes from
| Origin | Function | Result |
|---|---|---|
| Logged-out / no subject in context | usePermission effect |
DENIED_FINAL, no network |
Empty subject.id |
IamClient.check |
deny('no-subject') |
| Timeout / abort | requestJson → check |
deny('transport') |
| Non-2xx response | requestJson → check |
deny('transport') |
| Unparseable JSON | requestJson → check |
deny('transport') |
| Non-object body | decisionFromBody |
deny('invalid body') |
Missing allowed field |
decisionFromBody |
allowed: false |
| Allowed but step-up pending | isGranted |
allowed: false (UI prompts) |
| Stale/late resolution | cancelled guard |
update dropped |
Every row lands on allowed: false. There is exactly one way to allowed: true: a 2xx body that normalises to allowed && !requiresStepUp, delivered before the query re-keys.
verifyToken is the one exception path
verifyToken does not return a value on failure — it rejects with TokenVerificationError (audience missing, bad signature, wrong claims, JWKS unreachable). It follows its own flow (mandatory-audience guard → JWKS resolve with 10-min cache → ES256 verify → one refetch on key-rotation). See Verifying tokens.
Next steps
- Wire contract — the payload and envelope in detail.
- The hook lifecycle — the React state machine.
- The decision model — normalisation and
isGranted.