103a7f2f05
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.
57 lines
2.3 KiB
TypeScript
57 lines
2.3 KiB
TypeScript
// High-level wallet account operations shared by the join, recover and login
|
|
// flows. These compose the low-level primitives (derive / crypto / store) with the
|
|
// browser-native bus session so the page components stay thin.
|
|
|
|
import { bus } from "../busService";
|
|
import type { User } from "../types";
|
|
import { decryptJSON, encryptJSON } from "./crypto";
|
|
import type { WalletIdentity } from "./derive";
|
|
import { getIdentity, putIdentity, type StoredIdentity } from "./store";
|
|
|
|
// saveAndOpen encrypts the identity under `password`, stores it on this device, and
|
|
// opens a bus session as that user. Used by join (new identity) and recover
|
|
// (re-derived identity): both end with a locally-encrypted key plus a live session.
|
|
// The mnemonic/seed is NOT touched here — only the derived keypair is persisted
|
|
// (encrypted). The private key is used to open the session IN THE BROWSER and is
|
|
// never sent to any server (unlike the old gateway model).
|
|
export async function saveAndOpen(
|
|
identity: WalletIdentity,
|
|
handle: string,
|
|
password: string,
|
|
remember = false,
|
|
): Promise<User> {
|
|
const enc = await encryptJSON(identity, password);
|
|
await putIdentity({
|
|
handle,
|
|
signPub: identity.signPub,
|
|
kexPub: identity.kexPub,
|
|
enc,
|
|
createdAt: Date.now(),
|
|
});
|
|
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, 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, remember);
|
|
}
|
|
|
|
// localIdentity returns the device's stored identity record (or null), for the
|
|
// router to decide between the password-unlock screen and the welcome screen, and
|
|
// to greet the user by handle before unlocking.
|
|
export async function localIdentity(): Promise<StoredIdentity | null> {
|
|
return getIdentity();
|
|
}
|
|
|
|
export class NoLocalIdentityError extends Error {
|
|
constructor() {
|
|
super("no local identity on this device");
|
|
this.name = "NoLocalIdentityError";
|
|
}
|
|
}
|