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}