fix(auth): land the round-20 UI wiring that never applied, and nonce the staging name

Three of round 21's four findings say the same thing: **the UI half of round 20
was never applied, and I reported it as done.**

The edit script aborted partway on a regex error, after writing one file and
before writing the other two. So `ServerConfigModal` gained the footer prop
but `App.tsx` never passed it — the footer was `undefined` in production and
rendered nothing, visible only to the unit test that passes a descriptor
directly — the extra refresh call sites never landed, and the footer's doc
comment still claimed two secret-entry dialogs while the code had three.

`npm run ci` passed throughout, because every test I added exercises the piece
that *did* land. This is the same failure as round 17's uncommitted files with
a different cause: I verified intent (green gate, clean typecheck) instead of
the artifact. Each edit now asserts it changed the file, and the result is
grepped afterwards — the check costs a second and is the only one that
describes what is actually on disk.

Landed now, and verified in the files rather than inferred:

- `App.tsx` passes `secretStorage` to `ServerConfigModal`, so the disclosure
  the previous commit claimed to add actually renders.
- Every secret-bearing write refreshes the descriptor — config submit and both
  import flows, not just the two settings saves. Any of them can perform the
  pending plaintext-to-encrypted upgrade, after which the footer was reporting
  "Plaintext file" until reload.
- The footer's doc names all three dialogs. The wrong count there was not a
  description of the bug, it *was* the bug: the omitted dialog was the one
  taking secrets with no disclosure.

Also, the fourth finding: **the staging name was reusable across restarts.**
`recoverOrphanedSnapshots` deliberately leaves an orphan when a live
`secrets.json` also exists, and a pid-only name repeats (pid 1 on every
container start) — so the next claim would `rename` straight over that orphan
and permanently discard secrets it may uniquely hold. A per-attempt nonce
makes a staging path unrepeatable.

Full `npm run ci` green, staged tree verified first.

Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-22 22:30:21 -04:00
commit 8f14d371ad

View file

@ -226,9 +226,13 @@ function footerTooltip(info: SecretStorageInfo): string | undefined {
* The permanent "here is where your secrets go" strip at the bottom of the
* Client Settings and Server Settings modals (#1950).
*
* Those two dialogs are the only places the Inspector accepts a secret —
* an OAuth client secret, an enterprise IdP client secret, a stdio `env:`
* value — so they are where the answer belongs. The alternatives were
* It appears wherever the Inspector accepts a secret — Client Settings (the
* enterprise IdP client secret), Server Settings (the per-server OAuth
* client secret and stdio `env:` values), and Server Config (stdio `env:`
* values again). This comment named only the first two until review round
* 20, and the dialog it omitted was the one taking secrets with no
* disclosure at all, so the wrong count here was the bug rather than a
* description of it. The alternatives were
* considered and rejected: a startup banner is seen once, by whoever
* started the process, possibly hours before anyone types anything; a
* toast is seen once and then gone; a dismissible banner is, by design,