1use coven_protocol::store_commit::device_join_exchange::DeviceJoinAbandonment;
5use coven_protocol::store_commit::device_join_journal::{
6 DeviceJoinJournalRecord, DeviceJoinRoleProgress, JoinerJoinProgress, OwnerJoinProgress,
7};
8
9#[derive(Debug, thiserror::Error)]
12pub enum DeviceJoinJournalError {
13 #[error("device join journal transition is not the declared adjacent transition")]
14 NonAdjacentJournalTransition,
15 #[error("device join journal has a different durable value for this role and attempt")]
16 JournalConflict,
17 #[error("device join journal: {0}")]
18 Serialization(#[from] serde_json::Error),
19 #[error("device join journal: {0}")]
20 Database(#[from] crate::DbError),
21}
22
23pub fn validate_initial_progress(
25 progress: &DeviceJoinRoleProgress,
26) -> Result<(), DeviceJoinJournalError> {
27 if matches!(
28 progress,
29 DeviceJoinRoleProgress::Owner(OwnerJoinProgress::Offered(_))
30 | DeviceJoinRoleProgress::Joiner(JoinerJoinProgress::OfferReceived(_))
31 ) {
32 Ok(())
33 } else {
34 Err(DeviceJoinJournalError::NonAdjacentJournalTransition)
35 }
36}
37
38pub fn require_initial(record: &DeviceJoinJournalRecord) -> Result<(), DeviceJoinJournalError> {
39 validate_initial_progress(&record.progress)
40}
41
42pub fn validate_successor(
43 previous: &DeviceJoinJournalRecord,
44 next: &DeviceJoinJournalRecord,
45) -> Result<(), DeviceJoinJournalError> {
46 if previous.attempt_id != next.attempt_id {
47 return Err(DeviceJoinJournalError::JournalConflict);
48 }
49 validate_transition(&previous.progress, &next.progress)
50}
51
52pub fn joiner_abandonment_retires(
62 record: &DeviceJoinJournalRecord,
63 abandonment: &DeviceJoinAbandonment,
64) -> Result<(), DeviceJoinJournalError> {
65 if record.attempt_id != abandonment.abandonment.attempt_id {
66 return Err(DeviceJoinJournalError::JournalConflict);
67 }
68 match &*record.progress {
69 DeviceJoinRoleProgress::Joiner(
70 JoinerJoinProgress::AccessRequested(_) | JoinerJoinProgress::ApprovalReceived(_),
71 ) => Ok(()),
72 _ => Err(DeviceJoinJournalError::JournalConflict),
73 }
74}
75
76fn validate_transition(
77 previous: &DeviceJoinRoleProgress,
78 next: &DeviceJoinRoleProgress,
79) -> Result<(), DeviceJoinJournalError> {
80 let adjacent = match (previous, next) {
81 (DeviceJoinRoleProgress::Owner(previous), DeviceJoinRoleProgress::Owner(next)) => {
82 owner_adjacent(previous, next)
83 }
84 (DeviceJoinRoleProgress::Joiner(previous), DeviceJoinRoleProgress::Joiner(next)) => {
85 joiner_adjacent(previous, next)
86 }
87 _ => false,
88 };
89 if adjacent {
90 Ok(())
91 } else {
92 Err(DeviceJoinJournalError::NonAdjacentJournalTransition)
93 }
94}
95
96fn owner_adjacent(previous: &OwnerJoinProgress, next: &OwnerJoinProgress) -> bool {
106 if let (
107 OwnerJoinProgress::AccessRequested(request),
108 OwnerJoinProgress::ApprovalPrepared(approval),
109 ) = (previous, next)
110 {
111 return approval.request.as_ref() == request
112 && matches!(
113 approval.admission,
114 coven_protocol::store_commit::device_join_exchange::DeviceProviderAdmission::SamePrincipal
115 );
116 }
117 if let (
118 OwnerJoinProgress::ProviderReady(ready),
119 OwnerJoinProgress::Completed(
120 coven_protocol::store_commit::device_join_exchange::DeviceProviderAdmissionCompletion::SamePrincipal {
121 bootstrap,
122 },
123 ),
124 ) = (previous, next)
125 {
126 return ready == bootstrap.as_ref();
127 }
128 if let OwnerJoinProgress::StorePublicationPrepared(prepared) = previous {
129 return prepared
130 .validates_accepted_progress(prepared_operation_attempt_id(&prepared.operation), next);
131 }
132 if let OwnerJoinProgress::StorePublicationPrepared(prepared) = next {
133 return owner_publication_follows(previous, &prepared.operation);
134 }
135 matches!(
136 (previous, next),
137 (
138 OwnerJoinProgress::Offered(_),
139 OwnerJoinProgress::AccessRequested(_)
140 ) | (
141 OwnerJoinProgress::AccessGrantActivated { .. },
142 OwnerJoinProgress::ApprovalPrepared(_)
143 ) | (
144 OwnerJoinProgress::ApprovalPrepared(_),
145 OwnerJoinProgress::RegistrationRequested(_)
146 ) | (
147 OwnerJoinProgress::SamePrincipalActivated { .. },
148 OwnerJoinProgress::SamePrincipalCompleted { .. }
149 ) | (
150 OwnerJoinProgress::AttemptActivated(_),
151 OwnerJoinProgress::ChallengeCreateIntent(_)
152 ) | (
153 OwnerJoinProgress::ChallengeCreateIntent(_),
154 OwnerJoinProgress::ProviderReady(_)
155 ) | (
156 OwnerJoinProgress::ProviderReady(_),
157 OwnerJoinProgress::ResponseObserved(_)
158 ) | (
159 OwnerJoinProgress::ResponseObserved(_),
160 OwnerJoinProgress::Completed(_)
161 )
162 )
163}
164
165fn owner_publication_follows(
166 previous: &OwnerJoinProgress,
167 operation: &coven_protocol::store_commit::device_join_journal::OwnerJoinPublication,
168) -> bool {
169 use coven_protocol::store_commit::device_join_journal::OwnerJoinPublication;
170
171 match (previous, operation) {
172 (
173 OwnerJoinProgress::AccessRequested(previous),
174 OwnerJoinPublication::ProviderAccessGrant { request, .. },
175 ) => previous == request,
176 (
177 OwnerJoinProgress::RegistrationRequested(previous),
178 OwnerJoinPublication::Attempt { request }
179 | OwnerJoinPublication::SamePrincipalActivation { request },
180 ) => previous == request,
181 (
182 OwnerJoinProgress::Completed(previous),
183 OwnerJoinPublication::JoinActivation { completion },
184 ) => previous == completion,
185 (previous, OwnerJoinPublication::Abandonment { offer, .. }) => {
186 let durable_offer = match previous {
187 OwnerJoinProgress::Offered(durable) => Some(durable),
188 OwnerJoinProgress::AccessRequested(request) => Some(request.offer.as_ref()),
189 OwnerJoinProgress::AccessGrantActivated { request, .. } => {
190 Some(request.offer.as_ref())
191 }
192 OwnerJoinProgress::ApprovalPrepared(approval) => {
193 Some(approval.request.offer.as_ref())
194 }
195 OwnerJoinProgress::RegistrationRequested(request) => {
196 Some(request.approval().request.offer.as_ref())
197 }
198 _ => None,
199 };
200 durable_offer == Some(offer)
201 }
202 _ => false,
203 }
204}
205
206fn prepared_operation_attempt_id(
207 operation: &coven_protocol::store_commit::device_join_journal::OwnerJoinPublication,
208) -> coven_protocol::store_commit::DeviceJoinAttemptId {
209 use coven_protocol::store_commit::device_join_journal::OwnerJoinPublication;
210 match operation {
211 OwnerJoinPublication::ProviderAccessGrant { request, .. } => request.offer.attempt_id,
212 OwnerJoinPublication::Attempt { request }
213 | OwnerJoinPublication::SamePrincipalActivation { request } => {
214 request.approval().request.offer.attempt_id
215 }
216 OwnerJoinPublication::Abandonment { offer, .. } => offer.attempt_id,
217 OwnerJoinPublication::JoinActivation { completion } => completion.attempt_id(),
218 }
219}
220
221fn joiner_adjacent(previous: &JoinerJoinProgress, next: &JoinerJoinProgress) -> bool {
222 matches!(
223 (previous, next),
224 (
225 JoinerJoinProgress::OfferReceived(_),
226 JoinerJoinProgress::AccessRequested(_)
227 ) | (
228 JoinerJoinProgress::AccessRequested(_),
229 JoinerJoinProgress::ApprovalReceived(_)
230 ) | (
231 JoinerJoinProgress::ApprovalReceived(_),
232 JoinerJoinProgress::RegistrationPrepared(_)
233 ) | (
234 JoinerJoinProgress::RegistrationPrepared(_),
235 JoinerJoinProgress::Ready(_)
236 ) | (
237 JoinerJoinProgress::Ready(_),
238 JoinerJoinProgress::ActivationObserved { .. }
239 )
240 )
241}