Skip to content

fix(team-vault): never show a secret the server refused as saved (#608) - #609

Merged
kipavy merged 1 commit into
devfrom
fix/608-team-secret-type-support
Oct 10, 2026
Merged

kipavy merged 1 commit into
devfrom
fix/608-team-secret-type-support

Conversation

@kipavy

@kipavy kipavy commented Oct 10, 2026

Copy link
Copy Markdown
Contributor

Closes #608. Server side: VoltiusApp/server#103.

Problem

On a self-hosted server older than its port-knock support (VoltiusApp/server#100), editing a team-vault host:

  • Adding a knock port failed with team secret upload failed for knock_sequence:<id>: 400.
  • The port still showed when the editor was reopened. writeSecretAt cached the value before the upload and never took it back, so it lasted until the next vault load.
  • The Port Knocking switch is stored on the host itself and saved fine. The host could end up with knocking on and no ports, and then every teammate's connect failed with "The knock sequence for this host isn't available on this device."

The client had no way to know what the server supports.

Fix

  • Capability check (services/serverCapabilities.ts): reads team_secret_types from /v1/meta (added in feat(meta): advertise the team secret types the server accepts server#103), refreshed at most every 5 minutes and reset on logout. A type the server does not list is never uploaded.
  • Servers that advertise nothing: the upload is tried. A 400 on connection_knock_sequence is read as "server too old" and remembered until the server advertises a list. Servers that already store knock sequences but predate the list keep working.
  • Cache: a refused value is rolled back to what the server holds, so nothing looks saved.
  • Message and log: the toast reads "This Voltius server is too old to store this in a team vault. Ask its administrator to update the server, then save again." The app log gets a warning with the secret type and client version.
  • Knock panel: for a team-vault host on such a server it shows a warning instead of the port list, and the switch can only be turned off.
  • Stranded switch: if the sequence edited in this form was refused, the form turns knocking back off and saves that.
  • fetchServerMeta is now shared by identity picks and the capability check.

Personal-vault hosts are unaffected whatever the server supports.

Cons

  • A server that advertises nothing still gets one failed save before the panel locks, because only the refusal reveals it.
  • Hosts already left with knocking on and no ports are not repaired. Their owner turns the switch off, or re-enters the ports once the server is updated.
  • One extra unauthenticated GET /v1/meta at most every 5 minutes while team secrets are written or a host editor is open.
  • A secret queued for retry after a personal-to-team move still stays readable on the mover's device, as before. It uploads once the server is updated.

Deploy order

Server first (VoltiusApp/server#103), as usual. The client also works against the current server, which advertises nothing.

Tests

  • New serverCapabilities.test.ts (8 tests): advertised list, unknown server, refusal memory, refresh interval, failed fetch, reset.
  • secretRouting.test.ts: unlisted type never uploaded and cache restored; 400 from an old server drops the value and is remembered; 400 on a type every server stores stays an ordinary failure; queued key keeps its local copy; unreachable meta never blocks an upload.
  • PortKnockPanel.test.tsx and connectionForms.characterisation.test.tsx: warning shown, port list hidden, switch off-only, sequence withheld and knocking switched back off, personal host untouched.
  • tsc --noEmit: clean. vitest: all 807 test files pass.
  • Not run in the live app, and not run against a real pre-chore(deps): bump the actions group with 8 updates #100 server. I confirmed in the server source that a build before feat(team-vault): accept connection knock sequences as a team secret server#100 answers 400 to this type.

Saving a port-knock sequence on a team-vault host against a server
older than its knock support failed with a bare "400", yet the port
reappeared when the editor was reopened: the value was cached before
the upload and never taken back. The Port Knocking switch, stored on
the host itself, saved fine, so the host could end up knocking with no
ports and fail to connect for the whole team.

The client now reads the secret types a server advertises in /v1/meta
and never uploads one that is missing. A server too old to advertise
anything is still tried; a 400 on the knock sequence is read as "too
old" and remembered until the next check. Either way the cache goes
back to what the server holds, the error says the server needs an
update, and the refusal is logged with the secret type and client
version.

On such a server the knock panel of a team-vault host explains why
instead of offering ports, the switch can only be turned off, and a
switch whose sequence was just refused is turned back off.

A secret queued for retry after a move into a team vault keeps its
local copy as before and uploads once the server is updated.
@kipavy
kipavy merged commit c8f330b into dev Oct 10, 2026
5 checks passed
@kipavy
kipavy deleted the fix/608-team-secret-type-support branch October 10, 2026 21:51
kipavy added a commit that referenced this pull request Oct 11, 2026
)

A team secret whose upload failed stayed in the in-memory cache as if the
server held it, then vanished at the next vault load. #609 fixed that only for
a server too old for the secret's type.

The cache now keeps two layers: what the server holds, and over it the values
whose upload has not landed. A write shows its value from the second layer and
moves it to the first only once the server has accepted it.

- Refused (any 4xx except 401, 408 and 429): the value is dropped at once and
  the editor shows what the server holds again.
- Not reached (offline, timeout, 5xx, expired session): the value stays usable,
  survives vault reloads, and is uploaded again after every load of the team,
  foreground or background, until it lands or the app closes. Nothing is
  written to disk, so the memory-only rule of #402 holds. Clearing the secret
  drops the unsent value, so a later retry cannot bring it back.

Connect prompts, import, paste, duplicate, copy to vault, undo and plugin key
creation only logged these failures; they now raise one toast per distinct
failure. The opportunistic public-key backfill stays log-only.

Keys queued by a move into a team keep their on-device copy and their sweep
retry, as before.

Refs #608
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