Skip to main content

coven_sync/
operation_types.rs

1//! Application values for operations, members, circles and invitations (api.md).
2
3use coven_crypto::SecretText;
4use coven_format::admin::MemberRole;
5use coven_foundation::id_source::{InviteId, MemberId};
6use std::time::{Duration, SystemTime};
7
8pub use coven_database::{
9    Circle, CircleMemberInfo, DeviceInfo, DeviceState, MemberInfo, OperationId,
10};
11/// An operation failure retains the same typed causes as an ordinary sync call.
12pub type OperationError = crate::SyncError;
13
14/// The purpose of an unfinished operation, including maintenance.
15#[derive(
16    Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize,
17)]
18pub enum OperationKind {
19    /// Create the store's first slot.
20    CreateStore,
21    /// Set the store name.
22    RenameStore,
23    /// Register this installation.
24    AddDevice,
25    /// Set a member's role.
26    SetRole,
27    /// Replace provider access.
28    SetAccess,
29    /// Remove a member.
30    RemoveMember,
31    /// Remove an installation.
32    RemoveDevice,
33    /// Rename a circle.
34    RenameCircle,
35    /// Remove a circle member.
36    RemoveCircleMember,
37    /// Advance the floor and delete the covered prefix.
38    RetainSequence,
39    /// Delete unreferenced files after establishing retained-object protection.
40    RetainFiles,
41    /// Write and verify a maintenance snapshot.
42    WriteSnapshot,
43    /// Publish the app's migrated schema and its audience snapshots.
44    SchemaChange,
45    /// Snapshot this device and reset one audience to that state.
46    Reset,
47    /// Reload snapshots at the app's request, keeping waiting writes.
48    ReloadFromSnapshot,
49    /// Reload what sync requires: a changed schema or reset, a returning
50    /// circle, joining or recovery.
51    RequiredReload,
52    /// Publish a fresh audience key and its sealed copies.
53    RotateKey,
54    /// Send an older audience key to a current member whose sealing key has
55    /// no copy of it.
56    SendKeyCopy,
57    /// Create a circle and its first key.
58    CreateCircle,
59    /// Seal a circle's history to a member.
60    AddCircleMember,
61    /// Publish a circle's deletion; its rows are taken out from that slot
62    /// (§15.9).
63    DeleteCircle,
64    /// Share storage and settle an invitation.
65    Invite,
66    /// Align one provider account's grants with members and open invitations.
67    RevokeAccess,
68    /// Replace a member's signing and sealing pair: this member's own, with a
69    /// candidate kept in identity custody, or a prepared pair as an admin (§10).
70    ReplaceMemberKeys,
71    /// Set up storage on this device: check the provider, then create the
72    /// store or reconnect to it (§11.6, §20.8).
73    StorageSetup,
74}
75
76impl OperationKind {
77    /// Coven's own work, which no app call retries or discards: a failed
78    /// attempt retries under the shared backoff (§11, E5).
79    pub(crate) fn coven_owned(self) -> bool {
80        matches!(
81            self,
82            Self::WriteSnapshot
83                | Self::RetainSequence
84                | Self::RetainFiles
85                | Self::RequiredReload
86                | Self::RotateKey
87                | Self::SendKeyCopy
88                | Self::RevokeAccess
89        )
90    }
91}
92
93/// The admin's choice, fixed before the invite is shown (§13.2, §13.3).
94#[derive(Clone, Debug, PartialEq, Eq)]
95pub enum InviteTarget {
96    /// A new person: coven generates a fresh member id, added with this role.
97    NewMember {
98        /// The role the person receives on approval.
99        role: MemberRole,
100    },
101    /// An existing active member who lost every device: approval binds the
102    /// requester's new keys to this id, keeping its role, circles and devices.
103    ReplaceMemberKeys {
104        /// The member whose keys the approval replaces.
105        member: MemberId,
106    },
107}
108
109/// How the invited person reaches the store's provider.
110#[derive(Clone)]
111pub enum InviteAccess {
112    /// Share with the named provider account.
113    ProviderAccount {
114        /// The account's email address.
115        email: String,
116    },
117    /// Credentials created in the S3 console for this invite.
118    S3AccessKey {
119        /// Public key identifier.
120        access_key_id: String,
121        /// The key's secret, carried only in the invitation.
122        secret_access_key: SecretText,
123    },
124    /// The S3 key this device connects with, which coven reads from its
125    /// custody, so the secret never reaches the app (§14.3).
126    HeldS3AccessKey,
127}
128
129/// How long an invite stays open after it is made, by the inviting
130/// device's clock (§13.2).
131#[derive(Clone, Copy, Debug, PartialEq, Eq)]
132pub enum InviteLifetime {
133    /// Seven days.
134    SevenDays,
135    /// Thirty days.
136    ThirtyDays,
137}
138
139impl InviteLifetime {
140    pub(crate) fn duration(self) -> Duration {
141        Duration::from_secs(match self {
142            Self::SevenDays => 7 * 86_400,
143            Self::ThirtyDays => 30 * 86_400,
144        })
145    }
146}
147
148/// One of this device's open invites (§13.2).
149#[derive(Clone, Debug, PartialEq, Eq)]
150pub struct Invite {
151    /// The one-time request path's identity.
152    pub id: InviteId,
153    /// The invite's operation, which names its pending record in
154    /// `coven_pending` while its check or its grant waits or is refused.
155    pub operation: coven_database::OperationId,
156    /// The stable member id the invite fixed.
157    pub member: MemberId,
158    /// Whether the invite adds a new person or recovers a member.
159    pub kind: coven_format::codes::InviteKind,
160    /// The access the invite grants: the account shared with, or the id of
161    /// the S3 key it carries, never its secret (§14.3).
162    pub access: coven_format::MemberAccess,
163    /// The invite's lifetime after creation, on the inviting device's clock.
164    pub expires_at: SystemTime,
165    /// Where the invite is: granting its access, waiting for its request or
166    /// the app's decision, or approving its request.
167    pub stage: InviteStage,
168}
169
170/// Where one of this device's open invites is (§13.2).
171#[derive(Clone, PartialEq, Eq)]
172pub enum InviteStage {
173    /// The app's write made the invite. A pass checks it against the
174    /// history it catches up with and grants its storage access, and then
175    /// it shows its code. A check that refuses it, or a failed grant, is
176    /// its pending record under its operation; cancelling ends it.
177    Granting,
178    /// The code is shown, and the request, once one has arrived and been
179    /// checked, waits for the app to approve or decline it.
180    Waiting {
181        /// Code the app may show as a QR code, again whenever the admin
182        /// asks. Contains secrets.
183        code: String,
184        /// The checked request, once one has arrived.
185        request: Option<JoinRequest>,
186    },
187    /// Approval of this request has begun, and finishes without the app:
188    /// approving, declining or cancelling the invite fail with
189    /// `InvitationChanged`. The approval is the app's own, and
190    /// `coven_operations` lists its binding until the app acknowledges the
191    /// outcome.
192    Approving {
193        /// The code first shown, which the joining device scanned.
194        /// Contains secrets.
195        code: String,
196        /// The request being approved.
197        request: JoinRequest,
198        /// The operation that claims the request's copies and binds its
199        /// pair.
200        binding: coven_database::OperationId,
201    },
202}
203
204impl std::fmt::Debug for InviteStage {
205    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
206        match self {
207            Self::Granting => f.write_str("Granting"),
208            Self::Waiting { request, .. } => f
209                .debug_struct("Waiting")
210                .field("code", &"[REDACTED]")
211                .field("request", request)
212                .finish(),
213            Self::Approving { request, binding, .. } => f
214                .debug_struct("Approving")
215                .field("code", &"[REDACTED]")
216                .field("request", request)
217                .field("binding", binding)
218                .finish(),
219        }
220    }
221}
222
223/// A checked request, presented with the account from its local invitation.
224#[derive(Clone, Debug, PartialEq, Eq)]
225pub struct JoinRequest {
226    /// Invitation being answered.
227    pub invite: InviteId,
228    /// The stable member id the invite fixed and the request repeats.
229    pub member: MemberId,
230    /// The fresh public pair the request's signature proves it holds;
231    /// approval binds exactly this pair to `member` (§13.2).
232    pub keys: coven_crypto::MemberPublicKeys,
233    /// The device's name, covered by its signature.
234    pub device_name: String,
235    /// Who asks: a new person with the name they typed, or a recovery. It
236    /// matches the invite's kind (§13.2).
237    pub joiner: coven_format::objects::Joiner,
238    /// Account invited by the owner, absent on S3.
239    pub provider_account_email: Option<String>,
240    /// Computed from the invite id and `keys`; the requester's device shows
241    /// the same number (format.md D11).
242    pub confirmation: coven_crypto::ConfirmationNumber,
243    pub(crate) record: Vec<u8>,
244}