1use super::authority::{authorize_store, exact_slot_storage, resolved_provider_admin};
2use super::journal::{
3 advance_store_journal, database_error, load_store_journal, require_distinct_slots,
4};
5use super::*;
6
7impl Store {
8 #[doc(hidden)]
9 pub async fn prepare_device_join_cleanup(
10 &self,
11 identity_signer: &UserKeypair,
12 cancellation: DeviceJoinCancellation,
13 administrator_terminal: ProviderAdminJoinTerminal,
14 joiner_terminal: JoinerJoinTerminal,
15 ) -> Result<DeviceJoinCleanupReceipt, DeviceJoinError> {
16 let authorized = authorize_store(self).await?;
17 let exact = exact_slot_storage(self);
18 prepare_device_join_cleanup(
19 authorized.database(),
20 authorized.storage(),
21 exact,
22 authorized.membership(),
23 identity_signer,
24 cancellation,
25 administrator_terminal,
26 joiner_terminal,
27 )
28 .await
29 }
30
31 #[doc(hidden)]
32 pub async fn activate_device_join_cleanup(
33 &self,
34 identity_signer: &UserKeypair,
35 receipt: DeviceJoinCleanupReceipt,
36 ) -> Result<DeviceJoinCleanupActivation, DeviceJoinError> {
37 let authorized = authorize_store(self).await?;
38 activate_device_join_cleanup(
39 authorized.database(),
40 authorized.storage(),
41 authorized.membership(),
42 identity_signer,
43 receipt.receipt.attempt_id,
44 receipt,
45 )
46 .await
47 }
48}
49
50#[async_trait::async_trait]
51pub trait DeviceJoinWriteRevocationExecutor: Send + Sync {
52 async fn revoke_write_authority(
56 &self,
57 producer: DeviceJoinProducer,
58 authority: &ProviderWriteAuthorityRef,
59 locator: &crate::sync::provider::ProviderAccessLocator,
60 protected_slots: &[ObjectSlot],
61 ) -> Result<ProviderAccessWithdrawal, DeviceJoinError>;
62}
63
64pub(crate) fn prepare_device_join_cleanup<'a>(
65 database: &'a StoreDatabase,
66 storage: &'a dyn SyncStorage,
67 executor_exact: &'a dyn ExactSlotStorage,
68 authorization: &'a MembershipChain,
69 identity_signer: &'a UserKeypair,
70 cancellation: DeviceJoinCancellation,
71 administrator_terminal: ProviderAdminJoinTerminal,
72 joiner_terminal: JoinerJoinTerminal,
73) -> std::pin::Pin<
74 Box<dyn std::future::Future<Output = Result<DeviceJoinCleanupReceipt, DeviceJoinError>> + 'a>,
75> {
76 Box::pin(prepare_device_join_cleanup_inner(
77 database,
78 storage,
79 executor_exact,
80 authorization,
81 identity_signer,
82 Box::new(cancellation),
83 Box::new(administrator_terminal),
84 Box::new(joiner_terminal),
85 ))
86}
87
88#[allow(clippy::too_many_arguments)]
89async fn prepare_device_join_cleanup_inner(
90 database: &StoreDatabase,
91 storage: &dyn SyncStorage,
92 executor_exact: &dyn ExactSlotStorage,
93 authorization: &MembershipChain,
94 identity_signer: &UserKeypair,
95 cancellation: Box<DeviceJoinCancellation>,
96 administrator_terminal: Box<ProviderAdminJoinTerminal>,
97 joiner_terminal: Box<JoinerJoinTerminal>,
98) -> Result<DeviceJoinCleanupReceipt, DeviceJoinError> {
99 let db = database.sqlite();
100 require_cancelled_outcome(&cancellation.outcome)?;
101 let attempt_ref = cancellation.outcome.attempt().clone();
102 let current = load_store_journal(db, attempt_ref.attempt_id, DeviceJoinRole::Owner)
103 .await?
104 .ok_or(DeviceJoinError::JournalConflict)?;
105 if let DeviceJoinRoleProgress::Owner(OwnerJoinProgress::CleanupReceipt(existing)) =
106 &*current.progress
107 {
108 return Ok(existing.clone());
109 }
110 let durable_cancellation = match &*current.progress {
111 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Cancelled(durable)) => durable,
112 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::CleanupReceiptCreateIntent {
113 cancellation: durable,
114 ..
115 }) => durable,
116 _ => return Err(DeviceJoinError::JournalConflict),
117 };
118 if durable_cancellation != cancellation.as_ref() {
119 return Err(DeviceJoinError::JournalConflict);
120 }
121 let root = database
122 .local_store_root_ref()
123 .await
124 .map_err(database_error)?
125 .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
126 let attempt_context = crate::sync::storage::ProtocolObjectContext::signed_plaintext(
127 root.store_root_hash,
128 ProtocolObjectDomain::DeviceJoinAttempt,
129 );
130 let attempt_prefix =
131 crate::sync::store_commit::device_join_attempt_semantic_prefix(attempt_ref.attempt_id);
132 let attempt_bytes = storage
133 .read_protocol_object(&attempt_context, &attempt_ref.object, &attempt_prefix)
134 .await?;
135 let unverified_attempt: DeviceJoinAttempt = serde_json::from_slice(&attempt_bytes)?;
136 let owner = database
137 .activated_store_device_registration(unverified_attempt.owner_registration.clone())
138 .await
139 .map_err(database_error)?;
140 let attempt = crate::sync::store::load_verified_device_join_attempt_ref(
141 storage,
142 &root,
143 &attempt_ref,
144 &owner,
145 )
146 .await?
147 .value;
148 let outcome = crate::sync::store_objects::load_device_join_outcome_ref(
149 storage,
150 &root,
151 &cancellation.outcome,
152 &owner,
153 )
154 .await?
155 .value;
156 if !matches!(
157 outcome.body,
158 crate::sync::store_commit::DeviceJoinOutcomeBody::Cancelled
159 ) {
160 return Err(DeviceJoinError::AttemptMismatch);
161 }
162 validate_terminals(
163 &cancellation.outcome,
164 administrator_terminal.as_ref(),
165 joiner_terminal.as_ref(),
166 )?;
167 verify_cleanup_terminals(
168 database,
169 administrator_terminal.as_ref(),
170 joiner_terminal.as_ref(),
171 )
172 .await?;
173 let local_device_id = db
174 .get_protocol_state(crate::database::LOCAL_DEVICE_ID_STATE_KEY)
175 .await
176 .map_err(database_error)?
177 .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
178 let (local_root, executor_ref, executor, executor_signer) =
179 crate::sync::store::operations::load_local_store_authority(
180 database,
181 &local_device_id,
182 identity_signer,
183 )
184 .await?;
185 if local_root != root || !authorization.is_owner_now(&executor.author_pubkey) {
186 return Err(DeviceJoinError::OwnerAuthorityRequired);
187 }
188 let effective_executor = resolved_provider_admin(
189 authorization,
190 &attempt
191 .provider_approval
192 .request
193 .offer
194 .provider_admin
195 .grant_id,
196 )?;
197 if effective_executor != *attempt.provider_approval.request.offer.provider_admin
198 || effective_executor.administrator != executor_ref
199 || effective_executor.provider != executor.provider
200 {
201 return Err(DeviceJoinError::ProviderAdministratorRequired);
202 }
203 let context = crate::sync::storage::ProtocolObjectContext::signed_plaintext(
204 root.store_root_hash,
205 ProtocolObjectDomain::DeviceJoinCleanupReceipt,
206 );
207 let prefix = crate::sync::store_commit::device_join_cleanup_receipt_semantic_prefix(
208 attempt_ref.attempt_id,
209 );
210 let (receipt_object, receipt_ref, prepared, intent) = match &*current.progress {
211 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Cancelled(_)) => {
212 let plan = crate::sync::store::operations::prepare_plan(
213 database,
214 storage,
215 authorization,
216 &local_device_id,
217 identity_signer,
218 )
219 .await?;
220 let receipt_object = DeviceJoinCleanupReceiptObject::signed(
221 &attempt,
222 cancellation.outcome.clone(),
223 administrator_terminal.as_ref().clone(),
224 joiner_terminal.as_ref().clone(),
225 canonical_cleanup_slots(&attempt)?,
226 plan.membership_state().clone(),
227 attempt
228 .provider_approval
229 .request
230 .offer
231 .provider_admin
232 .grant_id
233 .clone(),
234 executor_ref,
235 &executor,
236 &executor_signer,
237 )?;
238 let slot = storage
239 .allocate_protocol_slot(&context, &prefix, ".json")
240 .await?;
241 let prepared = storage.prepare_protocol_object(
242 &context,
243 slot,
244 &prefix,
245 receipt_object.to_bytes(),
246 )?;
247 let receipt_ref = DeviceJoinCleanupReceiptRef {
248 attempt_id: attempt_ref.attempt_id,
249 receipt_hash: receipt_object.receipt_hash(),
250 object: prepared.reference().clone(),
251 };
252 let intent = DeviceJoinJournalRecord {
253 attempt_id: attempt_ref.attempt_id,
254 progress: Box::new(DeviceJoinRoleProgress::Owner(
255 OwnerJoinProgress::CleanupReceiptCreateIntent {
256 cancellation: cancellation.as_ref().clone(),
257 receipt: receipt_ref.clone(),
258 receipt_bytes: receipt_object.to_bytes(),
259 prepared: PreparedDeviceJoinObject::from_prepared(&prepared),
260 },
261 )),
262 };
263 advance_store_journal(db, ¤t, intent.clone()).await?;
264 (Box::new(receipt_object), receipt_ref, prepared, intent)
265 }
266 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::CleanupReceiptCreateIntent {
267 receipt,
268 receipt_bytes,
269 prepared,
270 ..
271 }) => {
272 let receipt_object: DeviceJoinCleanupReceiptObject =
273 serde_json::from_slice(receipt_bytes)?;
274 if receipt_object.to_bytes() != *receipt_bytes
275 || receipt_object.cancellation != cancellation.outcome
276 || &receipt_object.administrator_terminal != administrator_terminal.as_ref()
277 || &receipt_object.joiner_terminal != joiner_terminal.as_ref()
278 || receipt.attempt_id != attempt_ref.attempt_id
279 || receipt.receipt_hash != receipt_object.receipt_hash()
280 || receipt.object != prepared.object
281 {
282 return Err(DeviceJoinError::JournalConflict);
283 }
284 (
285 Box::new(receipt_object),
286 receipt.clone(),
287 crate::sync::storage::PreparedExactObject::new(
288 prepared.object.clone(),
289 prepared.stored_bytes.clone(),
290 )?,
291 current.clone(),
292 )
293 }
294 _ => return Err(DeviceJoinError::JournalConflict),
295 };
296 receipt_object.verify(&attempt, &executor)?;
297 for slot in &receipt_object.deleted_slots {
298 ensure_exact_slot_absent(executor_exact, slot).await?;
299 }
300 storage.create_protocol_object(&prepared).await?;
301 let opened = storage
302 .read_protocol_object(&context, prepared.reference(), &prefix)
303 .await?;
304 if opened != receipt_object.to_bytes() {
305 return Err(DeviceJoinError::CleanupMismatch);
306 }
307 receipt_ref.verify(&receipt_object, &executor)?;
308 let receipt = DeviceJoinCleanupReceipt {
309 receipt: receipt_ref,
310 };
311 advance_store_journal(
312 db,
313 &intent,
314 DeviceJoinJournalRecord {
315 attempt_id: attempt_ref.attempt_id,
316 progress: Box::new(DeviceJoinRoleProgress::Owner(
317 OwnerJoinProgress::CleanupReceipt(receipt.clone()),
318 )),
319 },
320 )
321 .await?;
322 Ok(receipt)
323}
324
325pub(super) async fn sign_device_join_producer_write_revocation(
326 database: &StoreDatabase,
327 storage: &dyn SyncStorage,
328 authorization: &MembershipChain,
329 identity_signer: &UserKeypair,
330 cancellation: DeviceJoinCancellation,
331 producer: DeviceJoinProducer,
332 revocation_executor: &dyn DeviceJoinWriteRevocationExecutor,
333 executor_grant: ProviderAdminGrantId,
334) -> Result<DeviceJoinProducerWriteRevocation, DeviceJoinError> {
335 let db = database.sqlite();
336 require_cancelled_outcome(&cancellation.outcome)?;
337 let attempt_ref = cancellation.outcome.attempt().clone();
338 let root = database
339 .local_store_root_ref()
340 .await
341 .map_err(database_error)?
342 .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
343 let attempt_context = crate::sync::storage::ProtocolObjectContext::signed_plaintext(
344 root.store_root_hash,
345 ProtocolObjectDomain::DeviceJoinAttempt,
346 );
347 let attempt_prefix =
348 crate::sync::store_commit::device_join_attempt_semantic_prefix(attempt_ref.attempt_id);
349 let attempt_bytes = storage
350 .read_protocol_object(&attempt_context, &attempt_ref.object, &attempt_prefix)
351 .await?;
352 let unverified_attempt: DeviceJoinAttempt = serde_json::from_slice(&attempt_bytes)?;
353 let owner = database
354 .activated_store_device_registration(unverified_attempt.owner_registration.clone())
355 .await
356 .map_err(database_error)?;
357 let attempt = crate::sync::store::load_verified_device_join_attempt_ref(
358 storage,
359 &root,
360 &attempt_ref,
361 &owner,
362 )
363 .await?
364 .value;
365 let outcome = crate::sync::store_objects::load_device_join_outcome_ref(
366 storage,
367 &root,
368 &cancellation.outcome,
369 &owner,
370 )
371 .await?
372 .value;
373 if !matches!(
374 outcome.body,
375 crate::sync::store_commit::DeviceJoinOutcomeBody::Cancelled
376 ) {
377 return Err(DeviceJoinError::AttemptMismatch);
378 }
379 let executor_admin = resolved_provider_admin(authorization, &executor_grant)?;
380 let executor = database
381 .activated_store_device_registration(executor_admin.administrator.clone())
382 .await
383 .map_err(database_error)?;
384 let local_device_id = db
385 .get_protocol_state(crate::database::LOCAL_DEVICE_ID_STATE_KEY)
386 .await
387 .map_err(database_error)?
388 .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
389 if executor.device_id.to_string() != local_device_id {
390 return Err(DeviceJoinError::ProviderAdministratorRequired);
391 }
392 let executor_signer = executor.device_signer(identity_signer)?;
393 let (authority, protected_slots, locator) = match producer {
394 DeviceJoinProducer::ProviderAdministrator => {
395 let DeviceProviderAdmissionChallenge::CrossPrincipal(challenge) =
396 &attempt.provider_approval.admission
397 else {
398 return Err(DeviceJoinError::CleanupMismatch);
399 };
400 (
401 ProviderWriteAuthorityRef::ProviderAdministrator(
402 attempt
403 .provider_approval
404 .request
405 .offer
406 .provider_admin
407 .grant_id
408 .clone(),
409 ),
410 vec![challenge.administrator_object.slot.clone()],
411 &attempt
412 .provider_approval
413 .request
414 .offer
415 .provider_admin
416 .access,
417 )
418 }
419 DeviceJoinProducer::Joiner => {
420 let mut slots = vec![
421 attempt.registration_slot.clone(),
422 attempt
423 .expected_registration
424 .acknowledgements
425 .first_slot()
426 .clone(),
427 ];
428 if let DeviceProviderResponseReservation::CrossPrincipal { response_slot } =
429 &attempt.provider_response
430 {
431 slots.push(response_slot.clone());
432 }
433 (
434 ProviderWriteAuthorityRef::MemberAccess(
435 attempt.provider_approval.access_grant.grant_ref.clone(),
436 ),
437 slots,
438 &attempt.provider_approval.access_grant.grant.locator,
439 )
440 }
441 };
442 let withdrawal = revocation_executor
443 .revoke_write_authority(producer, &authority, locator, &protected_slots)
444 .await?;
445 withdrawal
446 .verify_for_locator(locator)
447 .map_err(|_| DeviceJoinError::CleanupMismatch)?;
448 DeviceJoinProducerWriteRevocation::signed(
449 cancellation.outcome,
450 producer,
451 authority,
452 protected_slots,
453 withdrawal,
454 executor_grant,
455 executor_admin.administrator,
456 &executor,
457 &executor_signer,
458 )
459}
460
461#[allow(clippy::too_many_arguments)]
462pub(crate) async fn activate_device_join_cleanup(
463 database: &StoreDatabase,
464 storage: &dyn SyncStorage,
465 authorization: &MembershipChain,
466 identity_signer: &UserKeypair,
467 attempt_id: DeviceJoinAttemptId,
468 receipt: DeviceJoinCleanupReceipt,
469) -> Result<DeviceJoinCleanupActivation, DeviceJoinError> {
470 let db = database.sqlite();
471 let current = load_store_journal(db, attempt_id, DeviceJoinRole::Owner)
472 .await?
473 .ok_or(DeviceJoinError::JournalConflict)?;
474 if let DeviceJoinRoleProgress::Owner(OwnerJoinProgress::CleanupActivated(existing)) =
475 &*current.progress
476 {
477 if existing.receipt == receipt.receipt {
478 return Ok(existing.clone());
479 }
480 return Err(DeviceJoinError::JournalConflict);
481 }
482 let DeviceJoinRoleProgress::Owner(OwnerJoinProgress::CleanupReceipt(durable)) =
483 &*current.progress
484 else {
485 return Err(DeviceJoinError::JournalConflict);
486 };
487 if durable != &receipt {
488 return Err(DeviceJoinError::JournalConflict);
489 }
490 let local_device_id = db
491 .get_protocol_state(crate::database::LOCAL_DEVICE_ID_STATE_KEY)
492 .await
493 .map_err(database_error)?
494 .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
495 let plan = crate::sync::store::operations::prepare_plan(
496 database,
497 storage,
498 authorization,
499 &local_device_id,
500 identity_signer,
501 )
502 .await?;
503 let root = database
504 .local_store_root_ref()
505 .await
506 .map_err(database_error)?
507 .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
508 let context = crate::sync::storage::ProtocolObjectContext::signed_plaintext(
509 root.store_root_hash,
510 ProtocolObjectDomain::DeviceJoinCleanupReceipt,
511 );
512 let prefix = crate::sync::store_commit::device_join_cleanup_receipt_semantic_prefix(
513 receipt.receipt.attempt_id,
514 );
515 let bytes = storage
516 .read_protocol_object(&context, &receipt.receipt.object, &prefix)
517 .await?;
518 let receipt_object: DeviceJoinCleanupReceiptObject = serde_json::from_slice(&bytes)?;
519 let executor = database
520 .activated_store_device_registration(receipt_object.executor.clone())
521 .await
522 .map_err(database_error)?;
523 receipt.receipt.verify(&receipt_object, &executor)?;
524 if receipt_object.store_root_hash != root.store_root_hash
525 || plan.membership_state() != &receipt_object.membership
526 {
527 return Err(DeviceJoinError::CleanupMismatch);
528 }
529 let activation_ref = crate::sync::store::operations::activate_store_operation_commit(
530 database,
531 storage,
532 plan,
533 crate::sync::store::operations::StoreOperationBatch::CleanupReceipt(
534 receipt.receipt.clone(),
535 ),
536 )
537 .await?;
538 let activation = DeviceJoinCleanupActivation {
539 receipt: receipt.receipt,
540 activation: activation_ref,
541 };
542 advance_store_journal(
543 db,
544 ¤t,
545 DeviceJoinJournalRecord {
546 attempt_id,
547 progress: Box::new(DeviceJoinRoleProgress::Owner(
548 OwnerJoinProgress::CleanupActivated(activation.clone()),
549 )),
550 },
551 )
552 .await?;
553 Ok(activation)
554}
555
556pub(super) fn validate_member_for_join(
557 member_pubkey: &str,
558 members: &[(String, MemberRole)],
559) -> Result<(), DeviceJoinError> {
560 if members
561 .iter()
562 .any(|(pubkey, role)| pubkey == member_pubkey && role.can_write())
563 {
564 Ok(())
565 } else {
566 Err(DeviceJoinError::MemberNotEligible)
567 }
568}
569
570pub(super) fn canonical_cleanup_slots(
571 attempt: &DeviceJoinAttempt,
572) -> Result<Vec<ObjectSlot>, DeviceJoinError> {
573 let mut slots = vec![
574 attempt.registration_slot.clone(),
575 attempt
576 .expected_registration
577 .acknowledgements
578 .first_slot()
579 .clone(),
580 ];
581 match (
582 &attempt.provider_approval.admission,
583 &attempt.provider_response,
584 ) {
585 (
586 DeviceProviderAdmissionChallenge::SamePrincipal,
587 DeviceProviderResponseReservation::SamePrincipal,
588 ) => {}
589 (
590 DeviceProviderAdmissionChallenge::CrossPrincipal(challenge),
591 DeviceProviderResponseReservation::CrossPrincipal { response_slot },
592 ) => {
593 slots.push(challenge.administrator_object.slot.clone());
594 slots.push(response_slot.clone());
595 }
596 _ => return Err(DeviceJoinError::AttemptMismatch),
597 }
598 slots.sort();
599 require_distinct_slots(&slots)?;
600 Ok(slots)
601}
602
603pub(super) fn require_cancelled_outcome(
604 outcome: &DeviceJoinOutcomeRef,
605) -> Result<(), DeviceJoinError> {
606 if matches!(outcome, DeviceJoinOutcomeRef::Cancelled { .. }) {
607 Ok(())
608 } else {
609 Err(DeviceJoinError::AttemptMismatch)
610 }
611}
612
613pub(super) fn validate_terminals(
614 cancellation: &DeviceJoinOutcomeRef,
615 administrator: &ProviderAdminJoinTerminal,
616 joiner: &JoinerJoinTerminal,
617) -> Result<(), DeviceJoinError> {
618 let administrator_cancellation = match administrator {
619 ProviderAdminJoinTerminal::Completed(completion) => {
620 if completion.readiness.proof.attempt != *cancellation.attempt() {
621 return Err(DeviceJoinError::AttemptMismatch);
622 }
623 None
624 }
625 ProviderAdminJoinTerminal::Cancelled(closure) => Some(&closure.cancellation),
626 ProviderAdminJoinTerminal::WriteRevoked(revocation) => Some(&revocation.cancellation),
627 };
628 let joiner_cancellation = match joiner {
629 JoinerJoinTerminal::Ready(readiness) => {
630 if readiness.proof.attempt != *cancellation.attempt() {
631 return Err(DeviceJoinError::AttemptMismatch);
632 }
633 None
634 }
635 JoinerJoinTerminal::Cancelled(closure) => Some(&closure.cancellation),
636 JoinerJoinTerminal::WriteRevoked(revocation) => Some(&revocation.cancellation),
637 };
638 if administrator_cancellation.is_some_and(|value| value != cancellation)
639 || joiner_cancellation.is_some_and(|value| value != cancellation)
640 {
641 return Err(DeviceJoinError::AttemptMismatch);
642 }
643 Ok(())
644}
645
646async fn verify_cleanup_terminals(
647 database: &StoreDatabase,
648 administrator: &ProviderAdminJoinTerminal,
649 joiner: &JoinerJoinTerminal,
650) -> Result<(), DeviceJoinError> {
651 match administrator {
652 ProviderAdminJoinTerminal::Completed(_) => {}
653 ProviderAdminJoinTerminal::Cancelled(closure) => {
654 let registration = database
655 .activated_store_device_registration(closure.administrator_registration.clone())
656 .await
657 .map_err(database_error)?;
658 closure.verify(®istration)?;
659 }
660 ProviderAdminJoinTerminal::WriteRevoked(revocation) => {
661 let registration = database
662 .activated_store_device_registration(revocation.executor.clone())
663 .await
664 .map_err(database_error)?;
665 revocation.verify(®istration)?;
666 }
667 }
668 match joiner {
669 JoinerJoinTerminal::Ready(_) => {}
670 JoinerJoinTerminal::Cancelled(closure) => closure.verify()?,
671 JoinerJoinTerminal::WriteRevoked(revocation) => {
672 let registration = database
673 .activated_store_device_registration(revocation.executor.clone())
674 .await
675 .map_err(database_error)?;
676 revocation.verify(®istration)?;
677 }
678 }
679 Ok(())
680}
681
682pub(super) async fn ensure_exact_slot_absent(
683 storage: &dyn ExactSlotStorage,
684 slot: &ObjectSlot,
685) -> Result<(), DeviceJoinError> {
686 match storage.read_at(slot).await {
687 Err(crate::storage::cloud::CloudHomeError::NotFound(_)) => Ok(()),
688 Ok(_) => {
689 storage
690 .delete_at(slot)
691 .await
692 .map_err(|error| DeviceJoinError::Provider(error.to_string()))?;
693 match storage.read_at(slot).await {
694 Err(crate::storage::cloud::CloudHomeError::NotFound(_)) => Ok(()),
695 Ok(_) => Err(DeviceJoinError::CleanupMismatch),
696 Err(error) => Err(DeviceJoinError::Provider(error.to_string())),
697 }
698 }
699 Err(error) => Err(DeviceJoinError::Provider(error.to_string())),
700 }
701}
702
703pub(super) async fn observe_exact_slot(
704 storage: &dyn ExactSlotStorage,
705 slot: &ObjectSlot,
706) -> Result<SlotDisposition, DeviceJoinError> {
707 match storage.read_at(slot).await {
708 Err(crate::storage::cloud::CloudHomeError::NotFound(_)) => {
709 Ok(SlotDisposition::NeverCreated)
710 }
711 Ok(bytes) => Ok(SlotDisposition::Created(ExactObjectRef::new(
712 slot.clone(),
713 bytes.len() as u64,
714 ObjectHash::digest(&bytes),
715 ))),
716 Err(error) => Err(DeviceJoinError::Provider(error.to_string())),
717 }
718}