Skip to main content

coven_protocol/store_commit/
device_join_journal.rs

1//! The durable device-join journal model: role progress, and the status and
2//! action each recorded step derives.
3
4use serde::{Deserialize, Serialize};
5
6use crate::provider::DeviceJoinChallengePublicationAuthorization;
7use crate::provider::StoreMemberProviderAccessGrant;
8use crate::store_commit::device_join_exchange::{
9    DeviceJoinAbandonment, DeviceJoinAbandonmentObject, DeviceJoinActivation, DeviceJoinOffer,
10    DeviceJoinReadiness, DeviceProviderAccessRequest, DeviceProviderAdmissionApproval,
11    DeviceProviderAdmissionCompletion, DeviceRegistrationRequest, ProviderReadyDeviceBootstrap,
12    ProvisionalDeviceBootstrap, SamePrincipalDeviceJoin,
13};
14
15use super::*;
16
17/// Derived from a journal record on demand and never stored, so it carries no
18/// wire form of its own.
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub enum DeviceJoinStatus {
21    AwaitingAccessRequest {
22        offer: DeviceJoinOffer,
23    },
24    AwaitingProviderAdmission {
25        request: DeviceProviderAccessRequest,
26    },
27    AwaitingRegistrationRequest {
28        approval: DeviceProviderAdmissionApproval,
29    },
30    AwaitingBootstrap {
31        request: DeviceRegistrationRequest,
32    },
33    SamePrincipalActivationPublished {
34        request: DeviceRegistrationRequest,
35    },
36    AwaitingChallengePublication {
37        bootstrap: ProvisionalDeviceBootstrap,
38    },
39    AwaitingReadiness {
40        bootstrap: ProviderReadyDeviceBootstrap,
41    },
42    AwaitingProviderCompletion {
43        readiness: DeviceJoinReadiness,
44    },
45    AwaitingActivation {
46        completion: DeviceProviderAdmissionCompletion,
47    },
48    AwaitingCompletion {
49        activation: DeviceJoinActivation,
50    },
51    SamePrincipalCompleted {
52        join: SamePrincipalDeviceJoin,
53    },
54    Abandoned {
55        abandonment: DeviceJoinAbandonment,
56    },
57    ProviderAccessGrantPublished {
58        request: DeviceProviderAccessRequest,
59        grant: StoreMemberProviderAccessGrant,
60    },
61    StorePublicationPending {
62        operation: OwnerJoinPublication,
63    },
64}
65
66#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
67#[serde(rename_all = "snake_case", deny_unknown_fields)]
68pub enum DeviceJoinAction {
69    TransferOffer(DeviceJoinOffer),
70    TransferProviderAccessRequest(DeviceProviderAccessRequest),
71    TransferProviderAdmissionApproval(DeviceProviderAdmissionApproval),
72    TransferRegistrationRequest(DeviceRegistrationRequest),
73    TransferProviderReadyBootstrap(ProviderReadyDeviceBootstrap),
74    TransferReadiness(DeviceJoinReadiness),
75    TransferSamePrincipalJoin(SamePrincipalDeviceJoin),
76    TransferActivation(DeviceJoinActivation),
77    TransferAbandonment(DeviceJoinAbandonment),
78    CompleteJoin(DeviceJoinActivation),
79    ResumeOperation {
80        attempt_id: DeviceJoinAttemptId,
81        role: DeviceJoinRole,
82    },
83}
84
85#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
86#[serde(rename_all = "snake_case", deny_unknown_fields)]
87pub enum OwnerJoinProgress {
88    Offered(DeviceJoinOffer),
89    /// The joining device asked for provider access. The admitting device holds
90    /// the store's provider-administrator grant, so answering this request, the
91    /// grant it prepares, and the approval it signs are all its own steps.
92    AccessRequested(DeviceProviderAccessRequest),
93    StorePublicationPrepared(PreparedOwnerJoinPublication),
94    AccessGrantActivated {
95        request: DeviceProviderAccessRequest,
96        grant: StoreMemberProviderAccessGrant,
97        grant_ref: crate::provider::StoreMemberProviderAccessGrantRef,
98        activation: StoreBatchCommitRef,
99    },
100    ApprovalPrepared(DeviceProviderAdmissionApproval),
101    RegistrationRequested(DeviceRegistrationRequest),
102    AttemptActivated(ProvisionalDeviceBootstrap),
103    ChallengeCreateIntent(ProvisionalDeviceBootstrap),
104    ProviderReady(ProviderReadyDeviceBootstrap),
105    ResponseObserved(DeviceJoinReadiness),
106    Completed(DeviceProviderAdmissionCompletion),
107    SamePrincipalActivated {
108        request: DeviceRegistrationRequest,
109        registration: StoreDeviceRegistrationRef,
110        activation: StoreBatchCommitRef,
111        accepted_current: StoreCurrentPublicationRecord,
112    },
113    /// The owner published the activation commit and has nothing left to do
114    /// but hand the artifact over.
115    ///
116    /// `registration` is the joined device's, carried so the owner can tell
117    /// when that device has actually arrived: its announcement stream id is a
118    /// pure function of this reference, and a stream that appears in the
119    /// materialized frontier is the device's own first commit — the one thing
120    /// it publishes that the owner did not write for it.
121    ActivationPrepared {
122        completion: DeviceProviderAdmissionCompletion,
123        activation: DeviceJoinActivation,
124        registration: StoreDeviceRegistrationRef,
125    },
126    /// The same-principal join completed, carried closure and all.
127    ///
128    /// `registration` is the joined device's, for the same reason
129    /// [`ActivationPrepared`](Self::ActivationPrepared) carries one: this row is
130    /// the largest a join writes — a snapshot's metadata and the bootstrap
131    /// closure live inside `join` — and the owner needs to be able to tell when
132    /// the device it activated has arrived so the row can go.
133    SamePrincipalCompleted {
134        join: SamePrincipalDeviceJoin,
135        registration: StoreDeviceRegistrationRef,
136    },
137    Abandoned(DeviceJoinAbandonment),
138}
139
140#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
141#[serde(rename_all = "snake_case", deny_unknown_fields)]
142pub enum OwnerJoinPublication {
143    ProviderAccessGrant {
144        request: DeviceProviderAccessRequest,
145        grant: StoreMemberProviderAccessGrant,
146    },
147    Attempt {
148        request: DeviceRegistrationRequest,
149    },
150    Abandonment {
151        offer: DeviceJoinOffer,
152        abandonment: DeviceJoinAbandonmentObject,
153    },
154    SamePrincipalActivation {
155        request: DeviceRegistrationRequest,
156    },
157    JoinActivation {
158        completion: DeviceProviderAdmissionCompletion,
159    },
160}
161
162#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
163#[serde(deny_unknown_fields)]
164pub struct PreparedOwnerJoinPublication {
165    pub operation: OwnerJoinPublication,
166    pub candidate: Box<crate::prepared_commit::PreparedStoreOperationCommit>,
167}
168
169impl PreparedOwnerJoinPublication {
170    pub fn validate_for(
171        &self,
172        attempt_id: DeviceJoinAttemptId,
173    ) -> Result<(), crate::prepared_commit::PreparedCommitError> {
174        use crate::prepared_commit::PreparedCommitError;
175
176        self.candidate.validate_closed_shape()?;
177        let operations = self.candidate.commit.operations().ok_or_else(|| {
178            PreparedCommitError::Invariant(
179                "device join publication candidate contains another Store body".to_string(),
180            )
181        })?;
182        let has_unrelated_operation = operations.acknowledgement.is_some()
183            || !operations.circle_acknowledgements.is_empty()
184            || !operations.device_exclusion_proposals.is_empty()
185            || !operations.device_exclusion_outcomes.is_empty()
186            || !operations.stream_activations.is_empty()
187            || !operations.circle_controls.is_empty()
188            || operations.store_package.is_some()
189            || !operations.circle_packages.is_empty();
190        if has_unrelated_operation {
191            return Err(PreparedCommitError::Invariant(
192                "device join publication candidate contains an unrelated Store operation"
193                    .to_string(),
194            ));
195        }
196        match &self.operation {
197            OwnerJoinPublication::SamePrincipalActivation { .. }
198            | OwnerJoinPublication::JoinActivation { .. } => {
199                let publication = self.candidate.prepared_membership_publication()?;
200                if !matches!(&publication.entry.change,
201                    crate::membership::StoreAuthorityChange::DeviceRegistrationActivation { registration }
202                        if operations.device_registrations.as_slice() == std::slice::from_ref(registration))
203                {
204                    return Err(PreparedCommitError::Invariant(
205                        "join authority differs from its exact registration activation".into(),
206                    ));
207                }
208            }
209            _ if operations.control.is_some() => {
210                return Err(PreparedCommitError::Invariant(
211                    "device join operation has unrelated membership authority".into(),
212                ))
213            }
214            _ => {}
215        }
216        let registration = self.candidate.registration_activation.as_ref();
217        let exact = match &self.operation {
218            OwnerJoinPublication::ProviderAccessGrant { request, grant } => {
219                request.offer.attempt_id == attempt_id
220                    && operations.device_join_attempt_decisions.is_empty()
221                    && operations.device_registrations.is_empty()
222                    && registration.is_none()
223                    && matches!(operations.provider_access_grants.as_slice(), [reference]
224                        if reference.verify(grant).is_ok()
225                            && reference.object.verify(&grant.to_bytes()).is_ok())
226            }
227            OwnerJoinPublication::Attempt { request } => {
228                request.approval().request.offer.attempt_id == attempt_id
229                    && operations.provider_access_grants.is_empty()
230                    && operations.device_registrations.is_empty()
231                    && registration.is_none()
232                    && operations.device_join_attempt_decisions
233                        == [DeviceJoinAttemptDecisionRef::Attempt(attempt_id)]
234            }
235            OwnerJoinPublication::Abandonment { offer, abandonment } => {
236                offer.attempt_id == attempt_id
237                    && abandonment.attempt_id == attempt_id
238                    && abandonment.owner_registration == offer.owner_registration
239                    && operations.provider_access_grants.is_empty()
240                    && operations.device_registrations.is_empty()
241                    && registration.is_none()
242                    && matches!(operations.device_join_attempt_decisions.as_slice(),
243                        [DeviceJoinAttemptDecisionRef::Abandoned(reference)]
244                            if reference.attempt_id == attempt_id
245                                && reference.abandonment_hash == abandonment.abandonment_hash()
246                                && reference.object.verify(&abandonment.to_bytes()).is_ok())
247            }
248            OwnerJoinPublication::SamePrincipalActivation { request } => {
249                let expected = request.expected_registration();
250                request.approval().request.offer.attempt_id == attempt_id
251                    && operations.provider_access_grants.is_empty()
252                    && operations.device_join_attempt_decisions
253                        == [DeviceJoinAttemptDecisionRef::Attempt(attempt_id)]
254                    && matches!((operations.device_registrations.as_slice(), registration),
255                        ([reference], Some(activated))
256                            if activated.verify_reference(reference).is_ok()
257                                && activated.value() == expected
258                                && activated.reference().object.verify(&expected.to_bytes()).is_ok()
259                                && matches!(activated.activation(), StoreDeviceRegistrationActivation::Join { attempt_id: activated_attempt } if *activated_attempt == attempt_id))
260            }
261            OwnerJoinPublication::JoinActivation { completion } => {
262                let request = &completion.bootstrap().bootstrap.request;
263                let expected = request.expected_registration();
264                completion.attempt_id() == attempt_id
265                    && operations.provider_access_grants.is_empty()
266                    && operations.device_join_attempt_decisions.is_empty()
267                    && matches!((operations.device_registrations.as_slice(), registration),
268                        ([reference], Some(activated))
269                            if activated.verify_reference(reference).is_ok()
270                                && activated.value() == expected
271                                && activated.reference().object.verify(&expected.to_bytes()).is_ok()
272                                && matches!(activated.activation(), StoreDeviceRegistrationActivation::Join { attempt_id: activated_attempt } if *activated_attempt == attempt_id))
273            }
274        };
275        if !exact {
276            return Err(PreparedCommitError::Invariant(
277                "device join publication differs from its exact journal operation".to_string(),
278            ));
279        }
280        Ok(())
281    }
282
283    pub fn remote_objects(
284        &self,
285        attempt_id: DeviceJoinAttemptId,
286    ) -> Result<
287        Vec<crate::remote_object::ClosedRemoteObject>,
288        crate::prepared_commit::PreparedCommitError,
289    > {
290        self.validate_for(attempt_id)?;
291        let authority = self.authority_remote_object(attempt_id)?;
292        match authority {
293            Some(authority) => match &self.operation {
294                OwnerJoinPublication::SamePrincipalActivation { .. }
295                | OwnerJoinPublication::JoinActivation { .. } => self
296                    .candidate
297                    .retained_control_remote_objects(vec![authority]),
298                _ => self
299                    .candidate
300                    .retained_authority_remote_objects(vec![authority]),
301            },
302            None => Ok(vec![self.candidate.candidate_remote_object()?]),
303        }
304    }
305
306    pub fn authority_remote_object(
307        &self,
308        attempt_id: DeviceJoinAttemptId,
309    ) -> Result<
310        Option<crate::remote_object::ClosedRemoteObject>,
311        crate::prepared_commit::PreparedCommitError,
312    > {
313        self.validate_for(attempt_id)?;
314        let candidate = &self.candidate;
315        let activation = candidate.reference.clone();
316        Ok(match &self.operation {
317            OwnerJoinPublication::ProviderAccessGrant { grant, .. } => {
318                let reference = candidate.commit.provider_access_grants()[0].clone();
319                Some(crate::remote_object::RemoteObjectRecord::candidate_activated_provider_access_grant(
320                    reference,
321                    &grant.to_bytes(),
322                    &grant.to_bytes(),
323                    activation,
324                )?)
325            }
326            OwnerJoinPublication::Abandonment { abandonment, .. } => {
327                let DeviceJoinAttemptDecisionRef::Abandoned(reference) =
328                    &candidate.commit.device_join_attempt_decisions()[0]
329                else {
330                    unreachable!("validated abandonment publication has an abandonment reference")
331                };
332                Some(crate::remote_object::RemoteObjectRecord::candidate_activated_device_join_abandonment(
333                    reference.clone(),
334                    &abandonment.to_bytes(),
335                    &abandonment.to_bytes(),
336                    activation,
337                )?)
338            }
339            OwnerJoinPublication::SamePrincipalActivation { request } => {
340                let activated = candidate
341                    .registration_activation
342                    .as_ref()
343                    .expect("validated device registration publication has an activation");
344                Some(crate::remote_object::RemoteObjectRecord::candidate_activated_device_registration(
345                    activated.reference().clone(),
346                    &request.expected_registration().to_bytes(),
347                    &request.expected_registration().to_bytes(),
348                    activation,
349                )?)
350            }
351            OwnerJoinPublication::JoinActivation { completion } => {
352                let activated = candidate
353                    .registration_activation
354                    .as_ref()
355                    .expect("validated device registration publication has an activation");
356                let registration = completion
357                    .bootstrap()
358                    .bootstrap
359                    .request
360                    .expected_registration();
361                Some(crate::remote_object::RemoteObjectRecord::candidate_activated_device_registration(
362                    activated.reference().clone(),
363                    &registration.to_bytes(),
364                    &registration.to_bytes(),
365                    activation,
366                )?)
367            }
368            OwnerJoinPublication::Attempt { .. } => None,
369        })
370    }
371
372    pub fn accepted_progress(
373        &self,
374        attempt_id: DeviceJoinAttemptId,
375        accepted_current: StoreCurrentPublicationRecord,
376    ) -> Result<OwnerJoinProgress, crate::prepared_commit::PreparedCommitError> {
377        self.validate_for(attempt_id)?;
378        let activation = self.candidate.reference.clone();
379        Ok(match &self.operation {
380            OwnerJoinPublication::ProviderAccessGrant { request, grant } => {
381                OwnerJoinProgress::AccessGrantActivated {
382                    request: request.clone(),
383                    grant: grant.clone(),
384                    grant_ref: self.candidate.commit.provider_access_grants()[0].clone(),
385                    activation,
386                }
387            }
388            OwnerJoinPublication::Attempt { request } => {
389                OwnerJoinProgress::AttemptActivated(ProvisionalDeviceBootstrap {
390                    request: Box::new(request.clone()),
391                    publication_authorization: DeviceJoinChallengePublicationAuthorization {
392                        attempt_id,
393                        attempt_activation: activation,
394                    },
395                })
396            }
397            OwnerJoinPublication::Abandonment { .. } => {
398                let DeviceJoinAttemptDecisionRef::Abandoned(reference) =
399                    &self.candidate.commit.device_join_attempt_decisions()[0]
400                else {
401                    unreachable!("validated abandonment publication has an abandonment reference")
402                };
403                OwnerJoinProgress::Abandoned(DeviceJoinAbandonment {
404                    abandonment: reference.clone(),
405                    abandonment_activation: activation,
406                })
407            }
408            OwnerJoinPublication::SamePrincipalActivation { request } => {
409                OwnerJoinProgress::SamePrincipalActivated {
410                    request: request.clone(),
411                    registration: self
412                        .candidate
413                        .registration_activation
414                        .as_ref()
415                        .expect("validated registration publication has an activation")
416                        .reference()
417                        .clone(),
418                    activation,
419                    accepted_current,
420                }
421            }
422            OwnerJoinPublication::JoinActivation { completion } => {
423                OwnerJoinProgress::ActivationPrepared {
424                    completion: completion.clone(),
425                    activation: DeviceJoinActivation {
426                        attempt_id,
427                        outcome_activation: activation,
428                    },
429                    registration: self
430                        .candidate
431                        .registration_activation
432                        .as_ref()
433                        .expect("validated registration publication has an activation")
434                        .reference()
435                        .clone(),
436                }
437            }
438        })
439    }
440
441    pub fn validates_accepted_progress(
442        &self,
443        attempt_id: DeviceJoinAttemptId,
444        progress: &OwnerJoinProgress,
445    ) -> bool {
446        if self.validate_for(attempt_id).is_err() {
447            return false;
448        }
449        let candidate = &self.candidate;
450        let activated_registration = candidate
451            .registration_activation
452            .as_ref()
453            .map(ActivatedStoreDeviceRegistration::reference);
454        match (&self.operation, progress) {
455            (
456                OwnerJoinPublication::ProviderAccessGrant { request, grant },
457                OwnerJoinProgress::AccessGrantActivated {
458                    request: accepted_request,
459                    grant: accepted_grant,
460                    grant_ref,
461                    activation,
462                },
463            ) => {
464                request == accepted_request
465                    && grant == accepted_grant
466                    && candidate.commit.provider_access_grants() == std::slice::from_ref(grant_ref)
467                    && activation == &candidate.reference
468            }
469            (
470                OwnerJoinPublication::Attempt { request },
471                OwnerJoinProgress::AttemptActivated(bootstrap),
472            ) => {
473                bootstrap.request.as_ref() == request
474                    && bootstrap.publication_authorization.attempt_id == attempt_id
475                    && bootstrap.publication_authorization.attempt_activation == candidate.reference
476            }
477            (
478                OwnerJoinPublication::Abandonment { .. },
479                OwnerJoinProgress::Abandoned(abandonment),
480            ) => {
481                candidate.commit.device_join_attempt_decisions()
482                    == [DeviceJoinAttemptDecisionRef::Abandoned(
483                        abandonment.abandonment.clone(),
484                    )]
485                    && abandonment.abandonment_activation == candidate.reference
486            }
487            (
488                OwnerJoinPublication::SamePrincipalActivation { request },
489                OwnerJoinProgress::SamePrincipalActivated {
490                    request: accepted_request,
491                    registration,
492                    activation,
493                    accepted_current,
494                },
495            ) => {
496                request == accepted_request
497                    && Some(registration) == activated_registration
498                    && activation == &candidate.reference
499                    && accepted_current.store_root_hash == candidate.commit.store_root_hash
500                    && accepted_current.accepted().is_some()
501            }
502            (
503                OwnerJoinPublication::JoinActivation { completion },
504                OwnerJoinProgress::ActivationPrepared {
505                    completion: accepted_completion,
506                    activation,
507                    registration,
508                },
509            ) => {
510                completion == accepted_completion
511                    && Some(registration) == activated_registration
512                    && activation.attempt_id == attempt_id
513                    && activation.outcome_activation == candidate.reference
514            }
515            _ => false,
516        }
517    }
518}
519
520#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
521#[serde(rename_all = "snake_case", deny_unknown_fields)]
522pub enum JoinerJoinProgress {
523    OfferReceived(DeviceJoinOffer),
524    AccessRequested(DeviceProviderAccessRequest),
525    ApprovalReceived(DeviceProviderAdmissionApproval),
526    RegistrationPrepared(DeviceRegistrationRequest),
527    Ready(DeviceJoinReadiness),
528    ActivationObserved {
529        readiness: DeviceJoinReadiness,
530        activation: DeviceJoinActivation,
531    },
532}
533
534#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
535#[serde(rename_all = "snake_case", deny_unknown_fields)]
536pub enum DeviceJoinRoleProgress {
537    Owner(OwnerJoinProgress),
538    Joiner(JoinerJoinProgress),
539}
540
541/// A role's own progress type. Each one names the role whose journal rows hold
542/// it, so a journal bound to a role accepts only that role's progress.
543pub trait DeviceJoinRoleProgressKind: Into<DeviceJoinRoleProgress> {
544    const ROLE: DeviceJoinRole;
545}
546
547impl From<OwnerJoinProgress> for DeviceJoinRoleProgress {
548    fn from(progress: OwnerJoinProgress) -> Self {
549        Self::Owner(progress)
550    }
551}
552
553impl DeviceJoinRoleProgressKind for OwnerJoinProgress {
554    const ROLE: DeviceJoinRole = DeviceJoinRole::Owner;
555}
556
557impl From<JoinerJoinProgress> for DeviceJoinRoleProgress {
558    fn from(progress: JoinerJoinProgress) -> Self {
559        Self::Joiner(progress)
560    }
561}
562
563impl DeviceJoinRoleProgressKind for JoinerJoinProgress {
564    const ROLE: DeviceJoinRole = DeviceJoinRole::Joiner;
565}
566
567impl DeviceJoinRoleProgress {
568    pub fn role(&self) -> DeviceJoinRole {
569        match self {
570            Self::Owner(_) => DeviceJoinRole::Owner,
571            Self::Joiner(_) => DeviceJoinRole::Joiner,
572        }
573    }
574
575    pub fn role_name(&self) -> &'static str {
576        self.role().as_str()
577    }
578}
579
580#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
581#[serde(deny_unknown_fields)]
582pub struct DeviceJoinJournalRecord {
583    pub attempt_id: DeviceJoinAttemptId,
584    pub progress: Box<DeviceJoinRoleProgress>,
585}
586
587impl DeviceJoinJournalRecord {
588    pub fn owner_offered(offer: DeviceJoinOffer) -> Self {
589        Self {
590            attempt_id: offer.attempt_id,
591            progress: Box::new(DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Offered(
592                offer,
593            ))),
594        }
595    }
596
597    pub fn store_key(&self) -> String {
598        store_journal_key(self.attempt_id, self.progress.role_name())
599    }
600
601    pub fn store_key_for(attempt_id: DeviceJoinAttemptId, role: DeviceJoinRole) -> String {
602        store_journal_key(attempt_id, role.as_str())
603    }
604
605    pub fn status(&self) -> DeviceJoinStatus {
606        device_join_status(self)
607    }
608
609    pub fn action(&self) -> Option<DeviceJoinAction> {
610        device_join_action(self)
611    }
612
613    pub fn sort_key(&self) -> (DeviceJoinAttemptId, DeviceJoinRole) {
614        (self.attempt_id, self.progress.role())
615    }
616
617    pub fn attempt_key(&self) -> String {
618        attempt_key(self.attempt_id)
619    }
620}
621
622#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
623#[serde(rename_all = "snake_case", deny_unknown_fields)]
624/// The two sides of a join. One device admits — it answers the access
625/// request, prepares the storage grant, signs the approval, registers the
626/// device and activates it — and the other is the device being admitted.
627pub enum DeviceJoinRole {
628    Owner,
629    Joiner,
630}
631
632impl DeviceJoinRole {
633    pub fn as_str(self) -> &'static str {
634        match self {
635            Self::Owner => "owner",
636            Self::Joiner => "joiner",
637        }
638    }
639}
640
641pub(crate) fn device_join_status(record: &DeviceJoinJournalRecord) -> DeviceJoinStatus {
642    match &*record.progress {
643        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Offered(offer))
644        | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::OfferReceived(offer)) => {
645            DeviceJoinStatus::AwaitingAccessRequest {
646                offer: offer.clone(),
647            }
648        }
649        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::AccessRequested(request))
650        | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::AccessRequested(request)) => {
651            DeviceJoinStatus::AwaitingProviderAdmission {
652                request: request.clone(),
653            }
654        }
655        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::StorePublicationPrepared(prepared)) => {
656            DeviceJoinStatus::StorePublicationPending {
657                operation: prepared.operation.clone(),
658            }
659        }
660        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::AccessGrantActivated {
661            request,
662            grant,
663            ..
664        }) => DeviceJoinStatus::ProviderAccessGrantPublished {
665            request: request.clone(),
666            grant: grant.clone(),
667        },
668        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::ApprovalPrepared(approval))
669        | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::ApprovalReceived(approval)) => {
670            DeviceJoinStatus::AwaitingRegistrationRequest {
671                approval: approval.clone(),
672            }
673        }
674        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::RegistrationRequested(request))
675        | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::RegistrationPrepared(request)) => {
676            DeviceJoinStatus::AwaitingBootstrap {
677                request: request.clone(),
678            }
679        }
680        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::SamePrincipalActivated {
681            request,
682            ..
683        }) => DeviceJoinStatus::SamePrincipalActivationPublished {
684            request: request.clone(),
685        },
686        DeviceJoinRoleProgress::Owner(
687            OwnerJoinProgress::AttemptActivated(bootstrap)
688            | OwnerJoinProgress::ChallengeCreateIntent(bootstrap),
689        ) => DeviceJoinStatus::AwaitingChallengePublication {
690            bootstrap: bootstrap.clone(),
691        },
692        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::ProviderReady(bootstrap)) => {
693            DeviceJoinStatus::AwaitingReadiness {
694                bootstrap: bootstrap.clone(),
695            }
696        }
697        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::ResponseObserved(readiness))
698        | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::Ready(readiness)) => {
699            DeviceJoinStatus::AwaitingProviderCompletion {
700                readiness: readiness.clone(),
701            }
702        }
703        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Completed(completion)) => {
704            DeviceJoinStatus::AwaitingActivation {
705                completion: completion.clone(),
706            }
707        }
708        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::SamePrincipalCompleted {
709            join, ..
710        }) => DeviceJoinStatus::SamePrincipalCompleted { join: join.clone() },
711        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::ActivationPrepared {
712            activation, ..
713        })
714        | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::ActivationObserved {
715            activation,
716            ..
717        }) => DeviceJoinStatus::AwaitingCompletion {
718            activation: activation.clone(),
719        },
720        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Abandoned(abandonment)) => {
721            DeviceJoinStatus::Abandoned {
722                abandonment: abandonment.clone(),
723            }
724        }
725    }
726}
727
728pub fn device_join_action(record: &DeviceJoinJournalRecord) -> Option<DeviceJoinAction> {
729    let resume = || DeviceJoinAction::ResumeOperation {
730        attempt_id: record.attempt_id,
731        role: record.progress.role(),
732    };
733    match &*record.progress {
734        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Offered(offer)) => {
735            Some(DeviceJoinAction::TransferOffer(offer.clone()))
736        }
737        DeviceJoinRoleProgress::Owner(
738            OwnerJoinProgress::AccessRequested(_)
739            | OwnerJoinProgress::StorePublicationPrepared(_)
740            | OwnerJoinProgress::AccessGrantActivated { .. }
741            | OwnerJoinProgress::RegistrationRequested(_)
742            | OwnerJoinProgress::AttemptActivated(_)
743            | OwnerJoinProgress::ChallengeCreateIntent(_)
744            | OwnerJoinProgress::ResponseObserved(_)
745            | OwnerJoinProgress::Completed(_)
746            | OwnerJoinProgress::SamePrincipalActivated { .. },
747        ) => Some(resume()),
748        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::ApprovalPrepared(approval)) => Some(
749            DeviceJoinAction::TransferProviderAdmissionApproval(approval.clone()),
750        ),
751        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::ProviderReady(bootstrap)) => Some(
752            DeviceJoinAction::TransferProviderReadyBootstrap(bootstrap.clone()),
753        ),
754        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::SamePrincipalCompleted {
755            join, ..
756        }) => Some(DeviceJoinAction::TransferSamePrincipalJoin(join.clone())),
757        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::ActivationPrepared {
758            activation, ..
759        }) => Some(DeviceJoinAction::TransferActivation(activation.clone())),
760        DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Abandoned(abandonment)) => {
761            Some(DeviceJoinAction::TransferAbandonment(abandonment.clone()))
762        }
763
764        DeviceJoinRoleProgress::Joiner(
765            JoinerJoinProgress::OfferReceived(_) | JoinerJoinProgress::ApprovalReceived(_),
766        ) => Some(resume()),
767        DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::AccessRequested(request)) => Some(
768            DeviceJoinAction::TransferProviderAccessRequest(request.clone()),
769        ),
770        DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::RegistrationPrepared(request)) => Some(
771            DeviceJoinAction::TransferRegistrationRequest(request.clone()),
772        ),
773        DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::Ready(readiness)) => {
774            Some(DeviceJoinAction::TransferReadiness(readiness.clone()))
775        }
776        DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::ActivationObserved {
777            activation,
778            ..
779        }) => Some(DeviceJoinAction::CompleteJoin(activation.clone())),
780    }
781}
782
783pub(crate) fn store_journal_key(attempt_id: DeviceJoinAttemptId, role: &str) -> String {
784    format!("device_join/{}/{role}", attempt_key(attempt_id))
785}
786
787pub fn attempt_key(attempt_id: DeviceJoinAttemptId) -> String {
788    serde_json::to_value(attempt_id)
789        .expect("device join attempt id serialization cannot fail")
790        .as_str()
791        .expect("device join attempt id serializes as a string")
792        .to_string()
793}