/** * [Gitea #110, #123] One "security" nudge for this device, in priority order: * * - verify-device: the account has cross-signing but THIS device isn't signed. * It can't unlock backed-up history and shows as untrusted to others. * Verifying with the recovery key also connects the backup, so it comes first. * - connect-backup: a key backup exists on the server but this device isn't * backing up to it (`getActiveSessionBackupVersion()` null). * - setup-backup: no key backup at all (includes accounts that never set up * cross-signing: the setup flow does both). * * Anything not yet known (crypto still loading) → no nudge. Timing: not in * the first 24 h on a device (onboarding), "Not now" snoozes 7 days, three * dismissals of a nudge stop it for good; a healthy device never sees one. */ export type NudgeKind = 'verify-device' | 'connect-backup' | 'setup-backup'; export type SecurityState = { /** m.cross_signing.master account data present. */ crossSigning: boolean; /** This device cross-signing-verified; undefined while crypto isn't ready. */ deviceVerified: boolean | undefined; /** A key backup version on the server; undefined while loading. */ backupOnServer: boolean | undefined; /** This device is backing up to it; undefined while loading. */ backupActive: boolean | undefined; }; export const nudgeFor = (s: SecurityState): NudgeKind | undefined => { if (s.crossSigning) { if (s.deviceVerified === undefined) return undefined; if (!s.deviceVerified) return 'verify-device'; } if (s.backupOnServer === undefined) return undefined; if (!s.backupOnServer) return 'setup-backup'; if (s.backupActive === undefined) return undefined; return s.backupActive ? undefined : 'connect-backup'; }; export const NUDGE_GRACE_MS = 24 * 60 * 60 * 1000; export const NUDGE_SNOOZE_MS = 7 * 24 * 60 * 60 * 1000; export const NUDGE_MAX_DISMISSALS = 3; export type NudgeRecord = { /** When this device was first seen by the nudge (ms). */ firstSeen: number; dismissals: Partial>; }; export const shouldShowNudge = (kind: NudgeKind, record: NudgeRecord, now: number): boolean => { if (now - record.firstSeen < NUDGE_GRACE_MS) return false; const d = record.dismissals[kind]; if (!d) return true; return d.count < NUDGE_MAX_DISMISSALS && now - d.last >= NUDGE_SNOOZE_MS; }; export const dismissNudge = (kind: NudgeKind, record: NudgeRecord, now: number): NudgeRecord => ({ ...record, dismissals: { ...record.dismissals, [kind]: { count: (record.dismissals[kind]?.count ?? 0) + 1, last: now }, }, }); export const NUDGE_KEY_PREFIX = 'lotus-security-nudge-'; /** Parse a stored record; anything unexpected starts a fresh one (grace period from now). */ export const parseNudgeRecord = (raw: string | null, now: number): NudgeRecord => { try { const v = JSON.parse(raw ?? ''); if (v && typeof v.firstSeen === 'number' && v.dismissals && typeof v.dismissals === 'object') { return { firstSeen: v.firstSeen, dismissals: v.dismissals }; } } catch { /* fresh record below */ } return { firstSeen: now, dismissals: {} }; }; export const NUDGE_COPY: Record = { 'verify-device': { text: 'Verify this device so you can read your older encrypted messages and others see it as trusted.', action: 'Verify', }, 'connect-backup': { text: "This device isn't using your key backup yet. Connect it so your encrypted messages stay readable if you lose it.", action: 'Connect', }, 'setup-backup': { text: "Your encryption keys aren't backed up. Set up key backup so you don't lose your encrypted messages if you log out or lose this device.", action: 'Set up', }, };