feat(security): nudge to sign out sessions unused for 3 months (#113)
CI / Build & Quality Checks (push) Skipped
CI / Secret scan (gitleaks) (push) Skipped
CI / Playwright smoke (e2e) (push) Skipped
CI / Build & Quality Checks (pull_request) Successful in 4m4s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 17s
CI / Playwright smoke (e2e) (pull_request) Successful in 24m20s
CI / Build & Quality Checks (push) Skipped
CI / Secret scan (gitleaks) (push) Skipped
CI / Playwright smoke (e2e) (push) Skipped
CI / Build & Quality Checks (pull_request) Successful in 4m4s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 17s
CI / Playwright smoke (e2e) (pull_request) Successful in 24m20s
Sessions of this account not seen for 90+ days (never the current one; a
session with no last-seen time is left alone) get a nudge in the existing
security strip, after the device-security ones and with the same rules
("Not now" snoozes 7 days, gone after 3 dismissals; a snoozed security
nudge no longer hides it): "You have 3 sessions you haven't used in 3
months. Signing them out keeps your account safe if an old device is
lost." [Review]
Review opens Settings -> Devices with those sessions ticked for sign-out
(one confirm, the usual re-auth incl. SSO); they are listed first and
marked "Inactive for 3+ months". OIDC accounts sign out on the
provider's dashboard, so nothing is ticked there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
02c852e194
commit
c907b0ba99
@@ -9,12 +9,15 @@
|
||||
* - setup-backup: no key backup at all (includes accounts that never set up
|
||||
* cross-signing: the setup flow does both).
|
||||
*
|
||||
* - stale-sessions (#113, lowest priority): sessions of this account not used
|
||||
* for 90+ days; Review opens Settings → Devices with them ticked to sign out.
|
||||
*
|
||||
* 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 NudgeKind = 'verify-device' | 'connect-backup' | 'setup-backup' | 'stale-sessions';
|
||||
|
||||
export type SecurityState = {
|
||||
/** m.cross_signing.master account data present. */
|
||||
@@ -38,6 +41,51 @@ export const nudgeFor = (s: SecurityState): NudgeKind | undefined => {
|
||||
return s.backupActive ? undefined : 'connect-backup';
|
||||
};
|
||||
|
||||
/** [Gitea #113] A session counts as stale after this long unused. */
|
||||
export const STALE_SESSION_MS = 90 * 24 * 60 * 60 * 1000;
|
||||
|
||||
type SessionLike = { device_id: string; last_seen_ts?: number | null };
|
||||
|
||||
/**
|
||||
* Pure: other sessions of this account not seen for STALE_SESSION_MS. Never
|
||||
* the current session; a session with no last-seen time is left alone (we
|
||||
* can't tell it's unused).
|
||||
*/
|
||||
export const staleSessions = <T extends SessionLike>(
|
||||
devices: T[],
|
||||
currentDeviceId: string | undefined,
|
||||
now: number,
|
||||
): T[] =>
|
||||
devices.filter(
|
||||
(d) =>
|
||||
d.device_id !== currentDeviceId &&
|
||||
typeof d.last_seen_ts === 'number' &&
|
||||
now - d.last_seen_ts >= STALE_SESSION_MS,
|
||||
);
|
||||
|
||||
/** "1 session" / "3 sessions you haven't used in 3 months". */
|
||||
export const staleSessionsText = (count: number): string =>
|
||||
`You have ${count === 1 ? 'a session' : `${count} sessions`} you haven't used in 3 months. Signing ${
|
||||
count === 1 ? 'it' : 'them'
|
||||
} out keeps your account safe if an old device is lost.`;
|
||||
|
||||
/**
|
||||
* The nudge to show: the device-security one first, then stale sessions, each
|
||||
* only if its own snooze allows (so a snoozed security nudge doesn't hide the
|
||||
* stale-sessions one, and vice versa).
|
||||
*/
|
||||
export const pickNudge = (
|
||||
security: NudgeKind | undefined,
|
||||
staleCount: number,
|
||||
record: NudgeRecord,
|
||||
now: number,
|
||||
): NudgeKind | undefined => {
|
||||
const candidates: NudgeKind[] = [];
|
||||
if (security) candidates.push(security);
|
||||
if (staleCount > 0) candidates.push('stale-sessions');
|
||||
return candidates.find((k) => shouldShowNudge(k, record, now));
|
||||
};
|
||||
|
||||
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;
|
||||
@@ -87,6 +135,11 @@ export const NUDGE_COPY: Record<NudgeKind, { text: string; action: string }> = {
|
||||
text: "This device isn't using your key backup yet. Connect it so your encrypted messages stay readable if you lose it.",
|
||||
action: 'Connect',
|
||||
},
|
||||
'stale-sessions': {
|
||||
// The text depends on the count: see staleSessionsText.
|
||||
text: '',
|
||||
action: 'Review',
|
||||
},
|
||||
'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',
|
||||
|
||||
Reference in New Issue
Block a user