Skip to main content

dig_urn_protocol/
resolve.rs

1//! The resolution INTERFACE — the [`UrnResolver`] trait and its typed outcomes/errors.
2//!
3//! This crate defines the CONTRACT, not the transport: a concrete resolver (in `dig-urn-resolver`,
4//! the node, the browser host) implements [`UrnResolver`] over its own I/O, while this module fixes
5//! the shape every implementation shares — the three exhaustive outcomes and the catalogued error
6//! taxonomy — so consumers can depend on one stable interface.
7//!
8//! # Three outcomes, deliberately kept distinct
9//!
10//! * [`ResolveOutcome::Success`] — verified, decrypted content.
11//! * [`ResolveOutcome::IntegrityFailure`] — bytes WERE fetched but failed merkle/decrypt
12//!   verification (tampered / decoy / wrong root). A hard, fail-CLOSED security outcome; the
13//!   unverified bytes are NEVER carried here.
14//! * [`ResolveOutcome::Unreachable`] — every transport tier was down; nothing was fetched. A
15//!   friendly, retryable network state.
16//!
17//! `IntegrityFailure` (reached the network, bytes don't verify — security) and `Unreachable`
18//! (couldn't reach the network — retryable) are never conflated. A malformed URN, a not-found
19//! resource, and a reachable protocol error are hard [`ResolveError`]s.
20
21/// The resolved bytes plus their content type. Only ever the VERIFIED content of a
22/// [`ResolveOutcome::Success`].
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct ResolvedData {
25    /// The verified, decrypted resource bytes.
26    pub bytes: Vec<u8>,
27    /// The MIME type.
28    pub content_type: String,
29}
30
31impl ResolvedData {
32    /// Construct resolved data.
33    pub fn new(bytes: Vec<u8>, content_type: String) -> Self {
34        ResolvedData {
35            bytes,
36            content_type,
37        }
38    }
39}
40
41/// The typed result of a resolve. The three cases are exhaustive and never conflated.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum ResolveOutcome {
44    /// Verified, decrypted content.
45    Success(ResolvedData),
46    /// The served bytes failed integrity verification — a hard, fail-closed security failure. The
47    /// unverified bytes are NEVER carried here.
48    IntegrityFailure,
49    /// Every transport tier was unreachable — a friendly, retryable network state.
50    Unreachable,
51}
52
53impl ResolveOutcome {
54    /// `true` iff this is verified content.
55    pub fn is_success(&self) -> bool {
56        matches!(self, ResolveOutcome::Success(_))
57    }
58
59    /// The verified data, if this is a success.
60    pub fn data(&self) -> Option<&ResolvedData> {
61        match self {
62            ResolveOutcome::Success(d) => Some(d),
63            _ => None,
64        }
65    }
66
67    /// A stable machine-readable tag: `"success"` / `"integrity_failure"` / `"unreachable"`.
68    pub fn kind(&self) -> &'static str {
69        match self {
70            ResolveOutcome::Success(_) => "success",
71            ResolveOutcome::IntegrityFailure => "integrity_failure",
72            ResolveOutcome::Unreachable => "unreachable",
73        }
74    }
75}
76
77/// Options for a resolve. All optional; a resolver applies its own §5.3-ladder defaults.
78#[derive(Debug, Clone, Default, PartialEq, Eq)]
79pub struct ResolveOptions {
80    /// An explicit endpoint override. When set it WINS and skips the ladder (§5.3): a loopback host
81    /// may use the node path; any other host is a verified rpc endpoint.
82    pub endpoint: Option<String>,
83    /// Override the "connect a node" CTA target the resolver renders for an unreachable outcome.
84    pub connect_url: Option<String>,
85}
86
87/// A hard, fail-closed resolution failure. Distinct from [`ResolveOutcome::Unreachable`] (the
88/// network-down state) and [`ResolveOutcome::IntegrityFailure`] (bytes fetched but unverifiable).
89#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
90pub enum ResolveError {
91    /// The input was not a syntactically valid DIG URN.
92    #[error("invalid DIG URN: {0}")]
93    Parse(String),
94
95    /// A transport-level failure talking to a specific endpoint (DNS, TLS, connection, timeout,
96    /// malformed HTTP). Not "every tier down" (that is [`ResolveOutcome::Unreachable`]).
97    #[error("transport error: {0}")]
98    Transport(String),
99
100    /// The RPC endpoint returned a protocol error, or a response the resolver cannot interpret.
101    #[error("rpc error: {0}")]
102    Rpc(String),
103
104    /// The resource does not exist in the store at the resolved root. A hard, fail-closed verdict.
105    #[error("resource not found")]
106    NotFound,
107
108    /// A rootless URN was resolved over the untrusted blind tier, where the trust root cannot be
109    /// established without trusting the gateway. Pin a root in the URN, or resolve via a loopback
110    /// node. Fail-closed — the resolver will NOT verify against a gateway-asserted root.
111    #[error(
112        "a root-pinned URN is required to verify over the public gateway \
113         (rootless URNs are not chain-verified there)"
114    )]
115    RootRequired,
116
117    /// The served ciphertext failed integrity verification against the chain-anchored root (tampered
118    /// bytes, a non-chaining proof, or a decoy from a wrong store). FAIL-CLOSED: bytes discarded.
119    #[error("inclusion verification failed: {0}")]
120    VerifyFailed(String),
121
122    /// The verified ciphertext did not decrypt under the URN's key (AEAD tag failure — wrong
123    /// key/salt or corruption), or an untrusted chunk-length plan was inconsistent. FAIL-CLOSED.
124    #[error("decryption failed (wrong key/salt or corrupt ciphertext)")]
125    DecryptFailed,
126}
127
128/// Result alias for resolution operations.
129pub type Result<T> = core::result::Result<T, ResolveError>;
130
131/// The resolution contract: turn a [`DigUrn`](crate::DigUrn) into a typed [`ResolveOutcome`].
132///
133/// A concrete implementation walks the §5.3 node-first ladder over its own transport, verifies via
134/// the [`crate::verify`] contract, and MUST honour the fail-closed outcome distinction above — never
135/// returning unverified bytes as a `Success`.
136#[allow(async_fn_in_trait)] // A contract crate; a boxed-future/`Send` bound is the caller's choice.
137pub trait UrnResolver {
138    /// Resolve a URN string to a typed outcome, or a hard [`ResolveError`].
139    async fn resolve(&self, urn: &str, opts: &ResolveOptions) -> Result<ResolveOutcome>;
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[test]
147    fn outcome_accessors_are_exhaustive() {
148        let ok = ResolveOutcome::Success(ResolvedData::new(vec![1, 2], "text/plain".into()));
149        assert!(ok.is_success());
150        assert_eq!(ok.kind(), "success");
151        assert_eq!(ok.data().unwrap().bytes, vec![1, 2]);
152
153        assert_eq!(ResolveOutcome::IntegrityFailure.kind(), "integrity_failure");
154        assert!(!ResolveOutcome::IntegrityFailure.is_success());
155        assert!(ResolveOutcome::IntegrityFailure.data().is_none());
156
157        assert_eq!(ResolveOutcome::Unreachable.kind(), "unreachable");
158        assert!(ResolveOutcome::Unreachable.data().is_none());
159    }
160
161    #[test]
162    fn errors_render_stable_messages() {
163        assert!(ResolveError::RootRequired
164            .to_string()
165            .contains("root-pinned"));
166        assert_eq!(ResolveError::NotFound.to_string(), "resource not found");
167    }
168}