1use super::{SequenceSync, Session};
5use crate::sequence_sync::sequence_operations::SequenceOperation;
6use crate::sequence_sync::OperationState;
7use crate::{
8 Invite, InviteAccess, InviteLifetime, InviteTarget, JoinRequest, OperationKind,
9 OperationOutcome, OperationRejection, PendingReason, PendingRecord, PendingSubject,
10 ProviderAccessAction, SyncError,
11};
12use approval::Child;
13use coven_crypto::{ConfirmationNumber, InviteSecret, MemberIdentity, SecretText, StoreKeyring};
14use coven_database::{
15 operation_kind, DbError, Journal, NewOperation, OperationId, OperationRecord, SqlContext,
16};
17use coven_format::{
18 admin::{MemberRole, StoreChange},
19 codes::InviteCode,
20 invite_end::InviteEnd,
21 MemberAccess, Object,
22};
23use coven_foundation::id_source::{DeviceId, IdSource, InviteId, MemberId};
24use coven_merge::Audience;
25use coven_storage::{
26 CloudProvider, InviteStorage, ObjectPath, S3Credentials, StorageFailure, StorageInvitation,
27};
28use serde::{Deserialize, Serialize};
29use std::{collections::BTreeSet, time::SystemTime};
30
31#[derive(Serialize, Deserialize)]
33pub(crate) enum InviteGrant {
34 Account { email: String },
36 S3Key(S3Credentials),
38}
39
40impl InviteGrant {
41 fn member_access(&self) -> MemberAccess {
42 match self {
43 Self::Account { email } => MemberAccess::ProviderAccount(email.clone()),
44 Self::S3Key(key) => {
45 MemberAccess::S3AccessKey { access_key_id: key.access_key_id.clone() }
46 }
47 }
48 }
49}
50
51#[derive(Clone, Copy, Serialize, Deserialize)]
53pub(crate) enum InviteBinding {
54 Add { role: MemberRole },
56 Replace {
59 #[serde(with = "crate::pending_encoding::public_keys")]
60 expected: coven_crypto::MemberPublicKeys,
61 },
62}
63
64#[derive(Serialize, Deserialize)]
65pub(crate) enum InviteState {
66 Requested,
69 Sharing { share: OperationId },
72 Grant,
74 Waiting {
76 code: SecretText,
79 request: Option<Vec<u8>>,
80 },
81 Approving { code: SecretText, request: Vec<u8>, binding: OperationId },
84 Settled {
94 share: Option<OperationId>,
95 end: Option<OperationId>,
96 refusal: Option<coven_foundation::sharing::SharingRefusal>,
97 },
98}
99
100#[derive(Serialize, Deserialize)]
101pub(crate) struct InviteWork {
102 id: InviteId,
103 device: DeviceId,
107 member: MemberId,
108 inviter: String,
110 binding: InviteBinding,
111 access: InviteGrant,
112 secret: SecretText,
113 expires_at: std::time::SystemTime,
114 state: InviteState,
115}
116
117impl OperationState for InviteWork {}
118
119impl InviteWork {
120 fn read(record: &OperationRecord) -> Result<Self, SyncError> {
121 Ok(serde_json::from_slice(&record.data)?)
122 }
123 fn refusal(&self) -> Option<coven_foundation::sharing::SharingRefusal> {
125 match &self.state {
126 InviteState::Settled { refusal, .. } => *refusal,
127 _ => None,
128 }
129 }
130 fn secret(&self) -> Result<InviteSecret, SyncError> {
131 Ok(InviteSecret::from_bytes(serde_json::from_str(self.secret.as_str())?))
132 }
133 fn kind(&self) -> coven_format::codes::InviteKind {
136 match self.binding {
137 InviteBinding::Add { .. } => coven_format::codes::InviteKind::NewPerson,
138 InviteBinding::Replace { .. } => coven_format::codes::InviteKind::Recovery,
139 }
140 }
141 fn account(&self) -> Option<&str> {
142 match &self.access {
143 InviteGrant::Account { email } => Some(email),
144 _ => None,
145 }
146 }
147 fn expires(&self) -> bool {
151 matches!(
152 self.state,
153 InviteState::Requested
154 | InviteState::Sharing { .. }
155 | InviteState::Grant
156 | InviteState::Waiting { .. }
157 )
158 }
159 fn expired(&self, now: std::time::SystemTime) -> bool {
160 self.expires() && now >= self.expires_at
161 }
162
163 fn end(&mut self) {
166 let share = match self.state {
167 InviteState::Sharing { share } => Some(share),
168 _ => None,
169 };
170 self.state = InviteState::Settled { share, end: None, refusal: None };
171 }
172
173 fn naming(
177 record: &OperationRecord,
178 state: impl FnOnce(OperationId) -> InviteState + Send + 'static,
179 ) -> impl FnOnce(OperationId) -> Result<coven_database::OperationUpdate, SyncError> + Send + 'static
180 {
181 let record = record.clone();
182 move |child| {
183 let mut work = InviteWork::read(&record)?;
184 work.state = state(child);
185 work.update(&record)
186 }
187 }
188
189 fn binding_change(&self, request: &[u8]) -> Result<StoreChange, SyncError> {
193 let request = decode_request(request)?;
194 let (keys, device, device_name) = (request.keys, request.device, request.device_name);
195 Ok(match self.binding {
196 InviteBinding::Add { role } => StoreChange::AdmitMember {
197 member: self.member,
198 keys,
199 role,
200 access: self.access.member_access(),
201 device,
202 device_name,
203 },
204 InviteBinding::Replace { expected } => StoreChange::RecoverMember {
208 member: self.member,
209 expected,
210 keys,
211 access: match &self.access {
212 InviteGrant::S3Key(_) => Some(self.access.member_access()),
213 InviteGrant::Account { .. } => None,
214 },
215 device,
216 device_name,
217 },
218 })
219 }
220
221 fn shown(&self, operation: OperationId, stage: crate::InviteStage) -> Invite {
224 Invite {
225 id: self.id,
226 operation,
227 member: self.member,
228 kind: self.kind(),
229 access: self.access.member_access(),
230 expires_at: self.expires_at,
231 stage,
232 }
233 }
234
235 fn present_request(&self, bytes: &[u8]) -> Result<JoinRequest, SyncError> {
236 let request = decode_request(bytes)?;
237 Ok(JoinRequest {
238 invite: request.invite,
239 member: request.member,
240 confirmation: ConfirmationNumber::of(request.invite, &request.keys),
241 keys: request.keys,
242 device_name: request.device_name,
243 joiner: request.joiner,
244 provider_account_email: self.account().map(str::to_owned),
245 record: bytes.to_vec(),
246 })
247 }
248}
249
250fn listed(data: &[u8], operation: OperationId) -> Result<Invite, SyncError> {
253 let work: InviteWork = serde_json::from_slice(data)?;
254 let shown = |code: &SecretText| code.as_str().to_owned();
255 let stage = match &work.state {
256 InviteState::Requested | InviteState::Sharing { .. } | InviteState::Grant => {
257 crate::InviteStage::Granting
258 }
259 InviteState::Waiting { code, request } => crate::InviteStage::Waiting {
260 code: shown(code),
261 request: request.as_deref().map(|bytes| work.present_request(bytes)).transpose()?,
262 },
263 InviteState::Approving { code, request, binding } => crate::InviteStage::Approving {
264 code: shown(code),
265 request: work.present_request(request)?,
266 binding: *binding,
267 },
268 InviteState::Settled { .. } => {
269 return Err(coven_database::DbError::Invariant {
270 what: "coven_invites lists invites that haven't ended",
271 }
272 .into())
273 }
274 };
275 Ok(work.shown(operation, stage))
276}
277
278impl Invite {
279 pub fn list(sql: coven_database::SqlReadContext<'_>) -> Result<Vec<Self>, DbError> {
283 Ok(sql.query(
284 "SELECT invite,operation FROM coven_invites ORDER BY operation",
285 [],
286 |row| {
287 let data: Vec<u8> = row.get(0)?;
288 listed(&data, OperationId(row.get(1)?)).map_err(|error| {
289 coven_database::SqliteError::FromSqlConversionFailure(
290 0,
291 coven_database::types::Type::Blob,
292 Box::new(error),
293 )
294 })
295 },
296 )?)
297 }
298
299 #[cfg(any(test, feature = "test-utils"))]
301 pub fn shown_code(&self) -> &str {
302 match &self.stage {
303 crate::InviteStage::Granting => panic!("the invite shows no code before its grant"),
304 crate::InviteStage::Waiting { code, .. }
305 | crate::InviteStage::Approving { code, .. } => code,
306 }
307 }
308
309 #[cfg(any(test, feature = "test-utils"))]
311 pub fn shown_request(&self) -> Option<&JoinRequest> {
312 match &self.stage {
313 crate::InviteStage::Granting => None,
314 crate::InviteStage::Waiting { request, .. } => request.as_ref(),
315 crate::InviteStage::Approving { request, .. } => Some(request),
316 }
317 }
318
319 #[cfg(any(test, feature = "test-utils"))]
321 pub fn follow(
322 database: &coven_database::Database,
323 ) -> coven_database::test_utils::Followed<Vec<Self>, DbError> {
324 coven_database::test_utils::Followed::new(database.subscribe(Self::list))
325 }
326}
327
328impl SequenceSync {
329 #[cfg(test)]
333 pub(crate) async fn create_invite(
334 &self,
335 target: InviteTarget,
336 access: InviteAccess,
337 lifetime: InviteLifetime,
338 ) -> Result<Invite, SyncError> {
339 let inviter = "Ana".to_owned();
340 let steps = self.operation_steps();
341 let location = self.storage.config().clone();
342 coven_storage::record_config(&self.directory, &location)?;
343 let (invite, _, operation) = self
344 .database
345 .write_as(move |sql| {
346 let made = NewInvite { target, access, lifetime, inviter };
347 steps.create_invite(&sql, made)
348 })
349 .await?;
350 self.pass().await?;
351 let shown = self.database.read(Invite::list).await?.into_iter().find(|listed| {
352 listed.id == invite && !matches!(listed.stage, crate::InviteStage::Granting)
353 });
354 if let Some(shown) = shown {
355 return Ok(shown);
356 }
357 match self.database.operation_pending::<PendingRecord>(operation).await? {
358 Some(pending) => Err(SyncError::Pending(Box::new(pending))),
359 None => Err(SyncError::InvitationChanged),
360 }
361 }
362
363 #[cfg(test)]
366 pub(crate) async fn approve_join_request(
367 &self,
368 request: &JoinRequest,
369 ) -> Result<(), SyncError> {
370 let request = request.clone();
371 let steps = self.operation_steps();
372 let outcome = self
373 .journal_and_pass(move |sql| steps.approve_join_request(sql, &request).map(Some))
374 .await?
375 .expect("an approval always journals its binding");
376 crate::operation_outcome::into_slot(outcome).map(drop)
377 }
378
379 #[cfg(test)]
382 pub(crate) async fn decline_join_request(
383 &self,
384 request: &JoinRequest,
385 ) -> Result<(), SyncError> {
386 let request = request.clone();
387 let steps = self.operation_steps();
388 self.settle_and_pass(move |sql| steps.decline_join_request(sql, &request)).await
389 }
390
391 #[cfg(test)]
394 pub(crate) async fn cancel_invite(&self, invite: InviteId) -> Result<(), SyncError> {
395 let steps = self.operation_steps();
396 self.settle_and_pass(move |sql| steps.cancel_invite(sql, invite)).await
397 }
398
399 #[cfg(test)]
400 async fn settle_and_pass(
401 &self,
402 step: impl FnOnce(&SqlContext<'_, '_>) -> Result<(), SyncError> + Send + 'static,
403 ) -> Result<(), SyncError> {
404 self.database.write_as(move |sql| step(&sql)).await?;
405 self.pass().await.map(drop)
406 }
407
408 pub(crate) async fn resume_invites(
411 &self,
412 session: &mut Session,
413 ring: &mut StoreKeyring,
414 member: &MemberIdentity,
415 ) -> Result<(), SyncError> {
416 for record in self.database.operations().await? {
417 if record.kind != operation_kind::INVITE {
418 continue;
419 }
420 let expired = InviteWork::read(&record)?.expired(self.clock.now());
422 if !expired && !self.operation_ready(&record, ring).await? {
423 continue;
424 }
425 if let Err(error) = self.run_invite(session, ring, member, record.id).await {
426 if self.database.operation(record.id).await?.is_none() {
427 return Err(error);
428 }
429 }
430 self.state.check()?;
431 }
432 for path in self.database.due_object_removals(seconds(self.clock.now())?).await? {
435 let object = ObjectPath::parse(&path).map_err(|error| {
436 StorageFailure::Protocol.with_source(format!("a recorded removal's path: {error}"))
437 })?;
438 self.storage.delete_if_present(&object).await?;
439 self.database.object_removed(path).await?;
440 }
441 Ok(())
442 }
443
444 async fn advance_invite(
448 &self,
449 session: &mut Session,
450 ring: &mut StoreKeyring,
451 member: &MemberIdentity,
452 id: OperationId,
453 step: &mut Option<u32>,
454 ) -> Result<InviteProgress, SyncError> {
455 loop {
456 let record = self
457 .database
458 .operation(id)
459 .await?
460 .ok_or(coven_database::DbError::OperationChanged(id))?;
461 *step = Some(record.last_step);
462 let mut work = InviteWork::read(&record)?;
463 if work.expired(self.clock.now()) {
464 work.end();
465 self.database.advance_operation(work.update(&record)?).await?;
466 continue;
467 }
468 match &work.state {
469 InviteState::Requested => {
470 if let Err(refusal) = self.check_invite(session, member, &work) {
473 return Ok(InviteProgress::Waiting(self.error_reason(&refusal)));
474 }
475 if let Some(account) = work.account() {
478 let started =
479 self.start_invite_share(session, member, &record, &work, account);
480 match started.await? {
481 Child::Journaled(_) => continue,
482 Child::Decided(OperationOutcome::Rejected(reason)) => {
483 return Ok(InviteProgress::Waiting(
484 self.error_reason(&SyncError::Rejected(reason)),
485 ))
486 }
487 Child::Decided(_) => (),
488 }
489 }
490 self.grant(session, ring, member, &record, work, None, step).await?;
491 }
492 InviteState::Sharing { share } => {
493 let share = *share;
494 let outcome = self.run_administration(session, ring, member, share).await?;
495 match outcome {
496 OperationOutcome::Landed(_) | OperationOutcome::Satisfied => {
497 let shared = Some((share, outcome));
498 self.grant(session, ring, member, &record, work, shared, step).await?;
499 }
500 OperationOutcome::Rejected(_) => {
502 work.state = InviteState::Requested;
503 let update = work.update(&record)?;
504 self.database
505 .finish_operation_advancing(share, outcome, update)
506 .await?;
507 }
508 OperationOutcome::Stopped(_) => {
509 return Err(coven_database::DbError::Invariant {
510 what: "a stop for good ends the invite with its claims",
511 }
512 .into())
513 }
514 OperationOutcome::Cancelled | OperationOutcome::Undone(_) => {
515 return Err(coven_database::DbError::Invariant {
516 what: "only storage setup is cancelled or undone",
517 }
518 .into())
519 }
520 OperationOutcome::SharingRefused { .. } => {
521 return Err(coven_database::DbError::Invariant {
522 what: "only an invite ends with a sharing refusal",
523 }
524 .into())
525 }
526 }
527 }
528 InviteState::Grant => {
529 let invitation = match &work.access {
530 InviteGrant::Account { email } => {
531 let access = self
532 .database
533 .provider_access(email.clone())
534 .await?
535 .ok_or(coven_database::DbError::DamagedDatabase)?;
536 if access.request.is_some() || access.granted != Some(true) {
537 let failed = match &access.request {
540 Some(request) => {
541 self.database
542 .operation_pending::<PendingRecord>(request.operation)
543 .await?
544 }
545 None => None,
546 };
547 if let Some(PendingRecord {
548 reason:
549 PendingReason::Storage {
550 failure: StorageFailure::SharingRefused(refusal),
551 ..
552 },
553 ..
554 }) = &failed
555 {
556 let grant = access
557 .request
558 .as_ref()
559 .ok_or(coven_database::DbError::DamagedDatabase)?
560 .operation;
561 self.end_refused(id, grant, *refusal).await?;
562 continue;
563 }
564 return Ok(InviteProgress::Waiting(failed.map_or(
565 PendingReason::ProviderPending {
566 action: ProviderAccessAction::Grant,
567 account: email.clone(),
568 },
569 |failed| failed.reason,
570 )));
571 }
572 StorageInvitation::decode(
573 access
574 .invitation
575 .as_ref()
576 .ok_or(coven_database::DbError::DamagedDatabase)?
577 .as_bytes(),
578 )?
579 }
580 InviteGrant::S3Key(_) => {
581 StorageInvitation::for_account(self.storage.config())?
582 }
583 };
584 let code = self.invite_code(session, &work, invitation)?;
585 work.state = InviteState::Waiting { code, request: None };
586 self.database.advance_operation(work.update(&record)?).await?;
587 self.state.request_pass();
589 return Ok(InviteProgress::Shown);
590 }
591 InviteState::Waiting { request: Some(_), .. } => {
592 return Ok(InviteProgress::Waiting(PendingReason::Paused));
593 }
594 InviteState::Waiting { request: None, code } => {
595 let path = ObjectPath::join_request(self.store(), work.device, work.id);
596 let bytes = match self.storage.read(&path).await {
597 Ok(bytes) => bytes,
598 Err(error) if error.failure() == StorageFailure::NotFound => {
599 return Ok(InviteProgress::Waiting(PendingReason::Missing { path }));
600 }
601 Err(error) => return Err(error.into()),
602 };
603 let request = match self.open_request(&work, &session.history.state, &bytes)? {
604 Opened::Checked(request) => request,
605 Opened::Refused(refusal) => {
609 tracing::warn!(?id, %refusal, "a join request failed its checks");
610 work.end();
611 self.database.advance_operation(work.update(&record)?).await?;
612 continue;
613 }
614 };
615 work.state =
618 InviteState::Waiting { code: code.clone(), request: Some(request) };
619 self.database.advance_operation(work.update(&record)?).await?;
620 return Ok(InviteProgress::Waiting(PendingReason::Paused));
621 }
622 InviteState::Approving { binding, .. } => {
623 let binding = *binding;
624 if self.database.operation(binding).await?.is_none() {
627 return Err(coven_database::DbError::Invariant {
628 what: "an approving invite's binding ends with its step",
629 }
630 .into());
631 }
632 let outcome = self.run_administration(session, ring, member, binding).await?;
633 work.state = InviteState::Settled { share: None, end: None, refusal: None };
637 self.database
638 .finish_operation_advancing(binding, outcome, work.update(&record)?)
639 .await?;
640 }
641 InviteState::Settled { share, end, refusal } => {
642 let (share, end, refusal) = (*share, *end, *refusal);
643 let kept_until = self.end_request(&work).await?;
644 if let Some(share) = share {
648 self.settle_unneeded_claim(session, ring, member, share).await?;
649 }
650 let end = match end {
653 Some(end) => Some(end),
654 None => {
655 self.start_share_end(session, member, &record, &work, share).await?
656 }
657 };
658 if let Some(end) = end {
659 let outcome = self.run_administration(session, ring, member, end).await?;
660 if let OperationOutcome::Rejected(reason) = &outcome {
661 tracing::warn!(?id, ?reason, "an invite share's end was refused");
662 }
663 }
664 let removal = coven_database::ObjectRemoval {
667 path: ObjectPath::join_request(self.store(), work.device, work.id)
668 .as_str()
669 .to_owned(),
670 after: kept_until,
671 };
672 let children = share.into_iter().chain(end).collect();
673 let outcome = match refusal {
677 Some(refusal) => {
678 let account =
679 work.account().ok_or(coven_database::DbError::DamagedDatabase)?;
680 Some((
681 crate::OperationKind::Invite.listed(),
682 OperationOutcome::SharingRefused {
683 account: account.to_owned(),
684 refusal,
685 },
686 ))
687 }
688 None => None,
689 };
690 self.database
691 .finish_operation_with_removal(id, children, removal, outcome)
692 .await?;
693 return Ok(InviteProgress::Finished);
694 }
695 }
696 }
697 }
698
699 #[expect(
706 clippy::too_many_arguments,
707 reason = "a step of the invite's advance takes the pass's session, keys and member"
708 )]
709 async fn grant(
710 &self,
711 session: &mut Session,
712 ring: &mut StoreKeyring,
713 member: &MemberIdentity,
714 record: &OperationRecord,
715 mut work: InviteWork,
716 shared: Option<(OperationId, OperationOutcome)>,
717 step: &mut Option<u32>,
718 ) -> Result<(), SyncError> {
719 work.state = InviteState::Grant;
720 let changes = match work.account() {
721 Some(account) => vec![crate::sequence_sync::account_change(account, true, true)?],
722 None => Vec::new(),
723 };
724 let update = work.update(record)?;
725 let granting = update.last_step;
726 self.database
727 .advance_operation_with_access::<PendingRecord>(update, changes, shared)
728 .await?;
729 *step = Some(granting);
730 let grant = match work.account() {
731 Some(account) => self
732 .database
733 .provider_access(account.to_owned())
734 .await?
735 .ok_or(coven_database::DbError::DamagedDatabase)?
736 .request
737 .map(|request| request.operation),
738 None => None,
739 };
740 if let Some(grant) = grant {
741 if let Err(error) = self.apply_access(session, ring, member, grant).await {
742 return match sharing_refusal(&error) {
743 Some(refusal) => self.end_refused(record.id, grant, refusal).await,
744 None => Err(error),
745 };
746 }
747 }
748 Ok(())
749 }
750
751 async fn end_refused(
756 &self,
757 id: OperationId,
758 grant: OperationId,
759 refusal: coven_foundation::sharing::SharingRefusal,
760 ) -> Result<(), SyncError> {
761 let record = self
762 .database
763 .operation(id)
764 .await?
765 .ok_or(coven_database::DbError::OperationChanged(id))?;
766 let mut work = InviteWork::read(&record)?;
767 tracing::warn!(?id, ?refusal, "the provider refused to share the store for an invite");
768 let account = work.account().ok_or(coven_database::DbError::DamagedDatabase)?.to_owned();
769 work.state = InviteState::Settled { share: None, end: None, refusal: Some(refusal) };
770 self.database
771 .advance_operation_refusing_grant(work.update(&record)?, account, grant)
772 .await?;
773 Ok(())
774 }
775
776 async fn end_request(&self, work: &InviteWork) -> Result<u64, SyncError> {
782 let kept_until = kept_until(seconds(work.expires_at)?)?;
783 let marker = InviteEnd { kept_until }.encode();
784 let path = ObjectPath::join_request(self.store(), work.device, work.id);
785 loop {
786 self.storage.delete_if_present(&path).await?;
787 match self.storage.create(&path, &marker).await {
788 Ok(_) => return Ok(kept_until),
789 Err(error) if error.failure() == StorageFailure::AlreadyExists => {
790 match self.storage.read(&path).await {
791 Ok(found) if InviteEnd::decode(&found).is_ok() => return Ok(kept_until),
793 Ok(_) => (),
794 Err(error) if error.failure() == StorageFailure::NotFound => (),
795 Err(error) => return Err(error.into()),
796 }
797 }
798 Err(error) => return Err(error.into()),
799 }
800 }
801 }
802
803 fn check_invite(
809 &self,
810 session: &Session,
811 member: &MemberIdentity,
812 work: &InviteWork,
813 ) -> Result<(), SyncError> {
814 let me = member.member();
815 let state = &session.history.state;
816 if !self.registered(session) || !state.active_admin(&me) {
817 return Err(SyncError::Rejected(OperationRejection::NotAllowed));
818 }
819 if !session.history.owns_storage(me) {
820 return Err(StorageFailure::NotStoreOwner.into());
821 }
822 if let InviteBinding::Replace { expected } = work.binding {
823 match state.active_member(&work.member) {
824 None => {
825 return Err(SyncError::Rejected(OperationRejection::TargetGone(
826 crate::EntryTarget::Member(work.member),
827 )))
828 }
829 Some(current) if current.keys != expected => {
830 return Err(SyncError::Rejected(OperationRejection::MemberKeysChanged(
831 work.member,
832 )))
833 }
834 Some(_) => (),
835 }
836 }
837 Ok(())
838 }
839
840 fn invite_code(
843 &self,
844 session: &Session,
845 work: &InviteWork,
846 invitation: StorageInvitation,
847 ) -> Result<SecretText, SyncError> {
848 let storage = match &work.access {
849 InviteGrant::Account { .. } => InviteStorage::Account(invitation),
850 InviteGrant::S3Key(credentials) => {
851 InviteStorage::S3 { invitation, credentials: credentials.clone() }
852 }
853 };
854 let store =
855 session.history.state.store.as_ref().ok_or(coven_database::DbError::DamagedDatabase)?;
856 let initial_key = session
857 .history
858 .introductions
859 .iter()
860 .find(|intro| intro.audience == Audience::Store)
861 .ok_or(coven_database::DbError::DamagedDatabase)?
862 .key;
863 let code = InviteCode {
864 store: store.id,
865 name: store.name.clone(),
866 inviter: work.inviter.clone(),
867 kind: work.kind(),
868 expires_at: seconds(work.expires_at)?,
869 invite: work.id,
870 member: work.member,
871 secret: work.secret()?,
872 initial_key,
873 inviting_writer: work.device,
874 read_through: session.through,
875 storage: storage.encode()?,
876 }
877 .to_text()?;
878 Ok(SecretText::new(code.to_string()))
879 }
880}
881
882pub(crate) enum InviteProgress {
884 Shown,
886 Waiting(PendingReason),
888 Finished,
890}
891
892fn sharing_refusal(error: &SyncError) -> Option<coven_foundation::sharing::SharingRefusal> {
894 match error {
895 SyncError::Storage(error) => match error.failure() {
896 StorageFailure::SharingRefused(refusal) => Some(refusal),
897 _ => None,
898 },
899 _ => None,
900 }
901}
902
903fn kept_until(expires: u64) -> Result<u64, SyncError> {
907 expires.checked_add(super::sequence_retention::GRACE.as_secs()).ok_or_else(|| {
908 StorageFailure::InvalidConfiguration
909 .with_source("the clock cannot represent when an ended invite's marker may go")
910 .into()
911 })
912}
913
914fn seconds(at: SystemTime) -> Result<u64, SyncError> {
917 Ok(at
918 .duration_since(std::time::UNIX_EPOCH)
919 .map_err(|error| {
920 StorageFailure::InvalidConfiguration
921 .with_source(format!("an invite's time precedes 1970: {error}"))
922 })?
923 .as_secs())
924}
925
926fn decode_request(bytes: &[u8]) -> Result<coven_format::objects::JoinRequest, SyncError> {
927 match Object::decode(bytes)? {
928 Object::JoinRequest(request) => Ok(request),
929 _ => Err(SyncError::InvitationChanged),
930 }
931}
932
933#[cfg(test)]
934#[path = "sequence_invites_tests.rs"]
935pub(crate) mod tests;
936
937#[path = "sequence_invite_request.rs"]
938mod request;
939use request::Opened;
940
941#[path = "sequence_invite_approval.rs"]
942mod approval;
943
944#[path = "sequence_invite_reset.rs"]
945mod reset;
946
947#[path = "sequence_invite_shares.rs"]
948pub(crate) mod shares;
949
950#[path = "sequence_invite_steps.rs"]
951mod steps;
952pub use steps::NewInvite;
953pub(crate) use steps::{approve_request, end_invite, make_invite};
954
955#[path = "invite_expiry.rs"]
956pub(crate) mod expiry;