Skip to content

feat: renew a membership from the Communities view (vtc/members/renew/0.1) - #398

Merged
stormer78 merged 1 commit into
mainfrom
feat/renew-membership
Oct 1, 2026
Merged

stormer78 merged 1 commit into
mainfrom
feat/renew-membership

Conversation

@stormer78

Copy link
Copy Markdown
Contributor

Adds renew membership to the client using vtc/members/renew/0.1. After the DTG Credentials v1 migration (#397), OpenVTC sets aside stored pre-v1 credentials and tells the member to have the community re-issue them, but the member had no way to ask from the TUI. This adds that.

UX

  • Trigger: in the Communities view, press R on an Active membership. The R: renew hint shows only on Active rows.
  • Progress: the status line reads "Asking the community to renew your membership…", then "Renewal requested — waiting for the community to re-issue your credentials."
  • Outcome, on the status line and in the activity and Community logs:
    • Renewed: says which role was re-issued, whether personhood changed, and whether the acknowledgement was sent. If sending the acknowledgement failed, it says to press m.
    • Not a member: "Renewal refused: the community says you are not a member. If you were removed, join again."
    • Other refusals: the community's code and message.
  • Retired notices: the "set aside" notice now says "press R to renew your membership". It clears once the replacement credentials are stored.

How it works

  • Send: openvtc_core::renewal::request_renewal (new openvtc-core/src/renewal.rs) is fire-and-forget over DIDComm, like the other member verbs. It uses the same delivery and signing path as members::submit_member_vmc. The request is recorded in memory so only a reply threaded on it is taken.
  • Verify (verify_renewal_response, run off the loop as VerifyJob::Renewal):
    • The payload must parse, and did must be the recipient.
    • Both credentials must pass dtg::parse_conformant.
    • The VMC must be a Membership credential with issuerScope: public, issued by the community, with our member DID as subject.
    • The VAC must pass dtg::community_roles: scope is the community, public, no parent, subject is our member DID.
    • The community's signed document is verified, and each credential goes through verify_issued_credential (proof, validity window, revocation status, failing closed).
  • Store (apply_renewal): on an Active membership only, it replaces the Membership and Role credentials and clears their retired notices. Personhood follows from the new VMC.
  • Acknowledgement: a reciprocal member VMC is always issued and sent with issue_member_vmc_for, as join does. The new grant's digest changes, so the old acknowledgement no longer binds.
  • Refusals: a trust-task-error threaded on a pending renewal is verified as the community's signed reply. vtc/members/renew:notMember is recognised.

Tests

  • Core (10):
    • success, with both credentials stored and notices cleared
    • another member's renewal
    • non-conformant VMC, and non-conformant VAC
    • a credential the community didn't sign
    • no Active membership
    • notMember
    • request shape and request correlation
    • status wording
  • Dispatcher (3):
    • success end to end through process_inbound_message
    • an unsolicited reply ignored
    • notMember reported with no state change
  • UI: R gating, the hint, and the status line. The inbound gate census includes the renew response type.

All pass locally:

  • cargo fmt --check
  • cargo clippy --workspace --all-targets -- -D warnings
  • RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps
  • cargo test --workspace

Known limits

  • Restart while pending: if the client restarts while a renewal is outstanding, the reply isn't taken. Pressing R again is harmless, because the VTC re-mints into the same status-list slot.
  • A reply that fails verification is reported but doesn't cancel the pending request, so a forged reply can't pre-empt the real one.

…/0.1)

After the DTG Credentials v1 migration a member's pre-v1 VMC and role
credential are set aside on load, and the notice told them to ask the
community to re-issue them. There was no way to do that from the TUI.

openvtc-core: new `renewal` module.
- request_renewal sends vtc/members/renew/0.1 (no parameters, signed by
  the persona's authentication key) in the DIDComm binding envelope, the
  same delivery path as members/vmc, and records the request so only a
  reply threading on it is taken.
- verify_renewal_response holds the reply to the community's signed
  operational document (authentication key, addressed to the persona,
  fresh) and checks both credentials: conformant v1 (parse_conformant),
  issued by the community to that persona, the VMC a MembershipCredential
  with issuerScope public, the VAC a community role credential in the
  community's own scope (community_roles); then verifies each as an
  issued credential (assertionMethod proof, validity, revocation status).
  Local checks run before anything is resolved.
- apply_renewal stores both on the Active membership, replacing the old
  ones and clearing their retired notices; store_acknowledgement records
  the fresh member VMC. RenewalRefusal recognises
  vtc/members/renew:notMember; RenewalOutcome::describe is the member's
  status line.

openvtc:
- `R` on an Active community (Action::RenewMembership -> community job
  Verb::Renew). Status: "Asking..." -> "Renewal requested - waiting..."
  -> the outcome when the reply lands.
- The reply is verified off the loop (VerifyJob::Renewal), stored, and
  answered with a fresh members/vmc acknowledgement, as the join flow
  does on admission (the new grant has a new digest). A trust-task-error
  threading on a renewal is reported as a renewal refusal rather than
  falling into the join-error handler.
- The pre-v1 notice now says to press R.

Tests: core verification/storage over a did:key community (success with
retired notices cleared, wrong member, non-conformant credentials, a
credential the community did not sign, no Active membership), notMember;
dispatcher end-to-end for success, an unsolicited reply, and notMember;
key gating, hints and the community-job status line. Inbound gate census
includes the renew response type.

Signed-off-by: Glenn Gore <glenn.g@affinidi.com>
@stormer78
stormer78 requested a review from a team as a code owner October 1, 2026 08:24
@stormer78
stormer78 merged commit 3f65f31 into main Oct 1, 2026
15 checks passed
@stormer78
stormer78 deleted the feat/renew-membership branch October 1, 2026 08:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant