Skip to main content

coven_core/sync/
device_join.rs

1//! Provider-aware Store device admission and cancellation protocol.
2//!
3//! The wire values in this module are the transfer boundaries of the join
4//! exchange. Each value contains the exact signed value from the preceding
5//! boundary. Durable role journals store one closed progress value and advance
6//! only from the exact adjacent predecessor.
7
8use std::collections::BTreeSet;
9use std::path::{Path, PathBuf};
10
11use rusqlite::{Connection, OptionalExtension};
12use serde::{Deserialize, Serialize};
13
14use crate::database::Database;
15use crate::keys::{self, UserKeypair};
16use crate::storage::cloud::{ExactSlotStorage, ObjectSlot};
17use crate::sync::circle_control::StoreMembershipStateRef;
18use crate::sync::membership::{MemberRole, MembershipChain, MembershipGrantId};
19use crate::sync::provider::{
20    ActivatedStoreMemberProviderAccessGrant, CrossPrincipalProbeChallenge,
21    CrossPrincipalProbeReceipt, CrossPrincipalProbeResponse,
22    DeviceJoinChallengePublicationAuthorization, ProviderAccessGrantId, ProviderAccessWithdrawal,
23    ProviderAdminGrantId, ProviderAdminGrantRecord, StoreMemberProviderAccessGrant,
24    StoreMemberProviderAccessGrantRef,
25};
26use crate::sync::storage::{
27    CoordinationStorage, ExactObjectRef, ProviderDeviceBinding, StoreProviderBinding, SyncStorage,
28};
29use crate::sync::store_commit::{
30    DeviceJoinAttempt, DeviceJoinAttemptId, DeviceJoinAttemptRef, DeviceJoinOutcomeRef,
31    DeviceReadinessProof, ObjectHash, StoreBatchCommitRef, StoreDeviceRegistration,
32    StoreDeviceRegistrationRef, StoreRootRef, STORE_PROTOCOL_VERSION,
33};
34
35const OFFER_DOMAIN: &[u8] = b"coven.device-join-offer.v1\0";
36const ACCESS_REQUEST_DOMAIN: &[u8] = b"coven.device-provider-access-request.v1\0";
37const APPROVAL_DOMAIN: &[u8] = b"coven.device-provider-admission-approval.v1\0";
38const REGISTRATION_REQUEST_DOMAIN: &[u8] = b"coven.device-registration-request.v1\0";
39const ABANDONMENT_DOMAIN: &[u8] = b"coven.device-join-abandonment.v1\0";
40const PROVIDER_CLOSURE_DOMAIN: &[u8] = b"coven.device-join-provider-closure.v1\0";
41const JOINER_CLOSURE_DOMAIN: &[u8] = b"coven.device-join-joiner-closure.v1\0";
42const WRITE_REVOCATION_DOMAIN: &[u8] = b"coven.device-join-write-revocation.v1\0";
43const CLEANUP_RECEIPT_DOMAIN: &[u8] = b"coven.device-join-cleanup-receipt.v1\0";
44
45#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
46#[serde(deny_unknown_fields)]
47pub struct DeviceJoinOffer {
48    pub version: u32,
49    pub attempt_id: DeviceJoinAttemptId,
50    pub member_pubkey: String,
51    pub store_root: StoreRootRef,
52    pub provider: StoreProviderBinding,
53    pub attempt_slot: ObjectSlot,
54    pub outcome_slot: ObjectSlot,
55    pub owner_registration: StoreDeviceRegistrationRef,
56    pub owner_grant: MembershipGrantId,
57    pub provider_admin: Box<ProviderAdminGrantRecord>,
58    pub signature: String,
59}
60
61impl DeviceJoinOffer {
62    #[allow(clippy::too_many_arguments)]
63    pub fn signed(
64        attempt_id: DeviceJoinAttemptId,
65        member_pubkey: String,
66        store_root: StoreRootRef,
67        provider: StoreProviderBinding,
68        attempt_slot: ObjectSlot,
69        outcome_slot: ObjectSlot,
70        owner_registration: StoreDeviceRegistrationRef,
71        owner_grant: MembershipGrantId,
72        provider_admin: ProviderAdminGrantRecord,
73        owner: &StoreDeviceRegistration,
74        owner_device_signer: &UserKeypair,
75    ) -> Result<Self, DeviceJoinError> {
76        owner_registration.verify_registration(owner)?;
77        if keys::public_key_hex(owner_device_signer) != owner.device_signing_pubkey {
78            return Err(DeviceJoinError::InvalidSignature);
79        }
80        let mut value = Self {
81            version: STORE_PROTOCOL_VERSION,
82            attempt_id,
83            member_pubkey,
84            store_root,
85            provider,
86            attempt_slot,
87            outcome_slot,
88            owner_registration,
89            owner_grant,
90            provider_admin: Box::new(provider_admin),
91            signature: String::new(),
92        };
93        value.validate_shape()?;
94        value.signature = sign(owner_device_signer, OFFER_DOMAIN, &value.signed_fields());
95        Ok(value)
96    }
97
98    pub fn verify(&self, owner: &StoreDeviceRegistration) -> Result<(), DeviceJoinError> {
99        self.validate_shape()?;
100        self.owner_registration.verify_registration(owner)?;
101        verify_signature(
102            &owner.device_signing_pubkey,
103            &self.signature,
104            OFFER_DOMAIN,
105            &self.signed_fields(),
106        )
107    }
108
109    pub fn offer_hash(&self) -> ObjectHash {
110        ObjectHash::digest(&domain_json(OFFER_DOMAIN, &self.signed_fields()))
111    }
112
113    fn validate_shape(&self) -> Result<(), DeviceJoinError> {
114        if self.version != STORE_PROTOCOL_VERSION
115            || self.member_pubkey.is_empty()
116            || self.provider_admin.administrator != self.owner_registration
117                && self.provider_admin.administrator.device_id == self.owner_registration.device_id
118            || self.attempt_slot == self.outcome_slot
119        {
120            return Err(DeviceJoinError::OfferMismatch);
121        }
122        self.provider.validate()?;
123        self.provider_admin
124            .provider
125            .validate_for(&self.provider)
126            .map_err(DeviceJoinError::Storage)?;
127        if let crate::sync::provider::ProviderAdminGrantOrigin::Founder { root } =
128            &self.provider_admin.created_at
129        {
130            if root != &self.store_root {
131                return Err(DeviceJoinError::OfferMismatch);
132            }
133        }
134        Ok(())
135    }
136
137    fn signed_fields(&self) -> DeviceJoinOfferFields<'_> {
138        DeviceJoinOfferFields {
139            version: self.version,
140            attempt_id: self.attempt_id,
141            member_pubkey: &self.member_pubkey,
142            store_root: &self.store_root,
143            provider: &self.provider,
144            attempt_slot: &self.attempt_slot,
145            outcome_slot: &self.outcome_slot,
146            owner_registration: &self.owner_registration,
147            owner_grant: &self.owner_grant,
148            provider_admin: &self.provider_admin,
149        }
150    }
151}
152
153#[derive(Serialize)]
154struct DeviceJoinOfferFields<'a> {
155    version: u32,
156    attempt_id: DeviceJoinAttemptId,
157    member_pubkey: &'a str,
158    store_root: &'a StoreRootRef,
159    provider: &'a StoreProviderBinding,
160    attempt_slot: &'a ObjectSlot,
161    outcome_slot: &'a ObjectSlot,
162    owner_registration: &'a StoreDeviceRegistrationRef,
163    owner_grant: &'a MembershipGrantId,
164    provider_admin: &'a ProviderAdminGrantRecord,
165}
166
167#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
168#[serde(deny_unknown_fields)]
169pub struct DeviceProviderAccessRequest {
170    pub offer: Box<DeviceJoinOffer>,
171    pub peer_provider: ProviderDeviceBinding,
172    pub signature: String,
173}
174
175impl DeviceProviderAccessRequest {
176    pub fn signed(
177        offer: DeviceJoinOffer,
178        peer_provider: ProviderDeviceBinding,
179        member_signer: &UserKeypair,
180    ) -> Result<Self, DeviceJoinError> {
181        if keys::public_key_hex(member_signer) != offer.member_pubkey {
182            return Err(DeviceJoinError::InvalidSignature);
183        }
184        peer_provider.validate_for(&offer.provider)?;
185        let mut request = Self {
186            offer: Box::new(offer),
187            peer_provider,
188            signature: String::new(),
189        };
190        request.signature = sign(
191            member_signer,
192            ACCESS_REQUEST_DOMAIN,
193            &request.signed_fields(),
194        );
195        Ok(request)
196    }
197
198    pub fn verify(&self, owner: &StoreDeviceRegistration) -> Result<(), DeviceJoinError> {
199        self.offer.verify(owner)?;
200        self.peer_provider.validate_for(&self.offer.provider)?;
201        verify_signature(
202            &self.offer.member_pubkey,
203            &self.signature,
204            ACCESS_REQUEST_DOMAIN,
205            &self.signed_fields(),
206        )
207    }
208
209    pub fn request_hash(&self) -> ObjectHash {
210        ObjectHash::digest(&domain_json(ACCESS_REQUEST_DOMAIN, &self.signed_fields()))
211    }
212
213    fn signed_fields(&self) -> (&DeviceJoinOffer, &ProviderDeviceBinding) {
214        (&self.offer, &self.peer_provider)
215    }
216}
217
218#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
219#[serde(rename_all = "snake_case", deny_unknown_fields)]
220pub enum DeviceProviderAdmissionChallenge {
221    SamePrincipal,
222    CrossPrincipal(CrossPrincipalProbeChallenge),
223}
224
225#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
226#[serde(deny_unknown_fields)]
227pub struct DeviceProviderAdmissionApproval {
228    pub request: Box<DeviceProviderAccessRequest>,
229    pub access_grant: ActivatedStoreMemberProviderAccessGrant,
230    pub admission: DeviceProviderAdmissionChallenge,
231    pub signature: String,
232}
233
234impl DeviceProviderAdmissionApproval {
235    pub fn signed(
236        request: DeviceProviderAccessRequest,
237        access_grant: ActivatedStoreMemberProviderAccessGrant,
238        admission: DeviceProviderAdmissionChallenge,
239        administrator: &StoreDeviceRegistration,
240        administrator_device_signer: &UserKeypair,
241    ) -> Result<Self, DeviceJoinError> {
242        if keys::public_key_hex(administrator_device_signer) != administrator.device_signing_pubkey
243        {
244            return Err(DeviceJoinError::InvalidSignature);
245        }
246        let mut value = Self {
247            request: Box::new(request),
248            access_grant,
249            admission,
250            signature: String::new(),
251        };
252        value.validate_shape(administrator)?;
253        value.signature = sign(
254            administrator_device_signer,
255            APPROVAL_DOMAIN,
256            &value.signed_fields(),
257        );
258        Ok(value)
259    }
260
261    pub fn verify(
262        &self,
263        owner: &StoreDeviceRegistration,
264        administrator: &StoreDeviceRegistration,
265    ) -> Result<(), DeviceJoinError> {
266        self.request.verify(owner)?;
267        self.validate_shape(administrator)?;
268        verify_signature(
269            &administrator.device_signing_pubkey,
270            &self.signature,
271            APPROVAL_DOMAIN,
272            &self.signed_fields(),
273        )
274    }
275
276    fn validate_shape(
277        &self,
278        administrator: &StoreDeviceRegistration,
279    ) -> Result<(), DeviceJoinError> {
280        let offer = &self.request.offer;
281        if self.access_grant.grant.member_pubkey != offer.member_pubkey
282            || self.access_grant.grant.provider != self.request.peer_provider
283            || self.access_grant.grant_ref.grant_id != self.access_grant.grant.grant_id
284            || self.access_grant.grant_ref.grant_hash != self.access_grant.grant.grant_hash()
285            || self.access_grant.grant.administrator_grant != offer.provider_admin.grant_id
286            || self.access_grant.grant.administrator != offer.provider_admin.administrator
287            || !self
288                .access_grant
289                .activation
290                .coord
291                .policy()
292                .eq(&offer.store_root_policy_from_admin()?)
293        {
294            return Err(DeviceJoinError::ApprovalMismatch);
295        }
296        self.access_grant
297            .grant
298            .verify(&offer.provider, administrator)
299            .map_err(|error| DeviceJoinError::Provider(error.to_string()))?;
300        let same_principal = offer.provider_admin.provider == self.request.peer_provider;
301        if same_principal
302            != matches!(
303                self.admission,
304                DeviceProviderAdmissionChallenge::SamePrincipal
305            )
306        {
307            return Err(DeviceJoinError::ApprovalMismatch);
308        }
309        Ok(())
310    }
311
312    fn signed_fields(
313        &self,
314    ) -> (
315        &DeviceProviderAccessRequest,
316        &ActivatedStoreMemberProviderAccessGrant,
317        &DeviceProviderAdmissionChallenge,
318    ) {
319        (&self.request, &self.access_grant, &self.admission)
320    }
321}
322
323trait OfferPolicy {
324    fn store_root_policy_from_admin(&self) -> Result<crate::WritePolicy, DeviceJoinError>;
325}
326
327impl OfferPolicy for DeviceJoinOffer {
328    fn store_root_policy_from_admin(&self) -> Result<crate::WritePolicy, DeviceJoinError> {
329        match &self.provider_admin.created_at {
330            crate::sync::provider::ProviderAdminGrantOrigin::Founder { .. } => Ok(self
331                .provider_admin
332                .capability
333                .serial_coordination
334                .as_ref()
335                .map_or(crate::WritePolicy::MergeConcurrent, |_| {
336                    crate::WritePolicy::Serial
337                })),
338            crate::sync::provider::ProviderAdminGrantOrigin::MergeMembership { .. } => {
339                Ok(crate::WritePolicy::MergeConcurrent)
340            }
341            crate::sync::provider::ProviderAdminGrantOrigin::SerialCommit { .. } => {
342                Ok(crate::WritePolicy::Serial)
343            }
344        }
345    }
346}
347
348#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
349#[serde(rename_all = "snake_case", deny_unknown_fields)]
350pub enum DeviceProviderResponseReservation {
351    SamePrincipal,
352    CrossPrincipal { response_slot: ObjectSlot },
353}
354
355#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
356#[serde(deny_unknown_fields)]
357pub struct DeviceRegistrationRequest {
358    pub approval: Box<DeviceProviderAdmissionApproval>,
359    pub expected_registration: StoreDeviceRegistration,
360    pub registration_slot: ObjectSlot,
361    pub response: DeviceProviderResponseReservation,
362    pub signature: String,
363}
364
365impl DeviceRegistrationRequest {
366    pub fn signed(
367        approval: DeviceProviderAdmissionApproval,
368        expected_registration: StoreDeviceRegistration,
369        registration_slot: ObjectSlot,
370        response: DeviceProviderResponseReservation,
371        member_signer: &UserKeypair,
372    ) -> Result<Self, DeviceJoinError> {
373        if keys::public_key_hex(member_signer) != approval.request.offer.member_pubkey {
374            return Err(DeviceJoinError::InvalidSignature);
375        }
376        let mut value = Self {
377            approval: Box::new(approval),
378            expected_registration,
379            registration_slot,
380            response,
381            signature: String::new(),
382        };
383        value.validate_shape()?;
384        value.signature = sign(
385            member_signer,
386            REGISTRATION_REQUEST_DOMAIN,
387            &value.signed_fields(),
388        );
389        Ok(value)
390    }
391
392    pub fn verify(&self) -> Result<(), DeviceJoinError> {
393        self.validate_shape()?;
394        verify_signature(
395            &self.approval.request.offer.member_pubkey,
396            &self.signature,
397            REGISTRATION_REQUEST_DOMAIN,
398            &self.signed_fields(),
399        )
400    }
401
402    fn validate_shape(&self) -> Result<(), DeviceJoinError> {
403        let offer = &self.approval.request.offer;
404        if self.expected_registration.store_root != offer.store_root
405            || self.expected_registration.author_pubkey != offer.member_pubkey
406            || self.expected_registration.provider != self.approval.request.peer_provider
407            || self.registration_slot == offer.attempt_slot
408            || self.registration_slot == offer.outcome_slot
409        {
410            return Err(DeviceJoinError::RegistrationRequestMismatch);
411        }
412        match (
413            &self.approval.admission,
414            &self.response,
415            &self.expected_registration.origin,
416        ) {
417            (
418                DeviceProviderAdmissionChallenge::SamePrincipal,
419                DeviceProviderResponseReservation::SamePrincipal,
420                crate::sync::store_commit::StoreDeviceRegistrationOrigin::Join {
421                    attempt_id,
422                    attempt_slot,
423                    outcome_slot,
424                },
425            )
426            | (
427                DeviceProviderAdmissionChallenge::CrossPrincipal(_),
428                DeviceProviderResponseReservation::CrossPrincipal { .. },
429                crate::sync::store_commit::StoreDeviceRegistrationOrigin::Join {
430                    attempt_id,
431                    attempt_slot,
432                    outcome_slot,
433                },
434            ) if *attempt_id == offer.attempt_id
435                && attempt_slot == &offer.attempt_slot
436                && outcome_slot == &offer.outcome_slot => {}
437            _ => return Err(DeviceJoinError::RegistrationRequestMismatch),
438        }
439        let mut slots = vec![
440            offer.attempt_slot.clone(),
441            offer.outcome_slot.clone(),
442            self.registration_slot.clone(),
443            self.expected_registration
444                .acknowledgements
445                .first_slot()
446                .clone(),
447        ];
448        if let DeviceProviderAdmissionChallenge::CrossPrincipal(challenge) =
449            &self.approval.admission
450        {
451            slots.push(challenge.administrator_object.slot.clone());
452        }
453        if let DeviceProviderResponseReservation::CrossPrincipal { response_slot } = &self.response
454        {
455            slots.push(response_slot.clone());
456        }
457        require_distinct_slots(&slots)?;
458        Ok(())
459    }
460
461    fn signed_fields(
462        &self,
463    ) -> (
464        &DeviceProviderAdmissionApproval,
465        &StoreDeviceRegistration,
466        &ObjectSlot,
467        &DeviceProviderResponseReservation,
468    ) {
469        (
470            &self.approval,
471            &self.expected_registration,
472            &self.registration_slot,
473            &self.response,
474        )
475    }
476}
477
478trait DeviceStreamFirstSlot {
479    fn first_slot(&self) -> &ObjectSlot;
480}
481
482impl DeviceStreamFirstSlot for crate::sync::store_commit::DeviceStreamAnchor {
483    fn first_slot(&self) -> &ObjectSlot {
484        match self {
485            Self::StoreAnnouncements { first_slot }
486            | Self::StoreAcknowledgements { first_slot }
487            | Self::StoreSnapshots { first_slot }
488            | Self::CircleAcknowledgements { first_slot, .. }
489            | Self::CircleSnapshots { first_slot, .. } => first_slot,
490        }
491    }
492}
493
494#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
495#[serde(deny_unknown_fields)]
496pub struct ProvisionalDeviceBootstrap {
497    pub request: Box<DeviceRegistrationRequest>,
498    pub publication_authorization: DeviceJoinChallengePublicationAuthorization,
499}
500
501#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
502#[serde(rename_all = "snake_case", deny_unknown_fields)]
503pub enum DeviceProviderChallengePublication {
504    SamePrincipal,
505    CrossPrincipal {
506        challenge: CrossPrincipalProbeChallenge,
507    },
508}
509
510#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
511#[serde(deny_unknown_fields)]
512pub struct ProviderReadyDeviceBootstrap {
513    pub bootstrap: Box<ProvisionalDeviceBootstrap>,
514    pub challenge_publication: DeviceProviderChallengePublication,
515}
516
517#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
518#[serde(rename_all = "snake_case", deny_unknown_fields)]
519pub enum DeviceProviderReadiness {
520    SamePrincipal,
521    CrossPrincipal(CrossPrincipalProbeResponse),
522}
523
524#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
525#[serde(deny_unknown_fields)]
526pub struct DeviceJoinReadiness {
527    pub proof: DeviceReadinessProof,
528    pub provider: DeviceProviderReadiness,
529}
530
531#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
532#[serde(rename_all = "snake_case", deny_unknown_fields)]
533pub enum DeviceProviderAdmission {
534    SamePrincipal,
535    CrossPrincipal(CrossPrincipalProbeReceipt),
536}
537
538#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
539#[serde(deny_unknown_fields)]
540pub struct DeviceProviderAdmissionCompletion {
541    pub readiness: Box<DeviceJoinReadiness>,
542    pub admission: DeviceProviderAdmission,
543}
544
545#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
546#[serde(deny_unknown_fields)]
547pub struct DeviceJoinActivation {
548    pub outcome: DeviceJoinOutcomeRef,
549    pub outcome_activation: StoreBatchCommitRef,
550}
551
552#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
553#[serde(deny_unknown_fields)]
554pub struct DeviceJoinCancellation {
555    pub outcome: DeviceJoinOutcomeRef,
556    pub outcome_activation: StoreBatchCommitRef,
557}
558
559#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
560#[serde(deny_unknown_fields)]
561pub struct DeviceJoinAbandonmentRef {
562    pub attempt_id: DeviceJoinAttemptId,
563    pub abandonment_hash: ObjectHash,
564    pub object: ExactObjectRef,
565}
566
567impl DeviceJoinAbandonmentRef {
568    pub(crate) fn verify(
569        &self,
570        abandonment: &DeviceJoinAbandonmentObject,
571        owner: &StoreDeviceRegistration,
572    ) -> Result<(), DeviceJoinError> {
573        abandonment.owner_registration.verify_registration(owner)?;
574        if self.attempt_id != abandonment.attempt_id
575            || self.abandonment_hash != abandonment.abandonment_hash()
576        {
577            return Err(DeviceJoinError::AttemptMismatch);
578        }
579        verify_signature(
580            &owner.device_signing_pubkey,
581            &abandonment.signature,
582            ABANDONMENT_DOMAIN,
583            &abandonment.signed_fields(),
584        )
585    }
586}
587
588#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
589#[serde(deny_unknown_fields)]
590pub struct DeviceJoinAbandonmentObject {
591    pub version: u32,
592    pub store_root_hash: ObjectHash,
593    pub offer_hash: ObjectHash,
594    pub attempt_id: DeviceJoinAttemptId,
595    pub attempt_slot: ObjectSlot,
596    pub owner_registration: StoreDeviceRegistrationRef,
597    pub owner_grant: MembershipGrantId,
598    pub signature: String,
599}
600
601impl DeviceJoinAbandonmentObject {
602    pub fn signed(
603        offer: &DeviceJoinOffer,
604        owner: &StoreDeviceRegistration,
605        owner_device_signer: &UserKeypair,
606    ) -> Result<Self, DeviceJoinError> {
607        offer.verify(owner)?;
608        if keys::public_key_hex(owner_device_signer) != owner.device_signing_pubkey {
609            return Err(DeviceJoinError::InvalidSignature);
610        }
611        let mut value = Self {
612            version: STORE_PROTOCOL_VERSION,
613            store_root_hash: offer.store_root.store_root_hash,
614            offer_hash: offer.offer_hash(),
615            attempt_id: offer.attempt_id,
616            attempt_slot: offer.attempt_slot.clone(),
617            owner_registration: offer.owner_registration.clone(),
618            owner_grant: offer.owner_grant.clone(),
619            signature: String::new(),
620        };
621        value.signature = sign(
622            owner_device_signer,
623            ABANDONMENT_DOMAIN,
624            &value.signed_fields(),
625        );
626        Ok(value)
627    }
628
629    pub fn abandonment_hash(&self) -> ObjectHash {
630        ObjectHash::digest(&domain_json(ABANDONMENT_DOMAIN, &self.signed_fields()))
631    }
632
633    pub fn to_bytes(&self) -> Vec<u8> {
634        serde_json::to_vec(self).expect("device join abandonment serialization cannot fail")
635    }
636
637    fn signed_fields(
638        &self,
639    ) -> (
640        u32,
641        ObjectHash,
642        ObjectHash,
643        DeviceJoinAttemptId,
644        &ObjectSlot,
645        &StoreDeviceRegistrationRef,
646        &MembershipGrantId,
647    ) {
648        (
649            self.version,
650            self.store_root_hash,
651            self.offer_hash,
652            self.attempt_id,
653            &self.attempt_slot,
654            &self.owner_registration,
655            &self.owner_grant,
656        )
657    }
658}
659
660#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
661#[serde(deny_unknown_fields)]
662pub struct DeviceJoinAbandonment {
663    pub abandonment: DeviceJoinAbandonmentRef,
664    pub abandonment_activation: StoreBatchCommitRef,
665}
666
667#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
668#[serde(rename_all = "snake_case", deny_unknown_fields)]
669pub enum ProviderChallengeDisposition {
670    SamePrincipal,
671    NeverCreated,
672    Created(ExactObjectRef),
673    AlreadyDeleted(ExactObjectRef),
674}
675
676#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
677#[serde(deny_unknown_fields)]
678pub struct ProviderAdminJoinClosure {
679    pub cancellation: DeviceJoinOutcomeRef,
680    pub administrator_registration: StoreDeviceRegistrationRef,
681    pub challenge: ProviderChallengeDisposition,
682    pub prior_state_hash: ObjectHash,
683    pub signature: String,
684}
685
686impl ProviderAdminJoinClosure {
687    pub fn signed(
688        cancellation: DeviceJoinOutcomeRef,
689        administrator_registration: StoreDeviceRegistrationRef,
690        challenge: ProviderChallengeDisposition,
691        prior_state_hash: ObjectHash,
692        administrator: &StoreDeviceRegistration,
693        signer: &UserKeypair,
694    ) -> Result<Self, DeviceJoinError> {
695        require_cancelled_outcome(&cancellation)?;
696        administrator_registration.verify_registration(administrator)?;
697        if keys::public_key_hex(signer) != administrator.device_signing_pubkey {
698            return Err(DeviceJoinError::InvalidSignature);
699        }
700        let mut value = Self {
701            cancellation,
702            administrator_registration,
703            challenge,
704            prior_state_hash,
705            signature: String::new(),
706        };
707        value.signature = sign(signer, PROVIDER_CLOSURE_DOMAIN, &value.signed_fields());
708        Ok(value)
709    }
710
711    pub fn verify(&self, administrator: &StoreDeviceRegistration) -> Result<(), DeviceJoinError> {
712        require_cancelled_outcome(&self.cancellation)?;
713        self.administrator_registration
714            .verify_registration(administrator)?;
715        verify_signature(
716            &administrator.device_signing_pubkey,
717            &self.signature,
718            PROVIDER_CLOSURE_DOMAIN,
719            &self.signed_fields(),
720        )
721    }
722
723    fn signed_fields(
724        &self,
725    ) -> (
726        &DeviceJoinOutcomeRef,
727        &StoreDeviceRegistrationRef,
728        &ProviderChallengeDisposition,
729        ObjectHash,
730    ) {
731        (
732            &self.cancellation,
733            &self.administrator_registration,
734            &self.challenge,
735            self.prior_state_hash,
736        )
737    }
738}
739
740#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
741#[serde(rename_all = "snake_case", deny_unknown_fields)]
742pub enum SlotDisposition {
743    NeverCreated,
744    Created(ExactObjectRef),
745}
746
747#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
748#[serde(rename_all = "snake_case", deny_unknown_fields)]
749pub enum JoinerResponseDisposition {
750    SamePrincipal,
751    Slot(SlotDisposition),
752}
753
754#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
755#[serde(deny_unknown_fields)]
756pub struct JoinerJoinClosure {
757    pub cancellation: DeviceJoinOutcomeRef,
758    pub expected_registration: StoreDeviceRegistration,
759    pub registration: SlotDisposition,
760    pub initial_ack: SlotDisposition,
761    pub response: JoinerResponseDisposition,
762    pub prior_state_hash: ObjectHash,
763    pub signature: String,
764}
765
766impl JoinerJoinClosure {
767    #[allow(clippy::too_many_arguments)]
768    pub fn signed(
769        cancellation: DeviceJoinOutcomeRef,
770        expected_registration: StoreDeviceRegistration,
771        registration: SlotDisposition,
772        initial_ack: SlotDisposition,
773        response: JoinerResponseDisposition,
774        prior_state_hash: ObjectHash,
775        signer: &UserKeypair,
776    ) -> Result<Self, DeviceJoinError> {
777        require_cancelled_outcome(&cancellation)?;
778        if keys::public_key_hex(signer) != expected_registration.device_signing_pubkey {
779            return Err(DeviceJoinError::InvalidSignature);
780        }
781        let mut value = Self {
782            cancellation,
783            expected_registration,
784            registration,
785            initial_ack,
786            response,
787            prior_state_hash,
788            signature: String::new(),
789        };
790        value.signature = sign(signer, JOINER_CLOSURE_DOMAIN, &value.signed_fields());
791        Ok(value)
792    }
793
794    pub fn verify(&self) -> Result<(), DeviceJoinError> {
795        require_cancelled_outcome(&self.cancellation)?;
796        verify_signature(
797            &self.expected_registration.device_signing_pubkey,
798            &self.signature,
799            JOINER_CLOSURE_DOMAIN,
800            &self.signed_fields(),
801        )
802    }
803
804    fn signed_fields(
805        &self,
806    ) -> (
807        &DeviceJoinOutcomeRef,
808        &StoreDeviceRegistration,
809        &SlotDisposition,
810        &SlotDisposition,
811        &JoinerResponseDisposition,
812        ObjectHash,
813    ) {
814        (
815            &self.cancellation,
816            &self.expected_registration,
817            &self.registration,
818            &self.initial_ack,
819            &self.response,
820            self.prior_state_hash,
821        )
822    }
823}
824
825#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
826#[serde(rename_all = "snake_case")]
827pub enum DeviceJoinProducer {
828    ProviderAdministrator,
829    Joiner,
830}
831
832#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
833#[serde(rename_all = "snake_case", deny_unknown_fields)]
834pub enum ProviderWriteAuthorityRef {
835    ProviderAdministrator(ProviderAdminGrantId),
836    MemberAccess(StoreMemberProviderAccessGrantRef),
837}
838
839#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
840#[serde(deny_unknown_fields)]
841pub struct DeviceJoinProducerWriteRevocation {
842    pub cancellation: DeviceJoinOutcomeRef,
843    pub producer: DeviceJoinProducer,
844    pub authority: ProviderWriteAuthorityRef,
845    pub protected_slots: Vec<ObjectSlot>,
846    pub withdrawal: ProviderAccessWithdrawal,
847    pub executor_grant: ProviderAdminGrantId,
848    pub executor: StoreDeviceRegistrationRef,
849    pub signature: String,
850}
851
852impl DeviceJoinProducerWriteRevocation {
853    pub fn signed(
854        cancellation: DeviceJoinOutcomeRef,
855        producer: DeviceJoinProducer,
856        authority: ProviderWriteAuthorityRef,
857        mut protected_slots: Vec<ObjectSlot>,
858        withdrawal: ProviderAccessWithdrawal,
859        executor_grant: ProviderAdminGrantId,
860        executor: StoreDeviceRegistrationRef,
861        executor_registration: &StoreDeviceRegistration,
862        executor_signer: &UserKeypair,
863    ) -> Result<Self, DeviceJoinError> {
864        require_cancelled_outcome(&cancellation)?;
865        executor.verify_registration(executor_registration)?;
866        if keys::public_key_hex(executor_signer) != executor_registration.device_signing_pubkey {
867            return Err(DeviceJoinError::InvalidSignature);
868        }
869        protected_slots.sort();
870        if protected_slots.is_empty() || protected_slots.windows(2).any(|pair| pair[0] == pair[1]) {
871            return Err(DeviceJoinError::CleanupMismatch);
872        }
873        let mut value = Self {
874            cancellation,
875            producer,
876            authority,
877            protected_slots,
878            withdrawal,
879            executor_grant,
880            executor,
881            signature: String::new(),
882        };
883        value.signature = sign(
884            executor_signer,
885            WRITE_REVOCATION_DOMAIN,
886            &value.signed_fields(),
887        );
888        Ok(value)
889    }
890
891    pub fn verify(&self, executor: &StoreDeviceRegistration) -> Result<(), DeviceJoinError> {
892        require_cancelled_outcome(&self.cancellation)?;
893        self.executor.verify_registration(executor)?;
894        verify_signature(
895            &executor.device_signing_pubkey,
896            &self.signature,
897            WRITE_REVOCATION_DOMAIN,
898            &self.signed_fields(),
899        )
900    }
901
902    fn signed_fields(
903        &self,
904    ) -> (
905        &DeviceJoinOutcomeRef,
906        DeviceJoinProducer,
907        &ProviderWriteAuthorityRef,
908        &[ObjectSlot],
909        &ProviderAccessWithdrawal,
910        &ProviderAdminGrantId,
911        &StoreDeviceRegistrationRef,
912    ) {
913        (
914            &self.cancellation,
915            self.producer,
916            &self.authority,
917            &self.protected_slots,
918            &self.withdrawal,
919            &self.executor_grant,
920            &self.executor,
921        )
922    }
923}
924
925#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
926#[serde(rename_all = "snake_case", deny_unknown_fields)]
927pub enum ProviderAdminJoinTerminal {
928    Completed(DeviceProviderAdmissionCompletion),
929    Cancelled(ProviderAdminJoinClosure),
930    WriteRevoked(DeviceJoinProducerWriteRevocation),
931}
932
933#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
934#[serde(rename_all = "snake_case", deny_unknown_fields)]
935pub enum JoinerJoinTerminal {
936    Ready(DeviceJoinReadiness),
937    Cancelled(JoinerJoinClosure),
938    WriteRevoked(DeviceJoinProducerWriteRevocation),
939}
940
941#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
942#[serde(deny_unknown_fields)]
943pub struct DeviceJoinCleanupReceiptRef {
944    pub attempt_id: DeviceJoinAttemptId,
945    pub receipt_hash: ObjectHash,
946    pub object: ExactObjectRef,
947}
948
949impl DeviceJoinCleanupReceiptRef {
950    pub(crate) fn verify(
951        &self,
952        receipt: &DeviceJoinCleanupReceiptObject,
953        executor: &StoreDeviceRegistration,
954    ) -> Result<(), DeviceJoinError> {
955        receipt.executor.verify_registration(executor)?;
956        if self.attempt_id != receipt.cancellation.attempt().attempt_id
957            || self.receipt_hash != receipt.receipt_hash()
958        {
959            return Err(DeviceJoinError::CleanupMismatch);
960        }
961        verify_signature(
962            &executor.device_signing_pubkey,
963            &receipt.signature,
964            CLEANUP_RECEIPT_DOMAIN,
965            &receipt.signed_fields(),
966        )
967    }
968}
969
970#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
971#[serde(deny_unknown_fields)]
972pub struct DeviceJoinCleanupReceiptObject {
973    pub version: u32,
974    pub store_root_hash: ObjectHash,
975    pub cancellation: DeviceJoinOutcomeRef,
976    pub administrator_terminal: ProviderAdminJoinTerminal,
977    pub joiner_terminal: JoinerJoinTerminal,
978    pub deleted_slots: Vec<ObjectSlot>,
979    pub membership: StoreMembershipStateRef,
980    pub provider_admin_grant: ProviderAdminGrantId,
981    pub executor: StoreDeviceRegistrationRef,
982    pub signature: String,
983}
984
985impl DeviceJoinCleanupReceiptObject {
986    #[allow(clippy::too_many_arguments)]
987    pub fn signed(
988        attempt: &DeviceJoinAttempt,
989        cancellation: DeviceJoinOutcomeRef,
990        administrator_terminal: ProviderAdminJoinTerminal,
991        joiner_terminal: JoinerJoinTerminal,
992        deleted_slots: Vec<ObjectSlot>,
993        membership: StoreMembershipStateRef,
994        provider_admin_grant: ProviderAdminGrantId,
995        executor: StoreDeviceRegistrationRef,
996        executor_registration: &StoreDeviceRegistration,
997        executor_signer: &UserKeypair,
998    ) -> Result<Self, DeviceJoinError> {
999        require_cancelled_outcome(&cancellation)?;
1000        if cancellation.attempt().attempt_id != attempt.attempt_id {
1001            return Err(DeviceJoinError::AttemptMismatch);
1002        }
1003        executor.verify_registration(executor_registration)?;
1004        if executor_registration.store_root != attempt.store_root
1005            || keys::public_key_hex(executor_signer) != executor_registration.device_signing_pubkey
1006        {
1007            return Err(DeviceJoinError::InvalidSignature);
1008        }
1009        let mut value = Self {
1010            version: STORE_PROTOCOL_VERSION,
1011            store_root_hash: attempt.store_root.store_root_hash,
1012            cancellation,
1013            administrator_terminal,
1014            joiner_terminal,
1015            deleted_slots,
1016            membership,
1017            provider_admin_grant,
1018            executor,
1019            signature: String::new(),
1020        };
1021        value.validate_shape(attempt)?;
1022        value.signature = sign(
1023            executor_signer,
1024            CLEANUP_RECEIPT_DOMAIN,
1025            &value.signed_fields(),
1026        );
1027        Ok(value)
1028    }
1029
1030    pub fn receipt_hash(&self) -> ObjectHash {
1031        ObjectHash::digest(&domain_json(CLEANUP_RECEIPT_DOMAIN, &self.signed_fields()))
1032    }
1033
1034    pub(crate) fn verify(
1035        &self,
1036        attempt: &DeviceJoinAttempt,
1037        executor: &StoreDeviceRegistration,
1038    ) -> Result<(), DeviceJoinError> {
1039        if self.version != STORE_PROTOCOL_VERSION
1040            || self.store_root_hash != attempt.store_root.store_root_hash
1041        {
1042            return Err(DeviceJoinError::CleanupMismatch);
1043        }
1044        let mut verified = self.clone();
1045        verified.validate_shape(attempt)?;
1046        verify_signature(
1047            &executor.device_signing_pubkey,
1048            &self.signature,
1049            CLEANUP_RECEIPT_DOMAIN,
1050            &self.signed_fields(),
1051        )
1052    }
1053
1054    pub fn to_bytes(&self) -> Vec<u8> {
1055        serde_json::to_vec(self).expect("device join cleanup receipt serialization cannot fail")
1056    }
1057
1058    fn validate_shape(&mut self, attempt: &DeviceJoinAttempt) -> Result<(), DeviceJoinError> {
1059        validate_terminals(
1060            &self.cancellation,
1061            &self.administrator_terminal,
1062            &self.joiner_terminal,
1063        )?;
1064        let expected = canonical_cleanup_slots(attempt)?;
1065        self.deleted_slots.sort();
1066        if self.deleted_slots != expected {
1067            return Err(DeviceJoinError::CleanupMismatch);
1068        }
1069        Ok(())
1070    }
1071
1072    fn signed_fields(
1073        &self,
1074    ) -> (
1075        u32,
1076        ObjectHash,
1077        &DeviceJoinOutcomeRef,
1078        &ProviderAdminJoinTerminal,
1079        &JoinerJoinTerminal,
1080        &[ObjectSlot],
1081        &StoreMembershipStateRef,
1082        &ProviderAdminGrantId,
1083        &StoreDeviceRegistrationRef,
1084    ) {
1085        (
1086            self.version,
1087            self.store_root_hash,
1088            &self.cancellation,
1089            &self.administrator_terminal,
1090            &self.joiner_terminal,
1091            &self.deleted_slots,
1092            &self.membership,
1093            &self.provider_admin_grant,
1094            &self.executor,
1095        )
1096    }
1097}
1098
1099#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1100#[serde(deny_unknown_fields)]
1101pub struct DeviceJoinCleanupReceipt {
1102    pub receipt: DeviceJoinCleanupReceiptRef,
1103}
1104
1105#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1106#[serde(deny_unknown_fields)]
1107pub struct DeviceJoinCleanupActivation {
1108    pub receipt: DeviceJoinCleanupReceiptRef,
1109    pub activation: StoreBatchCommitRef,
1110}
1111
1112#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1113#[serde(deny_unknown_fields)]
1114pub struct JoinedStore {
1115    pub store_root: StoreRootRef,
1116    pub registration: StoreDeviceRegistrationRef,
1117    pub activation: DeviceJoinActivation,
1118}
1119
1120#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1121#[serde(rename_all = "snake_case", deny_unknown_fields)]
1122pub enum DeviceJoinCleanupProgress {
1123    AwaitingBoth,
1124    AwaitingAdministrator {
1125        joiner: JoinerJoinTerminal,
1126    },
1127    AwaitingJoiner {
1128        administrator: ProviderAdminJoinTerminal,
1129    },
1130    Ready {
1131        administrator: ProviderAdminJoinTerminal,
1132        joiner: JoinerJoinTerminal,
1133    },
1134}
1135
1136#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1137#[serde(rename_all = "snake_case", deny_unknown_fields)]
1138pub enum DeviceJoinStatus {
1139    OperationInProgress {
1140        attempt_id: DeviceJoinAttemptId,
1141    },
1142    AwaitingAccessRequest {
1143        offer: DeviceJoinOffer,
1144    },
1145    AwaitingProviderAdmission {
1146        request: DeviceProviderAccessRequest,
1147    },
1148    AwaitingRegistrationRequest {
1149        approval: DeviceProviderAdmissionApproval,
1150    },
1151    AwaitingBootstrap {
1152        request: DeviceRegistrationRequest,
1153    },
1154    AwaitingChallengePublication {
1155        bootstrap: ProvisionalDeviceBootstrap,
1156    },
1157    AwaitingReadiness {
1158        bootstrap: ProviderReadyDeviceBootstrap,
1159    },
1160    AwaitingProviderCompletion {
1161        readiness: DeviceJoinReadiness,
1162    },
1163    AwaitingActivation {
1164        completion: DeviceProviderAdmissionCompletion,
1165    },
1166    AwaitingCompletion {
1167        activation: DeviceJoinActivation,
1168    },
1169    Activated {
1170        store: JoinedStore,
1171    },
1172    Abandoned {
1173        abandonment: DeviceJoinAbandonment,
1174    },
1175    ProviderAccessGrantCreatePending {
1176        request: DeviceProviderAccessRequest,
1177        grant: StoreMemberProviderAccessGrant,
1178    },
1179    AbandonmentCreatePending {
1180        abandonment: DeviceJoinAbandonmentRef,
1181    },
1182    CancellationCreatePending {
1183        cancellation: DeviceJoinOutcomeRef,
1184    },
1185    ProviderClosurePending {
1186        cancellation: DeviceJoinCancellation,
1187        producer: DeviceJoinProducer,
1188    },
1189    ProviderClosed {
1190        terminal: ProviderAdminJoinTerminal,
1191    },
1192    JoinerClosed {
1193        terminal: JoinerJoinTerminal,
1194    },
1195    CleanupReceiptCreatePending {
1196        cancellation: DeviceJoinCancellation,
1197        receipt: DeviceJoinCleanupReceiptRef,
1198    },
1199    Cancelled {
1200        cancellation: DeviceJoinCancellation,
1201    },
1202    CleanupPending {
1203        cancellation: DeviceJoinCancellation,
1204        progress: DeviceJoinCleanupProgress,
1205    },
1206    AwaitingCleanupActivation {
1207        receipt: DeviceJoinCleanupReceipt,
1208    },
1209    CleanupActivated {
1210        activation: DeviceJoinCleanupActivation,
1211    },
1212}
1213
1214#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1215#[serde(rename_all = "snake_case", deny_unknown_fields)]
1216pub enum DeviceJoinAction {
1217    TransferOffer(DeviceJoinOffer),
1218    TransferProviderAccessRequest(DeviceProviderAccessRequest),
1219    TransferProviderAdmissionApproval(DeviceProviderAdmissionApproval),
1220    TransferRegistrationRequest(DeviceRegistrationRequest),
1221    TransferProvisionalBootstrap(ProvisionalDeviceBootstrap),
1222    TransferProviderReadyBootstrap(ProviderReadyDeviceBootstrap),
1223    TransferReadiness(DeviceJoinReadiness),
1224    TransferProviderAdmissionCompletion(DeviceProviderAdmissionCompletion),
1225    TransferActivation(DeviceJoinActivation),
1226    TransferAbandonment(DeviceJoinAbandonment),
1227    TransferCancellation(DeviceJoinCancellation),
1228    TransferProviderAdminClosure(ProviderAdminJoinClosure),
1229    TransferJoinerClosure(JoinerJoinClosure),
1230    TransferCleanupReceipt(DeviceJoinCleanupReceipt),
1231    TransferCleanupActivation(DeviceJoinCleanupActivation),
1232    Complete(DeviceJoinActivation),
1233    RetryProviderOperation { attempt_id: DeviceJoinAttemptId },
1234}
1235
1236#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1237#[serde(rename_all = "snake_case", deny_unknown_fields)]
1238pub enum OwnerJoinProgress {
1239    Offered(DeviceJoinOffer),
1240    RegistrationRequested(DeviceRegistrationRequest),
1241    AbandonmentCreateIntent {
1242        offer: DeviceJoinOffer,
1243        abandonment: DeviceJoinAbandonmentRef,
1244        prepared: PreparedDeviceJoinObject,
1245    },
1246    AttemptActivated(ProvisionalDeviceBootstrap),
1247    ProviderReady(ProviderReadyDeviceBootstrap),
1248    AdmissionCompleted(DeviceProviderAdmissionCompletion),
1249    CancellationCreateIntent {
1250        attempt: DeviceJoinAttemptRef,
1251        cancellation: DeviceJoinOutcomeRef,
1252        prepared: PreparedDeviceJoinObject,
1253    },
1254    ActivationPrepared(DeviceJoinActivation),
1255    Activated(JoinedStore),
1256    Abandoned(DeviceJoinAbandonment),
1257    Cancelled(DeviceJoinCancellation),
1258    CleanupReceiptCreateIntent {
1259        cancellation: DeviceJoinCancellation,
1260        receipt: DeviceJoinCleanupReceiptRef,
1261        prepared: PreparedDeviceJoinObject,
1262    },
1263    CleanupReceipt(DeviceJoinCleanupReceipt),
1264    CleanupActivated(DeviceJoinCleanupActivation),
1265    CancelledComplete(DeviceJoinCleanupActivation),
1266}
1267
1268#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1269#[serde(rename_all = "snake_case", deny_unknown_fields)]
1270pub enum ProviderAdminJoinProgress {
1271    AccessRequested(DeviceProviderAccessRequest),
1272    AccessGrantPrepared {
1273        request: DeviceProviderAccessRequest,
1274        grant: StoreMemberProviderAccessGrant,
1275        prepared: PreparedDeviceJoinObject,
1276    },
1277    ApprovalPrepared(DeviceProviderAdmissionApproval),
1278    AttemptObserved(ProvisionalDeviceBootstrap),
1279    ChallengeCreateIntent(ProvisionalDeviceBootstrap),
1280    ProviderReady(ProviderReadyDeviceBootstrap),
1281    ResponseObserved(DeviceJoinReadiness),
1282    CleanupIntent {
1283        cancellation: DeviceJoinCancellation,
1284        challenge: ProviderChallengeDisposition,
1285        prior_state_hash: ObjectHash,
1286    },
1287    Completed(DeviceProviderAdmissionCompletion),
1288    Cancelled(ProviderAdminJoinClosure),
1289    WriteRevoked(DeviceJoinProducerWriteRevocation),
1290}
1291
1292#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1293#[serde(deny_unknown_fields)]
1294pub struct PreparedDeviceJoinObject {
1295    pub object: ExactObjectRef,
1296    pub stored_bytes: Vec<u8>,
1297}
1298
1299impl PreparedDeviceJoinObject {
1300    fn from_prepared(prepared: &crate::sync::storage::PreparedExactObject) -> Self {
1301        Self {
1302            object: prepared.reference().clone(),
1303            stored_bytes: prepared.stored_bytes().to_vec(),
1304        }
1305    }
1306}
1307
1308#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1309#[serde(rename_all = "snake_case", deny_unknown_fields)]
1310pub enum JoinerJoinProgress {
1311    OfferReceived(DeviceJoinOffer),
1312    AccessRequested(DeviceProviderAccessRequest),
1313    ApprovalReceived(DeviceProviderAdmissionApproval),
1314    RegistrationPrepared(DeviceRegistrationRequest),
1315    ProviderReady(ProviderReadyDeviceBootstrap),
1316    RegistrationCreateIntent(ProviderReadyDeviceBootstrap),
1317    RegistrationCreated(StoreDeviceRegistrationRef),
1318    AckCreateIntent(StoreDeviceRegistrationRef),
1319    AckCreated(crate::sync::store_commit::StoreAckRef),
1320    ResponseCreateIntent(DeviceJoinReadiness),
1321    Ready(DeviceJoinReadiness),
1322    ActivationObserved(DeviceJoinActivation),
1323    Activated(JoinedStore),
1324    Abandoned(DeviceJoinAbandonment),
1325    CleanupIntent {
1326        cancellation: DeviceJoinCancellation,
1327        registration: SlotDisposition,
1328        initial_ack: SlotDisposition,
1329        response: JoinerResponseDisposition,
1330        prior_state_hash: ObjectHash,
1331    },
1332    Cancelled(JoinerJoinClosure),
1333    WriteRevoked(DeviceJoinProducerWriteRevocation),
1334    CleanupActivated(DeviceJoinCleanupActivation),
1335    CancelledComplete(DeviceJoinCleanupActivation),
1336}
1337
1338#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1339#[serde(rename_all = "snake_case", deny_unknown_fields)]
1340pub enum DeviceJoinRoleProgress {
1341    Owner(OwnerJoinProgress),
1342    ProviderAdministrator(ProviderAdminJoinProgress),
1343    Joiner(JoinerJoinProgress),
1344}
1345
1346impl DeviceJoinRoleProgress {
1347    fn role_name(&self) -> &'static str {
1348        match self {
1349            Self::Owner(_) => "owner",
1350            Self::ProviderAdministrator(_) => "provider_administrator",
1351            Self::Joiner(_) => "joiner",
1352        }
1353    }
1354
1355    fn validate_transition(&self, next: &Self) -> Result<(), DeviceJoinError> {
1356        let adjacent = match (self, next) {
1357            (Self::Owner(previous), Self::Owner(next)) => owner_adjacent(previous, next),
1358            (Self::ProviderAdministrator(previous), Self::ProviderAdministrator(next)) => {
1359                provider_admin_adjacent(previous, next)
1360            }
1361            (Self::Joiner(previous), Self::Joiner(next)) => joiner_adjacent(previous, next),
1362            _ => false,
1363        };
1364        if adjacent {
1365            Ok(())
1366        } else {
1367            Err(DeviceJoinError::NonAdjacentJournalTransition)
1368        }
1369    }
1370}
1371
1372#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1373#[serde(deny_unknown_fields)]
1374pub struct DeviceJoinJournalRecord {
1375    pub attempt_id: DeviceJoinAttemptId,
1376    pub progress: Box<DeviceJoinRoleProgress>,
1377}
1378
1379/// Durable role journal. Each row stores a closed progress value; SQLite's
1380/// compare-and-swap update rejects stale or skipped transitions.
1381#[derive(Clone, Debug)]
1382pub struct DeviceJoinJournalDatabase {
1383    path: PathBuf,
1384}
1385
1386impl DeviceJoinJournalDatabase {
1387    pub fn open(path: impl AsRef<Path>) -> Result<Self, DeviceJoinError> {
1388        let path = path.as_ref().to_path_buf();
1389        let connection = Connection::open(&path)?;
1390        connection.execute_batch(
1391            "PRAGMA foreign_keys = ON;
1392             CREATE TABLE IF NOT EXISTS device_join_journals (
1393                 attempt_id TEXT NOT NULL,
1394                 role TEXT NOT NULL,
1395                 payload TEXT NOT NULL,
1396                 PRIMARY KEY (attempt_id, role)
1397             ) STRICT, WITHOUT ROWID;
1398             CREATE TABLE IF NOT EXISTS pending_join_transfers (
1399                 attempt_id TEXT PRIMARY KEY,
1400                 payload_hash TEXT NOT NULL,
1401                 payload TEXT NOT NULL
1402             ) STRICT, WITHOUT ROWID;",
1403        )?;
1404        Ok(Self { path })
1405    }
1406
1407    pub(crate) fn path(&self) -> &Path {
1408        &self.path
1409    }
1410
1411    pub fn begin(
1412        &self,
1413        record: DeviceJoinJournalRecord,
1414    ) -> Result<DeviceJoinJournalRecord, DeviceJoinError> {
1415        validate_initial_progress(&record.progress)?;
1416        let connection = Connection::open(&self.path)?;
1417        let tx = connection.unchecked_transaction()?;
1418        let attempt_id = attempt_key(record.attempt_id);
1419        let role = record.progress.role_name();
1420        let payload = serde_json::to_string(&record)?;
1421        tx.execute(
1422            "INSERT OR IGNORE INTO device_join_journals (attempt_id, role, payload)
1423             VALUES (?1, ?2, ?3)",
1424            (&attempt_id, role, &payload),
1425        )?;
1426        let actual: String = tx.query_row(
1427            "SELECT payload FROM device_join_journals WHERE attempt_id = ?1 AND role = ?2",
1428            (&attempt_id, role),
1429            |row| row.get(0),
1430        )?;
1431        tx.commit()?;
1432        let actual = serde_json::from_str::<DeviceJoinJournalRecord>(&actual)?;
1433        if actual != record {
1434            return Err(DeviceJoinError::JournalConflict);
1435        }
1436        Ok(actual)
1437    }
1438
1439    pub fn load(
1440        &self,
1441        attempt_id: DeviceJoinAttemptId,
1442        role: DeviceJoinRole,
1443    ) -> Result<Option<DeviceJoinJournalRecord>, DeviceJoinError> {
1444        let connection = Connection::open(&self.path)?;
1445        let raw = connection
1446            .query_row(
1447                "SELECT payload FROM device_join_journals WHERE attempt_id = ?1 AND role = ?2",
1448                (attempt_key(attempt_id), role.as_str()),
1449                |row| row.get::<_, String>(0),
1450            )
1451            .optional()?;
1452        raw.map(|value| serde_json::from_str(&value).map_err(DeviceJoinError::from))
1453            .transpose()
1454    }
1455
1456    pub fn advance(
1457        &self,
1458        previous: &DeviceJoinJournalRecord,
1459        next: DeviceJoinJournalRecord,
1460    ) -> Result<(), DeviceJoinError> {
1461        if previous.attempt_id != next.attempt_id {
1462            return Err(DeviceJoinError::JournalConflict);
1463        }
1464        previous.progress.validate_transition(&next.progress)?;
1465        let previous_payload = serde_json::to_string(previous)?;
1466        let next_payload = serde_json::to_string(&next)?;
1467        let connection = Connection::open(&self.path)?;
1468        let changed = connection.execute(
1469            "UPDATE device_join_journals SET payload = ?1
1470             WHERE attempt_id = ?2 AND role = ?3 AND payload = ?4",
1471            (
1472                &next_payload,
1473                attempt_key(previous.attempt_id),
1474                previous.progress.role_name(),
1475                &previous_payload,
1476            ),
1477        )?;
1478        if changed != 1 {
1479            return Err(DeviceJoinError::JournalConflict);
1480        }
1481        Ok(())
1482    }
1483
1484    pub fn stage_pending_transfer(
1485        &self,
1486        record: &DeviceJoinJournalRecord,
1487    ) -> Result<ObjectHash, DeviceJoinError> {
1488        if !matches!(*record.progress, DeviceJoinRoleProgress::Joiner(_)) {
1489            return Err(DeviceJoinError::JournalConflict);
1490        }
1491        let payload = serde_json::to_string(record)?;
1492        let payload_hash = ObjectHash::digest(payload.as_bytes());
1493        let connection = Connection::open(&self.path)?;
1494        connection.execute(
1495            "INSERT OR IGNORE INTO pending_join_transfers (attempt_id, payload_hash, payload)
1496             VALUES (?1, ?2, ?3)",
1497            (
1498                attempt_key(record.attempt_id),
1499                payload_hash.to_string(),
1500                &payload,
1501            ),
1502        )?;
1503        let actual: (String, String) = connection.query_row(
1504            "SELECT payload_hash, payload FROM pending_join_transfers WHERE attempt_id = ?1",
1505            [attempt_key(record.attempt_id)],
1506            |row| Ok((row.get(0)?, row.get(1)?)),
1507        )?;
1508        if actual != (payload_hash.to_string(), payload) {
1509            return Err(DeviceJoinError::JournalConflict);
1510        }
1511        Ok(payload_hash)
1512    }
1513
1514    /// Move one pending joiner payload into the installed Store journal in one
1515    /// SQLite transaction spanning the source and destination databases.
1516    pub fn transfer_pending_to(
1517        &self,
1518        destination: &DeviceJoinJournalDatabase,
1519        attempt_id: DeviceJoinAttemptId,
1520    ) -> Result<DeviceJoinJournalRecord, DeviceJoinError> {
1521        let destination_connection = Connection::open(&destination.path)?;
1522        destination_connection.execute(
1523            "ATTACH DATABASE ?1 AS pending_join_source",
1524            [self.path.to_string_lossy().as_ref()],
1525        )?;
1526        let tx = destination_connection.unchecked_transaction()?;
1527        let attempt = attempt_key(attempt_id);
1528        let (payload_hash, payload): (String, String) = tx.query_row(
1529            "SELECT payload_hash, payload FROM pending_join_source.pending_join_transfers
1530             WHERE attempt_id = ?1",
1531            [&attempt],
1532            |row| Ok((row.get(0)?, row.get(1)?)),
1533        )?;
1534        let calculated = ObjectHash::digest(payload.as_bytes()).to_string();
1535        if calculated != payload_hash {
1536            return Err(DeviceJoinError::PendingTransferHashMismatch);
1537        }
1538        let record: DeviceJoinJournalRecord = serde_json::from_str(&payload)?;
1539        if record.attempt_id != attempt_id
1540            || !matches!(*record.progress, DeviceJoinRoleProgress::Joiner(_))
1541        {
1542            return Err(DeviceJoinError::JournalConflict);
1543        }
1544        tx.execute(
1545            "INSERT OR IGNORE INTO device_join_journals (attempt_id, role, payload)
1546             VALUES (?1, 'joiner', ?2)",
1547            (&attempt, &payload),
1548        )?;
1549        let installed: String = tx.query_row(
1550            "SELECT payload FROM device_join_journals
1551             WHERE attempt_id = ?1 AND role = 'joiner'",
1552            [&attempt],
1553            |row| row.get(0),
1554        )?;
1555        if installed != payload {
1556            return Err(DeviceJoinError::JournalConflict);
1557        }
1558        tx.execute(
1559            "DELETE FROM pending_join_source.pending_join_transfers WHERE attempt_id = ?1",
1560            [&attempt],
1561        )?;
1562        tx.commit()?;
1563        destination_connection.execute_batch("DETACH DATABASE pending_join_source")?;
1564        Ok(record)
1565    }
1566}
1567
1568#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1569pub enum DeviceJoinRole {
1570    Owner,
1571    ProviderAdministrator,
1572    Joiner,
1573}
1574
1575impl DeviceJoinRole {
1576    fn as_str(self) -> &'static str {
1577        match self {
1578            Self::Owner => "owner",
1579            Self::ProviderAdministrator => "provider_administrator",
1580            Self::Joiner => "joiner",
1581        }
1582    }
1583}
1584
1585#[async_trait::async_trait]
1586pub trait DeviceProviderAccessAdministrator: Send + Sync {
1587    async fn grant_member_access(
1588        &self,
1589        member_pubkey: &str,
1590        provider_account_email: Option<&str>,
1591        peer: &ProviderDeviceBinding,
1592    ) -> Result<crate::sync::provider::ProviderAccessLocator, DeviceJoinError>;
1593}
1594
1595#[derive(Clone, Debug)]
1596pub enum DeviceJoinAuthorization {
1597    MergeConcurrent(MembershipChain),
1598    Serial(crate::sync::membership::SerialAuthorizationState),
1599}
1600
1601impl DeviceJoinAuthorization {
1602    fn policy(&self) -> crate::WritePolicy {
1603        match self {
1604            Self::MergeConcurrent(_) => crate::WritePolicy::MergeConcurrent,
1605            Self::Serial(_) => crate::WritePolicy::Serial,
1606        }
1607    }
1608
1609    fn current_members(&self) -> Vec<(String, crate::sync::membership::MemberRole)> {
1610        match self {
1611            Self::MergeConcurrent(membership) => membership.current_members(),
1612            Self::Serial(authorization) => authorization.membership.current_members(),
1613        }
1614    }
1615
1616    fn active_owner_grant(&self, pubkey: &str) -> Option<MembershipGrantId> {
1617        match self {
1618            Self::MergeConcurrent(membership) => membership.active_owner_grant(pubkey),
1619            Self::Serial(authorization) => authorization.membership.active_owner_grant(pubkey),
1620        }
1621    }
1622
1623    fn is_owner_now(&self, pubkey: &str) -> bool {
1624        match self {
1625            Self::MergeConcurrent(membership) => membership.is_owner_now(pubkey),
1626            Self::Serial(authorization) => authorization.membership.is_owner(pubkey),
1627        }
1628    }
1629
1630    fn current_member_provider_email(&self, pubkey: &str) -> Option<&str> {
1631        match self {
1632            Self::MergeConcurrent(membership) => membership.current_member_provider_email(pubkey),
1633            Self::Serial(authorization) => authorization
1634                .membership
1635                .current_member_provider_email(pubkey),
1636        }
1637    }
1638
1639    fn merge_chain(&self) -> Option<&MembershipChain> {
1640        match self {
1641            Self::MergeConcurrent(membership) => Some(membership),
1642            Self::Serial(_) => None,
1643        }
1644    }
1645
1646    fn resolved_provider_admin(
1647        &self,
1648        grant_id: &ProviderAdminGrantId,
1649    ) -> Result<ProviderAdminGrantRecord, DeviceJoinError> {
1650        let state = match self {
1651            Self::MergeConcurrent(membership) => {
1652                let crate::sync::membership::MembershipStatus::Resolved(resolved) =
1653                    membership.status()
1654                else {
1655                    return Err(DeviceJoinError::MembershipConflict);
1656                };
1657                resolved.provider_admin.combined_state()
1658            }
1659            Self::Serial(authorization) => &authorization.provider_admin,
1660        };
1661        state
1662            .records()
1663            .get(grant_id)
1664            .filter(|record| state.authorizes(grant_id, &record.administrator))
1665            .cloned()
1666            .ok_or(DeviceJoinError::ProviderAdministratorRequired)
1667    }
1668}
1669
1670pub async fn load_current_device_join_authorization(
1671    db: &Database,
1672    storage: &dyn SyncStorage,
1673    coordination: Option<&dyn CoordinationStorage>,
1674) -> Result<DeviceJoinAuthorization, DeviceJoinError> {
1675    match db.write_policy() {
1676        crate::WritePolicy::MergeConcurrent => {
1677            let membership = crate::sync::pull::load_cycle_membership(storage, db)
1678                .await
1679                .map_err(|error| DeviceJoinError::Store(error.to_string()))?;
1680            let chain = membership
1681                .chain
1682                .ok_or(DeviceJoinError::MembershipConflict)?;
1683            Ok(DeviceJoinAuthorization::MergeConcurrent(chain))
1684        }
1685        crate::WritePolicy::Serial => {
1686            let coordination = coordination.ok_or(DeviceJoinError::MembershipConflict)?;
1687            Ok(DeviceJoinAuthorization::Serial(
1688                crate::sync::store_outbound::current_serial_authorization(
1689                    db,
1690                    storage,
1691                    coordination,
1692                )
1693                .await?,
1694            ))
1695        }
1696    }
1697}
1698
1699async fn require_authorization_policy(
1700    db: &Database,
1701    storage: &dyn SyncStorage,
1702    authorization: &DeviceJoinAuthorization,
1703) -> Result<(), DeviceJoinError> {
1704    let root = db
1705        .local_store_root_ref()
1706        .await
1707        .map_err(database_error)?
1708        .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
1709    let root_value = crate::sync::store_objects::load_store_protocol_root(storage, &root)
1710        .await?
1711        .value;
1712    if root_value.descriptor.write_policy != authorization.policy()
1713        || db.write_policy() != root_value.descriptor.write_policy
1714    {
1715        return Err(DeviceJoinError::OfferMismatch);
1716    }
1717    Ok(())
1718}
1719
1720#[allow(clippy::too_many_arguments)]
1721pub async fn begin_device_join(
1722    db: &Database,
1723    storage: &dyn SyncStorage,
1724    authorization: &DeviceJoinAuthorization,
1725    identity_signer: &UserKeypair,
1726    member_pubkey: &str,
1727    provider_admin_grant: ProviderAdminGrantId,
1728) -> Result<DeviceJoinOffer, DeviceJoinError> {
1729    require_authorization_policy(db, storage, authorization).await?;
1730    validate_member_for_join(member_pubkey, &authorization.current_members())?;
1731    let owner_pubkey = keys::public_key_hex(identity_signer);
1732    let owner_grant = authorization
1733        .active_owner_grant(&owner_pubkey)
1734        .ok_or(DeviceJoinError::OwnerAuthorityRequired)?;
1735    let provider_admin = authorization.resolved_provider_admin(&provider_admin_grant)?;
1736    let device_id = db
1737        .get_protocol_state(crate::database::LOCAL_DEVICE_ID_STATE_KEY)
1738        .await
1739        .map_err(database_error)?
1740        .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
1741    let (root, owner_registration, owner, owner_device_signer) =
1742        crate::sync::store_outbound::load_local_store_authority(db, &device_id, identity_signer)
1743            .await?;
1744    let binding = storage.provider_binding().await?;
1745    let attempt_id =
1746        DeviceJoinAttemptId::from_hash(ObjectHash::digest(db.new_write_id().as_str().as_bytes()));
1747    let attempt_context = crate::sync::storage::ProtocolObjectContext::store(
1748        root.store_root_hash,
1749        crate::sync::storage::ProtocolObjectDomain::DeviceJoinAttempt,
1750    );
1751    let attempt_slot = storage
1752        .allocate_protocol_slot(
1753            &attempt_context,
1754            &crate::sync::store_commit::device_join_attempt_semantic_prefix(attempt_id),
1755            ".json",
1756        )
1757        .await?;
1758    let outcome_context = crate::sync::storage::ProtocolObjectContext::store(
1759        root.store_root_hash,
1760        crate::sync::storage::ProtocolObjectDomain::DeviceJoinOutcome,
1761    );
1762    let outcome_slot = storage
1763        .allocate_protocol_slot(
1764            &outcome_context,
1765            &crate::sync::store_commit::device_join_outcome_semantic_prefix(attempt_id),
1766            ".json",
1767        )
1768        .await?;
1769    let offer = DeviceJoinOffer::signed(
1770        attempt_id,
1771        member_pubkey.to_string(),
1772        root,
1773        binding.store,
1774        attempt_slot,
1775        outcome_slot,
1776        owner_registration,
1777        owner_grant,
1778        provider_admin,
1779        &owner,
1780        &owner_device_signer,
1781    )?;
1782    begin_store_journal(
1783        db,
1784        DeviceJoinJournalRecord {
1785            attempt_id,
1786            progress: Box::new(DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Offered(
1787                offer.clone(),
1788            ))),
1789        },
1790    )
1791    .await?;
1792    Ok(offer)
1793}
1794
1795#[allow(clippy::too_many_arguments)]
1796pub async fn abandon_device_join(
1797    db: &Database,
1798    storage: &dyn SyncStorage,
1799    coordination: Option<&dyn CoordinationStorage>,
1800    authorization: &DeviceJoinAuthorization,
1801    identity_signer: &UserKeypair,
1802    offer: DeviceJoinOffer,
1803) -> Result<DeviceJoinAbandonment, DeviceJoinError> {
1804    require_authorization_policy(db, storage, authorization).await?;
1805    let current = load_store_journal(db, offer.attempt_id, DeviceJoinRole::Owner)
1806        .await?
1807        .ok_or(DeviceJoinError::JournalConflict)?;
1808    if let DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Abandoned(existing)) =
1809        &*current.progress
1810    {
1811        return Ok(existing.clone());
1812    }
1813    let owner = db
1814        .activated_store_device_registration(offer.owner_registration.clone())
1815        .await
1816        .map_err(database_error)?;
1817    offer.verify(&owner)?;
1818    let local_device_id = db
1819        .get_protocol_state(crate::database::LOCAL_DEVICE_ID_STATE_KEY)
1820        .await
1821        .map_err(database_error)?
1822        .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
1823    if owner.device_id.to_string() != local_device_id
1824        || !authorization.is_owner_now(&keys::public_key_hex(identity_signer))
1825    {
1826        return Err(DeviceJoinError::OwnerAuthorityRequired);
1827    }
1828    let owner_signer = owner.device_signer(identity_signer)?;
1829    let abandonment_object = DeviceJoinAbandonmentObject::signed(&offer, &owner, &owner_signer)?;
1830    let context = crate::sync::storage::ProtocolObjectContext::store(
1831        offer.store_root.store_root_hash,
1832        crate::sync::storage::ProtocolObjectDomain::DeviceJoinAbandonment,
1833    );
1834    let prefix =
1835        crate::sync::store_commit::device_join_abandonment_semantic_prefix(offer.attempt_id);
1836    let prepared = storage.prepare_protocol_object(
1837        &context,
1838        offer.attempt_slot.clone(),
1839        &prefix,
1840        abandonment_object.to_bytes(),
1841    )?;
1842    let abandonment_ref = DeviceJoinAbandonmentRef {
1843        attempt_id: offer.attempt_id,
1844        abandonment_hash: abandonment_object.abandonment_hash(),
1845        object: prepared.reference().clone(),
1846    };
1847    let intent = DeviceJoinJournalRecord {
1848        attempt_id: offer.attempt_id,
1849        progress: Box::new(DeviceJoinRoleProgress::Owner(
1850            OwnerJoinProgress::AbandonmentCreateIntent {
1851                offer: offer.clone(),
1852                abandonment: abandonment_ref.clone(),
1853                prepared: PreparedDeviceJoinObject::from_prepared(&prepared),
1854            },
1855        )),
1856    };
1857    match &*current.progress {
1858        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Offered(durable)) if durable == &offer => {
1859            advance_store_journal(db, &current, intent.clone()).await?;
1860        }
1861        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::RegistrationRequested(request))
1862            if request.approval.request.offer.as_ref() == &offer =>
1863        {
1864            advance_store_journal(db, &current, intent.clone()).await?;
1865        }
1866        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::AbandonmentCreateIntent {
1867            offer: durable_offer,
1868            abandonment,
1869            prepared: durable_prepared,
1870        }) if durable_offer == &offer
1871            && abandonment == &abandonment_ref
1872            && durable_prepared == &PreparedDeviceJoinObject::from_prepared(&prepared) => {}
1873        _ => return Err(DeviceJoinError::JournalConflict),
1874    }
1875    storage.create_protocol_object(&prepared).await?;
1876    let opened = storage
1877        .read_protocol_object(&context, prepared.reference(), &prefix)
1878        .await?;
1879    if opened != abandonment_object.to_bytes() {
1880        return Err(DeviceJoinError::AttemptMismatch);
1881    }
1882    abandonment_ref.verify(&abandonment_object, &owner)?;
1883    let plan = crate::sync::store_outbound::prepare_device_join_commit(
1884        db,
1885        storage,
1886        coordination,
1887        &local_device_id,
1888        identity_signer,
1889        authorization.merge_chain(),
1890    )
1891    .await?;
1892    let activation = crate::sync::store_outbound::activate_device_join_commit(
1893        db,
1894        storage,
1895        coordination,
1896        plan,
1897        crate::sync::store_outbound::DeviceJoinStoreBatch::Abandonment(abandonment_ref.clone()),
1898    )
1899    .await?;
1900    let abandonment = DeviceJoinAbandonment {
1901        abandonment: abandonment_ref,
1902        abandonment_activation: activation,
1903    };
1904    advance_store_journal(
1905        db,
1906        &intent,
1907        DeviceJoinJournalRecord {
1908            attempt_id: offer.attempt_id,
1909            progress: Box::new(DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Abandoned(
1910                abandonment.clone(),
1911            ))),
1912        },
1913    )
1914    .await?;
1915    Ok(abandonment)
1916}
1917
1918pub async fn prepare_device_provider_access_request(
1919    pending: &DeviceJoinJournalDatabase,
1920    provider_binding: crate::sync::storage::ResolvedProviderBinding,
1921    identity_signer: &UserKeypair,
1922    offer: DeviceJoinOffer,
1923) -> Result<DeviceProviderAccessRequest, DeviceJoinError> {
1924    if let Some(record) = pending.load(offer.attempt_id, DeviceJoinRole::Joiner)? {
1925        return match &*record.progress {
1926            DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::AccessRequested(request))
1927                if *request.offer == offer =>
1928            {
1929                Ok(request.clone())
1930            }
1931            DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::OfferReceived(durable))
1932                if durable == &offer =>
1933            {
1934                prepare_new_access_request(
1935                    pending,
1936                    provider_binding,
1937                    identity_signer,
1938                    record.clone(),
1939                    durable.clone(),
1940                )
1941                .await
1942            }
1943            _ => Err(DeviceJoinError::JournalConflict),
1944        };
1945    }
1946    let initial = DeviceJoinJournalRecord {
1947        attempt_id: offer.attempt_id,
1948        progress: Box::new(DeviceJoinRoleProgress::Joiner(
1949            JoinerJoinProgress::OfferReceived(offer.clone()),
1950        )),
1951    };
1952    let durable = pending.begin(initial.clone())?;
1953    if durable != initial {
1954        return Err(DeviceJoinError::JournalConflict);
1955    }
1956    prepare_new_access_request(pending, provider_binding, identity_signer, initial, offer).await
1957}
1958
1959async fn prepare_new_access_request(
1960    pending: &DeviceJoinJournalDatabase,
1961    provider_binding: crate::sync::storage::ResolvedProviderBinding,
1962    identity_signer: &UserKeypair,
1963    initial: DeviceJoinJournalRecord,
1964    offer: DeviceJoinOffer,
1965) -> Result<DeviceProviderAccessRequest, DeviceJoinError> {
1966    if provider_binding.store != offer.provider {
1967        return Err(DeviceJoinError::OfferMismatch);
1968    }
1969    let request =
1970        DeviceProviderAccessRequest::signed(offer, provider_binding.device, identity_signer)?;
1971    pending.advance(
1972        &initial,
1973        DeviceJoinJournalRecord {
1974            attempt_id: request.offer.attempt_id,
1975            progress: Box::new(DeviceJoinRoleProgress::Joiner(
1976                JoinerJoinProgress::AccessRequested(request.clone()),
1977            )),
1978        },
1979    )?;
1980    Ok(request)
1981}
1982
1983pub async fn prepare_device_registration_request(
1984    pending: &DeviceJoinJournalDatabase,
1985    storage: &dyn SyncStorage,
1986    peer_exact: Option<&dyn ExactSlotStorage>,
1987    identity_signer: &UserKeypair,
1988    approval: DeviceProviderAdmissionApproval,
1989) -> Result<DeviceRegistrationRequest, DeviceJoinError> {
1990    let attempt_id = approval.request.offer.attempt_id;
1991    if let Some(record) = pending.load(attempt_id, DeviceJoinRole::Joiner)? {
1992        if let DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::RegistrationPrepared(request)) =
1993            *record.progress
1994        {
1995            if *request.approval == approval {
1996                return Ok(request);
1997            }
1998            return Err(DeviceJoinError::JournalConflict);
1999        }
2000    }
2001    let owner = crate::sync::store_objects::load_registration_ref(
2002        storage,
2003        &approval.request.offer.store_root,
2004        &approval.request.offer.owner_registration,
2005    )
2006    .await?
2007    .value;
2008    let administrator = crate::sync::store_objects::load_registration_ref(
2009        storage,
2010        &approval.request.offer.store_root,
2011        &approval.request.offer.provider_admin.administrator,
2012    )
2013    .await?
2014    .value;
2015    approval.verify(&owner, &administrator)?;
2016    let live = storage.provider_binding().await?;
2017    if live.store != approval.request.offer.provider
2018        || live.device != approval.request.peer_provider
2019    {
2020        return Err(DeviceJoinError::ApprovalMismatch);
2021    }
2022    let current = pending
2023        .load(attempt_id, DeviceJoinRole::Joiner)?
2024        .ok_or(DeviceJoinError::JournalConflict)?;
2025    let access_request = match &*current.progress {
2026        DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::AccessRequested(request))
2027            if request == &*approval.request =>
2028        {
2029            request.clone()
2030        }
2031        DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::ApprovalReceived(existing))
2032            if existing == &approval =>
2033        {
2034            *approval.request.clone()
2035        }
2036        _ => return Err(DeviceJoinError::JournalConflict),
2037    };
2038    let approval_record = if matches!(
2039        *current.progress,
2040        DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::ApprovalReceived(_))
2041    ) {
2042        current
2043    } else {
2044        let next = DeviceJoinJournalRecord {
2045            attempt_id,
2046            progress: Box::new(DeviceJoinRoleProgress::Joiner(
2047                JoinerJoinProgress::ApprovalReceived(approval.clone()),
2048            )),
2049        };
2050        pending.advance(&current, next.clone())?;
2051        next
2052    };
2053    let root_value = crate::sync::store_objects::load_store_protocol_root(
2054        storage,
2055        &approval.request.offer.store_root,
2056    )
2057    .await?
2058    .value;
2059    let origin = crate::sync::store_commit::StoreDeviceRegistrationOrigin::Join {
2060        attempt_id,
2061        attempt_slot: approval.request.offer.attempt_slot.clone(),
2062        outcome_slot: approval.request.offer.outcome_slot.clone(),
2063    };
2064    let device_id = crate::sync::store_commit::StoreDeviceId::derive(
2065        &approval.request.offer.store_root,
2066        &origin,
2067    );
2068    let registration_context = crate::sync::storage::ProtocolObjectContext::store(
2069        approval.request.offer.store_root.store_root_hash,
2070        crate::sync::storage::ProtocolObjectDomain::StoreDeviceRegistration,
2071    );
2072    let registration_slot = storage
2073        .allocate_protocol_slot(
2074            &registration_context,
2075            &crate::sync::store_commit::registration_semantic_prefix(&device_id.to_string()),
2076            ".json",
2077        )
2078        .await?;
2079    let store_commits = match root_value.descriptor.write_policy {
2080        crate::WritePolicy::MergeConcurrent => {
2081            let context = crate::sync::storage::ProtocolObjectContext::store(
2082                approval.request.offer.store_root.store_root_hash,
2083                crate::sync::storage::ProtocolObjectDomain::StoreHead,
2084            );
2085            let first_slot = storage
2086                .allocate_protocol_slot(
2087                    &context,
2088                    &crate::sync::store_commit::head_slot_prefix(&device_id.to_string(), 1),
2089                    ".json",
2090                )
2091                .await?;
2092            crate::sync::store_commit::StoreCommitAnchor::MergeConcurrent {
2093                announcements: crate::sync::store_commit::DeviceStreamAnchor::StoreAnnouncements {
2094                    first_slot,
2095                },
2096            }
2097        }
2098        crate::WritePolicy::Serial => crate::sync::store_commit::StoreCommitAnchor::Serial,
2099    };
2100    let ack_context = crate::sync::storage::ProtocolObjectContext::store(
2101        approval.request.offer.store_root.store_root_hash,
2102        crate::sync::storage::ProtocolObjectDomain::StoreAck,
2103    );
2104    let first_ack = storage
2105        .allocate_protocol_slot(
2106            &ack_context,
2107            &crate::sync::store_commit::ack_slot_prefix(&device_id.to_string(), 1),
2108            ".json",
2109        )
2110        .await?;
2111    let snapshot_context = crate::sync::storage::ProtocolObjectContext::store(
2112        approval.request.offer.store_root.store_root_hash,
2113        crate::sync::storage::ProtocolObjectDomain::StoreSnapshotMeta,
2114    );
2115    let first_snapshot = storage
2116        .allocate_protocol_slot(
2117            &snapshot_context,
2118            &crate::sync::store_commit::snapshot_slot_prefix(&device_id.to_string(), 1),
2119            ".json",
2120        )
2121        .await?;
2122    let (response, response_slot) = match &approval.admission {
2123        DeviceProviderAdmissionChallenge::SamePrincipal => {
2124            (DeviceProviderResponseReservation::SamePrincipal, None)
2125        }
2126        DeviceProviderAdmissionChallenge::CrossPrincipal(challenge) => {
2127            let exact = peer_exact.ok_or(DeviceJoinError::ExactSlotStorageRequired)?;
2128            let logical = crate::sync::provider::cross_peer_logical_key(challenge.probe_id);
2129            let slot = exact
2130                .allocate_slot(&logical)
2131                .await
2132                .map_err(provider_error)?;
2133            if slot.logical_key() != logical {
2134                return Err(DeviceJoinError::RegistrationRequestMismatch);
2135            }
2136            (
2137                DeviceProviderResponseReservation::CrossPrincipal {
2138                    response_slot: slot.clone(),
2139                },
2140                Some(slot),
2141            )
2142        }
2143    };
2144    let _ = response_slot;
2145    let (registration, _) = crate::sync::store_registration::prepare_registration_for_origin(
2146        storage,
2147        identity_signer,
2148        root_value.descriptor.write_policy,
2149        approval.request.offer.store_root.clone(),
2150        origin,
2151        registration_slot.clone(),
2152        live.device,
2153        store_commits,
2154        crate::sync::store_commit::DeviceStreamAnchor::StoreAcknowledgements {
2155            first_slot: first_ack,
2156        },
2157        crate::sync::store_commit::DeviceStreamAnchor::StoreSnapshots {
2158            first_slot: first_snapshot,
2159        },
2160    )
2161    .await?;
2162    if access_request != *approval.request {
2163        return Err(DeviceJoinError::JournalConflict);
2164    }
2165    let request = DeviceRegistrationRequest::signed(
2166        approval,
2167        registration,
2168        registration_slot,
2169        response,
2170        identity_signer,
2171    )?;
2172    pending.advance(
2173        &approval_record,
2174        DeviceJoinJournalRecord {
2175            attempt_id,
2176            progress: Box::new(DeviceJoinRoleProgress::Joiner(
2177                JoinerJoinProgress::RegistrationPrepared(request.clone()),
2178            )),
2179        },
2180    )?;
2181    Ok(request)
2182}
2183
2184#[allow(clippy::too_many_arguments)]
2185pub async fn accept_device_registration_request(
2186    db: &Database,
2187    storage: &dyn SyncStorage,
2188    coordination: Option<&dyn CoordinationStorage>,
2189    authorization: &DeviceJoinAuthorization,
2190    identity_signer: &UserKeypair,
2191    request: DeviceRegistrationRequest,
2192) -> Result<ProvisionalDeviceBootstrap, DeviceJoinError> {
2193    require_authorization_policy(db, storage, authorization).await?;
2194    request.verify()?;
2195    let offer = &request.approval.request.offer;
2196    let owner = Box::pin(db.activated_store_device_registration(offer.owner_registration.clone()))
2197        .await
2198        .map_err(database_error)?;
2199    let administrator = Box::pin(
2200        db.activated_store_device_registration(offer.provider_admin.administrator.clone()),
2201    )
2202    .await
2203    .map_err(database_error)?;
2204    request.approval.verify(&owner, &administrator)?;
2205    let local_device_id = db
2206        .get_protocol_state(crate::database::LOCAL_DEVICE_ID_STATE_KEY)
2207        .await
2208        .map_err(database_error)?
2209        .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
2210    if owner.device_id.to_string() != local_device_id
2211        || !authorization.is_owner_now(&keys::public_key_hex(identity_signer))
2212    {
2213        return Err(DeviceJoinError::OwnerAuthorityRequired);
2214    }
2215    let owner_signer = owner.device_signer(identity_signer)?;
2216    let offered = DeviceJoinJournalRecord {
2217        attempt_id: offer.attempt_id,
2218        progress: Box::new(DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Offered(
2219            *offer.clone(),
2220        ))),
2221    };
2222    let durable = begin_store_journal(db, offered.clone()).await?;
2223    if let DeviceJoinRoleProgress::Owner(OwnerJoinProgress::AttemptActivated(bootstrap)) =
2224        *durable.progress
2225    {
2226        if *bootstrap.request == request {
2227            return Ok(bootstrap);
2228        }
2229        return Err(DeviceJoinError::JournalConflict);
2230    }
2231    if durable != offered {
2232        return Err(DeviceJoinError::JournalConflict);
2233    }
2234    let requested = DeviceJoinJournalRecord {
2235        attempt_id: offer.attempt_id,
2236        progress: Box::new(DeviceJoinRoleProgress::Owner(
2237            OwnerJoinProgress::RegistrationRequested(request.clone()),
2238        )),
2239    };
2240    advance_store_journal(db, &offered, requested.clone()).await?;
2241    let plan = crate::sync::store_outbound::prepare_device_join_commit(
2242        db,
2243        storage,
2244        coordination,
2245        &local_device_id,
2246        identity_signer,
2247        authorization.merge_chain(),
2248    )
2249    .await?;
2250    let cut = plan.predecessor_cut()?;
2251    if !history_cut_contains(&cut, &request.approval.access_grant.activation) {
2252        return Err(DeviceJoinError::ApprovalActivationMissing);
2253    }
2254    let attempt = DeviceJoinAttempt::signed(
2255        offer.store_root.clone(),
2256        offer.attempt_id,
2257        offer.attempt_slot.clone(),
2258        request.expected_registration.clone(),
2259        request.registration_slot.clone(),
2260        offer.outcome_slot.clone(),
2261        cut,
2262        plan.membership_state().clone(),
2263        offer.provider_admin.grant_id.clone(),
2264        *request.approval.clone(),
2265        request.response.clone(),
2266        offer.owner_registration.clone(),
2267        offer.owner_grant.clone(),
2268        &owner,
2269        &owner_signer,
2270    )?;
2271    let context = crate::sync::storage::ProtocolObjectContext::store(
2272        offer.store_root.store_root_hash,
2273        crate::sync::storage::ProtocolObjectDomain::DeviceJoinAttempt,
2274    );
2275    let prefix = crate::sync::store_commit::device_join_attempt_semantic_prefix(offer.attempt_id);
2276    let prepared = storage.prepare_protocol_object(
2277        &context,
2278        offer.attempt_slot.clone(),
2279        &prefix,
2280        attempt.to_bytes(),
2281    )?;
2282    storage.create_protocol_object(&prepared).await?;
2283    let opened = storage
2284        .read_protocol_object(&context, prepared.reference(), &prefix)
2285        .await?;
2286    if opened != attempt.to_bytes() {
2287        return Err(DeviceJoinError::AttemptMismatch);
2288    }
2289    let attempt_ref = DeviceJoinAttemptRef {
2290        attempt_id: offer.attempt_id,
2291        attempt_hash: attempt.attempt_hash(),
2292        object: prepared.reference().clone(),
2293    };
2294    let activation = crate::sync::store_outbound::activate_device_join_commit(
2295        db,
2296        storage,
2297        coordination,
2298        plan,
2299        crate::sync::store_outbound::DeviceJoinStoreBatch::Attempt(attempt_ref.clone()),
2300    )
2301    .await?;
2302    let attempt_id = offer.attempt_id;
2303    let bootstrap = ProvisionalDeviceBootstrap {
2304        request: Box::new(request),
2305        publication_authorization: DeviceJoinChallengePublicationAuthorization {
2306            attempt: attempt_ref,
2307            attempt_activation: activation,
2308        },
2309    };
2310    advance_store_journal(
2311        db,
2312        &requested,
2313        DeviceJoinJournalRecord {
2314            attempt_id,
2315            progress: Box::new(DeviceJoinRoleProgress::Owner(
2316                OwnerJoinProgress::AttemptActivated(bootstrap.clone()),
2317            )),
2318        },
2319    )
2320    .await?;
2321    Ok(bootstrap)
2322}
2323
2324fn history_cut_contains(
2325    cut: &crate::sync::store_commit::StoreHistoryCut,
2326    expected: &StoreBatchCommitRef,
2327) -> bool {
2328    match cut {
2329        crate::sync::store_commit::StoreHistoryCut::MergeConcurrent(frontier) => {
2330            frontier.values().any(|reference| reference == expected)
2331        }
2332        crate::sync::store_commit::StoreHistoryCut::Serial(
2333            crate::sync::store_commit::StoreSerialPredecessor::Commit(reference),
2334        ) => reference == expected,
2335        crate::sync::store_commit::StoreHistoryCut::Serial(
2336            crate::sync::store_commit::StoreSerialPredecessor::Genesis { .. },
2337        ) => false,
2338    }
2339}
2340
2341pub async fn publish_device_provider_challenge(
2342    db: &Database,
2343    storage: &dyn SyncStorage,
2344    administrator_exact: Option<&dyn ExactSlotStorage>,
2345    bootstrap: ProvisionalDeviceBootstrap,
2346) -> Result<ProviderReadyDeviceBootstrap, DeviceJoinError> {
2347    let offer = &bootstrap.request.approval.request.offer;
2348    let owner = Box::pin(db.activated_store_device_registration(offer.owner_registration.clone()))
2349        .await
2350        .map_err(database_error)?;
2351    let administrator = Box::pin(
2352        db.activated_store_device_registration(offer.provider_admin.administrator.clone()),
2353    )
2354    .await
2355    .map_err(database_error)?;
2356    bootstrap.request.approval.verify(&owner, &administrator)?;
2357    let challenge_publication = match &bootstrap.request.approval.admission {
2358        DeviceProviderAdmissionChallenge::SamePrincipal => {
2359            DeviceProviderChallengePublication::SamePrincipal
2360        }
2361        DeviceProviderAdmissionChallenge::CrossPrincipal(challenge) => {
2362            let exact = administrator_exact.ok_or(DeviceJoinError::ExactSlotStorageRequired)?;
2363            let context = cross_challenge_context(&bootstrap.request.approval.request);
2364            let (_, activation_author) =
2365                Box::pin(crate::sync::store_pull::load_commit_with_author(
2366                    storage,
2367                    &offer.store_root,
2368                    &bootstrap.publication_authorization.attempt_activation,
2369                ))
2370                .await?;
2371            let authorization = DeviceJoinChallengePublicationAuthorization {
2372                attempt: bootstrap.publication_authorization.attempt.clone(),
2373                attempt_activation: bootstrap
2374                    .publication_authorization
2375                    .attempt_activation
2376                    .clone(),
2377            };
2378            let published = Box::pin(crate::sync::provider::publish_cross_principal_challenge(
2379                storage,
2380                exact,
2381                db,
2382                &authorization,
2383                challenge,
2384                &context,
2385                &offer.provider,
2386                &owner,
2387                &activation_author,
2388                &administrator.device_signing_pubkey,
2389            ))
2390            .await
2391            .map_err(provider_error)?;
2392            DeviceProviderChallengePublication::CrossPrincipal {
2393                challenge: published,
2394            }
2395        }
2396    };
2397    let attempt_id = offer.attempt_id;
2398    let ready = ProviderReadyDeviceBootstrap {
2399        bootstrap: Box::new(bootstrap),
2400        challenge_publication,
2401    };
2402    if let Some(current) = Box::pin(load_store_journal(
2403        db,
2404        attempt_id,
2405        DeviceJoinRole::ProviderAdministrator,
2406    ))
2407    .await?
2408    {
2409        match &*current.progress {
2410            DeviceJoinRoleProgress::ProviderAdministrator(
2411                ProviderAdminJoinProgress::ProviderReady(existing),
2412            ) if existing == &ready => return Ok(ready),
2413            DeviceJoinRoleProgress::ProviderAdministrator(
2414                ProviderAdminJoinProgress::ApprovalPrepared(approval),
2415            ) if approval == &*ready.bootstrap.request.approval => {
2416                let observed = DeviceJoinJournalRecord {
2417                    attempt_id,
2418                    progress: Box::new(DeviceJoinRoleProgress::ProviderAdministrator(
2419                        ProviderAdminJoinProgress::AttemptObserved(*ready.bootstrap.clone()),
2420                    )),
2421                };
2422                Box::pin(advance_store_journal(db, &current, observed.clone())).await?;
2423                let intent = DeviceJoinJournalRecord {
2424                    attempt_id,
2425                    progress: Box::new(DeviceJoinRoleProgress::ProviderAdministrator(
2426                        ProviderAdminJoinProgress::ChallengeCreateIntent(*ready.bootstrap.clone()),
2427                    )),
2428                };
2429                Box::pin(advance_store_journal(db, &observed, intent.clone())).await?;
2430                Box::pin(advance_store_journal(
2431                    db,
2432                    &intent,
2433                    DeviceJoinJournalRecord {
2434                        attempt_id,
2435                        progress: Box::new(DeviceJoinRoleProgress::ProviderAdministrator(
2436                            ProviderAdminJoinProgress::ProviderReady(ready.clone()),
2437                        )),
2438                    },
2439                ))
2440                .await?;
2441            }
2442            _ => return Err(DeviceJoinError::JournalConflict),
2443        }
2444    }
2445    Ok(ready)
2446}
2447
2448#[allow(clippy::too_many_arguments)]
2449pub async fn bootstrap_pending_device(
2450    db: &Database,
2451    pending: &DeviceJoinJournalDatabase,
2452    storage: &dyn SyncStorage,
2453    peer_exact: Option<&dyn ExactSlotStorage>,
2454    identity_signer: &UserKeypair,
2455    bootstrap: ProviderReadyDeviceBootstrap,
2456    published_at: &str,
2457) -> Result<DeviceJoinReadiness, DeviceJoinError> {
2458    let offer = &bootstrap.bootstrap.request.approval.request.offer;
2459    let attempt_owner = Box::pin(crate::sync::store_objects::load_registration_ref(
2460        storage,
2461        &offer.store_root,
2462        &offer.owner_registration,
2463    ))
2464    .await?
2465    .value;
2466    let administrator = Box::pin(crate::sync::store_objects::load_registration_ref(
2467        storage,
2468        &offer.store_root,
2469        &offer.provider_admin.administrator,
2470    ))
2471    .await?
2472    .value;
2473    let verified_attempt = Box::pin(crate::sync::store_objects::load_device_join_attempt_ref(
2474        storage,
2475        &offer.store_root,
2476        &bootstrap.bootstrap.publication_authorization.attempt,
2477        &attempt_owner,
2478    ))
2479    .await?;
2480    if verified_attempt.value.expected_registration
2481        != bootstrap.bootstrap.request.expected_registration
2482    {
2483        return Err(DeviceJoinError::AttemptMismatch);
2484    }
2485    let bootstrap_authorization =
2486        Box::pin(crate::sync::store_pull::load_device_join_authorization(
2487            storage,
2488            &offer.store_root,
2489            &verified_attempt.value.membership,
2490        ))
2491        .await?;
2492    let bootstrap_plan = Box::pin(crate::sync::store_pull::prepare_device_join_bootstrap(
2493        storage,
2494        &offer.store_root,
2495        &verified_attempt.value.bootstrap_cut,
2496        &bootstrap
2497            .bootstrap
2498            .publication_authorization
2499            .attempt_activation,
2500        &bootstrap_authorization,
2501    ))
2502    .await?;
2503    let proof = Box::pin(crate::sync::store_registration::bootstrap_pending_device(
2504        db,
2505        storage,
2506        identity_signer,
2507        bootstrap
2508            .bootstrap
2509            .publication_authorization
2510            .attempt
2511            .clone(),
2512        verified_attempt,
2513        bootstrap_plan,
2514        bootstrap
2515            .bootstrap
2516            .publication_authorization
2517            .attempt_activation
2518            .clone(),
2519        &attempt_owner,
2520        published_at,
2521    ))
2522    .await?;
2523    let provider = match (
2524        &bootstrap.bootstrap.request.approval.admission,
2525        &bootstrap.bootstrap.request.response,
2526        &bootstrap.challenge_publication,
2527    ) {
2528        (
2529            DeviceProviderAdmissionChallenge::SamePrincipal,
2530            DeviceProviderResponseReservation::SamePrincipal,
2531            DeviceProviderChallengePublication::SamePrincipal,
2532        ) => DeviceProviderReadiness::SamePrincipal,
2533        (
2534            DeviceProviderAdmissionChallenge::CrossPrincipal(challenge),
2535            DeviceProviderResponseReservation::CrossPrincipal { response_slot },
2536            DeviceProviderChallengePublication::CrossPrincipal {
2537                challenge: published,
2538            },
2539        ) if challenge == published => {
2540            let exact = peer_exact.ok_or(DeviceJoinError::ExactSlotStorageRequired)?;
2541            let context = crate::sync::provider::CrossPrincipalResponseContext {
2542                challenge: cross_challenge_context(&bootstrap.bootstrap.request.approval.request),
2543                expected_registration_hash: bootstrap
2544                    .bootstrap
2545                    .request
2546                    .expected_registration
2547                    .registration_hash(),
2548                response_slot: response_slot.clone(),
2549            };
2550            DeviceProviderReadiness::CrossPrincipal(
2551                Box::pin(crate::sync::provider::create_cross_principal_response(
2552                    exact,
2553                    challenge,
2554                    &context,
2555                    &offer.provider,
2556                    &administrator.device_signing_pubkey,
2557                    identity_signer,
2558                ))
2559                .await
2560                .map_err(provider_error)?,
2561            )
2562        }
2563        _ => return Err(DeviceJoinError::AttemptMismatch),
2564    };
2565    let readiness = DeviceJoinReadiness { proof, provider };
2566    let pending = pending.clone();
2567    let bootstrap = Box::new(bootstrap);
2568    let readiness = Box::new(readiness);
2569    tokio::task::spawn_blocking(move || record_joiner_readiness(&pending, *bootstrap, *readiness))
2570        .await
2571        .map_err(|error| {
2572            DeviceJoinError::Store(format!("joiner readiness journal task failed: {error}"))
2573        })?
2574}
2575
2576fn record_joiner_readiness(
2577    pending: &DeviceJoinJournalDatabase,
2578    bootstrap: ProviderReadyDeviceBootstrap,
2579    readiness: DeviceJoinReadiness,
2580) -> Result<DeviceJoinReadiness, DeviceJoinError> {
2581    let offer = &bootstrap.bootstrap.request.approval.request.offer;
2582    let current = pending
2583        .load(offer.attempt_id, DeviceJoinRole::Joiner)?
2584        .ok_or(DeviceJoinError::JournalConflict)?;
2585    let prepared = match &*current.progress {
2586        DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::RegistrationPrepared(request))
2587            if request == &*bootstrap.bootstrap.request =>
2588        {
2589            current
2590        }
2591        DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::Ready(existing))
2592            if existing == &readiness =>
2593        {
2594            return Ok(readiness)
2595        }
2596        _ => return Err(DeviceJoinError::JournalConflict),
2597    };
2598    let provider_ready = DeviceJoinJournalRecord {
2599        attempt_id: offer.attempt_id,
2600        progress: Box::new(DeviceJoinRoleProgress::Joiner(
2601            JoinerJoinProgress::ProviderReady(bootstrap.clone()),
2602        )),
2603    };
2604    pending.advance(&prepared, provider_ready.clone())?;
2605    let registration_intent = DeviceJoinJournalRecord {
2606        attempt_id: offer.attempt_id,
2607        progress: Box::new(DeviceJoinRoleProgress::Joiner(
2608            JoinerJoinProgress::RegistrationCreateIntent(bootstrap.clone()),
2609        )),
2610    };
2611    pending.advance(&provider_ready, registration_intent.clone())?;
2612    let registration_created = DeviceJoinJournalRecord {
2613        attempt_id: offer.attempt_id,
2614        progress: Box::new(DeviceJoinRoleProgress::Joiner(
2615            JoinerJoinProgress::RegistrationCreated(readiness.proof.registration.clone()),
2616        )),
2617    };
2618    pending.advance(&registration_intent, registration_created.clone())?;
2619    let ack_intent = DeviceJoinJournalRecord {
2620        attempt_id: offer.attempt_id,
2621        progress: Box::new(DeviceJoinRoleProgress::Joiner(
2622            JoinerJoinProgress::AckCreateIntent(readiness.proof.registration.clone()),
2623        )),
2624    };
2625    pending.advance(&registration_created, ack_intent.clone())?;
2626    let ack_created = DeviceJoinJournalRecord {
2627        attempt_id: offer.attempt_id,
2628        progress: Box::new(DeviceJoinRoleProgress::Joiner(
2629            JoinerJoinProgress::AckCreated(readiness.proof.initial_ack.clone()),
2630        )),
2631    };
2632    pending.advance(&ack_intent, ack_created.clone())?;
2633    let ready_record = DeviceJoinJournalRecord {
2634        attempt_id: offer.attempt_id,
2635        progress: Box::new(DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::Ready(
2636            readiness.clone(),
2637        ))),
2638    };
2639    match readiness.provider {
2640        DeviceProviderReadiness::SamePrincipal => pending.advance(&ack_created, ready_record)?,
2641        DeviceProviderReadiness::CrossPrincipal(_) => {
2642            let response_intent = DeviceJoinJournalRecord {
2643                attempt_id: offer.attempt_id,
2644                progress: Box::new(DeviceJoinRoleProgress::Joiner(
2645                    JoinerJoinProgress::ResponseCreateIntent(readiness.clone()),
2646                )),
2647            };
2648            pending.advance(&ack_created, response_intent.clone())?;
2649            pending.advance(&response_intent, ready_record)?;
2650        }
2651    }
2652    Ok(readiness)
2653}
2654
2655fn cross_challenge_context(
2656    request: &DeviceProviderAccessRequest,
2657) -> crate::sync::provider::CrossPrincipalChallengeContext {
2658    crate::sync::provider::CrossPrincipalChallengeContext {
2659        root: request.offer.store_root.clone(),
2660        attempt_id: request.offer.attempt_id,
2661        access_request_hash: request.request_hash(),
2662        provider_admin_grant: request.offer.provider_admin.grant_id.clone(),
2663        owner_registration: request.offer.owner_registration.clone(),
2664        member_pubkey: request.offer.member_pubkey.clone(),
2665        administrator_binding: request.offer.provider_admin.provider.clone(),
2666        peer_binding: request.peer_provider.clone(),
2667    }
2668}
2669
2670pub async fn complete_device_provider_admission(
2671    db: &Database,
2672    administrator_exact: Option<&dyn ExactSlotStorage>,
2673    identity_signer: &UserKeypair,
2674    readiness: DeviceJoinReadiness,
2675) -> Result<DeviceProviderAdmissionCompletion, DeviceJoinError> {
2676    let attempt_id = readiness.proof.attempt.attempt_id;
2677    let current = load_store_journal(db, attempt_id, DeviceJoinRole::ProviderAdministrator)
2678        .await?
2679        .ok_or(DeviceJoinError::JournalConflict)?;
2680    if let DeviceJoinRoleProgress::ProviderAdministrator(ProviderAdminJoinProgress::Completed(
2681        existing,
2682    )) = &*current.progress
2683    {
2684        if *existing.readiness == readiness {
2685            return Ok(existing.clone());
2686        }
2687        return Err(DeviceJoinError::JournalConflict);
2688    }
2689    let bootstrap = match &*current.progress {
2690        DeviceJoinRoleProgress::ProviderAdministrator(
2691            ProviderAdminJoinProgress::ProviderReady(bootstrap),
2692        ) => bootstrap.clone(),
2693        _ => return Err(DeviceJoinError::JournalConflict),
2694    };
2695    if readiness.proof.attempt != bootstrap.bootstrap.publication_authorization.attempt {
2696        return Err(DeviceJoinError::AttemptMismatch);
2697    }
2698    let offer = &bootstrap.bootstrap.request.approval.request.offer;
2699    let administrator = db
2700        .activated_store_device_registration(offer.provider_admin.administrator.clone())
2701        .await
2702        .map_err(database_error)?;
2703    let administrator_signer = administrator.device_signer(identity_signer)?;
2704    let admission = match (
2705        &bootstrap.bootstrap.request.approval.admission,
2706        &bootstrap.bootstrap.request.response,
2707        &readiness.provider,
2708    ) {
2709        (
2710            DeviceProviderAdmissionChallenge::SamePrincipal,
2711            DeviceProviderResponseReservation::SamePrincipal,
2712            DeviceProviderReadiness::SamePrincipal,
2713        ) => DeviceProviderAdmission::SamePrincipal,
2714        (
2715            DeviceProviderAdmissionChallenge::CrossPrincipal(challenge),
2716            DeviceProviderResponseReservation::CrossPrincipal { response_slot },
2717            DeviceProviderReadiness::CrossPrincipal(response),
2718        ) => {
2719            let exact = administrator_exact.ok_or(DeviceJoinError::ExactSlotStorageRequired)?;
2720            let context = crate::sync::provider::CrossPrincipalResponseContext {
2721                challenge: cross_challenge_context(&bootstrap.bootstrap.request.approval.request),
2722                expected_registration_hash: bootstrap
2723                    .bootstrap
2724                    .request
2725                    .expected_registration
2726                    .registration_hash(),
2727                response_slot: response_slot.clone(),
2728            };
2729            DeviceProviderAdmission::CrossPrincipal(
2730                crate::sync::provider::complete_cross_principal_probe(
2731                    exact,
2732                    db,
2733                    challenge,
2734                    response,
2735                    &context,
2736                    &offer.provider,
2737                    &administrator_signer,
2738                    &offer.member_pubkey,
2739                )
2740                .await
2741                .map_err(provider_error)?,
2742            )
2743        }
2744        _ => return Err(DeviceJoinError::AttemptMismatch),
2745    };
2746    let completion = DeviceProviderAdmissionCompletion {
2747        readiness: Box::new(readiness.clone()),
2748        admission,
2749    };
2750    let observed = DeviceJoinJournalRecord {
2751        attempt_id,
2752        progress: Box::new(DeviceJoinRoleProgress::ProviderAdministrator(
2753            ProviderAdminJoinProgress::ResponseObserved(readiness),
2754        )),
2755    };
2756    advance_store_journal(db, &current, observed.clone()).await?;
2757    advance_store_journal(
2758        db,
2759        &observed,
2760        DeviceJoinJournalRecord {
2761            attempt_id,
2762            progress: Box::new(DeviceJoinRoleProgress::ProviderAdministrator(
2763                ProviderAdminJoinProgress::Completed(completion.clone()),
2764            )),
2765        },
2766    )
2767    .await?;
2768    Ok(completion)
2769}
2770
2771#[allow(clippy::too_many_arguments)]
2772pub async fn cancel_device_join(
2773    db: &Database,
2774    storage: &dyn SyncStorage,
2775    coordination: Option<&dyn CoordinationStorage>,
2776    authorization: &DeviceJoinAuthorization,
2777    identity_signer: &UserKeypair,
2778    attempt_ref: DeviceJoinAttemptRef,
2779) -> Result<DeviceJoinCancellation, DeviceJoinError> {
2780    require_authorization_policy(db, storage, authorization).await?;
2781    let current = load_store_journal(db, attempt_ref.attempt_id, DeviceJoinRole::Owner)
2782        .await?
2783        .ok_or(DeviceJoinError::JournalConflict)?;
2784    if let DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Cancelled(existing)) =
2785        &*current.progress
2786    {
2787        if existing.outcome.attempt() == &attempt_ref {
2788            return Ok(existing.clone());
2789        }
2790        return Err(DeviceJoinError::JournalConflict);
2791    }
2792    let expected_attempt = match &*current.progress {
2793        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::AttemptActivated(bootstrap)) => {
2794            &bootstrap.publication_authorization.attempt
2795        }
2796        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::ProviderReady(bootstrap)) => {
2797            &bootstrap.bootstrap.publication_authorization.attempt
2798        }
2799        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::AdmissionCompleted(completion)) => {
2800            &completion.readiness.proof.attempt
2801        }
2802        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::CancellationCreateIntent {
2803            attempt,
2804            ..
2805        }) => attempt,
2806        _ => return Err(DeviceJoinError::JournalConflict),
2807    };
2808    if expected_attempt != &attempt_ref {
2809        return Err(DeviceJoinError::AttemptMismatch);
2810    }
2811    let root = db
2812        .local_store_root_ref()
2813        .await
2814        .map_err(database_error)?
2815        .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
2816    let attempt_context = crate::sync::storage::ProtocolObjectContext::store(
2817        root.store_root_hash,
2818        crate::sync::storage::ProtocolObjectDomain::DeviceJoinAttempt,
2819    );
2820    let attempt_prefix =
2821        crate::sync::store_commit::device_join_attempt_semantic_prefix(attempt_ref.attempt_id);
2822    let attempt_bytes = storage
2823        .read_protocol_object(&attempt_context, &attempt_ref.object, &attempt_prefix)
2824        .await?;
2825    let unverified_attempt: DeviceJoinAttempt = serde_json::from_slice(&attempt_bytes)?;
2826    let owner = db
2827        .activated_store_device_registration(unverified_attempt.owner_registration.clone())
2828        .await
2829        .map_err(database_error)?;
2830    let attempt = crate::sync::store_objects::load_device_join_attempt_ref(
2831        storage,
2832        &root,
2833        &attempt_ref,
2834        &owner,
2835    )
2836    .await?
2837    .value;
2838    let local_device_id = db
2839        .get_protocol_state(crate::database::LOCAL_DEVICE_ID_STATE_KEY)
2840        .await
2841        .map_err(database_error)?
2842        .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
2843    if owner.device_id.to_string() != local_device_id
2844        || !authorization.is_owner_now(&keys::public_key_hex(identity_signer))
2845    {
2846        return Err(DeviceJoinError::OwnerAuthorityRequired);
2847    }
2848    let owner_signer = owner.device_signer(identity_signer)?;
2849    let outcome = crate::sync::store_commit::DeviceJoinOutcome::signed(
2850        attempt_ref.clone(),
2851        crate::sync::store_commit::DeviceJoinOutcomeBody::Cancelled,
2852        attempt.owner_registration.clone(),
2853        attempt.owner_grant.clone(),
2854        &owner,
2855        &owner_signer,
2856    )?;
2857    let context = crate::sync::storage::ProtocolObjectContext::store(
2858        root.store_root_hash,
2859        crate::sync::storage::ProtocolObjectDomain::DeviceJoinOutcome,
2860    );
2861    let prefix =
2862        crate::sync::store_commit::device_join_outcome_semantic_prefix(attempt_ref.attempt_id);
2863    let prepared = storage.prepare_protocol_object(
2864        &context,
2865        attempt.outcome_slot.clone(),
2866        &prefix,
2867        outcome.to_bytes(),
2868    )?;
2869    let outcome_ref = DeviceJoinOutcomeRef::Cancelled {
2870        attempt: attempt_ref.clone(),
2871        outcome_hash: outcome.outcome_hash(),
2872        object: prepared.reference().clone(),
2873    };
2874    let intent = DeviceJoinJournalRecord {
2875        attempt_id: attempt_ref.attempt_id,
2876        progress: Box::new(DeviceJoinRoleProgress::Owner(
2877            OwnerJoinProgress::CancellationCreateIntent {
2878                attempt: attempt_ref.clone(),
2879                cancellation: outcome_ref.clone(),
2880                prepared: PreparedDeviceJoinObject::from_prepared(&prepared),
2881            },
2882        )),
2883    };
2884    match &*current.progress {
2885        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::AttemptActivated(_))
2886        | DeviceJoinRoleProgress::Owner(OwnerJoinProgress::ProviderReady(_))
2887        | DeviceJoinRoleProgress::Owner(OwnerJoinProgress::AdmissionCompleted(_)) => {
2888            advance_store_journal(db, &current, intent.clone()).await?;
2889        }
2890        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::CancellationCreateIntent {
2891            attempt,
2892            cancellation,
2893            prepared: durable_prepared,
2894        }) if attempt == &attempt_ref
2895            && cancellation == &outcome_ref
2896            && durable_prepared == &PreparedDeviceJoinObject::from_prepared(&prepared) => {}
2897        _ => return Err(DeviceJoinError::JournalConflict),
2898    }
2899    storage.create_protocol_object(&prepared).await?;
2900    let opened = storage
2901        .read_protocol_object(&context, prepared.reference(), &prefix)
2902        .await?;
2903    if opened != outcome.to_bytes() {
2904        return Err(DeviceJoinError::AttemptMismatch);
2905    }
2906    let verified_outcome = crate::sync::store_objects::load_device_join_outcome_ref(
2907        storage,
2908        &root,
2909        &outcome_ref,
2910        &owner,
2911    )
2912    .await?;
2913    if verified_outcome.value != outcome {
2914        return Err(DeviceJoinError::AttemptMismatch);
2915    }
2916    let plan = crate::sync::store_outbound::prepare_device_join_commit(
2917        db,
2918        storage,
2919        coordination,
2920        &local_device_id,
2921        identity_signer,
2922        authorization.merge_chain(),
2923    )
2924    .await?;
2925    let outcome_activation = crate::sync::store_outbound::activate_device_join_commit(
2926        db,
2927        storage,
2928        coordination,
2929        plan,
2930        crate::sync::store_outbound::DeviceJoinStoreBatch::Outcome {
2931            outcome: outcome_ref.clone(),
2932            registration: None,
2933        },
2934    )
2935    .await?;
2936    let cancellation = DeviceJoinCancellation {
2937        outcome: outcome_ref,
2938        outcome_activation,
2939    };
2940    advance_store_journal(
2941        db,
2942        &intent,
2943        DeviceJoinJournalRecord {
2944            attempt_id: attempt_ref.attempt_id,
2945            progress: Box::new(DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Cancelled(
2946                cancellation.clone(),
2947            ))),
2948        },
2949    )
2950    .await?;
2951    Ok(cancellation)
2952}
2953
2954#[allow(clippy::too_many_arguments)]
2955pub async fn prepare_device_join_cleanup(
2956    db: &Database,
2957    storage: &dyn SyncStorage,
2958    coordination: Option<&dyn CoordinationStorage>,
2959    executor_exact: &dyn ExactSlotStorage,
2960    authorization: &DeviceJoinAuthorization,
2961    identity_signer: &UserKeypair,
2962    cancellation: DeviceJoinCancellation,
2963    administrator_terminal: ProviderAdminJoinTerminal,
2964    joiner_terminal: JoinerJoinTerminal,
2965) -> Result<DeviceJoinCleanupReceipt, DeviceJoinError> {
2966    require_authorization_policy(db, storage, authorization).await?;
2967    require_cancelled_outcome(&cancellation.outcome)?;
2968    let attempt_ref = cancellation.outcome.attempt().clone();
2969    let current = load_store_journal(db, attempt_ref.attempt_id, DeviceJoinRole::Owner)
2970        .await?
2971        .ok_or(DeviceJoinError::JournalConflict)?;
2972    if let DeviceJoinRoleProgress::Owner(OwnerJoinProgress::CleanupReceipt(existing)) =
2973        &*current.progress
2974    {
2975        return Ok(existing.clone());
2976    }
2977    let durable_cancellation = match &*current.progress {
2978        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Cancelled(durable)) => durable,
2979        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::CleanupReceiptCreateIntent {
2980            cancellation: durable,
2981            ..
2982        }) => durable,
2983        _ => return Err(DeviceJoinError::JournalConflict),
2984    };
2985    if durable_cancellation != &cancellation {
2986        return Err(DeviceJoinError::JournalConflict);
2987    }
2988    let root = db
2989        .local_store_root_ref()
2990        .await
2991        .map_err(database_error)?
2992        .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
2993    let attempt_context = crate::sync::storage::ProtocolObjectContext::store(
2994        root.store_root_hash,
2995        crate::sync::storage::ProtocolObjectDomain::DeviceJoinAttempt,
2996    );
2997    let attempt_prefix =
2998        crate::sync::store_commit::device_join_attempt_semantic_prefix(attempt_ref.attempt_id);
2999    let attempt_bytes = storage
3000        .read_protocol_object(&attempt_context, &attempt_ref.object, &attempt_prefix)
3001        .await?;
3002    let unverified_attempt: DeviceJoinAttempt = serde_json::from_slice(&attempt_bytes)?;
3003    let owner = db
3004        .activated_store_device_registration(unverified_attempt.owner_registration.clone())
3005        .await
3006        .map_err(database_error)?;
3007    let attempt = crate::sync::store_objects::load_device_join_attempt_ref(
3008        storage,
3009        &root,
3010        &attempt_ref,
3011        &owner,
3012    )
3013    .await?
3014    .value;
3015    let outcome = crate::sync::store_objects::load_device_join_outcome_ref(
3016        storage,
3017        &root,
3018        &cancellation.outcome,
3019        &owner,
3020    )
3021    .await?
3022    .value;
3023    if !matches!(
3024        outcome.body,
3025        crate::sync::store_commit::DeviceJoinOutcomeBody::Cancelled
3026    ) {
3027        return Err(DeviceJoinError::AttemptMismatch);
3028    }
3029    validate_terminals(
3030        &cancellation.outcome,
3031        &administrator_terminal,
3032        &joiner_terminal,
3033    )?;
3034    verify_cleanup_terminals(db, &administrator_terminal, &joiner_terminal).await?;
3035    let local_device_id = db
3036        .get_protocol_state(crate::database::LOCAL_DEVICE_ID_STATE_KEY)
3037        .await
3038        .map_err(database_error)?
3039        .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
3040    let (local_root, executor_ref, executor, executor_signer) =
3041        crate::sync::store_outbound::load_local_store_authority(
3042            db,
3043            &local_device_id,
3044            identity_signer,
3045        )
3046        .await?;
3047    if local_root != root || !authorization.is_owner_now(&executor.author_pubkey) {
3048        return Err(DeviceJoinError::OwnerAuthorityRequired);
3049    }
3050    let effective_executor = authorization.resolved_provider_admin(
3051        &attempt
3052            .provider_approval
3053            .request
3054            .offer
3055            .provider_admin
3056            .grant_id,
3057    )?;
3058    if effective_executor != *attempt.provider_approval.request.offer.provider_admin
3059        || effective_executor.administrator != executor_ref
3060        || effective_executor.provider != executor.provider
3061    {
3062        return Err(DeviceJoinError::ProviderAdministratorRequired);
3063    }
3064    let plan = crate::sync::store_outbound::prepare_device_join_commit(
3065        db,
3066        storage,
3067        coordination,
3068        &local_device_id,
3069        identity_signer,
3070        authorization.merge_chain(),
3071    )
3072    .await?;
3073    let deleted_slots = canonical_cleanup_slots(&attempt)?;
3074    let receipt_object = DeviceJoinCleanupReceiptObject::signed(
3075        &attempt,
3076        cancellation.outcome.clone(),
3077        administrator_terminal,
3078        joiner_terminal,
3079        deleted_slots.clone(),
3080        plan.membership_state().clone(),
3081        attempt
3082            .provider_approval
3083            .request
3084            .offer
3085            .provider_admin
3086            .grant_id
3087            .clone(),
3088        executor_ref,
3089        &executor,
3090        &executor_signer,
3091    )?;
3092    let context = crate::sync::storage::ProtocolObjectContext::store(
3093        root.store_root_hash,
3094        crate::sync::storage::ProtocolObjectDomain::DeviceJoinCleanupReceipt,
3095    );
3096    let prefix = crate::sync::store_commit::device_join_cleanup_receipt_semantic_prefix(
3097        attempt_ref.attempt_id,
3098    );
3099    let slot = storage
3100        .allocate_protocol_slot(&context, &prefix, ".json")
3101        .await?;
3102    let prepared =
3103        storage.prepare_protocol_object(&context, slot, &prefix, receipt_object.to_bytes())?;
3104    let receipt_ref = DeviceJoinCleanupReceiptRef {
3105        attempt_id: attempt_ref.attempt_id,
3106        receipt_hash: receipt_object.receipt_hash(),
3107        object: prepared.reference().clone(),
3108    };
3109    let intent = DeviceJoinJournalRecord {
3110        attempt_id: attempt_ref.attempt_id,
3111        progress: Box::new(DeviceJoinRoleProgress::Owner(
3112            OwnerJoinProgress::CleanupReceiptCreateIntent {
3113                cancellation: cancellation.clone(),
3114                receipt: receipt_ref.clone(),
3115                prepared: PreparedDeviceJoinObject::from_prepared(&prepared),
3116            },
3117        )),
3118    };
3119    match &*current.progress {
3120        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Cancelled(_)) => {
3121            advance_store_journal(db, &current, intent.clone()).await?;
3122        }
3123        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::CleanupReceiptCreateIntent {
3124            receipt,
3125            prepared: durable_prepared,
3126            ..
3127        }) if receipt == &receipt_ref
3128            && durable_prepared == &PreparedDeviceJoinObject::from_prepared(&prepared) => {}
3129        _ => return Err(DeviceJoinError::JournalConflict),
3130    }
3131    for slot in &deleted_slots {
3132        ensure_exact_slot_absent(executor_exact, slot).await?;
3133    }
3134    storage.create_protocol_object(&prepared).await?;
3135    let opened = storage
3136        .read_protocol_object(&context, prepared.reference(), &prefix)
3137        .await?;
3138    if opened != receipt_object.to_bytes() {
3139        return Err(DeviceJoinError::CleanupMismatch);
3140    }
3141    receipt_ref.verify(&receipt_object, &executor)?;
3142    let receipt = DeviceJoinCleanupReceipt {
3143        receipt: receipt_ref,
3144    };
3145    advance_store_journal(
3146        db,
3147        &intent,
3148        DeviceJoinJournalRecord {
3149            attempt_id: attempt_ref.attempt_id,
3150            progress: Box::new(DeviceJoinRoleProgress::Owner(
3151                OwnerJoinProgress::CleanupReceipt(receipt.clone()),
3152            )),
3153        },
3154    )
3155    .await?;
3156    Ok(receipt)
3157}
3158
3159pub async fn close_device_provider_admission(
3160    db: &Database,
3161    storage: &dyn SyncStorage,
3162    administrator_exact: Option<&dyn ExactSlotStorage>,
3163    identity_signer: &UserKeypair,
3164    cancellation: DeviceJoinCancellation,
3165) -> Result<ProviderAdminJoinTerminal, DeviceJoinError> {
3166    require_cancelled_outcome(&cancellation.outcome)?;
3167    let attempt_ref = cancellation.outcome.attempt().clone();
3168    let current = load_store_journal(
3169        db,
3170        attempt_ref.attempt_id,
3171        DeviceJoinRole::ProviderAdministrator,
3172    )
3173    .await?
3174    .ok_or(DeviceJoinError::JournalConflict)?;
3175    match &*current.progress {
3176        DeviceJoinRoleProgress::ProviderAdministrator(ProviderAdminJoinProgress::Completed(
3177            completion,
3178        )) => return Ok(ProviderAdminJoinTerminal::Completed(completion.clone())),
3179        DeviceJoinRoleProgress::ProviderAdministrator(ProviderAdminJoinProgress::Cancelled(
3180            closure,
3181        )) => return Ok(ProviderAdminJoinTerminal::Cancelled(closure.clone())),
3182        DeviceJoinRoleProgress::ProviderAdministrator(ProviderAdminJoinProgress::WriteRevoked(
3183            revocation,
3184        )) => return Ok(ProviderAdminJoinTerminal::WriteRevoked(revocation.clone())),
3185        _ => {}
3186    }
3187    let root = db
3188        .local_store_root_ref()
3189        .await
3190        .map_err(database_error)?
3191        .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
3192    let attempt_context = crate::sync::storage::ProtocolObjectContext::store(
3193        root.store_root_hash,
3194        crate::sync::storage::ProtocolObjectDomain::DeviceJoinAttempt,
3195    );
3196    let attempt_prefix =
3197        crate::sync::store_commit::device_join_attempt_semantic_prefix(attempt_ref.attempt_id);
3198    let attempt_bytes = storage
3199        .read_protocol_object(&attempt_context, &attempt_ref.object, &attempt_prefix)
3200        .await?;
3201    let unverified_attempt: DeviceJoinAttempt = serde_json::from_slice(&attempt_bytes)?;
3202    let owner = db
3203        .activated_store_device_registration(unverified_attempt.owner_registration.clone())
3204        .await
3205        .map_err(database_error)?;
3206    let attempt = crate::sync::store_objects::load_device_join_attempt_ref(
3207        storage,
3208        &root,
3209        &attempt_ref,
3210        &owner,
3211    )
3212    .await?
3213    .value;
3214    let outcome = crate::sync::store_objects::load_device_join_outcome_ref(
3215        storage,
3216        &root,
3217        &cancellation.outcome,
3218        &owner,
3219    )
3220    .await?
3221    .value;
3222    if !matches!(
3223        outcome.body,
3224        crate::sync::store_commit::DeviceJoinOutcomeBody::Cancelled
3225    ) {
3226        return Err(DeviceJoinError::AttemptMismatch);
3227    }
3228    let offer = &attempt.provider_approval.request.offer;
3229    let administrator = db
3230        .activated_store_device_registration(offer.provider_admin.administrator.clone())
3231        .await
3232        .map_err(database_error)?;
3233    let local_device_id = db
3234        .get_protocol_state(crate::database::LOCAL_DEVICE_ID_STATE_KEY)
3235        .await
3236        .map_err(database_error)?
3237        .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
3238    if administrator.device_id.to_string() != local_device_id {
3239        return Err(DeviceJoinError::ProviderAdministratorRequired);
3240    }
3241    let administrator_signer = administrator.device_signer(identity_signer)?;
3242    let (challenge, prior_state_hash) = match &*current.progress {
3243        DeviceJoinRoleProgress::ProviderAdministrator(
3244            ProviderAdminJoinProgress::CleanupIntent {
3245                cancellation: durable,
3246                challenge,
3247                prior_state_hash,
3248            },
3249        ) if durable == &cancellation => (challenge.clone(), *prior_state_hash),
3250        DeviceJoinRoleProgress::ProviderAdministrator(
3251            ProviderAdminJoinProgress::ApprovalPrepared(_)
3252            | ProviderAdminJoinProgress::AttemptObserved(_)
3253            | ProviderAdminJoinProgress::ChallengeCreateIntent(_)
3254            | ProviderAdminJoinProgress::ProviderReady(_)
3255            | ProviderAdminJoinProgress::ResponseObserved(_),
3256        ) => {
3257            let challenge = match &attempt.provider_approval.admission {
3258                DeviceProviderAdmissionChallenge::SamePrincipal => {
3259                    ProviderChallengeDisposition::SamePrincipal
3260                }
3261                DeviceProviderAdmissionChallenge::CrossPrincipal(challenge) => {
3262                    let exact =
3263                        administrator_exact.ok_or(DeviceJoinError::ExactSlotStorageRequired)?;
3264                    match exact.read_at(&challenge.administrator_object.slot).await {
3265                        Err(crate::storage::cloud::CloudHomeError::NotFound(_)) => {
3266                            ProviderChallengeDisposition::NeverCreated
3267                        }
3268                        Ok(bytes) => {
3269                            challenge
3270                                .administrator_object
3271                                .object
3272                                .verify(&bytes)
3273                                .map_err(|error| DeviceJoinError::Provider(error.to_string()))?;
3274                            ProviderChallengeDisposition::Created(
3275                                challenge.administrator_object.object.clone(),
3276                            )
3277                        }
3278                        Err(error) => return Err(DeviceJoinError::Provider(error.to_string())),
3279                    }
3280                }
3281            };
3282            let prior_state_hash = ObjectHash::digest(&serde_json::to_vec(&current.progress)?);
3283            let intent = DeviceJoinJournalRecord {
3284                attempt_id: attempt_ref.attempt_id,
3285                progress: Box::new(DeviceJoinRoleProgress::ProviderAdministrator(
3286                    ProviderAdminJoinProgress::CleanupIntent {
3287                        cancellation: cancellation.clone(),
3288                        challenge: challenge.clone(),
3289                        prior_state_hash,
3290                    },
3291                )),
3292            };
3293            advance_store_journal(db, &current, intent).await?;
3294            (challenge, prior_state_hash)
3295        }
3296        _ => return Err(DeviceJoinError::JournalConflict),
3297    };
3298    if let DeviceProviderAdmissionChallenge::CrossPrincipal(probe) =
3299        &attempt.provider_approval.admission
3300    {
3301        ensure_exact_slot_absent(
3302            administrator_exact.ok_or(DeviceJoinError::ExactSlotStorageRequired)?,
3303            &probe.administrator_object.slot,
3304        )
3305        .await?;
3306    }
3307    let closure = ProviderAdminJoinClosure::signed(
3308        cancellation.outcome,
3309        offer.provider_admin.administrator.clone(),
3310        challenge,
3311        prior_state_hash,
3312        &administrator,
3313        &administrator_signer,
3314    )?;
3315    let intent = load_store_journal(
3316        db,
3317        attempt_ref.attempt_id,
3318        DeviceJoinRole::ProviderAdministrator,
3319    )
3320    .await?
3321    .ok_or(DeviceJoinError::JournalConflict)?;
3322    advance_store_journal(
3323        db,
3324        &intent,
3325        DeviceJoinJournalRecord {
3326            attempt_id: attempt_ref.attempt_id,
3327            progress: Box::new(DeviceJoinRoleProgress::ProviderAdministrator(
3328                ProviderAdminJoinProgress::Cancelled(closure.clone()),
3329            )),
3330        },
3331    )
3332    .await?;
3333    Ok(ProviderAdminJoinTerminal::Cancelled(closure))
3334}
3335
3336pub async fn close_joining_device(
3337    pending: &DeviceJoinJournalDatabase,
3338    storage: &dyn SyncStorage,
3339    peer_exact: &dyn ExactSlotStorage,
3340    root: &StoreRootRef,
3341    identity_signer: &UserKeypair,
3342    cancellation: DeviceJoinCancellation,
3343) -> Result<JoinerJoinTerminal, DeviceJoinError> {
3344    require_cancelled_outcome(&cancellation.outcome)?;
3345    let attempt_ref = cancellation.outcome.attempt().clone();
3346    let current = pending
3347        .load(attempt_ref.attempt_id, DeviceJoinRole::Joiner)?
3348        .ok_or(DeviceJoinError::JournalConflict)?;
3349    match &*current.progress {
3350        DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::Cancelled(closure)) => {
3351            return Ok(JoinerJoinTerminal::Cancelled(closure.clone()));
3352        }
3353        DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::WriteRevoked(revocation)) => {
3354            return Ok(JoinerJoinTerminal::WriteRevoked(revocation.clone()));
3355        }
3356        _ => {}
3357    }
3358    let allowed = matches!(
3359        &*current.progress,
3360        DeviceJoinRoleProgress::Joiner(
3361            JoinerJoinProgress::RegistrationPrepared(_)
3362                | JoinerJoinProgress::ProviderReady(_)
3363                | JoinerJoinProgress::RegistrationCreateIntent(_)
3364                | JoinerJoinProgress::RegistrationCreated(_)
3365                | JoinerJoinProgress::AckCreateIntent(_)
3366                | JoinerJoinProgress::AckCreated(_)
3367                | JoinerJoinProgress::ResponseCreateIntent(_)
3368                | JoinerJoinProgress::Ready(_)
3369                | JoinerJoinProgress::CleanupIntent { .. }
3370        )
3371    );
3372    if !allowed {
3373        return Err(DeviceJoinError::JournalConflict);
3374    }
3375    let attempt_context = crate::sync::storage::ProtocolObjectContext::store(
3376        root.store_root_hash,
3377        crate::sync::storage::ProtocolObjectDomain::DeviceJoinAttempt,
3378    );
3379    let attempt_prefix =
3380        crate::sync::store_commit::device_join_attempt_semantic_prefix(attempt_ref.attempt_id);
3381    let attempt_bytes = storage
3382        .read_protocol_object(&attempt_context, &attempt_ref.object, &attempt_prefix)
3383        .await?;
3384    let unverified_attempt: DeviceJoinAttempt = serde_json::from_slice(&attempt_bytes)?;
3385    let owner = crate::sync::store_objects::load_registration_ref(
3386        storage,
3387        root,
3388        &unverified_attempt.owner_registration,
3389    )
3390    .await?
3391    .value;
3392    let attempt = crate::sync::store_objects::load_device_join_attempt_ref(
3393        storage,
3394        root,
3395        &attempt_ref,
3396        &owner,
3397    )
3398    .await?
3399    .value;
3400    let outcome = crate::sync::store_objects::load_device_join_outcome_ref(
3401        storage,
3402        root,
3403        &cancellation.outcome,
3404        &owner,
3405    )
3406    .await?
3407    .value;
3408    if !matches!(
3409        outcome.body,
3410        crate::sync::store_commit::DeviceJoinOutcomeBody::Cancelled
3411    ) {
3412        return Err(DeviceJoinError::AttemptMismatch);
3413    }
3414    let joining_device_signer = attempt
3415        .expected_registration
3416        .device_signer(identity_signer)?;
3417    let (registration, initial_ack, response, prior_state_hash, intent) = match &*current.progress {
3418        DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::CleanupIntent {
3419            cancellation: durable,
3420            registration,
3421            initial_ack,
3422            response,
3423            prior_state_hash,
3424        }) if durable == &cancellation => (
3425            registration.clone(),
3426            initial_ack.clone(),
3427            response.clone(),
3428            *prior_state_hash,
3429            current.clone(),
3430        ),
3431        _ => {
3432            let registration = observe_exact_slot(peer_exact, &attempt.registration_slot).await?;
3433            let initial_ack = observe_exact_slot(
3434                peer_exact,
3435                attempt.expected_registration.acknowledgements.first_slot(),
3436            )
3437            .await?;
3438            let response = match &attempt.provider_response {
3439                DeviceProviderResponseReservation::SamePrincipal => {
3440                    JoinerResponseDisposition::SamePrincipal
3441                }
3442                DeviceProviderResponseReservation::CrossPrincipal { response_slot } => {
3443                    JoinerResponseDisposition::Slot(
3444                        observe_exact_slot(peer_exact, response_slot).await?,
3445                    )
3446                }
3447            };
3448            let prior_state_hash = ObjectHash::digest(&serde_json::to_vec(&current.progress)?);
3449            let intent = DeviceJoinJournalRecord {
3450                attempt_id: attempt_ref.attempt_id,
3451                progress: Box::new(DeviceJoinRoleProgress::Joiner(
3452                    JoinerJoinProgress::CleanupIntent {
3453                        cancellation: cancellation.clone(),
3454                        registration: registration.clone(),
3455                        initial_ack: initial_ack.clone(),
3456                        response: response.clone(),
3457                        prior_state_hash,
3458                    },
3459                )),
3460            };
3461            pending.advance(&current, intent.clone())?;
3462            (
3463                registration,
3464                initial_ack,
3465                response,
3466                prior_state_hash,
3467                intent,
3468            )
3469        }
3470    };
3471    for slot in canonical_cleanup_slots(&attempt)? {
3472        ensure_exact_slot_absent(peer_exact, &slot).await?;
3473    }
3474    let closure = JoinerJoinClosure::signed(
3475        cancellation.outcome,
3476        attempt.expected_registration,
3477        registration,
3478        initial_ack,
3479        response,
3480        prior_state_hash,
3481        &joining_device_signer,
3482    )?;
3483    pending.advance(
3484        &intent,
3485        DeviceJoinJournalRecord {
3486            attempt_id: attempt_ref.attempt_id,
3487            progress: Box::new(DeviceJoinRoleProgress::Joiner(
3488                JoinerJoinProgress::Cancelled(closure.clone()),
3489            )),
3490        },
3491    )?;
3492    Ok(JoinerJoinTerminal::Cancelled(closure))
3493}
3494
3495#[allow(clippy::too_many_arguments)]
3496async fn sign_device_join_producer_write_revocation(
3497    db: &Database,
3498    storage: &dyn SyncStorage,
3499    authorization: &DeviceJoinAuthorization,
3500    identity_signer: &UserKeypair,
3501    cancellation: DeviceJoinCancellation,
3502    producer: DeviceJoinProducer,
3503    withdrawal: ProviderAccessWithdrawal,
3504    executor_grant: ProviderAdminGrantId,
3505) -> Result<DeviceJoinProducerWriteRevocation, DeviceJoinError> {
3506    require_authorization_policy(db, storage, authorization).await?;
3507    require_cancelled_outcome(&cancellation.outcome)?;
3508    let attempt_ref = cancellation.outcome.attempt().clone();
3509    let root = db
3510        .local_store_root_ref()
3511        .await
3512        .map_err(database_error)?
3513        .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
3514    let attempt_context = crate::sync::storage::ProtocolObjectContext::store(
3515        root.store_root_hash,
3516        crate::sync::storage::ProtocolObjectDomain::DeviceJoinAttempt,
3517    );
3518    let attempt_prefix =
3519        crate::sync::store_commit::device_join_attempt_semantic_prefix(attempt_ref.attempt_id);
3520    let attempt_bytes = storage
3521        .read_protocol_object(&attempt_context, &attempt_ref.object, &attempt_prefix)
3522        .await?;
3523    let unverified_attempt: DeviceJoinAttempt = serde_json::from_slice(&attempt_bytes)?;
3524    let owner = db
3525        .activated_store_device_registration(unverified_attempt.owner_registration.clone())
3526        .await
3527        .map_err(database_error)?;
3528    let attempt = crate::sync::store_objects::load_device_join_attempt_ref(
3529        storage,
3530        &root,
3531        &attempt_ref,
3532        &owner,
3533    )
3534    .await?
3535    .value;
3536    let outcome = crate::sync::store_objects::load_device_join_outcome_ref(
3537        storage,
3538        &root,
3539        &cancellation.outcome,
3540        &owner,
3541    )
3542    .await?
3543    .value;
3544    if !matches!(
3545        outcome.body,
3546        crate::sync::store_commit::DeviceJoinOutcomeBody::Cancelled
3547    ) {
3548        return Err(DeviceJoinError::AttemptMismatch);
3549    }
3550    let executor_admin = authorization.resolved_provider_admin(&executor_grant)?;
3551    let executor = db
3552        .activated_store_device_registration(executor_admin.administrator.clone())
3553        .await
3554        .map_err(database_error)?;
3555    let local_device_id = db
3556        .get_protocol_state(crate::database::LOCAL_DEVICE_ID_STATE_KEY)
3557        .await
3558        .map_err(database_error)?
3559        .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
3560    if executor.device_id.to_string() != local_device_id {
3561        return Err(DeviceJoinError::ProviderAdministratorRequired);
3562    }
3563    let executor_signer = executor.device_signer(identity_signer)?;
3564    let (authority, protected_slots, locator) = match producer {
3565        DeviceJoinProducer::ProviderAdministrator => {
3566            let DeviceProviderAdmissionChallenge::CrossPrincipal(challenge) =
3567                &attempt.provider_approval.admission
3568            else {
3569                return Err(DeviceJoinError::CleanupMismatch);
3570            };
3571            (
3572                ProviderWriteAuthorityRef::ProviderAdministrator(
3573                    attempt
3574                        .provider_approval
3575                        .request
3576                        .offer
3577                        .provider_admin
3578                        .grant_id
3579                        .clone(),
3580                ),
3581                vec![challenge.administrator_object.slot.clone()],
3582                &attempt
3583                    .provider_approval
3584                    .request
3585                    .offer
3586                    .provider_admin
3587                    .access,
3588            )
3589        }
3590        DeviceJoinProducer::Joiner => {
3591            let mut slots = vec![
3592                attempt.registration_slot.clone(),
3593                attempt
3594                    .expected_registration
3595                    .acknowledgements
3596                    .first_slot()
3597                    .clone(),
3598            ];
3599            if let DeviceProviderResponseReservation::CrossPrincipal { response_slot } =
3600                &attempt.provider_response
3601            {
3602                slots.push(response_slot.clone());
3603            }
3604            (
3605                ProviderWriteAuthorityRef::MemberAccess(
3606                    attempt.provider_approval.access_grant.grant_ref.clone(),
3607                ),
3608                slots,
3609                &attempt.provider_approval.access_grant.grant.locator,
3610            )
3611        }
3612    };
3613    if !withdrawal_matches_locator(&withdrawal, locator) {
3614        return Err(DeviceJoinError::CleanupMismatch);
3615    }
3616    DeviceJoinProducerWriteRevocation::signed(
3617        cancellation.outcome,
3618        producer,
3619        authority,
3620        protected_slots,
3621        withdrawal,
3622        executor_grant,
3623        executor_admin.administrator,
3624        &executor,
3625        &executor_signer,
3626    )
3627}
3628
3629#[allow(clippy::too_many_arguments)]
3630pub async fn revoke_device_provider_admission_writes(
3631    db: &Database,
3632    storage: &dyn SyncStorage,
3633    authorization: &DeviceJoinAuthorization,
3634    identity_signer: &UserKeypair,
3635    cancellation: DeviceJoinCancellation,
3636    withdrawal: ProviderAccessWithdrawal,
3637    executor_grant: ProviderAdminGrantId,
3638) -> Result<ProviderAdminJoinTerminal, DeviceJoinError> {
3639    let attempt_id = cancellation.outcome.attempt().attempt_id;
3640    let current = load_store_journal(db, attempt_id, DeviceJoinRole::ProviderAdministrator)
3641        .await?
3642        .ok_or(DeviceJoinError::JournalConflict)?;
3643    match &*current.progress {
3644        DeviceJoinRoleProgress::ProviderAdministrator(ProviderAdminJoinProgress::Completed(
3645            completion,
3646        )) => return Ok(ProviderAdminJoinTerminal::Completed(completion.clone())),
3647        DeviceJoinRoleProgress::ProviderAdministrator(ProviderAdminJoinProgress::Cancelled(
3648            closure,
3649        )) => return Ok(ProviderAdminJoinTerminal::Cancelled(closure.clone())),
3650        DeviceJoinRoleProgress::ProviderAdministrator(ProviderAdminJoinProgress::WriteRevoked(
3651            revocation,
3652        )) => return Ok(ProviderAdminJoinTerminal::WriteRevoked(revocation.clone())),
3653        DeviceJoinRoleProgress::ProviderAdministrator(
3654            ProviderAdminJoinProgress::ApprovalPrepared(_)
3655            | ProviderAdminJoinProgress::AttemptObserved(_)
3656            | ProviderAdminJoinProgress::ChallengeCreateIntent(_)
3657            | ProviderAdminJoinProgress::ProviderReady(_)
3658            | ProviderAdminJoinProgress::ResponseObserved(_)
3659            | ProviderAdminJoinProgress::CleanupIntent { .. },
3660        ) => {}
3661        _ => return Err(DeviceJoinError::JournalConflict),
3662    }
3663    let revocation = Box::pin(sign_device_join_producer_write_revocation(
3664        db,
3665        storage,
3666        authorization,
3667        identity_signer,
3668        cancellation,
3669        DeviceJoinProducer::ProviderAdministrator,
3670        withdrawal,
3671        executor_grant,
3672    ))
3673    .await?;
3674    advance_store_journal(
3675        db,
3676        &current,
3677        DeviceJoinJournalRecord {
3678            attempt_id,
3679            progress: Box::new(DeviceJoinRoleProgress::ProviderAdministrator(
3680                ProviderAdminJoinProgress::WriteRevoked(revocation.clone()),
3681            )),
3682        },
3683    )
3684    .await?;
3685    Ok(ProviderAdminJoinTerminal::WriteRevoked(revocation))
3686}
3687
3688#[allow(clippy::too_many_arguments)]
3689pub async fn revoke_joining_device_writes(
3690    pending: &DeviceJoinJournalDatabase,
3691    db: &Database,
3692    storage: &dyn SyncStorage,
3693    authorization: &DeviceJoinAuthorization,
3694    identity_signer: &UserKeypair,
3695    cancellation: DeviceJoinCancellation,
3696    withdrawal: ProviderAccessWithdrawal,
3697    executor_grant: ProviderAdminGrantId,
3698) -> Result<JoinerJoinTerminal, DeviceJoinError> {
3699    let attempt_id = cancellation.outcome.attempt().attempt_id;
3700    let current = pending
3701        .load(attempt_id, DeviceJoinRole::Joiner)?
3702        .ok_or(DeviceJoinError::JournalConflict)?;
3703    match &*current.progress {
3704        DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::Ready(readiness)) => {
3705            return Ok(JoinerJoinTerminal::Ready(readiness.clone()));
3706        }
3707        DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::Cancelled(closure)) => {
3708            return Ok(JoinerJoinTerminal::Cancelled(closure.clone()));
3709        }
3710        DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::WriteRevoked(revocation)) => {
3711            return Ok(JoinerJoinTerminal::WriteRevoked(revocation.clone()));
3712        }
3713        DeviceJoinRoleProgress::Joiner(
3714            JoinerJoinProgress::RegistrationPrepared(_)
3715            | JoinerJoinProgress::ProviderReady(_)
3716            | JoinerJoinProgress::RegistrationCreateIntent(_)
3717            | JoinerJoinProgress::RegistrationCreated(_)
3718            | JoinerJoinProgress::AckCreateIntent(_)
3719            | JoinerJoinProgress::AckCreated(_)
3720            | JoinerJoinProgress::ResponseCreateIntent(_)
3721            | JoinerJoinProgress::CleanupIntent { .. },
3722        ) => {}
3723        _ => return Err(DeviceJoinError::JournalConflict),
3724    }
3725    let revocation = Box::pin(sign_device_join_producer_write_revocation(
3726        db,
3727        storage,
3728        authorization,
3729        identity_signer,
3730        cancellation,
3731        DeviceJoinProducer::Joiner,
3732        withdrawal,
3733        executor_grant,
3734    ))
3735    .await?;
3736    pending.advance(
3737        &current,
3738        DeviceJoinJournalRecord {
3739            attempt_id,
3740            progress: Box::new(DeviceJoinRoleProgress::Joiner(
3741                JoinerJoinProgress::WriteRevoked(revocation.clone()),
3742            )),
3743        },
3744    )?;
3745    Ok(JoinerJoinTerminal::WriteRevoked(revocation))
3746}
3747
3748#[allow(clippy::too_many_arguments)]
3749pub async fn activate_device_join_cleanup(
3750    db: &Database,
3751    storage: &dyn SyncStorage,
3752    coordination: Option<&dyn CoordinationStorage>,
3753    authorization: &DeviceJoinAuthorization,
3754    identity_signer: &UserKeypair,
3755    attempt_id: DeviceJoinAttemptId,
3756    receipt: DeviceJoinCleanupReceipt,
3757) -> Result<DeviceJoinCleanupActivation, DeviceJoinError> {
3758    require_authorization_policy(db, storage, authorization).await?;
3759    let current = load_store_journal(db, attempt_id, DeviceJoinRole::Owner)
3760        .await?
3761        .ok_or(DeviceJoinError::JournalConflict)?;
3762    if let DeviceJoinRoleProgress::Owner(OwnerJoinProgress::CleanupActivated(existing)) =
3763        &*current.progress
3764    {
3765        if existing.receipt == receipt.receipt {
3766            return Ok(existing.clone());
3767        }
3768        return Err(DeviceJoinError::JournalConflict);
3769    }
3770    let DeviceJoinRoleProgress::Owner(OwnerJoinProgress::CleanupReceipt(durable)) =
3771        &*current.progress
3772    else {
3773        return Err(DeviceJoinError::JournalConflict);
3774    };
3775    if durable != &receipt {
3776        return Err(DeviceJoinError::JournalConflict);
3777    }
3778    let local_device_id = db
3779        .get_protocol_state(crate::database::LOCAL_DEVICE_ID_STATE_KEY)
3780        .await
3781        .map_err(database_error)?
3782        .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
3783    let plan = crate::sync::store_outbound::prepare_device_join_commit(
3784        db,
3785        storage,
3786        coordination,
3787        &local_device_id,
3788        identity_signer,
3789        authorization.merge_chain(),
3790    )
3791    .await?;
3792    let root = db
3793        .local_store_root_ref()
3794        .await
3795        .map_err(database_error)?
3796        .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
3797    let context = crate::sync::storage::ProtocolObjectContext::store(
3798        root.store_root_hash,
3799        crate::sync::storage::ProtocolObjectDomain::DeviceJoinCleanupReceipt,
3800    );
3801    let prefix = crate::sync::store_commit::device_join_cleanup_receipt_semantic_prefix(
3802        receipt.receipt.attempt_id,
3803    );
3804    let bytes = storage
3805        .read_protocol_object(&context, &receipt.receipt.object, &prefix)
3806        .await?;
3807    let receipt_object: DeviceJoinCleanupReceiptObject = serde_json::from_slice(&bytes)?;
3808    let executor = db
3809        .activated_store_device_registration(receipt_object.executor.clone())
3810        .await
3811        .map_err(database_error)?;
3812    receipt.receipt.verify(&receipt_object, &executor)?;
3813    if receipt_object.store_root_hash != root.store_root_hash
3814        || plan.membership_state() != &receipt_object.membership
3815    {
3816        return Err(DeviceJoinError::CleanupMismatch);
3817    }
3818    let activation_ref = crate::sync::store_outbound::activate_device_join_commit(
3819        db,
3820        storage,
3821        coordination,
3822        plan,
3823        crate::sync::store_outbound::DeviceJoinStoreBatch::CleanupReceipt(receipt.receipt.clone()),
3824    )
3825    .await?;
3826    let activation = DeviceJoinCleanupActivation {
3827        receipt: receipt.receipt,
3828        activation: activation_ref,
3829    };
3830    advance_store_journal(
3831        db,
3832        &current,
3833        DeviceJoinJournalRecord {
3834            attempt_id,
3835            progress: Box::new(DeviceJoinRoleProgress::Owner(
3836                OwnerJoinProgress::CleanupActivated(activation.clone()),
3837            )),
3838        },
3839    )
3840    .await?;
3841    Ok(activation)
3842}
3843
3844pub async fn complete_owner_device_join_cleanup(
3845    db: &Database,
3846    attempt_id: DeviceJoinAttemptId,
3847    activation: DeviceJoinCleanupActivation,
3848) -> Result<DeviceJoinCleanupActivation, DeviceJoinError> {
3849    let current = load_store_journal(db, attempt_id, DeviceJoinRole::Owner)
3850        .await?
3851        .ok_or(DeviceJoinError::JournalConflict)?;
3852    if let DeviceJoinRoleProgress::Owner(OwnerJoinProgress::CancelledComplete(existing)) =
3853        &*current.progress
3854    {
3855        if existing == &activation {
3856            return Ok(existing.clone());
3857        }
3858        return Err(DeviceJoinError::JournalConflict);
3859    }
3860    let DeviceJoinRoleProgress::Owner(OwnerJoinProgress::CleanupActivated(durable)) =
3861        &*current.progress
3862    else {
3863        return Err(DeviceJoinError::JournalConflict);
3864    };
3865    if durable != &activation {
3866        return Err(DeviceJoinError::JournalConflict);
3867    }
3868    advance_store_journal(
3869        db,
3870        &current,
3871        DeviceJoinJournalRecord {
3872            attempt_id,
3873            progress: Box::new(DeviceJoinRoleProgress::Owner(
3874                OwnerJoinProgress::CancelledComplete(activation.clone()),
3875            )),
3876        },
3877    )
3878    .await?;
3879    Ok(activation)
3880}
3881
3882pub fn complete_joiner_device_join_cleanup(
3883    pending: &DeviceJoinJournalDatabase,
3884    attempt_id: DeviceJoinAttemptId,
3885    activation: DeviceJoinCleanupActivation,
3886) -> Result<DeviceJoinCleanupActivation, DeviceJoinError> {
3887    let current = pending
3888        .load(attempt_id, DeviceJoinRole::Joiner)?
3889        .ok_or(DeviceJoinError::JournalConflict)?;
3890    if let DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::CancelledComplete(existing)) =
3891        &*current.progress
3892    {
3893        if existing == &activation {
3894            return Ok(existing.clone());
3895        }
3896        return Err(DeviceJoinError::JournalConflict);
3897    }
3898    let activated = DeviceJoinJournalRecord {
3899        attempt_id,
3900        progress: Box::new(DeviceJoinRoleProgress::Joiner(
3901            JoinerJoinProgress::CleanupActivated(activation.clone()),
3902        )),
3903    };
3904    match &*current.progress {
3905        DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::Cancelled(_))
3906        | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::WriteRevoked(_)) => {
3907            pending.advance(&current, activated.clone())?;
3908        }
3909        DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::CleanupActivated(durable))
3910            if durable == &activation => {}
3911        _ => return Err(DeviceJoinError::JournalConflict),
3912    }
3913    pending.advance(
3914        &activated,
3915        DeviceJoinJournalRecord {
3916            attempt_id,
3917            progress: Box::new(DeviceJoinRoleProgress::Joiner(
3918                JoinerJoinProgress::CancelledComplete(activation.clone()),
3919            )),
3920        },
3921    )?;
3922    Ok(activation)
3923}
3924
3925pub fn device_join_status(record: &DeviceJoinJournalRecord) -> DeviceJoinStatus {
3926    match &*record.progress {
3927        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Offered(offer))
3928        | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::OfferReceived(offer)) => {
3929            DeviceJoinStatus::AwaitingAccessRequest {
3930                offer: offer.clone(),
3931            }
3932        }
3933        DeviceJoinRoleProgress::ProviderAdministrator(
3934            ProviderAdminJoinProgress::AccessRequested(request),
3935        )
3936        | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::AccessRequested(request)) => {
3937            DeviceJoinStatus::AwaitingProviderAdmission {
3938                request: request.clone(),
3939            }
3940        }
3941        DeviceJoinRoleProgress::ProviderAdministrator(
3942            ProviderAdminJoinProgress::ApprovalPrepared(approval),
3943        )
3944        | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::ApprovalReceived(approval)) => {
3945            DeviceJoinStatus::AwaitingRegistrationRequest {
3946                approval: approval.clone(),
3947            }
3948        }
3949        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::RegistrationRequested(request))
3950        | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::RegistrationPrepared(request)) => {
3951            DeviceJoinStatus::AwaitingBootstrap {
3952                request: request.clone(),
3953            }
3954        }
3955        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::AttemptActivated(bootstrap))
3956        | DeviceJoinRoleProgress::ProviderAdministrator(
3957            ProviderAdminJoinProgress::AttemptObserved(bootstrap),
3958        )
3959        | DeviceJoinRoleProgress::ProviderAdministrator(
3960            ProviderAdminJoinProgress::ChallengeCreateIntent(bootstrap),
3961        ) => DeviceJoinStatus::AwaitingChallengePublication {
3962            bootstrap: bootstrap.clone(),
3963        },
3964        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::ProviderReady(bootstrap))
3965        | DeviceJoinRoleProgress::ProviderAdministrator(
3966            ProviderAdminJoinProgress::ProviderReady(bootstrap),
3967        )
3968        | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::ProviderReady(bootstrap))
3969        | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::RegistrationCreateIntent(bootstrap)) => {
3970            DeviceJoinStatus::AwaitingReadiness {
3971                bootstrap: bootstrap.clone(),
3972            }
3973        }
3974        DeviceJoinRoleProgress::ProviderAdministrator(
3975            ProviderAdminJoinProgress::ResponseObserved(readiness),
3976        )
3977        | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::Ready(readiness)) => {
3978            DeviceJoinStatus::AwaitingProviderCompletion {
3979                readiness: readiness.clone(),
3980            }
3981        }
3982        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::AdmissionCompleted(completion))
3983        | DeviceJoinRoleProgress::ProviderAdministrator(ProviderAdminJoinProgress::Completed(
3984            completion,
3985        )) => DeviceJoinStatus::AwaitingActivation {
3986            completion: completion.clone(),
3987        },
3988        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::ActivationPrepared(activation))
3989        | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::ActivationObserved(activation)) => {
3990            DeviceJoinStatus::AwaitingCompletion {
3991                activation: activation.clone(),
3992            }
3993        }
3994        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Activated(store))
3995        | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::Activated(store)) => {
3996            DeviceJoinStatus::Activated {
3997                store: store.clone(),
3998            }
3999        }
4000        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Abandoned(abandonment))
4001        | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::Abandoned(abandonment)) => {
4002            DeviceJoinStatus::Abandoned {
4003                abandonment: abandonment.clone(),
4004            }
4005        }
4006        DeviceJoinRoleProgress::ProviderAdministrator(
4007            ProviderAdminJoinProgress::AccessGrantPrepared { request, grant, .. },
4008        ) => DeviceJoinStatus::ProviderAccessGrantCreatePending {
4009            request: request.clone(),
4010            grant: grant.clone(),
4011        },
4012        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::AbandonmentCreateIntent {
4013            abandonment,
4014            ..
4015        }) => DeviceJoinStatus::AbandonmentCreatePending {
4016            abandonment: abandonment.clone(),
4017        },
4018        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::CancellationCreateIntent {
4019            cancellation,
4020            ..
4021        }) => DeviceJoinStatus::CancellationCreatePending {
4022            cancellation: cancellation.clone(),
4023        },
4024        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Cancelled(cancellation)) => {
4025            DeviceJoinStatus::CleanupPending {
4026                cancellation: cancellation.clone(),
4027                progress: DeviceJoinCleanupProgress::AwaitingBoth,
4028            }
4029        }
4030        DeviceJoinRoleProgress::ProviderAdministrator(
4031            ProviderAdminJoinProgress::CleanupIntent { cancellation, .. },
4032        ) => DeviceJoinStatus::ProviderClosurePending {
4033            cancellation: cancellation.clone(),
4034            producer: DeviceJoinProducer::ProviderAdministrator,
4035        },
4036        DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::CleanupIntent {
4037            cancellation, ..
4038        }) => DeviceJoinStatus::ProviderClosurePending {
4039            cancellation: cancellation.clone(),
4040            producer: DeviceJoinProducer::Joiner,
4041        },
4042        DeviceJoinRoleProgress::ProviderAdministrator(ProviderAdminJoinProgress::Cancelled(
4043            closure,
4044        )) => DeviceJoinStatus::ProviderClosed {
4045            terminal: ProviderAdminJoinTerminal::Cancelled(closure.clone()),
4046        },
4047        DeviceJoinRoleProgress::ProviderAdministrator(ProviderAdminJoinProgress::WriteRevoked(
4048            revocation,
4049        )) => DeviceJoinStatus::ProviderClosed {
4050            terminal: ProviderAdminJoinTerminal::WriteRevoked(revocation.clone()),
4051        },
4052        DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::Cancelled(closure)) => {
4053            DeviceJoinStatus::JoinerClosed {
4054                terminal: JoinerJoinTerminal::Cancelled(closure.clone()),
4055            }
4056        }
4057        DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::WriteRevoked(revocation)) => {
4058            DeviceJoinStatus::JoinerClosed {
4059                terminal: JoinerJoinTerminal::WriteRevoked(revocation.clone()),
4060            }
4061        }
4062        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::CleanupReceiptCreateIntent {
4063            cancellation,
4064            receipt,
4065            ..
4066        }) => DeviceJoinStatus::CleanupReceiptCreatePending {
4067            cancellation: cancellation.clone(),
4068            receipt: receipt.clone(),
4069        },
4070        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::CleanupReceipt(receipt)) => {
4071            DeviceJoinStatus::AwaitingCleanupActivation {
4072                receipt: receipt.clone(),
4073            }
4074        }
4075        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::CleanupActivated(activation))
4076        | DeviceJoinRoleProgress::Owner(OwnerJoinProgress::CancelledComplete(activation))
4077        | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::CleanupActivated(activation))
4078        | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::CancelledComplete(activation)) => {
4079            DeviceJoinStatus::CleanupActivated {
4080                activation: activation.clone(),
4081            }
4082        }
4083        _ => DeviceJoinStatus::OperationInProgress {
4084            attempt_id: record.attempt_id,
4085        },
4086    }
4087}
4088
4089pub async fn load_store_device_join_status(
4090    db: &Database,
4091    attempt_id: DeviceJoinAttemptId,
4092    role: DeviceJoinRole,
4093) -> Result<Option<DeviceJoinStatus>, DeviceJoinError> {
4094    load_store_journal(db, attempt_id, role)
4095        .await
4096        .map(|record| record.as_ref().map(device_join_status))
4097}
4098
4099pub fn load_pending_device_join_status(
4100    pending: &DeviceJoinJournalDatabase,
4101    attempt_id: DeviceJoinAttemptId,
4102) -> Result<Option<DeviceJoinStatus>, DeviceJoinError> {
4103    pending
4104        .load(attempt_id, DeviceJoinRole::Joiner)
4105        .map(|record| record.as_ref().map(device_join_status))
4106}
4107
4108pub async fn observe_device_join_abandonment(
4109    pending: &DeviceJoinJournalDatabase,
4110    storage: &dyn SyncStorage,
4111    root: &StoreRootRef,
4112    abandonment: DeviceJoinAbandonment,
4113) -> Result<DeviceJoinAbandonment, DeviceJoinError> {
4114    let current = pending
4115        .load(abandonment.abandonment.attempt_id, DeviceJoinRole::Joiner)?
4116        .ok_or(DeviceJoinError::JournalConflict)?;
4117    if let DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::Abandoned(existing)) =
4118        &*current.progress
4119    {
4120        if existing == &abandonment {
4121            return Ok(existing.clone());
4122        }
4123        return Err(DeviceJoinError::JournalConflict);
4124    }
4125    let context = crate::sync::storage::ProtocolObjectContext::store(
4126        root.store_root_hash,
4127        crate::sync::storage::ProtocolObjectDomain::DeviceJoinAbandonment,
4128    );
4129    let prefix = crate::sync::store_commit::device_join_abandonment_semantic_prefix(
4130        abandonment.abandonment.attempt_id,
4131    );
4132    let bytes = storage
4133        .read_protocol_object(&context, &abandonment.abandonment.object, &prefix)
4134        .await?;
4135    let object: DeviceJoinAbandonmentObject = serde_json::from_slice(&bytes)?;
4136    let owner = crate::sync::store_objects::load_registration_ref(
4137        storage,
4138        root,
4139        &object.owner_registration,
4140    )
4141    .await?
4142    .value;
4143    abandonment.abandonment.verify(&object, &owner)?;
4144    let (activation, author) = crate::sync::store_pull::load_commit_with_author(
4145        storage,
4146        root,
4147        &abandonment.abandonment_activation,
4148    )
4149    .await?;
4150    if author != owner
4151        || activation
4152            .device_join_abandonments()
4153            .binary_search(&abandonment.abandonment)
4154            .is_err()
4155    {
4156        return Err(DeviceJoinError::AttemptMismatch);
4157    }
4158    match &*current.progress {
4159        DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::AccessRequested(_))
4160        | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::ApprovalReceived(_)) => {}
4161        _ => return Err(DeviceJoinError::JournalConflict),
4162    }
4163    pending.advance(
4164        &current,
4165        DeviceJoinJournalRecord {
4166            attempt_id: abandonment.abandonment.attempt_id,
4167            progress: Box::new(DeviceJoinRoleProgress::Joiner(
4168                JoinerJoinProgress::Abandoned(abandonment.clone()),
4169            )),
4170        },
4171    )?;
4172    Ok(abandonment)
4173}
4174
4175#[allow(clippy::too_many_arguments)]
4176pub async fn finalize_device_join(
4177    db: &Database,
4178    storage: &dyn SyncStorage,
4179    coordination: Option<&dyn CoordinationStorage>,
4180    authorization: &DeviceJoinAuthorization,
4181    identity_signer: &UserKeypair,
4182    completion: DeviceProviderAdmissionCompletion,
4183) -> Result<DeviceJoinActivation, DeviceJoinError> {
4184    require_authorization_policy(db, storage, authorization).await?;
4185    let attempt_ref = completion.readiness.proof.attempt.clone();
4186    let attempt_id = attempt_ref.attempt_id;
4187    let current = load_store_journal(db, attempt_id, DeviceJoinRole::Owner)
4188        .await?
4189        .ok_or(DeviceJoinError::JournalConflict)?;
4190    if let DeviceJoinRoleProgress::Owner(OwnerJoinProgress::ActivationPrepared(existing)) =
4191        &*current.progress
4192    {
4193        return Ok(existing.clone());
4194    }
4195    let provisional = match &*current.progress {
4196        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::AttemptActivated(bootstrap)) => {
4197            bootstrap.clone()
4198        }
4199        _ => return Err(DeviceJoinError::JournalConflict),
4200    };
4201    let offer = &provisional.request.approval.request.offer;
4202    let owner = db
4203        .activated_store_device_registration(offer.owner_registration.clone())
4204        .await
4205        .map_err(database_error)?;
4206    let owner_signer = owner.device_signer(identity_signer)?;
4207    let attempt = crate::sync::store_objects::load_device_join_attempt_ref(
4208        storage,
4209        &offer.store_root,
4210        &attempt_ref,
4211        &owner,
4212    )
4213    .await?
4214    .value;
4215    let registration = crate::sync::store_objects::load_registration_ref(
4216        storage,
4217        &offer.store_root,
4218        &completion.readiness.proof.registration,
4219    )
4220    .await?
4221    .value;
4222    let ack = crate::sync::store_objects::load_store_ack_ref(
4223        storage,
4224        &offer.store_root,
4225        &completion.readiness.proof.initial_ack,
4226        &registration,
4227    )
4228    .await?
4229    .value;
4230    completion.readiness.proof.verify(
4231        &attempt_ref,
4232        &attempt,
4233        &registration,
4234        &completion.readiness.proof.initial_ack,
4235        &ack,
4236    )?;
4237    match (&attempt.provider_approval.admission, &completion.admission) {
4238        (
4239            DeviceProviderAdmissionChallenge::SamePrincipal,
4240            DeviceProviderAdmission::SamePrincipal,
4241        ) => {}
4242        (
4243            DeviceProviderAdmissionChallenge::CrossPrincipal(challenge),
4244            DeviceProviderAdmission::CrossPrincipal(receipt),
4245        ) => {
4246            let administrator = db
4247                .activated_store_device_registration(offer.provider_admin.administrator.clone())
4248                .await
4249                .map_err(database_error)?;
4250            let response_slot = match &attempt.provider_response {
4251                DeviceProviderResponseReservation::CrossPrincipal { response_slot } => {
4252                    response_slot.clone()
4253                }
4254                DeviceProviderResponseReservation::SamePrincipal => {
4255                    return Err(DeviceJoinError::AttemptMismatch);
4256                }
4257            };
4258            let context = crate::sync::provider::CrossPrincipalResponseContext {
4259                challenge: cross_challenge_context(&attempt.provider_approval.request),
4260                expected_registration_hash: attempt.expected_registration.registration_hash(),
4261                response_slot,
4262            };
4263            receipt
4264                .verify(
4265                    &context,
4266                    &offer.provider,
4267                    &administrator.device_signing_pubkey,
4268                    &offer.member_pubkey,
4269                )
4270                .map_err(provider_error)?;
4271            if &receipt.transcript.challenge != challenge {
4272                return Err(DeviceJoinError::AttemptMismatch);
4273            }
4274        }
4275        _ => return Err(DeviceJoinError::AttemptMismatch),
4276    }
4277    let outcome = crate::sync::store_commit::DeviceJoinOutcome::signed(
4278        attempt_ref.clone(),
4279        crate::sync::store_commit::DeviceJoinOutcomeBody::Activated {
4280            readiness: completion.readiness.proof.clone(),
4281        },
4282        offer.owner_registration.clone(),
4283        offer.owner_grant.clone(),
4284        &owner,
4285        &owner_signer,
4286    )?;
4287    let context = crate::sync::storage::ProtocolObjectContext::store(
4288        offer.store_root.store_root_hash,
4289        crate::sync::storage::ProtocolObjectDomain::DeviceJoinOutcome,
4290    );
4291    let prefix = crate::sync::store_commit::device_join_outcome_semantic_prefix(attempt_id);
4292    let prepared = storage.prepare_protocol_object(
4293        &context,
4294        attempt.outcome_slot.clone(),
4295        &prefix,
4296        outcome.to_bytes(),
4297    )?;
4298    storage.create_protocol_object(&prepared).await?;
4299    let opened = storage
4300        .read_protocol_object(&context, prepared.reference(), &prefix)
4301        .await?;
4302    if opened != outcome.to_bytes() {
4303        return Err(DeviceJoinError::AttemptMismatch);
4304    }
4305    let outcome_ref = DeviceJoinOutcomeRef::Activated {
4306        attempt: attempt_ref,
4307        outcome_hash: outcome.outcome_hash(),
4308        object: prepared.reference().clone(),
4309    };
4310    let activated_registration = crate::sync::store_outbound::DeviceJoinRegistrationActivation {
4311        reference: crate::sync::store_commit::ActivatedStoreDeviceRegistrationRef {
4312            registration: completion.readiness.proof.registration.clone(),
4313            authority: crate::sync::store_commit::StoreDeviceRegistrationActivationRef::Join {
4314                attempt_id,
4315                outcome: outcome_ref.clone(),
4316            },
4317        },
4318        registration: attempt.expected_registration.clone(),
4319        authority: crate::sync::store_commit::StoreDeviceRegistrationActivation::Join {
4320            attempt_id,
4321            outcome: outcome_ref.clone(),
4322        },
4323    };
4324    let local_device_id = db
4325        .get_protocol_state(crate::database::LOCAL_DEVICE_ID_STATE_KEY)
4326        .await
4327        .map_err(database_error)?
4328        .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
4329    let plan = crate::sync::store_outbound::prepare_device_join_commit(
4330        db,
4331        storage,
4332        coordination,
4333        &local_device_id,
4334        identity_signer,
4335        authorization.merge_chain(),
4336    )
4337    .await?;
4338    let activation_ref = crate::sync::store_outbound::activate_device_join_commit(
4339        db,
4340        storage,
4341        coordination,
4342        plan,
4343        crate::sync::store_outbound::DeviceJoinStoreBatch::Outcome {
4344            outcome: outcome_ref.clone(),
4345            registration: Some(activated_registration),
4346        },
4347    )
4348    .await?;
4349    let activation = DeviceJoinActivation {
4350        outcome: outcome_ref,
4351        outcome_activation: activation_ref,
4352    };
4353    let provider_ready = ProviderReadyDeviceBootstrap {
4354        bootstrap: Box::new(provisional),
4355        challenge_publication: match &attempt.provider_approval.admission {
4356            DeviceProviderAdmissionChallenge::SamePrincipal => {
4357                DeviceProviderChallengePublication::SamePrincipal
4358            }
4359            DeviceProviderAdmissionChallenge::CrossPrincipal(challenge) => {
4360                DeviceProviderChallengePublication::CrossPrincipal {
4361                    challenge: challenge.clone(),
4362                }
4363            }
4364        },
4365    };
4366    let ready_record = DeviceJoinJournalRecord {
4367        attempt_id,
4368        progress: Box::new(DeviceJoinRoleProgress::Owner(
4369            OwnerJoinProgress::ProviderReady(provider_ready),
4370        )),
4371    };
4372    advance_store_journal(db, &current, ready_record.clone()).await?;
4373    let completion_record = DeviceJoinJournalRecord {
4374        attempt_id,
4375        progress: Box::new(DeviceJoinRoleProgress::Owner(
4376            OwnerJoinProgress::AdmissionCompleted(completion),
4377        )),
4378    };
4379    advance_store_journal(db, &ready_record, completion_record.clone()).await?;
4380    advance_store_journal(
4381        db,
4382        &completion_record,
4383        DeviceJoinJournalRecord {
4384            attempt_id,
4385            progress: Box::new(DeviceJoinRoleProgress::Owner(
4386                OwnerJoinProgress::ActivationPrepared(activation.clone()),
4387            )),
4388        },
4389    )
4390    .await?;
4391    Ok(activation)
4392}
4393
4394pub async fn materialize_device_join_activation(
4395    db: &Database,
4396    storage: &dyn SyncStorage,
4397    activation: DeviceJoinActivation,
4398) -> Result<JoinedStore, DeviceJoinError> {
4399    if !matches!(activation.outcome, DeviceJoinOutcomeRef::Activated { .. }) {
4400        return Err(DeviceJoinError::AttemptMismatch);
4401    }
4402    let root = db
4403        .local_store_root_ref()
4404        .await
4405        .map_err(database_error)?
4406        .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
4407    let attempt_ref = activation.outcome.attempt().clone();
4408    let attempt_context = crate::sync::storage::ProtocolObjectContext::store(
4409        root.store_root_hash,
4410        crate::sync::storage::ProtocolObjectDomain::DeviceJoinAttempt,
4411    );
4412    let attempt_bytes = storage
4413        .read_protocol_object(
4414            &attempt_context,
4415            &attempt_ref.object,
4416            &crate::sync::store_commit::device_join_attempt_semantic_prefix(attempt_ref.attempt_id),
4417        )
4418        .await?;
4419    let unverified_attempt: DeviceJoinAttempt = serde_json::from_slice(&attempt_bytes)?;
4420    let owner = db
4421        .activated_store_device_registration(unverified_attempt.owner_registration.clone())
4422        .await
4423        .map_err(database_error)?;
4424    let attempt = crate::sync::store_objects::load_device_join_attempt_ref(
4425        storage,
4426        &root,
4427        &attempt_ref,
4428        &owner,
4429    )
4430    .await?
4431    .value;
4432    let authorization = Box::pin(crate::sync::store_pull::load_device_join_authorization(
4433        storage,
4434        &root,
4435        &attempt.membership,
4436    ))
4437    .await?;
4438    Box::pin(crate::sync::store_pull::materialize_device_join_activation(
4439        db,
4440        storage,
4441        &root,
4442        &activation.outcome_activation,
4443        &activation.outcome,
4444        &authorization,
4445    ))
4446    .await?;
4447    let outcome = crate::sync::store_objects::load_device_join_outcome_ref(
4448        storage,
4449        &root,
4450        &activation.outcome,
4451        &owner,
4452    )
4453    .await?
4454    .value;
4455    let crate::sync::store_commit::DeviceJoinOutcomeBody::Activated { readiness } = outcome.body
4456    else {
4457        return Err(DeviceJoinError::AttemptMismatch);
4458    };
4459    let local = db
4460        .latest_local_store_device_registration()
4461        .await
4462        .map_err(database_error)?
4463        .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
4464    if !local.is_activated()
4465        || local.registration_hash != readiness.registration.registration_hash
4466        || local.device_id != readiness.registration.device_id
4467        || attempt.expected_registration.to_bytes() != local.registration_bytes
4468    {
4469        return Err(DeviceJoinError::ActivationNotMaterialized);
4470    }
4471    let joined = JoinedStore {
4472        store_root: root,
4473        registration: readiness.registration.clone(),
4474        activation,
4475    };
4476    Ok(joined)
4477}
4478
4479pub async fn complete_device_join(
4480    db: &Database,
4481    pending: &DeviceJoinJournalDatabase,
4482    storage: &dyn SyncStorage,
4483    activation: DeviceJoinActivation,
4484) -> Result<JoinedStore, DeviceJoinError> {
4485    let attempt_id = activation.outcome.attempt().attempt_id;
4486    let joined = materialize_device_join_activation(db, storage, activation).await?;
4487    if let Some(record) = load_store_journal(db, attempt_id, DeviceJoinRole::Joiner).await? {
4488        return match &*record.progress {
4489            DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::Activated(existing))
4490                if existing == &joined =>
4491            {
4492                Ok(joined)
4493            }
4494            _ => Err(DeviceJoinError::JournalConflict),
4495        };
4496    }
4497    let current = pending
4498        .load(attempt_id, DeviceJoinRole::Joiner)?
4499        .ok_or(DeviceJoinError::JournalConflict)?;
4500    let DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::Ready(current_readiness)) =
4501        &*current.progress
4502    else {
4503        return Err(DeviceJoinError::JournalConflict);
4504    };
4505    if current_readiness.proof.registration != joined.registration {
4506        return Err(DeviceJoinError::JournalConflict);
4507    }
4508    let activated_record = DeviceJoinJournalRecord {
4509        attempt_id,
4510        progress: Box::new(DeviceJoinRoleProgress::Joiner(
4511            JoinerJoinProgress::Activated(joined.clone()),
4512        )),
4513    };
4514    let store_key = store_journal_key(attempt_id, DeviceJoinRole::Joiner.as_str());
4515    let store_payload = serde_json::to_string(&activated_record)?;
4516    let pending_path = pending.path().to_string_lossy().into_owned();
4517    let pending_attempt = attempt_key(attempt_id);
4518    let expected_pending = serde_json::to_string(&current)?;
4519    db.call(move |connection| {
4520        connection
4521            .execute("ATTACH DATABASE ?1 AS pending_join_source", [&pending_path])
4522            .map_err(crate::database::DbError::from)?;
4523        let tx = connection
4524            .unchecked_transaction()
4525            .map_err(crate::database::DbError::from)?;
4526        let actual: String = tx
4527            .query_row(
4528                "SELECT payload FROM pending_join_source.device_join_journals
4529                 WHERE attempt_id = ?1 AND role = 'joiner'",
4530                [&pending_attempt],
4531                |row| row.get(0),
4532            )
4533            .map_err(crate::database::DbError::from)?;
4534        if actual != expected_pending {
4535            return Err(crate::database::DbError::Message(
4536                "pending join journal changed before activation".to_string(),
4537            ));
4538        }
4539        tx.execute(
4540            "INSERT INTO protocol_state (key, value) VALUES (?1, ?2)
4541             ON CONFLICT(key) DO UPDATE SET value = excluded.value WHERE value = excluded.value",
4542            (&store_key, &store_payload),
4543        )
4544        .map_err(crate::database::DbError::from)?;
4545        tx.execute(
4546            "DELETE FROM pending_join_source.device_join_journals
4547             WHERE attempt_id = ?1 AND role = 'joiner' AND payload = ?2",
4548            (&pending_attempt, &expected_pending),
4549        )
4550        .map_err(crate::database::DbError::from)?;
4551        tx.commit().map_err(crate::database::DbError::from)?;
4552        connection
4553            .execute_batch("DETACH DATABASE pending_join_source")
4554            .map_err(crate::database::DbError::from)
4555    })
4556    .await
4557    .map_err(database_error)?;
4558    Ok(joined)
4559}
4560
4561#[allow(clippy::too_many_arguments)]
4562pub async fn authorize_device_provider_access(
4563    db: &Database,
4564    storage: &dyn SyncStorage,
4565    coordination: Option<&dyn CoordinationStorage>,
4566    administrator_exact: Option<&dyn ExactSlotStorage>,
4567    access_administrator: Option<&dyn DeviceProviderAccessAdministrator>,
4568    authorization: &DeviceJoinAuthorization,
4569    identity_signer: &UserKeypair,
4570    request: DeviceProviderAccessRequest,
4571) -> Result<DeviceProviderAdmissionApproval, DeviceJoinError> {
4572    require_authorization_policy(db, storage, authorization).await?;
4573    let owner = db
4574        .activated_store_device_registration(request.offer.owner_registration.clone())
4575        .await
4576        .map_err(database_error)?;
4577    request.verify(&owner)?;
4578    let provider_admin =
4579        authorization.resolved_provider_admin(&request.offer.provider_admin.grant_id)?;
4580    if provider_admin != *request.offer.provider_admin {
4581        return Err(DeviceJoinError::OfferMismatch);
4582    }
4583    let administrator = db
4584        .activated_store_device_registration(provider_admin.administrator.clone())
4585        .await
4586        .map_err(database_error)?;
4587    let local_device_id = db
4588        .get_protocol_state(crate::database::LOCAL_DEVICE_ID_STATE_KEY)
4589        .await
4590        .map_err(database_error)?
4591        .ok_or(DeviceJoinError::ActiveDeviceRequired)?;
4592    if administrator.device_id.to_string() != local_device_id {
4593        return Err(DeviceJoinError::ProviderAdministratorRequired);
4594    }
4595    let administrator_signer = administrator.device_signer(identity_signer)?;
4596    let initial = DeviceJoinJournalRecord {
4597        attempt_id: request.offer.attempt_id,
4598        progress: Box::new(DeviceJoinRoleProgress::ProviderAdministrator(
4599            ProviderAdminJoinProgress::AccessRequested(request.clone()),
4600        )),
4601    };
4602    let durable = begin_store_journal(db, initial.clone()).await?;
4603    let (grant, prepared, prepared_progress) = match &*durable.progress {
4604        DeviceJoinRoleProgress::ProviderAdministrator(
4605            ProviderAdminJoinProgress::ApprovalPrepared(approval),
4606        ) => return Ok(approval.clone()),
4607        DeviceJoinRoleProgress::ProviderAdministrator(
4608            ProviderAdminJoinProgress::AccessGrantPrepared {
4609                request: durable_request,
4610                grant,
4611                prepared,
4612            },
4613        ) if durable_request == &request => (
4614            grant.clone(),
4615            crate::sync::storage::PreparedExactObject::new(
4616                prepared.object.clone(),
4617                prepared.stored_bytes.clone(),
4618            )?,
4619            durable.clone(),
4620        ),
4621        DeviceJoinRoleProgress::ProviderAdministrator(
4622            ProviderAdminJoinProgress::AccessRequested(durable_request),
4623        ) if durable_request == &request => {
4624            let locator = if provider_admin.provider == request.peer_provider {
4625                provider_admin.access.clone()
4626            } else {
4627                let administrator =
4628                    access_administrator.ok_or(DeviceJoinError::ProviderAdministratorRequired)?;
4629                administrator
4630                    .grant_member_access(
4631                        &request.offer.member_pubkey,
4632                        authorization.current_member_provider_email(&request.offer.member_pubkey),
4633                        &request.peer_provider,
4634                    )
4635                    .await?
4636            };
4637            let grant_id = ProviderAccessGrantId::from_random_bytes(
4638                *ObjectHash::digest(db.new_write_id().as_str().as_bytes()).as_bytes(),
4639            );
4640            let grant = StoreMemberProviderAccessGrant::signed(
4641                grant_id,
4642                request.offer.member_pubkey.clone(),
4643                request.peer_provider.clone(),
4644                locator,
4645                provider_admin.grant_id.clone(),
4646                provider_admin.administrator.clone(),
4647                &request.offer.provider,
4648                &administrator,
4649                &administrator_signer,
4650            )
4651            .map_err(provider_error)?;
4652            let context = crate::sync::storage::ProtocolObjectContext::store(
4653                request.offer.store_root.store_root_hash,
4654                crate::sync::storage::ProtocolObjectDomain::ProviderAccessGrant,
4655            );
4656            let prefix =
4657                crate::sync::store_commit::provider_access_grant_semantic_prefix(&grant.grant_id);
4658            let slot = storage
4659                .allocate_protocol_slot(&context, &prefix, ".json")
4660                .await?;
4661            let prepared =
4662                storage.prepare_protocol_object(&context, slot, &prefix, grant.to_bytes())?;
4663            let prepared_progress = DeviceJoinJournalRecord {
4664                attempt_id: request.offer.attempt_id,
4665                progress: Box::new(DeviceJoinRoleProgress::ProviderAdministrator(
4666                    ProviderAdminJoinProgress::AccessGrantPrepared {
4667                        request: request.clone(),
4668                        grant: grant.clone(),
4669                        prepared: PreparedDeviceJoinObject::from_prepared(&prepared),
4670                    },
4671                )),
4672            };
4673            advance_store_journal(db, &initial, prepared_progress.clone()).await?;
4674            (grant, prepared, prepared_progress)
4675        }
4676        _ => return Err(DeviceJoinError::JournalConflict),
4677    };
4678    let context = crate::sync::storage::ProtocolObjectContext::store(
4679        request.offer.store_root.store_root_hash,
4680        crate::sync::storage::ProtocolObjectDomain::ProviderAccessGrant,
4681    );
4682    let prefix = crate::sync::store_commit::provider_access_grant_semantic_prefix(&grant.grant_id);
4683    storage.create_protocol_object(&prepared).await?;
4684    let opened = storage
4685        .read_protocol_object(&context, prepared.reference(), &prefix)
4686        .await?;
4687    if opened != grant.to_bytes() {
4688        return Err(DeviceJoinError::Provider(
4689            "provider access grant exact readback differs from its signed bytes".to_string(),
4690        ));
4691    }
4692    let grant_ref =
4693        StoreMemberProviderAccessGrantRef::from_grant(&grant, prepared.reference().clone());
4694    let plan = crate::sync::store_outbound::prepare_device_join_commit(
4695        db,
4696        storage,
4697        coordination,
4698        &local_device_id,
4699        identity_signer,
4700        authorization.merge_chain(),
4701    )
4702    .await?;
4703    let activation = crate::sync::store_outbound::activate_device_join_commit(
4704        db,
4705        storage,
4706        coordination,
4707        plan,
4708        crate::sync::store_outbound::DeviceJoinStoreBatch::ProviderAccessGrant(grant_ref.clone()),
4709    )
4710    .await?;
4711    let admission = if provider_admin.provider == request.peer_provider {
4712        DeviceProviderAdmissionChallenge::SamePrincipal
4713    } else {
4714        let exact = administrator_exact.ok_or(DeviceJoinError::ProviderAdministratorRequired)?;
4715        let challenge_context = crate::sync::provider::CrossPrincipalChallengeContext {
4716            root: request.offer.store_root.clone(),
4717            attempt_id: request.offer.attempt_id,
4718            access_request_hash: request.request_hash(),
4719            provider_admin_grant: provider_admin.grant_id.clone(),
4720            owner_registration: request.offer.owner_registration.clone(),
4721            member_pubkey: request.offer.member_pubkey.clone(),
4722            administrator_binding: provider_admin.provider.clone(),
4723            peer_binding: request.peer_provider.clone(),
4724        };
4725        let probe_id = crate::sync::provider::ProviderProbeId::from_bytes(
4726            *ObjectHash::digest(db.new_write_id().as_str().as_bytes()).as_bytes(),
4727        );
4728        DeviceProviderAdmissionChallenge::CrossPrincipal(
4729            crate::sync::provider::prepare_cross_principal_challenge(
4730                exact,
4731                db,
4732                probe_id,
4733                &request.offer.provider,
4734                &challenge_context,
4735                &administrator_signer,
4736            )
4737            .await
4738            .map_err(provider_error)?,
4739        )
4740    };
4741    let approval = DeviceProviderAdmissionApproval::signed(
4742        request,
4743        ActivatedStoreMemberProviderAccessGrant {
4744            grant,
4745            grant_ref,
4746            activation,
4747        },
4748        admission,
4749        &administrator,
4750        &administrator_signer,
4751    )?;
4752    advance_store_journal(
4753        db,
4754        &prepared_progress,
4755        DeviceJoinJournalRecord {
4756            attempt_id: approval.request.offer.attempt_id,
4757            progress: Box::new(DeviceJoinRoleProgress::ProviderAdministrator(
4758                ProviderAdminJoinProgress::ApprovalPrepared(approval.clone()),
4759            )),
4760        },
4761    )
4762    .await?;
4763    Ok(approval)
4764}
4765
4766async fn begin_store_journal(
4767    db: &Database,
4768    record: DeviceJoinJournalRecord,
4769) -> Result<DeviceJoinJournalRecord, DeviceJoinError> {
4770    validate_initial_progress(&record.progress)?;
4771    let key = store_journal_key(record.attempt_id, record.progress.role_name());
4772    let value = serde_json::to_string(&record)?;
4773    db.call(move |connection| {
4774        connection
4775            .execute(
4776                "INSERT OR IGNORE INTO protocol_state (key, value) VALUES (?1, ?2)",
4777                (&key, &value),
4778            )
4779            .map_err(crate::database::DbError::from)?;
4780        let actual = connection
4781            .query_row(
4782                "SELECT value FROM protocol_state WHERE key = ?1",
4783                [&key],
4784                |row| row.get::<_, String>(0),
4785            )
4786            .map_err(crate::database::DbError::from)?;
4787        serde_json::from_str(&actual)
4788            .map_err(|error| crate::database::DbError::Message(error.to_string()))
4789    })
4790    .await
4791    .map_err(database_error)
4792}
4793
4794async fn load_store_journal(
4795    db: &Database,
4796    attempt_id: DeviceJoinAttemptId,
4797    role: DeviceJoinRole,
4798) -> Result<Option<DeviceJoinJournalRecord>, DeviceJoinError> {
4799    let key = store_journal_key(attempt_id, role.as_str());
4800    let value = db.get_protocol_state(&key).await.map_err(database_error)?;
4801    value
4802        .map(|value| serde_json::from_str(&value).map_err(DeviceJoinError::from))
4803        .transpose()
4804}
4805
4806async fn advance_store_journal(
4807    db: &Database,
4808    previous: &DeviceJoinJournalRecord,
4809    next: DeviceJoinJournalRecord,
4810) -> Result<(), DeviceJoinError> {
4811    if previous.attempt_id != next.attempt_id {
4812        return Err(DeviceJoinError::JournalConflict);
4813    }
4814    previous.progress.validate_transition(&next.progress)?;
4815    let key = store_journal_key(previous.attempt_id, previous.progress.role_name());
4816    let previous = serde_json::to_string(previous)?;
4817    let next = serde_json::to_string(&next)?;
4818    let changed = db
4819        .call(move |connection| {
4820            connection
4821                .execute(
4822                    "UPDATE protocol_state SET value = ?1 WHERE key = ?2 AND value = ?3",
4823                    (&next, &key, &previous),
4824                )
4825                .map_err(crate::database::DbError::from)
4826        })
4827        .await
4828        .map_err(database_error)?;
4829    if changed == 1 {
4830        Ok(())
4831    } else {
4832        Err(DeviceJoinError::JournalConflict)
4833    }
4834}
4835
4836fn store_journal_key(attempt_id: DeviceJoinAttemptId, role: &str) -> String {
4837    format!("device_join/{}/{role}", attempt_key(attempt_id))
4838}
4839
4840fn database_error(error: crate::database::DbError) -> DeviceJoinError {
4841    DeviceJoinError::Store(error.into_message())
4842}
4843
4844fn provider_error(error: impl std::fmt::Display) -> DeviceJoinError {
4845    DeviceJoinError::Provider(error.to_string())
4846}
4847
4848pub fn validate_member_for_join(
4849    member_pubkey: &str,
4850    members: &[(String, MemberRole)],
4851) -> Result<(), DeviceJoinError> {
4852    if members
4853        .iter()
4854        .any(|(pubkey, role)| pubkey == member_pubkey && role.can_write())
4855    {
4856        Ok(())
4857    } else {
4858        Err(DeviceJoinError::MemberNotEligible)
4859    }
4860}
4861
4862pub fn canonical_cleanup_slots(
4863    attempt: &DeviceJoinAttempt,
4864) -> Result<Vec<ObjectSlot>, DeviceJoinError> {
4865    let mut slots = vec![
4866        attempt.registration_slot.clone(),
4867        attempt
4868            .expected_registration
4869            .acknowledgements
4870            .first_slot()
4871            .clone(),
4872    ];
4873    match (
4874        &attempt.provider_approval.admission,
4875        &attempt.provider_response,
4876    ) {
4877        (
4878            DeviceProviderAdmissionChallenge::SamePrincipal,
4879            DeviceProviderResponseReservation::SamePrincipal,
4880        ) => {}
4881        (
4882            DeviceProviderAdmissionChallenge::CrossPrincipal(challenge),
4883            DeviceProviderResponseReservation::CrossPrincipal { response_slot },
4884        ) => {
4885            slots.push(challenge.administrator_object.slot.clone());
4886            slots.push(response_slot.clone());
4887        }
4888        _ => return Err(DeviceJoinError::AttemptMismatch),
4889    }
4890    slots.sort();
4891    require_distinct_slots(&slots)?;
4892    Ok(slots)
4893}
4894
4895fn require_cancelled_outcome(outcome: &DeviceJoinOutcomeRef) -> Result<(), DeviceJoinError> {
4896    if matches!(outcome, DeviceJoinOutcomeRef::Cancelled { .. }) {
4897        Ok(())
4898    } else {
4899        Err(DeviceJoinError::AttemptMismatch)
4900    }
4901}
4902
4903fn validate_terminals(
4904    cancellation: &DeviceJoinOutcomeRef,
4905    administrator: &ProviderAdminJoinTerminal,
4906    joiner: &JoinerJoinTerminal,
4907) -> Result<(), DeviceJoinError> {
4908    let administrator_cancellation = match administrator {
4909        ProviderAdminJoinTerminal::Completed(completion) => {
4910            if completion.readiness.proof.attempt != *cancellation.attempt() {
4911                return Err(DeviceJoinError::AttemptMismatch);
4912            }
4913            None
4914        }
4915        ProviderAdminJoinTerminal::Cancelled(closure) => Some(&closure.cancellation),
4916        ProviderAdminJoinTerminal::WriteRevoked(revocation) => Some(&revocation.cancellation),
4917    };
4918    let joiner_cancellation = match joiner {
4919        JoinerJoinTerminal::Ready(readiness) => {
4920            if readiness.proof.attempt != *cancellation.attempt() {
4921                return Err(DeviceJoinError::AttemptMismatch);
4922            }
4923            None
4924        }
4925        JoinerJoinTerminal::Cancelled(closure) => Some(&closure.cancellation),
4926        JoinerJoinTerminal::WriteRevoked(revocation) => Some(&revocation.cancellation),
4927    };
4928    if administrator_cancellation.is_some_and(|value| value != cancellation)
4929        || joiner_cancellation.is_some_and(|value| value != cancellation)
4930    {
4931        return Err(DeviceJoinError::AttemptMismatch);
4932    }
4933    Ok(())
4934}
4935
4936async fn verify_cleanup_terminals(
4937    db: &Database,
4938    administrator: &ProviderAdminJoinTerminal,
4939    joiner: &JoinerJoinTerminal,
4940) -> Result<(), DeviceJoinError> {
4941    match administrator {
4942        ProviderAdminJoinTerminal::Completed(_) => {}
4943        ProviderAdminJoinTerminal::Cancelled(closure) => {
4944            let registration = db
4945                .activated_store_device_registration(closure.administrator_registration.clone())
4946                .await
4947                .map_err(database_error)?;
4948            closure.verify(&registration)?;
4949        }
4950        ProviderAdminJoinTerminal::WriteRevoked(revocation) => {
4951            let registration = db
4952                .activated_store_device_registration(revocation.executor.clone())
4953                .await
4954                .map_err(database_error)?;
4955            revocation.verify(&registration)?;
4956        }
4957    }
4958    match joiner {
4959        JoinerJoinTerminal::Ready(_) => {}
4960        JoinerJoinTerminal::Cancelled(closure) => closure.verify()?,
4961        JoinerJoinTerminal::WriteRevoked(revocation) => {
4962            let registration = db
4963                .activated_store_device_registration(revocation.executor.clone())
4964                .await
4965                .map_err(database_error)?;
4966            revocation.verify(&registration)?;
4967        }
4968    }
4969    Ok(())
4970}
4971
4972async fn ensure_exact_slot_absent(
4973    storage: &dyn ExactSlotStorage,
4974    slot: &ObjectSlot,
4975) -> Result<(), DeviceJoinError> {
4976    match storage.read_at(slot).await {
4977        Err(crate::storage::cloud::CloudHomeError::NotFound(_)) => Ok(()),
4978        Ok(_) => {
4979            storage
4980                .delete_at(slot)
4981                .await
4982                .map_err(|error| DeviceJoinError::Provider(error.to_string()))?;
4983            match storage.read_at(slot).await {
4984                Err(crate::storage::cloud::CloudHomeError::NotFound(_)) => Ok(()),
4985                Ok(_) => Err(DeviceJoinError::CleanupMismatch),
4986                Err(error) => Err(DeviceJoinError::Provider(error.to_string())),
4987            }
4988        }
4989        Err(error) => Err(DeviceJoinError::Provider(error.to_string())),
4990    }
4991}
4992
4993async fn observe_exact_slot(
4994    storage: &dyn ExactSlotStorage,
4995    slot: &ObjectSlot,
4996) -> Result<SlotDisposition, DeviceJoinError> {
4997    match storage.read_at(slot).await {
4998        Err(crate::storage::cloud::CloudHomeError::NotFound(_)) => {
4999            Ok(SlotDisposition::NeverCreated)
5000        }
5001        Ok(bytes) => Ok(SlotDisposition::Created(ExactObjectRef::new(
5002            slot.clone(),
5003            bytes.len() as u64,
5004            ObjectHash::digest(&bytes),
5005        ))),
5006        Err(error) => Err(DeviceJoinError::Provider(error.to_string())),
5007    }
5008}
5009
5010fn withdrawal_matches_locator(
5011    withdrawal: &ProviderAccessWithdrawal,
5012    locator: &crate::sync::provider::ProviderAccessLocator,
5013) -> bool {
5014    match (withdrawal, locator) {
5015        (
5016            ProviderAccessWithdrawal::Direct {
5017                locator: withdrawn,
5018                verified_absent: true,
5019            },
5020            expected,
5021        ) => withdrawn == expected,
5022        (
5023            ProviderAccessWithdrawal::S3CredentialRotation {
5024                retired_generation,
5025                active_generation,
5026                retired_credential_verified_rejected: true,
5027            },
5028            crate::sync::provider::ProviderAccessLocator::S3SharedCredentialGeneration {
5029                generation,
5030                ..
5031            },
5032        ) => retired_generation == generation && *active_generation == generation.saturating_add(1),
5033        _ => false,
5034    }
5035}
5036
5037fn owner_adjacent(previous: &OwnerJoinProgress, next: &OwnerJoinProgress) -> bool {
5038    matches!(
5039        (previous, next),
5040        (
5041            OwnerJoinProgress::Offered(_),
5042            OwnerJoinProgress::RegistrationRequested(_)
5043        ) | (
5044            OwnerJoinProgress::Offered(_),
5045            OwnerJoinProgress::AbandonmentCreateIntent { .. }
5046        ) | (
5047            OwnerJoinProgress::RegistrationRequested(_),
5048            OwnerJoinProgress::AttemptActivated(_)
5049        ) | (
5050            OwnerJoinProgress::RegistrationRequested(_),
5051            OwnerJoinProgress::AbandonmentCreateIntent { .. }
5052        ) | (
5053            OwnerJoinProgress::AbandonmentCreateIntent { .. },
5054            OwnerJoinProgress::Abandoned(_)
5055        ) | (
5056            OwnerJoinProgress::AttemptActivated(_),
5057            OwnerJoinProgress::ProviderReady(_)
5058        ) | (
5059            OwnerJoinProgress::AttemptActivated(_),
5060            OwnerJoinProgress::CancellationCreateIntent { .. }
5061        ) | (
5062            OwnerJoinProgress::ProviderReady(_),
5063            OwnerJoinProgress::AdmissionCompleted(_)
5064        ) | (
5065            OwnerJoinProgress::ProviderReady(_),
5066            OwnerJoinProgress::CancellationCreateIntent { .. }
5067        ) | (
5068            OwnerJoinProgress::AdmissionCompleted(_),
5069            OwnerJoinProgress::ActivationPrepared(_)
5070        ) | (
5071            OwnerJoinProgress::AdmissionCompleted(_),
5072            OwnerJoinProgress::CancellationCreateIntent { .. }
5073        ) | (
5074            OwnerJoinProgress::CancellationCreateIntent { .. },
5075            OwnerJoinProgress::Cancelled(_)
5076        ) | (
5077            OwnerJoinProgress::ActivationPrepared(_),
5078            OwnerJoinProgress::Activated(_)
5079        ) | (
5080            OwnerJoinProgress::Cancelled(_),
5081            OwnerJoinProgress::CleanupReceiptCreateIntent { .. }
5082        ) | (
5083            OwnerJoinProgress::CleanupReceiptCreateIntent { .. },
5084            OwnerJoinProgress::CleanupReceipt(_)
5085        ) | (
5086            OwnerJoinProgress::CleanupReceipt(_),
5087            OwnerJoinProgress::CleanupActivated(_)
5088        ) | (
5089            OwnerJoinProgress::CleanupActivated(_),
5090            OwnerJoinProgress::CancelledComplete(_)
5091        )
5092    )
5093}
5094
5095fn provider_admin_adjacent(
5096    previous: &ProviderAdminJoinProgress,
5097    next: &ProviderAdminJoinProgress,
5098) -> bool {
5099    matches!(
5100        (previous, next),
5101        (
5102            ProviderAdminJoinProgress::AccessRequested(_),
5103            ProviderAdminJoinProgress::AccessGrantPrepared { .. }
5104        ) | (
5105            ProviderAdminJoinProgress::AccessGrantPrepared { .. },
5106            ProviderAdminJoinProgress::ApprovalPrepared(_)
5107        ) | (
5108            ProviderAdminJoinProgress::ApprovalPrepared(_),
5109            ProviderAdminJoinProgress::AttemptObserved(_)
5110        ) | (
5111            ProviderAdminJoinProgress::ApprovalPrepared(_),
5112            ProviderAdminJoinProgress::CleanupIntent { .. }
5113        ) | (
5114            ProviderAdminJoinProgress::AttemptObserved(_),
5115            ProviderAdminJoinProgress::ChallengeCreateIntent(_)
5116        ) | (
5117            ProviderAdminJoinProgress::AttemptObserved(_),
5118            ProviderAdminJoinProgress::CleanupIntent { .. }
5119        ) | (
5120            ProviderAdminJoinProgress::ChallengeCreateIntent(_),
5121            ProviderAdminJoinProgress::ProviderReady(_)
5122        ) | (
5123            ProviderAdminJoinProgress::ChallengeCreateIntent(_),
5124            ProviderAdminJoinProgress::CleanupIntent { .. }
5125        ) | (
5126            ProviderAdminJoinProgress::ProviderReady(_),
5127            ProviderAdminJoinProgress::ResponseObserved(_)
5128        ) | (
5129            ProviderAdminJoinProgress::ProviderReady(_),
5130            ProviderAdminJoinProgress::CleanupIntent { .. }
5131        ) | (
5132            ProviderAdminJoinProgress::ResponseObserved(_),
5133            ProviderAdminJoinProgress::Completed(_)
5134        ) | (
5135            ProviderAdminJoinProgress::ResponseObserved(_),
5136            ProviderAdminJoinProgress::CleanupIntent { .. }
5137        ) | (
5138            ProviderAdminJoinProgress::ApprovalPrepared(_),
5139            ProviderAdminJoinProgress::WriteRevoked(_)
5140        ) | (
5141            ProviderAdminJoinProgress::AttemptObserved(_),
5142            ProviderAdminJoinProgress::WriteRevoked(_)
5143        ) | (
5144            ProviderAdminJoinProgress::ChallengeCreateIntent(_),
5145            ProviderAdminJoinProgress::WriteRevoked(_)
5146        ) | (
5147            ProviderAdminJoinProgress::ProviderReady(_),
5148            ProviderAdminJoinProgress::WriteRevoked(_)
5149        ) | (
5150            ProviderAdminJoinProgress::ResponseObserved(_),
5151            ProviderAdminJoinProgress::WriteRevoked(_)
5152        ) | (
5153            ProviderAdminJoinProgress::CleanupIntent { .. },
5154            ProviderAdminJoinProgress::Cancelled(_)
5155        ) | (
5156            ProviderAdminJoinProgress::CleanupIntent { .. },
5157            ProviderAdminJoinProgress::WriteRevoked(_)
5158        )
5159    )
5160}
5161
5162fn joiner_adjacent(previous: &JoinerJoinProgress, next: &JoinerJoinProgress) -> bool {
5163    matches!(
5164        (previous, next),
5165        (
5166            JoinerJoinProgress::OfferReceived(_),
5167            JoinerJoinProgress::AccessRequested(_)
5168        ) | (
5169            JoinerJoinProgress::AccessRequested(_),
5170            JoinerJoinProgress::ApprovalReceived(_)
5171        ) | (
5172            JoinerJoinProgress::AccessRequested(_),
5173            JoinerJoinProgress::Abandoned(_)
5174        ) | (
5175            JoinerJoinProgress::ApprovalReceived(_),
5176            JoinerJoinProgress::RegistrationPrepared(_)
5177        ) | (
5178            JoinerJoinProgress::ApprovalReceived(_),
5179            JoinerJoinProgress::Abandoned(_)
5180        ) | (
5181            JoinerJoinProgress::RegistrationPrepared(_),
5182            JoinerJoinProgress::ProviderReady(_)
5183        ) | (
5184            JoinerJoinProgress::RegistrationPrepared(_),
5185            JoinerJoinProgress::CleanupIntent { .. }
5186        ) | (
5187            JoinerJoinProgress::ProviderReady(_),
5188            JoinerJoinProgress::RegistrationCreateIntent(_)
5189        ) | (
5190            JoinerJoinProgress::ProviderReady(_),
5191            JoinerJoinProgress::CleanupIntent { .. }
5192        ) | (
5193            JoinerJoinProgress::RegistrationCreateIntent(_),
5194            JoinerJoinProgress::RegistrationCreated(_)
5195        ) | (
5196            JoinerJoinProgress::RegistrationCreateIntent(_),
5197            JoinerJoinProgress::CleanupIntent { .. }
5198        ) | (
5199            JoinerJoinProgress::RegistrationCreated(_),
5200            JoinerJoinProgress::AckCreateIntent(_)
5201        ) | (
5202            JoinerJoinProgress::RegistrationCreated(_),
5203            JoinerJoinProgress::CleanupIntent { .. }
5204        ) | (
5205            JoinerJoinProgress::AckCreateIntent(_),
5206            JoinerJoinProgress::AckCreated(_)
5207        ) | (
5208            JoinerJoinProgress::AckCreateIntent(_),
5209            JoinerJoinProgress::CleanupIntent { .. }
5210        ) | (
5211            JoinerJoinProgress::AckCreated(_),
5212            JoinerJoinProgress::ResponseCreateIntent(_)
5213        ) | (
5214            JoinerJoinProgress::AckCreated(_),
5215            JoinerJoinProgress::Ready(_)
5216        ) | (
5217            JoinerJoinProgress::AckCreated(_),
5218            JoinerJoinProgress::CleanupIntent { .. }
5219        ) | (
5220            JoinerJoinProgress::ResponseCreateIntent(_),
5221            JoinerJoinProgress::Ready(_)
5222        ) | (
5223            JoinerJoinProgress::ResponseCreateIntent(_),
5224            JoinerJoinProgress::CleanupIntent { .. }
5225        ) | (
5226            JoinerJoinProgress::Ready(_),
5227            JoinerJoinProgress::ActivationObserved(_)
5228        ) | (
5229            JoinerJoinProgress::Ready(_),
5230            JoinerJoinProgress::CleanupIntent { .. }
5231        ) | (
5232            JoinerJoinProgress::RegistrationPrepared(_),
5233            JoinerJoinProgress::WriteRevoked(_)
5234        ) | (
5235            JoinerJoinProgress::ProviderReady(_),
5236            JoinerJoinProgress::WriteRevoked(_)
5237        ) | (
5238            JoinerJoinProgress::RegistrationCreateIntent(_),
5239            JoinerJoinProgress::WriteRevoked(_)
5240        ) | (
5241            JoinerJoinProgress::RegistrationCreated(_),
5242            JoinerJoinProgress::WriteRevoked(_)
5243        ) | (
5244            JoinerJoinProgress::AckCreateIntent(_),
5245            JoinerJoinProgress::WriteRevoked(_)
5246        ) | (
5247            JoinerJoinProgress::AckCreated(_),
5248            JoinerJoinProgress::WriteRevoked(_)
5249        ) | (
5250            JoinerJoinProgress::ResponseCreateIntent(_),
5251            JoinerJoinProgress::WriteRevoked(_)
5252        ) | (
5253            JoinerJoinProgress::CleanupIntent { .. },
5254            JoinerJoinProgress::Cancelled(_)
5255        ) | (
5256            JoinerJoinProgress::ActivationObserved(_),
5257            JoinerJoinProgress::Activated(_)
5258        ) | (
5259            JoinerJoinProgress::Cancelled(_),
5260            JoinerJoinProgress::CleanupActivated(_)
5261        ) | (
5262            JoinerJoinProgress::WriteRevoked(_),
5263            JoinerJoinProgress::CleanupActivated(_)
5264        ) | (
5265            JoinerJoinProgress::CleanupActivated(_),
5266            JoinerJoinProgress::CancelledComplete(_)
5267        )
5268    )
5269}
5270
5271fn validate_initial_progress(progress: &DeviceJoinRoleProgress) -> Result<(), DeviceJoinError> {
5272    if matches!(
5273        progress,
5274        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Offered(_))
5275            | DeviceJoinRoleProgress::ProviderAdministrator(
5276                ProviderAdminJoinProgress::AccessRequested(_)
5277            )
5278            | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::OfferReceived(_))
5279    ) {
5280        Ok(())
5281    } else {
5282        Err(DeviceJoinError::NonAdjacentJournalTransition)
5283    }
5284}
5285
5286fn require_distinct_slots(slots: &[ObjectSlot]) -> Result<(), DeviceJoinError> {
5287    let unique = slots.iter().collect::<BTreeSet<_>>();
5288    if unique.len() == slots.len() {
5289        Ok(())
5290    } else {
5291        Err(DeviceJoinError::DuplicateReservedSlot)
5292    }
5293}
5294
5295fn attempt_key(attempt_id: DeviceJoinAttemptId) -> String {
5296    serde_json::to_value(attempt_id)
5297        .expect("device join attempt id serialization cannot fail")
5298        .as_str()
5299        .expect("device join attempt id serializes as a string")
5300        .to_string()
5301}
5302
5303fn sign<T: Serialize>(signer: &UserKeypair, domain: &[u8], value: &T) -> String {
5304    let digest = ObjectHash::digest(&domain_json(domain, value));
5305    hex::encode(signer.sign(digest.as_bytes()))
5306}
5307
5308fn verify_signature<T: Serialize>(
5309    public_key: &str,
5310    signature: &str,
5311    domain: &[u8],
5312    value: &T,
5313) -> Result<(), DeviceJoinError> {
5314    let digest = ObjectHash::digest(&domain_json(domain, value));
5315    if keys::verify_signature_hex(public_key, signature, digest.as_bytes()) {
5316        Ok(())
5317    } else {
5318        Err(DeviceJoinError::InvalidSignature)
5319    }
5320}
5321
5322fn domain_json<T: Serialize>(domain: &[u8], value: &T) -> Vec<u8> {
5323    let mut bytes = domain.to_vec();
5324    bytes.extend(serde_json::to_vec(value).expect("closed device join serialization cannot fail"));
5325    bytes
5326}
5327
5328#[derive(Debug, thiserror::Error)]
5329pub enum DeviceJoinError {
5330    #[error("device join signature is invalid")]
5331    InvalidSignature,
5332    #[error("device join offer does not name one active Store/member/provider authority")]
5333    OfferMismatch,
5334    #[error(
5335        "device provider admission approval differs from its request or activated access grant"
5336    )]
5337    ApprovalMismatch,
5338    #[error("device registration request differs from its offer, approval, or reserved slots")]
5339    RegistrationRequestMismatch,
5340    #[error("device join attempt differs from its signed exchange")]
5341    AttemptMismatch,
5342    #[error("device join cleanup does not contain the unconditional canonical slot set")]
5343    CleanupMismatch,
5344    #[error("device join journal transition is not the declared adjacent transition")]
5345    NonAdjacentJournalTransition,
5346    #[error("device join journal has a different durable value for this role and attempt")]
5347    JournalConflict,
5348    #[error("pending join payload hash differs from the exact transferred payload")]
5349    PendingTransferHashMismatch,
5350    #[error("device join reserved slots are not pairwise distinct")]
5351    DuplicateReservedSlot,
5352    #[error("device join requires an existing Member identity")]
5353    MemberNotEligible,
5354    #[error("provider operation failed: {0}")]
5355    Provider(String),
5356    #[error("Store device join state: {0}")]
5357    Store(String),
5358    #[error("device join requires an activated local Store device")]
5359    ActiveDeviceRequired,
5360    #[error("device join requires the active local Owner authority")]
5361    OwnerAuthorityRequired,
5362    #[error("device join requires the selected effective provider administrator")]
5363    ProviderAdministratorRequired,
5364    #[error("device join requires resolved Store membership")]
5365    MembershipConflict,
5366    #[error("device join requires the provider's exact-slot adapter")]
5367    ExactSlotStorageRequired,
5368    #[error("device join attempt cut does not include its provider-access activation")]
5369    ApprovalActivationMissing,
5370    #[error("device join activation is not materialized in the installed Store database")]
5371    ActivationNotMaterialized,
5372    #[error(transparent)]
5373    Object(#[from] crate::sync::store_objects::StoreObjectError),
5374    #[error(transparent)]
5375    Registration(#[from] crate::sync::store_registration::StoreRegistrationError),
5376    #[error(transparent)]
5377    Pull(#[from] crate::sync::store_pull::StorePullError),
5378    #[error(transparent)]
5379    Outbound(#[from] crate::sync::store_outbound::StoreOutboundError),
5380    #[error(transparent)]
5381    Protocol(#[from] crate::sync::store_commit::StoreProtocolError),
5382    #[error(transparent)]
5383    Storage(#[from] crate::sync::storage::StorageError),
5384    #[error(transparent)]
5385    Database(#[from] rusqlite::Error),
5386    #[error(transparent)]
5387    Serialization(#[from] serde_json::Error),
5388}