Skip to main content

heddle_thread_api/
transport.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Transport-neutral credentials and errors for typed Thread RPC clients.
3use std::future::Future;
4
5use api::{
6    framing,
7    heddle::api::common::{CallContext, CallFailure},
8    v2::MethodDescriptor,
9};
10use prost::Message;
11
12#[cfg(feature = "iroh")]
13mod iroh;
14#[cfg(feature = "iroh")]
15pub use iroh::{IrohTransport, Reader, Writer, accepted_stream};
16
17#[derive(Debug, thiserror::Error)]
18pub enum Error {
19    #[error("transport I/O: {0}")]
20    Io(String),
21    #[error("RPC made no progress before its timeout")]
22    Timeout,
23    #[error("invalid v2 transport: {0}")]
24    Protocol(&'static str),
25    #[error("RPC failed: {0:?}")]
26    Remote(RemoteFailure),
27    #[error(transparent)]
28    Framing(#[from] framing::FrameError),
29    #[error(transparent)]
30    Metadata(#[from] api::RequestMetadataError),
31    #[error(transparent)]
32    Decode(#[from] prost::DecodeError),
33}
34
35/// Keep large optional challenge/conflict details in their wire representation
36/// until an application needs them. Common error handling needs only code and
37/// message; every original typed detail remains available without heap boxing.
38#[derive(Debug)]
39pub struct RemoteFailure {
40    pub code: i32,
41    pub message: String,
42    detail: Option<Vec<u8>>,
43}
44impl RemoteFailure {
45    pub fn detail(
46        &self,
47    ) -> Result<Option<api::heddle::api::common::ErrorDetail>, prost::DecodeError> {
48        self.detail
49            .as_deref()
50            .map(prost::Message::decode)
51            .transpose()
52    }
53}
54impl From<CallFailure> for RemoteFailure {
55    fn from(failure: CallFailure) -> Self {
56        Self {
57            code: failure.code,
58            message: failure.message,
59            detail: failure.error.map(|e| e.encode_to_vec()),
60        }
61    }
62}
63
64/// Existing Heddle signer/broker integration plugs in here. Sign exactly the
65/// supplied method and encoded body; carry the owner's Biscuit and attachment.
66/// Shared CallContext/signing formats are retained data, not old RPC methods.
67pub trait Authorize: Send + Sync {
68    fn context(
69        &self,
70        method: &'static MethodDescriptor,
71        body: &[u8],
72    ) -> impl Future<Output = Result<CallContext, Error>> + Send;
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn remote_failure_retains_typed_details_without_boxing_the_error_path() {
81        use api::heddle::api::common::{ErrorDetail, ErrorReason};
82        let detail = ErrorDetail {
83            reason: ErrorReason::PolicyDenied as i32,
84            resource: "thread".into(),
85            ..Default::default()
86        };
87        let failure = RemoteFailure::from(CallFailure {
88            code: 7,
89            message: "review required".into(),
90            error: Some(detail.clone()),
91        });
92        assert_eq!(failure.detail().expect("typed detail"), Some(detail));
93        assert_eq!(failure.message, "review required");
94    }
95}