`allowedScopes: [openid, approval:read, approval:approve]`, and
`mfaRequired: true`. Do not add consume or other approval grants to that client.
No callback is invented here. The ID token is for the login client; present the
access token to approval-engine.
`approval:read` is present because approval-engine showed the surface cannot
render a decision without it: `GET /v1/approvals/{id}` and `/claim` both require
it, so the earlier `[openid, approval:approve]` would have let an approver submit
an entry they were never able to display. Reading through the owning component's
own service identity would also work, but it weakens the one claim that surface
exists to make — evidence of what *this person* was shown — so the read is
granted to the human principal instead. `approval:consume` stays excluded: human
principals are refused consume in approval-engine's code regardless, and
consumption belongs to the PEP causing the side effect.
### The assurance object
KeyCape emits `assurance` on every human token, unscoped. approval-engine stores
it verbatim into the approval entry, where it is the only place `mfaRequired:
true` survives into the record, so its shape is a contract:
| Field | Type | Meaning |
| --- | --- | --- |
| `level` | string | `aal1` or `aal2`. `aal2` exactly when MFA was verified in this authorization. The closest thing to `acr`. |
| `methods` | string[] | `["pwd"]`, or `["pwd","otp"]` when MFA was verified. The closest thing to `amr`. |
| `mfa` | bool | Whether MFA was verified. Redundant with `level` by construction, and kept because a consumer asserting on one should not have to know the mapping. |
| `source` | string | Always `key-cape`. Names which issuer made the assertion. |
| `at` | number | Unix seconds at which the user **authenticated** — not when the token was minted. |
`at` is authentication time on purpose. A reused browser session can be hours
old, and a record saying MFA happened at mint time would overstate how recently
the person proved anything. Where an authorization rides an existing session, the
original login instant is carried through.
The level is derived from what happened in *this* authorization, never from
enrollment state: a user with MFA enrolled who was not challenged gets `aal1`.
A consumer that needs a maximum age should compare `at`, not assume freshness.