myc

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

paths.rs (15329B)


      1 use std::path::{Path, PathBuf};
      2 
      3 use radroots_runtime_paths::{
      4     RadrootsPathProfile, RadrootsPathResolver, RadrootsRuntimePathSelection,
      5 };
      6 use serde::{Deserialize, Serialize};
      7 
      8 use crate::{
      9     config::{
     10         MycConfig, MycIdentityBackend, MycIdentitySourceSpec, config_parse_error,
     11         parse_optional_path_env,
     12     },
     13     error::MycError,
     14 };
     15 
     16 pub const DEFAULT_ENV_PATH: &str = "config.env";
     17 const DEFAULT_STATE_DIR_NAME: &str = "state";
     18 const DEFAULT_CUSTODY_DIR_NAME: &str = "custody";
     19 const DEFAULT_SIGNER_IDENTITY_FILE_NAME: &str = "signer-identity.json";
     20 const DEFAULT_USER_IDENTITY_FILE_NAME: &str = "user-identity.json";
     21 const DEFAULT_DISCOVERY_APP_IDENTITY_FILE_NAME: &str = "discovery-app-identity.json";
     22 const DEFAULT_SIGNER_MANAGED_ACCOUNT_FILE_NAME: &str = "signer-accounts.json";
     23 const DEFAULT_USER_MANAGED_ACCOUNT_FILE_NAME: &str = "user-accounts.json";
     24 const DEFAULT_DISCOVERY_MANAGED_ACCOUNT_FILE_NAME: &str = "discovery-accounts.json";
     25 const DEFAULT_DISCOVERY_PUBLIC_DIR_NAME: &str = "public";
     26 const DEFAULT_DISCOVERY_NIP05_RELATIVE_PATH: &str = ".well-known/nostr.json";
     27 const MYC_PATHS_PROFILE_ENV: &str = "MYC_PATHS_PROFILE";
     28 const MYC_PATHS_REPO_LOCAL_ROOT_ENV: &str = "MYC_PATHS_REPO_LOCAL_ROOT";
     29 
     30 #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
     31 #[serde(default, deny_unknown_fields)]
     32 pub struct MycPathsConfig {
     33     pub profile: MycPathProfile,
     34     pub repo_local_root: Option<PathBuf>,
     35     pub config_env_path: PathBuf,
     36     pub run_dir: PathBuf,
     37     pub state_dir: PathBuf,
     38     pub signer_identity_backend: MycIdentityBackend,
     39     pub signer_identity_path: PathBuf,
     40     pub signer_identity_keyring_account_id: Option<String>,
     41     pub signer_identity_keyring_service_name: String,
     42     pub signer_identity_profile_path: Option<PathBuf>,
     43     pub user_identity_backend: MycIdentityBackend,
     44     pub user_identity_path: PathBuf,
     45     pub user_identity_keyring_account_id: Option<String>,
     46     pub user_identity_keyring_service_name: String,
     47     pub user_identity_profile_path: Option<PathBuf>,
     48 }
     49 
     50 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
     51 #[serde(rename_all = "snake_case")]
     52 pub enum MycPathProfile {
     53     InteractiveUser,
     54     ServiceHost,
     55     RepoLocal,
     56 }
     57 
     58 #[derive(Debug, Clone, PartialEq, Eq)]
     59 struct MycResolvedRuntimePaths {
     60     config_env_path: PathBuf,
     61     logs_dir: PathBuf,
     62     run_dir: PathBuf,
     63     state_dir: PathBuf,
     64     signer_identity_path: PathBuf,
     65     user_identity_path: PathBuf,
     66     signer_managed_account_path: PathBuf,
     67     user_managed_account_path: PathBuf,
     68     discovery_app_identity_path: PathBuf,
     69     discovery_managed_account_path: PathBuf,
     70     discovery_nip05_output_path: PathBuf,
     71 }
     72 
     73 #[derive(Debug, Clone, Default, PartialEq, Eq)]
     74 pub(crate) struct MycPathOverrideFlags {
     75     pub(crate) logging_output_dir: bool,
     76     pub(crate) state_dir: bool,
     77     pub(crate) signer_identity_path: bool,
     78     pub(crate) user_identity_path: bool,
     79     pub(crate) discovery_app_identity_path: bool,
     80     pub(crate) discovery_nip05_output_path: bool,
     81 }
     82 
     83 impl Default for MycPathsConfig {
     84     fn default() -> Self {
     85         Self::default_with_path_selection(
     86             &RadrootsPathResolver::current(),
     87             MycPathProfile::InteractiveUser,
     88             None,
     89         )
     90         .expect("current process should resolve myc runtime paths")
     91     }
     92 }
     93 
     94 impl Default for MycPathProfile {
     95     fn default() -> Self {
     96         Self::InteractiveUser
     97     }
     98 }
     99 
    100 impl MycPathProfile {
    101     pub fn as_str(self) -> &'static str {
    102         match self {
    103             Self::InteractiveUser => "interactive_user",
    104             Self::ServiceHost => "service_host",
    105             Self::RepoLocal => "repo_local",
    106         }
    107     }
    108 
    109     fn into_radroots_profile(self) -> RadrootsPathProfile {
    110         match self {
    111             Self::InteractiveUser => RadrootsPathProfile::InteractiveUser,
    112             Self::ServiceHost => RadrootsPathProfile::ServiceHost,
    113             Self::RepoLocal => RadrootsPathProfile::RepoLocal,
    114         }
    115     }
    116 }
    117 
    118 impl MycResolvedRuntimePaths {
    119     fn resolve(
    120         resolver: &RadrootsPathResolver,
    121         profile: MycPathProfile,
    122         repo_local_root: Option<&Path>,
    123     ) -> Result<Self, MycError> {
    124         let selection = RadrootsRuntimePathSelection::caller(
    125             profile.into_radroots_profile(),
    126             repo_local_root.map(Path::to_path_buf),
    127         );
    128         let namespaced = selection
    129             .resolve_service_roots(
    130                 resolver,
    131                 "myc",
    132                 MYC_PATHS_PROFILE_ENV,
    133                 MYC_PATHS_REPO_LOCAL_ROOT_ENV,
    134             )
    135             .map_err(|error| {
    136                 MycError::InvalidConfig(format!("resolve myc runtime paths: {error}"))
    137             })?;
    138         let custody_dir = namespaced.data.join(DEFAULT_CUSTODY_DIR_NAME);
    139         Ok(Self {
    140             config_env_path: namespaced.config.join(DEFAULT_ENV_PATH),
    141             logs_dir: namespaced.logs,
    142             run_dir: namespaced.run,
    143             state_dir: namespaced.data.join(DEFAULT_STATE_DIR_NAME),
    144             signer_identity_path: namespaced.secrets.join(DEFAULT_SIGNER_IDENTITY_FILE_NAME),
    145             user_identity_path: namespaced.secrets.join(DEFAULT_USER_IDENTITY_FILE_NAME),
    146             signer_managed_account_path: custody_dir.join(DEFAULT_SIGNER_MANAGED_ACCOUNT_FILE_NAME),
    147             user_managed_account_path: custody_dir.join(DEFAULT_USER_MANAGED_ACCOUNT_FILE_NAME),
    148             discovery_app_identity_path: namespaced
    149                 .secrets
    150                 .join(DEFAULT_DISCOVERY_APP_IDENTITY_FILE_NAME),
    151             discovery_managed_account_path: custody_dir
    152                 .join(DEFAULT_DISCOVERY_MANAGED_ACCOUNT_FILE_NAME),
    153             discovery_nip05_output_path: namespaced
    154                 .data
    155                 .join(DEFAULT_DISCOVERY_PUBLIC_DIR_NAME)
    156                 .join(DEFAULT_DISCOVERY_NIP05_RELATIVE_PATH),
    157         })
    158     }
    159 }
    160 
    161 impl MycPathsConfig {
    162     pub(crate) fn default_with_path_selection(
    163         resolver: &RadrootsPathResolver,
    164         profile: MycPathProfile,
    165         repo_local_root: Option<&Path>,
    166     ) -> Result<Self, MycError> {
    167         let resolved = MycResolvedRuntimePaths::resolve(resolver, profile, repo_local_root)?;
    168         Ok(Self {
    169             profile,
    170             repo_local_root: repo_local_root.map(Path::to_path_buf),
    171             config_env_path: resolved.config_env_path,
    172             run_dir: resolved.run_dir,
    173             state_dir: resolved.state_dir,
    174             signer_identity_backend: MycIdentityBackend::EncryptedFile,
    175             signer_identity_path: resolved.signer_identity_path,
    176             signer_identity_keyring_account_id: None,
    177             signer_identity_keyring_service_name: "org.radroots.myc.signer".to_owned(),
    178             signer_identity_profile_path: None,
    179             user_identity_backend: MycIdentityBackend::EncryptedFile,
    180             user_identity_path: resolved.user_identity_path,
    181             user_identity_keyring_account_id: None,
    182             user_identity_keyring_service_name: "org.radroots.myc.user".to_owned(),
    183             user_identity_profile_path: None,
    184         })
    185     }
    186 
    187     pub fn signer_identity_source(&self) -> MycIdentitySourceSpec {
    188         MycIdentitySourceSpec {
    189             backend: self.signer_identity_backend,
    190             path: match self.signer_identity_backend {
    191                 MycIdentityBackend::EncryptedFile
    192                 | MycIdentityBackend::PlaintextFile
    193                 | MycIdentityBackend::ManagedAccount
    194                 | MycIdentityBackend::ExternalCommand => Some(self.signer_identity_path.clone()),
    195                 MycIdentityBackend::HostVault => None,
    196             },
    197             keyring_account_id: match self.signer_identity_backend {
    198                 MycIdentityBackend::EncryptedFile
    199                 | MycIdentityBackend::PlaintextFile
    200                 | MycIdentityBackend::ManagedAccount
    201                 | MycIdentityBackend::ExternalCommand => None,
    202                 MycIdentityBackend::HostVault => self.signer_identity_keyring_account_id.clone(),
    203             },
    204             keyring_service_name: match self.signer_identity_backend {
    205                 MycIdentityBackend::EncryptedFile
    206                 | MycIdentityBackend::PlaintextFile
    207                 | MycIdentityBackend::ExternalCommand => None,
    208                 MycIdentityBackend::HostVault | MycIdentityBackend::ManagedAccount => {
    209                     Some(self.signer_identity_keyring_service_name.clone())
    210                 }
    211             },
    212             profile_path: match self.signer_identity_backend {
    213                 MycIdentityBackend::EncryptedFile
    214                 | MycIdentityBackend::PlaintextFile
    215                 | MycIdentityBackend::ManagedAccount
    216                 | MycIdentityBackend::ExternalCommand => None,
    217                 MycIdentityBackend::HostVault => self.signer_identity_profile_path.clone(),
    218             },
    219         }
    220     }
    221 
    222     pub fn user_identity_source(&self) -> MycIdentitySourceSpec {
    223         MycIdentitySourceSpec {
    224             backend: self.user_identity_backend,
    225             path: match self.user_identity_backend {
    226                 MycIdentityBackend::EncryptedFile
    227                 | MycIdentityBackend::PlaintextFile
    228                 | MycIdentityBackend::ManagedAccount
    229                 | MycIdentityBackend::ExternalCommand => Some(self.user_identity_path.clone()),
    230                 MycIdentityBackend::HostVault => None,
    231             },
    232             keyring_account_id: match self.user_identity_backend {
    233                 MycIdentityBackend::EncryptedFile
    234                 | MycIdentityBackend::PlaintextFile
    235                 | MycIdentityBackend::ManagedAccount
    236                 | MycIdentityBackend::ExternalCommand => None,
    237                 MycIdentityBackend::HostVault => self.user_identity_keyring_account_id.clone(),
    238             },
    239             keyring_service_name: match self.user_identity_backend {
    240                 MycIdentityBackend::EncryptedFile
    241                 | MycIdentityBackend::PlaintextFile
    242                 | MycIdentityBackend::ExternalCommand => None,
    243                 MycIdentityBackend::HostVault | MycIdentityBackend::ManagedAccount => {
    244                     Some(self.user_identity_keyring_service_name.clone())
    245                 }
    246             },
    247             profile_path: match self.user_identity_backend {
    248                 MycIdentityBackend::EncryptedFile
    249                 | MycIdentityBackend::PlaintextFile
    250                 | MycIdentityBackend::ManagedAccount
    251                 | MycIdentityBackend::ExternalCommand => None,
    252                 MycIdentityBackend::HostVault => self.user_identity_profile_path.clone(),
    253             },
    254         }
    255     }
    256 }
    257 
    258 pub(crate) fn process_path_selection() -> Result<(MycPathProfile, Option<PathBuf>), MycError> {
    259     let selection = RadrootsRuntimePathSelection::from_env(
    260         MYC_PATHS_PROFILE_ENV,
    261         MYC_PATHS_REPO_LOCAL_ROOT_ENV,
    262         RadrootsPathProfile::InteractiveUser,
    263     )
    264     .map_err(|error| MycError::InvalidConfig(error.to_string()))?;
    265     Ok((
    266         from_radroots_profile(selection.profile),
    267         selection.repo_local_root,
    268     ))
    269 }
    270 
    271 fn from_radroots_profile(profile: RadrootsPathProfile) -> MycPathProfile {
    272     match profile {
    273         RadrootsPathProfile::InteractiveUser => MycPathProfile::InteractiveUser,
    274         RadrootsPathProfile::ServiceHost => MycPathProfile::ServiceHost,
    275         RadrootsPathProfile::RepoLocal => MycPathProfile::RepoLocal,
    276         RadrootsPathProfile::MobileNative => MycPathProfile::InteractiveUser,
    277     }
    278 }
    279 
    280 pub(crate) fn default_env_path_with_path_selection(
    281     resolver: &RadrootsPathResolver,
    282     profile: MycPathProfile,
    283     repo_local_root: Option<&Path>,
    284 ) -> Result<PathBuf, MycError> {
    285     Ok(MycResolvedRuntimePaths::resolve(resolver, profile, repo_local_root)?.config_env_path)
    286 }
    287 
    288 pub(crate) fn apply_path_defaults(
    289     config: &mut MycConfig,
    290     resolver: &RadrootsPathResolver,
    291     overrides: &MycPathOverrideFlags,
    292 ) -> Result<(), MycError> {
    293     let resolved = MycResolvedRuntimePaths::resolve(
    294         resolver,
    295         config.paths.profile,
    296         config.paths.repo_local_root.as_deref(),
    297     )?;
    298     config.paths.config_env_path = resolved.config_env_path;
    299     config.paths.run_dir = resolved.run_dir;
    300     if !overrides.logging_output_dir {
    301         config.logging.output_dir = Some(resolved.logs_dir);
    302     }
    303     if !overrides.state_dir {
    304         config.paths.state_dir = resolved.state_dir;
    305     }
    306     if !overrides.signer_identity_path {
    307         config.paths.signer_identity_path = match config.paths.signer_identity_backend {
    308             MycIdentityBackend::EncryptedFile | MycIdentityBackend::PlaintextFile => {
    309                 resolved.signer_identity_path
    310             }
    311             MycIdentityBackend::ManagedAccount => resolved.signer_managed_account_path,
    312             MycIdentityBackend::HostVault => PathBuf::new(),
    313             MycIdentityBackend::ExternalCommand => PathBuf::new(),
    314         };
    315     }
    316     if !overrides.user_identity_path {
    317         config.paths.user_identity_path = match config.paths.user_identity_backend {
    318             MycIdentityBackend::EncryptedFile | MycIdentityBackend::PlaintextFile => {
    319                 resolved.user_identity_path
    320             }
    321             MycIdentityBackend::ManagedAccount => resolved.user_managed_account_path,
    322             MycIdentityBackend::HostVault => PathBuf::new(),
    323             MycIdentityBackend::ExternalCommand => PathBuf::new(),
    324         };
    325     }
    326     if !overrides.discovery_app_identity_path {
    327         config.discovery.app_identity_path = match config.discovery.app_identity_backend {
    328             Some(MycIdentityBackend::EncryptedFile) | Some(MycIdentityBackend::PlaintextFile) => {
    329                 Some(resolved.discovery_app_identity_path)
    330             }
    331             Some(MycIdentityBackend::ManagedAccount) => {
    332                 Some(resolved.discovery_managed_account_path)
    333             }
    334             Some(MycIdentityBackend::HostVault) | None => None,
    335             Some(MycIdentityBackend::ExternalCommand) => None,
    336         };
    337     }
    338     if !overrides.discovery_nip05_output_path {
    339         config.discovery.nip05_output_path = Some(resolved.discovery_nip05_output_path);
    340     }
    341     Ok(())
    342 }
    343 
    344 pub(crate) fn path_selection_from_entries(
    345     entries: &[(String, String, usize)],
    346     path: &Path,
    347 ) -> Result<(MycPathProfile, Option<PathBuf>), MycError> {
    348     let mut profile = MycPathProfile::InteractiveUser;
    349     let mut repo_local_root = None;
    350     for (key, value, line_number) in entries {
    351         match key.as_str() {
    352             MYC_PATHS_PROFILE_ENV => {
    353                 profile = parse_path_profile_env(key, value, path, *line_number)?;
    354             }
    355             MYC_PATHS_REPO_LOCAL_ROOT_ENV => {
    356                 repo_local_root = parse_optional_path_env(value);
    357             }
    358             _ => {}
    359         }
    360     }
    361     Ok((profile, repo_local_root))
    362 }
    363 
    364 pub(crate) fn parse_path_profile_env(
    365     key: &str,
    366     value: &str,
    367     path: &Path,
    368     line_number: usize,
    369 ) -> Result<MycPathProfile, MycError> {
    370     match value {
    371         "interactive_user" => Ok(MycPathProfile::InteractiveUser),
    372         "service_host" => Ok(MycPathProfile::ServiceHost),
    373         "repo_local" => Ok(MycPathProfile::RepoLocal),
    374         _ => Err(config_parse_error(
    375             path,
    376             line_number,
    377             format!("{key} must be `interactive_user`, `service_host`, or `repo_local`"),
    378         )),
    379     }
    380 }