Skip to main content

coven/
lib.rs

1//! coven — host integration for end-to-end encrypted, multi-writer,
2//! bring-your-own-storage SQLite sync. The engine lives in `coven-core`; this
3//! crate wires it to the filesystem, the platform keyring, and cloud
4//! providers, and re-exports the curated host API.
5//!
6//! The public API is exactly the crate-root re-exports below. The engine's
7//! implementation modules are `pub(crate)`, so a host reaches coven only through
8//! these names — never through `coven::sync::…` or `coven::blob::…`. In
9//! particular the sync driver is private: it starts only through
10//! [`CovenHandle::connect_sync`], which holds the lifecycle lock, so a host
11//! cannot drive the loop out from under the handle.
12//!
13//! ```compile_fail
14//! // `sync` is a private module; the sync driver is unreachable from outside.
15//! let _ = coven::sync::sync_manager::SyncManager::start_sync;
16//! ```
17
18pub(crate) mod custody;
19pub(crate) mod envelope;
20pub(crate) mod identity_custody;
21pub(crate) mod keys;
22pub(crate) mod oauth;
23
24pub(crate) mod blob {
25    pub(crate) use coven_core::blob::*;
26    pub(crate) mod transition {
27        pub use coven_core::blob::transition::*;
28    }
29}
30
31pub(crate) mod clock {
32    pub(crate) use coven_core::clock::*;
33}
34
35pub(crate) mod config {
36    pub(crate) use coven_core::config::*;
37}
38
39pub(crate) mod database {
40    pub(crate) use coven_core::database::*;
41}
42
43pub(crate) mod encryption {
44    pub(crate) use coven_core::encryption::*;
45}
46
47pub(crate) mod id_provider {
48    pub(crate) use coven_core::id_provider::*;
49}
50
51pub(crate) mod join_code {
52    pub(crate) use coven_core::join_code::*;
53
54    /// Build a join request carrying a freshly minted pending identity (see
55    /// [`crate::keys::mint_pending_identity`]) — the joiner sends its public
56    /// key before it learns which store the invite names, so the keypair is
57    /// generated now and held under a pending slot keyed by that public key.
58    /// Completing the join with [`crate::DeviceJoinClient`] (constructed with
59    /// this same code) promotes it into the joined store's own identity;
60    /// [`crate::abandon_join_request`] discards it if the request is
61    /// abandoned instead.
62    pub fn generate_join_request(email: Option<String>) -> Result<String, crate::keys::KeyError> {
63        let keypair = crate::keys::mint_pending_identity()?;
64        Ok(coven_core::join_code::generate_join_request_for_keypair(
65            &keypair, email,
66        ))
67    }
68
69    /// Abandon a join request this device generated but never completed —
70    /// removes the pending identity [`generate_join_request`] minted for it.
71    /// `Ok` whether or not one was still pending.
72    pub fn abandon_join_request(request_code: &str) -> Result<(), crate::keys::KeyError> {
73        let request = decode_join_request(request_code)
74            .map_err(|e| crate::keys::KeyError::Crypto(e.to_string()))?;
75        crate::keys::discard_pending_identity(&request.public_key)
76    }
77}
78
79/// Fetch the email of the account `tokens` authenticated, for the given OAuth
80/// provider. The joining device calls this right after authenticating so the
81/// approver can share the OAuth folder to its provider-account email.
82///
83/// Only the OAuth providers are valid here; a non-OAuth provider (S3, CloudKit)
84/// is a programming error and surfaces as an error rather than a silent default.
85#[cfg(feature = "oauth-providers")]
86pub async fn fetch_account_email(
87    provider: crate::config::CloudProvider,
88    tokens: &oauth::OAuthTokens,
89) -> Result<String, oauth::OAuthError> {
90    use crate::config::CloudProvider;
91    use crate::storage::cloud::account_email;
92
93    let result = match provider {
94        CloudProvider::GoogleDrive => account_email::fetch_google(tokens).await,
95        CloudProvider::Dropbox => account_email::fetch_dropbox(tokens).await,
96        CloudProvider::OneDrive => account_email::fetch_onedrive(tokens).await,
97        other => {
98            return Err(oauth::OAuthError::AccountFetch(format!(
99                "{other:?} does not use OAuth; account email is only fetched for OAuth providers"
100            )))
101        }
102    };
103    result.map_err(|e| oauth::OAuthError::AccountFetch(e.to_string()))
104}
105
106pub(crate) mod store_dir {
107    pub(crate) use coven_core::store_dir::*;
108}
109
110pub(crate) mod local_blob {
111    pub(crate) use coven_core::local_blob::*;
112}
113
114pub(crate) mod migration {
115    pub(crate) use coven_core::migration::*;
116}
117
118pub(crate) mod storage {
119    pub(crate) mod cloud {
120        pub(crate) use coven_core::storage::cloud::*;
121
122        pub(crate) mod s3_common {
123            pub(crate) use coven_core::storage::cloud::s3_common::*;
124        }
125
126        #[cfg(feature = "oauth-providers")]
127        pub(crate) mod account_email;
128        pub(crate) mod cloudkit;
129        #[cfg(feature = "oauth-providers")]
130        pub(crate) mod dropbox;
131        #[cfg(feature = "oauth-providers")]
132        pub(crate) mod google_drive;
133        #[cfg(feature = "oauth-providers")]
134        mod http;
135        #[cfg(feature = "oauth-providers")]
136        mod key_encoding;
137        #[cfg(feature = "oauth-providers")]
138        mod oauth_rest;
139        #[cfg(feature = "oauth-providers")]
140        pub(crate) mod oauth_session;
141        #[cfg(feature = "oauth-providers")]
142        pub(crate) mod onedrive;
143        #[cfg(feature = "oauth-providers")]
144        mod resumable;
145        pub(crate) mod s3;
146        pub(crate) mod setup;
147        #[cfg(feature = "oauth-providers")]
148        mod sharing;
149
150        #[cfg(feature = "oauth-providers")]
151        fn require_oauth_token(
152            key_service: &crate::keys::StoreKeys,
153            provider_name: &str,
154        ) -> Result<String, CloudHomeError> {
155            match key_service.get_cloud_home_credentials().map_err(|e| {
156                CloudHomeError::Configuration(format!("{provider_name} credentials error: {e}"))
157            })? {
158                Some(crate::keys::CloudHomeCredentials::OAuth { token_json }) => Ok(token_json),
159                _ => Err(CloudHomeError::Configuration(format!(
160                    "{provider_name} OAuth token not in keyring"
161                ))),
162            }
163        }
164
165        #[cfg(feature = "oauth-providers")]
166        fn parse_oauth_tokens(
167            key_service: &crate::keys::StoreKeys,
168            provider_name: &str,
169        ) -> Result<crate::oauth::OAuthTokens, CloudHomeError> {
170            let token_json = require_oauth_token(key_service, provider_name)?;
171            serde_json::from_str(&token_json).map_err(|e| {
172                CloudHomeError::Configuration(format!("invalid OAuth token JSON: {e}"))
173            })
174        }
175
176        /// Build a [`CloudHome`] from `config`, surfacing a missing or malformed
177        /// provider configuration as a non-retryable
178        /// [`CloudHomeError::Configuration`] so a host can tell "fix your settings"
179        /// apart from a transient failure it should keep retrying.
180        pub async fn create_cloud_home(
181            config: &crate::config::Config,
182            key_service: &crate::keys::StoreKeys,
183            clock: crate::clock::ClockRef,
184        ) -> Result<Box<dyn CloudHome>, CloudHomeError> {
185            create_cloud_home_with_cloudkit(config, key_service, clock, None).await
186        }
187
188        pub(crate) async fn create_cloud_home_with_cloudkit(
189            config: &crate::config::Config,
190            key_service: &crate::keys::StoreKeys,
191            clock: crate::clock::ClockRef,
192            cloudkit_ops: Option<std::sync::Arc<dyn cloudkit::CloudKitOps>>,
193        ) -> Result<Box<dyn CloudHome>, CloudHomeError> {
194            use crate::config::CloudProvider;
195
196            #[cfg(not(feature = "oauth-providers"))]
197            let _ = &clock;
198
199            match config.cloud_home.provider {
200                Some(CloudProvider::S3) | None => {
201                    let bucket = config.cloud_home.s3_bucket.clone().ok_or_else(|| {
202                        CloudHomeError::Configuration("S3 bucket not configured".to_string())
203                    })?;
204                    let region = config.cloud_home.s3_region.clone().ok_or_else(|| {
205                        CloudHomeError::Configuration("S3 region not configured".to_string())
206                    })?;
207                    let endpoint = config.cloud_home.s3_endpoint.clone();
208
209                    let (access_key, secret_key) =
210                        match key_service.get_cloud_home_credentials().map_err(|e| {
211                            CloudHomeError::Configuration(format!("S3 credentials error: {e}"))
212                        })? {
213                            Some(crate::keys::CloudHomeCredentials::S3 {
214                                access_key,
215                                secret_key,
216                            }) => (access_key, secret_key),
217                            _ => {
218                                return Err(CloudHomeError::Configuration(
219                                    "S3 credentials not in keyring".to_string(),
220                                ))
221                            }
222                        };
223
224                    let s3 = s3::S3CloudHome::new(
225                        bucket,
226                        region,
227                        endpoint,
228                        access_key,
229                        secret_key,
230                        config.cloud_home.s3_key_prefix.clone(),
231                        config.cloud_home.s3_exact_slots,
232                    )
233                    .await?;
234                    Ok(Box::new(s3))
235                }
236                #[cfg(feature = "oauth-providers")]
237                Some(CloudProvider::GoogleDrive) => {
238                    let folder_id = config
239                        .cloud_home
240                        .google_drive_folder_id
241                        .clone()
242                        .ok_or_else(|| {
243                            CloudHomeError::Configuration(
244                                "Google Drive folder ID not configured".to_string(),
245                            )
246                        })?;
247                    let tokens = parse_oauth_tokens(key_service, "Google Drive")?;
248                    Ok(Box::new(google_drive::GoogleDriveCloudHome::new(
249                        folder_id,
250                        tokens,
251                        key_service.clone(),
252                        clock,
253                    )?))
254                }
255                #[cfg(feature = "oauth-providers")]
256                Some(CloudProvider::Dropbox) => {
257                    let folder_path =
258                        config
259                            .cloud_home
260                            .dropbox_folder_path
261                            .clone()
262                            .ok_or_else(|| {
263                                CloudHomeError::Configuration(
264                                    "Dropbox folder path not configured".to_string(),
265                                )
266                            })?;
267                    let tokens = parse_oauth_tokens(key_service, "Dropbox")?;
268                    Ok(Box::new(dropbox::DropboxCloudHome::new(
269                        folder_path,
270                        tokens,
271                        key_service.clone(),
272                        clock,
273                    )?))
274                }
275                #[cfg(feature = "oauth-providers")]
276                Some(CloudProvider::OneDrive) => {
277                    let drive_id =
278                        config.cloud_home.onedrive_drive_id.clone().ok_or_else(|| {
279                            CloudHomeError::Configuration(
280                                "OneDrive drive ID not configured".to_string(),
281                            )
282                        })?;
283                    let folder_id =
284                        config
285                            .cloud_home
286                            .onedrive_folder_id
287                            .clone()
288                            .ok_or_else(|| {
289                                CloudHomeError::Configuration(
290                                    "OneDrive folder ID not configured".to_string(),
291                                )
292                            })?;
293                    let tokens = parse_oauth_tokens(key_service, "OneDrive")?;
294                    Ok(Box::new(onedrive::OneDriveCloudHome::new(
295                        drive_id,
296                        folder_id,
297                        tokens,
298                        key_service.clone(),
299                        clock,
300                    )?))
301                }
302                #[cfg(not(feature = "oauth-providers"))]
303                Some(
304                    CloudProvider::GoogleDrive | CloudProvider::Dropbox | CloudProvider::OneDrive,
305                ) => Err(CloudHomeError::Configuration(
306                    "OAuth cloud providers are not supported in this build".to_string(),
307                )),
308                Some(CloudProvider::CloudKit) => {
309                    let ops = cloudkit_ops.ok_or_else(|| {
310                        CloudHomeError::Configuration("CloudKit driver not provided".to_string())
311                    })?;
312                    match (
313                        config.cloud_home.cloudkit_owner_name.as_ref(),
314                        config.cloud_home.cloudkit_zone_name.as_ref(),
315                    ) {
316                        (None, None) => {
317                            Ok(Box::new(cloudkit::CloudKitCloudHome::new_private(ops)))
318                        }
319                        (Some(owner_name), Some(zone_name)) => {
320                            Ok(Box::new(cloudkit::CloudKitCloudHome::new_shared(
321                                ops,
322                                owner_name.clone(),
323                                zone_name.clone(),
324                            )))
325                        }
326                        _ => Err(CloudHomeError::Configuration(
327                            "CloudKit share config requires both cloudkit_owner_name and cloudkit_zone_name"
328                                .to_string(),
329                        )),
330                    }
331                }
332            }
333        }
334
335        #[cfg(test)]
336        mod tests {
337            use super::*;
338            use crate::clock::FixedClock;
339            use crate::config::{CloudProvider, Config, HomeStorage};
340            use crate::keys::StoreKeys;
341            use crate::storage::cloud::cloudkit::{
342                CloudKitAcceptedShareRecord, CloudKitAtomicCreateBatch, CloudKitOps,
343                CloudKitProviderIdentity, CloudKitRecordCreate, CloudKitRecordVersion,
344                CloudKitScope, CloudKitShare,
345            };
346            use crate::store_dir::StoreDir;
347            use std::sync::Mutex;
348
349            /// Records the scope each `list_records` call carried, so a test can
350            /// tell whether `create_cloud_home_with_cloudkit` built a private or a
351            /// shared home without reaching into its private fields. Every other
352            /// method is unused by these tests and panics if called.
353            struct ScopeRecordingOps {
354                seen: Mutex<Vec<CloudKitScope>>,
355            }
356
357            impl ScopeRecordingOps {
358                fn new() -> Self {
359                    Self {
360                        seen: Mutex::new(Vec::new()),
361                    }
362                }
363            }
364
365            impl CloudKitOps for ScopeRecordingOps {
366                fn provider_identity(
367                    &self,
368                    scope: &CloudKitScope,
369                ) -> Result<CloudKitProviderIdentity, CloudHomeError> {
370                    let (owner_name, zone_name) = match scope {
371                        CloudKitScope::Private => ("test-owner", "test-zone"),
372                        CloudKitScope::Shared {
373                            owner_name,
374                            zone_name,
375                        } => (owner_name.as_str(), zone_name.as_str()),
376                    };
377                    Ok(CloudKitProviderIdentity {
378                        container_id: "iCloud.test.coven".to_string(),
379                        environment: crate::CloudKitEnvironment::Development,
380                        owner_name: owner_name.to_string(),
381                        zone_name: zone_name.to_string(),
382                        current_user_record_name: "test-user".to_string(),
383                    })
384                }
385
386                fn accepted_read_write_share(
387                    &self,
388                    _scope: &CloudKitScope,
389                ) -> Result<CloudKitAcceptedShareRecord, CloudHomeError> {
390                    Err(CloudHomeError::NotFound(
391                        "accepted CloudKit share".to_string(),
392                    ))
393                }
394
395                fn write_record(
396                    &self,
397                    _scope: &CloudKitScope,
398                    _key: &str,
399                    _data: Vec<u8>,
400                ) -> Result<(), CloudHomeError> {
401                    unimplemented!("not exercised by these tests")
402                }
403                fn read_record(
404                    &self,
405                    _scope: &CloudKitScope,
406                    _key: &str,
407                ) -> Result<Vec<u8>, CloudHomeError> {
408                    unimplemented!("not exercised by these tests")
409                }
410                fn list_records(
411                    &self,
412                    scope: &CloudKitScope,
413                    _prefix: &str,
414                ) -> Result<Vec<String>, CloudHomeError> {
415                    self.seen.lock().unwrap().push(scope.clone());
416                    Ok(Vec::new())
417                }
418                fn delete_record(
419                    &self,
420                    _scope: &CloudKitScope,
421                    _key: &str,
422                ) -> Result<(), CloudHomeError> {
423                    unimplemented!("not exercised by these tests")
424                }
425                fn record_exists(
426                    &self,
427                    _scope: &CloudKitScope,
428                    _key: &str,
429                ) -> Result<bool, CloudHomeError> {
430                    unimplemented!("not exercised by these tests")
431                }
432                fn read_versioned_record(
433                    &self,
434                    _scope: &CloudKitScope,
435                    _key: &str,
436                ) -> Result<crate::storage::cloud::CloudVersionedObject, CloudHomeError>
437                {
438                    unimplemented!("not exercised by these tests")
439                }
440
441                fn begin_atomic_create(
442                    &self,
443                    _scope: &CloudKitScope,
444                ) -> Result<CloudKitAtomicCreateBatch, CloudHomeError> {
445                    unimplemented!("not exercised by these tests")
446                }
447                fn stage_atomic_create_record(
448                    &self,
449                    _scope: &CloudKitScope,
450                    _batch: &CloudKitAtomicCreateBatch,
451                    _record: CloudKitRecordCreate,
452                ) -> Result<(), CloudHomeError> {
453                    unimplemented!("not exercised by these tests")
454                }
455                fn commit_atomic_create(
456                    &self,
457                    _scope: &CloudKitScope,
458                    _batch: &CloudKitAtomicCreateBatch,
459                ) -> Result<Vec<CloudKitRecordVersion>, CloudHomeError> {
460                    unimplemented!("not exercised by these tests")
461                }
462                fn discard_atomic_create(
463                    &self,
464                    _scope: &CloudKitScope,
465                    _batch: &CloudKitAtomicCreateBatch,
466                ) -> Result<(), CloudHomeError> {
467                    unimplemented!("not exercised by these tests")
468                }
469                fn delete_record_versions(
470                    &self,
471                    _scope: &CloudKitScope,
472                    _records: &[CloudKitRecordVersion],
473                ) -> Result<(), CloudHomeError> {
474                    unimplemented!("not exercised by these tests")
475                }
476                fn grant_share(
477                    &self,
478                    _member_pubkey: &str,
479                ) -> Result<CloudKitShare, CloudHomeError> {
480                    unimplemented!("not exercised by these tests")
481                }
482                fn share_for_member(
483                    &self,
484                    _member_pubkey: &str,
485                ) -> Result<Option<CloudKitShare>, CloudHomeError> {
486                    unimplemented!("not exercised by these tests")
487                }
488                fn revoke_share(&self, _member_pubkey: &str) -> Result<(), CloudHomeError> {
489                    unimplemented!("not exercised by these tests")
490                }
491                fn accept_share(&self, _share_url: &str) -> Result<CloudKitShare, CloudHomeError> {
492                    unimplemented!("not exercised by these tests")
493                }
494            }
495
496            fn cloudkit_config(owner_zone: Option<(&str, &str)>) -> Config {
497                let mut config = Config::with_defaults(
498                    "store-1".to_string(),
499                    "device-1".to_string(),
500                    StoreDir::new("unused-store-dir"),
501                    "CloudKit Store".to_string(),
502                );
503                config.cloud_home.provider = Some(CloudProvider::CloudKit);
504                config.cloud_home.storage = HomeStorage::Opaque;
505                if let Some((owner, zone)) = owner_zone {
506                    config.cloud_home.cloudkit_owner_name = Some(owner.to_string());
507                    config.cloud_home.cloudkit_zone_name = Some(zone.to_string());
508                }
509                config
510            }
511
512            /// Neither `cloudkit_owner_name` nor `cloudkit_zone_name` set builds a
513            /// private home — the scope every subsequent op sees is `Private`.
514            #[tokio::test]
515            async fn neither_owner_nor_zone_builds_a_private_home() {
516                let config = cloudkit_config(None);
517                let key_service = StoreKeys::new(config.store_id.clone());
518                let ops = std::sync::Arc::new(ScopeRecordingOps::new());
519                let clock: crate::clock::ClockRef =
520                    std::sync::Arc::new(FixedClock(chrono::Utc::now()));
521
522                let home = create_cloud_home_with_cloudkit(
523                    &config,
524                    &key_service,
525                    clock,
526                    Some(ops.clone()),
527                )
528                .await
529                .expect("private CloudKit config builds a home");
530                home.list("").await.expect("list against the built home");
531
532                assert_eq!(
533                    ops.seen.lock().unwrap().as_slice(),
534                    [CloudKitScope::Private]
535                );
536            }
537
538            /// Both `cloudkit_owner_name` and `cloudkit_zone_name` set builds a
539            /// shared home scoped to that owner/zone pair.
540            #[tokio::test]
541            async fn both_owner_and_zone_build_a_shared_home() {
542                let config = cloudkit_config(Some(("owner-name", "zone-name")));
543                let key_service = StoreKeys::new(config.store_id.clone());
544                let ops = std::sync::Arc::new(ScopeRecordingOps::new());
545                let clock: crate::clock::ClockRef =
546                    std::sync::Arc::new(FixedClock(chrono::Utc::now()));
547
548                let home = create_cloud_home_with_cloudkit(
549                    &config,
550                    &key_service,
551                    clock,
552                    Some(ops.clone()),
553                )
554                .await
555                .expect("shared CloudKit config builds a home");
556                home.list("").await.expect("list against the built home");
557
558                assert_eq!(
559                    ops.seen.lock().unwrap().as_slice(),
560                    [CloudKitScope::Shared {
561                        owner_name: "owner-name".to_string(),
562                        zone_name: "zone-name".to_string(),
563                    }]
564                );
565            }
566
567            /// Only one of `cloudkit_owner_name` / `cloudkit_zone_name` set is the
568            /// broken shape the config module can't rule out by construction (see
569            /// the module doc: config stays flat, provider-prefixed fields rather
570            /// than one enum for CloudKit alone) — surfaced as a `Configuration`
571            /// error naming both fields, not a silent guess at which home to build.
572            #[tokio::test]
573            async fn mixed_owner_zone_is_a_configuration_error() {
574                let mut config = cloudkit_config(None);
575                config.cloud_home.cloudkit_owner_name = Some("owner-name".to_string());
576                let key_service = StoreKeys::new(config.store_id.clone());
577                let ops = std::sync::Arc::new(ScopeRecordingOps::new());
578                let clock: crate::clock::ClockRef =
579                    std::sync::Arc::new(FixedClock(chrono::Utc::now()));
580
581                let result =
582                    create_cloud_home_with_cloudkit(&config, &key_service, clock, Some(ops)).await;
583                match result {
584                    Ok(_) => panic!("mixed owner/zone must not build a home"),
585                    Err(CloudHomeError::Configuration(message)) => {
586                        assert!(message.contains("cloudkit_owner_name"), "{message}");
587                        assert!(message.contains("cloudkit_zone_name"), "{message}");
588                    }
589                    Err(other) => panic!("expected Configuration error, got {other:?}"),
590                }
591            }
592        }
593    }
594
595    pub(crate) mod local;
596}
597
598pub(crate) mod sync {
599    pub(crate) use coven_core::sync::*;
600
601    pub(crate) mod device_join_transport;
602    #[cfg(test)]
603    mod device_join_transport_tests;
604    pub(crate) mod join;
605    #[cfg(test)]
606    mod join_tests;
607    pub(crate) mod restore;
608    #[cfg(test)]
609    mod restore_tests;
610    pub(crate) mod sync_loop;
611    pub(crate) mod sync_manager;
612}
613
614mod circles;
615mod coven;
616mod handle;
617mod keyring_backend;
618mod read_handle;
619
620// coven's public API is exactly the crate-root re-exports below. The
621// implementation modules are `pub(crate)`; a host reaches coven only through
622// these names, never through `coven::sync::…` or `coven::blob::…`.
623
624pub use coven::{
625    Coven, CovenBuilder, CovenConfig, CovenError, CovenResult, SqlContext, WriteBatch,
626};
627pub use handle::CovenHandle;
628pub use read_handle::CovenReadHandle;
629
630// --- coven-core's curated engine surface, re-exported so a host names it as
631//     `coven::…` and never depends on `coven-core` directly. ---
632
633/// The exact `rusqlite` coven owns the connection through; see [`CovenHandle::sql`].
634pub use coven_core::rusqlite;
635
636// Host schema declaration: the synced-table set and the synced-schema migration ladder.
637pub use coven_core::{BlobDecl, Migration, MigrationStep, RowIdentity, SyncedTable};
638
639// Config.
640pub use coven_core::sync::storage::CloudKitEnvironment;
641pub use coven_core::{
642    CloudHomeConfig, CloudProvider, Config, ConfigError, CustomS3ExactSlots, HomeStorage,
643};
644
645// Blob descriptors, cache error, the host-implemented transition observer.
646pub use coven_core::{
647    BlobCacheError, BlobRef, BlobReplacement, BlobScope, BlobTransitionObserver, CacheFill,
648    Provenance, RowBlobAuthority, RowBlobRef,
649};
650
651// Applied-sync change notification.
652pub use coven_core::{ChangeOp, RowChange};
653
654// At-rest crypto the host configures (the host sizes cloud stream reads from
655// `CHUNK_SIZE`), and the store directory and its host-configurable layout,
656// the DB error. `MasterKeyring` is the master-key custody value type — the
657// payload `KeyCustody::InMemory` takes and `import_master_key`/
658// `initialize_master_key` traffic in internally. `SealError` is what
659// `CovenHandle::seal_app_data` / `open_app_data` return.
660pub use coven_core::{
661    DbError, EncryptionError, MasterKeyring, SealError, StoreDir, StoreLayout, CHUNK_SIZE,
662};
663
664// `EncryptionService` is the cipher coven builds internally from whatever
665// custody supplies; a production host never constructs one. It stays
666// reachable for host integration tests, which build a `CloudCipher` directly
667// to drive `CovenHandle::connect_sync_with_test_home`.
668#[cfg(any(test, feature = "test-utils"))]
669pub use coven_core::EncryptionService;
670
671// The register clock vocabulary carried on every synced row.
672pub use coven_core::{Hlc, Timestamp, UpdatedAtStamper};
673
674// Membership. `MembershipCoord` (an author's membership-head coordinate) is
675// exposed only because `generate_restore_code` takes a caller-supplied
676// membership floor made of them; a host driving that free function directly
677// (bypassing `CovenHandle`) must be able to name the type.
678pub use circles::{CircleError, Circles};
679pub use coven_core::sync::membership::MembershipCoord;
680pub use coven_core::sync::store::{
681    DeviceJoinAbandonment, DeviceJoinAction, DeviceJoinActivation, DeviceJoinCancellation,
682    DeviceJoinCleanupActivation, DeviceJoinCleanupProgress, DeviceJoinCleanupReceipt,
683    DeviceJoinError, DeviceJoinJournalDatabase, DeviceJoinJournalRecord, DeviceJoinOffer,
684    DeviceJoinProducer, DeviceJoinProducerWriteRevocation, DeviceJoinReadiness, DeviceJoinRole,
685    DeviceJoinStatus, DeviceJoinWriteRevocationExecutor, DeviceProviderAccessAdministrator,
686    DeviceProviderAccessRequest, DeviceProviderAdmission, DeviceProviderAdmissionApproval,
687    DeviceProviderAdmissionCompletion, DeviceProviderReadiness, DeviceRegistrationRequest,
688    JoinedStore, JoinerJoinClosure, JoinerJoinTerminal, ProviderAdminJoinClosure,
689    ProviderAdminJoinTerminal, ProviderReadyDeviceBootstrap, ProviderWriteAuthorityRef,
690    ProvisionalDeviceBootstrap,
691};
692// The storage-mediated device-join transport: the offer bundle a host encodes
693// as its join code, the slot namespace it names, and the two role drivers.
694pub use coven_core::sync::store::{
695    abandon_device_join_via_transport, cancel_device_join_via_transport, drive_device_join,
696    DeviceJoinApproval, DeviceJoinApprovalPolicy, DeviceJoinDriveOutcome, DeviceJoinOfferBundle,
697    DeviceJoinRoles, DeviceJoinStep, DeviceJoinTransport, DeviceJoinTransportError,
698    DeviceJoinTransportKind, DeviceJoinTransportParams, DeviceJoinTransportTiming,
699};
700pub use coven_core::sync::store_commit::{DeviceJoinAttemptId, DeviceJoinAttemptRef};
701pub use coven_core::{
702    Audience, Circle, CircleCloseParticipant, CircleCloseSettlement, CircleCloseStatus,
703    CircleControlCoord, CircleEpochCloseId, CircleId, CircleMemberInfo, CircleOperationBlock,
704    CircleOperationId, CircleOperationInfo, CircleOperationKind, CircleOperationState, CircleRole,
705    CircleState, StoreDeviceId,
706};
707pub use coven_core::{MemberInfo, MemberRole, MembershipConflictChoice, MembershipConflictInfo};
708
709// Clock / id abstractions the host injects, plus the deterministic test fakes.
710pub use coven_core::{Clock, ClockRef, IdProvider, IdRef, SystemClock, UuidProvider};
711#[cfg(any(test, feature = "test-utils"))]
712pub use coven_core::{FixedClock, SequentialIdProvider, SteppingClock};
713
714// Bootstrap decoders.
715pub use coven_core::{
716    decode_invite_code_info, decode_join_request, decode_restore_code_info, JoinCodeError,
717};
718
719// The cloud at-rest cipher. coven resolves it from custody internally; a
720// production host never names it. Host integration tests build one directly
721// to drive `CovenHandle::connect_sync_with_test_home`.
722#[cfg(any(test, feature = "test-utils"))]
723pub use coven_core::CloudCipher;
724
725// Cloud provider trait surface a provider implementor needs and its
726// thread-safety floor.
727pub use coven_core::storage::cloud::{
728    write_cloud_object_stream, CloudFileReadError, CloudObjectStream, ExactSlotStorage, ObjectSlot,
729    PhysicalObjectLocator,
730};
731pub use coven_core::sync::provider::{
732    CloudKitAcceptedShare, CrossPrincipalProbeReceipt, ExactSlotProbeReceipt,
733    ProviderAccessLocator, ProviderAccessWithdrawal, ProviderAdminChange, ProviderAdminGrantId,
734    ProviderAdminGrantRecord, ProviderAdminMembershipChange, ProviderAdminState,
735    ProviderCapabilityProof, ProviderProbeId,
736};
737pub use coven_core::sync::storage::{
738    AwsPrincipal, GoogleDriveCorpus, ProviderDeviceBinding, ProviderPrincipalId,
739    ResolvedProviderBinding, S3EndpointBinding, StoreProviderBinding,
740};
741pub use coven_core::{
742    BlobBody, BoxPartSink, CloudAccessOutcome, CloudAccessState, CloudHome, CloudHomeError,
743    CloudHomeJoinInfo, CloudObjectVersion, CloudVersionedObject, PartSink, UploadProgress,
744};
745
746// Sync-status surface a host renders from `CovenHandle::subscribe_sync_status`:
747// the status enum, its completed-cycle success payload, the per-cycle alert
748// bundle, the per-device activity, and the held-changeset detail the alerts carry.
749pub use coven_core::{
750    AffectedRow, DeviceActivity, HeldStoreCoordinate, HeldStorePosition, HeldStorePositionReason,
751    ObjectHash, PendingWrite, PublishedPosition, StoreBatchCommitRef, StoreCommitCoord,
752    SyncLoopAlerts, SyncLoopSuccess, WriteBlock, WriteId, WriteReceipt, WriteResolution,
753    WriteStatus,
754};
755pub use sync::sync_loop::SyncLoopStatus;
756
757// In-memory cloud home and durable upload-queue rows for host integration tests.
758#[cfg(any(test, feature = "test-utils"))]
759pub use coven_core::InMemoryCloudHome;
760
761pub use blob::transition::{MakeLocalError, MakeRemoteError};
762pub use custody::{rewrap_passphrase_custody, KeyCustody, Passphrase};
763pub use identity_custody::{rewrap_passphrase_identity_custody, IdentityCustody};
764pub use join_code::{abandon_join_request, generate_join_request};
765pub use keys::{
766    keyring_service, set_keyring_service, CloudHomeCredentials, DeviceIdentityCustody,
767    IdentityError, KeyError, MasterKeyCustody, MasterKeyError, StoreKeys, UserKeypair,
768};
769
770// The sole keyring entry-construction chokepoint (`keys::entry_for`), reached
771// across the crate boundary so an integration test can install a specific
772// keyring store and assert which construction path it took, without
773// re-implementing that dispatch. A production host never calls this.
774#[cfg(any(test, feature = "test-utils"))]
775pub use keys::entry_for_test;
776
777pub use oauth::{set_oauth_client_creds, OAuthClientCreds, OAuthClientCredsConflict, OAuthTokens};
778pub use storage::cloud::setup::generate_restore_code;
779pub use storage::cloud::{
780    cloudkit::{
781        CloudKitAcceptedShareRecord, CloudKitAtomicCreateBatch, CloudKitOps,
782        CloudKitProviderIdentity, CloudKitRecordCreate, CloudKitRecordVersion, CloudKitScope,
783        CloudKitShare, CloudKitShareAcceptance, CloudKitSharePermission,
784    },
785    create_cloud_home,
786    s3::S3CloudHome,
787};
788pub use storage::local::BlobStore;
789pub use sync::device_join_transport::DeviceJoinTransportOutcome;
790pub use sync::join::{BootstrapError, DeviceJoinClient};
791pub use sync::restore::{restore_from_cloud, restore_from_code, RestoreSource};
792pub use sync::restore_code::{
793    ActivatedContinuation, OwnerRecoveryAuthority, RestoreAuthority, RestoreCode,
794};
795pub use sync::sync_manager::SyncError;
796
797#[cfg(feature = "oauth-providers")]
798pub use oauth::{
799    authorize_provider, build_authorize_request_for_provider, exchange_code_for_provider,
800    OAuthClientCredsError,
801};
802
803// `OAuthError` is compiled wherever the token-refresh path is — which includes a
804// test build with no `oauth-providers` feature, since refreshing is not the
805// interactive sign-in the feature gates. Its re-export carries the same cfg, so
806// the type is reachable from the crate root exactly where it exists; gating it on
807// the feature alone left it `pub` but unreachable, which `unreachable_pub` denies.
808#[cfg(any(test, feature = "oauth-providers"))]
809pub use oauth::OAuthError;
810
811#[cfg(feature = "oauth-providers")]
812pub use storage::cloud::setup::{sign_in_dropbox, sign_in_google_drive, sign_in_onedrive};