Skip to content

feat(groups): transfer group ownership to another member - #248

Merged
holkexyz merged 2 commits into
stagingfrom
feat/group-ownership-transfer
Sep 17, 2026
Merged

holkexyz merged 2 commits into
stagingfrom
feat/group-ownership-transfer

Conversation

@holkexyz

Copy link
Copy Markdown
Member

Surfaces the two-phase ownership handshake added in CGS 0.6.0 (app.certified.group.ownershipTransfer.*) as a Transfer ownership page in group settings, between "Members & Roles" and "Activity Log".

The owner proposes an existing member; ownership moves only once that member accepts, which proves they still control their DID. Either party can cancel, and an un-accepted proposal lapses after 7 days.

Do not merge to main until production CGS is on 0.6.0

Deployment CGS Version
staging.certified.app dev.groups.certified.app 0.6.0 — has it
certified.app groups.certified.app 0.5.0 — answers these NSIDs with 501 MethodNotImplemented

There is deliberately no runtime capability detection or feature flag: the merge gate is the guard. Check with curl -s https://groups.certified.app/health before promoting staging to main.

What changed

  • src/app/api/groups/[groupDid]/ownership-transfer/route.ts (new) — GET status, POST propose, PUT accept, DELETE cancel through callGroupServiceJson. Per the CGS fix: don't force OAuth consent screen on every email login #27 targeting rule, repo goes on the querystring for the status query and in the body for the three procedures. Propose validates the DID and is rate-limited 5/10min per caller; CSRF is checked on every mutation.
  • src/lib/groups/types.ts + api.ts — an OwnershipTransfer discriminated union and four client functions beside the existing member/role seam.
  • src/components/groups/org-settings.tsx — the new settings page plus transferViewerRole and eligibleTransferTargets as exported pure helpers. No new CSS: it reuses sx-subsection, org-members__* and org-manage__actions.

Two behaviours worth reviewing closely

  1. Non-parties never see the page. CGS reports pending: false to a member who is not a party, exactly as when no transfer exists, so the UI derives visibility from that response and never tries to tell those cases apart. The same reasoning applies to the NoPendingTransfer the route forwards untouched.
  2. Status is re-read after every member and role mutation, because CGS clears a pending proposal when ownership or a party's membership changes by another route — a cached proposal would render one that can no longer be accepted.

Copy avoids claiming the underlying PDS account moves; per the CGS integration guide the owner role is not proof of account control.

Testing

  • npx tsc --noEmit, npm run lint, npm test (1310 passing) all clean; the CLAUDE.md pre-merge greps are silent.
  • New tests cover the targeting form per method, DID validation, the CSRF and rate-limit gates, and error-code passthrough, plus the visibility helpers including the non-party case.
  • Smoke-tested against the 0.6.0 CGS on a local dev server: signed out GET is 401 and a forged origin is refused.
  • Not yet exercised end to end in the browser — a full handshake needs a second account, since accept is callable only by the proposed member.

🤖 Generated with Claude Code

Surfaces the two-phase ownership handshake added in CGS 0.6.0
(app.certified.group.ownershipTransfer.*) as a "Transfer ownership"
page in group settings, between Members & Roles and Activity Log.

The owner proposes an existing member; ownership moves only once that
member accepts, which proves they still control their DID. Either party
can cancel, and an un-accepted proposal lapses after 7 days.

- BFF route: GET status, POST propose, PUT accept, DELETE cancel. Per
  the CGS #27 targeting rule, `repo` goes on the querystring for the
  status query and in the body for the three procedures. Propose
  validates the DID and is rate-limited 5/10min per caller.
- UI: the owner picks from eligible members and confirms; the proposed
  member gets Accept/Decline; either party can cancel. Transfer status
  is re-read after every member and role mutation, because CGS clears a
  pending proposal when ownership or a party's membership changes by
  another route.
- Only the two parties ever see the page. CGS reports `pending: false`
  to a member who is not a party, exactly as when no transfer exists,
  so the UI derives visibility from that response and never tries to
  tell the two cases apart.

Copy avoids claiming the underlying PDS account moves — per the CGS
integration guide, the owner role is not proof of account control.

Requires the group service to be on 0.6.0+. staging.certified.app
already is; production is on 0.5.0 and answers these NSIDs with 501,
so this must not reach main until that deployment is upgraded.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
certified-app Ready Ready Preview Sep 17, 2026 7:24pm UTC

Request Review

@coderabbitai

coderabbitai Bot commented Sep 17, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 4a29a4f1-a3c1-49b4-919e-43bcfc136db2

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

The "Choose the new owner" block repeated what the page description
already introduced. Fold the terms — accept-before-effect, the
proposer's demotion to admin, and the 7-day expiry — into the page
description so they're read once, before the control, and drop the
duplicate paragraph.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@holkexyz
holkexyz merged commit 6603087 into staging Sep 17, 2026
7 checks passed
@holkexyz
holkexyz deleted the feat/group-ownership-transfer branch September 17, 2026 19:28

This branch was successfully deployed

1 active deployment
Preview — 7d901a0e Deployed Sep 17, 2026 by vercel[bot]
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