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::CloudVersionedHead, CloudHomeError>
437                {
438                    unimplemented!("not exercised by these tests")
439                }
440                fn create_record(
441                    &self,
442                    _scope: &CloudKitScope,
443                    _key: &str,
444                    _data: Vec<u8>,
445                ) -> Result<
446                    crate::storage::cloud::CloudVersionedHead,
447                    crate::storage::cloud::CloudHeadCreateError,
448                > {
449                    unimplemented!("not exercised by these tests")
450                }
451                fn replace_record(
452                    &self,
453                    _scope: &CloudKitScope,
454                    _key: &str,
455                    _expected: &crate::storage::cloud::CloudHeadVersion,
456                    _data: Vec<u8>,
457                ) -> Result<
458                    crate::storage::cloud::CloudVersionedHead,
459                    crate::storage::cloud::CloudHeadReplaceError,
460                > {
461                    unimplemented!("not exercised by these tests")
462                }
463                fn begin_atomic_create(
464                    &self,
465                    _scope: &CloudKitScope,
466                ) -> Result<CloudKitAtomicCreateBatch, CloudHomeError> {
467                    unimplemented!("not exercised by these tests")
468                }
469                fn stage_atomic_create_record(
470                    &self,
471                    _scope: &CloudKitScope,
472                    _batch: &CloudKitAtomicCreateBatch,
473                    _record: CloudKitRecordCreate,
474                ) -> Result<(), CloudHomeError> {
475                    unimplemented!("not exercised by these tests")
476                }
477                fn commit_atomic_create(
478                    &self,
479                    _scope: &CloudKitScope,
480                    _batch: &CloudKitAtomicCreateBatch,
481                ) -> Result<Vec<CloudKitRecordVersion>, CloudHomeError> {
482                    unimplemented!("not exercised by these tests")
483                }
484                fn discard_atomic_create(
485                    &self,
486                    _scope: &CloudKitScope,
487                    _batch: &CloudKitAtomicCreateBatch,
488                ) -> Result<(), CloudHomeError> {
489                    unimplemented!("not exercised by these tests")
490                }
491                fn delete_record_versions(
492                    &self,
493                    _scope: &CloudKitScope,
494                    _records: &[CloudKitRecordVersion],
495                ) -> Result<(), CloudHomeError> {
496                    unimplemented!("not exercised by these tests")
497                }
498                fn grant_share(
499                    &self,
500                    _member_pubkey: &str,
501                ) -> Result<CloudKitShare, CloudHomeError> {
502                    unimplemented!("not exercised by these tests")
503                }
504                fn share_for_member(
505                    &self,
506                    _member_pubkey: &str,
507                ) -> Result<Option<CloudKitShare>, CloudHomeError> {
508                    unimplemented!("not exercised by these tests")
509                }
510                fn revoke_share(&self, _member_pubkey: &str) -> Result<(), CloudHomeError> {
511                    unimplemented!("not exercised by these tests")
512                }
513                fn accept_share(&self, _share_url: &str) -> Result<CloudKitShare, CloudHomeError> {
514                    unimplemented!("not exercised by these tests")
515                }
516            }
517
518            fn cloudkit_config(owner_zone: Option<(&str, &str)>) -> Config {
519                let mut config = Config::with_defaults(
520                    "store-1".to_string(),
521                    "device-1".to_string(),
522                    StoreDir::new("unused-store-dir"),
523                    "CloudKit Store".to_string(),
524                );
525                config.cloud_home.provider = Some(CloudProvider::CloudKit);
526                config.cloud_home.storage = HomeStorage::Opaque;
527                if let Some((owner, zone)) = owner_zone {
528                    config.cloud_home.cloudkit_owner_name = Some(owner.to_string());
529                    config.cloud_home.cloudkit_zone_name = Some(zone.to_string());
530                }
531                config
532            }
533
534            /// Neither `cloudkit_owner_name` nor `cloudkit_zone_name` set builds a
535            /// private home — the scope every subsequent op sees is `Private`.
536            #[tokio::test]
537            async fn neither_owner_nor_zone_builds_a_private_home() {
538                let config = cloudkit_config(None);
539                let key_service = StoreKeys::new(config.store_id.clone());
540                let ops = std::sync::Arc::new(ScopeRecordingOps::new());
541                let clock: crate::clock::ClockRef =
542                    std::sync::Arc::new(FixedClock(chrono::Utc::now()));
543
544                let home = create_cloud_home_with_cloudkit(
545                    &config,
546                    &key_service,
547                    clock,
548                    Some(ops.clone()),
549                )
550                .await
551                .expect("private CloudKit config builds a home");
552                home.list("").await.expect("list against the built home");
553
554                assert_eq!(
555                    ops.seen.lock().unwrap().as_slice(),
556                    [CloudKitScope::Private]
557                );
558            }
559
560            /// Both `cloudkit_owner_name` and `cloudkit_zone_name` set builds a
561            /// shared home scoped to that owner/zone pair.
562            #[tokio::test]
563            async fn both_owner_and_zone_build_a_shared_home() {
564                let config = cloudkit_config(Some(("owner-name", "zone-name")));
565                let key_service = StoreKeys::new(config.store_id.clone());
566                let ops = std::sync::Arc::new(ScopeRecordingOps::new());
567                let clock: crate::clock::ClockRef =
568                    std::sync::Arc::new(FixedClock(chrono::Utc::now()));
569
570                let home = create_cloud_home_with_cloudkit(
571                    &config,
572                    &key_service,
573                    clock,
574                    Some(ops.clone()),
575                )
576                .await
577                .expect("shared CloudKit config builds a home");
578                home.list("").await.expect("list against the built home");
579
580                assert_eq!(
581                    ops.seen.lock().unwrap().as_slice(),
582                    [CloudKitScope::Shared {
583                        owner_name: "owner-name".to_string(),
584                        zone_name: "zone-name".to_string(),
585                    }]
586                );
587            }
588
589            /// Only one of `cloudkit_owner_name` / `cloudkit_zone_name` set is the
590            /// broken shape the config module can't rule out by construction (see
591            /// the module doc: config stays flat, provider-prefixed fields rather
592            /// than one enum for CloudKit alone) — surfaced as a `Configuration`
593            /// error naming both fields, not a silent guess at which home to build.
594            #[tokio::test]
595            async fn mixed_owner_zone_is_a_configuration_error() {
596                let mut config = cloudkit_config(None);
597                config.cloud_home.cloudkit_owner_name = Some("owner-name".to_string());
598                let key_service = StoreKeys::new(config.store_id.clone());
599                let ops = std::sync::Arc::new(ScopeRecordingOps::new());
600                let clock: crate::clock::ClockRef =
601                    std::sync::Arc::new(FixedClock(chrono::Utc::now()));
602
603                let result =
604                    create_cloud_home_with_cloudkit(&config, &key_service, clock, Some(ops)).await;
605                match result {
606                    Ok(_) => panic!("mixed owner/zone must not build a home"),
607                    Err(CloudHomeError::Configuration(message)) => {
608                        assert!(message.contains("cloudkit_owner_name"), "{message}");
609                        assert!(message.contains("cloudkit_zone_name"), "{message}");
610                    }
611                    Err(other) => panic!("expected Configuration error, got {other:?}"),
612                }
613            }
614        }
615    }
616
617    pub(crate) mod local;
618}
619
620pub(crate) mod sync {
621    pub(crate) use coven_core::sync::*;
622
623    pub(crate) mod join;
624    #[cfg(test)]
625    mod join_tests;
626    pub(crate) mod restore;
627    #[cfg(test)]
628    mod restore_tests;
629    pub(crate) mod sync_loop;
630    pub(crate) mod sync_manager;
631}
632
633mod coven;
634mod handle;
635mod keyring_backend;
636mod read_handle;
637
638// coven's public API is exactly the crate-root re-exports below. The
639// implementation modules are `pub(crate)`; a host reaches coven only through
640// these names, never through `coven::sync::…` or `coven::blob::…`.
641
642pub use coven::{
643    Coven, CovenBuilder, CovenConfig, CovenError, CovenResult, SqlContext, WriteBatch,
644};
645pub use handle::CovenHandle;
646pub use read_handle::CovenReadHandle;
647
648// --- coven-core's curated engine surface, re-exported so a host names it as
649//     `coven::…` and never depends on `coven-core` directly. ---
650
651/// The exact `rusqlite` coven owns the connection through; see [`CovenHandle::sql`].
652pub use coven_core::rusqlite;
653
654// Host schema declaration: the synced-table set and the synced-schema migration ladder.
655pub use coven_core::{BlobDecl, Migration, MigrationStep, RowIdentity, SyncedTable};
656
657// Config.
658pub use coven_core::sync::storage::CloudKitEnvironment;
659pub use coven_core::{
660    CloudHomeConfig, CloudProvider, Config, ConfigError, CustomS3ExactSlots, CustomS3Serial,
661    HomeStorage, WritePolicy,
662};
663
664// Blob descriptors, cache error, the host-implemented transition observer.
665pub use coven_core::{
666    BlobCacheError, BlobRef, BlobReplacement, BlobScope, BlobTransitionObserver, CacheFill,
667    Provenance, RowBlobAuthority, RowBlobRef,
668};
669
670// Applied-sync change notification.
671pub use coven_core::{ChangeOp, RowChange};
672
673// At-rest crypto the host configures (the host sizes cloud stream reads from
674// `CHUNK_SIZE`), and the store directory and its host-configurable layout,
675// the DB error. `MasterKeyring` is the master-key custody value type — the
676// payload `KeyCustody::InMemory` takes and `import_master_key`/
677// `initialize_master_key` traffic in internally. `SealError` is what
678// `CovenHandle::seal_app_data` / `open_app_data` return.
679pub use coven_core::{
680    DbError, EncryptionError, MasterKeyring, SealError, StoreDir, StoreLayout, CHUNK_SIZE,
681};
682
683// `EncryptionService` is the cipher coven builds internally from whatever
684// custody supplies; a production host never constructs one. It stays
685// reachable for host integration tests, which build a `CloudCipher` directly
686// to drive `CovenHandle::connect_sync_with_test_home`.
687#[cfg(any(test, feature = "test-utils"))]
688pub use coven_core::EncryptionService;
689
690// The register clock vocabulary carried on every synced row.
691pub use coven_core::{Hlc, Timestamp, UpdatedAtStamper};
692
693// Membership. `MembershipCoord` (an author's membership-head coordinate) is
694// exposed only because `generate_restore_code` takes a caller-supplied
695// membership floor made of them; a host driving that free function directly
696// (bypassing `CovenHandle`) must be able to name the type.
697pub use coven_core::sync::device_join::{
698    DeviceJoinAbandonment, DeviceJoinAction, DeviceJoinActivation, DeviceJoinAuthorization,
699    DeviceJoinCancellation, DeviceJoinCleanupActivation, DeviceJoinCleanupProgress,
700    DeviceJoinCleanupReceipt, DeviceJoinError, DeviceJoinJournalDatabase, DeviceJoinJournalRecord,
701    DeviceJoinOffer, DeviceJoinProducer, DeviceJoinProducerWriteRevocation, DeviceJoinReadiness,
702    DeviceJoinRole, DeviceJoinStatus, DeviceProviderAccessAdministrator,
703    DeviceProviderAccessRequest, DeviceProviderAdmission, DeviceProviderAdmissionApproval,
704    DeviceProviderAdmissionCompletion, DeviceProviderReadiness, DeviceRegistrationRequest,
705    JoinedStore, JoinerJoinClosure, JoinerJoinTerminal, ProviderAdminJoinClosure,
706    ProviderAdminJoinTerminal, ProviderReadyDeviceBootstrap, ProvisionalDeviceBootstrap,
707};
708pub use coven_core::sync::membership::MembershipCoord;
709pub use coven_core::sync::store_commit::{DeviceJoinAttemptId, DeviceJoinAttemptRef};
710pub use coven_core::{
711    Audience, CircleId, CircleInfo, CircleOperationInfo, CircleOperationState, CircleRole,
712};
713pub use coven_core::{MemberInfo, MemberRole};
714
715// Clock / id abstractions the host injects, plus the deterministic test fakes.
716pub use coven_core::{Clock, ClockRef, IdProvider, IdRef, SystemClock, UuidProvider};
717#[cfg(any(test, feature = "test-utils"))]
718pub use coven_core::{FixedClock, SequentialIdProvider, SteppingClock};
719
720// Bootstrap decoders.
721pub use coven_core::{
722    decode_invite_code_info, decode_join_request, decode_restore_code_info, JoinCodeError,
723};
724
725// The cloud at-rest cipher. coven resolves it from custody internally; a
726// production host never names it. Host integration tests build one directly
727// to drive `CovenHandle::connect_sync_with_test_home`.
728#[cfg(any(test, feature = "test-utils"))]
729pub use coven_core::CloudCipher;
730
731// Cloud provider trait surface a provider implementor needs and its
732// thread-safety floor.
733pub use coven_core::storage::cloud::{
734    write_cloud_object_stream, CloudFileReadError, CloudObjectStream, ExactSlotStorage, ObjectSlot,
735    PhysicalObjectLocator,
736};
737pub use coven_core::sync::provider::{
738    CloudKitAcceptedShare, CrossPrincipalProbeReceipt, ExactSlotProbeReceipt, ProviderAdminChange,
739    ProviderAdminGrantId, ProviderAdminGrantRecord, ProviderAdminMembershipChange,
740    ProviderAdminState, ProviderCapabilityProof, ProviderProbeId, SerialCoordinationProbeReceipt,
741};
742pub use coven_core::sync::storage::{
743    AwsPrincipal, GoogleDriveCorpus, ProviderDeviceBinding, ProviderPrincipalId,
744    ResolvedProviderBinding, S3EndpointBinding, StoreProviderBinding,
745};
746pub use coven_core::{
747    BlobBody, BoxPartSink, CloudAccessOutcome, CloudAccessState, CloudHeadCreateError,
748    CloudHeadReplaceError, CloudHeadVersion, CloudHome, CloudHomeError, CloudHomeJoinInfo,
749    CloudVersionedHead, PartSink, UploadProgress,
750};
751
752// Sync-status surface a host renders from `CovenHandle::subscribe_sync_status`:
753// the status enum, its completed-cycle success payload, the per-cycle alert
754// bundle, the per-device activity, and the held-changeset detail the alerts carry.
755pub use coven_core::{
756    AffectedRow, DeviceActivity, HeldStoreCoordinate, HeldStorePosition, HeldStorePositionReason,
757    ObjectHash, PendingBranch, PendingBranchId, PendingWrite, PublishedPosition,
758    SerializationConflict, StoreBatchCommitRef, StoreCommitCoord, StoreSerialPredecessor,
759    SyncLoopAlerts, SyncLoopSuccess, WriteBlock, WriteId, WriteReceipt, WriteResolution,
760    WriteStatus,
761};
762pub use sync::sync_loop::SyncLoopStatus;
763
764// In-memory cloud home and durable upload-queue rows for host integration tests.
765#[cfg(any(test, feature = "test-utils"))]
766pub use coven_core::InMemoryCloudHome;
767
768pub use blob::transition::{MakeLocalError, MakeRemoteError};
769pub use custody::{rewrap_passphrase_custody, KeyCustody, Passphrase};
770pub use identity_custody::{rewrap_passphrase_identity_custody, IdentityCustody};
771pub use join_code::{abandon_join_request, generate_join_request};
772pub use keys::{
773    keyring_service, set_keyring_service, CloudHomeCredentials, DeviceIdentityCustody,
774    IdentityError, KeyError, MasterKeyCustody, MasterKeyError, StoreKeys, UserKeypair,
775};
776
777// The sole keyring entry-construction chokepoint (`keys::entry_for`), reached
778// across the crate boundary so an integration test can install a specific
779// keyring store and assert which construction path it took, without
780// re-implementing that dispatch. A production host never calls this.
781#[cfg(any(test, feature = "test-utils"))]
782pub use keys::entry_for_test;
783
784pub use oauth::{set_oauth_client_creds, OAuthClientCreds, OAuthClientCredsConflict, OAuthTokens};
785pub use storage::cloud::setup::generate_restore_code;
786pub use storage::cloud::{
787    cloudkit::{
788        CloudKitAcceptedShareRecord, CloudKitAtomicCreateBatch, CloudKitOps,
789        CloudKitProviderIdentity, CloudKitRecordCreate, CloudKitRecordVersion, CloudKitScope,
790        CloudKitShare, CloudKitShareAcceptance, CloudKitSharePermission,
791    },
792    create_cloud_home,
793    s3::S3CloudHome,
794};
795pub use storage::local::BlobStore;
796pub use sync::join::{BootstrapError, DeviceJoinClient};
797pub use sync::restore::{restore_from_cloud, restore_from_code, RestoreSource};
798pub use sync::restore_code::{
799    ActivatedContinuation, OwnerRecoveryAuthority, RestoreAuthority, RestoreCode,
800};
801pub use sync::sync_manager::SyncError;
802
803#[cfg(feature = "oauth-providers")]
804pub use oauth::{
805    authorize_provider, build_authorize_request_for_provider, exchange_code_for_provider,
806    OAuthClientCredsError,
807};
808
809// `OAuthError` is compiled wherever the token-refresh path is — which includes a
810// test build with no `oauth-providers` feature, since refreshing is not the
811// interactive sign-in the feature gates. Its re-export carries the same cfg, so
812// the type is reachable from the crate root exactly where it exists; gating it on
813// the feature alone left it `pub` but unreachable, which `unreachable_pub` denies.
814#[cfg(any(test, feature = "oauth-providers"))]
815pub use oauth::OAuthError;
816
817#[cfg(feature = "oauth-providers")]
818pub use storage::cloud::setup::{sign_in_dropbox, sign_in_google_drive, sign_in_onedrive};