Skip to main content

auths_core/ports/
network.rs

1//! Network port and DID resolution.
2
3use std::future::Future;
4
5use auths_verifier::core::Ed25519PublicKey;
6
7/// Domain error for outbound network operations.
8///
9/// Adapters map transport-specific failures (e.g., HTTP timeouts, connection
10/// refused) into these variants. Domain logic never sees transport details.
11///
12/// Usage:
13/// ```ignore
14/// use auths_core::ports::network::NetworkError;
15///
16/// fn handle(err: NetworkError) {
17///     match err {
18///         NetworkError::Unreachable { endpoint } => eprintln!("cannot reach {endpoint}"),
19///         NetworkError::Timeout { endpoint } => eprintln!("timed out: {endpoint}"),
20///         NetworkError::NotFound { resource } => eprintln!("missing: {resource}"),
21///         NetworkError::Unauthorized => eprintln!("not authorized"),
22///         NetworkError::InvalidResponse { detail } => eprintln!("bad response: {detail}"),
23///         NetworkError::Internal(inner) => eprintln!("bug: {inner}"),
24///     }
25/// }
26/// ```
27#[derive(Debug, thiserror::Error)]
28#[non_exhaustive]
29pub enum NetworkError {
30    /// The endpoint could not be reached.
31    #[error("endpoint unreachable: {endpoint}")]
32    Unreachable {
33        /// The unreachable endpoint URL.
34        endpoint: String,
35    },
36
37    /// The request timed out.
38    #[error("request timed out: {endpoint}")]
39    Timeout {
40        /// The endpoint that timed out.
41        endpoint: String,
42    },
43
44    /// The requested resource was not found.
45    #[error("resource not found: {resource}")]
46    NotFound {
47        /// The missing resource identifier.
48        resource: String,
49    },
50
51    /// Authentication or authorisation failed.
52    #[error("unauthorized")]
53    Unauthorized,
54
55    /// The server returned an unexpected response.
56    #[error("invalid response: {detail}")]
57    InvalidResponse {
58        /// Details about the invalid response.
59        detail: String,
60    },
61
62    /// An unexpected internal error.
63    #[error("internal network error: {0}")]
64    Internal(Box<dyn std::error::Error + Send + Sync>),
65}
66
67impl auths_crypto::AuthsErrorInfo for NetworkError {
68    fn error_code(&self) -> &'static str {
69        match self {
70            Self::Unreachable { .. } => "AUTHS-E3601",
71            Self::Timeout { .. } => "AUTHS-E3602",
72            Self::NotFound { .. } => "AUTHS-E3603",
73            Self::Unauthorized => "AUTHS-E3604",
74            Self::InvalidResponse { .. } => "AUTHS-E3605",
75            Self::Internal(_) => "AUTHS-E3606",
76        }
77    }
78
79    fn suggestion(&self) -> Option<&'static str> {
80        match self {
81            Self::Unreachable { .. } => Some("Check your internet connection"),
82            Self::Timeout { .. } => Some("The server may be overloaded — retry later"),
83            Self::Unauthorized => Some("Check your authentication credentials"),
84            Self::NotFound { .. } => Some(
85                "The requested resource was not found on the server; verify the URL or identifier",
86            ),
87            Self::InvalidResponse { .. } => {
88                Some("The server returned an unexpected response; check server compatibility")
89            }
90            Self::Internal(_) => Some(
91                "The server encountered an internal error; retry later or contact the server administrator",
92            ),
93        }
94    }
95}
96
97/// Domain error for identity resolution operations.
98///
99/// Distinguishes resolution-specific failures (unknown DID, revoked key)
100/// from general transport failures via the `Network` variant.
101///
102/// Usage:
103/// ```ignore
104/// use auths_core::ports::network::ResolutionError;
105///
106/// fn handle(err: ResolutionError) {
107///     match err {
108///         ResolutionError::DidNotFound { did } => eprintln!("unknown: {did}"),
109///         ResolutionError::InvalidDid { did, reason } => eprintln!("{did}: {reason}"),
110///         ResolutionError::KeyRevoked { did } => eprintln!("revoked: {did}"),
111///         ResolutionError::Network(inner) => eprintln!("transport: {inner}"),
112///     }
113/// }
114/// ```
115#[derive(Debug, thiserror::Error)]
116#[non_exhaustive]
117pub enum ResolutionError {
118    /// The DID was not found.
119    #[error("DID not found: {did}")]
120    DidNotFound {
121        /// The DID that was not found.
122        did: String,
123    },
124
125    /// The DID is malformed.
126    #[error("invalid DID {did}: {reason}")]
127    InvalidDid {
128        /// The malformed DID.
129        did: String,
130        /// Reason the DID is invalid.
131        reason: String,
132    },
133
134    /// The key for this DID has been revoked.
135    #[error("key revoked for DID: {did}")]
136    KeyRevoked {
137        /// The DID whose key was revoked.
138        did: String,
139    },
140
141    /// A network error occurred during resolution.
142    #[error("network error: {0}")]
143    Network(#[from] NetworkError),
144}
145
146impl auths_crypto::AuthsErrorInfo for ResolutionError {
147    fn error_code(&self) -> &'static str {
148        match self {
149            Self::DidNotFound { .. } => "AUTHS-E3701",
150            Self::InvalidDid { .. } => "AUTHS-E3702",
151            Self::KeyRevoked { .. } => "AUTHS-E3703",
152            Self::Network(_) => "AUTHS-E3704",
153        }
154    }
155
156    fn suggestion(&self) -> Option<&'static str> {
157        match self {
158            Self::DidNotFound { .. } => Some("Verify the DID is correct and the identity exists"),
159            Self::InvalidDid { .. } => {
160                Some("Check the DID format (e.g., did:key:z6Mk... or did:keri:E...)")
161            }
162            Self::KeyRevoked { .. } => {
163                Some("This key has been revoked — contact the identity owner")
164            }
165            Self::Network(_) => Some("Check your internet connection"),
166        }
167    }
168}
169
170/// Cryptographic material resolved from a decentralized identifier.
171///
172/// Usage:
173/// ```ignore
174/// use auths_core::ports::network::ResolvedIdentity;
175///
176/// let identity: ResolvedIdentity = resolver.resolve_identity("did:key:z...").await?;
177/// let pk = identity.public_key();
178/// ```
179#[derive(Debug, Clone)]
180pub enum ResolvedIdentity {
181    /// Static did:key (no rotation possible).
182    Key {
183        /// The resolved DID string.
184        did: String,
185        /// The public key.
186        public_key: Ed25519PublicKey,
187    },
188    /// KERI-based identity with rotation capability.
189    Keri {
190        /// The resolved DID string.
191        did: String,
192        /// The public key.
193        public_key: Ed25519PublicKey,
194        /// Current KEL sequence number.
195        sequence: u128,
196        /// Whether key rotation is available.
197        can_rotate: bool,
198    },
199}
200
201impl ResolvedIdentity {
202    /// Returns the DID string.
203    pub fn did(&self) -> &str {
204        match self {
205            ResolvedIdentity::Key { did, .. } | ResolvedIdentity::Keri { did, .. } => did,
206        }
207    }
208
209    /// Returns the public key.
210    pub fn public_key(&self) -> &Ed25519PublicKey {
211        match self {
212            ResolvedIdentity::Key { public_key, .. }
213            | ResolvedIdentity::Keri { public_key, .. } => public_key,
214        }
215    }
216
217    /// Returns `true` if this is a `did:key` resolution.
218    pub fn is_key(&self) -> bool {
219        matches!(self, ResolvedIdentity::Key { .. })
220    }
221
222    /// Returns `true` if this is a `did:keri` resolution.
223    pub fn is_keri(&self) -> bool {
224        matches!(self, ResolvedIdentity::Keri { .. })
225    }
226}
227
228/// Resolves a decentralized identifier to its current cryptographic material.
229///
230/// Implementations may fetch data from local stores, remote registries, or
231/// peer-to-peer networks. The domain only provides a DID string and receives
232/// the resolved key material.
233///
234/// Usage:
235/// ```ignore
236/// use auths_core::ports::network::IdentityResolver;
237///
238/// async fn verify_signer(resolver: &dyn IdentityResolver, did: &str) -> Vec<u8> {
239///     let resolved = resolver.resolve_identity(did).await.unwrap();
240///     resolved.public_key
241/// }
242/// ```
243pub trait IdentityResolver: Send + Sync {
244    /// Resolves a DID string to its current public key and method metadata.
245    ///
246    /// Args:
247    /// * `did`: The decentralized identifier to resolve (e.g., `"did:keri:EAbcdef..."`).
248    ///
249    /// Usage:
250    /// ```ignore
251    /// let identity = resolver.resolve_identity("did:key:z6Mk...").await?;
252    /// ```
253    fn resolve_identity(
254        &self,
255        did: &str,
256    ) -> impl Future<Output = Result<ResolvedIdentity, ResolutionError>> + Send;
257}
258
259/// Rate limit information extracted from HTTP response headers.
260///
261/// Populated from standard `X-RateLimit-*` headers when present in the
262/// registry response.
263#[derive(Debug, Clone, Default)]
264pub struct RateLimitInfo {
265    /// Maximum requests allowed in the current window.
266    pub limit: Option<i32>,
267    /// Remaining requests in the current window.
268    pub remaining: Option<i32>,
269    /// Unix timestamp when the rate limit window resets.
270    pub reset: Option<i64>,
271    /// The access tier for this identity (e.g., "free", "team").
272    pub tier: Option<String>,
273}
274
275/// Response from a registry POST operation.
276///
277/// Carries the HTTP status code and body so callers can dispatch on
278/// status-specific business logic (e.g., 201 Created vs. 409 Conflict).
279#[derive(Debug)]
280pub struct RegistryResponse {
281    /// HTTP status code.
282    pub status: u16,
283    /// Response body bytes.
284    pub body: Vec<u8>,
285    /// Rate limit information extracted from response headers, if present.
286    pub rate_limit: Option<RateLimitInfo>,
287}
288
289/// Fetches and pushes data to a remote registry service.
290///
291/// Implementations handle the transport protocol (e.g., HTTP, gRPC).
292/// The domain provides logical paths and receives raw bytes.
293///
294/// Usage:
295/// ```ignore
296/// use auths_core::ports::network::RegistryClient;
297///
298/// async fn sync_identity(client: &dyn RegistryClient, url: &str) {
299///     let data = client.fetch_registry_data(url, "identities/abc123").await.unwrap();
300/// }
301/// ```
302pub trait RegistryClient: Send + Sync {
303    /// Fetches data from a registry at the given logical path.
304    ///
305    /// Args:
306    /// * `registry_url`: The registry service identifier.
307    /// * `path`: The logical path within the registry.
308    ///
309    /// Usage:
310    /// ```ignore
311    /// let data = client.fetch_registry_data("registry.example.com", "identities/abc123").await?;
312    /// ```
313    fn fetch_registry_data(
314        &self,
315        registry_url: &str,
316        path: &str,
317    ) -> impl Future<Output = Result<Vec<u8>, NetworkError>> + Send;
318
319    /// Pushes data to a registry at the given logical path.
320    ///
321    /// Args:
322    /// * `registry_url`: The registry service identifier.
323    /// * `path`: The logical path within the registry.
324    /// * `data`: The raw bytes to push.
325    ///
326    /// Usage:
327    /// ```ignore
328    /// client.push_registry_data("registry.example.com", "identities/abc123", &bytes).await?;
329    /// ```
330    fn push_registry_data(
331        &self,
332        registry_url: &str,
333        path: &str,
334        data: &[u8],
335    ) -> impl Future<Output = Result<(), NetworkError>> + Send;
336
337    /// POSTs a JSON payload to a registry endpoint and returns the raw response.
338    ///
339    /// Args:
340    /// * `registry_url`: Base URL of the registry service.
341    /// * `path`: The logical path within the registry (e.g., `"v1/identities"`).
342    /// * `json_body`: Serialized JSON bytes to send as the request body.
343    ///
344    /// Usage:
345    /// ```ignore
346    /// let resp = client.post_json("https://registry.example.com", "v1/identities", &body).await?;
347    /// match resp.status {
348    ///     201 => { /* success */ }
349    ///     409 => { /* conflict */ }
350    ///     _ => { /* error */ }
351    /// }
352    /// ```
353    fn post_json(
354        &self,
355        registry_url: &str,
356        path: &str,
357        json_body: &[u8],
358    ) -> impl Future<Output = Result<RegistryResponse, NetworkError>> + Send;
359}