Skip to main content

derec_library/protocol/events/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 DeRec Alliance. All rights reserved.
3
4#[cfg(any(feature = "ffi", target_arch = "wasm32"))]
5pub(crate) mod wire;
6
7use std::collections::HashMap;
8
9use crate::{
10    primitives::discovery::response::SecretVersionEntry,
11    protocol::types::{Target, UserSecret},
12    types::{ChannelId, SharedKey},
13};
14use derec_proto::{
15    ContactMessage, GetSecretIdsVersionsRequestMessage, GetShareRequestMessage, PairRequestMessage,
16    PrePairRequestMessage, SenderKind, StoreShareRequestMessage, TransportProtocol,
17    UnpairRequestMessage, UpdateChannelInfoRequestMessage, VerifyShareRequestMessage,
18};
19
20/// Lightweight discriminant of [`PendingAction`].
21///
22/// Carries no payload — useful for the
23/// [`DeRecEvent::AutoAccepted`] event (so listeners can route on
24/// "what flow just got auto-accepted" without holding the action's
25/// inner request/key material) and for the
26/// [`AutoAcceptPolicy::allows`] dispatch.
27#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
28pub enum PendingActionKind {
29    Pairing,
30    PrePair,
31    StoreShare,
32    VerifyShare,
33    Discovery,
34    GetShare,
35    Unpair,
36    UpdateChannelInfo,
37}
38
39/// Per-flow opt-in for auto-accepting incoming requests.
40///
41/// Default = every field `false` (no flow auto-accepted; `process()`
42/// emits [`DeRecEvent::ActionRequired`] like today). When a field is
43/// `true`, `process()` invokes the equivalent of
44/// [`super::DeRecProtocol::accept`] internally and emits
45/// [`DeRecEvent::AutoAccepted`] **in place of**
46/// [`DeRecEvent::ActionRequired`], followed by the same flow events
47/// the caller would have seen from a manual `accept`.
48///
49/// Wire the policy through
50/// [`super::DeRecProtocolBuilder::with_auto_accept`].
51///
52/// # Per-flow notes
53///
54/// - [`Self::pairing`] covers both standard and replica pairing.
55///   Standard pairing transitions the channel to `Paired` immediately;
56///   auto-accepting it skips any UI confirmation of "User X wants to
57///   pair." Replica pairing leaves the channel in `Pending` until both
58///   sides run `verify_fingerprint()`, so auto-accept here is benign
59///   (channel is inert until the out-of-band fingerprint match).
60/// - [`Self::pre_pair`] gates the plaintext `PrePair` leg of HashedKeys
61///   pairing. The MITM defence is on the *scanner* side
62///   (binding-hash check) and is unaffected by this flag, but enabling
63///   it turns the initiator into a request-amplification oracle: any
64///   party that knows the contact's `nonce` + `channel_id` can elicit
65///   a key-publish response. Prefer to keep this off unless you
66///   control both ends of the transport (LAN, integration tests).
67/// - [`Self::unpair`] is destructive — accepting deletes the local
68///   channel record and any shares/secrets associated with it. The
69///   [`DeRecEvent::Unpaired`] event still fires (after the deletion),
70///   so observability is preserved, but the user has no chance to
71///   intervene before the data is gone. Apps that want a "are you
72///   sure?" gate should keep this off.
73/// - [`Self::update_channel_info`] silently overwrites the stored
74///   channel record with the peer's new transport / communication
75///   info. Cryptographically safe (only the paired peer can send it),
76///   but a compromised peer key gets weaponised faster — outbound
77///   traffic on the channel re-routes to whatever endpoint the peer
78///   announced.
79///
80/// All other fields wrap routine request/response flows and have no
81/// security-sensitive caveats beyond "the caller decided not to gate
82/// them."
83#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
84pub struct AutoAcceptPolicy {
85    pub pairing: bool,
86    pub pre_pair: bool,
87    pub store_share: bool,
88    pub verify_share: bool,
89    pub discovery: bool,
90    pub get_share: bool,
91    pub unpair: bool,
92    pub update_channel_info: bool,
93}
94
95impl AutoAcceptPolicy {
96    /// Enable auto-accept for every flow. Equivalent to setting every
97    /// field to `true` — read the field-level docs on
98    /// [`AutoAcceptPolicy`] before using this in production; several
99    /// flows are state-changing or expose a request-amplification
100    /// surface.
101    pub fn all() -> Self {
102        Self {
103            pairing: true,
104            pre_pair: true,
105            store_share: true,
106            verify_share: true,
107            discovery: true,
108            get_share: true,
109            unpair: true,
110            update_channel_info: true,
111        }
112    }
113
114    /// `true` when the policy opts in to auto-accepting the given action.
115    /// Used by [`super::DeRecProtocol::process`] at the auto-accept
116    /// intercept site.
117    pub fn allows(&self, action: &PendingAction) -> bool {
118        match action.kind() {
119            PendingActionKind::Pairing => self.pairing,
120            PendingActionKind::PrePair => self.pre_pair,
121            PendingActionKind::StoreShare => self.store_share,
122            PendingActionKind::VerifyShare => self.verify_share,
123            PendingActionKind::Discovery => self.discovery,
124            PendingActionKind::GetShare => self.get_share,
125            PendingActionKind::Unpair => self.unpair,
126            PendingActionKind::UpdateChannelInfo => self.update_channel_info,
127        }
128    }
129}
130
131/// An opaque action token emitted inside [`DeRecEvent::ActionRequired`] events.
132///
133/// When `process()` receives an incoming protocol request, it returns an
134/// `ActionRequired` event carrying a `PendingAction`. The application must
135/// pass this token to [`super::DeRecProtocol::accept`] or
136/// [`super::DeRecProtocol::reject`] to complete the flow.
137///
138/// `Debug` shows only the [`PendingActionKind`] discriminant plus
139/// `channel_id`, keeping any peer-supplied request contents out of log
140/// lines. No variant carries private key material — the accept handlers
141/// reload the pairing secret from the secret store (single source of
142/// truth), so the token stays small and safe to persist or ship across a
143/// language boundary.
144pub enum PendingAction {
145    Pairing {
146        channel_id: ChannelId,
147        request: PairRequestMessage,
148        kind: SenderKind,
149        peer_communication_info: HashMap<String, String>,
150        /// Trace id read from the inbound `PairRequest` envelope. Echoed
151        /// verbatim on the `PairResponse` envelope when the application
152        /// calls `accept` or `reject`; see `DeRecMessage.traceId`.
153        trace_id: u64,
154    },
155    /// The peer scanned a `HashedKeys`-mode `ContactMessage` and is asking
156    /// for the real pairing public keys via a plaintext `PrePairRequest`.
157    ///
158    /// Accepting fetches `PairingSecret` from the secret store and replies
159    /// with the actual `mlkemEncapsulationKey` / `eciesPublicKey`; the
160    /// scanner then validates the published keys against the contact's
161    /// `contactBindingHash` and proceeds to a normal `PairRequest` flow.
162    /// Rejecting sends back a non-Ok `PrePairResponse` and keeps no state.
163    ///
164    /// Carries no `pairing_secret` — the handler loads it from the secret
165    /// store at `accept` time (single source of truth) and the action stays
166    /// small enough to round-trip cheaply across the WASM boundary.
167    PrePair {
168        channel_id: ChannelId,
169        request: PrePairRequestMessage,
170        /// Trace id read from the inbound `PrePairRequest` envelope. Echoed
171        /// verbatim on the `PrePairResponse` when the application calls
172        /// `accept` or `reject`; see `DeRecMessage.traceId`.
173        trace_id: u64,
174    },
175    StoreShare {
176        channel_id: ChannelId,
177        request: StoreShareRequestMessage,
178        shared_key: SharedKey,
179        /// Trace id read from the inbound request envelope. Echoed verbatim
180        /// on the response envelope when the application calls `accept` or
181        /// `reject`; see `DeRecMessage.traceId`.
182        trace_id: u64,
183    },
184    VerifyShare {
185        channel_id: ChannelId,
186        request: VerifyShareRequestMessage,
187        shared_key: SharedKey,
188        trace_id: u64,
189    },
190    /// The peer is asking us to enumerate which `(secret_id, version)`
191    /// tuples we currently hold for them on this channel (see
192    /// [`crate::primitives::discovery`]). Accepting replies with the
193    /// catalog so the asker can correlate it with their own view —
194    /// commonly the precursor an owner uses to drive `Recovery`, but
195    /// useful any time the owner wants to know what a given helper
196    /// still has. Rejecting sends back a non-`Ok` response and
197    /// discloses no catalog content.
198    Discovery {
199        channel_id: ChannelId,
200        request: GetSecretIdsVersionsRequestMessage,
201        shared_key: SharedKey,
202        trace_id: u64,
203    },
204    GetShare {
205        channel_id: ChannelId,
206        request: GetShareRequestMessage,
207        shared_key: SharedKey,
208        trace_id: u64,
209    },
210    /// The peer has asked us to drop our state for this channel
211    /// (see [`crate::primitives::unpairing`]). Accepting deletes the local
212    /// channel/share/secret state and sends back an `Ok` response; rejecting
213    /// sends a non-`Ok` response and keeps the state.
214    Unpair {
215        channel_id: ChannelId,
216        request: UnpairRequestMessage,
217        shared_key: SharedKey,
218        trace_id: u64,
219    },
220    /// The peer has announced an update to their communication info and/or
221    /// transport endpoint.
222    ///
223    /// Calling [`super::DeRecProtocol::accept`] on this action does the
224    /// state mutation **for you**: the orchestrator writes the new fields
225    /// onto the stored [`crate::protocol::types::Channel`] and sends back
226    /// an `Ok` response. When `transport_protocol` is part of the update,
227    /// the response is routed to the **new** endpoint, so subsequent
228    /// outbound traffic on this channel already targets the new address.
229    /// Applications do not need to call any setter themselves on the
230    /// receiving side — receiving-side endpoint changeover is handled
231    /// inside `accept`. The local-node endpoint setters
232    /// [`crate::protocol::DeRecProtocol::set_communication_info`] and
233    /// [`crate::protocol::DeRecProtocol::set_own_transport`] exist for
234    /// the **initiating** side only, where the announcement comes from.
235    ///
236    /// Calling [`super::DeRecProtocol::reject`] sends a non-`Ok`
237    /// response and leaves the stored channel state unchanged.
238    UpdateChannelInfo {
239        channel_id: ChannelId,
240        request: UpdateChannelInfoRequestMessage,
241        shared_key: SharedKey,
242        trace_id: u64,
243    },
244}
245
246impl std::fmt::Debug for PendingAction {
247    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248        let channel_id = match self {
249            PendingAction::Pairing { channel_id, .. }
250            | PendingAction::PrePair { channel_id, .. }
251            | PendingAction::StoreShare { channel_id, .. }
252            | PendingAction::VerifyShare { channel_id, .. }
253            | PendingAction::Discovery { channel_id, .. }
254            | PendingAction::GetShare { channel_id, .. }
255            | PendingAction::Unpair { channel_id, .. }
256            | PendingAction::UpdateChannelInfo { channel_id, .. } => channel_id,
257        };
258        f.debug_struct("PendingAction")
259            .field("kind", &self.kind())
260            .field("channel_id", &channel_id.0)
261            .finish()
262    }
263}
264
265impl PendingAction {
266    /// Return this action's discriminant — used by
267    /// [`AutoAcceptPolicy::allows`] and by the [`DeRecEvent::AutoAccepted`]
268    /// event so callers can route on flow kind without inspecting the
269    /// inner payload.
270    pub fn kind(&self) -> PendingActionKind {
271        match self {
272            PendingAction::Pairing { .. } => PendingActionKind::Pairing,
273            PendingAction::PrePair { .. } => PendingActionKind::PrePair,
274            PendingAction::StoreShare { .. } => PendingActionKind::StoreShare,
275            PendingAction::VerifyShare { .. } => PendingActionKind::VerifyShare,
276            PendingAction::Discovery { .. } => PendingActionKind::Discovery,
277            PendingAction::GetShare { .. } => PendingActionKind::GetShare,
278            PendingAction::Unpair { .. } => PendingActionKind::Unpair,
279            PendingAction::UpdateChannelInfo { .. } => PendingActionKind::UpdateChannelInfo,
280        }
281    }
282}
283
284/// Describes an outbound protocol flow to initiate via [`super::DeRecProtocol::start`].
285///
286/// # Role gating
287///
288/// The orchestrator enforces flow directionality against
289/// [`crate::protocol::types::Channel::role`] (set at pairing time):
290///
291/// - [`Self::Discovery`], [`Self::ProtectSecret`], [`Self::VerifyShares`],
292///   [`Self::RecoverSecret`], and [`Self::Unpair`] require this node to be
293///   the [`SenderKind::Owner`] on every targeted channel; otherwise
294///   [`crate::Error::RoleMismatch`] is returned.
295/// - [`Self::Pairing`] creates the channel, so no role exists yet.
296/// - [`Self::UpdateChannelInfo`] is symmetric — either party may initiate.
297pub enum DeRecFlow {
298    Pairing {
299        kind: SenderKind,
300        contact: ContactMessage,
301        /// App-level identity metadata for the peer being paired with.
302        /// Stored verbatim on the resulting [`crate::protocol::types::Channel`]
303        /// (`channel.communication_info`). The protocol does not inspect
304        /// it — pass an empty map to record nothing.
305        peer_communication_info: std::collections::HashMap<String, String>,
306    },
307    Discovery {
308        target: Target,
309    },
310    /// Publish the current secret to the protocol's paired peers.
311    ///
312    /// The secret identifier comes from the
313    /// [`super::DeRecProtocol`] instance (set at construction via
314    /// [`crate::protocol::DeRecProtocolBuilder::new`]) — one protocol
315    /// instance manages exactly one secret.
316    ///
317    /// The target set is derived from the channel store: every paired
318    /// Owner→Helper channel receives a share if the configured threshold
319    /// is met, and every paired Source→ReplicaDestination channel
320    /// receives the full secret payload. Apps that need to drive a single
321    /// peer should pair just that peer; the protocol no longer accepts
322    /// a per-call subset.
323    ///
324    /// When the count of paired Helpers is below
325    /// [`crate::protocol::DeRecProtocolBuilder::with_threshold`], no VSS
326    /// split runs and Helpers receive nothing — the secret still lands on
327    /// any paired Replica destinations in "secret-only" form.
328    ProtectSecret {
329        secrets: Vec<UserSecret>,
330        description: Option<String>,
331    },
332    VerifyShares {
333        secret_id: u64,
334        version: u32,
335        target: Target,
336    },
337    RecoverSecret {
338        secret_id: u64,
339        version: u32,
340    },
341    /// Initiate an unpair flow against a paired channel.
342    ///
343    /// **Owner-initiated only.** This node must hold
344    /// [`derec_proto::SenderKind::Owner`] on `channel_id`; otherwise
345    /// [`crate::Error::RoleMismatch`] is returned. Helpers cannot tear
346    /// down the relationship from the protocol layer — they may only
347    /// refuse an incoming unpair request via
348    /// [`super::DeRecProtocol::reject`].
349    ///
350    /// Whether the local state for `channel_id` is dropped immediately on
351    /// `start(Unpair)` or only after the peer acknowledges is governed by
352    /// [`crate::protocol::DeRecProtocolBuilder::with_unpair_ack`].
353    Unpair {
354        channel_id: ChannelId,
355        /// Optional human-readable reason embedded into the wire request.
356        /// Pass `None` (or an empty string) to omit.
357        memo: Option<String>,
358    },
359    /// Broadcast updated communication info and/or transport endpoint to one
360    /// or more paired channels.
361    ///
362    /// Either party may initiate. Either field may be `None` to leave it
363    /// unchanged; presence of `communication_info` (even with an empty map)
364    /// instructs the peer to replace its stored map for this channel with
365    /// the supplied one. Presence of `transport_protocol` instructs the peer
366    /// to use the new endpoint for the response and all subsequent messages.
367    ///
368    /// The application is responsible for calling
369    /// [`crate::protocol::DeRecProtocol::set_communication_info`] and/or
370    /// [`crate::protocol::DeRecProtocol::set_own_transport`] **before**
371    /// initiating this flow so the local state matches what is announced.
372    /// See the setter docs for the endpoint-changeover discipline.
373    UpdateChannelInfo {
374        target: Target,
375        /// Updated communication info. `None` leaves the peer's stored map
376        /// untouched; `Some(_)` replaces it (an empty `HashMap` clears it).
377        communication_info: Option<std::collections::HashMap<String, String>>,
378        /// Updated transport endpoint. `None` leaves it untouched.
379        transport_protocol: Option<TransportProtocol>,
380    },
381}
382
383/// Determines whether the unpair initiator waits for the peer's
384/// acknowledgement before dropping its local state for the channel.
385///
386/// See [`crate::protocol::DeRecProtocolBuilder::with_unpair_ack`].
387#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
388pub enum UnpairAck {
389    /// The initiator keeps its local channel/share/secret state until the
390    /// peer's `Ok` response arrives — or until the configured protocol
391    /// timeout elapses, at which point the state is dropped anyway and an
392    /// [`DeRecEvent::Unpaired`] event surfaces.
393    #[default]
394    Required,
395    /// The initiator drops its local state immediately after sending the
396    /// request and emits [`DeRecEvent::Unpaired`] right away. Any later
397    /// response is silently ignored.
398    NotRequired,
399}
400
401/// Events emitted by [`super::DeRecProtocol::process`].
402///
403/// The application reacts to these instead of routing raw messages manually.
404#[non_exhaustive]
405#[derive(Debug)]
406#[allow(clippy::large_enum_variant)]
407pub enum DeRecEvent {
408    /// Pairing completed — the shared key for `channel_id` is now persisted.
409    ///
410    /// `kind` is the local party's role in the pairing, also persisted as
411    /// [`crate::protocol::types::Channel::role`] and consulted by the orchestrator on
412    /// every subsequent flow start and inbound message. Applications use it
413    /// to decide what to do next:
414    ///
415    /// - [`SenderKind::Owner`] — the Owner completed pairing with a Helper.
416    ///   Call [`super::DeRecProtocol::start`] with [`DeRecFlow::ProtectSecret`]
417    ///   to distribute shares, or [`DeRecFlow::Discovery`] to ask the Helper
418    ///   which secrets it holds (e.g. after a recovery re-pairing).
419    /// - [`SenderKind::Helper`] — the Helper side completed pairing; no
420    ///   additional action is required (the Helper waits for incoming messages).
421    /// - [`SenderKind::ReplicaSource`] / [`SenderKind::ReplicaDestination`] —
422    ///   a replica pairing completed; the application may use the channel
423    ///   as needed (Source pushes via `ProtectSecret`, Destination receives
424    ///   via [`Self::ReplicaSecretReceived`]).
425    PairingCompleted {
426        /// Long-term `channel_id` both peers atomically rotated to at the
427        /// end of the handshake. All post-pairing traffic and library
428        /// state for this relationship keys on this value.
429        channel_id: ChannelId,
430        /// The transient `channel_id` used only during the pairing
431        /// handshake — the one that traveled on the `ContactMessage` and
432        /// the pairing envelopes. Already replaced with `channel_id` in
433        /// library state; the library will refuse messages routed to it
434        /// from this point on. Provided so applications that persisted
435        /// the pairing id (e.g. displayed it in a UI or mirrored it to
436        /// their own store) can rekey their own records.
437        pairing_channel_id: ChannelId,
438        kind: SenderKind,
439        /// Key-value pairs extracted from the peer's `CommunicationInfo`
440        /// (e.g. `"name"`, `"email"`, `"phone"`). Empty if the peer sent none.
441        peer_communication_info: HashMap<String, String>,
442    },
443
444    /// A replica-mode pair handshake completed. Fires **alongside**
445    /// [`Self::PairingCompleted`] on replica channels.
446    ///
447    /// Under the unidirectional replica model, the local side's role
448    /// (`ReplicaSource` or `ReplicaDestination`) is already on
449    /// [`crate::protocol::types::Channel::role`] — this event just adds the
450    /// peer's `replica_id`, which the app needs as a `from_replica_id`
451    /// when subsequent secret syncs arrive or when targeting the peer via
452    /// `ProtectSecret`.
453    ReplicaPaired {
454        /// The channel the pair handshake just completed on.
455        channel_id: ChannelId,
456        /// The peer's replica identity, extracted from
457        /// `derec.replica_id` in the peer's `CommunicationInfo`.
458        peer_replica_id: u64,
459    },
460
461    /// A share was accepted and stored locally (Helper side).
462    ///
463    /// `replica_id` is the stable per-device identifier of the writer,
464    /// copied from the inbound `StoreShareRequestMessage.replica_id`.
465    /// `None` indicates the writer was a non-replica `Owner`. The field
466    /// is metadata for the application; see
467    /// [`crate::protocol::DeRecShareStore::save`] for the storage
468    /// disambiguation contract that makes concurrent writes from
469    /// distinct replicas coexist.
470    ShareStored {
471        channel_id: ChannelId,
472        version: u32,
473        replica_id: Option<u64>,
474    },
475
476    /// A `ReplicaSource` peer pushed a secret sync on a `ReplicaDestination`
477    /// channel. The library has already auto-acked the inbound
478    /// `StoreShareRequest` and decoded the `ReplicaSecretPayload` into
479    /// typed fields — the application can install `secret` directly,
480    /// optionally using `shares` to verify or to take over the recovery
481    /// flow toward each helper.
482    ///
483    /// **Recovery transitivity**: `secret.helpers[i].shared_key` lets
484    /// the receiver authenticate as the Source toward each helper. For
485    /// replica-to-replica traffic, all replicas share a single
486    /// group-wide channel key (see
487    /// [`crate::protocol::types::ReplicaSecretPayload`] for the handover
488    /// protocol) — the receiver's own
489    /// [`crate::protocol::DeRecSecretStore`] entry for this channel
490    /// holds that group key, so impersonating the Source toward another
491    /// destination is just a normal channel-key load. Treat the
492    /// receiving device accordingly — see
493    /// [`crate::protocol::types::ReplicaInfo`] for the security note.
494    ReplicaSecretReceived {
495        /// The channel the request arrived on.
496        channel_id: ChannelId,
497        /// The peer's replica identity (from `Channel.replica_id`,
498        /// populated at pair time).
499        from_replica_id: u64,
500        /// `secret_id` echoed from the inbound `StoreShareRequest`.
501        secret_id: u64,
502        /// `version` echoed from the inbound `StoreShareRequest`.
503        version: u32,
504        /// Decoded full secret — same shape the sender wrote. The
505        /// `helpers`, `replicas`, `secrets`, and `owner_replica_id`
506        /// fields carry the canonical roster snapshot for this version.
507        secret: crate::protocol::types::Secret,
508        /// Per-helper VSS share map. Each entry pairs a helper's
509        /// `channel_id` with the serialized `CommittedDeRecShare` bytes
510        /// the helper received — sufficient material for the receiver
511        /// to drive a recovery against those helpers if needed.
512        shares: Vec<crate::protocol::types::ChannelShare>,
513    },
514
515    /// A replica peer's `StoreShareResponse` to a secret sync we sent
516    /// earlier. Fires on the replica channel, mirroring
517    /// [`Self::ShareConfirmed`] / [`Self::ShareRejected`] on the helper
518    /// side.
519    ///
520    /// `status` and `memo` come straight from the peer's
521    /// `StoreShareResponseMessage.result`. Apps decide whether to retry,
522    /// rebroadcast, or surface the failure to the user.
523    ReplicaSecretAcked {
524        channel_id: ChannelId,
525        /// The peer's replica identity (from `Channel.replica_id`).
526        from_replica_id: u64,
527        /// `secret_id` echoed from the response.
528        secret_id: u64,
529        /// `version` echoed from the response.
530        version: u32,
531        /// The `StatusEnum` value from the peer's response.
532        status: i32,
533        /// Human-readable explanation from the peer (empty on `Ok`).
534        memo: String,
535    },
536
537    /// A Helper confirmed it stored our share (Owner side).
538    ShareConfirmed { channel_id: ChannelId, version: u32 },
539
540    /// A Helper rejected or failed to store our share (Owner side).
541    ///
542    /// The protocol absorbs sharing failures and converts them to events
543    /// so the application can display per-participant progress. `status` and
544    /// `memo` come from the Helper's response (or are synthetic for timeouts).
545    ShareRejected {
546        channel_id: ChannelId,
547        version: u32,
548        /// The `StatusEnum` value from the Helper's response.
549        status: i32,
550        /// Human-readable reason from the Helper, or `"timeout"`.
551        memo: String,
552    },
553
554    /// A sharing round has completed (all participants responded or timed out).
555    ///
556    /// Emitted once per [`DeRecFlow::ProtectSecret`] flow after every targeted
557    /// Helper has either confirmed, rejected, or timed out.
558    SharingComplete {
559        version: u32,
560        confirmed_count: usize,
561        failed_count: usize,
562        /// `true` when `confirmed_count >= threshold`.
563        threshold_met: bool,
564    },
565
566    /// A Helper's verification proof checked out (Owner side).
567    ShareVerified { channel_id: ChannelId, version: u32 },
568
569    /// A Helper reported all secrets it currently stores for this channel (Owner side).
570    ///
571    /// Emitted after the Owner calls [`super::DeRecProtocol::start`] with
572    /// [`DeRecFlow::Discovery`] and the Helper responds. Each
573    /// [`SecretVersionEntry`] carries a `secret_id` and a list of
574    /// `(version, description)` pairs for every share the Helper holds.
575    ///
576    /// The application should persist this list and, once enough Helpers have
577    /// responded, call [`super::DeRecProtocol::start`] with
578    /// [`DeRecFlow::RecoverSecret`] for the desired `(secret_id, version)`.
579    SecretsDiscovered {
580        channel_id: ChannelId,
581        /// All secrets and their stored versions the Helper holds for this channel.
582        secrets: Vec<SecretVersionEntry>,
583    },
584
585    /// A recovery share response was received from a Helper but reconstruction
586    /// cannot succeed yet — more shares are needed to meet the threshold.
587    ///
588    /// - `channel_id` identifies the Helper that sent this share response.
589    /// - `shares_received` is the total number of share responses collected so far
590    ///   for this `(secret_id, version)` recovery context.
591    RecoveryShareReceived {
592        channel_id: ChannelId,
593        shares_received: usize,
594    },
595
596    /// A recovery share response was received but reconstruction failed for a
597    /// reason other than insufficient shares (e.g. corrupted share, version
598    /// mismatch, decode error).
599    ///
600    /// - `channel_id` identifies the Helper that sent this share response.
601    /// - `shares_received` is the total number of share responses collected so far.
602    /// - `error` describes the failure cause.
603    RecoveryShareError {
604        channel_id: ChannelId,
605        shares_received: usize,
606        error: String,
607    },
608
609    /// Recovery completed — the reconstructed
610    /// [`crate::protocol::types::Secret`] is returned exactly once.
611    ///
612    /// The variant mirrors [`Self::ReplicaSecretReceived`]: the
613    /// inner `secret` carries the full typed snapshot
614    /// — `secrets: Vec<UserSecret>` (the user-facing entries the
615    /// owner originally protected) plus the roster snapshot
616    /// (`helpers`, `replicas`, `owner_replica_id`) captured at
617    /// distribution time. Apps that only care about the user-facing
618    /// entries read `secret.secrets`; the roster fields are useful
619    /// when the recovering owner wants to know who held the shares,
620    /// re-pair with the same helpers, or sync replicas after the
621    /// recovery completes.
622    ///
623    /// The library decodes the two-layer (`DeRecSecret` → `Secret`)
624    /// protobuf wrapping internally; a decode failure surfaces as
625    /// [`Self::RecoveryShareError`] for that final share, not as
626    /// `SecretRecovered` with bogus contents.
627    ///
628    /// Pass `secret` to [`super::DeRecProtocol::restore`] on a fresh
629    /// protocol instance to commit canonical helper / replica state
630    /// and wipe the throwaway recovery-mode channels.
631    SecretRecovered {
632        secret: crate::protocol::types::Secret,
633    },
634
635    /// An incoming request requires application confirmation before the library responds.
636    ///
637    /// Emitted by [`super::DeRecProtocol::process`] for every incoming request
638    /// that is **not** opted into auto-accept by [`AutoAcceptPolicy`].
639    /// The application must call [`super::DeRecProtocol::accept`] or
640    /// [`super::DeRecProtocol::reject`] to complete the flow.
641    ActionRequired {
642        channel_id: ChannelId,
643        action: PendingAction,
644    },
645
646    /// The library auto-accepted an incoming request because the
647    /// configured [`AutoAcceptPolicy`] opted in to its flow.
648    ///
649    /// Emitted by [`super::DeRecProtocol::process`] **in place of**
650    /// [`Self::ActionRequired`] for the auto-accepted flow, followed
651    /// (in the same event vec) by the same flow events a manual
652    /// `accept(action)` would have produced (e.g. `ShareStored`,
653    /// `PairingCompleted`, `Unpaired`). Applications use this event
654    /// for observability / audit logging — no further action is
655    /// required from the caller.
656    AutoAccepted {
657        channel_id: ChannelId,
658        /// The action's discriminant. The original
659        /// [`PendingAction`] payload is consumed by the internal
660        /// `accept` and is not surfaced here; routing on the kind is
661        /// enough for observability since the flow-completion events
662        /// that follow carry the per-flow details.
663        action_kind: PendingActionKind,
664    },
665
666    /// The local channel state for `channel_id` has been dropped as the
667    /// result of an unpair flow.
668    ///
669    /// Surfaces on **both** sides of the flow:
670    ///
671    /// - Initiator: emitted when (a) `UnpairAck::NotRequired` and the request
672    ///   has just been sent, (b) `UnpairAck::Required` and the peer
673    ///   acknowledged with `Ok`, or (c) `UnpairAck::Required` and the
674    ///   configured timeout elapsed without a response.
675    /// - Responder: emitted by [`super::DeRecProtocol::accept`] after the
676    ///   channel/share/secret state for the requesting peer has been removed.
677    Unpaired { channel_id: ChannelId },
678
679    /// The peer answered an outbound unpair request with a non-`Ok` status.
680    ///
681    /// The initiator's local state is **not** dropped — the application
682    /// decides what to do (retry, escalate, or force-delete locally).
683    UnpairRejected {
684        channel_id: ChannelId,
685        /// The `StatusEnum` value from the peer's response.
686        status: i32,
687        /// Human-readable reason from the peer.
688        memo: String,
689    },
690
691    /// The contact creator answered our `PrePairRequest` with a non-`Ok`
692    /// status (scanner side, `HashedKeys` flow).
693    ///
694    /// The scanner cannot proceed to a normal `PairRequest` because the
695    /// public keys were never published. Distinct from
696    /// [`crate::primitives::pairing::PairingError::PrePairHashMismatch`],
697    /// which fires when keys *were* published but failed the binding-hash
698    /// check — a cryptographic failure surfaced as `Err`, not an event.
699    PrePairRejected {
700        channel_id: ChannelId,
701        /// The `StatusEnum` value from the contact creator's response.
702        status: i32,
703        /// Human-readable reason from the contact creator.
704        memo: String,
705    },
706
707    /// The stored [`crate::protocol::types::Channel`] for `channel_id` has been updated
708    /// with new communication info and/or transport endpoint.
709    ///
710    /// Surfaces on **both** sides of the flow:
711    ///
712    /// - Responder: emitted by [`super::DeRecProtocol::accept`] after the
713    ///   update has been persisted to the local channel store.
714    /// - Initiator: emitted by [`super::DeRecProtocol::process`] when the
715    ///   peer's `Ok` response arrives.
716    ///
717    /// The new `communication_info` / `transport_protocol` values are
718    /// already on the local
719    /// [`crate::protocol::types::Channel`] by the time the event fires;
720    /// applications that care about the post-update state read it from
721    /// the channel store directly.
722    ChannelInfoUpdated { channel_id: ChannelId },
723
724    /// The peer answered an outbound `UpdateChannelInfo` request with a
725    /// non-`Ok` status. The peer's stored state is unchanged. The initiator's
726    /// own state is also unaffected.
727    ChannelInfoUpdateRejected {
728        channel_id: ChannelId,
729        /// The `StatusEnum` value from the peer's response.
730        status: i32,
731        /// Human-readable reason from the peer.
732        memo: String,
733    },
734
735    /// Well-formed message with no actionable effect (e.g. an ACK).
736    NoOp,
737
738    /// A pairing flow was initiated for `channel_id` with the local
739    /// role `kind`. Emitted synchronously from
740    /// [`super::DeRecProtocol::start`] when the `PairRequest` (or
741    /// `PrePairRequest` for `HashedKeys` / `NoKeys` modes) was
742    /// dispatched successfully. Followed by
743    /// [`Self::PairingCompleted`] (or a failure variant) once the peer
744    /// responds. On a local send failure, `start` returns `Err` instead
745    /// — no `PairingStarted` is emitted.
746    PairingStarted {
747        channel_id: ChannelId,
748        /// The local party's role in the pairing (same value that will
749        /// appear on [`Self::PairingCompleted::kind`] and be persisted
750        /// as [`crate::protocol::types::Channel::role`]).
751        kind: SenderKind,
752    },
753
754    /// A discovery request was dispatched to `channel_id`. Emitted per
755    /// targeted channel by [`super::DeRecProtocol::start`]. Followed by
756    /// [`Self::SecretsDiscovered`] once the helper responds.
757    DiscoveryStarted { channel_id: ChannelId },
758
759    /// A discovery request could not be dispatched to `channel_id`.
760    /// Emitted per targeted channel by [`super::DeRecProtocol::start`]
761    /// when the outbound send (or per-channel preparation) failed. The
762    /// remaining channels in the same fan-out are unaffected.
763    DiscoveryFailed {
764        channel_id: ChannelId,
765        error: String,
766    },
767
768    /// A share-storage request was dispatched to `channel_id` for
769    /// `version`. Emitted per targeted helper (and per targeted replica
770    /// destination) by [`super::DeRecProtocol::start`]. Followed by
771    /// [`Self::ShareStored`] / [`Self::ShareConfirmed`] /
772    /// [`Self::ShareRejected`] as the peer responds; the whole round
773    /// finishes with [`Self::SharingComplete`].
774    ProtectSecretStarted { channel_id: ChannelId, version: u32 },
775
776    /// A share-storage request could not be dispatched to `channel_id`
777    /// for `version`. Remaining fan-out targets are unaffected.
778    ProtectSecretFailed {
779        channel_id: ChannelId,
780        version: u32,
781        error: String,
782    },
783
784    /// A verify-share challenge was dispatched to `channel_id` for
785    /// `version`. Followed by [`Self::ShareVerified`] once the helper
786    /// responds.
787    VerifySharesStarted { channel_id: ChannelId, version: u32 },
788
789    /// A verify-share challenge could not be dispatched to `channel_id`
790    /// for `version`.
791    VerifySharesFailed {
792        channel_id: ChannelId,
793        version: u32,
794        error: String,
795    },
796
797    /// A recovery share request was dispatched to `channel_id` for
798    /// `version`. Followed by [`Self::RecoveryShareReceived`] /
799    /// [`Self::RecoveryShareError`] / [`Self::SecretRecovered`] as
800    /// helper responses arrive.
801    RecoverSecretStarted { channel_id: ChannelId, version: u32 },
802
803    /// A recovery share request could not be dispatched to `channel_id`
804    /// for `version`.
805    RecoverSecretFailed {
806        channel_id: ChannelId,
807        version: u32,
808        error: String,
809    },
810
811    /// An unpair request was dispatched to `channel_id`. Followed by
812    /// [`Self::Unpaired`] once the peer acknowledges (or, under
813    /// [`UnpairAck::NotRequired`], emitted in the same event vec
814    /// immediately after `UnpairStarted`).
815    UnpairStarted { channel_id: ChannelId },
816
817    /// An update-channel-info request was dispatched to `channel_id`.
818    /// Followed by [`Self::ChannelInfoUpdated`] once the peer responds.
819    UpdateChannelInfoStarted { channel_id: ChannelId },
820
821    /// An update-channel-info request could not be dispatched to
822    /// `channel_id`.
823    UpdateChannelInfoFailed {
824        channel_id: ChannelId,
825        error: String,
826    },
827}
828
829#[cfg(test)]
830mod tests {
831    use super::*;
832    use derec_proto::{
833        GetSecretIdsVersionsRequestMessage, GetShareRequestMessage, PrePairRequestMessage,
834        StoreShareRequestMessage, UnpairRequestMessage, UpdateChannelInfoRequestMessage,
835        VerifyShareRequestMessage,
836    };
837
838    fn store_share_action() -> PendingAction {
839        PendingAction::StoreShare {
840            channel_id: ChannelId(1),
841            request: StoreShareRequestMessage::default(),
842            shared_key: [0u8; 32],
843            trace_id: 0,
844        }
845    }
846
847    fn verify_share_action() -> PendingAction {
848        PendingAction::VerifyShare {
849            channel_id: ChannelId(1),
850            request: VerifyShareRequestMessage::default(),
851            shared_key: [0u8; 32],
852            trace_id: 0,
853        }
854    }
855
856    fn discovery_action() -> PendingAction {
857        PendingAction::Discovery {
858            channel_id: ChannelId(1),
859            request: GetSecretIdsVersionsRequestMessage::default(),
860            shared_key: [0u8; 32],
861            trace_id: 0,
862        }
863    }
864
865    fn get_share_action() -> PendingAction {
866        PendingAction::GetShare {
867            channel_id: ChannelId(1),
868            request: GetShareRequestMessage::default(),
869            shared_key: [0u8; 32],
870            trace_id: 0,
871        }
872    }
873
874    fn unpair_action() -> PendingAction {
875        PendingAction::Unpair {
876            channel_id: ChannelId(1),
877            request: UnpairRequestMessage::default(),
878            shared_key: [0u8; 32],
879            trace_id: 0,
880        }
881    }
882
883    fn update_channel_info_action() -> PendingAction {
884        PendingAction::UpdateChannelInfo {
885            channel_id: ChannelId(1),
886            request: UpdateChannelInfoRequestMessage::default(),
887            shared_key: [0u8; 32],
888            trace_id: 0,
889        }
890    }
891
892    fn pre_pair_action() -> PendingAction {
893        PendingAction::PrePair {
894            channel_id: ChannelId(1),
895            request: PrePairRequestMessage::default(),
896            trace_id: 0,
897        }
898    }
899
900    #[test]
901    fn pending_action_kind_round_trips_each_variant() {
902        assert_eq!(store_share_action().kind(), PendingActionKind::StoreShare);
903        assert_eq!(verify_share_action().kind(), PendingActionKind::VerifyShare);
904        assert_eq!(discovery_action().kind(), PendingActionKind::Discovery);
905        assert_eq!(get_share_action().kind(), PendingActionKind::GetShare);
906        assert_eq!(unpair_action().kind(), PendingActionKind::Unpair);
907        assert_eq!(
908            update_channel_info_action().kind(),
909            PendingActionKind::UpdateChannelInfo
910        );
911        assert_eq!(pre_pair_action().kind(), PendingActionKind::PrePair);
912    }
913
914    #[test]
915    fn auto_accept_policy_default_is_all_false() {
916        let p = AutoAcceptPolicy::default();
917        assert!(!p.pairing);
918        assert!(!p.pre_pair);
919        assert!(!p.store_share);
920        assert!(!p.verify_share);
921        assert!(!p.discovery);
922        assert!(!p.get_share);
923        assert!(!p.unpair);
924        assert!(!p.update_channel_info);
925    }
926
927    #[test]
928    fn auto_accept_policy_all_is_all_true() {
929        let p = AutoAcceptPolicy::all();
930        assert!(p.pairing);
931        assert!(p.pre_pair);
932        assert!(p.store_share);
933        assert!(p.verify_share);
934        assert!(p.discovery);
935        assert!(p.get_share);
936        assert!(p.unpair);
937        assert!(p.update_channel_info);
938    }
939
940    #[test]
941    fn auto_accept_policy_allows_dispatches_per_flow() {
942        let only_store = AutoAcceptPolicy {
943            store_share: true,
944            ..Default::default()
945        };
946        assert!(only_store.allows(&store_share_action()));
947        assert!(!only_store.allows(&verify_share_action()));
948        assert!(!only_store.allows(&discovery_action()));
949        assert!(!only_store.allows(&get_share_action()));
950        assert!(!only_store.allows(&unpair_action()));
951        assert!(!only_store.allows(&update_channel_info_action()));
952        assert!(!only_store.allows(&pre_pair_action()));
953
954        let none = AutoAcceptPolicy::default();
955        assert!(!none.allows(&store_share_action()));
956
957        let all = AutoAcceptPolicy::all();
958        assert!(all.allows(&store_share_action()));
959        assert!(all.allows(&verify_share_action()));
960        assert!(all.allows(&discovery_action()));
961        assert!(all.allows(&get_share_action()));
962        assert!(all.allows(&unpair_action()));
963        assert!(all.allows(&update_channel_info_action()));
964        assert!(all.allows(&pre_pair_action()));
965    }
966}