Repository navigation
fix(vaults): tell the truth about make-private, and stop it losing data (#68) - #223
Merged
Merged
Conversation
…ta (#68) The consent modal for turning a private vault into a team vault ended with "There's no way back to private." That is false: `handleMakePrivate` in `VaultsSection.tsx` converts a team vault back, and its gate (`isTeam && isOwner && detail.kind === "local"`) describes exactly the person reading the modal. The line now says what is actually true — you can go back, but it removes everyone's access and does not take back the copies they hold. Making that promise honest meant looking at the handler behind it, which had three ways to fail quietly: - Every entity was re-written to local disk under one `Promise.allSettled` whose rejections were discarded, and the team was deleted afterwards. Any write that failed had its last remaining copy deleted off the server a moment later. A failed write now aborts before anything destructive runs. - `deleteTeam(teamId).catch(() => {})` meant a failed server call still unlinked the vault locally and reported "Vault is now private" while every member kept their access. The delete now runs before the local teardown and a failure stops the whole operation, because the server call is the part that actually revokes access. - The outer catch only reached the console, so an unexpected failure showed the user nothing at all. It now raises a toast. Error reasons go through `userFacingReason`, extracted from `vaultShare`'s `inviteFailureReason` (which now delegates to it): a raw transport failure carries the server URL in its message, and that must not reach the UI. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014gY5k7XbeR5zoTXdzsuWxF
This was referenced Sep 8, 2026
kipavy
added a commit
that referenced
this pull request
Sep 9, 2026
The vault name in the header is now a menu trigger, and right-clicking a vault in the rail opens the same menu. Rename, delete and make-private are one click from the vault instead of four clicks into Settings. Settings -> Vaults loses its Members and Roles tabs, which re-rendered what the Members page already owns; the circular link between the two surfaces is gone in both directions. The section stays as the way to administer a vault you are not currently in. VaultsSection.tsx: 930 -> 244 lines. Also removed: PrivateVaultMembersPanel (a weaker third copy of the private->team conversion), RolesSection's unroutable default export, a dead duplicate UpgradeToTeamsCTA, and the retired locale keys in all four locales. handleMakePrivate moved verbatim into useVaultAdminActions, preserving the three properties hardened in #223, with its test file and all 27 assertions.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #220. Two changes that turned out to be the same change.
(a) The consent modal was lying
The private→team modal added in #179 ends with "There's no way back to private." It isn't true.
handleMakePrivateinVaultsSection.tsxconverts a team vault back to a personal one, gated byisTeam && isOwner && detail.kind === "local"— which describes exactly the person reading that modal, since they are about to become the owner of a converted local vault.The line now reads:
That keeps the honest part of the warning (removal is the server refusing to serve, not a cryptographic revocation) without claiming the door is locked. All four locales updated.
(b) …so I looked at the thing the new sentence promises
handleMakePrivatehad three quiet failure modes:It could delete the last copy of your data. Every entity — connections, identities, keys, folders, snippets, snippet folders, port-forward rules — was re-written to local disk inside one
Promise.allSettled, whose rejections were thrown away. Then the team was deleted server-side, cascading every team table. Anything that failed to write existed only inside that team, so the delete destroyed it. A rejected write now aborts before anything destructive runs, and the user is told the copy was incomplete and nothing was deleted.It could report success while everyone still had access.
deleteTeam(teamId).catch(() => {})— a failed server call still unlinked the vault locally and toasted "Vault is now private", while the team and every member's access survived on the server. Same shape as the DB-only revoke bug from the invite follow-ups.deleteTeamnow runs before the local teardown and a failure stops the operation: the server call is the part that actually revokes access, so nothing local should happen until it lands.An unexpected failure was invisible. The outer catch did
console.errorand nothing else. It now raises an error toast too.New order: copy → verify every write landed → delete the team on the server → tear down locally → success toast.
Error text
The failure toasts interpolate a reason, and a raw transport error embeds the server URL in its message — the leak fixed for the invite panel in #179.
inviteFailureReasonwas already doing that filtering, so it moved toservices/errorReason.tsasuserFacingReasonandinviteFailureReasonnow delegates to it. One test pins that a transport failure's URL never reaches the toast.Tests
New
VaultsSection.MakePrivate.test.tsx: a rejected write aborts beforedeleteTeamand before the vault is unlinked; a rejecteddeleteTeamleaves the vault linked with an error toast and no success toast; the happy path still copies, deletes, unlinks, toasts and navigates back; an unexpected throw surfaces as a toast; and a transport failure's URL never reaches the DOM.tsc clean.
src/components/settings,src/services,src/i18n,src/components/members,src/components/vault-shareall pass — 158 files, 1322 tests.Not done here
The Vaults settings section itself needs a rework — filed as #222.
🤖 Generated with Claude Code
https://claude.ai/code/session_014gY5k7XbeR5zoTXdzsuWxF