1use 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#[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 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 ActivationPrepared {
122 completion: DeviceProviderAdmissionCompletion,
123 activation: DeviceJoinActivation,
124 registration: StoreDeviceRegistrationRef,
125 },
126 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 ®istration.to_bytes(),
364 ®istration.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
541pub 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)]
624pub 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}