Skip to content

fix(vaults): tell the truth about make-private, and stop it losing data (#68) - #223

Merged
kipavy merged 1 commit into
devfrom
fix/make-private-honesty
Sep 8, 2026
Merged

kipavy merged 1 commit into
devfrom
fix/make-private-honesty

Conversation

@kipavy

@kipavy kipavy commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

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. handleMakePrivate in VaultsSection.tsx converts a team vault back to a personal one, gated by isTeam && 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:

You can make it private again later — that removes everyone's access, but not the copies they already have.

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

handleMakePrivate had three quiet failure modes:

  1. 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.

  2. 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. deleteTeam now 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.

  3. An unexpected failure was invisible. The outer catch did console.error and 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. inviteFailureReason was already doing that filtering, so it moved to services/errorReason.ts as userFacingReason and inviteFailureReason now 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 before deleteTeam and before the vault is unlinked; a rejected deleteTeam leaves 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-share all 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

…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
@kipavy
kipavy merged commit a3abe7a into dev Sep 8, 2026
4 checks passed
@kipavy
kipavy deleted the fix/make-private-honesty branch September 8, 2026 08:41
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.
@kipavy
kipavy restored the fix/make-private-honesty branch September 9, 2026 17:11
@kipavy
kipavy deleted the fix/make-private-honesty branch September 9, 2026 17:15
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