1use super::*;
8use coven_sync::DeviceCredentials;
9
10pub async fn restore_from_code(
19 builder: CovenBuilder,
20 code: &str,
21 s3_credentials: Option<S3Credentials>,
22 device_name: &str,
23 on_status: impl Fn(BootstrapStatus),
24 cancel: &watch::Receiver<bool>,
25) -> Result<CovenHandle, BootstrapError> {
26 check_cancel(cancel)?;
27 let code = coven_sync::read_restore_code(code)?;
28 let storage = RestoreStorage::decode(code.storage.as_bytes()).map_err(SyncError::from)?;
29 let (data, sign_in) = restore_connection(&builder, storage, s3_credentials)?;
30 let sign_in = sign_in.map_or(SignInCustody::None, SignInCustody::Session);
31 Box::pin(bootstrap_device(
32 builder,
33 BootstrapRequest::Restore { code, data, sign_in, name: device_name.into() },
34 on_status,
35 cancel,
36 ))
37 .await?
38 .ok_or_else(|| SyncError::from(StorageFailure::MemberKeysMissing).into())
39}
40
41pub async fn restore_from_saved_code(
47 builder: CovenBuilder,
48 saved: &SavedRestoreCode,
49 s3_credentials: Option<S3Credentials>,
50 device_name: &str,
51 on_status: impl Fn(BootstrapStatus),
52 cancel: &watch::Receiver<bool>,
53) -> Result<Option<CovenHandle>, BootstrapError> {
54 check_cancel(cancel)?;
55 let person = PersonEntries::new(builder.keychain()?, saved.store_id);
56 let Some((code, location)) = keychain_code(&person, saved.store_id)? else {
57 return Ok(None);
58 };
59 let s3_credentials = if location.provider() == CloudProvider::S3 && s3_credentials.is_none() {
60 person
61 .s3_credentials()?
62 .map(|bytes| S3Credentials::decode(bytes.as_bytes()).map_err(SyncError::from))
63 .transpose()?
64 } else {
65 s3_credentials
66 };
67 let storage = RestoreStorage::location_only(location);
69 let (data, sign_in) = restore_connection(&builder, storage, s3_credentials)?;
70 let sign_in = sign_in.map_or(SignInCustody::None, SignInCustody::Session);
71 Box::pin(bootstrap_device(
72 builder,
73 BootstrapRequest::Restore { code, data, sign_in, name: device_name.into() },
74 on_status,
75 cancel,
76 ))
77 .await
78}
79
80#[expect(
81 clippy::map_err_ignore,
82 reason = "read_saved_code logs why a saved code is unreadable; restoring sees Invalid"
83)]
84pub(super) fn keychain_code(
85 keychain: &PersonEntries,
86 store: StoreId,
87) -> Result<Option<(RestoreCode, StorageConfig)>, BootstrapError> {
88 keychain
89 .restore_code()?
90 .map(|bytes| {
91 coven_sync::read_saved_code(store, bytes.as_bytes())
92 .map(|(saved, location)| (saved.code, location))
93 .map_err(|_| CodeError::Invalid.into())
94 })
95 .transpose()
96}
97
98pub(super) struct Recording {
104 entries: Arc<StoreKeychain>,
105 credentials: Arc<DeviceCredentials>,
106 name: String,
107 replacement: bool,
108}
109
110impl Recording {
111 pub(super) fn new(
113 builder: &CovenBuilder,
114 store: StoreId,
115 name: &str,
116 ) -> Result<Self, BootstrapError> {
117 Self::kept(builder, store, name, false)
118 }
119
120 pub(super) fn replacement(
122 builder: &CovenBuilder,
123 store: StoreId,
124 name: &str,
125 ) -> Result<Self, BootstrapError> {
126 Self::kept(builder, store, name, true)
127 }
128
129 fn kept(
130 builder: &CovenBuilder,
131 store: StoreId,
132 name: &str,
133 replacement: bool,
134 ) -> Result<Self, BootstrapError> {
135 let keychain = builder.keychain()?;
136 Ok(Self {
137 entries: Arc::new(StoreKeychain::new(keychain.clone(), store)),
138 credentials: crate::builder::device_credentials(
139 keychain,
140 store,
141 builder.directories.store_dir(&store),
142 builder.oauth_exchange(),
143 ),
144 name: name.to_owned(),
145 replacement,
146 })
147 }
148
149 pub(super) fn record(
151 &self,
152 member: &MemberIdentity,
153 store: StoreId,
154 data: &ConnectionCredentials,
155 sign_in: SignInCustody,
156 ) -> Result<SignInCustody, BootstrapError> {
157 self.keep(member, store, data)?;
158 self.keep_sign_in(sign_in)
159 }
160
161 pub(super) fn keep(
163 &self,
164 member: &MemberIdentity,
165 store: StoreId,
166 data: &ConnectionCredentials,
167 ) -> Result<(), BootstrapError> {
168 let s3_key = match &data.credentials {
169 StorageCredentials::S3(key) => Some(key.clone()),
170 _ => None,
171 };
172 let code = RestoreCode {
173 store,
174 member: member.member(),
175 member_keys: member.keys().clone(),
176 storage: RestoreStorage { location: data.location.clone(), s3_key }
177 .encode()
178 .map_err(SyncError::from)?,
179 };
180 let text = SecretText::new(code.to_text().map_err(SyncError::from)?.to_string());
181 match self.replacement {
182 true => self.entries.set_replacement_record(&text, &self.name)?,
183 false => self.entries.set_restore_record(&text, &self.name)?,
184 }
185 Ok(())
186 }
187
188 pub(super) fn forget(&self) -> Result<(), BootstrapError> {
190 match self.replacement {
191 true => self.entries.delete_replacement_record()?,
192 false => self.entries.delete_restore_record()?,
193 }
194 Ok(())
195 }
196
197 pub(super) fn keep_sign_in(
202 &self,
203 sign_in: SignInCustody,
204 ) -> Result<SignInCustody, BootstrapError> {
205 Ok(match sign_in {
206 SignInCustody::Session(session) => {
207 session.publish(&self.credentials, |keychains, tokens| {
208 coven_sync::commit_credentials(
209 keychains,
210 &StorageCredentials::OAuth(tokens.clone()),
211 None,
212 )
213 .map(drop)
214 })?;
215 SignInCustody::Device { credentials: self.credentials.clone(), kept: false }
216 }
217 custody => custody,
218 })
219 }
220}
221
222pub(crate) async fn unfinished_restores(
224 builder: &CovenBuilder,
225) -> Result<Vec<CodeMetadata>, BootstrapError> {
226 let keychain = builder.keychain()?;
227 let mut restores = Vec::new();
228 for store in builder
229 .directories
230 .unfinished_first_installations()
231 .await
232 .map_err(BootstrapDirectoryError::from)?
233 {
234 let entries = StoreKeychain::new(keychain.clone(), store);
237 let Kept::Restore(code) = kept(builder, &entries, store).await? else {
238 continue;
239 };
240 let metadata = match coven_sync::decode_code_info(code.as_str())? {
241 CodeInfo::Restore(metadata) => metadata,
242 CodeInfo::Invite(_) => {
243 return Err(CodeError::WrongKind {
244 expected: CodeKind::Restore,
245 actual: CodeKind::Invite,
246 }
247 .into())
248 }
249 };
250 CodeError::require_store(store, metadata.store_id)?;
251 restores.push(metadata);
252 }
253 Ok(restores)
254}
255
256pub(crate) async fn resume_restore(
258 builder: CovenBuilder,
259 store: StoreId,
260 on_status: impl Fn(BootstrapStatus),
261 cancel: &watch::Receiver<bool>,
262) -> Result<CovenHandle, BootstrapError> {
263 check_cancel(cancel)?;
264 let (text, name) = StoreKeychain::new(builder.keychain()?, store)
265 .restore_record()?
266 .ok_or(BootstrapError::NoUnfinishedRestore(store))?;
267 let code = coven_sync::read_restore_code(text.as_str())?;
268 CodeError::require_store(store, code.store)?;
269 let storage = RestoreStorage::decode(code.storage.as_bytes()).map_err(SyncError::from)?;
270 let (data, sign_in) = match storage.location.provider() {
271 provider if provider.signs_in_with_oauth() => {
273 let (custody, tokens) = join_sign_in(&builder, store, &storage.location)?;
274 let data = ConnectionCredentials {
275 location: storage.location,
276 credentials: StorageCredentials::OAuth(tokens),
277 };
278 data.validate().map_err(SyncError::from)?;
279 (data, custody)
280 }
281 _ => {
283 let (data, _) = restore_connection(&builder, storage, None)?;
284 (data, SignInCustody::None)
285 }
286 };
287 Box::pin(bootstrap_device(
288 builder,
289 BootstrapRequest::Restore { code, data, sign_in, name },
290 on_status,
291 cancel,
292 ))
293 .await?
294 .ok_or_else(|| SyncError::from(StorageFailure::MemberKeysMissing).into())
295}
296
297pub(super) async fn ended(
309 builder: &CovenBuilder,
310 pending: &BootstrapStore,
311 entries: &StoreKeychain,
312 error: BootstrapError,
313) -> Result<Option<CovenHandle>, BootstrapError> {
314 let recorded = match entries.restore_record() {
315 Ok(record) => record.is_some(),
316 Err(read) => return combine(Err(error), Err(read.into())),
317 };
318 if !recorded {
319 return remove_unfinished(entries, pending, Err(error));
320 }
321 let cancelled = matches!(error, BootstrapError::Cancelled);
322 let ends = cancelled
323 || matches!(
324 error,
325 BootstrapError::Sync(
326 SyncError::Removed | SyncError::MemberKeysReplaced { .. } | SyncError::Rejected(_)
327 )
328 );
329 if !ends {
330 return Err(error);
331 }
332 match registered(builder, pending.directory()).await {
333 Ok(false) => remove_unfinished(entries, pending, Err(error)),
334 Ok(true) if cancelled => Err(BootstrapError::RestoreRegistered),
335 Ok(true) => Err(error),
336 Err(read) => combine(Err(error), Err(read)),
337 }
338}
339
340pub(super) async fn registered(
349 builder: &CovenBuilder,
350 directory: StoreDir,
351) -> Result<bool, BootstrapError> {
352 let device = directory.settings().map_err(CovenError::from)?.device_id;
353 let database = open_installation(builder.database(directory)?, &|_| ()).await?;
354 let facts = async {
355 if database.device_stop().await.map_err(CovenError::from)?.is_some() {
356 return Ok(false);
357 }
358 match database.store_state().await?.devices.get(&device) {
359 Some(_) => Ok(true),
360 None => Ok::<_, BootstrapError>(coven_sync::registration_journaled(&database).await?),
361 }
362 }
363 .await;
364 combine(facts, close_database(&database).await)
365}