Skip to main content

rvoip_core_traits/
adapter.rs

1use crate::capability::CapabilityDescriptor;
2use crate::connection::{Connection, Direction, Transport};
3use crate::data::DataMessage;
4use crate::identity::{AuthenticatedPrincipal, IdentityAssurance, Jwk, PrincipalOwnershipKey};
5use crate::ids::{ConnectionId, ParticipantId, PlaybackId, SessionId, TransferAttemptId};
6use crate::stream::QualitySnapshot;
7use std::any::Any;
8use std::fmt;
9use std::sync::Arc;
10use thiserror::Error;
11use tokio::sync::oneshot;
12use zeroize::Zeroize;
13
14/// Maximum UTF-8 size of an adapter-owned inbound routing hint.
15pub const MAX_INBOUND_ROUTING_HINT_BYTES: usize = 2_048;
16/// Maximum number of ordered metadata fields retained for one inbound leg.
17pub const MAX_INBOUND_METADATA_FIELDS: usize = 32;
18/// Maximum UTF-8 size of one inbound metadata field name.
19pub const MAX_INBOUND_METADATA_NAME_BYTES: usize = 128;
20/// Maximum UTF-8 size of one inbound metadata field value.
21pub const MAX_INBOUND_METADATA_VALUE_BYTES: usize = 4_096;
22/// Maximum aggregate UTF-8 size of inbound metadata names and values.
23pub const MAX_INBOUND_METADATA_BYTES: usize = 16 * 1_024;
24/// Maximum number of adapter-owned external identifiers on one activation.
25pub const MAX_EXTERNAL_CONNECTION_REFERENCES: usize = 16;
26/// Maximum UTF-8 size of an external identifier namespace.
27pub const MAX_EXTERNAL_REFERENCE_KIND_BYTES: usize = 64;
28/// Maximum UTF-8 size of one external identifier value.
29pub const MAX_EXTERNAL_REFERENCE_VALUE_BYTES: usize = 4_096;
30
31/// Validation failure for an adapter-owned external connection reference.
32///
33/// Variants deliberately contain no adapter-provided text so formatting an
34/// error cannot disclose identifier values.
35#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
36pub enum ExternalConnectionReferenceError {
37    #[error("the external reference kind is empty")]
38    EmptyKind,
39    #[error("the external reference kind is too large")]
40    KindTooLarge,
41    #[error("the external reference kind is not a valid token")]
42    InvalidKind,
43    #[error("the external reference value is empty")]
44    EmptyValue,
45    #[error("the external reference value is too large")]
46    ValueTooLarge,
47    #[error("the external reference value contains forbidden control characters")]
48    InvalidValue,
49    #[error("there are too many external connection references")]
50    TooManyReferences,
51}
52
53/// Validation failure for adapter-supplied inbound connection context.
54///
55/// Variants deliberately contain no user-controlled text so formatting an
56/// error cannot disclose a routing secret or signaling metadata value.
57#[derive(Clone, Copy, Debug, Eq, Error, PartialEq)]
58pub enum InboundContextError {
59    #[error("the authenticated principal has no tenant")]
60    MissingTenant,
61    #[error("the authenticated principal has expired")]
62    ExpiredPrincipal,
63    #[error("the inbound routing hint is empty")]
64    EmptyRoutingHint,
65    #[error("the inbound routing hint is too large")]
66    RoutingHintTooLarge,
67    #[error("the inbound routing hint contains forbidden control characters")]
68    InvalidRoutingHint,
69    #[error("there are too many inbound metadata fields")]
70    TooManyMetadataFields,
71    #[error("an inbound metadata name is invalid")]
72    InvalidMetadataName,
73    #[error("an inbound metadata name is too large")]
74    MetadataNameTooLarge,
75    #[error("an inbound metadata value is too large")]
76    MetadataValueTooLarge,
77    #[error("an inbound metadata value contains forbidden characters")]
78    InvalidMetadataValue,
79    #[error("the aggregate inbound metadata is too large")]
80    MetadataTooLarge,
81}
82
83/// Opaque, redacted routing material captured before an inbound adapter
84/// normalizes its signaling event.
85///
86/// This value intentionally implements neither `Display` nor serialization.
87/// Callers must explicitly opt in to reading it through [`Self::expose_secret`].
88#[derive(Eq, PartialEq)]
89pub struct InboundRoutingHint(String);
90
91impl InboundRoutingHint {
92    pub fn new(value: impl Into<String>) -> Result<Self, InboundContextError> {
93        let value = value.into();
94        if value.is_empty() {
95            return Err(InboundContextError::EmptyRoutingHint);
96        }
97        if value.len() > MAX_INBOUND_ROUTING_HINT_BYTES {
98            return Err(InboundContextError::RoutingHintTooLarge);
99        }
100        if value.chars().any(char::is_control) {
101            return Err(InboundContextError::InvalidRoutingHint);
102        }
103        Ok(Self(value))
104    }
105
106    /// Reveal the routing hint to the policy layer that will consume it.
107    /// Avoid formatting or logging the returned value.
108    pub fn expose_secret(&self) -> &str {
109        &self.0
110    }
111
112    /// Transfer the owned routing material to the policy layer.
113    ///
114    /// The returned `String` remains sensitive and its final owner must
115    /// zeroize it. The wrapper's `Drop` implementation clears any value that
116    /// is not explicitly transferred.
117    pub fn into_secret(mut self) -> String {
118        std::mem::take(&mut self.0)
119    }
120}
121
122impl Drop for InboundRoutingHint {
123    fn drop(&mut self) {
124        self.0.zeroize();
125    }
126}
127
128impl fmt::Debug for InboundRoutingHint {
129    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
130        formatter.write_str("InboundRoutingHint([redacted])")
131    }
132}
133
134/// Ordered, duplicate-preserving signaling metadata captured for an inbound
135/// connection. Field values are redacted from `Debug` output.
136#[derive(Default, Eq, PartialEq)]
137pub struct InboundSignalingMetadata {
138    entries: Vec<(String, String)>,
139}
140
141impl InboundSignalingMetadata {
142    pub fn new<I, N, V>(entries: I) -> Result<Self, InboundContextError>
143    where
144        I: IntoIterator<Item = (N, V)>,
145        N: Into<String>,
146        V: Into<String>,
147    {
148        let entries = entries
149            .into_iter()
150            .map(|(name, value)| (name.into(), value.into()))
151            .collect::<Vec<_>>();
152        if entries.len() > MAX_INBOUND_METADATA_FIELDS {
153            return Err(InboundContextError::TooManyMetadataFields);
154        }
155
156        let mut aggregate_bytes = 0usize;
157        let mut normalized = Vec::with_capacity(entries.len());
158        for (name, value) in entries {
159            if name.len() > MAX_INBOUND_METADATA_NAME_BYTES {
160                return Err(InboundContextError::MetadataNameTooLarge);
161            }
162            if name.is_empty() || !name.bytes().all(is_metadata_name_byte) {
163                return Err(InboundContextError::InvalidMetadataName);
164            }
165            if value.len() > MAX_INBOUND_METADATA_VALUE_BYTES {
166                return Err(InboundContextError::MetadataValueTooLarge);
167            }
168            if value
169                .chars()
170                .any(|character| character != '\t' && character.is_control())
171            {
172                return Err(InboundContextError::InvalidMetadataValue);
173            }
174            aggregate_bytes = aggregate_bytes
175                .checked_add(name.len())
176                .and_then(|total| total.checked_add(value.len()))
177                .ok_or(InboundContextError::MetadataTooLarge)?;
178            if aggregate_bytes > MAX_INBOUND_METADATA_BYTES {
179                return Err(InboundContextError::MetadataTooLarge);
180            }
181            normalized.push((name.to_ascii_lowercase(), value));
182        }
183        Ok(Self {
184            entries: normalized,
185        })
186    }
187
188    pub fn is_empty(&self) -> bool {
189        self.entries.is_empty()
190    }
191
192    pub fn len(&self) -> usize {
193        self.entries.len()
194    }
195
196    /// Iterate over metadata values. Avoid formatting or logging the values.
197    pub fn iter(&self) -> impl ExactSizeIterator<Item = (&str, &str)> {
198        self.entries
199            .iter()
200            .map(|(name, value)| (name.as_str(), value.as_str()))
201    }
202
203    pub fn values<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a str> + 'a {
204        self.entries.iter().filter_map(move |(candidate, value)| {
205            candidate
206                .eq_ignore_ascii_case(name)
207                .then_some(value.as_str())
208        })
209    }
210}
211
212impl fmt::Debug for InboundSignalingMetadata {
213    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
214        formatter
215            .debug_struct("InboundSignalingMetadata")
216            .field("field_count", &self.entries.len())
217            .finish()
218    }
219}
220
221impl Drop for InboundSignalingMetadata {
222    fn drop(&mut self) {
223        for (name, value) in &mut self.entries {
224            name.zeroize();
225            value.zeroize();
226        }
227    }
228}
229
230const fn is_metadata_name_byte(byte: u8) -> bool {
231    byte.is_ascii_alphanumeric()
232        || matches!(
233            byte,
234            b'!' | b'#'
235                | b'$'
236                | b'%'
237                | b'&'
238                | b'\''
239                | b'*'
240                | b'+'
241                | b'-'
242                | b'.'
243                | b'^'
244                | b'_'
245                | b'`'
246                | b'|'
247                | b'~'
248        )
249}
250
251/// Principal- and transport-bound context for one inbound connection.
252///
253/// Adapters may expose this exactly once. The Orchestrator retains it until
254/// an authenticated owner consumes it or the connection terminates.
255pub struct InboundConnectionContext {
256    connection_id: ConnectionId,
257    transport: Transport,
258    owner: PrincipalOwnershipKey,
259    routing_hint: Option<InboundRoutingHint>,
260    metadata: InboundSignalingMetadata,
261}
262
263impl InboundConnectionContext {
264    pub fn new(
265        connection_id: ConnectionId,
266        transport: Transport,
267        principal: &AuthenticatedPrincipal,
268        routing_hint: Option<InboundRoutingHint>,
269        metadata: InboundSignalingMetadata,
270    ) -> Result<Self, InboundContextError> {
271        if principal.tenant.as_deref().is_none_or(str::is_empty) {
272            return Err(InboundContextError::MissingTenant);
273        }
274        if principal.is_expired() {
275            return Err(InboundContextError::ExpiredPrincipal);
276        }
277        Ok(Self {
278            connection_id,
279            transport,
280            owner: principal.ownership_key(),
281            routing_hint,
282            metadata,
283        })
284    }
285
286    pub fn connection_id(&self) -> &ConnectionId {
287        &self.connection_id
288    }
289
290    pub const fn transport(&self) -> Transport {
291        self.transport
292    }
293
294    pub fn routing_hint(&self) -> Option<&InboundRoutingHint> {
295        self.routing_hint.as_ref()
296    }
297
298    /// Take the routing hint exactly once for an owning policy boundary.
299    /// Untaken routing material is zeroized when this context is dropped.
300    pub fn take_routing_hint(&mut self) -> Option<InboundRoutingHint> {
301        self.routing_hint.take()
302    }
303
304    pub fn metadata(&self) -> &InboundSignalingMetadata {
305        &self.metadata
306    }
307
308    pub fn is_bound_to(
309        &self,
310        connection_id: &ConnectionId,
311        transport: Transport,
312        principal: &AuthenticatedPrincipal,
313    ) -> bool {
314        !principal.is_expired()
315            && self.connection_id == *connection_id
316            && self.transport == transport
317            && self.owner == principal.ownership_key()
318    }
319}
320
321impl fmt::Debug for InboundConnectionContext {
322    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
323        formatter
324            .debug_struct("InboundConnectionContext")
325            .field("connection_id", &self.connection_id)
326            .field("transport", &self.transport)
327            .field("owner", &"[redacted]")
328            .field("has_routing_hint", &self.routing_hint.is_some())
329            .field("metadata_field_count", &self.metadata.len())
330            .finish()
331    }
332}
333
334#[derive(Clone, Copy, Debug, Eq, PartialEq)]
335pub enum AdapterKind {
336    /// UCTP-native (QUIC, WebTransport, WebSocket).
337    Substrate,
338    /// Gateway to a foreign protocol (SIP, WebRTC).
339    Interop,
340}
341
342/// Opaque adapter-owned options for one outbound originate operation.
343///
344/// Core transports this value without inspecting it. An adapter defines its
345/// own context type and recovers it with [`Self::downcast_ref`] or
346/// [`Self::downcast_arc`]. The concrete type and value are intentionally
347/// omitted from `Debug`; this type implements neither `Display` nor
348/// serialization.
349#[derive(Clone, Default)]
350pub struct OriginateContext {
351    inner: Option<Arc<dyn Any + Send + Sync>>,
352}
353
354impl OriginateContext {
355    /// Wrap adapter-owned, immutable originate options.
356    pub fn new<T>(value: T) -> Self
357    where
358        T: Any + Send + Sync,
359    {
360        Self {
361            inner: Some(Arc::new(value)),
362        }
363    }
364
365    /// Whether this request carries no adapter-owned options.
366    pub fn is_empty(&self) -> bool {
367        self.inner.is_none()
368    }
369
370    /// Borrow the adapter-owned options when their concrete type matches.
371    pub fn downcast_ref<T>(&self) -> Option<&T>
372    where
373        T: Any + Send + Sync,
374    {
375        self.inner.as_deref()?.downcast_ref::<T>()
376    }
377
378    /// Clone the shared adapter-owned options when their concrete type
379    /// matches. A failed downcast does not consume or reveal the context.
380    pub fn downcast_arc<T>(&self) -> Option<Arc<T>>
381    where
382        T: Any + Send + Sync,
383    {
384        Arc::clone(self.inner.as_ref()?).downcast::<T>().ok()
385    }
386}
387
388impl fmt::Debug for OriginateContext {
389    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
390        formatter
391            .debug_struct("OriginateContext")
392            .field("present", &self.inner.is_some())
393            .finish()
394    }
395}
396
397#[derive(Clone)]
398pub struct OriginateRequest {
399    pub session_id: SessionId,
400    pub participant_id: ParticipantId,
401    pub target: String,
402    pub direction: Direction,
403    pub capabilities: CapabilityDescriptor,
404    /// P6 — transport selector. When `Some`, the Orchestrator
405    /// dispatches the originate through the adapter registered for
406    /// this transport. When `None`, the "first registered adapter"
407    /// fallback applies (single-adapter deployments).
408    pub transport: Option<Transport>,
409    /// Opaque, typed options interpreted only by the selected adapter.
410    pub context: OriginateContext,
411}
412
413impl OriginateRequest {
414    /// Build a transport-neutral outbound request with no adapter context.
415    /// Select a transport with [`Self::with_transport`] when more than one
416    /// adapter is registered.
417    pub fn new(
418        session_id: SessionId,
419        participant_id: ParticipantId,
420        target: impl Into<String>,
421        direction: Direction,
422        capabilities: CapabilityDescriptor,
423    ) -> Self {
424        Self {
425            session_id,
426            participant_id,
427            target: target.into(),
428            direction,
429            capabilities,
430            transport: None,
431            context: OriginateContext::default(),
432        }
433    }
434
435    /// Select the adapter transport for this request.
436    pub fn with_transport(mut self, transport: Transport) -> Self {
437        self.transport = Some(transport);
438        self
439    }
440
441    /// Attach options owned and interpreted by the selected adapter.
442    pub fn with_context<T>(mut self, context: T) -> Self
443    where
444        T: Any + Send + Sync,
445    {
446        self.context = OriginateContext::new(context);
447        self
448    }
449
450    /// Attach an already type-erased adapter context.
451    pub fn with_originate_context(mut self, context: OriginateContext) -> Self {
452        self.context = context;
453        self
454    }
455}
456
457impl fmt::Debug for OriginateRequest {
458    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
459        formatter
460            .debug_struct("OriginateRequest")
461            .field("session_id", &self.session_id)
462            .field("participant_id", &self.participant_id)
463            .field("target", &"[redacted]")
464            .field("direction", &self.direction)
465            .field("capabilities", &self.capabilities)
466            .field("transport", &self.transport)
467            .field("context_present", &!self.context.is_empty())
468            .finish()
469    }
470}
471
472/// A stable external identifier learned while activating an outbound route.
473///
474/// The identifier namespace is adapter-owned (for example an adapter may use
475/// `sip.call-id`). Core treats both namespace and value as opaque. The value
476/// is deliberately available only through [`Self::expose_secret`], and
477/// formatting never reveals either component.
478#[derive(Clone, Eq, Hash, PartialEq)]
479pub struct ExternalConnectionReference {
480    kind: Arc<str>,
481    value: Arc<str>,
482}
483
484impl ExternalConnectionReference {
485    pub fn new(
486        kind: impl Into<Arc<str>>,
487        value: impl Into<Arc<str>>,
488    ) -> Result<Self, ExternalConnectionReferenceError> {
489        let kind = kind.into();
490        if kind.is_empty() {
491            return Err(ExternalConnectionReferenceError::EmptyKind);
492        }
493        if kind.len() > MAX_EXTERNAL_REFERENCE_KIND_BYTES {
494            return Err(ExternalConnectionReferenceError::KindTooLarge);
495        }
496        let mut kind_bytes = kind.bytes();
497        if !kind_bytes
498            .next()
499            .is_some_and(|byte| byte.is_ascii_alphanumeric())
500            || !kind_bytes
501                .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-' | b'_'))
502        {
503            return Err(ExternalConnectionReferenceError::InvalidKind);
504        }
505
506        let value = value.into();
507        if value.is_empty() {
508            return Err(ExternalConnectionReferenceError::EmptyValue);
509        }
510        if value.len() > MAX_EXTERNAL_REFERENCE_VALUE_BYTES {
511            return Err(ExternalConnectionReferenceError::ValueTooLarge);
512        }
513        if value.chars().any(char::is_control) {
514            return Err(ExternalConnectionReferenceError::InvalidValue);
515        }
516        Ok(Self { kind, value })
517    }
518
519    /// Adapter-owned identifier namespace. Do not derive routing policy from
520    /// this untrusted string without an exact allowlist.
521    pub fn kind(&self) -> &str {
522        &self.kind
523    }
524
525    /// Explicitly reveal the external identifier to its durable owner.
526    /// Avoid formatting or logging the returned value.
527    pub fn expose_secret(&self) -> &str {
528        &self.value
529    }
530}
531
532impl fmt::Debug for ExternalConnectionReference {
533    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
534        formatter.write_str("ExternalConnectionReference([redacted])")
535    }
536}
537
538/// Adapter receipt produced only after outbound activation succeeds.
539///
540/// A receipt may contain more than one adapter-owned external identifier.
541/// Core retains it unchanged and does not interpret identifier namespaces.
542#[derive(Clone, Default, Eq, PartialEq)]
543pub struct OutboundActivation {
544    external_references: Arc<[ExternalConnectionReference]>,
545}
546
547impl OutboundActivation {
548    pub fn new(
549        external_references: impl IntoIterator<Item = ExternalConnectionReference>,
550    ) -> Result<Self, ExternalConnectionReferenceError> {
551        let mut bounded = Vec::with_capacity(MAX_EXTERNAL_CONNECTION_REFERENCES);
552        for external_reference in external_references {
553            if bounded.len() == MAX_EXTERNAL_CONNECTION_REFERENCES {
554                return Err(ExternalConnectionReferenceError::TooManyReferences);
555            }
556            bounded.push(external_reference);
557        }
558        Ok(Self {
559            external_references: bounded.into(),
560        })
561    }
562
563    pub fn with_external_reference(external_reference: ExternalConnectionReference) -> Self {
564        Self {
565            external_references: Arc::from([external_reference]),
566        }
567    }
568
569    pub fn external_references(&self) -> &[ExternalConnectionReference] {
570        &self.external_references
571    }
572
573    pub fn is_empty(&self) -> bool {
574        self.external_references.is_empty()
575    }
576}
577
578impl fmt::Debug for OutboundActivation {
579    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
580        formatter
581            .debug_struct("OutboundActivation")
582            .field("external_reference_count", &self.external_references.len())
583            .finish()
584    }
585}
586
587#[derive(Clone)]
588pub struct ConnectionHandle {
589    pub connection: Connection,
590    outbound_activation: OutboundActivation,
591}
592
593impl ConnectionHandle {
594    /// Construct the provisional handle returned by
595    /// `ConnectionAdapter::originate`.
596    pub fn new(connection: Connection) -> Self {
597        Self {
598            connection,
599            outbound_activation: OutboundActivation::default(),
600        }
601    }
602
603    /// Receipt made visible by core only after outbound activation and all
604    /// post-activation liveness checks complete.
605    pub fn outbound_activation(&self) -> &OutboundActivation {
606        &self.outbound_activation
607    }
608
609    /// Core-only activation receipt attachment. This is public because
610    /// `rvoip-core` and `rvoip-core-traits` are separate crates; adapters
611    /// should construct provisional handles with [`Self::new`].
612    #[doc(hidden)]
613    pub fn attach_outbound_activation(&mut self, activation: OutboundActivation) {
614        self.outbound_activation = activation;
615    }
616
617    /// Discard any receipt attached before core has activated the route.
618    #[doc(hidden)]
619    pub fn clear_outbound_activation(&mut self) {
620        self.outbound_activation = OutboundActivation::default();
621    }
622}
623
624impl fmt::Debug for ConnectionHandle {
625    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
626        formatter
627            .debug_struct("ConnectionHandle")
628            .field("connection", &self.connection)
629            .field(
630                "external_reference_count",
631                &self.outbound_activation.external_references.len(),
632            )
633            .finish()
634    }
635}
636
637#[derive(Clone)]
638pub enum RejectReason {
639    Busy,
640    Decline,
641    NotFound,
642    Forbidden,
643    NotAcceptable,
644    ServerError,
645    Custom { code: u16, phrase: String },
646}
647
648impl fmt::Debug for RejectReason {
649    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
650        match self {
651            Self::Busy => formatter.write_str("Busy"),
652            Self::Decline => formatter.write_str("Decline"),
653            Self::NotFound => formatter.write_str("NotFound"),
654            Self::Forbidden => formatter.write_str("Forbidden"),
655            Self::NotAcceptable => formatter.write_str("NotAcceptable"),
656            Self::ServerError => formatter.write_str("ServerError"),
657            Self::Custom { code, phrase } => formatter
658                .debug_struct("Custom")
659                .field("code", code)
660                .field("phrase_present", &!phrase.is_empty())
661                .field("phrase_bytes", &phrase.len())
662                .finish(),
663        }
664    }
665}
666
667#[derive(Clone)]
668pub enum EndReason {
669    Normal,
670    Cancelled,
671    Failed { detail: String },
672    Timeout,
673    BridgeTorn,
674}
675
676impl fmt::Debug for EndReason {
677    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
678        match self {
679            Self::Normal => formatter.write_str("Normal"),
680            Self::Cancelled => formatter.write_str("Cancelled"),
681            Self::Failed { detail } => formatter
682                .debug_struct("Failed")
683                .field("detail_present", &!detail.is_empty())
684                .field("detail_bytes", &detail.len())
685                .finish(),
686            Self::Timeout => formatter.write_str("Timeout"),
687            Self::BridgeTorn => formatter.write_str("BridgeTorn"),
688        }
689    }
690}
691
692#[derive(Clone)]
693pub enum TransferTarget {
694    Uri(String),
695    Connection(ConnectionId),
696    Session(SessionId),
697}
698
699impl fmt::Debug for TransferTarget {
700    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
701        formatter.write_str(match self {
702            Self::Uri(_) => "Uri([redacted])",
703            Self::Connection(_) => "Connection([redacted])",
704            Self::Session(_) => "Session([redacted])",
705        })
706    }
707}
708
709/// Transport-neutral asynchronous transfer status.
710///
711/// Returning successfully from an adapter's `transfer` operation means only
712/// that it submitted the transfer request. Protocols such
713/// as SIP report authoritative progress and completion later (for example via
714/// REFER NOTIFY sipfrags); adapters surface those reports with
715/// [`AdapterEvent::TransferStatus`].
716#[derive(Clone, Eq, PartialEq)]
717#[non_exhaustive]
718pub enum TransferStatus {
719    /// The remote endpoint accepted responsibility for processing the transfer.
720    Accepted,
721    /// A provisional status was reported for the transfer target.
722    Progress { status_code: u16, reason: String },
723    /// A final successful status was reported.
724    Completed { status_code: u16, reason: String },
725    /// A final failure status was reported.
726    Failed { status_code: u16, reason: String },
727}
728
729impl fmt::Debug for TransferStatus {
730    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
731        match self {
732            Self::Accepted => formatter.write_str("Accepted"),
733            Self::Progress {
734                status_code,
735                reason,
736            } => formatter
737                .debug_struct("Progress")
738                .field("status_code", status_code)
739                .field("reason", &"[redacted]")
740                .field("reason_bytes", &reason.len())
741                .finish(),
742            Self::Completed {
743                status_code,
744                reason,
745            } => formatter
746                .debug_struct("Completed")
747                .field("status_code", status_code)
748                .field("reason", &"[redacted]")
749                .field("reason_bytes", &reason.len())
750                .finish(),
751            Self::Failed {
752                status_code,
753                reason,
754            } => formatter
755                .debug_struct("Failed")
756                .field("status_code", status_code)
757                .field("reason", &"[redacted]")
758                .field("reason_bytes", &reason.len())
759                .finish(),
760        }
761    }
762}
763
764/// Handle returned by adapter playback paths that lets callers stop an
765/// in-flight playback.
766pub struct PlaybackHandle {
767    id: PlaybackId,
768    cancel_tx: oneshot::Sender<()>,
769}
770
771impl fmt::Debug for PlaybackHandle {
772    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
773        formatter
774            .debug_struct("PlaybackHandle")
775            .field("id", &self.id)
776            .field("cancel_closed", &self.cancel_tx.is_closed())
777            .finish()
778    }
779}
780
781impl PlaybackHandle {
782    /// Adapter helper: build a handle + the matching cancel receiver.
783    pub fn new(id: PlaybackId) -> (Self, oneshot::Receiver<()>) {
784        let (tx, rx) = oneshot::channel();
785        (Self { id, cancel_tx: tx }, rx)
786    }
787
788    pub fn id(&self) -> &PlaybackId {
789        &self.id
790    }
791
792    /// Best-effort cancellation. Returns `Err` only when the adapter's
793    /// playback task already exited.
794    pub fn cancel(self) -> std::result::Result<(), &'static str> {
795        self.cancel_tx
796            .send(())
797            .map_err(|_| "playback already ended")
798    }
799}
800
801#[derive(Clone)]
802pub struct SignatureHeaders {
803    pub signature: String,
804    pub signature_input: String,
805    pub signature_key: Option<Jwk>,
806    pub signature_agent: Option<Jwk>,
807}
808
809impl fmt::Debug for SignatureHeaders {
810    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
811        formatter
812            .debug_struct("SignatureHeaders")
813            .field("signature_present", &!self.signature.is_empty())
814            .field("signature_bytes", &self.signature.len())
815            .field("signature_input_present", &!self.signature_input.is_empty())
816            .field("signature_input_bytes", &self.signature_input.len())
817            .field("signature_key_present", &self.signature_key.is_some())
818            .field("signature_agent_present", &self.signature_agent.is_some())
819            .finish()
820    }
821}
822
823/// Adapter-native event surface. `rvoip-core` normalizes these into the
824/// orchestration event vocabulary; consumers wanting protocol-native
825/// access can subscribe directly to the adapter.
826#[derive(Clone)]
827#[non_exhaustive]
828pub enum AdapterEvent {
829    InboundConnection {
830        connection: Connection,
831    },
832    Connected {
833        connection_id: ConnectionId,
834    },
835    /// Connection-scoped provisional signaling reported by an adapter.
836    ///
837    /// `early_media` is true only when the provisional response established
838    /// a usable media description (for SIP, a 183 response carrying SDP).
839    /// The raw transport description is intentionally not exposed through
840    /// this transport-neutral event.
841    Progress {
842        connection_id: ConnectionId,
843        status_code: u16,
844        reason: String,
845        early_media: bool,
846    },
847    Authenticated {
848        connection_id: ConnectionId,
849        identity_id: String,
850        participant_id: String,
851        assurance: IdentityAssurance,
852    },
853    /// Additive full-principal authentication event. The legacy
854    /// `Authenticated` variant remains unchanged for source compatibility.
855    PrincipalAuthenticated {
856        connection_id: ConnectionId,
857        participant_id: String,
858        principal: AuthenticatedPrincipal,
859    },
860    Ended {
861        connection_id: ConnectionId,
862        reason: EndReason,
863    },
864    Failed {
865        connection_id: ConnectionId,
866        detail: String,
867    },
868    Dtmf {
869        connection_id: ConnectionId,
870        digits: String,
871        duration_ms: u32,
872    },
873    Quality {
874        connection_id: ConnectionId,
875        snapshot: QualitySnapshot,
876    },
877    Message {
878        connection_id: ConnectionId,
879        text: String,
880    },
881    DataMessage {
882        connection_id: ConnectionId,
883        message: DataMessage,
884    },
885    /// An asynchronous, protocol-authoritative transfer update.
886    TransferStatus {
887        connection_id: ConnectionId,
888        /// Correlates this update with the exact submitted transfer when the
889        /// adapter can provide transaction-safe correlation. Legacy adapters
890        /// report `None`.
891        attempt_id: Option<TransferAttemptId>,
892        status: TransferStatus,
893    },
894    StepUpResponse {
895        connection_id: ConnectionId,
896        method: String,
897        credential: String,
898    },
899    Native {
900        kind: &'static str,
901        detail: String,
902    },
903}
904
905impl fmt::Debug for AdapterEvent {
906    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
907        match self {
908            Self::InboundConnection { .. } => formatter.write_str("InboundConnection"),
909            Self::Connected { .. } => formatter.write_str("Connected"),
910            Self::Progress {
911                status_code,
912                reason,
913                early_media,
914                ..
915            } => formatter
916                .debug_struct("Progress")
917                .field("status_code", status_code)
918                .field("reason_present", &!reason.is_empty())
919                .field("reason_bytes", &reason.len())
920                .field("early_media", early_media)
921                .finish(),
922            Self::Authenticated { .. } => formatter.write_str("Authenticated"),
923            Self::PrincipalAuthenticated { .. } => formatter.write_str("PrincipalAuthenticated"),
924            Self::Ended { .. } => formatter.write_str("Ended"),
925            Self::Failed { detail, .. } => formatter
926                .debug_struct("Failed")
927                .field("detail_present", &!detail.is_empty())
928                .field("detail_bytes", &detail.len())
929                .finish(),
930            Self::Dtmf {
931                digits,
932                duration_ms,
933                ..
934            } => formatter
935                .debug_struct("Dtmf")
936                .field("digit_count", &digits.chars().count())
937                .field("duration_ms", duration_ms)
938                .finish(),
939            Self::Quality { .. } => formatter.write_str("Quality"),
940            Self::Message { text, .. } => formatter
941                .debug_struct("Message")
942                .field("text_present", &!text.is_empty())
943                .field("text_bytes", &text.len())
944                .finish(),
945            Self::DataMessage { message, .. } => formatter
946                .debug_struct("DataMessage")
947                .field("body_bytes", &message.bytes.len())
948                .finish(),
949            Self::TransferStatus {
950                attempt_id, status, ..
951            } => formatter
952                .debug_struct("TransferStatus")
953                .field("attempt_id_present", &attempt_id.is_some())
954                .field("status", status)
955                .finish(),
956            Self::StepUpResponse {
957                method, credential, ..
958            } => formatter
959                .debug_struct("StepUpResponse")
960                .field("method_present", &!method.is_empty())
961                .field("method_bytes", &method.len())
962                .field("credential_present", &!credential.is_empty())
963                .field("credential_bytes", &credential.len())
964                .finish(),
965            Self::Native { kind, detail } => formatter
966                .debug_struct("Native")
967                .field("kind", kind)
968                .field("detail_present", &!detail.is_empty())
969                .field("detail_bytes", &detail.len())
970                .finish(),
971        }
972    }
973}
974
975#[cfg(test)]
976mod tests {
977    use super::*;
978    use crate::identity::{AuthenticationMethod, IdentityAssurance};
979
980    fn principal(tenant: Option<&str>) -> AuthenticatedPrincipal {
981        AuthenticatedPrincipal {
982            subject: "routing-owner".into(),
983            tenant: tenant.map(str::to_owned),
984            scopes: vec!["call:attach".into()],
985            issuer: Some("https://issuer.invalid".into()),
986            expires_at: None,
987            method: AuthenticationMethod::Jwt,
988            assurance: IdentityAssurance::Identified {
989                credential_kind: crate::identity::CredentialKind::Oidc,
990            },
991        }
992    }
993
994    #[test]
995    fn inbound_context_is_bound_and_debug_redacted() {
996        let connection_id = ConnectionId::new();
997        let owner = principal(Some("tenant-a"));
998        let hint = InboundRoutingHint::new("attachment-secret").unwrap();
999        let metadata = InboundSignalingMetadata::new([
1000            ("X-Correlation-Id", "correlation-secret"),
1001            ("X-Correlation-Id", "second-secret"),
1002        ])
1003        .unwrap();
1004        let context = InboundConnectionContext::new(
1005            connection_id.clone(),
1006            Transport::Sip,
1007            &owner,
1008            Some(hint),
1009            metadata,
1010        )
1011        .unwrap();
1012
1013        assert!(context.is_bound_to(&connection_id, Transport::Sip, &owner));
1014        assert_eq!(
1015            context.routing_hint().unwrap().expose_secret(),
1016            "attachment-secret"
1017        );
1018        assert_eq!(
1019            context
1020                .metadata()
1021                .values("x-correlation-id")
1022                .collect::<Vec<_>>(),
1023            vec!["correlation-secret", "second-secret"]
1024        );
1025
1026        let debug = format!("{context:?}");
1027        assert!(!debug.contains("attachment-secret"));
1028        assert!(!debug.contains("correlation-secret"));
1029        assert!(!debug.contains("second-secret"));
1030        assert!(debug.contains("[redacted]"));
1031    }
1032
1033    #[test]
1034    fn inbound_routing_hint_transfers_owned_secret_exactly_once() {
1035        let connection_id = ConnectionId::new();
1036        let owner = principal(Some("tenant-a"));
1037        let mut context = InboundConnectionContext::new(
1038            connection_id,
1039            Transport::WebRtc,
1040            &owner,
1041            Some(InboundRoutingHint::new("attachment-secret").unwrap()),
1042            InboundSignalingMetadata::default(),
1043        )
1044        .unwrap();
1045
1046        let mut secret = context.take_routing_hint().unwrap().into_secret();
1047        assert_eq!(secret, "attachment-secret");
1048        assert!(context.take_routing_hint().is_none());
1049        secret.zeroize();
1050        assert!(secret.is_empty());
1051    }
1052
1053    #[test]
1054    fn inbound_context_rejects_unscoped_or_unsafe_values() {
1055        assert_eq!(
1056            InboundConnectionContext::new(
1057                ConnectionId::new(),
1058                Transport::Sip,
1059                &principal(None),
1060                None,
1061                InboundSignalingMetadata::default(),
1062            )
1063            .unwrap_err(),
1064            InboundContextError::MissingTenant
1065        );
1066        assert_eq!(
1067            InboundRoutingHint::new("secret\r\nheader").unwrap_err(),
1068            InboundContextError::InvalidRoutingHint
1069        );
1070        assert_eq!(
1071            InboundSignalingMetadata::new([("x-safe", "unsafe\r\nvalue")]).unwrap_err(),
1072            InboundContextError::InvalidMetadataValue
1073        );
1074    }
1075
1076    #[derive(Debug)]
1077    struct AdapterOwnedOptions {
1078        endpoint: String,
1079        bearer: String,
1080    }
1081
1082    #[test]
1083    fn originate_context_round_trips_by_type_and_redacts_debug() {
1084        let context = OriginateContext::new(AdapterOwnedOptions {
1085            endpoint: "https://private.invalid/session".into(),
1086            bearer: "private-bearer".into(),
1087        });
1088        let cloned = context.clone();
1089
1090        assert_eq!(
1091            context
1092                .downcast_ref::<AdapterOwnedOptions>()
1093                .unwrap()
1094                .endpoint,
1095            "https://private.invalid/session"
1096        );
1097        assert!(context.downcast_ref::<String>().is_none());
1098        let first = context.downcast_arc::<AdapterOwnedOptions>().unwrap();
1099        let second = cloned.downcast_arc::<AdapterOwnedOptions>().unwrap();
1100        assert!(Arc::ptr_eq(&first, &second));
1101
1102        let request = OriginateRequest {
1103            session_id: SessionId::new(),
1104            participant_id: ParticipantId::new(),
1105            target: "sip:private-target@example.invalid".into(),
1106            direction: Direction::Outbound,
1107            capabilities: CapabilityDescriptor::default(),
1108            transport: Some(Transport::Sip),
1109            context,
1110        };
1111        let debug = format!("{request:?}");
1112        assert!(!debug.contains("private.invalid"));
1113        assert!(!debug.contains("private-bearer"));
1114        assert!(!debug.contains("AdapterOwnedOptions"));
1115        assert!(debug.contains("[redacted]"));
1116        assert_eq!(first.bearer, "private-bearer");
1117
1118        let default_context = OriginateContext::default();
1119        assert!(default_context.is_empty());
1120        assert!(default_context
1121            .downcast_ref::<AdapterOwnedOptions>()
1122            .is_none());
1123    }
1124
1125    #[test]
1126    fn external_references_are_bounded_and_redacted() {
1127        let reference =
1128            ExternalConnectionReference::new("sip.call-id", "private-call-id@example.invalid")
1129                .unwrap();
1130        assert_eq!(reference.kind(), "sip.call-id");
1131        assert_eq!(reference.expose_secret(), "private-call-id@example.invalid");
1132        let debug = format!("{reference:?}");
1133        assert!(!debug.contains("sip.call-id"));
1134        assert!(!debug.contains("private-call-id"));
1135        assert!(debug.contains("[redacted]"));
1136
1137        let activation = OutboundActivation::new([reference.clone()]).unwrap();
1138        assert_eq!(activation.external_references(), &[reference.clone()]);
1139        let debug = format!("{activation:?}");
1140        assert!(!debug.contains("sip.call-id"));
1141        assert!(!debug.contains("private-call-id"));
1142        assert!(debug.contains("external_reference_count: 1"));
1143
1144        assert_eq!(
1145            ExternalConnectionReference::new("", "value").unwrap_err(),
1146            ExternalConnectionReferenceError::EmptyKind
1147        );
1148        assert_eq!(
1149            ExternalConnectionReference::new("-invalid", "value").unwrap_err(),
1150            ExternalConnectionReferenceError::InvalidKind
1151        );
1152        assert_eq!(
1153            ExternalConnectionReference::new(
1154                "x".repeat(MAX_EXTERNAL_REFERENCE_KIND_BYTES + 1),
1155                "value"
1156            )
1157            .unwrap_err(),
1158            ExternalConnectionReferenceError::KindTooLarge
1159        );
1160        assert_eq!(
1161            ExternalConnectionReference::new("provider.id", "").unwrap_err(),
1162            ExternalConnectionReferenceError::EmptyValue
1163        );
1164        assert_eq!(
1165            ExternalConnectionReference::new(
1166                "provider.id",
1167                "x".repeat(MAX_EXTERNAL_REFERENCE_VALUE_BYTES + 1)
1168            )
1169            .unwrap_err(),
1170            ExternalConnectionReferenceError::ValueTooLarge
1171        );
1172        assert_eq!(
1173            ExternalConnectionReference::new("provider.id", "unsafe\r\nvalue").unwrap_err(),
1174            ExternalConnectionReferenceError::InvalidValue
1175        );
1176
1177        let too_many = std::iter::repeat_n(reference, MAX_EXTERNAL_CONNECTION_REFERENCES + 1);
1178        assert_eq!(
1179            OutboundActivation::new(too_many).unwrap_err(),
1180            ExternalConnectionReferenceError::TooManyReferences
1181        );
1182    }
1183
1184    #[test]
1185    fn signature_and_step_up_debug_never_render_credential_values() {
1186        const CANARY: &str = "adapter-credential-canary\r\nAuthorization: exposed";
1187        let headers = SignatureHeaders {
1188            signature: CANARY.into(),
1189            signature_input: CANARY.into(),
1190            signature_key: None,
1191            signature_agent: None,
1192        };
1193        let event = AdapterEvent::StepUpResponse {
1194            connection_id: ConnectionId::new(),
1195            method: "bearer".into(),
1196            credential: CANARY.into(),
1197        };
1198
1199        for rendered in [format!("{headers:?}"), format!("{event:?}")] {
1200            assert!(!rendered.contains(CANARY), "credential leaked: {rendered}");
1201        }
1202        assert_eq!(headers.signature, CANARY);
1203        match event {
1204            AdapterEvent::StepUpResponse { credential, .. } => assert_eq!(credential, CANARY),
1205            other => panic!("unexpected event: {other:?}"),
1206        }
1207    }
1208
1209    #[test]
1210    fn transfer_status_debug_never_renders_peer_reason() {
1211        const CANARY: &str = "transfer-reason-canary\r\nAuthorization: exposed";
1212        let event = AdapterEvent::TransferStatus {
1213            connection_id: ConnectionId::new(),
1214            attempt_id: Some(TransferAttemptId::from_string(CANARY)),
1215            status: TransferStatus::Failed {
1216                status_code: 503,
1217                reason: CANARY.into(),
1218            },
1219        };
1220
1221        let rendered = format!("{event:?}");
1222        assert!(
1223            !rendered.contains(CANARY),
1224            "transfer reason leaked: {rendered}"
1225        );
1226        assert!(rendered.contains("503"));
1227        assert!(rendered.contains("[redacted]"));
1228        assert!(rendered.contains("attempt_id_present"));
1229    }
1230
1231    #[test]
1232    fn adapter_control_diagnostics_never_render_peer_values() {
1233        const CANARY: &str = "adapter-control-canary\r\nAuthorization: exposed";
1234        let values = [
1235            format!(
1236                "{:?}",
1237                RejectReason::Custom {
1238                    code: 488,
1239                    phrase: CANARY.into(),
1240                }
1241            ),
1242            format!(
1243                "{:?}",
1244                EndReason::Failed {
1245                    detail: CANARY.into()
1246                }
1247            ),
1248            format!("{:?}", TransferTarget::Uri(CANARY.into())),
1249            format!(
1250                "{:?}",
1251                TransferTarget::Connection(ConnectionId::from_string(CANARY))
1252            ),
1253        ];
1254        for debug in values {
1255            assert!(!debug.contains(CANARY));
1256        }
1257    }
1258}