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(
26        "original and matched evidence exceed batch budget: operation {operation} requires {bytes} bytes (limit {limit})"
27    )]
28    OriginalOperationTooLarge {
29        operation: usize,
30        bytes: usize,
31        limit: usize,
32    },
33    #[error("RPC failed: {0:?}")]
34    Remote(RemoteFailure),
35    #[error(transparent)]
36    Framing(#[from] framing::FrameError),
37    #[error(transparent)]
38    Metadata(#[from] api::RequestMetadataError),
39    #[error(transparent)]
40    Decode(#[from] prost::DecodeError),
41}
42
43/// Keep large optional challenge/conflict details in their wire representation
44/// until an application needs them. Common error handling needs only code and
45/// message; every original typed detail remains available without heap boxing.
46#[derive(Debug)]
47pub struct RemoteFailure {
48    pub code: i32,
49    pub message: String,
50    detail: Option<Vec<u8>>,
51}
52impl RemoteFailure {
53    pub fn detail(
54        &self,
55    ) -> Result<Option<api::heddle::api::common::ErrorDetail>, prost::DecodeError> {
56        self.detail
57            .as_deref()
58            .map(prost::Message::decode)
59            .transpose()
60    }
61}
62impl From<CallFailure> for RemoteFailure {
63    fn from(failure: CallFailure) -> Self {
64        Self {
65            code: failure.code,
66            message: failure.message,
67            detail: failure.error.map(|e| e.encode_to_vec()),
68        }
69    }
70}
71
72/// Existing Heddle signer/broker integration plugs in here. Sign exactly the
73/// supplied method and encoded body; carry the owner's Biscuit and attachment.
74/// Shared CallContext/signing formats are retained data, not old RPC methods.
75pub trait Authorize: Send + Sync {
76    fn context(
77        &self,
78        method: &'static MethodDescriptor,
79        body: &[u8],
80    ) -> impl Future<Output = Result<CallContext, Error>> + Send;
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn remote_failure_retains_typed_details_without_boxing_the_error_path() {
89        use api::heddle::api::common::{ErrorDetail, ErrorReason};
90        let detail = ErrorDetail {
91            reason: ErrorReason::PolicyDenied as i32,
92            resource: "thread".into(),
93            ..Default::default()
94        };
95        let failure = RemoteFailure::from(CallFailure {
96            code: 7,
97            message: "review required".into(),
98            error: Some(detail.clone()),
99        });
100        assert_eq!(failure.detail().expect("typed detail"), Some(detail));
101        assert_eq!(failure.message, "review required");
102    }
103}