Events
Events are not part of the DeRec wire protocol. They are an abstraction of the DeRecProtocol orchestrator only — see Concepts / Primitives and Orchestrator for the two-altitude split. The normative wire protocol is defined by Messages, States, and Flows; events are how the orchestrator surfaces what it did (or what it needs the application to decide) after processing an inbound envelope. Applications integrating directly through the primitives layer do not see events at all — they own the state machine and the response bytes themselves.
Events serve two distinct purposes, and every DeRecEvent falls into one of them:
Notification. Most events tell the application what has happened as a result of driving a flow — a channel became
Paired, a share was stored, a Secret was recovered, an inbound Secret sync arrived. The application reacts to these to update its own UI, its own state, and downstream systems. The protocol has already decided what to do; the event is the report.Control point. One event variant —
DeRecEvent::ActionRequired { channel_id, action: PendingAction }— pauses a flow and hands the decision back to the application (or to the human behind it). The application either callsaccept(action)to let the flow proceed on protocol-correct terms, orreject(action, status, memo)to refuse. This is how the application (or its user) gates every inbound request that admits an application-level choice — accepting a pairing, storing a share, disclosing a share during recovery, and so on.ActionRequiredis the mechanism by which policy — not just protocol — reaches the wire.
This chapter is the reference enumeration of every DeRecEvent the orchestrator emits and every PendingAction the application can be asked to resolve. Per-flow walkthroughs of when each fires live in Flows; SDK-specific naming (Rust enum variants, .NET subclass records, TypeScript discriminated unions) lives in the SDK chapters.
How events are surfaced
The DeRecProtocol orchestrator is event-driven by design. protocol.process(envelope_bytes) decodes inbound wire bytes and returns a Vec<DeRecEvent> describing what changed; an empty vector or a single NoOp means nothing actionable happened. protocol.start(flow) may also queue events that fire on a later process call (for example SharingComplete once every targeted Helper has responded or timed out).
DeRecEvent::ActionRequired — the control-point event described above — is what gives the application its say. Every inbound request that needs an application-level policy decision is funnelled through ActionRequired: the orchestrator has decoded the request, validated its shape, and is ready to send the protocol-correct response — but it will not do so until the application (or its user) has been consulted. accept(action) releases the response; reject(action, status, memo) sends back a refusal with the supplied status and human-readable reason.
DeRecEvent::NoOp is the explicit "nothing meaningful happened" signal — emitted for stray or replayed inbound responses that the library safely dropped (see Verification binding gate for one example). Applications can usually ignore NoOp; it exists so the event stream is total rather than carrying a None semantic.
The event surface is identical across SDKs — only the language-specific naming differs. The variant set, field set, and emission semantics are defined once by the orchestrator API; bindings just re-expose the same values under language-idiomatic names. See Cross-SDK naming at the bottom of this page.
Event catalogue
Events are grouped by the flow that produces them, matching the order in Flows. Each entry documents the field shape and what state transition triggers emission.
Pairing
PairingCompleted
PairingCompletedThe pair handshake closed successfully on this side. Helper/Owner channels reach Paired status immediately; replica channels stay Pending until both sides also complete fingerprint cross-confirmation.
channel_id
ChannelId
The channel the handshake completed on.
kind
SenderKind
This node's role on the channel, persisted as Channel.role. Drives flow gating from here on.
peer_communication_info
HashMap<String, String>
Key-value identity metadata extracted from the peer's CommunicationInfo (e.g. "name", "email"). Empty if absent.
Triggers:
Owner side: arrival of a
PairResponseMessagewhose nonce and binding fields validate against the cached pairing state.Helper / responder side: completion of
accepton the correspondingPendingAction::Pairing.
See Pairing.
ReplicaPaired
ReplicaPairedFires alongside PairingCompleted on replica-mode channels. Carries the peer's stable replica_id, which the application needs as a from_replica_id correlation handle for later Secret syncs.
channel_id
ChannelId
The channel the pair handshake completed on.
peer_replica_id
u64
Peer's replica_id, extracted from the derec.replica_id entry in their CommunicationInfo.
Triggers:
Same as
PairingCompleted, restricted to channels whoseroleisReplicaSourceorReplicaDestination.
See Pairing.
ReplicaSecretReceived
ReplicaSecretReceivedA ReplicaSource peer pushed a Secret snapshot on a ReplicaDestination channel. The library has already auto-acked the inbound StoreShareRequest and decoded the ReplicaSecretPayload into typed fields. The destination can install secret directly, optionally using shares to verify or to take over recovery toward each helper.
channel_id
ChannelId
The channel the Secret sync arrived on.
from_replica_id
u64
Peer's replica_id (from Channel.replica_id, populated at pair time).
secret_id
u64
Echoed from the inbound StoreShareRequest.
version
u32
Echoed from the inbound StoreShareRequest.
secret
Secret
Decoded full Secret: helpers, secrets, replicas, owner_replica_id. See the Secret payload section in Messages.
shares
Vec<ChannelShare>
Per-helper VSS share map. Each entry pairs a helper's channel_id with the serialized CommittedDeRecShare bytes that helper holds.
Triggers:
Inbound
StoreShareRequeston aReplicaDestinationchannel whose payload decodes as aReplicaSecretPayload.
ReplicaSecretAcked
ReplicaSecretAckedA replica peer's StoreShareResponse to a Secret sync this node sent earlier. Fires on the ReplicaSource side and mirrors ShareConfirmed / ShareRejected on the helper side.
channel_id
ChannelId
The channel the response arrived on.
from_replica_id
u64
Peer's replica_id (from Channel.replica_id).
secret_id
u64
Echoed from the response.
version
u32
Echoed from the response.
status
i32
StatusEnum value from StoreShareResponseMessage.result.
memo
String
Human-readable reason from the peer (empty on Ok).
Triggers:
Inbound
StoreShareResponseon aReplicaSourcechannel correlating to an outbound Secret-sync request.
Sharing
ShareStored
ShareStoredA share was accepted and stored locally on the helper side after the application's accept resolved the matching PendingAction::StoreShare.
channel_id
ChannelId
Channel from the inbound request.
version
u32
Version stored.
replica_id
Option<u64>
Stable per-device identifier of the writer, copied from StoreShareRequestMessage.replica_id. None when the writer was a non-replica Owner.
Triggers:
protocol.accepton aPendingAction::StoreShareafter the share has been persisted viaDeRecShareStore::save.
See Sharing.
ShareConfirmed
ShareConfirmedA helper confirmed it stored an owner-side share. { channel_id: ChannelId, version: u32 }.
Triggers:
Inbound
StoreShareResponseMessagewithresult.status == Okfor a known outbound share.
See Sharing.
ShareRejected
ShareRejectedA helper rejected or failed to store a share. The protocol absorbs the failure and surfaces it as an event so the application can render per-participant progress.
channel_id
ChannelId
Channel of the rejecting helper.
version
u32
Version that was being distributed.
status
i32
StatusEnum value from the helper's response.
memo
String
Human-readable reason from the helper, or "timeout" (synthetic).
Triggers:
Inbound
StoreShareResponseMessagewith non-Okstatus.Local timeout firing on an outstanding outbound store-share request.
See Sharing.
SharingComplete
SharingCompleteAggregate result for one DeRecFlow::ProtectSecret round. Emitted exactly once per round after every targeted helper has either confirmed, rejected, or timed out.
version
u32
The version this round distributed.
confirmed_count
usize
Number of helpers that confirmed with Ok.
failed_count
usize
Number of helpers that rejected or timed out.
threshold_met
bool
true when confirmed_count >= threshold.
Triggers:
Resolution of the last outstanding store-share request in the round.
See Sharing.
Verification
ShareVerified
ShareVerifiedA helper's VerifyShareResponse passed both the (nonce, secret_id, version) binding gate and the SHA-384 hash check. { channel_id: ChannelId, version: u32 }.
Triggers:
Inbound
VerifyShareResponseMessagethat binds to an outstandingVerifyShareRequestand whose proof hash matches the locally-recomputedSHA-384(share || nonce).
See Verification.
Discovery
SecretsDiscovered
SecretsDiscoveredA helper returned its catalogue of (secret_id, version, replica_id) tuples it currently holds for this Owner identity. Application persists this and uses it to drive DeRecFlow::RecoverSecret and to detect concurrent-write conflicts between sibling Replicas.
channel_id
ChannelId
Channel of the responding helper.
secrets
Vec<SecretVersionEntry>
One entry per known secret_id, each carrying a Vec<VersionEntry> of (version, description, replica_id).
Each VersionEntry carries a replica_id field that lets the application detect concurrent writes — two distinct replica_ids with the same version for the same secret_id signal a conflict the application must reconcile before driving recovery.
Triggers:
Inbound
GetSecretIdsVersionsResponseMessagecorrelating to an outstandingDeRecFlow::Discoveryrequest.
See Discovery.
Recovery
RecoveryShareReceived
RecoveryShareReceivedA GetShareResponse was accumulated into the recovery context but the threshold of valid shares has not yet been collected.
channel_id
ChannelId
Helper that sent this share response.
shares_received
usize
Total share responses collected so far for the current (secret_id, version) context.
Triggers:
Inbound
GetShareResponseMessagewhose share decoded cleanly but did not yet complete reconstruction.
See Recovery.
RecoveryShareError
RecoveryShareErrorA GetShareResponse was received but reconstruction failed for a reason other than insufficient shares (corrupted share, version mismatch, decode error, Merkle-root inconsistency).
channel_id
ChannelId
Helper that sent the offending share response.
shares_received
usize
Total share responses collected so far.
error
String
Description of the failure cause.
Triggers:
Inbound
GetShareResponseMessagewhose share could not be reconciled with the accumulating recovery context.
See Recovery.
SecretRecovered
SecretRecoveredReconstruction succeeded. The reconstructed Secret is returned exactly once.
secret
Vec<u8>
The reconstructed Secret bytes. Decode as a Secret message to access helpers, secrets, replicas, and owner_replica_id.
Triggers:
Threshold of valid, mutually-consistent shares accumulated for
(secret_id, version).
See Recovery.
UpdateChannelInfo
ChannelInfoUpdated
ChannelInfoUpdatedThe stored Channel record was updated with new communication_info and/or transport_protocol. Surfaces on both sides of the flow: on the responder side from accept, on the initiator side from process when the peer's Ok response arrives. { channel_id: ChannelId }.
The post-update values are already on the local Channel by the time the event fires; applications that care about the new state read it from the channel store directly.
Triggers:
Responder:
protocol.accepton aPendingAction::UpdateChannelInfowrites the new fields to theChannelrecord.Initiator: inbound
UpdateChannelInfoResponseMessagewithOkstatus.
See UpdateChannelInfo.
ChannelInfoUpdateRejected
ChannelInfoUpdateRejectedThe peer answered an outbound UpdateChannelInfo with a non-Ok status. The peer's stored state is unchanged; the initiator's local state is also unaffected.
channel_id
ChannelId
Channel the update was targeting.
status
i32
StatusEnum value from the peer's response.
memo
String
Human-readable reason from the peer.
Triggers:
Inbound
UpdateChannelInfoResponseMessagewith non-Okstatus.
Unpair
Unpaired
UnpairedThe local channel/share/secret state for channel_id has been dropped as the result of an unpair flow. { channel_id: ChannelId }.
Surfaces on both sides of the flow:
Initiator: emitted when (a)
UnpairAck::NotRequiredand the request has just been sent, (b)UnpairAck::Requiredand the peer acknowledged withOk, or (c)UnpairAck::Requiredand the configured timeout elapsed without a response.Responder: emitted by
acceptafter the channel/share/secret state for the requesting peer has been removed.
Triggers:
See per-side conditions above. Self-initiated, peer-initiated, and timeout-induced tear-downs all surface through the same event variant; distinguishing them is the application's responsibility (typically by tracking whether
start(Unpair)was called locally).
See Unpair.
UnpairRejected
UnpairRejectedThe peer answered an outbound unpair request with a non-Ok status. The initiator's local state is not dropped — the application decides whether to retry, escalate, or force-delete locally.
channel_id
ChannelId
Channel the unpair was targeting.
status
i32
StatusEnum value from the peer's response.
memo
String
Human-readable reason from the peer.
Triggers:
Inbound
UnpairResponseMessagewith non-Okstatus.
See Unpair.
Pairing — PrePair tail
PrePairRejected
PrePairRejectedThe contact creator answered the scanner's PrePairRequest with a non-Ok status (the HashedKeys contact-delivery sub-flow). The scanner cannot proceed to a normal PairRequest because the public keys were never published.
channel_id
ChannelId
Channel the pre-pair was targeting.
status
i32
StatusEnum value from the contact creator's response.
memo
String
Human-readable reason from the contact creator.
Distinct from PairingError::PrePairHashMismatch, which fires when keys were published but failed the binding-hash check — that surfaces as a thrown error from process(), not an event.
Triggers:
Inbound
PrePairResponseMessagewith non-Okstatus on aHashedKeysflow.
See Pairing — HashedKeys sub-flow.
Lifecycle and housekeeping
ActionRequired
ActionRequiredAn inbound request needs application confirmation before the library responds.
channel_id
ChannelId
Channel the inbound request arrived on.
action
PendingAction
Opaque token round-tripped to protocol.accept or protocol.reject. See the PendingAction catalogue.
Triggers:
Any inbound request message that admits an application-level policy decision.
NoOp
NoOpExplicit "nothing meaningful happened" — emitted for stray or replayed inbound responses the library safely dropped. Carries no fields. Applications can usually ignore.
Triggers:
Inbound response that fails the per-flow binding gate (e.g. a
VerifyShareResponsewhosenoncedoes not match any outstanding request — see Verification binding gate).Replay of an already-processed response.
PendingAction catalogue
PendingAction is the opaque token carried by DeRecEvent::ActionRequired. The application must call protocol.accept(action) to drive the protocol-correct response, or protocol.reject(action, status, memo) to refuse. Each variant carries the inbound request payload plus the trace id, shared key, and any other state the orchestrator needs at resolution time.
Pairing
PendingAction::Pairing
PendingAction::PairingThe peer sent a PairRequest. Emitted on the responder side.
channel_id
ChannelId
Channel the request arrived on.
request
PairRequestMessage
The decoded inbound request (peer's public keys, senderKind, communicationInfo, ...).
pairing_secret
PairingSecretKeyMaterial
The local ephemeral ECIES / ML-KEM material the responder will use to derive the shared key.
kind
SenderKind
The peer's declared role on this channel.
peer_communication_info
HashMap<String, String>
Identity metadata copied from the peer's CommunicationInfo.
trace_id
u64
Trace id read from the inbound envelope, echoed verbatim on the response.
accept derives the shared key, persists the channel, and sends back a PairResponseMessage with Ok. reject sends a non-Ok response and discards the pairing state.
See Pairing.
PendingAction::PrePair
PendingAction::PrePairThe peer scanned a HashedKeys-mode ContactMessage and is asking for the real pairing public keys via a plaintext PrePairRequest. Emitted on the contact-creator side.
channel_id
ChannelId
Channel the request arrived on.
request
PrePairRequestMessage
The decoded inbound PrePairRequest.
trace_id
u64
Trace id read from the inbound envelope, echoed on the response.
accept loads PairingSecret from the secret store and replies with the actual mlkemEncapsulationKey / eciesPublicKey; the scanner then validates the published keys against the contact's contactBindingHash and proceeds to a normal PairRequest. reject sends back a non-Ok PrePairResponse and keeps no state.
See Pairing — HashedKeys sub-flow.
Sharing
PendingAction::StoreShare
PendingAction::StoreShareThe peer sent a StoreShareRequest. Emitted on the helper side (and on ReplicaDestination channels carrying a ReplicaSecretPayload, where the orchestrator auto-accepts internally before surfacing ReplicaSecretReceived).
channel_id
ChannelId
Channel the request arrived on.
request
StoreShareRequestMessage
The decoded inbound request.
shared_key
SharedKey
The post-pairing symmetric key for this channel.
trace_id
u64
Trace id read from the inbound envelope, echoed on the response.
accept persists the share via DeRecShareStore::save and sends back Ok. reject returns a non-Ok and stores nothing.
See Sharing.
Verification
PendingAction::VerifyShare
PendingAction::VerifyShareThe peer sent a VerifyShareRequest. Emitted on the helper side.
channel_id
ChannelId
Channel the request arrived on.
request
VerifyShareRequestMessage
The decoded inbound request (carries secret_id, version, nonce).
shared_key
SharedKey
Channel symmetric key.
trace_id
u64
Trace id, echoed verbatim on the response.
accept loads the stored share for (secret_id, version), computes SHA-384(share || nonce), and returns the proof. reject returns a non-Ok and discloses nothing.
See Verification.
Discovery
PendingAction::Discovery
PendingAction::DiscoveryThe peer is asking the helper to enumerate the (secret_id, version) tuples it holds for them. Emitted on the helper side.
channel_id
ChannelId
Channel the request arrived on.
request
GetSecretIdsVersionsRequestMessage
The decoded inbound request.
shared_key
SharedKey
Channel symmetric key.
trace_id
u64
Trace id, echoed verbatim on the response.
accept builds the catalogue from DeRecShareStore::load_all and replies. reject returns a non-Ok and discloses no catalogue content.
See Discovery.
Recovery
PendingAction::GetShare
PendingAction::GetShareThe peer sent a GetShareRequest. Emitted on the helper side.
channel_id
ChannelId
Channel the request arrived on.
request
GetShareRequestMessage
The decoded inbound request (carries secret_id, version).
shared_key
SharedKey
Channel symmetric key.
trace_id
u64
Trace id, echoed verbatim on the response.
accept returns the stored committed share for (secret_id, version). reject returns a non-Ok and discloses nothing.
See Recovery.
UpdateChannelInfo
PendingAction::UpdateChannelInfo
PendingAction::UpdateChannelInfoThe peer announced an update to their communication_info and/or transport endpoint. Emitted on the receiver side.
channel_id
ChannelId
Channel the request arrived on.
request
UpdateChannelInfoRequestMessage
The decoded inbound request (carries the new fields).
shared_key
SharedKey
Channel symmetric key.
trace_id
u64
Trace id, echoed verbatim on the response.
accept writes the new fields onto the stored Channel record automatically — the application does not call set_communication_info / set_own_transport here. When transport_protocol is part of the update, the response is routed to the new endpoint, so subsequent outbound traffic on this channel already targets the new address. See UpdateChannelInfo for the endpoint-changeover discipline. reject sends a non-Ok response and leaves the stored channel state unchanged.
Unpair
PendingAction::Unpair
PendingAction::UnpairThe peer is asking this node to drop its state for the channel. Emitted on the receiver side.
channel_id
ChannelId
Channel the request arrived on.
request
UnpairRequestMessage
The decoded inbound request (carries memo).
shared_key
SharedKey
Channel symmetric key.
trace_id
u64
Trace id, echoed verbatim on the response.
accept deletes the local channel/share/secret state for this peer and sends back Ok (also emits Unpaired). reject sends a non-Ok response and keeps the state.
See Unpair.
Cross-SDK naming
Variant names differ slightly per SDK but the semantics are identical. The table below maps a representative subset; remaining variants follow the same convention (Rust DeRecEvent::Foo → .NET FooEvent → TypeScript { type: "Foo", ... }).
Events
DeRecEvent::PairingCompleted
PairingCompletedEvent
{ type: "PairingCompleted", ... }
DeRecEvent::ActionRequired
ActionRequiredEvent
{ type: "ActionRequired", ... }
DeRecEvent::ShareStored
ShareStoredEvent
{ type: "ShareStored", ... }
DeRecEvent::SharingComplete
SharingCompleteEvent
{ type: "SharingComplete", ... }
DeRecEvent::ShareVerified
ShareVerifiedEvent
{ type: "ShareVerified", ... }
DeRecEvent::SecretsDiscovered
SecretsDiscoveredEvent
{ type: "SecretsDiscovered", ... }
DeRecEvent::SecretRecovered
SecretRecoveredEvent
{ type: "SecretRecovered", ... }
DeRecEvent::ChannelInfoUpdated
ChannelInfoUpdatedEvent
{ type: "ChannelInfoUpdated", ... }
DeRecEvent::Unpaired
UnpairedEvent
{ type: "Unpaired", ... }
DeRecEvent::NoOp
(filtered by DeRecEventConverter)
{ type: "NoOp" }
Actions
PendingAction does not surface as a top-level discriminator in the bindings — both .NET and TypeScript carry it as the action payload of ActionRequired, with a side-channel discriminator (Kind / action_kind) the application switches on.
PendingAction::Pairing
ActionRequiredEvent with Kind == Pairing
ActionRequired event with action_kind: "pairing"
PendingAction::PrePair
ActionRequiredEvent with Kind == PrePair
ActionRequired event with action_kind: "pre_pair"
PendingAction::StoreShare
ActionRequiredEvent with Kind == StoreShare
ActionRequired event with action_kind: "store_share"
PendingAction::VerifyShare
ActionRequiredEvent with Kind == VerifyShare
ActionRequired event with action_kind: "verify_share"
PendingAction::Discovery
ActionRequiredEvent with Kind == Discovery
ActionRequired event with action_kind: "discovery"
PendingAction::GetShare
ActionRequiredEvent with Kind == GetShare
ActionRequired event with action_kind: "get_share"
PendingAction::UpdateChannelInfo
ActionRequiredEvent with Kind == UpdateChannelInfo
ActionRequired event with action_kind: "update_channel_info"
PendingAction::Unpair
ActionRequiredEvent with Kind == Unpair
ActionRequired event with action_kind: "unpair"
Refer to the SDK chapters for the exact field shapes and decoding helpers each binding ships.
Last updated