Skip to main content

liminal_protocol/lifecycle/operations/
enrollment_operation.rs

1//! Total initial-enrollment operation composition.
2//!
3//! Token lookup runs before every capacity check. Fresh initial enrollment then
4//! composes the shared stage-6, stage-8 through stage-12 selectors and exposes a
5//! single opaque commit. Participant-slot allocation is lazy and occurs only at
6//! stage 13, so a replay or refusal cannot consume the monotone allocator.
7
8use alloc::boxed::Box;
9
10use crate::wire::{
11    AttachSecret, ClosureCheckedEnvelope, EnrollmentEnvelope, EnrollmentRequest,
12    EnrollmentResponse, OrderAllocatingEnvelope, SequenceAllocatingEnvelope,
13};
14
15use super::super::{
16    AllocatedParticipantSlot, AttachedRecordPosition, BindingSlotDecision, BindingSlotOccupancy,
17    BindingState, ConnectionConversationCapacityCommit, ConnectionConversationTracking,
18    EnrollmentCapacityCommit, EnrollmentCapacityCounters, EnrollmentCapacityDecision,
19    EnrollmentCommit, EnrollmentCommitError, EnrollmentCommitParameters, EnrollmentFingerprint,
20    EnrollmentLookupResult, EnrollmentTokenPhase, InitialEnrollmentClosureError,
21    InitialEnrollmentClosureInput, InitialEnrollmentClosureProjection, ObserverCheckedOperation,
22    ObserverFloorDecision, ObserverFloorPermit, OrderAdmissionError, OrderAllocation,
23    ParticipantSlotAllocationError, ParticipantSlotAllocatorProof, RemainingClosureDecision,
24    RemainingClosurePermit, SemanticConnectionCapacityDecision, SequenceAdmission,
25    SequenceAdmissionError, admit_sequence, allocate_order, check_observer_floor,
26    commit_enrollment, lookup_enrollment, project_initial_enrollment_closure,
27    select_enrollment_binding_slot, select_enrollment_capacity,
28    select_semantic_connection_capacity,
29};
30
31/// Persisted prestate read by one initial `EnrollmentRequest` attempt.
32pub struct InitialEnrollmentOperationInput<'a, EF, V, LF> {
33    request: &'a EnrollmentRequest,
34    token_phase: EnrollmentTokenPhase<'a, EF, V, LF>,
35    lookup_binding: &'a BindingState,
36    connection_tracking: ConnectionConversationTracking,
37    connection_capacity: super::super::CapacityCounter,
38    binding_occupancy: BindingSlotOccupancy,
39    enrollment_capacity: EnrollmentCapacityCounters,
40    closure: InitialEnrollmentClosureInput,
41}
42
43impl<'a, EF, V, LF> InitialEnrollmentOperationInput<'a, EF, V, LF> {
44    /// Captures every unchanged persisted fact used by stages 2, 6, and 8-12.
45    #[allow(clippy::too_many_arguments)]
46    #[must_use]
47    pub const fn new(
48        request: &'a EnrollmentRequest,
49        token_phase: EnrollmentTokenPhase<'a, EF, V, LF>,
50        lookup_binding: &'a BindingState,
51        connection_tracking: ConnectionConversationTracking,
52        connection_capacity: super::super::CapacityCounter,
53        binding_occupancy: BindingSlotOccupancy,
54        enrollment_capacity: EnrollmentCapacityCounters,
55        closure: InitialEnrollmentClosureInput,
56    ) -> Self {
57        Self {
58            request,
59            token_phase,
60            lookup_binding,
61            connection_tracking,
62            connection_capacity,
63            binding_occupancy,
64            enrollment_capacity,
65            closure,
66        }
67    }
68}
69
70/// Checked receipt and provenance deadlines derived from one admitted clock read.
71///
72/// Construction widens the monotonic `u64` clock and both validated `u64` TTLs
73/// to `u128` before addition. The provenance deadline therefore cannot precede
74/// the receipt deadline, and neither addition can overflow.
75#[derive(Clone, Copy, Debug, PartialEq, Eq)]
76pub struct ReceiptDeadlines {
77    receipt_expires_at: u128,
78    provenance_expires_at: u128,
79}
80
81impl ReceiptDeadlines {
82    /// Validates TTLs in frozen configuration precedence and derives deadlines.
83    ///
84    /// A zero receipt TTL precedes a zero provenance TTL, which precedes the
85    /// provenance-order check.
86    ///
87    /// # Errors
88    ///
89    /// Returns [`ReceiptDeadlineError`] for the first zero TTL in frozen
90    /// configuration order or when provenance is shorter than the receipt.
91    pub fn try_from_ttls(
92        now_ms: u64,
93        attach_receipt_ttl_ms: u64,
94        receipt_provenance_ttl_ms: u64,
95    ) -> Result<Self, ReceiptDeadlineError> {
96        if attach_receipt_ttl_ms == 0 {
97            return Err(ReceiptDeadlineError::ZeroAttachReceiptTtl);
98        }
99        if receipt_provenance_ttl_ms == 0 {
100            return Err(ReceiptDeadlineError::ZeroReceiptProvenanceTtl);
101        }
102        if receipt_provenance_ttl_ms < attach_receipt_ttl_ms {
103            return Err(ReceiptDeadlineError::ProvenanceTtlShorterThanReceipt {
104                attach_receipt_ttl_ms,
105                receipt_provenance_ttl_ms,
106            });
107        }
108        let widened_now = u128::from(now_ms);
109        Ok(Self {
110            receipt_expires_at: widened_now + u128::from(attach_receipt_ttl_ms),
111            provenance_expires_at: widened_now + u128::from(receipt_provenance_ttl_ms),
112        })
113    }
114
115    /// Validates an absolute durable receipt/provenance deadline pair.
116    ///
117    /// Storage persists the checked results rather than the clock reading that
118    /// derived them, so replay recovers the same typed pair without inventing a
119    /// synthetic clock.
120    ///
121    /// # Errors
122    ///
123    /// Returns [`ReceiptDeadlineError::ZeroAbsoluteReceiptDeadline`] for a zero
124    /// receipt deadline or [`ReceiptDeadlineError::AbsoluteProvenanceBeforeReceipt`]
125    /// when provenance precedes it.
126    pub const fn try_from_absolute(
127        receipt_expires_at: u128,
128        provenance_expires_at: u128,
129    ) -> Result<Self, ReceiptDeadlineError> {
130        if receipt_expires_at == 0 {
131            return Err(ReceiptDeadlineError::ZeroAbsoluteReceiptDeadline);
132        }
133        if provenance_expires_at < receipt_expires_at {
134            return Err(ReceiptDeadlineError::AbsoluteProvenanceBeforeReceipt);
135        }
136        Ok(Self {
137            receipt_expires_at,
138            provenance_expires_at,
139        })
140    }
141
142    /// Returns the checked monotonic receipt deadline.
143    #[must_use]
144    pub const fn receipt_expires_at(self) -> u128 {
145        self.receipt_expires_at
146    }
147
148    /// Returns the checked monotonic provenance deadline.
149    #[must_use]
150    pub const fn provenance_expires_at(self) -> u128 {
151        self.provenance_expires_at
152    }
153}
154
155/// Failure to derive the frozen receipt/provenance deadline pair.
156#[derive(Clone, Copy, Debug, PartialEq, Eq)]
157pub enum ReceiptDeadlineError {
158    /// `attach_receipt_ttl_ms` was zero.
159    ZeroAttachReceiptTtl,
160    /// `receipt_provenance_ttl_ms` was zero after the receipt TTL passed.
161    ZeroReceiptProvenanceTtl,
162    /// Provenance would expire before the receipt it explains.
163    ProvenanceTtlShorterThanReceipt {
164        /// Validated nonzero receipt TTL.
165        attach_receipt_ttl_ms: u64,
166        /// Validated nonzero but insufficient provenance TTL.
167        receipt_provenance_ttl_ms: u64,
168    },
169    /// A durable absolute receipt deadline was zero.
170    ZeroAbsoluteReceiptDeadline,
171    /// A durable absolute provenance deadline preceded its receipt deadline.
172    AbsoluteProvenanceBeforeReceipt,
173}
174
175/// Values minted or deadline-derived only after every admission gate passes.
176#[derive(Clone, Debug, PartialEq, Eq)]
177pub struct InitialEnrollmentCommitValues<F> {
178    attach_secret: AttachSecret,
179    deadlines: ReceiptDeadlines,
180    enrollment_fingerprint: EnrollmentFingerprint<F>,
181}
182
183impl<F> InitialEnrollmentCommitValues<F> {
184    /// Creates the exact generation-one secret, deadlines, and token mapping.
185    #[must_use]
186    pub const fn new(
187        attach_secret: AttachSecret,
188        deadlines: ReceiptDeadlines,
189        enrollment_fingerprint: EnrollmentFingerprint<F>,
190    ) -> Self {
191        Self {
192            attach_secret,
193            deadlines,
194            enrollment_fingerprint,
195        }
196    }
197}
198
199/// Complete atomic initial-enrollment commit.
200///
201/// Every field is produced by a shared protocol selector. A server binding may
202/// persist these values together, but cannot construct this commit directly.
203#[derive(Clone, Debug, PartialEq, Eq)]
204pub struct InitialEnrollmentOperationCommit<F> {
205    enrollment: EnrollmentCommit<F>,
206    connection_capacity: ConnectionConversationCapacityCommit,
207    enrollment_capacity: EnrollmentCapacityCommit,
208    order: OrderAllocation,
209    sequence: SequenceAdmission,
210    observer_floor: ObserverFloorPermit,
211    closure_permit: Box<RemainingClosurePermit>,
212    closure_projection: InitialEnrollmentClosureProjection,
213}
214
215impl<F> InitialEnrollmentOperationCommit<F> {
216    /// Returns membership, binding, Attached record, and canonical receipt.
217    #[must_use]
218    pub const fn enrollment(&self) -> &EnrollmentCommit<F> {
219        &self.enrollment
220    }
221
222    /// Returns the resulting semantic connection-conversation occupancy.
223    #[must_use]
224    pub const fn connection_capacity(&self) -> ConnectionConversationCapacityCommit {
225        self.connection_capacity
226    }
227
228    /// Returns all seven resulting identity/receipt/provenance counters.
229    #[must_use]
230    pub const fn enrollment_capacity(&self) -> EnrollmentCapacityCommit {
231        self.enrollment_capacity
232    }
233
234    /// Returns the allocated caller major and complete resulting order ledger.
235    #[must_use]
236    pub const fn order(&self) -> OrderAllocation {
237        self.order
238    }
239
240    /// Returns the complete admitted sequence ledger.
241    #[must_use]
242    pub const fn sequence(&self) -> SequenceAdmission {
243        self.sequence
244    }
245
246    /// Returns the exact stage-11 floor proof.
247    #[must_use]
248    pub const fn observer_floor(&self) -> ObserverFloorPermit {
249        self.observer_floor
250    }
251
252    /// Returns the exact stage-12 successor-coverage proof.
253    #[must_use]
254    pub const fn closure_permit(&self) -> &RemainingClosurePermit {
255        &self.closure_permit
256    }
257
258    /// Returns the complete persistable floor/retention/debt projection.
259    #[must_use]
260    pub const fn closure_projection(&self) -> &InitialEnrollmentClosureProjection {
261        &self.closure_projection
262    }
263
264    /// Consumes the atomic operation after its frontier owner has been acquired,
265    /// returning the exact enrollment commit for the conversation event layer.
266    ///
267    /// The remaining permits are deliberately consumed with this value: they
268    /// have already been incorporated into the protocol-produced frontier and
269    /// cannot be reused to authorize another transition.
270    #[must_use]
271    pub fn into_enrollment(self) -> EnrollmentCommit<F> {
272        self.enrollment
273    }
274}
275
276/// Internal invariant fault separated from every wire-visible outcome.
277#[derive(Clone, Debug, PartialEq, Eq)]
278pub enum InitialEnrollmentOperationFault {
279    /// Durable/configuration closure facts are malformed.
280    Closure(InitialEnrollmentClosureError),
281    /// Sealed order planning failed without producing wire exhaustion.
282    Order(OrderAdmissionError),
283    /// Sealed sequence planning failed without producing wire exhaustion.
284    Sequence(SequenceAdmissionError),
285    /// Lazy monotone allocator rejected its proof.
286    SlotAllocation(ParticipantSlotAllocationError),
287    /// Allocator and closure projection selected different permanent indices.
288    AllocatedParticipantMismatch {
289        /// Participant derived by the closure projection.
290        expected: u64,
291        /// Participant produced by the allocator proof.
292        actual: u64,
293    },
294    /// Allocator and closure projection used different identity domains.
295    AllocatedIdentityLimitMismatch {
296        /// Validated identity-slot count used by the closure projection.
297        expected: u64,
298        /// Half-open identity limit bound into the allocator proof.
299        actual: u64,
300    },
301    /// Final membership/binding/receipt construction rejected inconsistent data.
302    Commit(EnrollmentCommitError),
303}
304
305/// Exhaustive initial-enrollment operation result.
306#[derive(Clone, Debug, PartialEq, Eq)]
307pub enum InitialEnrollmentOperationDecision<F> {
308    /// Exact stable replay or first applicable wire refusal.
309    Respond(EnrollmentResponse),
310    /// Every stage passed and all resulting state may commit atomically.
311    Commit(Box<InitialEnrollmentOperationCommit<F>>),
312    /// Durable/configuration state violated a protocol invariant.
313    Fault(InitialEnrollmentOperationFault),
314}
315
316struct InitialEnrollmentCapacityPermits {
317    connection: ConnectionConversationCapacityCommit,
318    enrollment: EnrollmentCapacityCommit,
319}
320
321struct OrderedInitialEnrollment {
322    projection: InitialEnrollmentClosureProjection,
323    order: OrderAllocation,
324}
325
326struct ClosedInitialEnrollment {
327    projection: InitialEnrollmentClosureProjection,
328    order: OrderAllocation,
329    sequence: SequenceAdmission,
330    observer_floor: ObserverFloorPermit,
331    closure_permit: Box<RemainingClosurePermit>,
332}
333
334enum InitialEnrollmentGateFailure {
335    Respond(Box<EnrollmentResponse>),
336    Fault(Box<InitialEnrollmentOperationFault>),
337}
338
339impl InitialEnrollmentGateFailure {
340    fn respond(value: EnrollmentResponse) -> Self {
341        Self::Respond(Box::new(value))
342    }
343
344    fn fault(value: InitialEnrollmentOperationFault) -> Self {
345        Self::Fault(Box::new(value))
346    }
347
348    fn into_decision<F>(self) -> InitialEnrollmentOperationDecision<F> {
349        match self {
350            Self::Respond(value) => InitialEnrollmentOperationDecision::Respond(*value),
351            Self::Fault(value) => InitialEnrollmentOperationDecision::Fault(*value),
352        }
353    }
354}
355
356/// Applies frozen stages 2, 6, and 8-13 to initial enrollment.
357///
358/// Lookup/tombstone/receipt replay precedes semantic connection capacity. Fresh
359/// enrollment then checks semantic capacity, binding-slot occupancy, the five
360/// reachable runtime-capacity scopes, order, sequence, hard observer retention,
361/// remaining closure, and only then invokes the lazy slot allocator and
362/// [`commit_enrollment`]. No refusal exposes a partial commit.
363#[must_use]
364pub fn apply_initial_enrollment<EF, V, LF, F, P, A, M>(
365    input: &InitialEnrollmentOperationInput<'_, EF, V, LF>,
366    mint_commit_values: M,
367    allocate_participant: A,
368) -> InitialEnrollmentOperationDecision<F>
369where
370    P: ParticipantSlotAllocatorProof,
371    A: FnOnce() -> Result<AllocatedParticipantSlot<P>, ParticipantSlotAllocationError>,
372    M: FnOnce() -> InitialEnrollmentCommitValues<F>,
373{
374    let envelope = enrollment_envelope(input.request);
375    if let Some(response) = initial_enrollment_lookup_response(input) {
376        return InitialEnrollmentOperationDecision::Respond(response);
377    }
378    let capacity = match admit_initial_enrollment_capacity(input, &envelope) {
379        Ok(value) => value,
380        Err(error) => return error.into_decision(),
381    };
382    let ordered = match plan_initial_enrollment_order(&input.closure, &envelope) {
383        Ok(value) => value,
384        Err(error) => return error.into_decision(),
385    };
386    let closed = match close_initial_enrollment(ordered, &envelope) {
387        Ok(value) => value,
388        Err(error) => return error.into_decision(),
389    };
390    commit_initial_enrollment(
391        input.request,
392        &capacity,
393        closed,
394        mint_commit_values,
395        allocate_participant,
396    )
397}
398
399fn initial_enrollment_lookup_response<EF, V, LF>(
400    input: &InitialEnrollmentOperationInput<'_, EF, V, LF>,
401) -> Option<EnrollmentResponse> {
402    match lookup_enrollment(input.token_phase, input.lookup_binding, input.request) {
403        EnrollmentLookupResult::Retired(value) => Some(EnrollmentResponse::from_retired(value)),
404        EnrollmentLookupResult::Bound(value) => Some(EnrollmentResponse::from_bound(value)),
405        EnrollmentLookupResult::UnboundReceipt(value) => {
406            Some(EnrollmentResponse::from_unbound_receipt(value))
407        }
408        EnrollmentLookupResult::ReceiptExpired(value) => {
409            Some(EnrollmentResponse::from_receipt_expired(value))
410        }
411        EnrollmentLookupResult::EnrollmentKnown(value) => {
412            Some(EnrollmentResponse::enrollment_known(value))
413        }
414        EnrollmentLookupResult::AuthorizedNew => None,
415    }
416}
417
418fn admit_initial_enrollment_capacity<EF, V, LF>(
419    input: &InitialEnrollmentOperationInput<'_, EF, V, LF>,
420    envelope: &EnrollmentEnvelope,
421) -> Result<InitialEnrollmentCapacityPermits, InitialEnrollmentGateFailure> {
422    let connection = match select_semantic_connection_capacity(
423        input.connection_tracking,
424        input.connection_capacity,
425    ) {
426        SemanticConnectionCapacityDecision::Commit(value) => value,
427        SemanticConnectionCapacityDecision::Respond { limit } => {
428            return Err(InitialEnrollmentGateFailure::respond(
429                EnrollmentResponse::connection_conversation_capacity_exceeded(
430                    envelope.clone(),
431                    limit,
432                ),
433            ));
434        }
435    };
436    if let BindingSlotDecision::Respond(value) =
437        select_enrollment_binding_slot(input.request, input.binding_occupancy)
438    {
439        return Err(InitialEnrollmentGateFailure::respond(value));
440    }
441    let enrollment = match select_enrollment_capacity(input.request, input.enrollment_capacity) {
442        EnrollmentCapacityDecision::Commit(value) => value,
443        EnrollmentCapacityDecision::Respond(value) => {
444            return Err(InitialEnrollmentGateFailure::respond(value));
445        }
446    };
447    Ok(InitialEnrollmentCapacityPermits {
448        connection,
449        enrollment,
450    })
451}
452
453fn plan_initial_enrollment_order(
454    closure: &InitialEnrollmentClosureInput,
455    envelope: &EnrollmentEnvelope,
456) -> Result<OrderedInitialEnrollment, InitialEnrollmentGateFailure> {
457    let projection = project_initial_enrollment_closure(*closure).map_err(|error| {
458        InitialEnrollmentGateFailure::fault(InitialEnrollmentOperationFault::Closure(error))
459    })?;
460    let order_plan = projection
461        .plan_order()
462        .map_err(initial_enrollment_order_failure)?;
463    let order = allocate_order(
464        OrderAllocatingEnvelope::Enrollment(envelope.clone()),
465        projection.current_order(),
466        order_plan,
467    )
468    .map_err(initial_enrollment_order_failure)?;
469    Ok(OrderedInitialEnrollment { projection, order })
470}
471
472fn initial_enrollment_order_failure(error: OrderAdmissionError) -> InitialEnrollmentGateFailure {
473    match error {
474        OrderAdmissionError::Exhausted(value) => InitialEnrollmentGateFailure::respond(
475            EnrollmentResponse::from_conversation_order_exhausted(value),
476        ),
477        other => InitialEnrollmentGateFailure::fault(InitialEnrollmentOperationFault::Order(other)),
478    }
479}
480
481fn close_initial_enrollment(
482    ordered: OrderedInitialEnrollment,
483    envelope: &EnrollmentEnvelope,
484) -> Result<ClosedInitialEnrollment, InitialEnrollmentGateFailure> {
485    let sequence_plan = ordered
486        .projection
487        .plan_sequence()
488        .map_err(initial_enrollment_sequence_failure)?;
489    let sequence = admit_sequence(
490        SequenceAllocatingEnvelope::Enrollment(envelope.clone()),
491        sequence_plan,
492    )
493    .map_err(initial_enrollment_sequence_failure)?;
494    let observer_floor = match check_observer_floor(
495        ObserverCheckedOperation::Enrollment(envelope.clone()),
496        ordered.projection.observer_progress(),
497        ordered.projection.resulting_floor(),
498    ) {
499        ObserverFloorDecision::Eligible(value) => value,
500        ObserverFloorDecision::Respond(value) => {
501            return Err(InitialEnrollmentGateFailure::respond(
502                EnrollmentResponse::from_observer_backpressure(value),
503            ));
504        }
505    };
506    let closure_permit = match ordered
507        .projection
508        .remaining_closure_decision(&ClosureCheckedEnvelope::Enrollment(envelope.clone()))
509    {
510        RemainingClosureDecision::Eligible(value) => value,
511        RemainingClosureDecision::Respond(value) => {
512            return Err(InitialEnrollmentGateFailure::respond(
513                EnrollmentResponse::from_marker_closure_capacity_exceeded(value),
514            ));
515        }
516    };
517    Ok(ClosedInitialEnrollment {
518        projection: ordered.projection,
519        order: ordered.order,
520        sequence,
521        observer_floor,
522        closure_permit,
523    })
524}
525
526fn initial_enrollment_sequence_failure(
527    error: SequenceAdmissionError,
528) -> InitialEnrollmentGateFailure {
529    match error {
530        SequenceAdmissionError::Exhausted(value) => InitialEnrollmentGateFailure::respond(
531            EnrollmentResponse::from_conversation_sequence_exhausted(value),
532        ),
533        other => {
534            InitialEnrollmentGateFailure::fault(InitialEnrollmentOperationFault::Sequence(other))
535        }
536    }
537}
538
539fn commit_initial_enrollment<F, P, A, M>(
540    request: &EnrollmentRequest,
541    capacity: &InitialEnrollmentCapacityPermits,
542    closed: ClosedInitialEnrollment,
543    mint_commit_values: M,
544    allocate_participant: A,
545) -> InitialEnrollmentOperationDecision<F>
546where
547    P: ParticipantSlotAllocatorProof,
548    A: FnOnce() -> Result<AllocatedParticipantSlot<P>, ParticipantSlotAllocationError>,
549    M: FnOnce() -> InitialEnrollmentCommitValues<F>,
550{
551    let participant_slot = match allocate_participant() {
552        Ok(value) => value,
553        Err(error) => {
554            return InitialEnrollmentOperationDecision::Fault(
555                InitialEnrollmentOperationFault::SlotAllocation(error),
556            );
557        }
558    };
559    if participant_slot.participant_id() != closed.projection.participant_index() {
560        return InitialEnrollmentOperationDecision::Fault(
561            InitialEnrollmentOperationFault::AllocatedParticipantMismatch {
562                expected: closed.projection.participant_index(),
563                actual: participant_slot.participant_id(),
564            },
565        );
566    }
567    if participant_slot.identity_limit() != closed.projection.identity_slots() {
568        return InitialEnrollmentOperationDecision::Fault(
569            InitialEnrollmentOperationFault::AllocatedIdentityLimitMismatch {
570                expected: closed.projection.identity_slots(),
571                actual: participant_slot.identity_limit(),
572            },
573        );
574    }
575    let commit_values = mint_commit_values();
576    let enrollment = match commit_enrollment(
577        request,
578        EnrollmentCommitParameters {
579            allocated_slot: participant_slot,
580            attach_secret: commit_values.attach_secret,
581            origin_binding_epoch: closed.projection.binding_epoch(),
582            attached_position: AttachedRecordPosition::new(
583                closed.order.major(),
584                closed.sequence.resulting().high_watermark(),
585            ),
586            receipt_expires_at: commit_values.deadlines.receipt_expires_at(),
587            provenance_expires_at: commit_values.deadlines.provenance_expires_at(),
588            enrollment_fingerprint: commit_values.enrollment_fingerprint,
589        },
590    ) {
591        Ok(value) => value,
592        Err(error) => {
593            return InitialEnrollmentOperationDecision::Fault(
594                InitialEnrollmentOperationFault::Commit(error),
595            );
596        }
597    };
598
599    InitialEnrollmentOperationDecision::Commit(Box::new(InitialEnrollmentOperationCommit {
600        enrollment,
601        connection_capacity: capacity.connection,
602        enrollment_capacity: capacity.enrollment,
603        order: closed.order,
604        sequence: closed.sequence,
605        observer_floor: closed.observer_floor,
606        closure_permit: closed.closure_permit,
607        closure_projection: closed.projection,
608    }))
609}
610
611const fn enrollment_envelope(request: &EnrollmentRequest) -> EnrollmentEnvelope {
612    EnrollmentEnvelope {
613        conversation_id: request.conversation_id,
614        enrollment_token: request.enrollment_token,
615    }
616}