atagen

macltok

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

Blame: src/account.rs

62fe32759529atagen2026-09-26 08:50 UTC1
#[cfg(feature = "sqlite")]
62fe32759529atagen2026-09-26 08:50 UTC2
use std::path::PathBuf;
f12377bce9b9atagen2026-09-26 12:32 UTC3
#[cfg(not(target_family = "wasm"))]
f12377bce9b9atagen2026-09-26 12:32 UTC4
use std::sync::atomic::{AtomicBool, Ordering};
713205a95af2atagen2026-09-26 09:24 UTC5
use std::{collections::HashMap, sync::Arc};
96fdf90aacd6atagen2026-09-08 08:24 UTC6
96fdf90aacd6atagen2026-09-08 08:24 UTC7
use matrix_sdk::{
f12377bce9b9atagen2026-09-26 12:32 UTC8
    Client, ClientBuilder, SendOutsideWasm, SessionChange, SessionMeta, SyncOutsideWasm,
f12377bce9b9atagen2026-09-26 12:32 UTC9
    ThreadingSupport,
96fdf90aacd6atagen2026-09-08 08:24 UTC10
    authentication::{
96fdf90aacd6atagen2026-09-08 08:24 UTC11
        AuthSession,
96fdf90aacd6atagen2026-09-08 08:24 UTC12
        matrix::MatrixSession,
96fdf90aacd6atagen2026-09-08 08:24 UTC13
        oauth::{
d8d8a9fe770aatagen2026-10-22 07:44 UTC14
            ClientId, ClientRegistrationData, CsrfToken, OAuthSession, PendingAuthorization,
d8d8a9fe770aatagen2026-10-22 07:44 UTC15
            UserSession,
96fdf90aacd6atagen2026-09-08 08:24 UTC16
            registration::{ApplicationType, ClientMetadata, Localized, OAuthGrantType},
96fdf90aacd6atagen2026-09-08 08:24 UTC17
        },
96fdf90aacd6atagen2026-09-08 08:24 UTC18
    },
62fe32759529atagen2026-09-26 08:50 UTC19
    cross_process_lock::CrossProcessLockConfig,
96fdf90aacd6atagen2026-09-08 08:24 UTC20
    encryption::{BackupDownloadStrategy, EncryptionSettings},
d8d8a9fe770aatagen2026-10-22 07:44 UTC21
    ruma::{MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedUserId, serde::Raw},
96fdf90aacd6atagen2026-09-08 08:24 UTC22
};
96fdf90aacd6atagen2026-09-08 08:24 UTC23
use serde::{Deserialize, Serialize};
96fdf90aacd6atagen2026-09-08 08:24 UTC24
use thiserror::Error;
f12377bce9b9atagen2026-09-26 12:32 UTC25
use tokio::sync::{broadcast, mpsc};
96fdf90aacd6atagen2026-09-08 08:24 UTC26
use url::Url;
96fdf90aacd6atagen2026-09-08 08:24 UTC27
713205a95af2atagen2026-09-26 09:24 UTC28
use crate::{
9a25b6ce66f4atagen2026-10-08 11:15 UTC29
    ClientHandle, Engine, EngineConfig, EngineError, ErrorKind, EventStream, ServerError,
9a25b6ce66f4atagen2026-10-08 11:15 UTC30
    SessionStore,
a250c7fa43fbatagen2026-09-26 13:35 UTC31
    engine::{Persistence, StoreLock},
a250c7fa43fbatagen2026-09-26 13:35 UTC32
    error::SourceError,
713205a95af2atagen2026-09-26 09:24 UTC33
};
96fdf90aacd6atagen2026-09-08 08:24 UTC34
f12377bce9b9atagen2026-09-26 12:32 UTC35
/// a logged in client and the `SessionStore` its record lives in. the engine it builds
f12377bce9b9atagen2026-09-26 12:32 UTC36
/// saves every token rotation there, unless told `without_persistence`
96fdf90aacd6atagen2026-09-08 08:24 UTC37
pub struct Session {
96fdf90aacd6atagen2026-09-08 08:24 UTC38
    client: Client,
f12377bce9b9atagen2026-09-26 12:32 UTC39
    tracking: Tracking,
a250c7fa43fbatagen2026-09-26 13:35 UTC40
    store_lock: StoreLock,
f12377bce9b9atagen2026-09-26 12:32 UTC41
}
f12377bce9b9atagen2026-09-26 12:32 UTC42
f12377bce9b9atagen2026-09-26 12:32 UTC43
/// taken as soon as the client exists, since the sdk can refresh before there's an engine
f12377bce9b9atagen2026-09-26 12:32 UTC44
struct Tracking {
cbb715e11963atagen2026-10-24 23:24 UTC45
    store: Arc<dyn SessionStore>,
f12377bce9b9atagen2026-09-26 12:32 UTC46
    session_changes: broadcast::Receiver<SessionChange>,
f12377bce9b9atagen2026-09-26 12:32 UTC47
    persistence: Option<Persistence>,
f12377bce9b9atagen2026-09-26 12:32 UTC48
    #[cfg(not(target_family = "wasm"))]
f12377bce9b9atagen2026-09-26 12:32 UTC49
    inline: Arc<AtomicBool>,
96fdf90aacd6atagen2026-09-08 08:24 UTC50
}
96fdf90aacd6atagen2026-09-08 08:24 UTC51
31ce2453c82fatagen2026-09-24 13:07 UTC52
#[non_exhaustive]
96fdf90aacd6atagen2026-09-08 08:24 UTC53
pub struct PasswordLogin {
96fdf90aacd6atagen2026-09-08 08:24 UTC54
    pub homeserver: String,
96fdf90aacd6atagen2026-09-08 08:24 UTC55
    pub username: String,
96fdf90aacd6atagen2026-09-08 08:24 UTC56
    pub password: String,
96fdf90aacd6atagen2026-09-08 08:24 UTC57
    pub device_display_name: Option<String>,
fa9114fcf75fatagen2026-09-24 12:51 UTC58
    /// set after a soft logout so the store's keys still belong to us
fa9114fcf75fatagen2026-09-24 12:51 UTC59
    pub device_id: Option<OwnedDeviceId>,
9c05b1dd327aatagen2026-10-08 11:27 UTC60
    /// let plain http through, for loopback hosts only
9c05b1dd327aatagen2026-10-08 11:27 UTC61
    pub allow_insecure_loopback: bool,
96fdf90aacd6atagen2026-09-08 08:24 UTC62
}
96fdf90aacd6atagen2026-09-08 08:24 UTC63
31ce2453c82fatagen2026-09-24 13:07 UTC64
impl PasswordLogin {
31ce2453c82fatagen2026-09-24 13:07 UTC65
    #[must_use]
31ce2453c82fatagen2026-09-24 13:07 UTC66
    pub fn new(
31ce2453c82fatagen2026-09-24 13:07 UTC67
        homeserver: impl Into<String>,
31ce2453c82fatagen2026-09-24 13:07 UTC68
        username: impl Into<String>,
31ce2453c82fatagen2026-09-24 13:07 UTC69
        password: impl Into<String>,
31ce2453c82fatagen2026-09-24 13:07 UTC70
    ) -> Self {
31ce2453c82fatagen2026-09-24 13:07 UTC71
        Self {
31ce2453c82fatagen2026-09-24 13:07 UTC72
            homeserver: homeserver.into(),
31ce2453c82fatagen2026-09-24 13:07 UTC73
            username: username.into(),
31ce2453c82fatagen2026-09-24 13:07 UTC74
            password: password.into(),
31ce2453c82fatagen2026-09-24 13:07 UTC75
            device_display_name: None,
31ce2453c82fatagen2026-09-24 13:07 UTC76
            device_id: None,
9c05b1dd327aatagen2026-10-08 11:27 UTC77
            allow_insecure_loopback: false,
31ce2453c82fatagen2026-09-24 13:07 UTC78
        }
31ce2453c82fatagen2026-09-24 13:07 UTC79
    }
31ce2453c82fatagen2026-09-24 13:07 UTC80
31ce2453c82fatagen2026-09-24 13:07 UTC81
    #[must_use]
31ce2453c82fatagen2026-09-24 13:07 UTC82
    pub fn with_device_display_name(mut self, name: impl Into<String>) -> Self {
31ce2453c82fatagen2026-09-24 13:07 UTC83
        self.device_display_name = Some(name.into());
31ce2453c82fatagen2026-09-24 13:07 UTC84
        self
31ce2453c82fatagen2026-09-24 13:07 UTC85
    }
31ce2453c82fatagen2026-09-24 13:07 UTC86
31ce2453c82fatagen2026-09-24 13:07 UTC87
    #[must_use]
31ce2453c82fatagen2026-09-24 13:07 UTC88
    pub fn with_device_id(mut self, device_id: OwnedDeviceId) -> Self {
31ce2453c82fatagen2026-09-24 13:07 UTC89
        self.device_id = Some(device_id);
31ce2453c82fatagen2026-09-24 13:07 UTC90
        self
31ce2453c82fatagen2026-09-24 13:07 UTC91
    }
9c05b1dd327aatagen2026-10-08 11:27 UTC92
9c05b1dd327aatagen2026-10-08 11:27 UTC93
    #[must_use]
9c05b1dd327aatagen2026-10-08 11:27 UTC94
    pub fn allow_insecure_loopback(mut self, allow: bool) -> Self {
9c05b1dd327aatagen2026-10-08 11:27 UTC95
        self.allow_insecure_loopback = allow;
9c05b1dd327aatagen2026-10-08 11:27 UTC96
        self
9c05b1dd327aatagen2026-10-08 11:27 UTC97
    }
31ce2453c82fatagen2026-09-24 13:07 UTC98
}
31ce2453c82fatagen2026-09-24 13:07 UTC99
31ce2453c82fatagen2026-09-24 13:07 UTC100
#[non_exhaustive]
96fdf90aacd6atagen2026-09-08 08:24 UTC101
pub struct OAuthLogin {
96fdf90aacd6atagen2026-09-08 08:24 UTC102
    pub homeserver: String,
96fdf90aacd6atagen2026-09-08 08:24 UTC103
    pub redirect_uri: String,
96fdf90aacd6atagen2026-09-08 08:24 UTC104
    pub client_uri: String,
96fdf90aacd6atagen2026-09-08 08:24 UTC105
    pub client_name: Option<String>,
96fdf90aacd6atagen2026-09-08 08:24 UTC106
    pub client_id: Option<String>,
96fdf90aacd6atagen2026-09-08 08:24 UTC107
    pub device_id: Option<OwnedDeviceId>,
96fdf90aacd6atagen2026-09-08 08:24 UTC108
    pub application_type: OAuthApplicationType,
9c05b1dd327aatagen2026-10-08 11:27 UTC109
    /// let plain http through, for loopback hosts only
9c05b1dd327aatagen2026-10-08 11:27 UTC110
    pub allow_insecure_loopback: bool,
96fdf90aacd6atagen2026-09-08 08:24 UTC111
}
96fdf90aacd6atagen2026-09-08 08:24 UTC112
31ce2453c82fatagen2026-09-24 13:07 UTC113
impl OAuthLogin {
31ce2453c82fatagen2026-09-24 13:07 UTC114
    #[must_use]
31ce2453c82fatagen2026-09-24 13:07 UTC115
    pub fn new(
31ce2453c82fatagen2026-09-24 13:07 UTC116
        homeserver: impl Into<String>,
31ce2453c82fatagen2026-09-24 13:07 UTC117
        redirect_uri: impl Into<String>,
31ce2453c82fatagen2026-09-24 13:07 UTC118
        client_uri: impl Into<String>,
31ce2453c82fatagen2026-09-24 13:07 UTC119
        application_type: OAuthApplicationType,
31ce2453c82fatagen2026-09-24 13:07 UTC120
    ) -> Self {
31ce2453c82fatagen2026-09-24 13:07 UTC121
        Self {
31ce2453c82fatagen2026-09-24 13:07 UTC122
            homeserver: homeserver.into(),
31ce2453c82fatagen2026-09-24 13:07 UTC123
            redirect_uri: redirect_uri.into(),
31ce2453c82fatagen2026-09-24 13:07 UTC124
            client_uri: client_uri.into(),
31ce2453c82fatagen2026-09-24 13:07 UTC125
            client_name: None,
31ce2453c82fatagen2026-09-24 13:07 UTC126
            client_id: None,
31ce2453c82fatagen2026-09-24 13:07 UTC127
            device_id: None,
31ce2453c82fatagen2026-09-24 13:07 UTC128
            application_type,
9c05b1dd327aatagen2026-10-08 11:27 UTC129
            allow_insecure_loopback: false,
31ce2453c82fatagen2026-09-24 13:07 UTC130
        }
31ce2453c82fatagen2026-09-24 13:07 UTC131
    }
31ce2453c82fatagen2026-09-24 13:07 UTC132
31ce2453c82fatagen2026-09-24 13:07 UTC133
    #[must_use]
31ce2453c82fatagen2026-09-24 13:07 UTC134
    pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
31ce2453c82fatagen2026-09-24 13:07 UTC135
        self.client_name = Some(name.into());
31ce2453c82fatagen2026-09-24 13:07 UTC136
        self
31ce2453c82fatagen2026-09-24 13:07 UTC137
    }
31ce2453c82fatagen2026-09-24 13:07 UTC138
31ce2453c82fatagen2026-09-24 13:07 UTC139
    /// a statically registered client id, skipping dynamic registration
31ce2453c82fatagen2026-09-24 13:07 UTC140
    #[must_use]
31ce2453c82fatagen2026-09-24 13:07 UTC141
    pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
31ce2453c82fatagen2026-09-24 13:07 UTC142
        self.client_id = Some(client_id.into());
31ce2453c82fatagen2026-09-24 13:07 UTC143
        self
31ce2453c82fatagen2026-09-24 13:07 UTC144
    }
31ce2453c82fatagen2026-09-24 13:07 UTC145
31ce2453c82fatagen2026-09-24 13:07 UTC146
    #[must_use]
31ce2453c82fatagen2026-09-24 13:07 UTC147
    pub fn with_device_id(mut self, device_id: OwnedDeviceId) -> Self {
31ce2453c82fatagen2026-09-24 13:07 UTC148
        self.device_id = Some(device_id);
31ce2453c82fatagen2026-09-24 13:07 UTC149
        self
31ce2453c82fatagen2026-09-24 13:07 UTC150
    }
9c05b1dd327aatagen2026-10-08 11:27 UTC151
9c05b1dd327aatagen2026-10-08 11:27 UTC152
    #[must_use]
9c05b1dd327aatagen2026-10-08 11:27 UTC153
    pub fn allow_insecure_loopback(mut self, allow: bool) -> Self {
9c05b1dd327aatagen2026-10-08 11:27 UTC154
        self.allow_insecure_loopback = allow;
9c05b1dd327aatagen2026-10-08 11:27 UTC155
        self
9c05b1dd327aatagen2026-10-08 11:27 UTC156
    }
31ce2453c82fatagen2026-09-24 13:07 UTC157
}
31ce2453c82fatagen2026-09-24 13:07 UTC158
96fdf90aacd6atagen2026-09-08 08:24 UTC159
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
31ce2453c82fatagen2026-09-24 13:07 UTC160
#[non_exhaustive]
96fdf90aacd6atagen2026-09-08 08:24 UTC161
pub enum OAuthApplicationType {
96fdf90aacd6atagen2026-09-08 08:24 UTC162
    Native,
96fdf90aacd6atagen2026-09-08 08:24 UTC163
    Web,
96fdf90aacd6atagen2026-09-08 08:24 UTC164
}
96fdf90aacd6atagen2026-09-08 08:24 UTC165
96fdf90aacd6atagen2026-09-08 08:24 UTC166
pub struct PendingOAuthLogin {
96fdf90aacd6atagen2026-09-08 08:24 UTC167
    client: Client,
713205a95af2atagen2026-09-26 09:24 UTC168
    sessions: Arc<dyn SessionStore>,
a250c7fa43fbatagen2026-09-26 13:35 UTC169
    tracking: Option<(Tracking, StoreLock)>,
96fdf90aacd6atagen2026-09-08 08:24 UTC170
    authorization_url: String,
96fdf90aacd6atagen2026-09-08 08:24 UTC171
    state: Option<CsrfToken>,
d8d8a9fe770aatagen2026-10-22 07:44 UTC172
    previous: Option<SessionRecord>,
96fdf90aacd6atagen2026-09-08 08:24 UTC173
}
96fdf90aacd6atagen2026-09-08 08:24 UTC174
96fdf90aacd6atagen2026-09-08 08:24 UTC175
impl PendingOAuthLogin {
96fdf90aacd6atagen2026-09-08 08:24 UTC176
    #[must_use]
96fdf90aacd6atagen2026-09-08 08:24 UTC177
    pub fn authorization_url(&self) -> &str {
96fdf90aacd6atagen2026-09-08 08:24 UTC178
        &self.authorization_url
96fdf90aacd6atagen2026-09-08 08:24 UTC179
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC180
96fdf90aacd6atagen2026-09-08 08:24 UTC181
    #[must_use]
96fdf90aacd6atagen2026-09-08 08:24 UTC182
    pub fn registered_client_id(&self) -> String {
96fdf90aacd6atagen2026-09-08 08:24 UTC183
        self.client
96fdf90aacd6atagen2026-09-08 08:24 UTC184
            .oauth()
96fdf90aacd6atagen2026-09-08 08:24 UTC185
            .client_id()
96fdf90aacd6atagen2026-09-08 08:24 UTC186
            .expect("OAuth client was registered before authorization")
96fdf90aacd6atagen2026-09-08 08:24 UTC187
            .as_str()
96fdf90aacd6atagen2026-09-08 08:24 UTC188
            .to_owned()
96fdf90aacd6atagen2026-09-08 08:24 UTC189
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC190
96fdf90aacd6atagen2026-09-08 08:24 UTC191
    #[must_use]
96fdf90aacd6atagen2026-09-08 08:24 UTC192
    pub const fn is_pending(&self) -> bool {
96fdf90aacd6atagen2026-09-08 08:24 UTC193
        self.state.is_some()
96fdf90aacd6atagen2026-09-08 08:24 UTC194
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC195
96fdf90aacd6atagen2026-09-08 08:24 UTC196
    pub async fn finish(&mut self, callback_url: &str) -> Result<Session, AccountError> {
96fdf90aacd6atagen2026-09-08 08:24 UTC197
        let callback_url = Url::parse(callback_url)
96fdf90aacd6atagen2026-09-08 08:24 UTC198
            .map_err(|error| account_error("parsing the OAuth callback URL", error))?;
96fdf90aacd6atagen2026-09-08 08:24 UTC199
        let state = self
96fdf90aacd6atagen2026-09-08 08:24 UTC200
            .state
96fdf90aacd6atagen2026-09-08 08:24 UTC201
            .take()
96fdf90aacd6atagen2026-09-08 08:24 UTC202
            .ok_or(AccountError::OAuthCallbackAlreadySubmitted)?;
96fdf90aacd6atagen2026-09-08 08:24 UTC203
        let oauth = self.client.oauth();
96fdf90aacd6atagen2026-09-08 08:24 UTC204
        if let Err(error) = oauth.finish_login(callback_url.into()).await {
96fdf90aacd6atagen2026-09-08 08:24 UTC205
            oauth.abort_login(&state).await;
d8d8a9fe770aatagen2026-10-22 07:44 UTC206
            if let Some(previous) = self.previous.take() {
d8d8a9fe770aatagen2026-10-22 07:44 UTC207
                put_back(self.sessions.as_ref(), previous).await;
d8d8a9fe770aatagen2026-10-22 07:44 UTC208
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC209
            return Err(account_error("OAuth login", error));
96fdf90aacd6atagen2026-09-08 08:24 UTC210
        }
a250c7fa43fbatagen2026-09-26 13:35 UTC211
        let (tracking, store_lock) = self
f12377bce9b9atagen2026-09-26 12:32 UTC212
            .tracking
f12377bce9b9atagen2026-09-26 12:32 UTC213
            .take()
f12377bce9b9atagen2026-09-26 12:32 UTC214
            .ok_or(AccountError::OAuthCallbackAlreadySubmitted)?;
a250c7fa43fbatagen2026-09-26 13:35 UTC215
        Session::saved(
a250c7fa43fbatagen2026-09-26 13:35 UTC216
            self.client.clone(),
a250c7fa43fbatagen2026-09-26 13:35 UTC217
            self.sessions.clone(),
a250c7fa43fbatagen2026-09-26 13:35 UTC218
            tracking,
a250c7fa43fbatagen2026-09-26 13:35 UTC219
            store_lock,
a250c7fa43fbatagen2026-09-26 13:35 UTC220
        )
a250c7fa43fbatagen2026-09-26 13:35 UTC221
        .await
96fdf90aacd6atagen2026-09-08 08:24 UTC222
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC223
d8d8a9fe770aatagen2026-10-22 07:44 UTC224
    /// puts back what the store held before, or deletes the store if it held nothing
d8d8a9fe770aatagen2026-10-22 07:44 UTC225
    pub async fn abort(self) {
d8d8a9fe770aatagen2026-10-22 07:44 UTC226
        let Self {
d8d8a9fe770aatagen2026-10-22 07:44 UTC227
            client,
d8d8a9fe770aatagen2026-10-22 07:44 UTC228
            sessions,
d8d8a9fe770aatagen2026-10-22 07:44 UTC229
            tracking,
d8d8a9fe770aatagen2026-10-22 07:44 UTC230
            state,
d8d8a9fe770aatagen2026-10-22 07:44 UTC231
            previous,
d8d8a9fe770aatagen2026-10-22 07:44 UTC232
            ..
d8d8a9fe770aatagen2026-10-22 07:44 UTC233
        } = self;
d8d8a9fe770aatagen2026-10-22 07:44 UTC234
        let Some(state) = state else {
d8d8a9fe770aatagen2026-10-22 07:44 UTC235
            return;
d8d8a9fe770aatagen2026-10-22 07:44 UTC236
        };
d8d8a9fe770aatagen2026-10-22 07:44 UTC237
        client.oauth().abort_login(&state).await;
d8d8a9fe770aatagen2026-10-22 07:44 UTC238
        drop(client);
d8d8a9fe770aatagen2026-10-22 07:44 UTC239
        match previous {
d8d8a9fe770aatagen2026-10-22 07:44 UTC240
            Some(previous) => put_back(sessions.as_ref(), previous).await,
d8d8a9fe770aatagen2026-10-22 07:44 UTC241
            None => {
d8d8a9fe770aatagen2026-10-22 07:44 UTC242
                if let Err(error) = sessions.discard().await {
d8d8a9fe770aatagen2026-10-22 07:44 UTC243
                    tracing::warn!(%error, "deleting an abandoned OAuth login's store failed");
d8d8a9fe770aatagen2026-10-22 07:44 UTC244
                }
d8d8a9fe770aatagen2026-10-22 07:44 UTC245
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC246
        }
d8d8a9fe770aatagen2026-10-22 07:44 UTC247
        drop(tracking);
d8d8a9fe770aatagen2026-10-22 07:44 UTC248
    }
d8d8a9fe770aatagen2026-10-22 07:44 UTC249
}
d8d8a9fe770aatagen2026-10-22 07:44 UTC250
d8d8a9fe770aatagen2026-10-22 07:44 UTC251
async fn put_back(sessions: &dyn SessionStore, previous: SessionRecord) {
d8d8a9fe770aatagen2026-10-22 07:44 UTC252
    if let Err(error) = sessions.save(&previous).await {
d8d8a9fe770aatagen2026-10-22 07:44 UTC253
        tracing::warn!(%error, "putting back the record an OAuth login replaced failed");
96fdf90aacd6atagen2026-09-08 08:24 UTC254
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC255
}
96fdf90aacd6atagen2026-09-08 08:24 UTC256
96fdf90aacd6atagen2026-09-08 08:24 UTC257
pub struct StoreConfig {
62fe32759529atagen2026-09-26 08:50 UTC258
    location: StoreLocation,
96fdf90aacd6atagen2026-09-08 08:24 UTC259
    passphrase: String,
62fe32759529atagen2026-09-26 08:50 UTC260
}
62fe32759529atagen2026-09-26 08:50 UTC261
62fe32759529atagen2026-09-26 08:50 UTC262
enum StoreLocation {
62fe32759529atagen2026-09-26 08:50 UTC263
    #[cfg(feature = "sqlite")]
62fe32759529atagen2026-09-26 08:50 UTC264
    Sqlite(PathBuf),
62fe32759529atagen2026-09-26 08:50 UTC265
    #[cfg(feature = "indexeddb")]
62fe32759529atagen2026-09-26 08:50 UTC266
    IndexedDb(String),
96fdf90aacd6atagen2026-09-08 08:24 UTC267
}
96fdf90aacd6atagen2026-09-08 08:24 UTC268
96fdf90aacd6atagen2026-09-08 08:24 UTC269
impl StoreConfig {
62fe32759529atagen2026-09-26 08:50 UTC270
    #[cfg(feature = "sqlite")]
62fe32759529atagen2026-09-26 08:50 UTC271
    #[must_use]
62fe32759529atagen2026-09-26 08:50 UTC272
    pub fn sqlite(path: impl Into<PathBuf>, passphrase: impl Into<String>) -> Self {
62fe32759529atagen2026-09-26 08:50 UTC273
        Self {
62fe32759529atagen2026-09-26 08:50 UTC274
            location: StoreLocation::Sqlite(path.into()),
62fe32759529atagen2026-09-26 08:50 UTC275
            passphrase: passphrase.into(),
62fe32759529atagen2026-09-26 08:50 UTC276
        }
62fe32759529atagen2026-09-26 08:50 UTC277
    }
62fe32759529atagen2026-09-26 08:50 UTC278
62fe32759529atagen2026-09-26 08:50 UTC279
    #[cfg(feature = "indexeddb")]
96fdf90aacd6atagen2026-09-08 08:24 UTC280
    #[must_use]
62fe32759529atagen2026-09-26 08:50 UTC281
    pub fn indexeddb(name: impl Into<String>, passphrase: impl Into<String>) -> Self {
96fdf90aacd6atagen2026-09-08 08:24 UTC282
        Self {
62fe32759529atagen2026-09-26 08:50 UTC283
            location: StoreLocation::IndexedDb(name.into()),
96fdf90aacd6atagen2026-09-08 08:24 UTC284
            passphrase: passphrase.into(),
96fdf90aacd6atagen2026-09-08 08:24 UTC285
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC286
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC287
713205a95af2atagen2026-09-26 09:24 UTC288
    /// delete the store's databases, once nothing has them open
713205a95af2atagen2026-09-26 09:24 UTC289
    pub async fn delete(&self) -> Result<(), AccountError> {
713205a95af2atagen2026-09-26 09:24 UTC290
        match &self.location {
713205a95af2atagen2026-09-26 09:24 UTC291
            #[cfg(feature = "sqlite")]
713205a95af2atagen2026-09-26 09:24 UTC292
            StoreLocation::Sqlite(path) => crate::session_store::delete_sqlite_store(path)
713205a95af2atagen2026-09-26 09:24 UTC293
                .map_err(|error| account_error("deleting the Matrix store", error)),
713205a95af2atagen2026-09-26 09:24 UTC294
            #[cfg(feature = "indexeddb")]
713205a95af2atagen2026-09-26 09:24 UTC295
            StoreLocation::IndexedDb(name) => crate::session_store::delete_indexeddb_store(name)
713205a95af2atagen2026-09-26 09:24 UTC296
                .await
6db49c0ecf71atagen2026-09-26 12:41 UTC297
                .map_err(|error| error.into_account_error("deleting the Matrix store")),
713205a95af2atagen2026-09-26 09:24 UTC298
        }
713205a95af2atagen2026-09-26 09:24 UTC299
    }
713205a95af2atagen2026-09-26 09:24 UTC300
a250c7fa43fbatagen2026-09-26 13:35 UTC301
    /// taken before the client opens the store, and held until the engine and every
a250c7fa43fbatagen2026-09-26 13:35 UTC302
    /// `ClientHandle` are gone
a250c7fa43fbatagen2026-09-26 13:35 UTC303
    pub(crate) async fn lock(&self) -> Result<StoreLock, AccountError> {
a250c7fa43fbatagen2026-09-26 13:35 UTC304
        match &self.location {
a250c7fa43fbatagen2026-09-26 13:35 UTC305
            #[cfg(feature = "sqlite")]
a250c7fa43fbatagen2026-09-26 13:35 UTC306
            StoreLocation::Sqlite(path) => StoreLock::file(path),
a250c7fa43fbatagen2026-09-26 13:35 UTC307
            #[cfg(feature = "indexeddb")]
a250c7fa43fbatagen2026-09-26 13:35 UTC308
            StoreLocation::IndexedDb(name) => StoreLock::web(name).await,
a250c7fa43fbatagen2026-09-26 13:35 UTC309
        }
a250c7fa43fbatagen2026-09-26 13:35 UTC310
    }
a250c7fa43fbatagen2026-09-26 13:35 UTC311
713205a95af2atagen2026-09-26 09:24 UTC312
    pub(crate) fn passphrase(&self) -> &str {
713205a95af2atagen2026-09-26 09:24 UTC313
        &self.passphrase
713205a95af2atagen2026-09-26 09:24 UTC314
    }
713205a95af2atagen2026-09-26 09:24 UTC315
cbb715e11963atagen2026-10-24 23:24 UTC316
    #[cfg(feature = "sqlite")]
cbb715e11963atagen2026-10-24 23:24 UTC317
    pub(crate) fn mentions_location(&self) -> PathBuf {
cbb715e11963atagen2026-10-24 23:24 UTC318
        let StoreLocation::Sqlite(path) = &self.location;
cbb715e11963atagen2026-10-24 23:24 UTC319
        path.join("macltok-mentions.sqlite3")
cbb715e11963atagen2026-10-24 23:24 UTC320
    }
cbb715e11963atagen2026-10-24 23:24 UTC321
cbb715e11963atagen2026-10-24 23:24 UTC322
    #[cfg(feature = "indexeddb")]
cbb715e11963atagen2026-10-24 23:24 UTC323
    pub(crate) fn mentions_location(&self) -> String {
cbb715e11963atagen2026-10-24 23:24 UTC324
        format!("{}::macltok-mentions", self.indexeddb_name())
cbb715e11963atagen2026-10-24 23:24 UTC325
    }
cbb715e11963atagen2026-10-24 23:24 UTC326
c9effeac326aatagen2026-10-25 10:32 UTC327
    #[cfg(feature = "sqlite")]
c9effeac326aatagen2026-10-25 10:32 UTC328
    pub(crate) fn search_location(&self) -> PathBuf {
c9effeac326aatagen2026-10-25 10:32 UTC329
        let StoreLocation::Sqlite(path) = &self.location;
c9effeac326aatagen2026-10-25 10:32 UTC330
        path.join("macltok-search.sqlite3")
c9effeac326aatagen2026-10-25 10:32 UTC331
    }
c9effeac326aatagen2026-10-25 10:32 UTC332
c9effeac326aatagen2026-10-25 10:32 UTC333
    #[cfg(feature = "indexeddb")]
c9effeac326aatagen2026-10-25 10:32 UTC334
    pub(crate) fn search_location(&self) -> String {
c9effeac326aatagen2026-10-25 10:32 UTC335
        format!("{}::macltok-search", self.indexeddb_name())
c9effeac326aatagen2026-10-25 10:32 UTC336
    }
c9effeac326aatagen2026-10-25 10:32 UTC337
713205a95af2atagen2026-09-26 09:24 UTC338
    #[cfg(feature = "indexeddb")]
713205a95af2atagen2026-09-26 09:24 UTC339
    pub(crate) fn indexeddb_name(&self) -> &str {
713205a95af2atagen2026-09-26 09:24 UTC340
        let StoreLocation::IndexedDb(name) = &self.location;
713205a95af2atagen2026-09-26 09:24 UTC341
        name
713205a95af2atagen2026-09-26 09:24 UTC342
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC343
}
96fdf90aacd6atagen2026-09-08 08:24 UTC344
f12377bce9b9atagen2026-09-26 12:32 UTC345
/// Serializable authentication secrets. A `SessionStore` keeps it, and the engine
f12377bce9b9atagen2026-09-26 12:32 UTC346
/// replaces it there on every token rotation.
96fdf90aacd6atagen2026-09-08 08:24 UTC347
#[derive(Clone, Deserialize, Serialize)]
96fdf90aacd6atagen2026-09-08 08:24 UTC348
pub struct SessionRecord {
96fdf90aacd6atagen2026-09-08 08:24 UTC349
    homeserver: String,
96fdf90aacd6atagen2026-09-08 08:24 UTC350
    authentication: AuthenticationRecord,
96fdf90aacd6atagen2026-09-08 08:24 UTC351
}
96fdf90aacd6atagen2026-09-08 08:24 UTC352
96fdf90aacd6atagen2026-09-08 08:24 UTC353
#[derive(Clone, Deserialize, Serialize)]
96fdf90aacd6atagen2026-09-08 08:24 UTC354
#[serde(tag = "type", content = "session", rename_all = "snake_case")]
96fdf90aacd6atagen2026-09-08 08:24 UTC355
enum AuthenticationRecord {
96fdf90aacd6atagen2026-09-08 08:24 UTC356
    Matrix(MatrixSession),
96fdf90aacd6atagen2026-09-08 08:24 UTC357
    OAuth {
96fdf90aacd6atagen2026-09-08 08:24 UTC358
        client_id: ClientId,
96fdf90aacd6atagen2026-09-08 08:24 UTC359
        user: UserSession,
96fdf90aacd6atagen2026-09-08 08:24 UTC360
    },
6f1d6591c10datagen2026-09-26 09:52 UTC361
    SoftLoggedOut {
6f1d6591c10datagen2026-09-26 09:52 UTC362
        meta: SessionMeta,
6f1d6591c10datagen2026-09-26 09:52 UTC363
        client_id: Option<ClientId>,
6f1d6591c10datagen2026-09-26 09:52 UTC364
    },
d8d8a9fe770aatagen2026-10-22 07:44 UTC365
    /// an oauth login waiting on its redirect. `authorization` holds the pkce verifier
d8d8a9fe770aatagen2026-10-22 07:44 UTC366
    PendingOAuth {
d8d8a9fe770aatagen2026-10-22 07:44 UTC367
        client_id: ClientId,
d8d8a9fe770aatagen2026-10-22 07:44 UTC368
        state: CsrfToken,
d8d8a9fe770aatagen2026-10-22 07:44 UTC369
        authorization: Box<PendingAuthorization>,
d8d8a9fe770aatagen2026-10-22 07:44 UTC370
        started_ms: u64,
d8d8a9fe770aatagen2026-10-22 07:44 UTC371
        /// what the store held before, put back if the login never finishes
d8d8a9fe770aatagen2026-10-22 07:44 UTC372
        previous: Option<Box<SessionRecord>>,
d8d8a9fe770aatagen2026-10-22 07:44 UTC373
    },
d8d8a9fe770aatagen2026-10-22 07:44 UTC374
}
d8d8a9fe770aatagen2026-10-22 07:44 UTC375
d8d8a9fe770aatagen2026-10-22 07:44 UTC376
/// authorization codes live for minutes, so a login this old was abandoned
d8d8a9fe770aatagen2026-10-22 07:44 UTC377
const OAUTH_PENDING_FOR_MS: u64 = 60 * 60 * 1000;
d8d8a9fe770aatagen2026-10-22 07:44 UTC378
d8d8a9fe770aatagen2026-10-22 07:44 UTC379
/// the authorization server answers in the query, or the fragment when asked to
d8d8a9fe770aatagen2026-10-22 07:44 UTC380
pub(crate) fn callback_state(callback: &Url) -> Option<String> {
d8d8a9fe770aatagen2026-10-22 07:44 UTC381
    let query = callback.query_pairs();
d8d8a9fe770aatagen2026-10-22 07:44 UTC382
    let fragment = url::form_urlencoded::parse(callback.fragment().unwrap_or_default().as_bytes());
d8d8a9fe770aatagen2026-10-22 07:44 UTC383
    query
d8d8a9fe770aatagen2026-10-22 07:44 UTC384
        .chain(fragment)
d8d8a9fe770aatagen2026-10-22 07:44 UTC385
        .find(|(key, _)| key == "state")
d8d8a9fe770aatagen2026-10-22 07:44 UTC386
        .map(|(_, state)| state.into_owned())
d8d8a9fe770aatagen2026-10-22 07:44 UTC387
}
d8d8a9fe770aatagen2026-10-22 07:44 UTC388
d8d8a9fe770aatagen2026-10-22 07:44 UTC389
fn now_ms() -> u64 {
d8d8a9fe770aatagen2026-10-22 07:44 UTC390
    MilliSecondsSinceUnixEpoch::now().get().into()
96fdf90aacd6atagen2026-09-08 08:24 UTC391
}
96fdf90aacd6atagen2026-09-08 08:24 UTC392
96fdf90aacd6atagen2026-09-08 08:24 UTC393
impl SessionRecord {
96fdf90aacd6atagen2026-09-08 08:24 UTC394
    #[must_use]
96fdf90aacd6atagen2026-09-08 08:24 UTC395
    pub fn homeserver(&self) -> &str {
96fdf90aacd6atagen2026-09-08 08:24 UTC396
        &self.homeserver
96fdf90aacd6atagen2026-09-08 08:24 UTC397
    }
6f1d6591c10datagen2026-09-26 09:52 UTC398
d8d8a9fe770aatagen2026-10-22 07:44 UTC399
    /// a pending oauth login is nobody yet, unless it resumes someone
d8d8a9fe770aatagen2026-10-22 07:44 UTC400
    pub(crate) fn user_id(&self) -> Option<&OwnedUserId> {
d8d8a9fe770aatagen2026-10-22 07:44 UTC401
        match &self.authentication {
d8d8a9fe770aatagen2026-10-22 07:44 UTC402
            AuthenticationRecord::Matrix(session) => Some(&session.meta.user_id),
d8d8a9fe770aatagen2026-10-22 07:44 UTC403
            AuthenticationRecord::OAuth { user, .. } => Some(&user.meta.user_id),
d8d8a9fe770aatagen2026-10-22 07:44 UTC404
            AuthenticationRecord::SoftLoggedOut { meta, .. } => Some(&meta.user_id),
d8d8a9fe770aatagen2026-10-22 07:44 UTC405
            AuthenticationRecord::PendingOAuth { previous, .. } => previous.as_ref()?.user_id(),
d8d8a9fe770aatagen2026-10-22 07:44 UTC406
        }
d8d8a9fe770aatagen2026-10-22 07:44 UTC407
    }
d8d8a9fe770aatagen2026-10-22 07:44 UTC408
d8d8a9fe770aatagen2026-10-22 07:44 UTC409
    /// what a login should resume. a pending oauth login stands in for the record before it
d8d8a9fe770aatagen2026-10-22 07:44 UTC410
    pub(crate) fn resumable(self) -> Option<Self> {
d8d8a9fe770aatagen2026-10-22 07:44 UTC411
        match self.authentication {
d8d8a9fe770aatagen2026-10-22 07:44 UTC412
            AuthenticationRecord::PendingOAuth { previous, .. } => {
d8d8a9fe770aatagen2026-10-22 07:44 UTC413
                previous.and_then(|previous| previous.resumable())
d8d8a9fe770aatagen2026-10-22 07:44 UTC414
            }
d8d8a9fe770aatagen2026-10-22 07:44 UTC415
            _ => Some(self),
d8d8a9fe770aatagen2026-10-22 07:44 UTC416
        }
d8d8a9fe770aatagen2026-10-22 07:44 UTC417
    }
d8d8a9fe770aatagen2026-10-22 07:44 UTC418
d8d8a9fe770aatagen2026-10-22 07:44 UTC419
    pub(crate) fn pending_oauth_state(&self) -> Option<&str> {
cb0668d30c14atagen2026-10-08 11:34 UTC420
        match &self.authentication {
d8d8a9fe770aatagen2026-10-22 07:44 UTC421
            AuthenticationRecord::PendingOAuth { state, .. } => Some(state.secret()),
d8d8a9fe770aatagen2026-10-22 07:44 UTC422
            _ => None,
cb0668d30c14atagen2026-10-08 11:34 UTC423
        }
cb0668d30c14atagen2026-10-08 11:34 UTC424
    }
cb0668d30c14atagen2026-10-08 11:34 UTC425
d8d8a9fe770aatagen2026-10-22 07:44 UTC426
    pub(crate) fn oauth_expired(&self) -> bool {
d8d8a9fe770aatagen2026-10-22 07:44 UTC427
        let AuthenticationRecord::PendingOAuth { started_ms, .. } = &self.authentication else {
d8d8a9fe770aatagen2026-10-22 07:44 UTC428
            return false;
d8d8a9fe770aatagen2026-10-22 07:44 UTC429
        };
d8d8a9fe770aatagen2026-10-22 07:44 UTC430
        now_ms().saturating_sub(*started_ms) > OAUTH_PENDING_FOR_MS
d8d8a9fe770aatagen2026-10-22 07:44 UTC431
    }
d8d8a9fe770aatagen2026-10-22 07:44 UTC432
cb0668d30c14atagen2026-10-08 11:34 UTC433
    pub(crate) fn soft_logout(&self) -> Option<SoftLogout> {
d8d8a9fe770aatagen2026-10-22 07:44 UTC434
        let (meta, client_id) = match &self.authentication {
d8d8a9fe770aatagen2026-10-22 07:44 UTC435
            AuthenticationRecord::SoftLoggedOut { meta, client_id } => (meta, client_id),
d8d8a9fe770aatagen2026-10-22 07:44 UTC436
            AuthenticationRecord::PendingOAuth { previous, .. } => {
d8d8a9fe770aatagen2026-10-22 07:44 UTC437
                return previous.as_ref()?.soft_logout();
d8d8a9fe770aatagen2026-10-22 07:44 UTC438
            }
d8d8a9fe770aatagen2026-10-22 07:44 UTC439
            _ => return None,
cb0668d30c14atagen2026-10-08 11:34 UTC440
        };
cb0668d30c14atagen2026-10-08 11:34 UTC441
        Some(SoftLogout {
cb0668d30c14atagen2026-10-08 11:34 UTC442
            homeserver: self.homeserver.clone(),
cb0668d30c14atagen2026-10-08 11:34 UTC443
            user_id: meta.user_id.clone(),
cb0668d30c14atagen2026-10-08 11:34 UTC444
            device_id: meta.device_id.clone(),
cb0668d30c14atagen2026-10-08 11:34 UTC445
            client_id: client_id
cb0668d30c14atagen2026-10-08 11:34 UTC446
                .as_ref()
cb0668d30c14atagen2026-10-08 11:34 UTC447
                .map(|client_id| client_id.as_str().to_owned()),
cb0668d30c14atagen2026-10-08 11:34 UTC448
        })
cb0668d30c14atagen2026-10-08 11:34 UTC449
    }
cb0668d30c14atagen2026-10-08 11:34 UTC450
6f1d6591c10datagen2026-09-26 09:52 UTC451
    pub(crate) fn soft_logged_out(self) -> Self {
6f1d6591c10datagen2026-09-26 09:52 UTC452
        let authentication = match self.authentication {
6f1d6591c10datagen2026-09-26 09:52 UTC453
            AuthenticationRecord::Matrix(session) => AuthenticationRecord::SoftLoggedOut {
6f1d6591c10datagen2026-09-26 09:52 UTC454
                meta: session.meta,
6f1d6591c10datagen2026-09-26 09:52 UTC455
                client_id: None,
6f1d6591c10datagen2026-09-26 09:52 UTC456
            },
6f1d6591c10datagen2026-09-26 09:52 UTC457
            AuthenticationRecord::OAuth { client_id, user } => {
6f1d6591c10datagen2026-09-26 09:52 UTC458
                AuthenticationRecord::SoftLoggedOut {
6f1d6591c10datagen2026-09-26 09:52 UTC459
                    meta: user.meta,
6f1d6591c10datagen2026-09-26 09:52 UTC460
                    client_id: Some(client_id),
6f1d6591c10datagen2026-09-26 09:52 UTC461
                }
6f1d6591c10datagen2026-09-26 09:52 UTC462
            }
6f1d6591c10datagen2026-09-26 09:52 UTC463
            soft @ AuthenticationRecord::SoftLoggedOut { .. } => soft,
d8d8a9fe770aatagen2026-10-22 07:44 UTC464
            pending @ AuthenticationRecord::PendingOAuth { .. } => pending,
6f1d6591c10datagen2026-09-26 09:52 UTC465
        };
6f1d6591c10datagen2026-09-26 09:52 UTC466
        Self {
6f1d6591c10datagen2026-09-26 09:52 UTC467
            homeserver: self.homeserver,
6f1d6591c10datagen2026-09-26 09:52 UTC468
            authentication,
6f1d6591c10datagen2026-09-26 09:52 UTC469
        }
6f1d6591c10datagen2026-09-26 09:52 UTC470
    }
6f1d6591c10datagen2026-09-26 09:52 UTC471
}
6f1d6591c10datagen2026-09-26 09:52 UTC472
6f1d6591c10datagen2026-09-26 09:52 UTC473
pub enum Restore {
6f1d6591c10datagen2026-09-26 09:52 UTC474
    Resumed(Session),
6f1d6591c10datagen2026-09-26 09:52 UTC475
    SoftLoggedOut(SoftLogout),
d8d8a9fe770aatagen2026-10-22 07:44 UTC476
    /// an oauth login is waiting on its redirect. `Session::finish_oauth` takes the callback
d8d8a9fe770aatagen2026-10-22 07:44 UTC477
    OAuthPending,
6f1d6591c10datagen2026-09-26 09:52 UTC478
}
6f1d6591c10datagen2026-09-26 09:52 UTC479
6f1d6591c10datagen2026-09-26 09:52 UTC480
/// who to log in again as. the store keeps this device's e2ee keys, so a login
6f1d6591c10datagen2026-09-26 09:52 UTC481
/// as any other device would orphan them
6f1d6591c10datagen2026-09-26 09:52 UTC482
#[derive(Clone, Debug)]
6f1d6591c10datagen2026-09-26 09:52 UTC483
#[non_exhaustive]
6f1d6591c10datagen2026-09-26 09:52 UTC484
pub struct SoftLogout {
6f1d6591c10datagen2026-09-26 09:52 UTC485
    pub homeserver: String,
6f1d6591c10datagen2026-09-26 09:52 UTC486
    pub user_id: OwnedUserId,
6f1d6591c10datagen2026-09-26 09:52 UTC487
    pub device_id: OwnedDeviceId,
6f1d6591c10datagen2026-09-26 09:52 UTC488
    /// set when the login was OAuth, for `OAuthLogin::with_client_id`
6f1d6591c10datagen2026-09-26 09:52 UTC489
    pub client_id: Option<String>,
6f1d6591c10datagen2026-09-26 09:52 UTC490
}
6f1d6591c10datagen2026-09-26 09:52 UTC491
6f1d6591c10datagen2026-09-26 09:52 UTC492
impl SoftLogout {
6f1d6591c10datagen2026-09-26 09:52 UTC493
    #[must_use]
6f1d6591c10datagen2026-09-26 09:52 UTC494
    pub fn password_login(&self, password: impl Into<String>) -> PasswordLogin {
6f1d6591c10datagen2026-09-26 09:52 UTC495
        PasswordLogin::new(self.homeserver.clone(), self.user_id.as_str(), password)
6f1d6591c10datagen2026-09-26 09:52 UTC496
            .with_device_id(self.device_id.clone())
9c05b1dd327aatagen2026-10-08 11:27 UTC497
            .allow_insecure_loopback(true)
6f1d6591c10datagen2026-09-26 09:52 UTC498
    }
cb0668d30c14atagen2026-10-08 11:34 UTC499
cb0668d30c14atagen2026-10-08 11:34 UTC500
    #[must_use]
cb0668d30c14atagen2026-10-08 11:34 UTC501
    pub fn oauth_login(
cb0668d30c14atagen2026-10-08 11:34 UTC502
        &self,
cb0668d30c14atagen2026-10-08 11:34 UTC503
        redirect_uri: impl Into<String>,
cb0668d30c14atagen2026-10-08 11:34 UTC504
        client_uri: impl Into<String>,
cb0668d30c14atagen2026-10-08 11:34 UTC505
        application_type: OAuthApplicationType,
cb0668d30c14atagen2026-10-08 11:34 UTC506
    ) -> OAuthLogin {
cb0668d30c14atagen2026-10-08 11:34 UTC507
        let login = OAuthLogin::new(
cb0668d30c14atagen2026-10-08 11:34 UTC508
            self.homeserver.clone(),
cb0668d30c14atagen2026-10-08 11:34 UTC509
            redirect_uri,
cb0668d30c14atagen2026-10-08 11:34 UTC510
            client_uri,
cb0668d30c14atagen2026-10-08 11:34 UTC511
            application_type,
cb0668d30c14atagen2026-10-08 11:34 UTC512
        )
cb0668d30c14atagen2026-10-08 11:34 UTC513
        .with_device_id(self.device_id.clone())
cb0668d30c14atagen2026-10-08 11:34 UTC514
        .allow_insecure_loopback(true);
cb0668d30c14atagen2026-10-08 11:34 UTC515
        match &self.client_id {
cb0668d30c14atagen2026-10-08 11:34 UTC516
            Some(client_id) => login.with_client_id(client_id.clone()),
cb0668d30c14atagen2026-10-08 11:34 UTC517
            None => login,
cb0668d30c14atagen2026-10-08 11:34 UTC518
        }
cb0668d30c14atagen2026-10-08 11:34 UTC519
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC520
}
96fdf90aacd6atagen2026-09-08 08:24 UTC521
96fdf90aacd6atagen2026-09-08 08:24 UTC522
#[derive(Debug, Error)]
96fdf90aacd6atagen2026-09-08 08:24 UTC523
pub enum AccountError {
96fdf90aacd6atagen2026-09-08 08:24 UTC524
    #[error("{context} failed")]
96fdf90aacd6atagen2026-09-08 08:24 UTC525
    Operation {
96fdf90aacd6atagen2026-09-08 08:24 UTC526
        context: &'static str,
9a25b6ce66f4atagen2026-10-08 11:15 UTC527
        kind: ErrorKind,
9a25b6ce66f4atagen2026-10-08 11:15 UTC528
        server: Option<ServerError>,
96fdf90aacd6atagen2026-09-08 08:24 UTC529
        #[source]
62fe32759529atagen2026-09-26 08:50 UTC530
        source: Box<SourceError>,
96fdf90aacd6atagen2026-09-08 08:24 UTC531
    },
96fdf90aacd6atagen2026-09-08 08:24 UTC532
    #[error("the homeserver returned no authenticated session")]
96fdf90aacd6atagen2026-09-08 08:24 UTC533
    MissingSession,
96fdf90aacd6atagen2026-09-08 08:24 UTC534
    #[error("the SDK returned an unsupported authentication session")]
96fdf90aacd6atagen2026-09-08 08:24 UTC535
    UnsupportedAuthentication,
96fdf90aacd6atagen2026-09-08 08:24 UTC536
    #[error("the OAuth callback was already submitted")]
96fdf90aacd6atagen2026-09-08 08:24 UTC537
    OAuthCallbackAlreadySubmitted,
d8d8a9fe770aatagen2026-10-22 07:44 UTC538
    /// nothing is waiting for this callback. it was finished, abandoned, expired, or begun
d8d8a9fe770aatagen2026-10-22 07:44 UTC539
    /// somewhere else
d8d8a9fe770aatagen2026-10-22 07:44 UTC540
    #[error("no OAuth login is waiting for this callback")]
d8d8a9fe770aatagen2026-10-22 07:44 UTC541
    NoPendingOAuth,
9c05b1dd327aatagen2026-10-08 11:27 UTC542
    /// what was typed can't name a homeserver, so nothing was sent anywhere
9c05b1dd327aatagen2026-10-08 11:27 UTC543
    #[error("that isn't a usable homeserver, {0}")]
9c05b1dd327aatagen2026-10-08 11:27 UTC544
    InvalidHomeserver(&'static str),
cb0668d30c14atagen2026-10-08 11:34 UTC545
    /// `Accounts` already has a live login for them. a second would be another device
cb0668d30c14atagen2026-10-08 11:34 UTC546
    #[error("{0} is already logged in")]
cb0668d30c14atagen2026-10-08 11:34 UTC547
    AlreadyLoggedIn(OwnedUserId),
6db49c0ecf71atagen2026-09-26 12:41 UTC548
    /// an IndexedDB delete is waiting on the store's other connections. it goes through
6db49c0ecf71atagen2026-09-26 12:41 UTC549
    /// once they close, and opening the store again waits behind it
6db49c0ecf71atagen2026-09-26 12:41 UTC550
    #[error("the store is still open elsewhere, it will be deleted once that closes")]
6db49c0ecf71atagen2026-09-26 12:41 UTC551
    StoreDeletePending,
a250c7fa43fbatagen2026-09-26 13:35 UTC552
    /// another engine, in this process or another, has the store. only one runs per store
a250c7fa43fbatagen2026-09-26 13:35 UTC553
    #[error("another engine is using this store")]
a250c7fa43fbatagen2026-09-26 13:35 UTC554
    StoreInUse,
a250c7fa43fbatagen2026-09-26 13:35 UTC555
    /// the login worked but its first record didn't save. the server already has the
a250c7fa43fbatagen2026-09-26 13:35 UTC556
    /// device, so retry the save rather than log in again
a250c7fa43fbatagen2026-09-26 13:35 UTC557
    #[error("saving the new login's record failed")]
a250c7fa43fbatagen2026-09-26 13:35 UTC558
    Unsaved {
a250c7fa43fbatagen2026-09-26 13:35 UTC559
        session: Box<UnsavedSession>,
a250c7fa43fbatagen2026-09-26 13:35 UTC560
        #[source]
a250c7fa43fbatagen2026-09-26 13:35 UTC561
        source: Box<AccountError>,
a250c7fa43fbatagen2026-09-26 13:35 UTC562
    },
96fdf90aacd6atagen2026-09-08 08:24 UTC563
    #[error(transparent)]
96fdf90aacd6atagen2026-09-08 08:24 UTC564
    Engine(#[from] EngineError),
96fdf90aacd6atagen2026-09-08 08:24 UTC565
}
96fdf90aacd6atagen2026-09-08 08:24 UTC566
9a25b6ce66f4atagen2026-10-08 11:15 UTC567
impl AccountError {
9a25b6ce66f4atagen2026-10-08 11:15 UTC568
    /// what the homeserver said, when it refused
9a25b6ce66f4atagen2026-10-08 11:15 UTC569
    #[must_use]
9a25b6ce66f4atagen2026-10-08 11:15 UTC570
    pub fn server(&self) -> Option<&ServerError> {
9a25b6ce66f4atagen2026-10-08 11:15 UTC571
        match self {
9a25b6ce66f4atagen2026-10-08 11:15 UTC572
            Self::Operation { server, .. } => server.as_ref(),
9a25b6ce66f4atagen2026-10-08 11:15 UTC573
            Self::Unsaved { source, .. } => source.server(),
9a25b6ce66f4atagen2026-10-08 11:15 UTC574
            _ => None,
9a25b6ce66f4atagen2026-10-08 11:15 UTC575
        }
9a25b6ce66f4atagen2026-10-08 11:15 UTC576
    }
9a25b6ce66f4atagen2026-10-08 11:15 UTC577
}
9a25b6ce66f4atagen2026-10-08 11:15 UTC578
a250c7fa43fbatagen2026-09-26 13:35 UTC579
/// a live login whose record never reached its `SessionStore`
a250c7fa43fbatagen2026-09-26 13:35 UTC580
pub struct UnsavedSession {
a250c7fa43fbatagen2026-09-26 13:35 UTC581
    session: Session,
a250c7fa43fbatagen2026-09-26 13:35 UTC582
    sessions: Arc<dyn SessionStore>,
a250c7fa43fbatagen2026-09-26 13:35 UTC583
}
a250c7fa43fbatagen2026-09-26 13:35 UTC584
a250c7fa43fbatagen2026-09-26 13:35 UTC585
impl UnsavedSession {
a250c7fa43fbatagen2026-09-26 13:35 UTC586
    pub async fn save(self) -> Result<Session, AccountError> {
a250c7fa43fbatagen2026-09-26 13:35 UTC587
        let saved = match session_record(&self.session.client) {
a250c7fa43fbatagen2026-09-26 13:35 UTC588
            Ok(record) => self.sessions.save(&record).await,
a250c7fa43fbatagen2026-09-26 13:35 UTC589
            Err(error) => Err(error),
a250c7fa43fbatagen2026-09-26 13:35 UTC590
        };
a250c7fa43fbatagen2026-09-26 13:35 UTC591
        match saved {
a250c7fa43fbatagen2026-09-26 13:35 UTC592
            Ok(()) => Ok(self.session),
a250c7fa43fbatagen2026-09-26 13:35 UTC593
            Err(source) => Err(AccountError::Unsaved {
a250c7fa43fbatagen2026-09-26 13:35 UTC594
                session: Box::new(self),
a250c7fa43fbatagen2026-09-26 13:35 UTC595
                source: Box::new(source),
a250c7fa43fbatagen2026-09-26 13:35 UTC596
            }),
a250c7fa43fbatagen2026-09-26 13:35 UTC597
        }
a250c7fa43fbatagen2026-09-26 13:35 UTC598
    }
a250c7fa43fbatagen2026-09-26 13:35 UTC599
a250c7fa43fbatagen2026-09-26 13:35 UTC600
    /// the record to keep somewhere else, if the store won't take it
a250c7fa43fbatagen2026-09-26 13:35 UTC601
    pub fn record(&self) -> Result<SessionRecord, AccountError> {
a250c7fa43fbatagen2026-09-26 13:35 UTC602
        session_record(&self.session.client)
a250c7fa43fbatagen2026-09-26 13:35 UTC603
    }
a250c7fa43fbatagen2026-09-26 13:35 UTC604
}
a250c7fa43fbatagen2026-09-26 13:35 UTC605
a250c7fa43fbatagen2026-09-26 13:35 UTC606
impl std::fmt::Debug for UnsavedSession {
a250c7fa43fbatagen2026-09-26 13:35 UTC607
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
a250c7fa43fbatagen2026-09-26 13:35 UTC608
        formatter
a250c7fa43fbatagen2026-09-26 13:35 UTC609
            .debug_struct("UnsavedSession")
a250c7fa43fbatagen2026-09-26 13:35 UTC610
            .finish_non_exhaustive()
a250c7fa43fbatagen2026-09-26 13:35 UTC611
    }
a250c7fa43fbatagen2026-09-26 13:35 UTC612
}
a250c7fa43fbatagen2026-09-26 13:35 UTC613
96fdf90aacd6atagen2026-09-08 08:24 UTC614
impl Session {
96fdf90aacd6atagen2026-09-08 08:24 UTC615
    pub async fn login_password(
96fdf90aacd6atagen2026-09-08 08:24 UTC616
        login: PasswordLogin,
f12377bce9b9atagen2026-09-26 12:32 UTC617
        sessions: Arc<dyn SessionStore>,
96fdf90aacd6atagen2026-09-08 08:24 UTC618
    ) -> Result<Self, AccountError> {
cb0668d30c14atagen2026-10-08 11:34 UTC619
        homeserver_input(&login.homeserver, login.allow_insecure_loopback)?;
a250c7fa43fbatagen2026-09-26 13:35 UTC620
        let store_lock = sessions.store().lock().await?;
cb0668d30c14atagen2026-10-08 11:34 UTC621
        Self::login_password_locked(login, sessions, store_lock).await
cb0668d30c14atagen2026-10-08 11:34 UTC622
    }
cb0668d30c14atagen2026-10-08 11:34 UTC623
cb0668d30c14atagen2026-10-08 11:34 UTC624
    pub(crate) async fn login_password_locked(
cb0668d30c14atagen2026-10-08 11:34 UTC625
        login: PasswordLogin,
cb0668d30c14atagen2026-10-08 11:34 UTC626
        sessions: Arc<dyn SessionStore>,
cb0668d30c14atagen2026-10-08 11:34 UTC627
        store_lock: StoreLock,
cb0668d30c14atagen2026-10-08 11:34 UTC628
    ) -> Result<Self, AccountError> {
cb0668d30c14atagen2026-10-08 11:34 UTC629
        let homeserver = homeserver_input(&login.homeserver, login.allow_insecure_loopback)?;
713205a95af2atagen2026-09-26 09:24 UTC630
        let client = matrix_client_builder(sessions.store())
9c05b1dd327aatagen2026-10-08 11:27 UTC631
            .server_name_or_homeserver_url(homeserver)
96fdf90aacd6atagen2026-09-08 08:24 UTC632
            .build()
96fdf90aacd6atagen2026-09-08 08:24 UTC633
            .await
96fdf90aacd6atagen2026-09-08 08:24 UTC634
            .map_err(|error| account_error("building the Matrix client", error))?;
f12377bce9b9atagen2026-09-26 12:32 UTC635
        let tracking = Tracking::start(&client, sessions.clone())?;
96fdf90aacd6atagen2026-09-08 08:24 UTC636
        let mut request = client
96fdf90aacd6atagen2026-09-08 08:24 UTC637
            .matrix_auth()
96fdf90aacd6atagen2026-09-08 08:24 UTC638
            .login_username(login.username.trim(), &login.password)
96fdf90aacd6atagen2026-09-08 08:24 UTC639
            .request_refresh_token();
96fdf90aacd6atagen2026-09-08 08:24 UTC640
        if let Some(name) = login.device_display_name.as_deref() {
96fdf90aacd6atagen2026-09-08 08:24 UTC641
            request = request.initial_device_display_name(name);
96fdf90aacd6atagen2026-09-08 08:24 UTC642
        }
fa9114fcf75fatagen2026-09-24 12:51 UTC643
        if let Some(device_id) = login.device_id.as_deref() {
fa9114fcf75fatagen2026-09-24 12:51 UTC644
            request = request.device_id(device_id.as_str());
fa9114fcf75fatagen2026-09-24 12:51 UTC645
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC646
        request
96fdf90aacd6atagen2026-09-08 08:24 UTC647
            .send()
96fdf90aacd6atagen2026-09-08 08:24 UTC648
            .await
96fdf90aacd6atagen2026-09-08 08:24 UTC649
            .map_err(|error| account_error("password login", error))?;
a250c7fa43fbatagen2026-09-26 13:35 UTC650
        Self::saved(client, sessions, tracking, store_lock).await
96fdf90aacd6atagen2026-09-08 08:24 UTC651
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC652
a250c7fa43fbatagen2026-09-26 13:35 UTC653
    /// the pending login lives only in this process, so the one that begins it has to finish it
96fdf90aacd6atagen2026-09-08 08:24 UTC654
    pub async fn begin_oauth(
96fdf90aacd6atagen2026-09-08 08:24 UTC655
        login: OAuthLogin,
713205a95af2atagen2026-09-26 09:24 UTC656
        sessions: Arc<dyn SessionStore>,
96fdf90aacd6atagen2026-09-08 08:24 UTC657
    ) -> Result<PendingOAuthLogin, AccountError> {
cb0668d30c14atagen2026-10-08 11:34 UTC658
        homeserver_input(&login.homeserver, login.allow_insecure_loopback)?;
cb0668d30c14atagen2026-10-08 11:34 UTC659
        let store_lock = sessions.store().lock().await?;
cb0668d30c14atagen2026-10-08 11:34 UTC660
        Self::begin_oauth_locked(login, sessions, store_lock).await
cb0668d30c14atagen2026-10-08 11:34 UTC661
    }
cb0668d30c14atagen2026-10-08 11:34 UTC662
cb0668d30c14atagen2026-10-08 11:34 UTC663
    pub(crate) async fn begin_oauth_locked(
cb0668d30c14atagen2026-10-08 11:34 UTC664
        login: OAuthLogin,
cb0668d30c14atagen2026-10-08 11:34 UTC665
        sessions: Arc<dyn SessionStore>,
cb0668d30c14atagen2026-10-08 11:34 UTC666
        store_lock: StoreLock,
cb0668d30c14atagen2026-10-08 11:34 UTC667
    ) -> Result<PendingOAuthLogin, AccountError> {
96fdf90aacd6atagen2026-09-08 08:24 UTC668
        let redirect_uri = Url::parse(login.redirect_uri.trim())
96fdf90aacd6atagen2026-09-08 08:24 UTC669
            .map_err(|error| account_error("parsing the OAuth redirect URI", error))?;
96fdf90aacd6atagen2026-09-08 08:24 UTC670
        let client_uri = Url::parse(login.client_uri.trim())
96fdf90aacd6atagen2026-09-08 08:24 UTC671
            .map_err(|error| account_error("parsing the OAuth client URI", error))?;
9c05b1dd327aatagen2026-10-08 11:27 UTC672
        let homeserver = homeserver_input(&login.homeserver, login.allow_insecure_loopback)?;
713205a95af2atagen2026-09-26 09:24 UTC673
        let client = matrix_client_builder(sessions.store())
9c05b1dd327aatagen2026-10-08 11:27 UTC674
            .server_name_or_homeserver_url(homeserver)
96fdf90aacd6atagen2026-09-08 08:24 UTC675
            .build()
96fdf90aacd6atagen2026-09-08 08:24 UTC676
            .await
96fdf90aacd6atagen2026-09-08 08:24 UTC677
            .map_err(|error| account_error("building the Matrix client", error))?;
f12377bce9b9atagen2026-09-26 12:32 UTC678
        let tracking = Tracking::start(&client, sessions.clone())?;
96fdf90aacd6atagen2026-09-08 08:24 UTC679
        let application_type = match login.application_type {
96fdf90aacd6atagen2026-09-08 08:24 UTC680
            OAuthApplicationType::Native => ApplicationType::Native,
96fdf90aacd6atagen2026-09-08 08:24 UTC681
            OAuthApplicationType::Web => ApplicationType::Web,
96fdf90aacd6atagen2026-09-08 08:24 UTC682
        };
96fdf90aacd6atagen2026-09-08 08:24 UTC683
        let mut metadata = ClientMetadata::new(
96fdf90aacd6atagen2026-09-08 08:24 UTC684
            application_type,
96fdf90aacd6atagen2026-09-08 08:24 UTC685
            vec![OAuthGrantType::AuthorizationCode {
96fdf90aacd6atagen2026-09-08 08:24 UTC686
                redirect_uris: vec![redirect_uri.clone()],
96fdf90aacd6atagen2026-09-08 08:24 UTC687
            }],
96fdf90aacd6atagen2026-09-08 08:24 UTC688
            Localized::new(client_uri, []),
96fdf90aacd6atagen2026-09-08 08:24 UTC689
        );
96fdf90aacd6atagen2026-09-08 08:24 UTC690
        metadata.client_name = login.client_name.map(|name| Localized::new(name, []));
96fdf90aacd6atagen2026-09-08 08:24 UTC691
        let metadata = Raw::new(&metadata)
96fdf90aacd6atagen2026-09-08 08:24 UTC692
            .map_err(|error| account_error("serializing OAuth client metadata", error))?;
96fdf90aacd6atagen2026-09-08 08:24 UTC693
        let mut registration = ClientRegistrationData::new(metadata);
96fdf90aacd6atagen2026-09-08 08:24 UTC694
        if let Some(client_id) = login.client_id {
96fdf90aacd6atagen2026-09-08 08:24 UTC695
            registration.static_registrations = Some(HashMap::from([(
96fdf90aacd6atagen2026-09-08 08:24 UTC696
                client.homeserver(),
96fdf90aacd6atagen2026-09-08 08:24 UTC697
                ClientId::new(client_id),
96fdf90aacd6atagen2026-09-08 08:24 UTC698
            )]));
96fdf90aacd6atagen2026-09-08 08:24 UTC699
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC700
        let authorization = client
96fdf90aacd6atagen2026-09-08 08:24 UTC701
            .oauth()
96fdf90aacd6atagen2026-09-08 08:24 UTC702
            .login(redirect_uri, login.device_id, Some(registration), None)
96fdf90aacd6atagen2026-09-08 08:24 UTC703
            .build()
96fdf90aacd6atagen2026-09-08 08:24 UTC704
            .await
96fdf90aacd6atagen2026-09-08 08:24 UTC705
            .map_err(|error| account_error("starting OAuth login", error))?;
d8d8a9fe770aatagen2026-10-22 07:44 UTC706
        let previous = sessions.load().await?.and_then(SessionRecord::resumable);
d8d8a9fe770aatagen2026-10-22 07:44 UTC707
        let pending = SessionRecord {
d8d8a9fe770aatagen2026-10-22 07:44 UTC708
            homeserver: client.homeserver().to_string(),
d8d8a9fe770aatagen2026-10-22 07:44 UTC709
            authentication: AuthenticationRecord::PendingOAuth {
d8d8a9fe770aatagen2026-10-22 07:44 UTC710
                client_id: client
d8d8a9fe770aatagen2026-10-22 07:44 UTC711
                    .oauth()
d8d8a9fe770aatagen2026-10-22 07:44 UTC712
                    .client_id()
d8d8a9fe770aatagen2026-10-22 07:44 UTC713
                    .ok_or(AccountError::MissingSession)?
d8d8a9fe770aatagen2026-10-22 07:44 UTC714
                    .clone(),
d8d8a9fe770aatagen2026-10-22 07:44 UTC715
                state: authorization.state.clone(),
d8d8a9fe770aatagen2026-10-22 07:44 UTC716
                authorization: Box::new(
d8d8a9fe770aatagen2026-10-22 07:44 UTC717
                    client
d8d8a9fe770aatagen2026-10-22 07:44 UTC718
                        .oauth()
d8d8a9fe770aatagen2026-10-22 07:44 UTC719
                        .pending_authorization(&authorization.state)
d8d8a9fe770aatagen2026-10-22 07:44 UTC720
                        .await
d8d8a9fe770aatagen2026-10-22 07:44 UTC721
                        .ok_or(AccountError::MissingSession)?,
d8d8a9fe770aatagen2026-10-22 07:44 UTC722
                ),
d8d8a9fe770aatagen2026-10-22 07:44 UTC723
                started_ms: now_ms(),
d8d8a9fe770aatagen2026-10-22 07:44 UTC724
                previous: previous.clone().map(Box::new),
d8d8a9fe770aatagen2026-10-22 07:44 UTC725
            },
d8d8a9fe770aatagen2026-10-22 07:44 UTC726
        };
d8d8a9fe770aatagen2026-10-22 07:44 UTC727
        sessions.save(&pending).await?;
96fdf90aacd6atagen2026-09-08 08:24 UTC728
        Ok(PendingOAuthLogin {
96fdf90aacd6atagen2026-09-08 08:24 UTC729
            client,
713205a95af2atagen2026-09-26 09:24 UTC730
            sessions,
a250c7fa43fbatagen2026-09-26 13:35 UTC731
            tracking: Some((tracking, store_lock)),
96fdf90aacd6atagen2026-09-08 08:24 UTC732
            authorization_url: authorization.url.to_string(),
96fdf90aacd6atagen2026-09-08 08:24 UTC733
            state: Some(authorization.state),
d8d8a9fe770aatagen2026-10-22 07:44 UTC734
            previous,
96fdf90aacd6atagen2026-09-08 08:24 UTC735
        })
96fdf90aacd6atagen2026-09-08 08:24 UTC736
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC737
d8d8a9fe770aatagen2026-10-22 07:44 UTC738
    /// finishes a login `begin_oauth` started in another process, from the callback the
d8d8a9fe770aatagen2026-10-22 07:44 UTC739
    /// redirect brought back. the pending login is in `sessions`, so nothing else is needed
d8d8a9fe770aatagen2026-10-22 07:44 UTC740
    pub async fn finish_oauth(
d8d8a9fe770aatagen2026-10-22 07:44 UTC741
        sessions: Arc<dyn SessionStore>,
d8d8a9fe770aatagen2026-10-22 07:44 UTC742
        callback_url: &str,
d8d8a9fe770aatagen2026-10-22 07:44 UTC743
    ) -> Result<Self, AccountError> {
d8d8a9fe770aatagen2026-10-22 07:44 UTC744
        let store_lock = sessions.store().lock().await?;
d8d8a9fe770aatagen2026-10-22 07:44 UTC745
        Self::finish_oauth_locked(sessions, store_lock, callback_url).await
d8d8a9fe770aatagen2026-10-22 07:44 UTC746
    }
d8d8a9fe770aatagen2026-10-22 07:44 UTC747
d8d8a9fe770aatagen2026-10-22 07:44 UTC748
    pub(crate) async fn finish_oauth_locked(
d8d8a9fe770aatagen2026-10-22 07:44 UTC749
        sessions: Arc<dyn SessionStore>,
d8d8a9fe770aatagen2026-10-22 07:44 UTC750
        store_lock: StoreLock,
d8d8a9fe770aatagen2026-10-22 07:44 UTC751
        callback_url: &str,
d8d8a9fe770aatagen2026-10-22 07:44 UTC752
    ) -> Result<Self, AccountError> {
d8d8a9fe770aatagen2026-10-22 07:44 UTC753
        let callback = Url::parse(callback_url)
d8d8a9fe770aatagen2026-10-22 07:44 UTC754
            .map_err(|error| account_error("parsing the OAuth callback URL", error))?;
d8d8a9fe770aatagen2026-10-22 07:44 UTC755
        let record = sessions.load().await?.ok_or(AccountError::NoPendingOAuth)?;
d8d8a9fe770aatagen2026-10-22 07:44 UTC756
        if record.pending_oauth_state() != callback_state(&callback).as_deref() {
d8d8a9fe770aatagen2026-10-22 07:44 UTC757
            return Err(AccountError::NoPendingOAuth);
d8d8a9fe770aatagen2026-10-22 07:44 UTC758
        }
d8d8a9fe770aatagen2026-10-22 07:44 UTC759
        let AuthenticationRecord::PendingOAuth {
d8d8a9fe770aatagen2026-10-22 07:44 UTC760
            client_id,
d8d8a9fe770aatagen2026-10-22 07:44 UTC761
            state,
d8d8a9fe770aatagen2026-10-22 07:44 UTC762
            authorization,
d8d8a9fe770aatagen2026-10-22 07:44 UTC763
            previous,
d8d8a9fe770aatagen2026-10-22 07:44 UTC764
            ..
d8d8a9fe770aatagen2026-10-22 07:44 UTC765
        } = record.authentication
d8d8a9fe770aatagen2026-10-22 07:44 UTC766
        else {
d8d8a9fe770aatagen2026-10-22 07:44 UTC767
            return Err(AccountError::NoPendingOAuth);
d8d8a9fe770aatagen2026-10-22 07:44 UTC768
        };
d8d8a9fe770aatagen2026-10-22 07:44 UTC769
        let client = matrix_client_builder(sessions.store())
d8d8a9fe770aatagen2026-10-22 07:44 UTC770
            .homeserver_url(&record.homeserver)
d8d8a9fe770aatagen2026-10-22 07:44 UTC771
            .build()
d8d8a9fe770aatagen2026-10-22 07:44 UTC772
            .await
d8d8a9fe770aatagen2026-10-22 07:44 UTC773
            .map_err(|error| account_error("reopening the Matrix store", error))?;
d8d8a9fe770aatagen2026-10-22 07:44 UTC774
        let tracking = Tracking::start(&client, sessions.clone())?;
d8d8a9fe770aatagen2026-10-22 07:44 UTC775
        let oauth = client.oauth();
d8d8a9fe770aatagen2026-10-22 07:44 UTC776
        oauth.restore_registered_client(client_id);
d8d8a9fe770aatagen2026-10-22 07:44 UTC777
        let finished = match oauth
d8d8a9fe770aatagen2026-10-22 07:44 UTC778
            .restore_pending_authorization(state, *authorization)
d8d8a9fe770aatagen2026-10-22 07:44 UTC779
            .await
d8d8a9fe770aatagen2026-10-22 07:44 UTC780
        {
d8d8a9fe770aatagen2026-10-22 07:44 UTC781
            Ok(()) => oauth.finish_login(callback.into()).await,
d8d8a9fe770aatagen2026-10-22 07:44 UTC782
            Err(error) => Err(error.into()),
d8d8a9fe770aatagen2026-10-22 07:44 UTC783
        };
d8d8a9fe770aatagen2026-10-22 07:44 UTC784
        if let Err(error) = finished {
d8d8a9fe770aatagen2026-10-22 07:44 UTC785
            match previous {
d8d8a9fe770aatagen2026-10-22 07:44 UTC786
                Some(previous) => put_back(sessions.as_ref(), *previous).await,
d8d8a9fe770aatagen2026-10-22 07:44 UTC787
                None => {
d8d8a9fe770aatagen2026-10-22 07:44 UTC788
                    drop((tracking, client));
d8d8a9fe770aatagen2026-10-22 07:44 UTC789
                    if let Err(error) = sessions.discard().await {
d8d8a9fe770aatagen2026-10-22 07:44 UTC790
                        tracing::warn!(%error, "deleting a failed OAuth login's store failed");
d8d8a9fe770aatagen2026-10-22 07:44 UTC791
                    }
d8d8a9fe770aatagen2026-10-22 07:44 UTC792
                }
d8d8a9fe770aatagen2026-10-22 07:44 UTC793
            }
d8d8a9fe770aatagen2026-10-22 07:44 UTC794
            return Err(account_error("OAuth login", error));
d8d8a9fe770aatagen2026-10-22 07:44 UTC795
        }
d8d8a9fe770aatagen2026-10-22 07:44 UTC796
        Self::saved(client, sessions, tracking, store_lock).await
d8d8a9fe770aatagen2026-10-22 07:44 UTC797
    }
d8d8a9fe770aatagen2026-10-22 07:44 UTC798
713205a95af2atagen2026-09-26 09:24 UTC799
    /// Restore the record `sessions` holds, or `None` when it holds none.
f12377bce9b9atagen2026-09-26 12:32 UTC800
    /// The caller must also handle rejection and explicit logout.
f12377bce9b9atagen2026-09-26 12:32 UTC801
    pub async fn restore(sessions: Arc<dyn SessionStore>) -> Result<Option<Restore>, AccountError> {
713205a95af2atagen2026-09-26 09:24 UTC802
        let Some(record) = sessions.load().await? else {
713205a95af2atagen2026-09-26 09:24 UTC803
            return Ok(None);
713205a95af2atagen2026-09-26 09:24 UTC804
        };
713205a95af2atagen2026-09-26 09:24 UTC805
        let authentication = match record.authentication {
96fdf90aacd6atagen2026-09-08 08:24 UTC806
            AuthenticationRecord::Matrix(session) => AuthSession::Matrix(session),
96fdf90aacd6atagen2026-09-08 08:24 UTC807
            AuthenticationRecord::OAuth { client_id, user } => {
96fdf90aacd6atagen2026-09-08 08:24 UTC808
                OAuthSession { client_id, user }.into()
96fdf90aacd6atagen2026-09-08 08:24 UTC809
            }
d8d8a9fe770aatagen2026-10-22 07:44 UTC810
            AuthenticationRecord::PendingOAuth { .. } if record.oauth_expired() => {
d8d8a9fe770aatagen2026-10-22 07:44 UTC811
                let previous = record.resumable();
d8d8a9fe770aatagen2026-10-22 07:44 UTC812
                match previous {
d8d8a9fe770aatagen2026-10-22 07:44 UTC813
                    Some(previous) => put_back(sessions.as_ref(), previous).await,
d8d8a9fe770aatagen2026-10-22 07:44 UTC814
                    None => sessions.discard().await?,
d8d8a9fe770aatagen2026-10-22 07:44 UTC815
                }
d8d8a9fe770aatagen2026-10-22 07:44 UTC816
                return Box::pin(Self::restore(sessions)).await;
d8d8a9fe770aatagen2026-10-22 07:44 UTC817
            }
d8d8a9fe770aatagen2026-10-22 07:44 UTC818
            AuthenticationRecord::PendingOAuth { .. } => return Ok(Some(Restore::OAuthPending)),
6f1d6591c10datagen2026-09-26 09:52 UTC819
            AuthenticationRecord::SoftLoggedOut { meta, client_id } => {
6f1d6591c10datagen2026-09-26 09:52 UTC820
                return Ok(Some(Restore::SoftLoggedOut(SoftLogout {
6f1d6591c10datagen2026-09-26 09:52 UTC821
                    homeserver: record.homeserver,
6f1d6591c10datagen2026-09-26 09:52 UTC822
                    user_id: meta.user_id,
6f1d6591c10datagen2026-09-26 09:52 UTC823
                    device_id: meta.device_id,
6f1d6591c10datagen2026-09-26 09:52 UTC824
                    client_id: client_id.map(|client_id| client_id.as_str().to_owned()),
6f1d6591c10datagen2026-09-26 09:52 UTC825
                })));
6f1d6591c10datagen2026-09-26 09:52 UTC826
            }
96fdf90aacd6atagen2026-09-08 08:24 UTC827
        };
a250c7fa43fbatagen2026-09-26 13:35 UTC828
        let store_lock = sessions.store().lock().await?;
6f1d6591c10datagen2026-09-26 09:52 UTC829
        let client = matrix_client_builder(sessions.store())
6f1d6591c10datagen2026-09-26 09:52 UTC830
            .homeserver_url(&record.homeserver)
6f1d6591c10datagen2026-09-26 09:52 UTC831
            .build()
6f1d6591c10datagen2026-09-26 09:52 UTC832
            .await
6f1d6591c10datagen2026-09-26 09:52 UTC833
            .map_err(|error| account_error("reopening the Matrix store", error))?;
f12377bce9b9atagen2026-09-26 12:32 UTC834
        let tracking = Tracking::start(&client, sessions)?;
96fdf90aacd6atagen2026-09-08 08:24 UTC835
        client
96fdf90aacd6atagen2026-09-08 08:24 UTC836
            .restore_session(authentication)
96fdf90aacd6atagen2026-09-08 08:24 UTC837
            .await
96fdf90aacd6atagen2026-09-08 08:24 UTC838
            .map_err(|error| account_error("restoring the authenticated session", error))?;
a250c7fa43fbatagen2026-09-26 13:35 UTC839
        Ok(Some(Restore::Resumed(Self {
a250c7fa43fbatagen2026-09-26 13:35 UTC840
            client,
a250c7fa43fbatagen2026-09-26 13:35 UTC841
            tracking,
a250c7fa43fbatagen2026-09-26 13:35 UTC842
            store_lock,
a250c7fa43fbatagen2026-09-26 13:35 UTC843
        })))
f12377bce9b9atagen2026-09-26 12:32 UTC844
    }
f12377bce9b9atagen2026-09-26 12:32 UTC845
f12377bce9b9atagen2026-09-26 12:32 UTC846
    /// leave the record to the host, which then saves `ClientHandle::session_record()`
f12377bce9b9atagen2026-09-26 12:32 UTC847
    /// on every `SessionTokensChanged`
f12377bce9b9atagen2026-09-26 12:32 UTC848
    #[must_use]
f12377bce9b9atagen2026-09-26 12:32 UTC849
    pub fn without_persistence(mut self) -> Self {
f12377bce9b9atagen2026-09-26 12:32 UTC850
        self.tracking.persistence = None;
f12377bce9b9atagen2026-09-26 12:32 UTC851
        #[cfg(not(target_family = "wasm"))]
f12377bce9b9atagen2026-09-26 12:32 UTC852
        self.tracking.inline.store(false, Ordering::Release);
f12377bce9b9atagen2026-09-26 12:32 UTC853
        self
96fdf90aacd6atagen2026-09-08 08:24 UTC854
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC855
96fdf90aacd6atagen2026-09-08 08:24 UTC856
    pub async fn build(
96fdf90aacd6atagen2026-09-08 08:24 UTC857
        self,
96fdf90aacd6atagen2026-09-08 08:24 UTC858
        config: EngineConfig,
96fdf90aacd6atagen2026-09-08 08:24 UTC859
    ) -> Result<(Engine, ClientHandle, EventStream), AccountError> {
cbb715e11963atagen2026-10-24 23:24 UTC860
        let account = crate::mentions::store::Account {
cbb715e11963atagen2026-10-24 23:24 UTC861
            homeserver: self.client.homeserver().to_string(),
cbb715e11963atagen2026-10-24 23:24 UTC862
            user_id: self
cbb715e11963atagen2026-10-24 23:24 UTC863
                .client
cbb715e11963atagen2026-10-24 23:24 UTC864
                .user_id()
cbb715e11963atagen2026-10-24 23:24 UTC865
                .ok_or(AccountError::MissingSession)?
cbb715e11963atagen2026-10-24 23:24 UTC866
                .to_owned(),
cbb715e11963atagen2026-10-24 23:24 UTC867
        };
c9effeac326aatagen2026-10-25 10:32 UTC868
        let search =
c9effeac326aatagen2026-10-25 10:32 UTC869
            crate::search::store::SearchStore::open(self.tracking.store.store(), account.clone())
c9effeac326aatagen2026-10-25 10:32 UTC870
                .await;
82d37c369846atagen2026-10-25 10:55 UTC871
        let opener = crate::mentions::store::Opener::new(
cbb715e11963atagen2026-10-24 23:24 UTC872
            self.tracking.store.store(),
cbb715e11963atagen2026-10-24 23:24 UTC873
            account,
cbb715e11963atagen2026-10-24 23:24 UTC874
            config.max_mentions_bytes,
82d37c369846atagen2026-10-25 10:55 UTC875
        );
82d37c369846atagen2026-10-25 10:55 UTC876
        let mentions = match opener.open().await {
82d37c369846atagen2026-10-25 10:55 UTC877
            Ok(store) => Ok(store),
82d37c369846atagen2026-10-25 10:55 UTC878
            Err(crate::mentions::store::OpenError::Unavailable(error)) => {
82d37c369846atagen2026-10-25 10:55 UTC879
                Err(crate::mentions::service::Unopened {
82d37c369846atagen2026-10-25 10:55 UTC880
                    error,
82d37c369846atagen2026-10-25 10:55 UTC881
                    retry: Some(opener),
82d37c369846atagen2026-10-25 10:55 UTC882
                })
82d37c369846atagen2026-10-25 10:55 UTC883
            }
82d37c369846atagen2026-10-25 10:55 UTC884
            Err(crate::mentions::store::OpenError::Newer(error)) => {
82d37c369846atagen2026-10-25 10:55 UTC885
                Err(crate::mentions::service::Unopened { error, retry: None })
82d37c369846atagen2026-10-25 10:55 UTC886
            }
82d37c369846atagen2026-10-25 10:55 UTC887
        };
f12377bce9b9atagen2026-09-26 12:32 UTC888
        Engine::with_persistence(
f12377bce9b9atagen2026-09-26 12:32 UTC889
            self.client,
f12377bce9b9atagen2026-09-26 12:32 UTC890
            config,
f12377bce9b9atagen2026-09-26 12:32 UTC891
            self.tracking.session_changes,
f12377bce9b9atagen2026-09-26 12:32 UTC892
            self.tracking.persistence,
a250c7fa43fbatagen2026-09-26 13:35 UTC893
            Some(self.store_lock),
cbb715e11963atagen2026-10-24 23:24 UTC894
            Some(mentions),
c9effeac326aatagen2026-10-25 10:32 UTC895
            Some(search),
f12377bce9b9atagen2026-09-26 12:32 UTC896
        )
f12377bce9b9atagen2026-09-26 12:32 UTC897
        .await
f12377bce9b9atagen2026-09-26 12:32 UTC898
        .map_err(AccountError::from)
96fdf90aacd6atagen2026-09-08 08:24 UTC899
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC900
f12377bce9b9atagen2026-09-26 12:32 UTC901
    async fn saved(
f12377bce9b9atagen2026-09-26 12:32 UTC902
        client: Client,
a250c7fa43fbatagen2026-09-26 13:35 UTC903
        sessions: Arc<dyn SessionStore>,
f12377bce9b9atagen2026-09-26 12:32 UTC904
        tracking: Tracking,
a250c7fa43fbatagen2026-09-26 13:35 UTC905
        store_lock: StoreLock,
f12377bce9b9atagen2026-09-26 12:32 UTC906
    ) -> Result<Self, AccountError> {
a250c7fa43fbatagen2026-09-26 13:35 UTC907
        UnsavedSession {
a250c7fa43fbatagen2026-09-26 13:35 UTC908
            session: Self {
a250c7fa43fbatagen2026-09-26 13:35 UTC909
                client,
a250c7fa43fbatagen2026-09-26 13:35 UTC910
                tracking,
a250c7fa43fbatagen2026-09-26 13:35 UTC911
                store_lock,
a250c7fa43fbatagen2026-09-26 13:35 UTC912
            },
a250c7fa43fbatagen2026-09-26 13:35 UTC913
            sessions,
a250c7fa43fbatagen2026-09-26 13:35 UTC914
        }
a250c7fa43fbatagen2026-09-26 13:35 UTC915
        .save()
a250c7fa43fbatagen2026-09-26 13:35 UTC916
        .await
f12377bce9b9atagen2026-09-26 12:32 UTC917
    }
f12377bce9b9atagen2026-09-26 12:32 UTC918
}
f12377bce9b9atagen2026-09-26 12:32 UTC919
f12377bce9b9atagen2026-09-26 12:32 UTC920
impl Tracking {
f12377bce9b9atagen2026-09-26 12:32 UTC921
    fn start(client: &Client, sessions: Arc<dyn SessionStore>) -> Result<Self, AccountError> {
f12377bce9b9atagen2026-09-26 12:32 UTC922
        let session_changes = client.subscribe_to_session_changes();
f12377bce9b9atagen2026-09-26 12:32 UTC923
        #[cfg_attr(target_family = "wasm", allow(unused_variables))]
f12377bce9b9atagen2026-09-26 12:32 UTC924
        let (results, inline_results) = mpsc::unbounded_channel();
f12377bce9b9atagen2026-09-26 12:32 UTC925
        #[cfg(not(target_family = "wasm"))]
f12377bce9b9atagen2026-09-26 12:32 UTC926
        let inline = save_inline(client, sessions.clone(), results)?;
f12377bce9b9atagen2026-09-26 12:32 UTC927
        Ok(Self {
cbb715e11963atagen2026-10-24 23:24 UTC928
            store: sessions.clone(),
f12377bce9b9atagen2026-09-26 12:32 UTC929
            session_changes,
f12377bce9b9atagen2026-09-26 12:32 UTC930
            persistence: Some(Persistence {
f12377bce9b9atagen2026-09-26 12:32 UTC931
                save: Box::new(move |client| {
f12377bce9b9atagen2026-09-26 12:32 UTC932
                    let sessions = sessions.clone();
f12377bce9b9atagen2026-09-26 12:32 UTC933
                    Box::pin(async move {
f12377bce9b9atagen2026-09-26 12:32 UTC934
                        let record = session_record(&client).map_err(save_failed)?;
f12377bce9b9atagen2026-09-26 12:32 UTC935
                        sessions.save(&record).await.map_err(save_failed)
f12377bce9b9atagen2026-09-26 12:32 UTC936
                    })
f12377bce9b9atagen2026-09-26 12:32 UTC937
                }),
f12377bce9b9atagen2026-09-26 12:32 UTC938
                inline: inline_results,
f12377bce9b9atagen2026-09-26 12:32 UTC939
            }),
f12377bce9b9atagen2026-09-26 12:32 UTC940
            #[cfg(not(target_family = "wasm"))]
f12377bce9b9atagen2026-09-26 12:32 UTC941
            inline,
f12377bce9b9atagen2026-09-26 12:32 UTC942
        })
96fdf90aacd6atagen2026-09-08 08:24 UTC943
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC944
}
96fdf90aacd6atagen2026-09-08 08:24 UTC945
f12377bce9b9atagen2026-09-26 12:32 UTC946
/// the sdk calls this inside its refresh, after it has the new tokens and before it uses
f12377bce9b9atagen2026-09-26 12:32 UTC947
/// them, so a store that writes there and then can't lose a refresh token to a crash
f12377bce9b9atagen2026-09-26 12:32 UTC948
#[cfg(not(target_family = "wasm"))]
f12377bce9b9atagen2026-09-26 12:32 UTC949
fn save_inline(
f12377bce9b9atagen2026-09-26 12:32 UTC950
    client: &Client,
f12377bce9b9atagen2026-09-26 12:32 UTC951
    sessions: Arc<dyn SessionStore>,
f12377bce9b9atagen2026-09-26 12:32 UTC952
    results: mpsc::UnboundedSender<Result<(), crate::Error>>,
f12377bce9b9atagen2026-09-26 12:32 UTC953
) -> Result<Arc<AtomicBool>, AccountError> {
f12377bce9b9atagen2026-09-26 12:32 UTC954
    let enabled = Arc::new(AtomicBool::new(true));
f12377bce9b9atagen2026-09-26 12:32 UTC955
    let saving = enabled.clone();
f12377bce9b9atagen2026-09-26 12:32 UTC956
    client
f12377bce9b9atagen2026-09-26 12:32 UTC957
        .set_session_callbacks(
f12377bce9b9atagen2026-09-26 12:32 UTC958
            // only asked for with the cross process refresh lock, which we never enable
f12377bce9b9atagen2026-09-26 12:32 UTC959
            Box::new(|client| {
f12377bce9b9atagen2026-09-26 12:32 UTC960
                client
f12377bce9b9atagen2026-09-26 12:32 UTC961
                    .session_tokens()
f12377bce9b9atagen2026-09-26 12:32 UTC962
                    .ok_or_else(|| AccountError::MissingSession.into())
f12377bce9b9atagen2026-09-26 12:32 UTC963
            }),
f12377bce9b9atagen2026-09-26 12:32 UTC964
            Box::new(move |client| {
f12377bce9b9atagen2026-09-26 12:32 UTC965
                if !saving.load(Ordering::Acquire) {
f12377bce9b9atagen2026-09-26 12:32 UTC966
                    return Ok(());
f12377bce9b9atagen2026-09-26 12:32 UTC967
                }
f12377bce9b9atagen2026-09-26 12:32 UTC968
                let record = session_record(&client)?;
f12377bce9b9atagen2026-09-26 12:32 UTC969
                if let Some(result) = sessions.save_blocking(&record) {
f12377bce9b9atagen2026-09-26 12:32 UTC970
                    let _ = results.send(result.map_err(save_failed));
f12377bce9b9atagen2026-09-26 12:32 UTC971
                }
f12377bce9b9atagen2026-09-26 12:32 UTC972
                Ok(())
f12377bce9b9atagen2026-09-26 12:32 UTC973
            }),
f12377bce9b9atagen2026-09-26 12:32 UTC974
        )
f12377bce9b9atagen2026-09-26 12:32 UTC975
        .map_err(|error| account_error("registering the session callbacks", error))?;
f12377bce9b9atagen2026-09-26 12:32 UTC976
    Ok(enabled)
f12377bce9b9atagen2026-09-26 12:32 UTC977
}
f12377bce9b9atagen2026-09-26 12:32 UTC978
f12377bce9b9atagen2026-09-26 12:32 UTC979
fn save_failed(error: AccountError) -> crate::Error {
f12377bce9b9atagen2026-09-26 12:32 UTC980
    let message = format!("saving the session record failed, {error}");
f12377bce9b9atagen2026-09-26 12:32 UTC981
    crate::Error::with_source(ErrorKind::Store, message, error)
f12377bce9b9atagen2026-09-26 12:32 UTC982
}
f12377bce9b9atagen2026-09-26 12:32 UTC983
96fdf90aacd6atagen2026-09-08 08:24 UTC984
pub(crate) fn session_record(client: &Client) -> Result<SessionRecord, AccountError> {
96fdf90aacd6atagen2026-09-08 08:24 UTC985
    let authentication = match client.session().ok_or(AccountError::MissingSession)? {
96fdf90aacd6atagen2026-09-08 08:24 UTC986
        AuthSession::Matrix(session) => AuthenticationRecord::Matrix(session),
96fdf90aacd6atagen2026-09-08 08:24 UTC987
        AuthSession::OAuth(session) => {
96fdf90aacd6atagen2026-09-08 08:24 UTC988
            let OAuthSession { client_id, user } = *session;
96fdf90aacd6atagen2026-09-08 08:24 UTC989
            AuthenticationRecord::OAuth { client_id, user }
96fdf90aacd6atagen2026-09-08 08:24 UTC990
        }
96fdf90aacd6atagen2026-09-08 08:24 UTC991
        _ => return Err(AccountError::UnsupportedAuthentication),
96fdf90aacd6atagen2026-09-08 08:24 UTC992
    };
96fdf90aacd6atagen2026-09-08 08:24 UTC993
    Ok(SessionRecord {
96fdf90aacd6atagen2026-09-08 08:24 UTC994
        homeserver: client.homeserver().to_string(),
96fdf90aacd6atagen2026-09-08 08:24 UTC995
        authentication,
96fdf90aacd6atagen2026-09-08 08:24 UTC996
    })
96fdf90aacd6atagen2026-09-08 08:24 UTC997
}
96fdf90aacd6atagen2026-09-08 08:24 UTC998
96fdf90aacd6atagen2026-09-08 08:24 UTC999
fn encryption_settings() -> EncryptionSettings {
96fdf90aacd6atagen2026-09-08 08:24 UTC1000
    EncryptionSettings {
96fdf90aacd6atagen2026-09-08 08:24 UTC1001
        auto_enable_cross_signing: true,
96fdf90aacd6atagen2026-09-08 08:24 UTC1002
        backup_download_strategy: BackupDownloadStrategy::OneShot,
96fdf90aacd6atagen2026-09-08 08:24 UTC1003
        auto_enable_backups: true,
96fdf90aacd6atagen2026-09-08 08:24 UTC1004
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC1005
}
96fdf90aacd6atagen2026-09-08 08:24 UTC1006
a250c7fa43fbatagen2026-09-26 13:35 UTC1007
/// `StoreLock` already keeps every other client off the store, and the sdk's lease would cost
a250c7fa43fbatagen2026-09-26 13:35 UTC1008
/// a store write per locked section and a task renewing it every 50ms
96fdf90aacd6atagen2026-09-08 08:24 UTC1009
fn matrix_client_builder(store: &StoreConfig) -> ClientBuilder {
a250c7fa43fbatagen2026-09-26 13:35 UTC1010
    let builder = Client::builder()
96fdf90aacd6atagen2026-09-08 08:24 UTC1011
        .with_encryption_settings(encryption_settings())
96fdf90aacd6atagen2026-09-08 08:24 UTC1012
        .with_threading_support(ThreadingSupport::Enabled {
96fdf90aacd6atagen2026-09-08 08:24 UTC1013
            with_subscriptions: false,
96fdf90aacd6atagen2026-09-08 08:24 UTC1014
        })
a250c7fa43fbatagen2026-09-26 13:35 UTC1015
        .handle_refresh_tokens()
a250c7fa43fbatagen2026-09-26 13:35 UTC1016
        .cross_process_store_config(CrossProcessLockConfig::SingleProcess);
62fe32759529atagen2026-09-26 08:50 UTC1017
    match &store.location {
62fe32759529atagen2026-09-26 08:50 UTC1018
        #[cfg(feature = "sqlite")]
62fe32759529atagen2026-09-26 08:50 UTC1019
        StoreLocation::Sqlite(path) => builder.sqlite_store(path, Some(&store.passphrase)),
62fe32759529atagen2026-09-26 08:50 UTC1020
        #[cfg(feature = "indexeddb")]
62fe32759529atagen2026-09-26 08:50 UTC1021
        StoreLocation::IndexedDb(name) => builder.indexeddb_store(name, Some(&store.passphrase)),
62fe32759529atagen2026-09-26 08:50 UTC1022
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC1023
}
96fdf90aacd6atagen2026-09-08 08:24 UTC1024
9c05b1dd327aatagen2026-10-08 11:27 UTC1025
/// a bare server name goes through untouched, since that's what makes the sdk try `.well-known`
cb0668d30c14atagen2026-10-08 11:34 UTC1026
pub(crate) fn homeserver_input(
cb0668d30c14atagen2026-10-08 11:34 UTC1027
    input: &str,
cb0668d30c14atagen2026-10-08 11:34 UTC1028
    allow_insecure_loopback: bool,
cb0668d30c14atagen2026-10-08 11:34 UTC1029
) -> Result<&str, AccountError> {
9c05b1dd327aatagen2026-10-08 11:27 UTC1030
    let input = input.trim();
9c05b1dd327aatagen2026-10-08 11:27 UTC1031
    let invalid = |reason| Err(AccountError::InvalidHomeserver(reason));
9c05b1dd327aatagen2026-10-08 11:27 UTC1032
    if input.is_empty() {
9c05b1dd327aatagen2026-10-08 11:27 UTC1033
        return invalid("it's empty");
9c05b1dd327aatagen2026-10-08 11:27 UTC1034
    }
9c05b1dd327aatagen2026-10-08 11:27 UTC1035
    if input.contains(char::is_whitespace) {
9c05b1dd327aatagen2026-10-08 11:27 UTC1036
        return invalid("it has spaces in it");
9c05b1dd327aatagen2026-10-08 11:27 UTC1037
    }
9c05b1dd327aatagen2026-10-08 11:27 UTC1038
    if !input.contains("://") {
9c05b1dd327aatagen2026-10-08 11:27 UTC1039
        return match <&ruma::ServerName>::try_from(input) {
9c05b1dd327aatagen2026-10-08 11:27 UTC1040
            Ok(_) => Ok(input),
9c05b1dd327aatagen2026-10-08 11:27 UTC1041
            Err(_) => invalid("it's neither a server name nor a url"),
9c05b1dd327aatagen2026-10-08 11:27 UTC1042
        };
9c05b1dd327aatagen2026-10-08 11:27 UTC1043
    }
9c05b1dd327aatagen2026-10-08 11:27 UTC1044
    let Ok(url) = Url::parse(input) else {
9c05b1dd327aatagen2026-10-08 11:27 UTC1045
        return invalid("it isn't a valid url");
9c05b1dd327aatagen2026-10-08 11:27 UTC1046
    };
9c05b1dd327aatagen2026-10-08 11:27 UTC1047
    if !url.username().is_empty() || url.password().is_some() {
9c05b1dd327aatagen2026-10-08 11:27 UTC1048
        return invalid("it has a username in it");
9c05b1dd327aatagen2026-10-08 11:27 UTC1049
    }
9c05b1dd327aatagen2026-10-08 11:27 UTC1050
    let loopback = match url.host() {
9c05b1dd327aatagen2026-10-08 11:27 UTC1051
        None => return invalid("it has no host"),
9c05b1dd327aatagen2026-10-08 11:27 UTC1052
        Some(url::Host::Domain(domain)) => domain.eq_ignore_ascii_case("localhost"),
9c05b1dd327aatagen2026-10-08 11:27 UTC1053
        Some(url::Host::Ipv4(address)) => address.is_loopback(),
9c05b1dd327aatagen2026-10-08 11:27 UTC1054
        Some(url::Host::Ipv6(address)) => address.is_loopback(),
9c05b1dd327aatagen2026-10-08 11:27 UTC1055
    };
9c05b1dd327aatagen2026-10-08 11:27 UTC1056
    match url.scheme() {
9c05b1dd327aatagen2026-10-08 11:27 UTC1057
        "https" => Ok(input),
9c05b1dd327aatagen2026-10-08 11:27 UTC1058
        "http" if allow_insecure_loopback && loopback => Ok(input),
9c05b1dd327aatagen2026-10-08 11:27 UTC1059
        "http" if allow_insecure_loopback => invalid("plain http only reaches loopback hosts"),
9c05b1dd327aatagen2026-10-08 11:27 UTC1060
        "http" => invalid("it isn't https"),
9c05b1dd327aatagen2026-10-08 11:27 UTC1061
        _ => invalid("only http and https are spoken"),
9c05b1dd327aatagen2026-10-08 11:27 UTC1062
    }
9c05b1dd327aatagen2026-10-08 11:27 UTC1063
}
9c05b1dd327aatagen2026-10-08 11:27 UTC1064
713205a95af2atagen2026-09-26 09:24 UTC1065
pub(crate) fn account_error(
96fdf90aacd6atagen2026-09-08 08:24 UTC1066
    context: &'static str,
62fe32759529atagen2026-09-26 08:50 UTC1067
    source: impl std::error::Error + SendOutsideWasm + SyncOutsideWasm + 'static,
96fdf90aacd6atagen2026-09-08 08:24 UTC1068
) -> AccountError {
96fdf90aacd6atagen2026-09-08 08:24 UTC1069
    AccountError::Operation {
96fdf90aacd6atagen2026-09-08 08:24 UTC1070
        context,
9a25b6ce66f4atagen2026-10-08 11:15 UTC1071
        kind: crate::error::classify(&source),
9a25b6ce66f4atagen2026-10-08 11:15 UTC1072
        server: ServerError::find(&source),
96fdf90aacd6atagen2026-09-08 08:24 UTC1073
        source: Box::new(source),
96fdf90aacd6atagen2026-09-08 08:24 UTC1074
    }
96fdf90aacd6atagen2026-09-08 08:24 UTC1075
}