Skip to main content

liminal_protocol/wire/
request.rs

1use alloc::vec::Vec;
2
3use super::{
4    AttachAttemptToken, AttachSecret, ClientDiscriminant, ConversationId, DeliverySeq,
5    DetachAttemptToken, EnrollmentToken, Generation, LeaveAttemptToken, ObserverEpoch,
6    ParticipantId, RecordAdmissionAttemptToken,
7};
8
9/// Enrollment request body (`0x0001`).
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct EnrollmentRequest {
12    /// Conversation to enroll in.
13    pub conversation_id: ConversationId,
14    /// Durable single-purpose enrollment token.
15    pub enrollment_token: EnrollmentToken,
16}
17
18/// Credential-bearing attach request body (`0x0002`).
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub struct CredentialAttachRequest {
21    /// Conversation containing the participant.
22    pub conversation_id: ConversationId,
23    /// Permanent participant identity.
24    pub participant_id: ParticipantId,
25    /// Presented nonzero credential generation.
26    pub capability_generation: Generation,
27    /// Presented attach secret.
28    pub attach_secret: AttachSecret,
29    /// Durable single-purpose attach token.
30    pub attach_attempt_token: AttachAttemptToken,
31    /// Marker accepted atomically by a fenced recovery attach.
32    pub accept_marker_delivery_seq: Option<DeliverySeq>,
33}
34
35/// Explicit detach request body (`0x0003`).
36#[derive(Clone, Debug, PartialEq, Eq)]
37pub struct DetachRequest {
38    /// Conversation containing the participant.
39    pub conversation_id: ConversationId,
40    /// Permanent participant identity.
41    pub participant_id: ParticipantId,
42    /// Presented nonzero credential generation.
43    pub capability_generation: Generation,
44    /// Durable single-purpose detach token.
45    pub detach_attempt_token: DetachAttemptToken,
46}
47
48/// Continuous cumulative acknowledgement body (`0x0004`).
49#[derive(Clone, Debug, PartialEq, Eq)]
50pub struct ParticipantAck {
51    /// Conversation containing the participant.
52    pub conversation_id: ConversationId,
53    /// Permanent participant identity.
54    pub participant_id: ParticipantId,
55    /// Presented nonzero credential generation.
56    pub capability_generation: Generation,
57    /// Greatest continuously available sequence being acknowledged.
58    pub through_seq: DeliverySeq,
59}
60
61/// Terminal participant Leave body (`0x0005`).
62#[derive(Clone, Debug, PartialEq, Eq)]
63pub struct LeaveRequest {
64    /// Conversation containing the participant.
65    pub conversation_id: ConversationId,
66    /// Permanent participant identity.
67    pub participant_id: ParticipantId,
68    /// Presented nonzero credential generation.
69    pub capability_generation: Generation,
70    /// Presented attach secret; it is never echoed in a response envelope.
71    pub attach_secret: AttachSecret,
72    /// Durable single-purpose Leave token.
73    pub leave_attempt_token: LeaveAttemptToken,
74}
75
76/// Explicit marker acknowledgement body (`0x0006`).
77#[derive(Clone, Debug, PartialEq, Eq)]
78pub struct MarkerAck {
79    /// Conversation containing the participant.
80    pub conversation_id: ConversationId,
81    /// Permanent participant identity.
82    pub participant_id: ParticipantId,
83    /// Presented nonzero credential generation.
84    pub capability_generation: Generation,
85    /// Delivered marker being accepted.
86    pub marker_delivery_seq: DeliverySeq,
87}
88
89/// Ordinary record-admission body (`0x0007`).
90#[derive(Clone, Debug, PartialEq, Eq)]
91pub struct RecordAdmission {
92    /// Conversation receiving the record.
93    pub conversation_id: ConversationId,
94    /// Verified sender participant.
95    pub participant_id: ParticipantId,
96    /// Presented nonzero credential generation.
97    pub capability_generation: Generation,
98    /// Client-selected identity of this record-admission request attempt.
99    ///
100    /// # It must be minted once per RECORD, not once per presentation
101    ///
102    /// This token is the client's half of ordinary-admission idempotence
103    /// (contract amendment A2, §0.13). The server deduplicates on the identity
104    /// triple (token, payload fingerprint, verified participant), so an
105    /// answer-lost re-present is answered with the original commit only if it
106    /// arrives carrying the SAME token.
107    ///
108    /// Mint it once when the record is staged, persist it beside the staged
109    /// bytes, and re-present that exact token after a lost answer — the
110    /// write-ahead discipline R-C0 already requires for the tokenized families,
111    /// applied one layer up.
112    ///
113    /// Deriving it per presentation from anything that can change between
114    /// attempts silently defeats this. That is not hypothetical: in the field
115    /// (2026-08-08, conversation 6) a client derived the token per presentation
116    /// with the CURRENT capability generation as an input, a recovery attach
117    /// rotated the generation 4 -> 6 between the two presentations, and the
118    /// same bytes arrived under two different tokens — committing a
119    /// byte-identical second copy at a new sequence.
120    ///
121    /// The server cannot close this from its side, and deliberately does not
122    /// try: two intent-distinct sends of the same body also carry distinct
123    /// tokens and MUST remain two commits, so any dedup keyed on payload bytes
124    /// alone would collapse a legitimate pair. Both halves are pinned in
125    /// `tests_record_admission_dedup`.
126    ///
127    /// Re-enrolling changes the verified participant and therefore the triple,
128    /// so it defeats dedup by design.
129    pub record_admission_attempt_token: RecordAdmissionAttemptToken,
130    /// Opaque application payload; it is never echoed in a response envelope.
131    pub payload: Vec<u8>,
132}
133
134/// One observer refusal supplied during reconnect recovery.
135#[derive(Clone, Copy, Debug, PartialEq, Eq)]
136pub struct ObserverRefusal {
137    /// Refused conversation.
138    pub conversation_id: ConversationId,
139    /// Refusal epoch the SDK needs to arm or classify.
140    pub refused_epoch: ObserverEpoch,
141}
142
143/// One-shot observer-recovery batch body (`0x0008`).
144#[derive(Clone, Debug, PartialEq, Eq)]
145pub struct ObserverRecoveryHandshake {
146    /// Request-ordered refusal list. Its wire count is the special `u64` count.
147    pub observer_refusals: Vec<ObserverRefusal>,
148}
149
150/// Exhaustive client-to-server participant request.
151#[derive(Clone, Debug, PartialEq, Eq)]
152pub enum ClientRequest {
153    /// `0x0001` enrollment request.
154    Enrollment(EnrollmentRequest),
155    /// `0x0002` credential attach request.
156    CredentialAttach(CredentialAttachRequest),
157    /// `0x0003` explicit detach request.
158    Detach(DetachRequest),
159    /// `0x0004` continuous acknowledgement.
160    ParticipantAck(ParticipantAck),
161    /// `0x0005` terminal Leave request.
162    Leave(LeaveRequest),
163    /// `0x0006` marker acknowledgement.
164    MarkerAck(MarkerAck),
165    /// `0x0007` ordinary record admission.
166    RecordAdmission(RecordAdmission),
167    /// `0x0008` reconnect recovery batch.
168    ObserverRecovery(ObserverRecoveryHandshake),
169}
170
171impl ClientRequest {
172    /// Returns the stable request discriminant.
173    #[must_use]
174    pub const fn discriminant(&self) -> ClientDiscriminant {
175        match self {
176            Self::Enrollment(_) => ClientDiscriminant::EnrollmentRequest,
177            Self::CredentialAttach(_) => ClientDiscriminant::CredentialAttachRequest,
178            Self::Detach(_) => ClientDiscriminant::DetachRequest,
179            Self::ParticipantAck(_) => ClientDiscriminant::ParticipantAck,
180            Self::Leave(_) => ClientDiscriminant::LeaveRequest,
181            Self::MarkerAck(_) => ClientDiscriminant::MarkerAck,
182            Self::RecordAdmission(_) => ClientDiscriminant::RecordAdmission,
183            Self::ObserverRecovery(_) => ClientDiscriminant::ObserverRecoveryHandshake,
184        }
185    }
186}