1use std::collections::BTreeMap;
21use std::time::Duration;
22
23use serde::{Deserialize, Serialize};
24
25use crate::sync::store::{
26 DeviceJoinAbandonment, DeviceJoinAction, DeviceJoinActivation, DeviceJoinError,
27 DeviceJoinOffer, DeviceJoinReadiness, DeviceJoinRole, DeviceJoinStatus,
28 DeviceProviderAccessAdministrator, DeviceProviderAccessRequest,
29 DeviceProviderAdmissionApproval, DeviceRegistrationRequest, SamePrincipalDeviceJoin, Store,
30};
31use coven_keys::encryption::{EncryptionService, MasterKeyring, SealError};
32use coven_protocol::objects::ObjectSlot;
33use coven_protocol::objects::{ProtocolObjectContext, ProtocolObjectDomain, StorageError};
34use coven_protocol::store_commit::device_join_exchange::DeviceProviderAdmission;
35use coven_protocol::store_commit::device_join_exchange::DeviceProviderChallengePublication;
36use coven_protocol::store_commit::{DeviceJoinAttemptId, ObjectHash, STORE_PROTOCOL_VERSION};
37use coven_storage::CloudSyncObjectStorage;
38
39const TRANSPORT_ROOT: &str = "store-v1/device-join-transport";
41
42const SEAL_AAD_LABEL: &[u8] = b"coven.device-join-transport.v1";
45
46#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
49#[serde(rename_all = "kebab-case", deny_unknown_fields)]
50pub enum DeviceJoinTransportKind {
51 ProviderAccessRequest,
52 ProviderAdmissionApproval,
53 RegistrationRequest,
54 ProviderReadyBootstrap,
55 Readiness,
56 SamePrincipalJoin,
57 Activation,
58 Abandonment,
59}
60
61impl DeviceJoinTransportKind {
62 pub const ALL: [Self; 8] = [
65 Self::ProviderAccessRequest,
66 Self::ProviderAdmissionApproval,
67 Self::RegistrationRequest,
68 Self::ProviderReadyBootstrap,
69 Self::Readiness,
70 Self::SamePrincipalJoin,
71 Self::Activation,
72 Self::Abandonment,
73 ];
74
75 fn slug(self) -> &'static str {
77 match self {
78 Self::ProviderAccessRequest => "provider-access-request",
79 Self::ProviderAdmissionApproval => "provider-admission-approval",
80 Self::RegistrationRequest => "registration-request",
81 Self::ProviderReadyBootstrap => "provider-ready-bootstrap",
82 Self::Readiness => "readiness",
83 Self::SamePrincipalJoin => "same-principal-join",
84 Self::Activation => "activation",
85 Self::Abandonment => "abandonment",
86 }
87 }
88
89 fn producer(self) -> DeviceJoinRole {
92 match self {
93 Self::ProviderAccessRequest | Self::RegistrationRequest | Self::Readiness => {
94 DeviceJoinRole::Joiner
95 }
96 Self::ProviderAdmissionApproval
97 | Self::ProviderReadyBootstrap
98 | Self::SamePrincipalJoin
99 | Self::Activation
100 | Self::Abandonment => DeviceJoinRole::Owner,
101 }
102 }
103
104 fn of(action: &DeviceJoinAction) -> Option<Self> {
108 match action {
109 DeviceJoinAction::TransferProviderAccessRequest(_) => Some(Self::ProviderAccessRequest),
110 DeviceJoinAction::TransferProviderAdmissionApproval(_) => {
111 Some(Self::ProviderAdmissionApproval)
112 }
113 DeviceJoinAction::TransferRegistrationRequest(_) => Some(Self::RegistrationRequest),
114 DeviceJoinAction::TransferProviderReadyBootstrap(_) => {
115 Some(Self::ProviderReadyBootstrap)
116 }
117 DeviceJoinAction::TransferReadiness(_) => Some(Self::Readiness),
118 DeviceJoinAction::TransferSamePrincipalJoin(_) => Some(Self::SamePrincipalJoin),
119 DeviceJoinAction::TransferActivation(_) => Some(Self::Activation),
120 DeviceJoinAction::TransferAbandonment(_) => Some(Self::Abandonment),
121 DeviceJoinAction::TransferOffer(_)
122 | DeviceJoinAction::CompleteJoin(_)
123 | DeviceJoinAction::ResumeOperation { .. } => None,
124 }
125 }
126}
127
128pub trait DeviceJoinArtifact: Sized {
131 const KIND: DeviceJoinTransportKind;
132
133 fn from_action(action: DeviceJoinAction) -> Option<Self>;
134}
135
136macro_rules! device_join_artifact {
137 ($type:ty, $kind:ident, $variant:ident) => {
138 impl DeviceJoinArtifact for $type {
139 const KIND: DeviceJoinTransportKind = DeviceJoinTransportKind::$kind;
140
141 fn from_action(action: DeviceJoinAction) -> Option<Self> {
142 match action {
143 DeviceJoinAction::$variant(value) => Some(value),
144 _ => None,
145 }
146 }
147 }
148 };
149}
150
151device_join_artifact!(
152 DeviceProviderAccessRequest,
153 ProviderAccessRequest,
154 TransferProviderAccessRequest
155);
156device_join_artifact!(
157 DeviceProviderAdmissionApproval,
158 ProviderAdmissionApproval,
159 TransferProviderAdmissionApproval
160);
161device_join_artifact!(
162 DeviceRegistrationRequest,
163 RegistrationRequest,
164 TransferRegistrationRequest
165);
166device_join_artifact!(
167 coven_protocol::store_commit::device_join_exchange::ProviderReadyDeviceBootstrap,
168 ProviderReadyBootstrap,
169 TransferProviderReadyBootstrap
170);
171device_join_artifact!(DeviceJoinReadiness, Readiness, TransferReadiness);
172device_join_artifact!(
173 SamePrincipalDeviceJoin,
174 SamePrincipalJoin,
175 TransferSamePrincipalJoin
176);
177device_join_artifact!(DeviceJoinActivation, Activation, TransferActivation);
178device_join_artifact!(DeviceJoinAbandonment, Abandonment, TransferAbandonment);
179
180#[derive(Clone, Debug, Serialize, Deserialize)]
188#[serde(deny_unknown_fields)]
189pub struct DeviceJoinTransportParams {
190 pub version: u32,
191 pub attempt_namespace: String,
192 pub slots: BTreeMap<DeviceJoinTransportKind, ObjectSlot>,
193 #[serde(with = "seal_key")]
194 seal_key: MasterKeyring,
195}
196
197mod seal_key {
200 use super::MasterKeyring;
201 use serde::{Deserialize, Deserializer, Serializer};
202
203 pub(super) fn serialize<S: Serializer>(
204 keyring: &MasterKeyring,
205 serializer: S,
206 ) -> Result<S::Ok, S::Error> {
207 serializer.serialize_str(&keyring.to_serialized())
208 }
209
210 pub(super) fn deserialize<'de, D: Deserializer<'de>>(
211 deserializer: D,
212 ) -> Result<MasterKeyring, D::Error> {
213 let encoded = String::deserialize(deserializer)?;
214 MasterKeyring::from_serialized(&encoded).map_err(serde::de::Error::custom)
215 }
216}
217
218impl DeviceJoinTransportParams {
219 pub(crate) fn new(
220 attempt_namespace: String,
221 slots: BTreeMap<DeviceJoinTransportKind, ObjectSlot>,
222 seal_key: MasterKeyring,
223 ) -> Self {
224 Self {
225 version: STORE_PROTOCOL_VERSION,
226 attempt_namespace,
227 slots,
228 seal_key,
229 }
230 }
231
232 fn slot(&self, kind: DeviceJoinTransportKind) -> Result<&ObjectSlot, DeviceJoinTransportError> {
233 self.slots
234 .get(&kind)
235 .ok_or(DeviceJoinTransportError::MissingSlot { kind })
236 }
237
238 fn validate_for(&self, offer: &DeviceJoinOffer) -> Result<(), DeviceJoinTransportError> {
239 if self.version != STORE_PROTOCOL_VERSION
240 || self.attempt_namespace != attempt_namespace(offer.attempt_id)
241 {
242 return Err(DeviceJoinTransportError::BundleMismatch);
243 }
244 let context = slot_context(offer.store_root.store_root_hash);
245 for kind in DeviceJoinTransportKind::ALL {
246 context.validate_slot(
247 self.slot(kind)?,
248 &semantic_prefix(&self.attempt_namespace, kind),
249 )?;
250 }
251 Ok(())
252 }
253}
254
255#[derive(Clone, Debug, Serialize, Deserialize)]
259#[serde(deny_unknown_fields)]
260pub struct DeviceJoinOfferBundle {
261 pub version: u32,
262 pub offer: DeviceJoinOffer,
263 pub transport: DeviceJoinTransportParams,
264}
265
266impl DeviceJoinOfferBundle {
267 pub fn to_bytes(&self) -> Vec<u8> {
268 serde_json::to_vec(self).expect("device join offer bundle serialization cannot fail")
269 }
270
271 pub fn from_bytes(bytes: &[u8]) -> Result<Self, DeviceJoinTransportError> {
272 let bundle: Self = serde_json::from_slice(bytes)?;
273 if bundle.version != STORE_PROTOCOL_VERSION {
274 return Err(DeviceJoinTransportError::BundleMismatch);
275 }
276 bundle.transport.validate_for(&bundle.offer)?;
277 Ok(bundle)
278 }
279}
280
281#[derive(Clone, Debug, PartialEq, Eq)]
284pub enum DeviceJoinStep<T> {
285 Continue(T),
286 Abandoned(DeviceJoinAbandonment),
287}
288
289#[derive(Clone, Debug, PartialEq, Eq)]
291pub enum DeviceJoinDriveOutcome {
292 Activated(DeviceJoinActivation),
293 Abandoned(DeviceJoinAbandonment),
294}
295
296#[derive(Clone, Debug, PartialEq, Eq)]
301pub enum JoiningDeviceJoinProgress {
302 WaitingForApproval,
303 RequestingProviderAccess,
304 WaitingForProviderAccess,
305 RegisteringDevice,
306 WaitingForLibrary,
307 DownloadingSnapshot { bytes_done: u64, bytes_total: u64 },
308 InstallingSnapshot,
309 WaitingForActivation,
310 CatchingUp,
311 SavingLibrary,
312}
313
314pub type JoiningDeviceJoinProgressObserver =
317 std::sync::Arc<dyn Fn(JoiningDeviceJoinProgress) + Send + Sync>;
318
319#[derive(Clone, Copy, Debug, PartialEq, Eq)]
322pub enum AdmittingDeviceJoinProgress {
323 PreparingInvitation,
324 WaitingForProviderAccessRequest,
325 GrantingProviderAccess,
326 WaitingForRegistrationRequest,
327 RegisteringDevice,
328 PreparingLibrary,
329 WaitingForJoiningDevice,
330 ActivatingDevice,
331}
332
333#[derive(Clone, Copy, Debug, PartialEq, Eq)]
336pub struct DeviceJoinTransportTiming {
337 pub poll: Duration,
338 pub deadline: Duration,
339}
340
341impl DeviceJoinTransportTiming {
342 pub const fn interactive() -> Self {
346 Self {
347 poll: Duration::from_millis(100),
348 deadline: Duration::from_secs(180),
349 }
350 }
351
352 fn polls(self) -> JoinPollBackoff {
354 JoinPollBackoff {
355 next: self.poll,
356 ceiling: JOIN_POLL_CEILING.max(self.poll),
357 }
358 }
359}
360
361const JOIN_POLL_CEILING: Duration = Duration::from_secs(2);
370
371struct JoinPollBackoff {
372 next: Duration,
373 ceiling: Duration,
374}
375
376impl JoinPollBackoff {
377 fn next(&mut self) -> Duration {
378 let current = self.next;
379 self.next = (current * 2).min(self.ceiling);
380 current
381 }
382}
383
384pub async fn timed_owner_join_step<T>(
394 step: &'static str,
395 requests: Option<std::sync::Arc<dyn coven_foundation::stage_timing::ProviderRequests>>,
396 work: impl std::future::Future<Output = T>,
397) -> T {
398 let mut timings =
399 coven_foundation::stage_timing::StageTimings::counting("Device join owner step", requests);
400 let outcome = timings.stage(step, work).await;
401 timings.report();
402 outcome
403}
404
405struct JoinWait {
412 kind: DeviceJoinTransportKind,
413 started: coven_foundation::clock::Stopwatch,
414 polls: std::sync::atomic::AtomicU64,
415}
416
417impl JoinWait {
418 fn begin(kind: DeviceJoinTransportKind) -> Self {
419 Self {
420 kind,
421 started: coven_foundation::clock::Stopwatch::start(),
422 polls: std::sync::atomic::AtomicU64::new(0),
423 }
424 }
425
426 fn polled(&self) {
427 self.polls
428 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
429 }
430
431 fn report(self) {
432 tracing::info!(
433 kind = ?self.kind,
434 produced_by = ?self.kind.producer(),
435 waited_ms = self.started.elapsed().as_millis() as u64,
436 looks = self.polls.load(std::sync::atomic::Ordering::Relaxed),
437 "Device join waited for its counterpart"
438 );
439 }
440}
441
442#[derive(Debug, thiserror::Error)]
444pub enum DeviceJoinTransportError {
445 #[error("storage: {0}")]
446 Storage(#[from] StorageError),
447 #[error("device join: {0}")]
448 DeviceJoin(#[from] DeviceJoinError),
449 #[error("transport artifact is not valid JSON: {0}")]
450 Malformed(#[from] serde_json::Error),
451 #[error("transport artifact could not be unsealed: {0}")]
452 Unsealable(#[from] SealError),
453 #[error("the offer bundle does not describe this attempt's transport")]
454 BundleMismatch,
455 #[error("this attempt's transport has no {kind:?} slot")]
456 MissingSlot { kind: DeviceJoinTransportKind },
457 #[error("{0:?} carries nothing for the transport to deliver")]
461 NotTransferable(Box<DeviceJoinAction>),
462 #[error("a {kind:?} artifact is the {role:?}'s to publish, not this device's")]
464 WrongProducer {
465 kind: DeviceJoinTransportKind,
466 role: DeviceJoinRole,
467 },
468 #[error("the {kind:?} slot already holds a different artifact")]
472 ArtifactConflict { kind: DeviceJoinTransportKind },
473 #[error("the {kind:?} slot was written concurrently with different bytes")]
476 SlotConflict { kind: DeviceJoinTransportKind },
477 #[error("the {kind:?} slot holds an artifact of another kind")]
479 KindMismatch { kind: DeviceJoinTransportKind },
480 #[error("the {producer:?} never published its {kind:?} artifact")]
481 Timeout {
482 kind: DeviceJoinTransportKind,
483 producer: DeviceJoinRole,
484 },
485}
486
487pub struct DeviceJoinTransport<'a> {
490 storage: &'a dyn CloudSyncObjectStorage,
491 params: &'a DeviceJoinTransportParams,
492 store_root_hash: ObjectHash,
493 seal: EncryptionService,
494 role: DeviceJoinRole,
495}
496
497impl<'a> DeviceJoinTransport<'a> {
498 pub fn open(
502 storage: &'a dyn CloudSyncObjectStorage,
503 bundle: &'a DeviceJoinOfferBundle,
504 role: DeviceJoinRole,
505 ) -> Result<Self, DeviceJoinTransportError> {
506 bundle.transport.validate_for(&bundle.offer)?;
507 Ok(Self {
508 storage,
509 params: &bundle.transport,
510 store_root_hash: bundle.offer.store_root.store_root_hash,
511 seal: EncryptionService::from(bundle.transport.seal_key.clone()),
512 role,
513 })
514 }
515
516 pub async fn publish(&self, action: &DeviceJoinAction) -> Result<(), DeviceJoinTransportError> {
525 let kind = DeviceJoinTransportKind::of(action)
526 .ok_or_else(|| DeviceJoinTransportError::NotTransferable(Box::new(action.clone())))?;
527 let producer = kind.producer();
528 if self.role != producer {
529 return Err(DeviceJoinTransportError::WrongProducer {
530 kind,
531 role: producer,
532 });
533 }
534 let sealed = self
535 .seal
536 .seal_app_data(&serde_json::to_vec(action)?, &self.seal_aad(kind));
537 let prepared = self.storage.prepare_protocol_object(
538 &slot_context(self.store_root_hash),
539 self.params.slot(kind)?.clone(),
540 &self.semantic_prefix(kind),
541 sealed,
542 )?;
543 match self.storage.create_protocol_object(&prepared).await {
544 Ok(()) => Ok(()),
545 Err(StorageError::SlotCollision(_)) => match self.read(kind).await? {
546 Some(existing) if existing == *action => Ok(()),
547 Some(_) => Err(DeviceJoinTransportError::ArtifactConflict { kind }),
548 None => Err(DeviceJoinTransportError::SlotConflict { kind }),
549 },
550 Err(error) => Err(error.into()),
551 }
552 }
553
554 pub async fn read(
556 &self,
557 kind: DeviceJoinTransportKind,
558 ) -> Result<Option<DeviceJoinAction>, DeviceJoinTransportError> {
559 let sealed = match self
560 .storage
561 .read_protocol_slot(
562 &slot_context(self.store_root_hash),
563 self.params.slot(kind)?,
564 &self.semantic_prefix(kind),
565 )
566 .await
567 {
568 Ok((sealed, _)) => sealed,
569 Err(StorageError::NotFound(_)) => return Ok(None),
570 Err(error) => return Err(error.into()),
571 };
572 let opened = self.seal.open_app_data(&sealed, &self.seal_aad(kind))?;
573 let action: DeviceJoinAction = serde_json::from_slice(&opened)?;
574 if DeviceJoinTransportKind::of(&action) != Some(kind) {
575 return Err(DeviceJoinTransportError::KindMismatch { kind });
576 }
577 Ok(Some(action))
578 }
579
580 pub async fn await_artifact<T: DeviceJoinArtifact>(
584 &self,
585 timing: DeviceJoinTransportTiming,
586 ) -> Result<T, DeviceJoinTransportError> {
587 let kind = T::KIND;
588 let wait = JoinWait::begin(kind);
589 let polled = tokio::time::timeout(timing.deadline, async {
590 let mut poll = timing.polls();
591 loop {
592 wait.polled();
593 if let Some(action) = self.read(kind).await? {
594 return T::from_action(action)
595 .ok_or(DeviceJoinTransportError::KindMismatch { kind });
596 }
597 tokio::time::sleep(poll.next()).await;
598 }
599 })
600 .await;
601 wait.report();
602 match polled {
603 Ok(artifact) => artifact,
604 Err(_) => Err(DeviceJoinTransportError::Timeout {
605 kind,
606 producer: kind.producer(),
607 }),
608 }
609 }
610
611 pub async fn observe_artifact<T: DeviceJoinArtifact>(
627 &self,
628 timing: DeviceJoinTransportTiming,
629 ) -> Result<T, DeviceJoinTransportError> {
630 let kind = T::KIND;
631 let mut poll = timing.polls();
632 loop {
633 if let Some(action) = self.read(kind).await? {
634 return T::from_action(action)
635 .ok_or(DeviceJoinTransportError::KindMismatch { kind });
636 }
637 tokio::time::sleep(poll.next()).await;
638 }
639 }
640
641 pub async fn await_step<T: DeviceJoinArtifact>(
649 &self,
650 timing: DeviceJoinTransportTiming,
651 ) -> Result<DeviceJoinStep<T>, DeviceJoinTransportError> {
652 let kind = T::KIND;
653 let wait = JoinWait::begin(kind);
654 let polled = tokio::time::timeout(timing.deadline, async {
655 let mut poll = timing.polls();
656 loop {
657 wait.polled();
658 if let Some(action) = self.read(DeviceJoinTransportKind::Abandonment).await? {
659 return DeviceJoinAbandonment::from_action(action)
660 .map(DeviceJoinStep::Abandoned)
661 .ok_or(DeviceJoinTransportError::KindMismatch {
662 kind: DeviceJoinTransportKind::Abandonment,
663 });
664 }
665 if let Some(action) = self.read(kind).await? {
666 return T::from_action(action)
667 .map(DeviceJoinStep::Continue)
668 .ok_or(DeviceJoinTransportError::KindMismatch { kind });
669 }
670 tokio::time::sleep(poll.next()).await;
671 }
672 })
673 .await;
674 wait.report();
675 match polled {
676 Ok(step) => step,
677 Err(_) => Err(DeviceJoinTransportError::Timeout {
678 kind,
679 producer: kind.producer(),
680 }),
681 }
682 }
683
684 pub async fn delete_attempt_slots(&self) -> Result<(), DeviceJoinTransportError> {
706 let context = slot_context(self.store_root_hash);
707 let listed = self
708 .storage
709 .list_protocol_slots(&context, &format!("{}/", self.params.attempt_namespace))
710 .await?;
711 let deletions = futures_util::future::join_all(listed.iter().map(|slot| async move {
712 let Some(object) = self.storage.observe_exact_slot(slot).await? else {
713 return Ok(());
714 };
715 self.storage
716 .delete_protocol_object(&object)
717 .await
718 .map_err(DeviceJoinTransportError::from)
719 }))
720 .await;
721 for result in deletions {
722 result?;
723 }
724 Ok(())
725 }
726
727 fn semantic_prefix(&self, kind: DeviceJoinTransportKind) -> String {
728 semantic_prefix(&self.params.attempt_namespace, kind)
729 }
730
731 fn seal_aad(&self, kind: DeviceJoinTransportKind) -> Vec<u8> {
734 let prefix = self.semantic_prefix(kind);
735 let mut aad = SEAL_AAD_LABEL.to_vec();
736 aad.extend_from_slice(self.store_root_hash.as_bytes());
737 aad.extend_from_slice(&(prefix.len() as u64).to_le_bytes());
738 aad.extend_from_slice(prefix.as_bytes());
739 aad
740 }
741}
742
743pub(crate) fn attempt_namespace(attempt_id: DeviceJoinAttemptId) -> String {
744 format!("{TRANSPORT_ROOT}/{attempt_id}")
745}
746
747pub(crate) fn semantic_prefix(attempt_namespace: &str, kind: DeviceJoinTransportKind) -> String {
748 format!("{attempt_namespace}/{}", kind.slug())
749}
750
751pub(crate) fn slot_context(store_root_hash: ObjectHash) -> ProtocolObjectContext {
752 ProtocolObjectContext::recipient_sealed(
753 store_root_hash,
754 ProtocolObjectDomain::DeviceJoinTransport,
755 )
756}
757
758pub enum DeviceJoinApprovalPolicy<'a> {
760 AutoApproveSelfIssued,
765 Ask(&'a (dyn Fn(&DeviceProviderAccessRequest) -> DeviceJoinApproval + Send + Sync)),
767}
768
769#[derive(Clone, Copy, Debug, PartialEq, Eq)]
770pub enum DeviceJoinApproval {
771 Approve,
772 Refuse,
773}
774
775pub struct StoreDeviceJoinTransport<'store> {
776 store: &'store Store,
777}
778
779impl<'store> StoreDeviceJoinTransport<'store> {
780 pub(crate) fn new(store: &'store Store) -> Self {
781 Self { store }
782 }
783
784 pub async fn allocate_bundle(
785 &self,
786 offer: DeviceJoinOffer,
787 ) -> Result<DeviceJoinOfferBundle, DeviceJoinTransportError> {
788 self.store
789 .allocate_device_join_transport_bundle(offer)
790 .await
791 }
792
793 pub async fn drive(
794 &self,
795 bundle: &DeviceJoinOfferBundle,
796 policy: DeviceJoinApprovalPolicy<'_>,
797 access_administrator: Option<&dyn DeviceProviderAccessAdministrator>,
798 on_progress: &(dyn Fn(AdmittingDeviceJoinProgress) + Send + Sync),
799 timing: DeviceJoinTransportTiming,
800 ) -> Result<DeviceJoinDriveOutcome, DeviceJoinTransportError> {
801 retrying_activation_conflicts(|| async {
802 AttemptTransport::open(self.store, bundle)
803 .await?
804 .drive_once(&policy, access_administrator, on_progress, timing)
805 .await
806 })
807 .await
808 }
809
810 pub async fn abandon(
811 &self,
812 bundle: &DeviceJoinOfferBundle,
813 ) -> Result<DeviceJoinAbandonment, DeviceJoinTransportError> {
814 let attempt = AttemptTransport::open(self.store, bundle).await?;
815 let abandonment = self.store.abandon_device_join(bundle.offer.clone()).await?;
816 attempt.finish_abandonment(&abandonment).await?;
817 Ok(abandonment)
818 }
819
820 pub async fn abort(
828 &self,
829 bundle: &DeviceJoinOfferBundle,
830 ) -> Result<(), DeviceJoinTransportError> {
831 let attempt = AttemptTransport::open(self.store, bundle).await?;
832 match attempt.owner_status().await? {
833 None => Ok(()),
838 Some(
839 DeviceJoinStatus::AwaitingAccessRequest { .. }
840 | DeviceJoinStatus::AwaitingProviderAdmission { .. }
841 | DeviceJoinStatus::ProviderAccessGrantCreatePending { .. }
842 | DeviceJoinStatus::AwaitingRegistrationRequest { .. }
843 | DeviceJoinStatus::AwaitingBootstrap { .. }
844 | DeviceJoinStatus::AbandonmentCreatePending { .. }
845 | DeviceJoinStatus::Abandoned { .. },
846 ) => {
847 self.abandon(bundle).await?;
848 Ok(())
849 }
850 status => Err(DeviceJoinError::Store(format!(
851 "device join {} is past the point it could be given up on: {status:?}",
852 bundle.offer.attempt_id
853 ))
854 .into()),
855 }
856 }
857}
858
859struct AttemptTransport<'attempt> {
862 store: &'attempt Store,
863 bundle: &'attempt DeviceJoinOfferBundle,
864 attempt_id: DeviceJoinAttemptId,
865}
866
867impl<'attempt> AttemptTransport<'attempt> {
868 async fn open(
869 store: &'attempt Store,
870 bundle: &'attempt DeviceJoinOfferBundle,
871 ) -> Result<Self, DeviceJoinTransportError> {
872 store.require_device_join_admitter(&bundle.offer).await?;
873 Ok(Self {
874 store,
875 bundle,
876 attempt_id: bundle.offer.attempt_id,
877 })
878 }
879
880 async fn publish(&self, action: DeviceJoinAction) -> Result<(), DeviceJoinTransportError> {
884 self.step(
885 "publish artifact",
886 self.store
887 .publish_device_join_transport_artifact(self.bundle, &action),
888 )
889 .await
890 }
891
892 async fn finish_abandonment(
900 &self,
901 abandonment: &DeviceJoinAbandonment,
902 ) -> Result<(), DeviceJoinTransportError> {
903 self.publish(DeviceJoinAction::TransferAbandonment(abandonment.clone()))
904 .await?;
905 self.store
906 .retire_device_join_row(self.attempt_id, DeviceJoinRole::Owner)
907 .await
908 }
909
910 async fn step<T>(&self, step: &'static str, work: impl std::future::Future<Output = T>) -> T {
916 timed_owner_join_step(step, self.store.provider_requests(), Box::pin(work)).await
917 }
918
919 async fn await_artifact<T: DeviceJoinArtifact>(
921 &self,
922 timing: DeviceJoinTransportTiming,
923 ) -> Result<T, DeviceJoinTransportError> {
924 self.store
925 .await_device_join_transport_artifact::<T>(self.bundle, timing)
926 .await
927 }
928
929 async fn owner_status(&self) -> Result<Option<DeviceJoinStatus>, DeviceJoinTransportError> {
930 self.store
931 .device_join_transport_status(self.attempt_id, DeviceJoinRole::Owner)
932 .await
933 }
934
935 async fn drive_once(
942 &self,
943 policy: &DeviceJoinApprovalPolicy<'_>,
944 access_administrator: Option<&dyn DeviceProviderAccessAdministrator>,
945 on_progress: &(dyn Fn(AdmittingDeviceJoinProgress) + Send + Sync),
946 timing: DeviceJoinTransportTiming,
947 ) -> Result<DeviceJoinDriveOutcome, DeviceJoinTransportError> {
948 loop {
949 match self.owner_status().await? {
950 Some(DeviceJoinStatus::Abandoned { abandonment }) => {
956 self.finish_abandonment(&abandonment).await?;
957 return Ok(DeviceJoinDriveOutcome::Abandoned(abandonment));
958 }
959 Some(DeviceJoinStatus::SamePrincipalCompleted { join }) => {
960 self.publish(DeviceJoinAction::TransferSamePrincipalJoin(join.clone()))
961 .await?;
962 return Ok(DeviceJoinDriveOutcome::Activated(join.activation));
963 }
964 Some(DeviceJoinStatus::AwaitingCompletion { activation }) => {
965 self.publish(DeviceJoinAction::TransferActivation(activation.clone()))
966 .await?;
967 return Ok(DeviceJoinDriveOutcome::Activated(activation));
968 }
969 None | Some(DeviceJoinStatus::AwaitingAccessRequest { .. }) => {
970 on_progress(AdmittingDeviceJoinProgress::WaitingForProviderAccessRequest);
971 let request = self
972 .await_artifact::<DeviceProviderAccessRequest>(timing)
973 .await?;
974 self.step(
975 "approve access request",
976 self.approve_access_request(&request, policy),
977 )
978 .await?;
979 if request.offer.provider_admin.provider == request.peer_provider {
980 on_progress(AdmittingDeviceJoinProgress::RegisteringDevice);
981 let join = self
982 .step(
983 "activate same-provider device",
984 self.activate_same_principal(request, access_administrator),
985 )
986 .await?;
987 self.publish(DeviceJoinAction::TransferSamePrincipalJoin(join.clone()))
988 .await?;
989 return Ok(DeviceJoinDriveOutcome::Activated(join.activation));
990 }
991 on_progress(AdmittingDeviceJoinProgress::GrantingProviderAccess);
992 let approval = self
993 .step(
994 "authorize provider access",
995 self.store
996 .authorize_device_provider_access(request, access_administrator),
997 )
998 .await?;
999 self.publish(DeviceJoinAction::TransferProviderAdmissionApproval(
1000 approval,
1001 ))
1002 .await?;
1003 }
1004 Some(
1005 DeviceJoinStatus::AwaitingProviderAdmission { request }
1006 | DeviceJoinStatus::ProviderAccessGrantCreatePending { request, .. },
1007 ) => {
1008 on_progress(AdmittingDeviceJoinProgress::GrantingProviderAccess);
1009 let approval = self
1010 .step(
1011 "authorize provider access",
1012 self.store
1013 .authorize_device_provider_access(request, access_administrator),
1014 )
1015 .await?;
1016 self.publish(DeviceJoinAction::TransferProviderAdmissionApproval(
1017 approval,
1018 ))
1019 .await?;
1020 }
1021 Some(DeviceJoinStatus::AwaitingRegistrationRequest { approval }) => {
1022 self.publish(DeviceJoinAction::TransferProviderAdmissionApproval(
1023 approval.clone(),
1024 ))
1025 .await?;
1026 if matches!(approval.admission, DeviceProviderAdmission::SamePrincipal) {
1027 let request = DeviceRegistrationRequest::same_principal(approval)
1028 .map_err(DeviceJoinError::from)?;
1029 on_progress(AdmittingDeviceJoinProgress::RegisteringDevice);
1030 let join = self
1031 .step(
1032 "activate same-provider device",
1033 self.store.resume_same_principal_device_join(request),
1034 )
1035 .await?;
1036 self.publish(DeviceJoinAction::TransferSamePrincipalJoin(join.clone()))
1037 .await?;
1038 return Ok(DeviceJoinDriveOutcome::Activated(join.activation));
1039 }
1040 on_progress(AdmittingDeviceJoinProgress::WaitingForRegistrationRequest);
1041 let request = self
1042 .await_artifact::<DeviceRegistrationRequest>(timing)
1043 .await?;
1044 on_progress(AdmittingDeviceJoinProgress::RegisteringDevice);
1045 self.accept_registration(request).await?;
1046 }
1047 Some(DeviceJoinStatus::AwaitingBootstrap { request }) => {
1048 on_progress(AdmittingDeviceJoinProgress::RegisteringDevice);
1049 if matches!(request, DeviceRegistrationRequest::SamePrincipal { .. }) {
1050 let join = self
1051 .step(
1052 "activate same-provider device",
1053 self.store.resume_same_principal_device_join(request),
1054 )
1055 .await?;
1056 self.publish(DeviceJoinAction::TransferSamePrincipalJoin(join.clone()))
1057 .await?;
1058 return Ok(DeviceJoinDriveOutcome::Activated(join.activation));
1059 }
1060 self.accept_registration(request).await?;
1061 }
1062 Some(DeviceJoinStatus::SamePrincipalActivationCreatePending { request }) => {
1063 on_progress(AdmittingDeviceJoinProgress::RegisteringDevice);
1064 let join = self
1065 .step(
1066 "activate same-provider device",
1067 self.store.resume_same_principal_device_join(request),
1068 )
1069 .await?;
1070 self.publish(DeviceJoinAction::TransferSamePrincipalJoin(join.clone()))
1071 .await?;
1072 return Ok(DeviceJoinDriveOutcome::Activated(join.activation));
1073 }
1074 Some(DeviceJoinStatus::AwaitingChallengePublication { bootstrap }) => {
1075 on_progress(AdmittingDeviceJoinProgress::PreparingLibrary);
1076 let ready = self
1077 .step(
1078 "publish provider challenge",
1079 self.store.publish_device_provider_challenge(bootstrap),
1080 )
1081 .await?;
1082 self.publish(DeviceJoinAction::TransferProviderReadyBootstrap(ready))
1083 .await?;
1084 }
1085 Some(DeviceJoinStatus::AwaitingReadiness { bootstrap }) => {
1086 self.publish(DeviceJoinAction::TransferProviderReadyBootstrap(
1087 bootstrap.clone(),
1088 ))
1089 .await?;
1090 if matches!(
1091 bootstrap.challenge_publication,
1092 DeviceProviderChallengePublication::SamePrincipal
1093 ) {
1094 self.step(
1095 "complete same-provider admission",
1096 self.store
1097 .complete_same_principal_device_admission(bootstrap),
1098 )
1099 .await?;
1100 continue;
1101 }
1102 on_progress(AdmittingDeviceJoinProgress::WaitingForJoiningDevice);
1103 let readiness = self.await_artifact::<DeviceJoinReadiness>(timing).await?;
1104 on_progress(AdmittingDeviceJoinProgress::ActivatingDevice);
1105 self.step(
1106 "complete provider admission",
1107 self.store.complete_device_provider_admission(readiness),
1108 )
1109 .await?;
1110 }
1111 Some(DeviceJoinStatus::AwaitingProviderCompletion { readiness }) => {
1112 on_progress(AdmittingDeviceJoinProgress::ActivatingDevice);
1113 self.step(
1114 "complete provider admission",
1115 self.store.complete_device_provider_admission(readiness),
1116 )
1117 .await?;
1118 }
1119 Some(DeviceJoinStatus::AwaitingActivation { completion }) => {
1120 on_progress(AdmittingDeviceJoinProgress::ActivatingDevice);
1121 let activation = self
1122 .step(
1123 "publish activation",
1124 self.store.finalize_device_join(completion),
1125 )
1126 .await?;
1127 self.publish(DeviceJoinAction::TransferActivation(activation.clone()))
1128 .await?;
1129 return Ok(DeviceJoinDriveOutcome::Activated(activation));
1130 }
1131 status => {
1132 return Err(DeviceJoinError::Store(format!(
1133 "device join {} has no admitting step from {status:?}",
1134 self.attempt_id
1135 ))
1136 .into());
1137 }
1138 }
1139 }
1140 }
1141
1142 async fn accept_registration(
1143 &self,
1144 request: DeviceRegistrationRequest,
1145 ) -> Result<(), DeviceJoinTransportError> {
1146 self.step(
1147 "accept registration",
1148 self.store.accept_device_registration_request(request),
1149 )
1150 .await?;
1151 Ok(())
1152 }
1153
1154 async fn activate_same_principal(
1160 &self,
1161 request: DeviceProviderAccessRequest,
1162 access_administrator: Option<&dyn DeviceProviderAccessAdministrator>,
1163 ) -> Result<SamePrincipalDeviceJoin, DeviceJoinTransportError> {
1164 let mut timings = coven_foundation::stage_timing::StageTimings::counting(
1167 "Device join same-provider activation",
1168 self.store.provider_requests(),
1169 );
1170 let outcome = async {
1171 let mut writer = timings
1172 .stage("authorize writer", self.store.authorize_writer())
1173 .await
1174 .map_err(DeviceJoinError::from)?;
1175 let approval = timings
1176 .stage(
1177 "authorize provider access",
1178 writer
1179 .join_operation()
1180 .authorize_access(request, access_administrator),
1181 )
1182 .await?;
1183 let registration = DeviceRegistrationRequest::same_principal(approval)
1184 .map_err(DeviceJoinError::from)?;
1185 timings
1186 .stage(
1187 "activate the join",
1188 writer
1189 .join_operation()
1190 .activate_same_principal_join(registration),
1191 )
1192 .await
1193 .map_err(DeviceJoinTransportError::from)
1194 }
1195 .await;
1196 timings.report();
1197 outcome
1198 }
1199
1200 async fn approve_access_request(
1201 &self,
1202 request: &DeviceProviderAccessRequest,
1203 policy: &DeviceJoinApprovalPolicy<'_>,
1204 ) -> Result<(), DeviceJoinTransportError> {
1205 let offer = &self.bundle.offer;
1206 let approval = match policy {
1207 DeviceJoinApprovalPolicy::AutoApproveSelfIssued => {
1208 if self.self_issued().await? && request.offer.as_ref() == offer {
1209 DeviceJoinApproval::Approve
1210 } else {
1211 DeviceJoinApproval::Refuse
1212 }
1213 }
1214 DeviceJoinApprovalPolicy::Ask(ask) => ask(request),
1215 };
1216 match approval {
1217 DeviceJoinApproval::Approve => Ok(()),
1218 DeviceJoinApproval::Refuse => Err(DeviceJoinError::OfferMismatch.into()),
1219 }
1220 }
1221
1222 async fn self_issued(&self) -> Result<bool, DeviceJoinTransportError> {
1231 Ok(self.owner_status().await?.is_some())
1232 }
1233}
1234
1235const ACTIVATION_CONFLICT_RETRIES: usize = 8;
1243const ACTIVATION_CONFLICT_BACKOFF: Duration = Duration::from_millis(25);
1244
1245fn is_activation_conflict(error: &DeviceJoinTransportError) -> bool {
1248 matches!(
1249 error,
1250 DeviceJoinTransportError::DeviceJoin(DeviceJoinError::Outbound(
1251 crate::sync::store::StoreError::ActivationConflict
1252 ))
1253 )
1254}
1255
1256async fn retrying_activation_conflicts<Pass, Fut, T>(
1265 mut pass: Pass,
1266) -> Result<T, DeviceJoinTransportError>
1267where
1268 Pass: FnMut() -> Fut,
1269 Fut: std::future::Future<Output = Result<T, DeviceJoinTransportError>>,
1270{
1271 for attempt in 0..ACTIVATION_CONFLICT_RETRIES {
1274 match Box::pin(pass()).await {
1275 Err(error) if is_activation_conflict(&error) => {
1276 tokio::time::sleep(ACTIVATION_CONFLICT_BACKOFF * (attempt as u32 + 1)).await;
1277 }
1278 settled => return settled,
1279 }
1280 }
1281 Box::pin(pass()).await
1282}
1283
1284impl From<coven_database::DeviceJoinJournalError> for DeviceJoinTransportError {
1285 fn from(error: coven_database::DeviceJoinJournalError) -> Self {
1286 DeviceJoinTransportError::from(super::DeviceJoinError::from(error))
1287 }
1288}
1289
1290#[cfg(test)]
1291#[path = "transport_tests.rs"]
1292mod tests;