myc

Self-custodial remote signer for Radroots apps
git clone https://radroots.dev/git/myc.git
Log | Files | Refs | README | LICENSE

error.rs (13426B)


      1 use std::net::SocketAddr;
      2 use std::path::PathBuf;
      3 
      4 use radroots_identity::IdentityError;
      5 use radroots_nostr::prelude::RadrootsNostrError;
      6 use radroots_nostr_accounts::prelude::RadrootsNostrAccountsError;
      7 use radroots_nostr_connect::prelude::RadrootsNostrConnectError;
      8 use radroots_nostr_signer::prelude::RadrootsNostrSignerError;
      9 use radroots_sql_core::error::SqlError;
     10 use thiserror::Error;
     11 
     12 use crate::config::MycTransportDeliveryPolicy;
     13 
     14 #[derive(Debug, Error)]
     15 pub enum MycError {
     16     #[error("config io error at {path}: {source}")]
     17     ConfigIo {
     18         path: PathBuf,
     19         #[source]
     20         source: std::io::Error,
     21     },
     22     #[error("config parse error at {path}:{line_number}: {message}")]
     23     ConfigParse {
     24         path: PathBuf,
     25         line_number: usize,
     26         message: String,
     27     },
     28     #[error("invalid config: {0}")]
     29     InvalidConfig(String),
     30     #[error("invalid operation: {0}")]
     31     InvalidOperation(String),
     32     #[error("invalid log filter `{filter}`: {source}")]
     33     InvalidLogFilter {
     34         filter: String,
     35         #[source]
     36         source: tracing_subscriber::filter::ParseError,
     37     },
     38     #[error("logging already initialized")]
     39     LoggingAlreadyInitialized,
     40     #[error("failed to create directory {path}: {source}")]
     41     CreateDir {
     42         path: PathBuf,
     43         #[source]
     44         source: std::io::Error,
     45     },
     46     #[error("persistence io error at {path}: {source}")]
     47     PersistenceIo {
     48         path: PathBuf,
     49         #[source]
     50         source: std::io::Error,
     51     },
     52     #[error("failed to serialize persistence data at {path}: {source}")]
     53     PersistenceSerialize {
     54         path: PathBuf,
     55         #[source]
     56         source: serde_json::Error,
     57     },
     58     #[error("failed to parse persistence backup manifest at {path}: {source}")]
     59     PersistenceManifestParse {
     60         path: PathBuf,
     61         #[source]
     62         source: serde_json::Error,
     63     },
     64     #[error("failed to bind observability server at {bind_addr}: {source}")]
     65     ObservabilityBind {
     66         bind_addr: SocketAddr,
     67         #[source]
     68         source: std::io::Error,
     69     },
     70     #[error("observability server failed at {bind_addr}: {source}")]
     71     ObservabilityServe {
     72         bind_addr: SocketAddr,
     73         #[source]
     74         source: std::io::Error,
     75     },
     76     #[error("audit io error at {path}: {source}")]
     77     AuditIo {
     78         path: PathBuf,
     79         #[source]
     80         source: std::io::Error,
     81     },
     82     #[error("audit parse error at {path}:{line_number}: {source}")]
     83     AuditParse {
     84         path: PathBuf,
     85         line_number: usize,
     86         #[source]
     87         source: serde_json::Error,
     88     },
     89     #[error("failed to serialize audit record at {path}: {source}")]
     90     AuditSerialize {
     91         path: PathBuf,
     92         #[source]
     93         source: serde_json::Error,
     94     },
     95     #[error("audit sqlite error at {path}: {source}")]
     96     AuditSql {
     97         path: PathBuf,
     98         #[source]
     99         source: SqlError,
    100     },
    101     #[error("audit sqlite decode error at {path}: {source}")]
    102     AuditSqlDecode {
    103         path: PathBuf,
    104         #[source]
    105         source: serde_json::Error,
    106     },
    107     #[error("delivery outbox sqlite error at {path}: {source}")]
    108     DeliveryOutboxSql {
    109         path: PathBuf,
    110         #[source]
    111         source: SqlError,
    112     },
    113     #[error("delivery outbox sqlite decode error at {path}: {source}")]
    114     DeliveryOutboxSqlDecode {
    115         path: PathBuf,
    116         #[source]
    117         source: serde_json::Error,
    118     },
    119     #[error("failed to serialize delivery outbox record at {path}: {source}")]
    120     DeliveryOutboxSerialize {
    121         path: PathBuf,
    122         #[source]
    123         source: serde_json::Error,
    124     },
    125     #[error("invalid delivery outbox job id `{0}`")]
    126     InvalidDeliveryOutboxJobId(String),
    127     #[error("delivery outbox job not found: {0}")]
    128     DeliveryOutboxJobNotFound(String),
    129     #[error("discovery io error at {path}: {source}")]
    130     DiscoveryIo {
    131         path: PathBuf,
    132         #[source]
    133         source: std::io::Error,
    134     },
    135     #[error("discovery parse error at {path}: {source}")]
    136     DiscoveryParse {
    137         path: PathBuf,
    138         #[source]
    139         source: serde_json::Error,
    140     },
    141     #[error("invalid discovery bundle: {0}")]
    142     InvalidDiscoveryBundle(String),
    143     #[error("invalid discovery event: {0}")]
    144     InvalidDiscoveryEvent(String),
    145     #[error(
    146         "failed to fetch discovery state from all configured relays ({relay_count}): {details}"
    147     )]
    148     DiscoveryFetchUnavailable { relay_count: usize, details: String },
    149     #[error("discovery refresh attempt {attempt_id} failed: {source}")]
    150     DiscoveryRefreshFailed {
    151         attempt_id: String,
    152         #[source]
    153         source: Box<MycError>,
    154     },
    155     #[error("custody manager error for {role} identity: {source}")]
    156     CustodyManager {
    157         role: String,
    158         #[source]
    159         source: RadrootsNostrAccountsError,
    160     },
    161     #[error("custody vault error for {role} identity: {source}")]
    162     CustodyVault {
    163         role: String,
    164         #[source]
    165         source: RadrootsNostrAccountsError,
    166     },
    167     #[error(
    168         "no secret found in custody vault service `{service_name}` for {role} identity `{account_id}`"
    169     )]
    170     CustodySecretNotFound {
    171         role: String,
    172         service_name: String,
    173         account_id: String,
    174     },
    175     #[error(
    176         "custody vault service `{service_name}` resolved {role} identity `{resolved_identity_id}` but expected `{account_id}`"
    177     )]
    178     CustodySecretIdentityMismatch {
    179         role: String,
    180         service_name: String,
    181         account_id: String,
    182         resolved_identity_id: String,
    183     },
    184     #[error(
    185         "public identity file {path} resolved {role} identity `{profile_identity_id}` but expected `{account_id}`"
    186     )]
    187     CustodyProfileIdentityMismatch {
    188         role: String,
    189         path: PathBuf,
    190         account_id: String,
    191         profile_identity_id: String,
    192     },
    193     #[error("no selected managed account configured for {role} identity store {path}")]
    194     CustodyManagedAccountNotConfigured { role: String, path: PathBuf },
    195     #[error(
    196         "selected managed account `{account_id}` in {path} for {role} identity has no secret in keyring service `{service_name}`"
    197     )]
    198     CustodyManagedAccountPublicOnly {
    199         role: String,
    200         path: PathBuf,
    201         service_name: String,
    202         account_id: String,
    203     },
    204     #[error("external custody command io error for {role} identity at {path}: {source}")]
    205     CustodyExternalCommandIo {
    206         role: String,
    207         path: PathBuf,
    208         #[source]
    209         source: std::io::Error,
    210     },
    211     #[error(
    212         "external custody command for {role} identity at {path} timed out after {timeout_secs}s"
    213     )]
    214     CustodyExternalCommandTimedOut {
    215         role: String,
    216         path: PathBuf,
    217         timeout_secs: u64,
    218     },
    219     #[error(
    220         "external custody command for {role} identity at {path} failed with status {status}: {stderr}"
    221     )]
    222     CustodyExternalCommandFailed {
    223         role: String,
    224         path: PathBuf,
    225         status: String,
    226         stderr: String,
    227     },
    228     #[error(
    229         "external custody command response parse error for {role} identity at {path}: {source}"
    230     )]
    231     CustodyExternalCommandParse {
    232         role: String,
    233         path: PathBuf,
    234         #[source]
    235         source: serde_json::Error,
    236     },
    237     #[error(
    238         "external custody command returned invalid public identity for {role} at {path}: {message}"
    239     )]
    240     CustodyExternalCommandInvalidIdentity {
    241         role: String,
    242         path: PathBuf,
    243         message: String,
    244     },
    245     #[error(transparent)]
    246     Identity(#[from] IdentityError),
    247     #[error(transparent)]
    248     Nostr(#[from] RadrootsNostrError),
    249     #[error(transparent)]
    250     NostrConnect(#[from] RadrootsNostrConnectError),
    251     #[error(transparent)]
    252     SignerState(#[from] RadrootsNostrSignerError),
    253     #[error(transparent)]
    254     Json(#[from] serde_json::Error),
    255     #[error("NIP-46 decrypt failed: {0}")]
    256     Nip46Decrypt(String),
    257     #[error("NIP-46 encrypt failed: {0}")]
    258     Nip46Encrypt(String),
    259     #[error("NIP-46 listener notifications closed")]
    260     Nip46ListenerClosed,
    261     #[error(
    262         "Nostr publish failed for {operation} after {attempt_count} attempt(s) with delivery policy {} requiring {required_acknowledged_relay_count} acknowledgements: {details}",
    263         delivery_policy.as_str()
    264     )]
    265     PublishRejected {
    266         operation: String,
    267         relay_count: usize,
    268         acknowledged_relay_count: usize,
    269         required_acknowledged_relay_count: usize,
    270         delivery_policy: MycTransportDeliveryPolicy,
    271         attempt_count: usize,
    272         details: String,
    273         rejected_relays: Vec<String>,
    274     },
    275     #[error(
    276         "configured signer identity `{configured_identity_id}` at {identity_path} does not match persisted signer identity `{persisted_identity_id}` in {state_path}"
    277     )]
    278     SignerIdentityMismatch {
    279         identity_path: PathBuf,
    280         state_path: PathBuf,
    281         configured_identity_id: String,
    282         persisted_identity_id: String,
    283     },
    284     #[error(
    285         "configured signer identity `{configured_identity_id}` does not match imported signer identity `{imported_identity_id}` from {state_path}"
    286     )]
    287     SignerIdentityImportMismatch {
    288         state_path: PathBuf,
    289         configured_identity_id: String,
    290         imported_identity_id: String,
    291     },
    292 }
    293 
    294 impl MycError {
    295     pub fn with_discovery_refresh_attempt_id(self, attempt_id: impl Into<String>) -> Self {
    296         match self {
    297             Self::DiscoveryRefreshFailed { .. } => self,
    298             source => Self::DiscoveryRefreshFailed {
    299                 attempt_id: attempt_id.into(),
    300                 source: Box::new(source),
    301             },
    302         }
    303     }
    304 
    305     pub fn discovery_refresh_attempt_id(&self) -> Option<&str> {
    306         match self {
    307             Self::DiscoveryRefreshFailed { attempt_id, .. } => Some(attempt_id.as_str()),
    308             _ => None,
    309         }
    310     }
    311 
    312     pub fn publish_rejection_details(&self) -> Option<&str> {
    313         match self {
    314             Self::PublishRejected { details, .. } => Some(details.as_str()),
    315             Self::DiscoveryRefreshFailed { source, .. } => source.publish_rejection_details(),
    316             _ => None,
    317         }
    318     }
    319 
    320     pub fn publish_rejection_counts(&self) -> Option<(usize, usize)> {
    321         match self {
    322             Self::PublishRejected {
    323                 relay_count,
    324                 acknowledged_relay_count,
    325                 ..
    326             } => Some((*relay_count, *acknowledged_relay_count)),
    327             Self::DiscoveryRefreshFailed { source, .. } => source.publish_rejection_counts(),
    328             _ => None,
    329         }
    330     }
    331 
    332     pub fn publish_rejected_relays(&self) -> Option<&[String]> {
    333         match self {
    334             Self::PublishRejected {
    335                 rejected_relays, ..
    336             } => Some(rejected_relays.as_slice()),
    337             Self::DiscoveryRefreshFailed { source, .. } => source.publish_rejected_relays(),
    338             _ => None,
    339         }
    340     }
    341 
    342     pub fn publish_delivery_policy(&self) -> Option<MycTransportDeliveryPolicy> {
    343         match self {
    344             Self::PublishRejected {
    345                 delivery_policy, ..
    346             } => Some(*delivery_policy),
    347             Self::DiscoveryRefreshFailed { source, .. } => source.publish_delivery_policy(),
    348             _ => None,
    349         }
    350     }
    351 
    352     pub fn publish_attempt_count(&self) -> Option<usize> {
    353         match self {
    354             Self::PublishRejected { attempt_count, .. } => Some(*attempt_count),
    355             Self::DiscoveryRefreshFailed { source, .. } => source.publish_attempt_count(),
    356             _ => None,
    357         }
    358     }
    359 
    360     pub fn publish_required_acknowledged_relay_count(&self) -> Option<usize> {
    361         match self {
    362             Self::PublishRejected {
    363                 required_acknowledged_relay_count,
    364                 ..
    365             } => Some(*required_acknowledged_relay_count),
    366             Self::DiscoveryRefreshFailed { source, .. } => {
    367                 source.publish_required_acknowledged_relay_count()
    368             }
    369             _ => None,
    370         }
    371     }
    372 }
    373 
    374 #[cfg(test)]
    375 mod tests {
    376     use crate::config::MycTransportDeliveryPolicy;
    377 
    378     use super::MycError;
    379 
    380     #[test]
    381     fn discovery_refresh_wrapper_preserves_attempt_id_and_publish_details() {
    382         let wrapped = MycError::PublishRejected {
    383             operation: "discovery refresh".to_owned(),
    384             relay_count: 2,
    385             acknowledged_relay_count: 0,
    386             required_acknowledged_relay_count: 1,
    387             delivery_policy: MycTransportDeliveryPolicy::Any,
    388             attempt_count: 2,
    389             details: "relay-a: blocked".to_owned(),
    390             rejected_relays: vec!["wss://relay-a.example.com".to_owned()],
    391         }
    392         .with_discovery_refresh_attempt_id("attempt-1");
    393 
    394         assert_eq!(wrapped.discovery_refresh_attempt_id(), Some("attempt-1"));
    395         assert_eq!(
    396             wrapped.publish_rejection_details(),
    397             Some("relay-a: blocked")
    398         );
    399         assert_eq!(wrapped.publish_rejection_counts(), Some((2, 0)));
    400         assert_eq!(
    401             wrapped.publish_rejected_relays(),
    402             Some(["wss://relay-a.example.com".to_owned()].as_slice())
    403         );
    404         assert_eq!(
    405             wrapped.publish_delivery_policy(),
    406             Some(MycTransportDeliveryPolicy::Any)
    407         );
    408         assert_eq!(wrapped.publish_required_acknowledged_relay_count(), Some(1));
    409         assert_eq!(wrapped.publish_attempt_count(), Some(2));
    410     }
    411 }