atagen

macltok

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