scout-finance validates its session against this API and needs four
things whoami did not return. Without them it reports itself down
rather than degrading, which is correct and also useless.
- id, so a finance row can carry entered_by. The email is
display-facing and is the wrong thing to write rows against.
- preferred_name / full_name, for entered_by_name, captured at write
time so a historical report carries the name as of that date.
- global_capabilities, separate from the union. The union answers
"may they see this screen"; the site-wide set answers "does this
grant reach a unit they hold no membership in". For an admin who is
also a den leader those are not the same, and collapsing them lets
a pack-only grant travel to the troop.
- memberships[].capabilities, so a separate service scopes per unit
without keeping a second copy of CAPS. Nothing outside this file
may map a role to a capability.
Built in identity.whoami_payload() rather than in the route, so it is
testable with no HTTP and the capability map stays in one place. An
API key narrows the per-membership sets too, so a key can never appear
to hold what can() would refuse.
CAPS: finance:read and finance:write on leader, because a treasurer is
a leader and leader-wide read is a deliberate design decision in
finance.md. finance:admin on admin only, for categories, accounts and
finance settings, which are site-wide.
The break-glass token path keeps the same shape with a null id and no
memberships. It has no person behind it, so nothing it did could be
attributed; scout-finance refuses it outright.
Additive throughout. scout-control reads none of these fields.
smoke_identity 138, smoke_admin 164, smoke_documents 24.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AHy2gB4QvKmRurfXwYbwCB