* refactor(ownership): collapse OwnerId+Identity into UserId with role variants
- Expand UserRole to {Owner, Admin, Regular}
- UserId carries role; methods is_owner()/is_admin()/is_regular()
- Remove From<String>/From<&str> impls (enforces types.md rule)
- Validated construction via new(); from_trusted() for DB-sourced values
Addresses bug pattern from #2561, #2620, #2349 where owner_id silently
round-tripped as String.
* refactor(ownership): address review feedback — id-only equality, persist owner role, doc fixes
- UserId PartialEq/Eq/Hash now compare only `id`, not `role`. Role is
metadata that travels with the identity; two UserIds with the same id
but different roles must be interchangeable as HashMap/HashSet keys
and cache lookup targets. Added a regression test that builds a
HashSet keyed on UserId and asserts cross-role `.contains()`
membership, plus a hash-equality check.
- CLI pairing path now persists the "owner" role string (via
UserRole::Owner.as_db_role()) instead of the hardcoded "admin", so
a reload through UserRole::from_db_role stays Owner rather than
being silently downgraded to Admin.
- Update the feature/pairing approve handler to mirror the refactor:
build UserId via from_trusted + UserRole::from_db_role(&user.role)
instead of the removed OwnerId::from.
- AdminScope doc comment now reflects that Owner also passes
is_admin().
- AdminUser extractor error message now reads "Admin privileges
required (admin or owner)" so the forbidden response matches the
actual gate.
---------
Co-authored-by: Henry Park <henrypark133@gmail.com>