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