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