1use std::sync::Arc;
2
3#[cfg(feature = "sqlite")]
4use tokio::sync::OnceCell;
5
6mod bootstrap;
7mod client;
8pub(crate) mod options;
9
10pub use self::bootstrap::{
11 CoreBootstrap, LocalStateStatus, MigrationReport, PathCheck, PathValidationReport,
12};
13pub use self::client::ImClient;
14pub use self::options::{IdentitySecretStoragePolicy, ImCoreOpenOptions, ImCoreSecretVaultOptions};
15
16pub(crate) struct ImCoreInner {
17 pub(crate) sdk_config: crate::ImCoreConfig,
18 pub(crate) sdk_paths: crate::ImCorePaths,
19 pub(crate) identity_secret_storage_policy: IdentitySecretStoragePolicy,
20 pub(crate) identity_vault: Option<options::IdentityVaultContext>,
21 #[cfg(feature = "sqlite")]
22 pub(crate) local_state_db: OnceCell<crate::internal::local_state::actor::LocalStateDb>,
23}
24
25#[derive(Clone)]
26pub struct ImCore {
27 inner: Arc<ImCoreInner>,
28}
29
30impl ImCore {
31 pub async fn open(
32 sdk_config: crate::ImCoreConfig,
33 sdk_paths: crate::ImCorePaths,
34 ) -> crate::ImResult<Self> {
35 Self::new(sdk_config, sdk_paths)
36 }
37
38 pub async fn open_with_options(
39 sdk_config: crate::ImCoreConfig,
40 sdk_paths: crate::ImCorePaths,
41 options: ImCoreOpenOptions,
42 ) -> crate::ImResult<Self> {
43 Self::new_with_options(sdk_config, sdk_paths, options)
44 }
45
46 pub fn new(
47 sdk_config: crate::ImCoreConfig,
48 sdk_paths: crate::ImCorePaths,
49 ) -> crate::ImResult<Self> {
50 Self::new_with_options(sdk_config, sdk_paths, ImCoreOpenOptions::default())
51 }
52
53 pub fn new_with_options(
54 sdk_config: crate::ImCoreConfig,
55 sdk_paths: crate::ImCorePaths,
56 options: ImCoreOpenOptions,
57 ) -> crate::ImResult<Self> {
58 if sdk_config.did_domain.trim().is_empty() {
59 return Err(crate::ImError::invalid_input(
60 Some("did_domain".to_string()),
61 "DID domain must not be empty",
62 ));
63 }
64 let identity_vault = options
65 .identity_secret_vault
66 .map(options::IdentityVaultContext::from_options)
67 .transpose()?
68 .map(|context| context.with_policy(options.identity_secret_storage_policy));
69 if matches!(
70 options.identity_secret_storage_policy,
71 IdentitySecretStoragePolicy::VaultRequired
72 ) && identity_vault.is_none()
73 {
74 return Err(crate::ImError::LocalStateUnavailable {
75 detail: "identity secret storage policy is VaultRequired but no identity secret vault was provided".to_owned(),
76 });
77 }
78 Ok(Self {
79 inner: Arc::new(ImCoreInner {
80 sdk_config,
81 sdk_paths,
82 identity_secret_storage_policy: options.identity_secret_storage_policy,
83 identity_vault,
84 #[cfg(feature = "sqlite")]
85 local_state_db: OnceCell::new(),
86 }),
87 })
88 }
89
90 pub fn identities(&self) -> crate::identity::IdentityRegistry<'_> {
91 crate::identity::IdentityRegistry::new(self)
92 }
93
94 pub fn bootstrap(&self) -> CoreBootstrap<'_> {
95 CoreBootstrap::new(self)
96 }
97
98 pub fn client(&self, selector: crate::identity::IdentitySelector) -> crate::ImResult<ImClient> {
99 let runtime = self.identities().load_runtime(selector)?;
100 Ok(ImClient::new(self.inner.clone(), runtime))
101 }
102
103 pub async fn client_async(
104 &self,
105 selector: crate::identity::IdentitySelector,
106 ) -> crate::ImResult<ImClient> {
107 let runtime = self.identities().load_runtime_async(selector).await?;
108 Ok(ImClient::new(self.inner.clone(), runtime))
109 }
110
111 pub fn client_with_identity_material(
112 &self,
113 material: crate::identity::HostedIdentityMaterial,
114 ) -> crate::ImResult<ImClient> {
115 let identity_id = crate::ids::IdentityId::parse(&material.identity_id)?;
116 let did = crate::ids::Did::parse(&material.did)?;
117 let handle = material
118 .handle
119 .as_deref()
120 .map(|handle| crate::ids::Handle::parse(handle, &self.inner.sdk_config().did_domain))
121 .transpose()?;
122 let key_provider = std::sync::Arc::new(
123 crate::internal::key_provider::HostedKeyMaterialProvider::new(&material)?,
124 );
125 let runtime = crate::internal::identity_runtime::ClientIdentityRuntime {
126 summary: crate::identity::IdentitySummary {
127 id: identity_id.clone(),
128 did: did.clone(),
129 handle,
130 display_name: material.display_name,
131 local_alias: None,
132 device_id: None,
133 is_default: false,
134 readiness: crate::identity::IdentityReadiness {
135 ready_for_auth: true,
136 ready_for_messaging: true,
137 missing: Vec::new(),
138 },
139 },
140 did_document_path: std::path::PathBuf::new(),
141 private_key_path: std::path::PathBuf::new(),
142 e2ee_agreement_private_key_path: std::path::PathBuf::new(),
143 auth_state_path: std::path::PathBuf::new(),
144 key_provider,
145 owner: crate::internal::identity_runtime::LocalOwnerContext {
146 identity_id,
147 current_did: did,
148 },
149 };
150 Ok(ImClient::new(self.inner.clone(), runtime))
151 }
152
153 pub(crate) fn inner(&self) -> &ImCoreInner {
154 &self.inner
155 }
156}
157
158impl ImCoreInner {
159 pub(crate) fn sdk_config(&self) -> &crate::ImCoreConfig {
160 &self.sdk_config
161 }
162
163 pub(crate) fn sdk_paths(&self) -> &crate::ImCorePaths {
164 &self.sdk_paths
165 }
166
167 pub(crate) fn identity_secret_storage_policy(&self) -> IdentitySecretStoragePolicy {
168 self.identity_secret_storage_policy
169 }
170
171 pub(crate) fn identity_vault(&self) -> Option<&options::IdentityVaultContext> {
172 self.identity_vault.as_ref()
173 }
174
175 #[cfg(feature = "sqlite")]
176 pub(crate) async fn local_state_db(
177 &self,
178 ) -> crate::ImResult<crate::internal::local_state::actor::LocalStateDb> {
179 self.local_state_db
180 .get_or_try_init(|| {
181 crate::internal::local_state::actor::LocalStateDb::open(
182 self.sdk_paths.local_state.sqlite_path.clone(),
183 )
184 })
185 .await
186 .cloned()
187 }
188}
189
190#[cfg(all(test, feature = "sqlite"))]
191mod tests {
192 use super::*;
193
194 #[tokio::test]
195 async fn local_state_db_concurrent_first_open_shares_actor() {
196 let root = tempfile::tempdir().unwrap();
197 let core = ImCore::new(
198 crate::ImCoreConfig {
199 service_base_url: crate::ServiceEndpoint::parse("https://example.test").unwrap(),
200 did_domain: "awiki.test".to_owned(),
201 user_service_endpoint: None,
202 message_service_endpoint: None,
203 mail_service_endpoint: None,
204 anp_service_endpoint: None,
205 anp_service_did: None,
206 ca_bundle: None,
207 transport_policy: crate::MessageTransportPolicy::HttpOnly,
208 },
209 crate::ImCorePaths {
210 identities: crate::IdentityRegistryPaths {
211 identity_root_dir: root.path().join("identities"),
212 registry_path: root.path().join("identities").join("registry.json"),
213 default_identity_path: Some(root.path().join("identities").join("default")),
214 },
215 local_state: crate::LocalStatePaths {
216 sqlite_path: root.path().join("local").join("im.sqlite"),
217 },
218 runtime: crate::RuntimePaths {
219 cache_dir: root.path().join("cache"),
220 temp_dir: root.path().join("tmp"),
221 },
222 },
223 )
224 .unwrap();
225
226 let inner = core.inner.clone();
227 let mut tasks = Vec::new();
228 for _ in 0..8 {
229 let inner = inner.clone();
230 tasks.push(tokio::spawn(async move {
231 let db = inner.local_state_db().await.unwrap();
232 db.current_schema_version().await.unwrap()
233 }));
234 }
235
236 for task in tasks {
237 assert_eq!(
238 task.await.unwrap(),
239 crate::internal::local_state::schema::SCHEMA_VERSION
240 );
241 }
242 assert!(core.inner().local_state_db.get().is_some());
243 }
244}