Cross-backend parity suite for the role-grant-offer decline and retract
refusals — the error paths beneath the happy-path WS notification suite
(describe_role_grant_offer_notification_ws_tests), which is the only other
cross-process caller of these two verbs.
What both spines must agree on, per the handlers
(auth/role_grant_offer_actions.ts and the queries behind them):
- not yours → 404
role_grant_offer_not_found. Decline is scoped to the
recipient account and retract to the grantor actor, so an offer the caller
is not party to answers the same not_found as an id that names no offer
(docs/security.md §"404-over-403 is the general mask"), and the offer is
left pending. The scope check comes before the terminal check, so a
stranger learns nothing of an offer's state either. - a decline or retract answers the terminal offer — the offer as the
history then lists it,
declined_at (with any decline_reason) or
retracted_at stamped, as accept answers its own. - already decided → 400
invalid_request with reason
role_grant_offer_terminal — a decline or retract after an accept, a
second decline, a second retract, and either one after the other. - a grantor's lost authority ends its pending offers. Revoking a
grantor's grant, or deleting its account, supersedes every pending offer
it made — gone from the recipient's inbox, an accept answers
role_grant_offer_terminal, and a role_grant_offer_supersede row records
it (role_grant_revoked with the grant as cause, account_deleted with
the account). The keeper's own admin grant refuses a revoke, 403
cannot_revoke_keeper_admin. - one audit row per success, none per refusal. A decline writes
role_grant_offer_decline attributed to the recipient and targeting the
grantor, a retract role_grant_offer_retract attributed to the grantor
and targeting the recipient, each with its event's metadata; the refusals
above write nothing. Read through list_audit_events, which drains first
and parses each row's metadata against its event's schema.
Multi-step (each case needs an offer in a given state), so this is an
imperative suite; the single-request rows — decline and retract of an id
that names no offer — live in the conformance security slate. The verbs are
on every spine's standard RPC surface, so the suite is ungated. The keeper
is the grantor, so the fixture's keeper must hold ROLE_ADMIN
(extra_keeper_roles), and it offers an admin-grantable role — the
role option, SPINE_PARTICIPANT_ROLE by default; ROLE_ADMIN serves a
backend with only the builtin roles.