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}