atagen

macltok

public
Star0
Watch0
ffe4843e546db1e01d5ff509132aae1a431c0be9
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git

Blame: src/accounts.rs

cb0668d30c14atagen2026-10-08 11:34 UTC1
use std::sync::Arc;
cb0668d30c14atagen2026-10-08 11:34 UTC2
#[cfg(feature = "sqlite")]
cb0668d30c14atagen2026-10-08 11:34 UTC3
use std::{collections::BTreeSet, path::PathBuf};
cb0668d30c14atagen2026-10-08 11:34 UTC4
cb0668d30c14atagen2026-10-08 11:34 UTC5
use ruma::TransactionId;
cb0668d30c14atagen2026-10-08 11:34 UTC6
cb0668d30c14atagen2026-10-08 11:34 UTC7
use crate::{
cb0668d30c14atagen2026-10-08 11:34 UTC8
    AccountError, EncryptedSessionStore, OAuthLogin, PasswordLogin, PendingOAuthLogin, Restore,
d8d8a9fe770aatagen2026-10-22 07:44 UTC9
    Session, SessionRecord, SessionStore, SoftLogout, StoreConfig,
d8d8a9fe770aatagen2026-10-22 07:44 UTC10
    account::{callback_state, homeserver_input},
cb0668d30c14atagen2026-10-08 11:34 UTC11
    engine::StoreLock,
cb0668d30c14atagen2026-10-08 11:34 UTC12
};
cb0668d30c14atagen2026-10-08 11:34 UTC13
cb0668d30c14atagen2026-10-08 11:34 UTC14
/// every account one owner has, a store each under one root and one passphrase. whoever holds
cb0668d30c14atagen2026-10-08 11:34 UTC15
/// the set can resume any soft-logged-out device in it with just its password, and the device's
cb0668d30c14atagen2026-10-08 11:34 UTC16
/// store has its e2ee keys, so a set belongs to one person and never to a whole machine
cb0668d30c14atagen2026-10-08 11:34 UTC17
pub struct Accounts {
cb0668d30c14atagen2026-10-08 11:34 UTC18
    root: Root,
cb0668d30c14atagen2026-10-08 11:34 UTC19
    passphrase: String,
cb0668d30c14atagen2026-10-08 11:34 UTC20
}
cb0668d30c14atagen2026-10-08 11:34 UTC21
cb0668d30c14atagen2026-10-08 11:34 UTC22
enum Root {
cb0668d30c14atagen2026-10-08 11:34 UTC23
    #[cfg(feature = "sqlite")]
cb0668d30c14atagen2026-10-08 11:34 UTC24
    Directory(PathBuf),
cb0668d30c14atagen2026-10-08 11:34 UTC25
    #[cfg(feature = "indexeddb")]
cb0668d30c14atagen2026-10-08 11:34 UTC26
    Prefix(String),
cb0668d30c14atagen2026-10-08 11:34 UTC27
}
cb0668d30c14atagen2026-10-08 11:34 UTC28
cb0668d30c14atagen2026-10-08 11:34 UTC29
/// one account's record and store
cb0668d30c14atagen2026-10-08 11:34 UTC30
#[derive(Clone)]
cb0668d30c14atagen2026-10-08 11:34 UTC31
pub struct Account {
cb0668d30c14atagen2026-10-08 11:34 UTC32
    id: String,
cb0668d30c14atagen2026-10-08 11:34 UTC33
    sessions: Arc<EncryptedSessionStore>,
cb0668d30c14atagen2026-10-08 11:34 UTC34
}
cb0668d30c14atagen2026-10-08 11:34 UTC35
cb0668d30c14atagen2026-10-08 11:34 UTC36
impl Account {
cb0668d30c14atagen2026-10-08 11:34 UTC37
    /// stable for the account's life, and safe in a cookie or a url
cb0668d30c14atagen2026-10-08 11:34 UTC38
    #[must_use]
cb0668d30c14atagen2026-10-08 11:34 UTC39
    pub fn id(&self) -> &str {
cb0668d30c14atagen2026-10-08 11:34 UTC40
        &self.id
cb0668d30c14atagen2026-10-08 11:34 UTC41
    }
cb0668d30c14atagen2026-10-08 11:34 UTC42
cb0668d30c14atagen2026-10-08 11:34 UTC43
    /// for `Termination::retain`. a discarded account drops out of `Accounts::list`
cb0668d30c14atagen2026-10-08 11:34 UTC44
    #[must_use]
cb0668d30c14atagen2026-10-08 11:34 UTC45
    pub fn sessions(&self) -> Arc<dyn SessionStore> {
cb0668d30c14atagen2026-10-08 11:34 UTC46
        self.sessions.clone()
cb0668d30c14atagen2026-10-08 11:34 UTC47
    }
cb0668d30c14atagen2026-10-08 11:34 UTC48
cb0668d30c14atagen2026-10-08 11:34 UTC49
    pub async fn restore(&self) -> Result<Option<Restore>, AccountError> {
cb0668d30c14atagen2026-10-08 11:34 UTC50
        Session::restore(self.sessions()).await
cb0668d30c14atagen2026-10-08 11:34 UTC51
    }
cb0668d30c14atagen2026-10-08 11:34 UTC52
}
cb0668d30c14atagen2026-10-08 11:34 UTC53
cb0668d30c14atagen2026-10-08 11:34 UTC54
impl std::fmt::Debug for Account {
cb0668d30c14atagen2026-10-08 11:34 UTC55
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
cb0668d30c14atagen2026-10-08 11:34 UTC56
        formatter
cb0668d30c14atagen2026-10-08 11:34 UTC57
            .debug_struct("Account")
cb0668d30c14atagen2026-10-08 11:34 UTC58
            .field("id", &self.id)
cb0668d30c14atagen2026-10-08 11:34 UTC59
            .finish_non_exhaustive()
cb0668d30c14atagen2026-10-08 11:34 UTC60
    }
cb0668d30c14atagen2026-10-08 11:34 UTC61
}
cb0668d30c14atagen2026-10-08 11:34 UTC62
cb0668d30c14atagen2026-10-08 11:34 UTC63
enum Found {
cb0668d30c14atagen2026-10-08 11:34 UTC64
    Nobody,
cb0668d30c14atagen2026-10-08 11:34 UTC65
    SoftLoggedOut(Account, SoftLogout),
cb0668d30c14atagen2026-10-08 11:34 UTC66
}
cb0668d30c14atagen2026-10-08 11:34 UTC67
cb0668d30c14atagen2026-10-08 11:34 UTC68
impl Accounts {
cb0668d30c14atagen2026-10-08 11:34 UTC69
    /// each account is a store directory under `root` with its record beside it
cb0668d30c14atagen2026-10-08 11:34 UTC70
    #[cfg(feature = "sqlite")]
cb0668d30c14atagen2026-10-08 11:34 UTC71
    #[must_use]
cb0668d30c14atagen2026-10-08 11:34 UTC72
    pub fn sqlite(root: impl Into<PathBuf>, passphrase: impl Into<String>) -> Self {
cb0668d30c14atagen2026-10-08 11:34 UTC73
        Self {
cb0668d30c14atagen2026-10-08 11:34 UTC74
            root: Root::Directory(root.into()),
cb0668d30c14atagen2026-10-08 11:34 UTC75
            passphrase: passphrase.into(),
cb0668d30c14atagen2026-10-08 11:34 UTC76
        }
cb0668d30c14atagen2026-10-08 11:34 UTC77
    }
cb0668d30c14atagen2026-10-08 11:34 UTC78
cb0668d30c14atagen2026-10-08 11:34 UTC79
    /// each account is a store named `<prefix>::<id>`, listed in `<prefix>::macltok-accounts`
cb0668d30c14atagen2026-10-08 11:34 UTC80
    #[cfg(feature = "indexeddb")]
cb0668d30c14atagen2026-10-08 11:34 UTC81
    #[must_use]
cb0668d30c14atagen2026-10-08 11:34 UTC82
    pub fn indexeddb(prefix: impl Into<String>, passphrase: impl Into<String>) -> Self {
cb0668d30c14atagen2026-10-08 11:34 UTC83
        Self {
cb0668d30c14atagen2026-10-08 11:34 UTC84
            root: Root::Prefix(prefix.into()),
cb0668d30c14atagen2026-10-08 11:34 UTC85
            passphrase: passphrase.into(),
cb0668d30c14atagen2026-10-08 11:34 UTC86
        }
cb0668d30c14atagen2026-10-08 11:34 UTC87
    }
cb0668d30c14atagen2026-10-08 11:34 UTC88
cb0668d30c14atagen2026-10-08 11:34 UTC89
    /// `None` for anything `Account::id` couldn't have been
cb0668d30c14atagen2026-10-08 11:34 UTC90
    #[must_use]
cb0668d30c14atagen2026-10-08 11:34 UTC91
    pub fn account(&self, id: &str) -> Option<Account> {
cb0668d30c14atagen2026-10-08 11:34 UTC92
        let valid =
cb0668d30c14atagen2026-10-08 11:34 UTC93
            !id.is_empty() && id.len() <= 64 && id.bytes().all(|b| b.is_ascii_alphanumeric());
cb0668d30c14atagen2026-10-08 11:34 UTC94
        valid.then(|| self.open(id.to_owned()))
cb0668d30c14atagen2026-10-08 11:34 UTC95
    }
cb0668d30c14atagen2026-10-08 11:34 UTC96
cb0668d30c14atagen2026-10-08 11:34 UTC97
    /// every account with a record. a store left without one, by a login that died halfway or a
d8d8a9fe770aatagen2026-10-22 07:44 UTC98
    /// discard that couldn't finish, is deleted on the way past, and so is an oauth login whose
d8d8a9fe770aatagen2026-10-22 07:44 UTC99
    /// redirect never came back. one that resumed an account puts that account back
cb0668d30c14atagen2026-10-08 11:34 UTC100
    pub async fn list(&self) -> Result<Vec<Account>, AccountError> {
cb0668d30c14atagen2026-10-08 11:34 UTC101
        let mut accounts = Vec::new();
cb0668d30c14atagen2026-10-08 11:34 UTC102
        for account in self.slots().await? {
cb0668d30c14atagen2026-10-08 11:34 UTC103
            match account.sessions.load().await {
cb0668d30c14atagen2026-10-08 11:34 UTC104
                Ok(None) => self.sweep(&account).await,
d8d8a9fe770aatagen2026-10-22 07:44 UTC105
                Ok(Some(record)) if record.oauth_expired() => {
d8d8a9fe770aatagen2026-10-22 07:44 UTC106
                    if self.expire(&account).await {
d8d8a9fe770aatagen2026-10-22 07:44 UTC107
                        accounts.push(account);
d8d8a9fe770aatagen2026-10-22 07:44 UTC108
                    }
d8d8a9fe770aatagen2026-10-22 07:44 UTC109
                }
cb0668d30c14atagen2026-10-08 11:34 UTC110
                Ok(Some(_)) | Err(_) => accounts.push(account),
cb0668d30c14atagen2026-10-08 11:34 UTC111
            }
cb0668d30c14atagen2026-10-08 11:34 UTC112
        }
cb0668d30c14atagen2026-10-08 11:34 UTC113
        Ok(accounts)
cb0668d30c14atagen2026-10-08 11:34 UTC114
    }
cb0668d30c14atagen2026-10-08 11:34 UTC115
d8d8a9fe770aatagen2026-10-22 07:44 UTC116
    /// finishes an oauth login begun before a redirect, finding its account by the callback's
d8d8a9fe770aatagen2026-10-22 07:44 UTC117
    /// state, so the host needs to have kept nothing across the redirect
d8d8a9fe770aatagen2026-10-22 07:44 UTC118
    pub async fn finish_oauth(
d8d8a9fe770aatagen2026-10-22 07:44 UTC119
        &self,
d8d8a9fe770aatagen2026-10-22 07:44 UTC120
        callback_url: &str,
d8d8a9fe770aatagen2026-10-22 07:44 UTC121
    ) -> Result<(Account, Session), AccountError> {
d8d8a9fe770aatagen2026-10-22 07:44 UTC122
        let state = url::Url::parse(callback_url)
d8d8a9fe770aatagen2026-10-22 07:44 UTC123
            .ok()
d8d8a9fe770aatagen2026-10-22 07:44 UTC124
            .as_ref()
d8d8a9fe770aatagen2026-10-22 07:44 UTC125
            .and_then(callback_state)
d8d8a9fe770aatagen2026-10-22 07:44 UTC126
            .ok_or(AccountError::NoPendingOAuth)?;
d8d8a9fe770aatagen2026-10-22 07:44 UTC127
        for account in self.slots().await? {
d8d8a9fe770aatagen2026-10-22 07:44 UTC128
            let Ok(Some(record)) = account.sessions.load().await else {
d8d8a9fe770aatagen2026-10-22 07:44 UTC129
                continue;
d8d8a9fe770aatagen2026-10-22 07:44 UTC130
            };
d8d8a9fe770aatagen2026-10-22 07:44 UTC131
            if record.pending_oauth_state() != Some(state.as_str()) {
d8d8a9fe770aatagen2026-10-22 07:44 UTC132
                continue;
d8d8a9fe770aatagen2026-10-22 07:44 UTC133
            }
d8d8a9fe770aatagen2026-10-22 07:44 UTC134
            let store_lock = account.sessions.store().lock().await?;
d8d8a9fe770aatagen2026-10-22 07:44 UTC135
            let session =
d8d8a9fe770aatagen2026-10-22 07:44 UTC136
                Session::finish_oauth_locked(account.sessions(), store_lock, callback_url).await?;
d8d8a9fe770aatagen2026-10-22 07:44 UTC137
            return Ok((account, session));
d8d8a9fe770aatagen2026-10-22 07:44 UTC138
        }
d8d8a9fe770aatagen2026-10-22 07:44 UTC139
        Err(AccountError::NoPendingOAuth)
d8d8a9fe770aatagen2026-10-22 07:44 UTC140
    }
d8d8a9fe770aatagen2026-10-22 07:44 UTC141
cb0668d30c14atagen2026-10-08 11:34 UTC142
    /// a soft-logged-out account the login names is resumed as its own device, keeping its keys.
cb0668d30c14atagen2026-10-08 11:34 UTC143
    /// anyone else gets a new account
cb0668d30c14atagen2026-10-08 11:34 UTC144
    pub async fn login_password(
cb0668d30c14atagen2026-10-08 11:34 UTC145
        &self,
cb0668d30c14atagen2026-10-08 11:34 UTC146
        login: PasswordLogin,
cb0668d30c14atagen2026-10-08 11:34 UTC147
    ) -> Result<(Account, Session), AccountError> {
cb0668d30c14atagen2026-10-08 11:34 UTC148
        homeserver_input(&login.homeserver, login.allow_insecure_loopback)?;
cb0668d30c14atagen2026-10-08 11:34 UTC149
        let found = self
cb0668d30c14atagen2026-10-08 11:34 UTC150
            .find(|record| record_names(record, &login.homeserver, &login.username))
cb0668d30c14atagen2026-10-08 11:34 UTC151
            .await?;
cb0668d30c14atagen2026-10-08 11:34 UTC152
        match found {
cb0668d30c14atagen2026-10-08 11:34 UTC153
            Found::SoftLoggedOut(account, soft) => {
cb0668d30c14atagen2026-10-08 11:34 UTC154
                let store_lock = account.sessions.store().lock().await?;
cb0668d30c14atagen2026-10-08 11:34 UTC155
                let login = PasswordLogin {
cb0668d30c14atagen2026-10-08 11:34 UTC156
                    device_display_name: login.device_display_name,
cb0668d30c14atagen2026-10-08 11:34 UTC157
                    ..soft.password_login(login.password)
cb0668d30c14atagen2026-10-08 11:34 UTC158
                };
cb0668d30c14atagen2026-10-08 11:34 UTC159
                let session =
cb0668d30c14atagen2026-10-08 11:34 UTC160
                    Session::login_password_locked(login, account.sessions(), store_lock).await?;
cb0668d30c14atagen2026-10-08 11:34 UTC161
                Ok((account, session))
cb0668d30c14atagen2026-10-08 11:34 UTC162
            }
cb0668d30c14atagen2026-10-08 11:34 UTC163
            Found::Nobody => {
cb0668d30c14atagen2026-10-08 11:34 UTC164
                let (account, store_lock) = self.fresh().await?;
cb0668d30c14atagen2026-10-08 11:34 UTC165
                let login = PasswordLogin {
cb0668d30c14atagen2026-10-08 11:34 UTC166
                    device_id: None,
cb0668d30c14atagen2026-10-08 11:34 UTC167
                    ..login
cb0668d30c14atagen2026-10-08 11:34 UTC168
                };
cb0668d30c14atagen2026-10-08 11:34 UTC169
                match Session::login_password_locked(login, account.sessions(), store_lock).await {
cb0668d30c14atagen2026-10-08 11:34 UTC170
                    Ok(session) => Ok((account, session)),
cb0668d30c14atagen2026-10-08 11:34 UTC171
                    Err(error) => Err(self.abandon(&account, error).await),
cb0668d30c14atagen2026-10-08 11:34 UTC172
                }
cb0668d30c14atagen2026-10-08 11:34 UTC173
            }
cb0668d30c14atagen2026-10-08 11:34 UTC174
        }
cb0668d30c14atagen2026-10-08 11:34 UTC175
    }
cb0668d30c14atagen2026-10-08 11:34 UTC176
cb0668d30c14atagen2026-10-08 11:34 UTC177
    /// resumes the soft-logged-out account whose device `login` names, as
cb0668d30c14atagen2026-10-08 11:34 UTC178
    /// `SoftLogout::oauth_login` sets it. without one it's a new account, since which account an
cb0668d30c14atagen2026-10-08 11:34 UTC179
    /// OAuth login is for isn't known until it finishes
cb0668d30c14atagen2026-10-08 11:34 UTC180
    pub async fn begin_oauth(
cb0668d30c14atagen2026-10-08 11:34 UTC181
        &self,
cb0668d30c14atagen2026-10-08 11:34 UTC182
        login: OAuthLogin,
cb0668d30c14atagen2026-10-08 11:34 UTC183
    ) -> Result<(Account, PendingOAuthLogin), AccountError> {
cb0668d30c14atagen2026-10-08 11:34 UTC184
        homeserver_input(&login.homeserver, login.allow_insecure_loopback)?;
cb0668d30c14atagen2026-10-08 11:34 UTC185
        let device_id = login.device_id.clone();
cb0668d30c14atagen2026-10-08 11:34 UTC186
        let found = self
cb0668d30c14atagen2026-10-08 11:34 UTC187
            .find(|record| {
cb0668d30c14atagen2026-10-08 11:34 UTC188
                record
cb0668d30c14atagen2026-10-08 11:34 UTC189
                    .soft_logout()
cb0668d30c14atagen2026-10-08 11:34 UTC190
                    .is_some_and(|soft| device_id.as_ref() == Some(&soft.device_id))
cb0668d30c14atagen2026-10-08 11:34 UTC191
            })
cb0668d30c14atagen2026-10-08 11:34 UTC192
            .await?;
cb0668d30c14atagen2026-10-08 11:34 UTC193
        match found {
cb0668d30c14atagen2026-10-08 11:34 UTC194
            Found::SoftLoggedOut(account, soft) => {
cb0668d30c14atagen2026-10-08 11:34 UTC195
                let store_lock = account.sessions.store().lock().await?;
cb0668d30c14atagen2026-10-08 11:34 UTC196
                let login = OAuthLogin {
cb0668d30c14atagen2026-10-08 11:34 UTC197
                    homeserver: soft.homeserver,
cb0668d30c14atagen2026-10-08 11:34 UTC198
                    client_id: login.client_id.or(soft.client_id),
cb0668d30c14atagen2026-10-08 11:34 UTC199
                    allow_insecure_loopback: true,
cb0668d30c14atagen2026-10-08 11:34 UTC200
                    ..login
cb0668d30c14atagen2026-10-08 11:34 UTC201
                };
cb0668d30c14atagen2026-10-08 11:34 UTC202
                let pending =
cb0668d30c14atagen2026-10-08 11:34 UTC203
                    Session::begin_oauth_locked(login, account.sessions(), store_lock).await?;
cb0668d30c14atagen2026-10-08 11:34 UTC204
                Ok((account, pending))
cb0668d30c14atagen2026-10-08 11:34 UTC205
            }
cb0668d30c14atagen2026-10-08 11:34 UTC206
            Found::Nobody => {
cb0668d30c14atagen2026-10-08 11:34 UTC207
                let (account, store_lock) = self.fresh().await?;
cb0668d30c14atagen2026-10-08 11:34 UTC208
                let login = OAuthLogin {
cb0668d30c14atagen2026-10-08 11:34 UTC209
                    device_id: None,
cb0668d30c14atagen2026-10-08 11:34 UTC210
                    ..login
cb0668d30c14atagen2026-10-08 11:34 UTC211
                };
cb0668d30c14atagen2026-10-08 11:34 UTC212
                match Session::begin_oauth_locked(login, account.sessions(), store_lock).await {
cb0668d30c14atagen2026-10-08 11:34 UTC213
                    Ok(pending) => Ok((account, pending)),
cb0668d30c14atagen2026-10-08 11:34 UTC214
                    Err(error) => Err(self.abandon(&account, error).await),
cb0668d30c14atagen2026-10-08 11:34 UTC215
                }
cb0668d30c14atagen2026-10-08 11:34 UTC216
            }
cb0668d30c14atagen2026-10-08 11:34 UTC217
        }
cb0668d30c14atagen2026-10-08 11:34 UTC218
    }
cb0668d30c14atagen2026-10-08 11:34 UTC219
cb0668d30c14atagen2026-10-08 11:34 UTC220
    /// a live login for the same person is refused rather than given a second device
cb0668d30c14atagen2026-10-08 11:34 UTC221
    async fn find(&self, resumes: impl Fn(&SessionRecord) -> bool) -> Result<Found, AccountError> {
cb0668d30c14atagen2026-10-08 11:34 UTC222
        for account in self.slots().await? {
cb0668d30c14atagen2026-10-08 11:34 UTC223
            let Ok(Some(record)) = account.sessions.load().await else {
cb0668d30c14atagen2026-10-08 11:34 UTC224
                continue;
cb0668d30c14atagen2026-10-08 11:34 UTC225
            };
cb0668d30c14atagen2026-10-08 11:34 UTC226
            if !resumes(&record) {
cb0668d30c14atagen2026-10-08 11:34 UTC227
                continue;
cb0668d30c14atagen2026-10-08 11:34 UTC228
            }
d8d8a9fe770aatagen2026-10-22 07:44 UTC229
            return match (record.soft_logout(), record.user_id()) {
d8d8a9fe770aatagen2026-10-22 07:44 UTC230
                (Some(soft), _) => Ok(Found::SoftLoggedOut(account, soft)),
d8d8a9fe770aatagen2026-10-22 07:44 UTC231
                (None, Some(user_id)) => Err(AccountError::AlreadyLoggedIn(user_id.clone())),
d8d8a9fe770aatagen2026-10-22 07:44 UTC232
                (None, None) => continue,
cb0668d30c14atagen2026-10-08 11:34 UTC233
            };
cb0668d30c14atagen2026-10-08 11:34 UTC234
        }
cb0668d30c14atagen2026-10-08 11:34 UTC235
        Ok(Found::Nobody)
cb0668d30c14atagen2026-10-08 11:34 UTC236
    }
cb0668d30c14atagen2026-10-08 11:34 UTC237
cb0668d30c14atagen2026-10-08 11:34 UTC238
    /// locked before anything else can see it, so `list` never sweeps a login still under way
cb0668d30c14atagen2026-10-08 11:34 UTC239
    async fn fresh(&self) -> Result<(Account, StoreLock), AccountError> {
cb0668d30c14atagen2026-10-08 11:34 UTC240
        let account = self.open(TransactionId::new().to_string());
cb0668d30c14atagen2026-10-08 11:34 UTC241
        let store_lock = account.sessions.store().lock().await?;
cb0668d30c14atagen2026-10-08 11:34 UTC242
        #[cfg(feature = "indexeddb")]
cb0668d30c14atagen2026-10-08 11:34 UTC243
        {
cb0668d30c14atagen2026-10-08 11:34 UTC244
            let Root::Prefix(prefix) = &self.root;
cb0668d30c14atagen2026-10-08 11:34 UTC245
            index::add(prefix, &account.id).await?;
cb0668d30c14atagen2026-10-08 11:34 UTC246
        }
cb0668d30c14atagen2026-10-08 11:34 UTC247
        Ok((account, store_lock))
cb0668d30c14atagen2026-10-08 11:34 UTC248
    }
cb0668d30c14atagen2026-10-08 11:34 UTC249
cb0668d30c14atagen2026-10-08 11:34 UTC250
    /// a failed first login leaves nothing behind. one whose record didn't save has a live device
cb0668d30c14atagen2026-10-08 11:34 UTC251
    /// on the server, so that stays for `UnsavedSession::save`
cb0668d30c14atagen2026-10-08 11:34 UTC252
    async fn abandon(&self, account: &Account, error: AccountError) -> AccountError {
cb0668d30c14atagen2026-10-08 11:34 UTC253
        if !matches!(error, AccountError::Unsaved { .. }) {
cb0668d30c14atagen2026-10-08 11:34 UTC254
            self.sweep(account).await;
cb0668d30c14atagen2026-10-08 11:34 UTC255
        }
cb0668d30c14atagen2026-10-08 11:34 UTC256
        error
cb0668d30c14atagen2026-10-08 11:34 UTC257
    }
cb0668d30c14atagen2026-10-08 11:34 UTC258
cb0668d30c14atagen2026-10-08 11:34 UTC259
    /// best effort, since whatever it leaves is found and swept again by the next `list`
cb0668d30c14atagen2026-10-08 11:34 UTC260
    async fn sweep(&self, account: &Account) {
cb0668d30c14atagen2026-10-08 11:34 UTC261
        let Ok(_store_lock) = account.sessions.store().lock().await else {
cb0668d30c14atagen2026-10-08 11:34 UTC262
            return;
cb0668d30c14atagen2026-10-08 11:34 UTC263
        };
d8d8a9fe770aatagen2026-10-22 07:44 UTC264
        if matches!(account.sessions.load().await, Ok(None)) {
d8d8a9fe770aatagen2026-10-22 07:44 UTC265
            self.remove(account).await;
cb0668d30c14atagen2026-10-08 11:34 UTC266
        }
d8d8a9fe770aatagen2026-10-22 07:44 UTC267
    }
d8d8a9fe770aatagen2026-10-22 07:44 UTC268
d8d8a9fe770aatagen2026-10-22 07:44 UTC269
    /// whether the account is still there. a login running in another tab holds the lock and
d8d8a9fe770aatagen2026-10-22 07:44 UTC270
    /// is left alone
d8d8a9fe770aatagen2026-10-22 07:44 UTC271
    async fn expire(&self, account: &Account) -> bool {
d8d8a9fe770aatagen2026-10-22 07:44 UTC272
        let Ok(_store_lock) = account.sessions.store().lock().await else {
d8d8a9fe770aatagen2026-10-22 07:44 UTC273
            return true;
d8d8a9fe770aatagen2026-10-22 07:44 UTC274
        };
d8d8a9fe770aatagen2026-10-22 07:44 UTC275
        let Ok(Some(record)) = account.sessions.load().await else {
d8d8a9fe770aatagen2026-10-22 07:44 UTC276
            return true;
d8d8a9fe770aatagen2026-10-22 07:44 UTC277
        };
d8d8a9fe770aatagen2026-10-22 07:44 UTC278
        if !record.oauth_expired() {
d8d8a9fe770aatagen2026-10-22 07:44 UTC279
            return true;
d8d8a9fe770aatagen2026-10-22 07:44 UTC280
        }
d8d8a9fe770aatagen2026-10-22 07:44 UTC281
        match record.resumable() {
d8d8a9fe770aatagen2026-10-22 07:44 UTC282
            Some(previous) => {
d8d8a9fe770aatagen2026-10-22 07:44 UTC283
                let _ = account.sessions.save(&previous).await;
d8d8a9fe770aatagen2026-10-22 07:44 UTC284
                true
d8d8a9fe770aatagen2026-10-22 07:44 UTC285
            }
d8d8a9fe770aatagen2026-10-22 07:44 UTC286
            None => {
d8d8a9fe770aatagen2026-10-22 07:44 UTC287
                self.remove(account).await;
d8d8a9fe770aatagen2026-10-22 07:44 UTC288
                false
d8d8a9fe770aatagen2026-10-22 07:44 UTC289
            }
d8d8a9fe770aatagen2026-10-22 07:44 UTC290
        }
d8d8a9fe770aatagen2026-10-22 07:44 UTC291
    }
d8d8a9fe770aatagen2026-10-22 07:44 UTC292
d8d8a9fe770aatagen2026-10-22 07:44 UTC293
    async fn remove(&self, account: &Account) {
cb0668d30c14atagen2026-10-08 11:34 UTC294
        if account.sessions.discard().await.is_err() {
cb0668d30c14atagen2026-10-08 11:34 UTC295
            return;
cb0668d30c14atagen2026-10-08 11:34 UTC296
        }
cb0668d30c14atagen2026-10-08 11:34 UTC297
        #[cfg(feature = "indexeddb")]
cb0668d30c14atagen2026-10-08 11:34 UTC298
        {
cb0668d30c14atagen2026-10-08 11:34 UTC299
            let Root::Prefix(prefix) = &self.root;
cb0668d30c14atagen2026-10-08 11:34 UTC300
            let _ = index::remove(prefix, &account.id).await;
cb0668d30c14atagen2026-10-08 11:34 UTC301
        }
cb0668d30c14atagen2026-10-08 11:34 UTC302
    }
cb0668d30c14atagen2026-10-08 11:34 UTC303
cb0668d30c14atagen2026-10-08 11:34 UTC304
    fn open(&self, id: String) -> Account {
cb0668d30c14atagen2026-10-08 11:34 UTC305
        let sessions = match &self.root {
cb0668d30c14atagen2026-10-08 11:34 UTC306
            #[cfg(feature = "sqlite")]
cb0668d30c14atagen2026-10-08 11:34 UTC307
            Root::Directory(root) => EncryptedSessionStore::file(
cb0668d30c14atagen2026-10-08 11:34 UTC308
                root.join(format!("{id}.session")),
cb0668d30c14atagen2026-10-08 11:34 UTC309
                StoreConfig::sqlite(root.join(&id), self.passphrase.clone()),
cb0668d30c14atagen2026-10-08 11:34 UTC310
            ),
cb0668d30c14atagen2026-10-08 11:34 UTC311
            #[cfg(feature = "indexeddb")]
cb0668d30c14atagen2026-10-08 11:34 UTC312
            Root::Prefix(prefix) => EncryptedSessionStore::indexeddb(StoreConfig::indexeddb(
cb0668d30c14atagen2026-10-08 11:34 UTC313
                format!("{prefix}::{id}"),
cb0668d30c14atagen2026-10-08 11:34 UTC314
                self.passphrase.clone(),
cb0668d30c14atagen2026-10-08 11:34 UTC315
            )),
cb0668d30c14atagen2026-10-08 11:34 UTC316
        };
cb0668d30c14atagen2026-10-08 11:34 UTC317
        Account {
cb0668d30c14atagen2026-10-08 11:34 UTC318
            id,
cb0668d30c14atagen2026-10-08 11:34 UTC319
            sessions: Arc::new(sessions),
cb0668d30c14atagen2026-10-08 11:34 UTC320
        }
cb0668d30c14atagen2026-10-08 11:34 UTC321
    }
cb0668d30c14atagen2026-10-08 11:34 UTC322
cb0668d30c14atagen2026-10-08 11:34 UTC323
    async fn slots(&self) -> Result<Vec<Account>, AccountError> {
cb0668d30c14atagen2026-10-08 11:34 UTC324
        let ids = match &self.root {
cb0668d30c14atagen2026-10-08 11:34 UTC325
            #[cfg(feature = "sqlite")]
cb0668d30c14atagen2026-10-08 11:34 UTC326
            Root::Directory(root) => directory_slots(root)?,
cb0668d30c14atagen2026-10-08 11:34 UTC327
            #[cfg(feature = "indexeddb")]
cb0668d30c14atagen2026-10-08 11:34 UTC328
            Root::Prefix(prefix) => index::list(prefix).await?,
cb0668d30c14atagen2026-10-08 11:34 UTC329
        };
cb0668d30c14atagen2026-10-08 11:34 UTC330
        Ok(ids.iter().filter_map(|id| self.account(id)).collect())
cb0668d30c14atagen2026-10-08 11:34 UTC331
    }
cb0668d30c14atagen2026-10-08 11:34 UTC332
}
cb0668d30c14atagen2026-10-08 11:34 UTC333
cb0668d30c14atagen2026-10-08 11:34 UTC334
/// a localpart or a full user id names the same account, but only on the same server. the server
cb0668d30c14atagen2026-10-08 11:34 UTC335
/// is compared by name as well as by url, so a delegated `example.org` still finds the account
cb0668d30c14atagen2026-10-08 11:34 UTC336
/// discovery took to `matrix.example.org`
cb0668d30c14atagen2026-10-08 11:34 UTC337
fn record_names(record: &SessionRecord, homeserver: &str, username: &str) -> bool {
d8d8a9fe770aatagen2026-10-22 07:44 UTC338
    let Some(user_id) = record.user_id() else {
d8d8a9fe770aatagen2026-10-22 07:44 UTC339
        return false;
d8d8a9fe770aatagen2026-10-22 07:44 UTC340
    };
cb0668d30c14atagen2026-10-08 11:34 UTC341
    let username = username.trim();
cb0668d30c14atagen2026-10-08 11:34 UTC342
    let user = if username.starts_with('@') {
cb0668d30c14atagen2026-10-08 11:34 UTC343
        username.eq_ignore_ascii_case(user_id.as_str())
cb0668d30c14atagen2026-10-08 11:34 UTC344
    } else {
cb0668d30c14atagen2026-10-08 11:34 UTC345
        username.eq_ignore_ascii_case(user_id.localpart())
cb0668d30c14atagen2026-10-08 11:34 UTC346
    };
cb0668d30c14atagen2026-10-08 11:34 UTC347
    let typed = authority(homeserver);
cb0668d30c14atagen2026-10-08 11:34 UTC348
    user && (typed.eq_ignore_ascii_case(user_id.server_name().as_str())
cb0668d30c14atagen2026-10-08 11:34 UTC349
        || typed.eq_ignore_ascii_case(authority(record.homeserver())))
cb0668d30c14atagen2026-10-08 11:34 UTC350
}
cb0668d30c14atagen2026-10-08 11:34 UTC351
cb0668d30c14atagen2026-10-08 11:34 UTC352
fn authority(homeserver: &str) -> &str {
cb0668d30c14atagen2026-10-08 11:34 UTC353
    let homeserver = homeserver.trim();
cb0668d30c14atagen2026-10-08 11:34 UTC354
    let rest = homeserver
cb0668d30c14atagen2026-10-08 11:34 UTC355
        .split_once("://")
cb0668d30c14atagen2026-10-08 11:34 UTC356
        .map_or(homeserver, |(_, rest)| rest);
cb0668d30c14atagen2026-10-08 11:34 UTC357
    rest.split('/').next().unwrap_or(rest)
cb0668d30c14atagen2026-10-08 11:34 UTC358
}
cb0668d30c14atagen2026-10-08 11:34 UTC359
cb0668d30c14atagen2026-10-08 11:34 UTC360
#[cfg(feature = "sqlite")]
cb0668d30c14atagen2026-10-08 11:34 UTC361
fn directory_slots(root: &std::path::Path) -> Result<BTreeSet<String>, AccountError> {
cb0668d30c14atagen2026-10-08 11:34 UTC362
    use std::io::ErrorKind;
cb0668d30c14atagen2026-10-08 11:34 UTC363
cb0668d30c14atagen2026-10-08 11:34 UTC364
    let entries = match std::fs::read_dir(root) {
cb0668d30c14atagen2026-10-08 11:34 UTC365
        Ok(entries) => entries,
cb0668d30c14atagen2026-10-08 11:34 UTC366
        Err(error) if error.kind() == ErrorKind::NotFound => return Ok(BTreeSet::new()),
cb0668d30c14atagen2026-10-08 11:34 UTC367
        Err(error) => return Err(crate::account::account_error("listing the accounts", error)),
cb0668d30c14atagen2026-10-08 11:34 UTC368
    };
cb0668d30c14atagen2026-10-08 11:34 UTC369
    let mut ids = BTreeSet::new();
cb0668d30c14atagen2026-10-08 11:34 UTC370
    for entry in entries {
cb0668d30c14atagen2026-10-08 11:34 UTC371
        let entry =
cb0668d30c14atagen2026-10-08 11:34 UTC372
            entry.map_err(|error| crate::account::account_error("listing the accounts", error))?;
cb0668d30c14atagen2026-10-08 11:34 UTC373
        let name = entry.file_name();
cb0668d30c14atagen2026-10-08 11:34 UTC374
        let Some(name) = name.to_str() else {
cb0668d30c14atagen2026-10-08 11:34 UTC375
            continue;
cb0668d30c14atagen2026-10-08 11:34 UTC376
        };
cb0668d30c14atagen2026-10-08 11:34 UTC377
        if entry.path().is_dir() {
cb0668d30c14atagen2026-10-08 11:34 UTC378
            ids.insert(name.to_owned());
cb0668d30c14atagen2026-10-08 11:34 UTC379
        } else if let Some(id) = name.strip_suffix(".session") {
cb0668d30c14atagen2026-10-08 11:34 UTC380
            ids.insert(id.to_owned());
cb0668d30c14atagen2026-10-08 11:34 UTC381
        }
cb0668d30c14atagen2026-10-08 11:34 UTC382
    }
cb0668d30c14atagen2026-10-08 11:34 UTC383
    Ok(ids)
cb0668d30c14atagen2026-10-08 11:34 UTC384
}
cb0668d30c14atagen2026-10-08 11:34 UTC385
cb0668d30c14atagen2026-10-08 11:34 UTC386
/// a key per account, so two tabs adding at once can't lose one to the other
cb0668d30c14atagen2026-10-08 11:34 UTC387
#[cfg(feature = "indexeddb")]
cb0668d30c14atagen2026-10-08 11:34 UTC388
mod index {
cb0668d30c14atagen2026-10-08 11:34 UTC389
    use std::collections::BTreeSet;
cb0668d30c14atagen2026-10-08 11:34 UTC390
cb0668d30c14atagen2026-10-08 11:34 UTC391
    use indexed_db_futures::{
cb0668d30c14atagen2026-10-08 11:34 UTC392
        Build,
cb0668d30c14atagen2026-10-08 11:34 UTC393
        database::Database,
cb0668d30c14atagen2026-10-08 11:34 UTC394
        error::{Error, OpenDbError},
cb0668d30c14atagen2026-10-08 11:34 UTC395
        prelude::*,
cb0668d30c14atagen2026-10-08 11:34 UTC396
        transaction::{Transaction, TransactionMode},
cb0668d30c14atagen2026-10-08 11:34 UTC397
    };
cb0668d30c14atagen2026-10-08 11:34 UTC398
cb0668d30c14atagen2026-10-08 11:34 UTC399
    use crate::{AccountError, account::account_error};
cb0668d30c14atagen2026-10-08 11:34 UTC400
cb0668d30c14atagen2026-10-08 11:34 UTC401
    const ACCOUNTS: &str = "accounts";
cb0668d30c14atagen2026-10-08 11:34 UTC402
cb0668d30c14atagen2026-10-08 11:34 UTC403
    fn name(prefix: &str) -> String {
cb0668d30c14atagen2026-10-08 11:34 UTC404
        format!("{prefix}::macltok-accounts")
cb0668d30c14atagen2026-10-08 11:34 UTC405
    }
cb0668d30c14atagen2026-10-08 11:34 UTC406
cb0668d30c14atagen2026-10-08 11:34 UTC407
    async fn open(prefix: &str) -> Result<Database, OpenDbError> {
cb0668d30c14atagen2026-10-08 11:34 UTC408
        Database::open(&name(prefix))
cb0668d30c14atagen2026-10-08 11:34 UTC409
            .with_version(1u32)
cb0668d30c14atagen2026-10-08 11:34 UTC410
            .with_on_upgrade_needed(|_, transaction: &Transaction<'_>| -> Result<(), Error> {
cb0668d30c14atagen2026-10-08 11:34 UTC411
                transaction.db().create_object_store(ACCOUNTS).build()?;
cb0668d30c14atagen2026-10-08 11:34 UTC412
                Ok(())
cb0668d30c14atagen2026-10-08 11:34 UTC413
            })
cb0668d30c14atagen2026-10-08 11:34 UTC414
            .build()?
cb0668d30c14atagen2026-10-08 11:34 UTC415
            .await
cb0668d30c14atagen2026-10-08 11:34 UTC416
    }
cb0668d30c14atagen2026-10-08 11:34 UTC417
cb0668d30c14atagen2026-10-08 11:34 UTC418
    pub(super) async fn list(prefix: &str) -> Result<BTreeSet<String>, AccountError> {
cb0668d30c14atagen2026-10-08 11:34 UTC419
        let failed = |error| account_error("listing the accounts", error);
cb0668d30c14atagen2026-10-08 11:34 UTC420
        let database = open(prefix).await.map_err(failed)?;
cb0668d30c14atagen2026-10-08 11:34 UTC421
        let ids = keys(&database).await;
cb0668d30c14atagen2026-10-08 11:34 UTC422
        database.close();
cb0668d30c14atagen2026-10-08 11:34 UTC423
        ids.map_err(|error| account_error("listing the accounts", error))
cb0668d30c14atagen2026-10-08 11:34 UTC424
    }
cb0668d30c14atagen2026-10-08 11:34 UTC425
cb0668d30c14atagen2026-10-08 11:34 UTC426
    pub(super) async fn add(prefix: &str, id: &str) -> Result<(), AccountError> {
cb0668d30c14atagen2026-10-08 11:34 UTC427
        let failed = |error| account_error("adding the account", error);
cb0668d30c14atagen2026-10-08 11:34 UTC428
        let database = open(prefix).await.map_err(failed)?;
cb0668d30c14atagen2026-10-08 11:34 UTC429
        let added = write(&database, |store| {
cb0668d30c14atagen2026-10-08 11:34 UTC430
            store.put(true).with_key(id).build()?;
cb0668d30c14atagen2026-10-08 11:34 UTC431
            Ok(())
cb0668d30c14atagen2026-10-08 11:34 UTC432
        })
cb0668d30c14atagen2026-10-08 11:34 UTC433
        .await;
cb0668d30c14atagen2026-10-08 11:34 UTC434
        database.close();
cb0668d30c14atagen2026-10-08 11:34 UTC435
        added.map_err(|error| account_error("adding the account", error))
cb0668d30c14atagen2026-10-08 11:34 UTC436
    }
cb0668d30c14atagen2026-10-08 11:34 UTC437
cb0668d30c14atagen2026-10-08 11:34 UTC438
    pub(super) async fn remove(prefix: &str, id: &str) -> Result<(), AccountError> {
cb0668d30c14atagen2026-10-08 11:34 UTC439
        let failed = |error| account_error("removing the account", error);
cb0668d30c14atagen2026-10-08 11:34 UTC440
        let database = open(prefix).await.map_err(failed)?;
cb0668d30c14atagen2026-10-08 11:34 UTC441
        let removed = write(&database, |store| {
cb0668d30c14atagen2026-10-08 11:34 UTC442
            store.delete(id).build()?;
cb0668d30c14atagen2026-10-08 11:34 UTC443
            Ok(())
cb0668d30c14atagen2026-10-08 11:34 UTC444
        })
cb0668d30c14atagen2026-10-08 11:34 UTC445
        .await;
cb0668d30c14atagen2026-10-08 11:34 UTC446
        database.close();
cb0668d30c14atagen2026-10-08 11:34 UTC447
        removed.map_err(|error| account_error("removing the account", error))
cb0668d30c14atagen2026-10-08 11:34 UTC448
    }
cb0668d30c14atagen2026-10-08 11:34 UTC449
cb0668d30c14atagen2026-10-08 11:34 UTC450
    async fn keys(database: &Database) -> Result<BTreeSet<String>, Error> {
cb0668d30c14atagen2026-10-08 11:34 UTC451
        let transaction = database
cb0668d30c14atagen2026-10-08 11:34 UTC452
            .transaction(ACCOUNTS)
cb0668d30c14atagen2026-10-08 11:34 UTC453
            .with_mode(TransactionMode::Readonly)
cb0668d30c14atagen2026-10-08 11:34 UTC454
            .build()?;
cb0668d30c14atagen2026-10-08 11:34 UTC455
        let store = transaction.object_store(ACCOUNTS)?;
cb0668d30c14atagen2026-10-08 11:34 UTC456
        store.get_all_keys::<String>().await?.collect()
cb0668d30c14atagen2026-10-08 11:34 UTC457
    }
cb0668d30c14atagen2026-10-08 11:34 UTC458
cb0668d30c14atagen2026-10-08 11:34 UTC459
    async fn write(
cb0668d30c14atagen2026-10-08 11:34 UTC460
        database: &Database,
cb0668d30c14atagen2026-10-08 11:34 UTC461
        change: impl FnOnce(&indexed_db_futures::object_store::ObjectStore<'_>) -> Result<(), Error>,
cb0668d30c14atagen2026-10-08 11:34 UTC462
    ) -> Result<(), Error> {
cb0668d30c14atagen2026-10-08 11:34 UTC463
        let transaction = database
cb0668d30c14atagen2026-10-08 11:34 UTC464
            .transaction(ACCOUNTS)
cb0668d30c14atagen2026-10-08 11:34 UTC465
            .with_mode(TransactionMode::Readwrite)
cb0668d30c14atagen2026-10-08 11:34 UTC466
            .build()?;
cb0668d30c14atagen2026-10-08 11:34 UTC467
        change(&transaction.object_store(ACCOUNTS)?)?;
cb0668d30c14atagen2026-10-08 11:34 UTC468
        transaction.commit().await
cb0668d30c14atagen2026-10-08 11:34 UTC469
    }
cb0668d30c14atagen2026-10-08 11:34 UTC470
}