Skip to main content

coven_core/sync/store/device_join/
cleanup.rs

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    /// Idempotently withdraws the exact provider authority, then verifies that
53    /// the withdrawn authority cannot write any `protected_slots` before
54    /// returning its provider-specific evidence.
55    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, &current, 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        &current,
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(&registration)?;
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(&registration)?;
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(&registration)?;
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}