fix(messages): address S2 review — partner key refresh + reactive me

- On a decrypt failure, refresh the partner's cached public key (and drop the
  stale derived conversation key) once per pass and retry. A passphrase reset
  rotates a keypair, so a partner who cached the old public key would otherwise
  encrypt undecryptable messages until a reload; the first failed decrypt now
  self-heals reads AND future sends. (pubCache was assumed set-once.)
- KeyGate reads has_password via a reactive useQuery(['me'], enabled:false)
  passive observer instead of a one-shot getQueryData, so the reset password
  field stays correct even if me resolves after mount.
- Document the remaining multi-device staleness (a reset on another device
  leaves this one 'ready' with the dead key) as a known limitation in useKeyState.
This commit is contained in:
2026-07-23 03:11:35 +02:00
parent 1119eb0218
commit 26e6562921
4 changed files with 53 additions and 11 deletions
+4 -2
View File
@@ -22,8 +22,10 @@ export default function KeyGate({ meId, compact = false }: { meId: number; compa
});
const configured = keyQ.data?.configured ?? false;
// Password accounts must re-verify to reset (guards a hijacked session); Google-only accounts
// have no password to check. Read from the App-owned cache — KeyGate is always rendered signed in.
const hasPassword = qc.getQueryData<{ has_password?: boolean }>(["me"])?.has_password ?? false;
// have no password to check. Reactive passive observer of the App-owned ["me"] query (enabled:
// false — App does the fetching), so hasPassword stays correct even if me resolves after mount.
const meQ = useQuery({ queryKey: ["me"], queryFn: api.me, enabled: false });
const hasPassword = meQ.data?.has_password ?? false;
const [view, setView] = useState<"gate" | "reset">("gate");
const [pass, setPass] = useState("");