feat: persistent session (no re-unlock on reload) + reconnect ACL after createRoom

Session persistence (web/src/session.ts): the unlocked wallet identity is kept
across reloads so an F5 no longer forces a password re-unlock. By default it lives
in sessionStorage (survives F5, cleared with the tab); with 'keep me signed in' it
lives in localStorage (survives closing the browser) bounded by a 30-day absolute
TTL and a 12-hour inactivity auto-lock. logout clears it; activity (send/createRoom)
refreshes the idle timer. No cookie is ever used — the private key never travels to
any server. WalletLogin gains the 'keep me signed in' checkbox; Recover/Join keep
the session by default (recovering/creating on a device implies it is yours).
App.tsx restores the session on mount before falling back to the unlock screen.

ACL reconnect: a room created while connected was not in the NATS per-subject ACL
grant (subjects are frozen at connect time), so its first messages silently did not
deliver until a re-login. WsNatsTransport gains reconnect(); BusClient.refresh()
calls it; busService.createRoom reconnects after creating so the new room is usable
immediately. Bumps uniweb to 0.4.0.
This commit is contained in:
2026-06-14 13:58:06 +02:00
parent 1dc8b6257a
commit 103a7f2f05
10 changed files with 229 additions and 33 deletions
+4 -3
View File
@@ -18,6 +18,7 @@ export async function saveAndOpen(
identity: WalletIdentity,
handle: string,
password: string,
remember = false,
): Promise<User> {
const enc = await encryptJSON(identity, password);
await putIdentity({
@@ -27,17 +28,17 @@ export async function saveAndOpen(
enc,
createdAt: Date.now(),
});
return bus.openSession(identity, handle);
return bus.openSession(identity, handle, remember);
}
// unlockAndOpen reads this device's stored identity, decrypts the private key with
// `password`, and opens a bus session locally. Throws WrongPasswordError on a bad
// password (GCM auth failure) and NoLocalIdentityError if the device has none.
export async function unlockAndOpen(password: string): Promise<User> {
export async function unlockAndOpen(password: string, remember = false): Promise<User> {
const stored = await getIdentity();
if (!stored) throw new NoLocalIdentityError();
const identity = await decryptJSON<WalletIdentity>(stored.enc, password);
return bus.openSession(identity, stored.handle);
return bus.openSession(identity, stored.handle, remember);
}
// localIdentity returns the device's stored identity record (or null), for the