Cross-backend parity suite for the role_grant_offer_accept enumeration
boundary — the deliberate 403-vs-404 split on the accept path.
role_grant_offer_accept distinguishes two denials, and the distinction is a
conscious decision (not an accident to mask away):
- a genuinely-nonexistent offer (or one on another account) → 404
role_grant_offer_not_found. The account-scoped IDOR guard refuses to
confirm an offer id the caller's account doesn't own — so a cross-account
prober learns nothing. - an offer that exists on the caller's OWN account but is targeted to a
different actor (a sibling persona) → 403
role_grant_offer_actor_mismatch.
This reveals "an offer exists for a sibling actor" to a co-account caller —
but there is no cross-account leak: every actor in the distinction
belongs to the one account already authenticated, so the 403 discloses
nothing a principal can't already see about its own account. Masking this to
404 would only obscure a legitimate "not yours to accept, pick the right
persona" signal. So 403 is the chosen, defensible behavior — and both spines
must agree on it (a future Rust or TS change that over-masked the sibling
case to 404, or under-masked the cross-account case to 403, is the
regression this suite catches).
The actor-mismatch arm only fires for an *actor-targeted* offer accepted by a
sibling actor, so the suite needs a multi-actor recipient. The keeper is the
only fixture account that can be seeded multi-actor (extra_actors), and an
account can't offer to itself — so the grantor is a separate admin account and
the recipient is the keeper (actor A = fixture.actor is the offer target;
actor B = fixture.extra_actors[0] is the rejected sibling).
Multi-step (create → accept) and using the acting selector, so this is an
imperative suite (not a conformance_table row). The accept verb is on every
spine's standard RPC surface, so the suite is ungated. Cross-process only: the
sibling-actor / actor-targeted-offer setup is a wire flow. Cited property:
docs/security.md §"Authorization" (the 404-over-403 mask is scoped to
cross-principal leaks; an intra-account sibling-actor offer stays a 403
role_grant_offer_actor_mismatch).
$lib-free by contract (relative specifiers only), like the sibling
cross-backend suites.