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
14pub const MAX_INBOUND_ROUTING_HINT_BYTES: usize = 2_048;
16pub const MAX_INBOUND_METADATA_FIELDS: usize = 32;
18pub const MAX_INBOUND_METADATA_NAME_BYTES: usize = 128;
20pub const MAX_INBOUND_METADATA_VALUE_BYTES: usize = 4_096;
22pub const MAX_INBOUND_METADATA_BYTES: usize = 16 * 1_024;
24pub const MAX_EXTERNAL_CONNECTION_REFERENCES: usize = 16;
26pub const MAX_EXTERNAL_REFERENCE_KIND_BYTES: usize = 64;
28pub const MAX_EXTERNAL_REFERENCE_VALUE_BYTES: usize = 4_096;
30
31#[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#[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#[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 pub fn expose_secret(&self) -> &str {
109 &self.0
110 }
111
112 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#[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 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
251pub 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 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 Substrate,
338 Interop,
340}
341
342#[derive(Clone, Default)]
350pub struct OriginateContext {
351 inner: Option<Arc<dyn Any + Send + Sync>>,
352}
353
354impl OriginateContext {
355 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 pub fn is_empty(&self) -> bool {
367 self.inner.is_none()
368 }
369
370 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 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 pub transport: Option<Transport>,
409 pub context: OriginateContext,
411}
412
413impl OriginateRequest {
414 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 pub fn with_transport(mut self, transport: Transport) -> Self {
437 self.transport = Some(transport);
438 self
439 }
440
441 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 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#[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 pub fn kind(&self) -> &str {
522 &self.kind
523 }
524
525 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#[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 pub fn new(connection: Connection) -> Self {
597 Self {
598 connection,
599 outbound_activation: OutboundActivation::default(),
600 }
601 }
602
603 pub fn outbound_activation(&self) -> &OutboundActivation {
606 &self.outbound_activation
607 }
608
609 #[doc(hidden)]
613 pub fn attach_outbound_activation(&mut self, activation: OutboundActivation) {
614 self.outbound_activation = activation;
615 }
616
617 #[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#[derive(Clone, Eq, PartialEq)]
717#[non_exhaustive]
718pub enum TransferStatus {
719 Accepted,
721 Progress { status_code: u16, reason: String },
723 Completed { status_code: u16, reason: String },
725 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
764pub 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 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 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#[derive(Clone)]
827#[non_exhaustive]
828pub enum AdapterEvent {
829 InboundConnection {
830 connection: Connection,
831 },
832 Connected {
833 connection_id: ConnectionId,
834 },
835 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 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 TransferStatus {
887 connection_id: ConnectionId,
888 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}