lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

error.rs (7181B)


      1 //! Stable Nostr transport failures.
      2 
      3 use core::fmt;
      4 
      5 /// Error returned by the Nostr transport adapter.
      6 #[derive(Clone, Debug, Eq, PartialEq)]
      7 #[non_exhaustive]
      8 pub enum Error {
      9     /// No relay was configured.
     10     EmptyRelaySet,
     11     /// The configured relay count exceeds the adapter bound.
     12     TooManyRelays { max: usize, actual: usize },
     13     /// A canonical relay URL occurs more than once.
     14     DuplicateRelayUrl,
     15     /// The URL is not a valid canonical Nostr relay target.
     16     InvalidRelayUrl,
     17     /// The URL scheme is not permitted by the selected policy.
     18     RelaySchemeDenied,
     19     /// The URL destination is not permitted by the selected policy.
     20     RelayDestinationDenied,
     21     /// DNS resolution produced no addresses.
     22     EmptyResolution,
     23     /// A resolved address violates the selected policy.
     24     ResolvedAddressDenied,
     25     /// An endpoint policy is incompatible with its host profile kind.
     26     RelayProfilePolicyMismatch,
     27     /// A connection or request timeout is outside its governed bounds.
     28     InvalidTimeout { field: &'static str, value_ms: u64 },
     29     /// The per-operation connection limit is outside its governed bounds.
     30     InvalidConnectionLimit { value: usize },
     31     /// The reconnect delay policy is empty, inverted, or exceeds its bound.
     32     InvalidReconnectBackoff {
     33         initial_delay_ms: u64,
     34         max_delay_ms: u64,
     35     },
     36     /// A transport-neutral target is not a Nostr target.
     37     UnexpectedTransport,
     38     /// A relay cursor contains a noncanonical event position.
     39     InvalidRelayCursor,
     40     /// The generic transport target rejected the relay URL.
     41     Target,
     42     /// The relay challenge is empty, malformed, or outside its time bounds.
     43     InvalidAuthChallenge,
     44     /// A different live challenge already exists for this relay.
     45     AuthChallengeConflict,
     46     /// No matching live challenge exists for this relay.
     47     AuthChallengeMissing,
     48     /// The matching challenge expired before response submission.
     49     AuthChallengeExpired,
     50     /// The host supplied no signed NIP-42 response.
     51     AuthSignerUnavailable,
     52     /// The signed response does not match the pending relay and challenge.
     53     AuthResponseMismatch,
     54     /// The signed response is malformed or cryptographically invalid.
     55     AuthResponseInvalid,
     56     /// The host explicitly rejected the pending challenge.
     57     AuthRejected,
     58     /// Internal authentication state could not be accessed safely.
     59     AuthStateUnavailable,
     60     /// The relay rejected or failed the explicit AUTH message.
     61     AuthTransport,
     62 }
     63 
     64 impl fmt::Display for Error {
     65     fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
     66         match self {
     67             Self::EmptyRelaySet => formatter.write_str("relay set must not be empty"),
     68             Self::TooManyRelays { max, actual } => {
     69                 write!(formatter, "relay count {actual} exceeds maximum {max}")
     70             }
     71             Self::DuplicateRelayUrl => formatter.write_str("duplicate relay URL"),
     72             Self::InvalidRelayUrl => formatter.write_str("invalid relay URL"),
     73             Self::RelaySchemeDenied => formatter.write_str("relay URL scheme is denied by policy"),
     74             Self::RelayDestinationDenied => {
     75                 formatter.write_str("relay URL destination is denied by policy")
     76             }
     77             Self::EmptyResolution => formatter.write_str("relay URL resolved to no addresses"),
     78             Self::ResolvedAddressDenied => {
     79                 formatter.write_str("relay URL resolved to a denied address")
     80             }
     81             Self::RelayProfilePolicyMismatch => {
     82                 formatter.write_str("relay endpoint policy does not match its profile kind")
     83             }
     84             Self::InvalidTimeout { field, value_ms } => {
     85                 write!(formatter, "invalid {field} timeout: {value_ms}ms")
     86             }
     87             Self::InvalidConnectionLimit { value } => {
     88                 write!(formatter, "invalid connection limit: {value}")
     89             }
     90             Self::InvalidReconnectBackoff {
     91                 initial_delay_ms,
     92                 max_delay_ms,
     93             } => write!(
     94                 formatter,
     95                 "invalid reconnect backoff: initial={initial_delay_ms}ms max={max_delay_ms}ms"
     96             ),
     97             Self::UnexpectedTransport => formatter.write_str("expected Nostr transport target"),
     98             Self::InvalidRelayCursor => formatter.write_str("invalid relay cursor"),
     99             Self::Target => formatter.write_str("transport target error"),
    100             Self::InvalidAuthChallenge => formatter.write_str("invalid NIP-42 challenge"),
    101             Self::AuthChallengeConflict => {
    102                 formatter.write_str("a different NIP-42 challenge is already pending")
    103             }
    104             Self::AuthChallengeMissing => {
    105                 formatter.write_str("matching NIP-42 challenge is not pending")
    106             }
    107             Self::AuthChallengeExpired => formatter.write_str("NIP-42 challenge expired"),
    108             Self::AuthSignerUnavailable => {
    109                 formatter.write_str("NIP-42 response signer is unavailable")
    110             }
    111             Self::AuthResponseMismatch => {
    112                 formatter.write_str("NIP-42 response does not match its challenge")
    113             }
    114             Self::AuthResponseInvalid => formatter.write_str("NIP-42 response is invalid"),
    115             Self::AuthRejected => formatter.write_str("NIP-42 challenge was rejected"),
    116             Self::AuthStateUnavailable => {
    117                 formatter.write_str("NIP-42 authentication state is unavailable")
    118             }
    119             Self::AuthTransport => formatter.write_str("NIP-42 relay submission failed"),
    120         }
    121     }
    122 }
    123 
    124 impl std::error::Error for Error {}
    125 
    126 #[cfg(test)]
    127 mod tests {
    128     use super::*;
    129     use std::error::Error as _;
    130 
    131     #[test]
    132     fn every_error_has_a_stable_nonempty_message() {
    133         let errors = [
    134             Error::EmptyRelaySet,
    135             Error::TooManyRelays { max: 1, actual: 2 },
    136             Error::DuplicateRelayUrl,
    137             Error::InvalidRelayUrl,
    138             Error::RelaySchemeDenied,
    139             Error::RelayDestinationDenied,
    140             Error::EmptyResolution,
    141             Error::ResolvedAddressDenied,
    142             Error::RelayProfilePolicyMismatch,
    143             Error::InvalidTimeout {
    144                 field: "request",
    145                 value_ms: 0,
    146             },
    147             Error::InvalidConnectionLimit { value: 0 },
    148             Error::InvalidReconnectBackoff {
    149                 initial_delay_ms: 0,
    150                 max_delay_ms: 1,
    151             },
    152             Error::UnexpectedTransport,
    153             Error::InvalidRelayCursor,
    154             Error::Target,
    155             Error::InvalidAuthChallenge,
    156             Error::AuthChallengeConflict,
    157             Error::AuthChallengeMissing,
    158             Error::AuthChallengeExpired,
    159             Error::AuthSignerUnavailable,
    160             Error::AuthResponseMismatch,
    161             Error::AuthResponseInvalid,
    162             Error::AuthRejected,
    163             Error::AuthStateUnavailable,
    164             Error::AuthTransport,
    165         ];
    166         for error in errors {
    167             assert!(!error.to_string().is_empty());
    168             assert!(error.source().is_none());
    169         }
    170     }
    171 }