atagen

macltok

public
Star0
Watch0
9a25b6ce66f4d4430155c94da07fcc0e7b2f32b6
3
ssh://brewery@pls.lobotomise.me:33/atagen/macltok.git
src/account.rsRust706 lines23.2 KB
1
#[cfg(feature = "sqlite")]
2
use std::path::PathBuf;
3
#[cfg(not(target_family = "wasm"))]
4
use std::sync::atomic::{AtomicBool, Ordering};
5
use std::{collections::HashMap, sync::Arc};
6
7
use matrix_sdk::{
8
    Client, ClientBuilder, SendOutsideWasm, SessionChange, SessionMeta, SyncOutsideWasm,
9
    ThreadingSupport,
10
    authentication::{
11
        AuthSession,
12
        matrix::MatrixSession,
13
        oauth::{
14
            ClientId, ClientRegistrationData, CsrfToken, OAuthSession, UserSession,
15
            registration::{ApplicationType, ClientMetadata, Localized, OAuthGrantType},
16
        },
17
    },
18
    cross_process_lock::CrossProcessLockConfig,
19
    encryption::{BackupDownloadStrategy, EncryptionSettings},
20
    ruma::{OwnedDeviceId, OwnedUserId, serde::Raw},
21
};
22
use serde::{Deserialize, Serialize};
23
use thiserror::Error;
24
use tokio::sync::{broadcast, mpsc};
25
use url::Url;
26
27
use crate::{
28
    ClientHandle, Engine, EngineConfig, EngineError, ErrorKind, EventStream, ServerError,
29
    SessionStore,
30
    engine::{Persistence, StoreLock},
31
    error::SourceError,
32
};
33
34
/// a logged in client and the `SessionStore` its record lives in. the engine it builds
35
/// saves every token rotation there, unless told `without_persistence`
36
pub struct Session {
37
    client: Client,
38
    tracking: Tracking,
39
    store_lock: StoreLock,
40
}
41
42
/// taken as soon as the client exists, since the sdk can refresh before there's an engine
43
struct Tracking {
44
    session_changes: broadcast::Receiver<SessionChange>,
45
    persistence: Option<Persistence>,
46
    #[cfg(not(target_family = "wasm"))]
47
    inline: Arc<AtomicBool>,
48
}
49
50
#[non_exhaustive]
51
pub struct PasswordLogin {
52
    pub homeserver: String,
53
    pub username: String,
54
    pub password: String,
55
    pub device_display_name: Option<String>,
56
    /// set after a soft logout so the store's keys still belong to us
57
    pub device_id: Option<OwnedDeviceId>,
58
}
59
60
impl PasswordLogin {
61
    #[must_use]
62
    pub fn new(
63
        homeserver: impl Into<String>,
64
        username: impl Into<String>,
65
        password: impl Into<String>,
66
    ) -> Self {
67
        Self {
68
            homeserver: homeserver.into(),
69
            username: username.into(),
70
            password: password.into(),
71
            device_display_name: None,
72
            device_id: None,
73
        }
74
    }
75
76
    #[must_use]
77
    pub fn with_device_display_name(mut self, name: impl Into<String>) -> Self {
78
        self.device_display_name = Some(name.into());
79
        self
80
    }
81
82
    #[must_use]
83
    pub fn with_device_id(mut self, device_id: OwnedDeviceId) -> Self {
84
        self.device_id = Some(device_id);
85
        self
86
    }
87
}
88
89
#[non_exhaustive]
90
pub struct OAuthLogin {
91
    pub homeserver: String,
92
    pub redirect_uri: String,
93
    pub client_uri: String,
94
    pub client_name: Option<String>,
95
    pub client_id: Option<String>,
96
    pub device_id: Option<OwnedDeviceId>,
97
    pub application_type: OAuthApplicationType,
98
}
99
100
impl OAuthLogin {
101
    #[must_use]
102
    pub fn new(
103
        homeserver: impl Into<String>,
104
        redirect_uri: impl Into<String>,
105
        client_uri: impl Into<String>,
106
        application_type: OAuthApplicationType,
107
    ) -> Self {
108
        Self {
109
            homeserver: homeserver.into(),
110
            redirect_uri: redirect_uri.into(),
111
            client_uri: client_uri.into(),
112
            client_name: None,
113
            client_id: None,
114
            device_id: None,
115
            application_type,
116
        }
117
    }
118
119
    #[must_use]
120
    pub fn with_client_name(mut self, name: impl Into<String>) -> Self {
121
        self.client_name = Some(name.into());
122
        self
123
    }
124
125
    /// a statically registered client id, skipping dynamic registration
126
    #[must_use]
127
    pub fn with_client_id(mut self, client_id: impl Into<String>) -> Self {
128
        self.client_id = Some(client_id.into());
129
        self
130
    }
131
132
    #[must_use]
133
    pub fn with_device_id(mut self, device_id: OwnedDeviceId) -> Self {
134
        self.device_id = Some(device_id);
135
        self
136
    }
137
}
138
139
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
140
#[non_exhaustive]
141
pub enum OAuthApplicationType {
142
    Native,
143
    Web,
144
}
145
146
pub struct PendingOAuthLogin {
147
    client: Client,
148
    sessions: Arc<dyn SessionStore>,
149
    tracking: Option<(Tracking, StoreLock)>,
150
    authorization_url: String,
151
    state: Option<CsrfToken>,
152
}
153
154
impl PendingOAuthLogin {
155
    #[must_use]
156
    pub fn authorization_url(&self) -> &str {
157
        &self.authorization_url
158
    }
159
160
    #[must_use]
161
    pub fn registered_client_id(&self) -> String {
162
        self.client
163
            .oauth()
164
            .client_id()
165
            .expect("OAuth client was registered before authorization")
166
            .as_str()
167
            .to_owned()
168
    }
169
170
    #[must_use]
171
    pub const fn is_pending(&self) -> bool {
172
        self.state.is_some()
173
    }
174
175
    pub async fn finish(&mut self, callback_url: &str) -> Result<Session, AccountError> {
176
        let callback_url = Url::parse(callback_url)
177
            .map_err(|error| account_error("parsing the OAuth callback URL", error))?;
178
        let state = self
179
            .state
180
            .take()
181
            .ok_or(AccountError::OAuthCallbackAlreadySubmitted)?;
182
        let oauth = self.client.oauth();
183
        if let Err(error) = oauth.finish_login(callback_url.into()).await {
184
            oauth.abort_login(&state).await;
185
            return Err(account_error("OAuth login", error));
186
        }
187
        let (tracking, store_lock) = self
188
            .tracking
189
            .take()
190
            .ok_or(AccountError::OAuthCallbackAlreadySubmitted)?;
191
        Session::saved(
192
            self.client.clone(),
193
            self.sessions.clone(),
194
            tracking,
195
            store_lock,
196
        )
197
        .await
198
    }
199
200
    pub async fn abort(mut self) {
201
        if let Some(state) = self.state.take() {
202
            self.client.oauth().abort_login(&state).await;
203
        }
204
    }
205
}
206
207
pub struct StoreConfig {
208
    location: StoreLocation,
209
    passphrase: String,
210
}
211
212
enum StoreLocation {
213
    #[cfg(feature = "sqlite")]
214
    Sqlite(PathBuf),
215
    #[cfg(feature = "indexeddb")]
216
    IndexedDb(String),
217
}
218
219
impl StoreConfig {
220
    #[cfg(feature = "sqlite")]
221
    #[must_use]
222
    pub fn sqlite(path: impl Into<PathBuf>, passphrase: impl Into<String>) -> Self {
223
        Self {
224
            location: StoreLocation::Sqlite(path.into()),
225
            passphrase: passphrase.into(),
226
        }
227
    }
228
229
    #[cfg(feature = "indexeddb")]
230
    #[must_use]
231
    pub fn indexeddb(name: impl Into<String>, passphrase: impl Into<String>) -> Self {
232
        Self {
233
            location: StoreLocation::IndexedDb(name.into()),
234
            passphrase: passphrase.into(),
235
        }
236
    }
237
238
    /// delete the store's databases, once nothing has them open
239
    pub async fn delete(&self) -> Result<(), AccountError> {
240
        match &self.location {
241
            #[cfg(feature = "sqlite")]
242
            StoreLocation::Sqlite(path) => crate::session_store::delete_sqlite_store(path)
243
                .map_err(|error| account_error("deleting the Matrix store", error)),
244
            #[cfg(feature = "indexeddb")]
245
            StoreLocation::IndexedDb(name) => crate::session_store::delete_indexeddb_store(name)
246
                .await
247
                .map_err(|error| error.into_account_error("deleting the Matrix store")),
248
        }
249
    }
250
251
    /// taken before the client opens the store, and held until the engine and every
252
    /// `ClientHandle` are gone
253
    pub(crate) async fn lock(&self) -> Result<StoreLock, AccountError> {
254
        match &self.location {
255
            #[cfg(feature = "sqlite")]
256
            StoreLocation::Sqlite(path) => StoreLock::file(path),
257
            #[cfg(feature = "indexeddb")]
258
            StoreLocation::IndexedDb(name) => StoreLock::web(name).await,
259
        }
260
    }
261
262
    pub(crate) fn passphrase(&self) -> &str {
263
        &self.passphrase
264
    }
265
266
    #[cfg(feature = "indexeddb")]
267
    pub(crate) fn indexeddb_name(&self) -> &str {
268
        let StoreLocation::IndexedDb(name) = &self.location;
269
        name
270
    }
271
}
272
273
/// Serializable authentication secrets. A `SessionStore` keeps it, and the engine
274
/// replaces it there on every token rotation.
275
#[derive(Clone, Deserialize, Serialize)]
276
pub struct SessionRecord {
277
    homeserver: String,
278
    authentication: AuthenticationRecord,
279
}
280
281
#[derive(Clone, Deserialize, Serialize)]
282
#[serde(tag = "type", content = "session", rename_all = "snake_case")]
283
enum AuthenticationRecord {
284
    Matrix(MatrixSession),
285
    OAuth {
286
        client_id: ClientId,
287
        user: UserSession,
288
    },
289
    SoftLoggedOut {
290
        meta: SessionMeta,
291
        client_id: Option<ClientId>,
292
    },
293
}
294
295
impl SessionRecord {
296
    #[must_use]
297
    pub fn homeserver(&self) -> &str {
298
        &self.homeserver
299
    }
300
301
    pub(crate) fn soft_logged_out(self) -> Self {
302
        let authentication = match self.authentication {
303
            AuthenticationRecord::Matrix(session) => AuthenticationRecord::SoftLoggedOut {
304
                meta: session.meta,
305
                client_id: None,
306
            },
307
            AuthenticationRecord::OAuth { client_id, user } => {
308
                AuthenticationRecord::SoftLoggedOut {
309
                    meta: user.meta,
310
                    client_id: Some(client_id),
311
                }
312
            }
313
            soft @ AuthenticationRecord::SoftLoggedOut { .. } => soft,
314
        };
315
        Self {
316
            homeserver: self.homeserver,
317
            authentication,
318
        }
319
    }
320
}
321
322
pub enum Restore {
323
    Resumed(Session),
324
    SoftLoggedOut(SoftLogout),
325
}
326
327
/// who to log in again as. the store keeps this device's e2ee keys, so a login
328
/// as any other device would orphan them
329
#[derive(Clone, Debug)]
330
#[non_exhaustive]
331
pub struct SoftLogout {
332
    pub homeserver: String,
333
    pub user_id: OwnedUserId,
334
    pub device_id: OwnedDeviceId,
335
    /// set when the login was OAuth, for `OAuthLogin::with_client_id`
336
    pub client_id: Option<String>,
337
}
338
339
impl SoftLogout {
340
    #[must_use]
341
    pub fn password_login(&self, password: impl Into<String>) -> PasswordLogin {
342
        PasswordLogin::new(self.homeserver.clone(), self.user_id.as_str(), password)
343
            .with_device_id(self.device_id.clone())
344
    }
345
}
346
347
#[derive(Debug, Error)]
348
pub enum AccountError {
349
    #[error("{context} failed")]
350
    Operation {
351
        context: &'static str,
352
        kind: ErrorKind,
353
        server: Option<ServerError>,
354
        #[source]
355
        source: Box<SourceError>,
356
    },
357
    #[error("the homeserver returned no authenticated session")]
358
    MissingSession,
359
    #[error("the SDK returned an unsupported authentication session")]
360
    UnsupportedAuthentication,
361
    #[error("the OAuth callback was already submitted")]
362
    OAuthCallbackAlreadySubmitted,
363
    /// an IndexedDB delete is waiting on the store's other connections. it goes through
364
    /// once they close, and opening the store again waits behind it
365
    #[error("the store is still open elsewhere, it will be deleted once that closes")]
366
    StoreDeletePending,
367
    /// another engine, in this process or another, has the store. only one runs per store
368
    #[error("another engine is using this store")]
369
    StoreInUse,
370
    /// the login worked but its first record didn't save. the server already has the
371
    /// device, so retry the save rather than log in again
372
    #[error("saving the new login's record failed")]
373
    Unsaved {
374
        session: Box<UnsavedSession>,
375
        #[source]
376
        source: Box<AccountError>,
377
    },
378
    #[error(transparent)]
379
    Engine(#[from] EngineError),
380
}
381
382
impl AccountError {
383
    /// what the homeserver said, when it refused
384
    #[must_use]
385
    pub fn server(&self) -> Option<&ServerError> {
386
        match self {
387
            Self::Operation { server, .. } => server.as_ref(),
388
            Self::Unsaved { source, .. } => source.server(),
389
            _ => None,
390
        }
391
    }
392
}
393
394
/// a live login whose record never reached its `SessionStore`
395
pub struct UnsavedSession {
396
    session: Session,
397
    sessions: Arc<dyn SessionStore>,
398
}
399
400
impl UnsavedSession {
401
    pub async fn save(self) -> Result<Session, AccountError> {
402
        let saved = match session_record(&self.session.client) {
403
            Ok(record) => self.sessions.save(&record).await,
404
            Err(error) => Err(error),
405
        };
406
        match saved {
407
            Ok(()) => Ok(self.session),
408
            Err(source) => Err(AccountError::Unsaved {
409
                session: Box::new(self),
410
                source: Box::new(source),
411
            }),
412
        }
413
    }
414
415
    /// the record to keep somewhere else, if the store won't take it
416
    pub fn record(&self) -> Result<SessionRecord, AccountError> {
417
        session_record(&self.session.client)
418
    }
419
}
420
421
impl std::fmt::Debug for UnsavedSession {
422
    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
423
        formatter
424
            .debug_struct("UnsavedSession")
425
            .finish_non_exhaustive()
426
    }
427
}
428
429
impl Session {
430
    pub async fn login_password(
431
        login: PasswordLogin,
432
        sessions: Arc<dyn SessionStore>,
433
    ) -> Result<Self, AccountError> {
434
        let store_lock = sessions.store().lock().await?;
435
        let client = matrix_client_builder(sessions.store())
436
            .server_name_or_homeserver_url(login.homeserver.trim())
437
            .build()
438
            .await
439
            .map_err(|error| account_error("building the Matrix client", error))?;
440
        let tracking = Tracking::start(&client, sessions.clone())?;
441
        let mut request = client
442
            .matrix_auth()
443
            .login_username(login.username.trim(), &login.password)
444
            .request_refresh_token();
445
        if let Some(name) = login.device_display_name.as_deref() {
446
            request = request.initial_device_display_name(name);
447
        }
448
        if let Some(device_id) = login.device_id.as_deref() {
449
            request = request.device_id(device_id.as_str());
450
        }
451
        request
452
            .send()
453
            .await
454
            .map_err(|error| account_error("password login", error))?;
455
        Self::saved(client, sessions, tracking, store_lock).await
456
    }
457
458
    /// the pending login lives only in this process, so the one that begins it has to finish it
459
    pub async fn begin_oauth(
460
        login: OAuthLogin,
461
        sessions: Arc<dyn SessionStore>,
462
    ) -> Result<PendingOAuthLogin, AccountError> {
463
        let redirect_uri = Url::parse(login.redirect_uri.trim())
464
            .map_err(|error| account_error("parsing the OAuth redirect URI", error))?;
465
        let client_uri = Url::parse(login.client_uri.trim())
466
            .map_err(|error| account_error("parsing the OAuth client URI", error))?;
467
        let store_lock = sessions.store().lock().await?;
468
        let client = matrix_client_builder(sessions.store())
469
            .server_name_or_homeserver_url(login.homeserver.trim())
470
            .build()
471
            .await
472
            .map_err(|error| account_error("building the Matrix client", error))?;
473
        let tracking = Tracking::start(&client, sessions.clone())?;
474
        let application_type = match login.application_type {
475
            OAuthApplicationType::Native => ApplicationType::Native,
476
            OAuthApplicationType::Web => ApplicationType::Web,
477
        };
478
        let mut metadata = ClientMetadata::new(
479
            application_type,
480
            vec![OAuthGrantType::AuthorizationCode {
481
                redirect_uris: vec![redirect_uri.clone()],
482
            }],
483
            Localized::new(client_uri, []),
484
        );
485
        metadata.client_name = login.client_name.map(|name| Localized::new(name, []));
486
        let metadata = Raw::new(&metadata)
487
            .map_err(|error| account_error("serializing OAuth client metadata", error))?;
488
        let mut registration = ClientRegistrationData::new(metadata);
489
        if let Some(client_id) = login.client_id {
490
            registration.static_registrations = Some(HashMap::from([(
491
                client.homeserver(),
492
                ClientId::new(client_id),
493
            )]));
494
        }
495
        let authorization = client
496
            .oauth()
497
            .login(redirect_uri, login.device_id, Some(registration), None)
498
            .build()
499
            .await
500
            .map_err(|error| account_error("starting OAuth login", error))?;
501
        Ok(PendingOAuthLogin {
502
            client,
503
            sessions,
504
            tracking: Some((tracking, store_lock)),
505
            authorization_url: authorization.url.to_string(),
506
            state: Some(authorization.state),
507
        })
508
    }
509
510
    /// Restore the record `sessions` holds, or `None` when it holds none.
511
    /// The caller must also handle rejection and explicit logout.
512
    pub async fn restore(sessions: Arc<dyn SessionStore>) -> Result<Option<Restore>, AccountError> {
513
        let Some(record) = sessions.load().await? else {
514
            return Ok(None);
515
        };
516
        let authentication = match record.authentication {
517
            AuthenticationRecord::Matrix(session) => AuthSession::Matrix(session),
518
            AuthenticationRecord::OAuth { client_id, user } => {
519
                OAuthSession { client_id, user }.into()
520
            }
521
            AuthenticationRecord::SoftLoggedOut { meta, client_id } => {
522
                return Ok(Some(Restore::SoftLoggedOut(SoftLogout {
523
                    homeserver: record.homeserver,
524
                    user_id: meta.user_id,
525
                    device_id: meta.device_id,
526
                    client_id: client_id.map(|client_id| client_id.as_str().to_owned()),
527
                })));
528
            }
529
        };
530
        let store_lock = sessions.store().lock().await?;
531
        let client = matrix_client_builder(sessions.store())
532
            .homeserver_url(&record.homeserver)
533
            .build()
534
            .await
535
            .map_err(|error| account_error("reopening the Matrix store", error))?;
536
        let tracking = Tracking::start(&client, sessions)?;
537
        client
538
            .restore_session(authentication)
539
            .await
540
            .map_err(|error| account_error("restoring the authenticated session", error))?;
541
        Ok(Some(Restore::Resumed(Self {
542
            client,
543
            tracking,
544
            store_lock,
545
        })))
546
    }
547
548
    /// leave the record to the host, which then saves `ClientHandle::session_record()`
549
    /// on every `SessionTokensChanged`
550
    #[must_use]
551
    pub fn without_persistence(mut self) -> Self {
552
        self.tracking.persistence = None;
553
        #[cfg(not(target_family = "wasm"))]
554
        self.tracking.inline.store(false, Ordering::Release);
555
        self
556
    }
557
558
    pub async fn build(
559
        self,
560
        config: EngineConfig,
561
    ) -> Result<(Engine, ClientHandle, EventStream), AccountError> {
562
        Engine::with_persistence(
563
            self.client,
564
            config,
565
            self.tracking.session_changes,
566
            self.tracking.persistence,
567
            Some(self.store_lock),
568
        )
569
        .await
570
        .map_err(AccountError::from)
571
    }
572
573
    async fn saved(
574
        client: Client,
575
        sessions: Arc<dyn SessionStore>,
576
        tracking: Tracking,
577
        store_lock: StoreLock,
578
    ) -> Result<Self, AccountError> {
579
        UnsavedSession {
580
            session: Self {
581
                client,
582
                tracking,
583
                store_lock,
584
            },
585
            sessions,
586
        }
587
        .save()
588
        .await
589
    }
590
}
591
592
impl Tracking {
593
    fn start(client: &Client, sessions: Arc<dyn SessionStore>) -> Result<Self, AccountError> {
594
        let session_changes = client.subscribe_to_session_changes();
595
        #[cfg_attr(target_family = "wasm", allow(unused_variables))]
596
        let (results, inline_results) = mpsc::unbounded_channel();
597
        #[cfg(not(target_family = "wasm"))]
598
        let inline = save_inline(client, sessions.clone(), results)?;
599
        Ok(Self {
600
            session_changes,
601
            persistence: Some(Persistence {
602
                save: Box::new(move |client| {
603
                    let sessions = sessions.clone();
604
                    Box::pin(async move {
605
                        let record = session_record(&client).map_err(save_failed)?;
606
                        sessions.save(&record).await.map_err(save_failed)
607
                    })
608
                }),
609
                inline: inline_results,
610
            }),
611
            #[cfg(not(target_family = "wasm"))]
612
            inline,
613
        })
614
    }
615
}
616
617
/// the sdk calls this inside its refresh, after it has the new tokens and before it uses
618
/// them, so a store that writes there and then can't lose a refresh token to a crash
619
#[cfg(not(target_family = "wasm"))]
620
fn save_inline(
621
    client: &Client,
622
    sessions: Arc<dyn SessionStore>,
623
    results: mpsc::UnboundedSender<Result<(), crate::Error>>,
624
) -> Result<Arc<AtomicBool>, AccountError> {
625
    let enabled = Arc::new(AtomicBool::new(true));
626
    let saving = enabled.clone();
627
    client
628
        .set_session_callbacks(
629
            // only asked for with the cross process refresh lock, which we never enable
630
            Box::new(|client| {
631
                client
632
                    .session_tokens()
633
                    .ok_or_else(|| AccountError::MissingSession.into())
634
            }),
635
            Box::new(move |client| {
636
                if !saving.load(Ordering::Acquire) {
637
                    return Ok(());
638
                }
639
                let record = session_record(&client)?;
640
                if let Some(result) = sessions.save_blocking(&record) {
641
                    let _ = results.send(result.map_err(save_failed));
642
                }
643
                Ok(())
644
            }),
645
        )
646
        .map_err(|error| account_error("registering the session callbacks", error))?;
647
    Ok(enabled)
648
}
649
650
fn save_failed(error: AccountError) -> crate::Error {
651
    let message = format!("saving the session record failed, {error}");
652
    crate::Error::with_source(ErrorKind::Store, message, error)
653
}
654
655
pub(crate) fn session_record(client: &Client) -> Result<SessionRecord, AccountError> {
656
    let authentication = match client.session().ok_or(AccountError::MissingSession)? {
657
        AuthSession::Matrix(session) => AuthenticationRecord::Matrix(session),
658
        AuthSession::OAuth(session) => {
659
            let OAuthSession { client_id, user } = *session;
660
            AuthenticationRecord::OAuth { client_id, user }
661
        }
662
        _ => return Err(AccountError::UnsupportedAuthentication),
663
    };
664
    Ok(SessionRecord {
665
        homeserver: client.homeserver().to_string(),
666
        authentication,
667
    })
668
}
669
670
fn encryption_settings() -> EncryptionSettings {
671
    EncryptionSettings {
672
        auto_enable_cross_signing: true,
673
        backup_download_strategy: BackupDownloadStrategy::OneShot,
674
        auto_enable_backups: true,
675
    }
676
}
677
678
/// `StoreLock` already keeps every other client off the store, and the sdk's lease would cost
679
/// a store write per locked section and a task renewing it every 50ms
680
fn matrix_client_builder(store: &StoreConfig) -> ClientBuilder {
681
    let builder = Client::builder()
682
        .with_encryption_settings(encryption_settings())
683
        .with_threading_support(ThreadingSupport::Enabled {
684
            with_subscriptions: false,
685
        })
686
        .handle_refresh_tokens()
687
        .cross_process_store_config(CrossProcessLockConfig::SingleProcess);
688
    match &store.location {
689
        #[cfg(feature = "sqlite")]
690
        StoreLocation::Sqlite(path) => builder.sqlite_store(path, Some(&store.passphrase)),
691
        #[cfg(feature = "indexeddb")]
692
        StoreLocation::IndexedDb(name) => builder.indexeddb_store(name, Some(&store.passphrase)),
693
    }
694
}
695
696
pub(crate) fn account_error(
697
    context: &'static str,
698
    source: impl std::error::Error + SendOutsideWasm + SyncOutsideWasm + 'static,
699
) -> AccountError {
700
    AccountError::Operation {
701
        context,
702
        kind: crate::error::classify(&source),
703
        server: ServerError::find(&source),
704
        source: Box::new(source),
705
    }
706
}