atagen

macltok

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