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}