lib

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

store.rs (44498B)


      1 use crate::error::RadrootsNostrSignerError;
      2 use crate::model::RadrootsNostrSignerStoreState;
      3 use radroots_runtime::json::{JsonFile, JsonWriteOptions};
      4 #[cfg(feature = "native")]
      5 use serde::{Deserialize, de::DeserializeOwned};
      6 #[cfg(feature = "native")]
      7 use serde_json::{Value, json};
      8 use std::path::{Path, PathBuf};
      9 use std::sync::{Arc, RwLock};
     10 
     11 #[cfg(feature = "native")]
     12 use crate::model::{
     13     RadrootsNostrSignerApprovalRequirement, RadrootsNostrSignerApprovalState,
     14     RadrootsNostrSignerAuthChallenge, RadrootsNostrSignerAuthState,
     15     RadrootsNostrSignerConnectSecretHash, RadrootsNostrSignerConnectionRecord,
     16     RadrootsNostrSignerConnectionStatus, RadrootsNostrSignerPendingRequest,
     17     RadrootsNostrSignerPermissionGrant, RadrootsNostrSignerPublishWorkflowKind,
     18     RadrootsNostrSignerPublishWorkflowRecord, RadrootsNostrSignerPublishWorkflowState,
     19     RadrootsNostrSignerRequestAuditRecord, RadrootsNostrSignerRequestDecision,
     20 };
     21 #[cfg(feature = "native")]
     22 use crate::sqlite::RadrootsNostrSignerSqliteDb;
     23 #[cfg(feature = "native")]
     24 use nostr::RelayUrl;
     25 #[cfg(feature = "native")]
     26 use radroots_identity::RadrootsIdentityPublic;
     27 #[cfg(feature = "native")]
     28 use radroots_nostr_connect::prelude::{
     29     RadrootsNostrConnectMethod, RadrootsNostrConnectPermission, RadrootsNostrConnectRequestMessage,
     30 };
     31 #[cfg(feature = "native")]
     32 use radroots_sql_core::SqlExecutor;
     33 #[cfg(feature = "native")]
     34 use std::collections::BTreeMap;
     35 
     36 pub trait RadrootsNostrSignerStore: Send + Sync {
     37     fn load(&self) -> Result<RadrootsNostrSignerStoreState, RadrootsNostrSignerError>;
     38     fn save(&self, state: &RadrootsNostrSignerStoreState) -> Result<(), RadrootsNostrSignerError>;
     39 }
     40 
     41 #[derive(Debug, Clone)]
     42 pub struct RadrootsNostrFileSignerStore {
     43     path: PathBuf,
     44 }
     45 
     46 #[derive(Debug, Clone, Default)]
     47 pub struct RadrootsNostrMemorySignerStore {
     48     state: Arc<RwLock<RadrootsNostrSignerStoreState>>,
     49 }
     50 
     51 #[cfg(feature = "native")]
     52 #[derive(Clone)]
     53 pub struct RadrootsNostrSqliteSignerStore {
     54     db: Arc<RadrootsNostrSignerSqliteDb>,
     55 }
     56 
     57 impl RadrootsNostrFileSignerStore {
     58     pub fn new(path: impl AsRef<Path>) -> Self {
     59         Self {
     60             path: path.as_ref().to_path_buf(),
     61         }
     62     }
     63 
     64     pub fn path(&self) -> &Path {
     65         self.path.as_path()
     66     }
     67 }
     68 
     69 impl RadrootsNostrMemorySignerStore {
     70     pub fn new() -> Self {
     71         Self::default()
     72     }
     73 }
     74 
     75 #[cfg(feature = "native")]
     76 impl RadrootsNostrSqliteSignerStore {
     77     pub fn open(path: impl AsRef<Path>) -> Result<Self, RadrootsNostrSignerError> {
     78         Ok(Self {
     79             db: Arc::new(RadrootsNostrSignerSqliteDb::open(path)?),
     80         })
     81     }
     82 
     83     pub fn open_memory() -> Result<Self, RadrootsNostrSignerError> {
     84         Ok(Self {
     85             db: Arc::new(RadrootsNostrSignerSqliteDb::open_memory()?),
     86         })
     87     }
     88 }
     89 
     90 impl RadrootsNostrSignerStore for RadrootsNostrFileSignerStore {
     91     fn load(&self) -> Result<RadrootsNostrSignerStoreState, RadrootsNostrSignerError> {
     92         if !self.path.exists() {
     93             return Ok(RadrootsNostrSignerStoreState::default());
     94         }
     95         let file = JsonFile::<RadrootsNostrSignerStoreState>::load(self.path.as_path())?;
     96         Ok(file.value)
     97     }
     98 
     99     fn save(&self, state: &RadrootsNostrSignerStoreState) -> Result<(), RadrootsNostrSignerError> {
    100         let mut file = JsonFile::load_or_create_with(self.path.as_path(), || state.clone())?;
    101         file.set_options(JsonWriteOptions {
    102             pretty: true,
    103             mode_unix: Some(0o600),
    104         });
    105         file.value = state.clone();
    106         file.save()?;
    107         Ok(())
    108     }
    109 }
    110 
    111 impl RadrootsNostrSignerStore for RadrootsNostrMemorySignerStore {
    112     fn load(&self) -> Result<RadrootsNostrSignerStoreState, RadrootsNostrSignerError> {
    113         let guard = self
    114             .state
    115             .read()
    116             .map_err(|_| RadrootsNostrSignerError::Store("memory store lock poisoned".into()))?;
    117         Ok(guard.clone())
    118     }
    119 
    120     fn save(&self, state: &RadrootsNostrSignerStoreState) -> Result<(), RadrootsNostrSignerError> {
    121         let mut guard = self
    122             .state
    123             .write()
    124             .map_err(|_| RadrootsNostrSignerError::Store("memory store lock poisoned".into()))?;
    125         *guard = state.clone();
    126         Ok(())
    127     }
    128 }
    129 
    130 #[cfg(feature = "native")]
    131 impl RadrootsNostrSignerStore for RadrootsNostrSqliteSignerStore {
    132     fn load(&self) -> Result<RadrootsNostrSignerStoreState, RadrootsNostrSignerError> {
    133         let metadata_rows: Vec<SignerStoreMetadataRow> = query_rows(
    134             self.db.as_ref(),
    135             "SELECT store_version, signer_identity_json FROM signer_store_metadata WHERE singleton_id = 1",
    136         )?;
    137         let metadata = match metadata_rows.as_slice() {
    138             [row] => row,
    139             [] => {
    140                 return Err(RadrootsNostrSignerError::Store(
    141                     "sqlite signer metadata row missing".into(),
    142                 ));
    143             }
    144             _ => {
    145                 return Err(RadrootsNostrSignerError::Store(
    146                     "sqlite signer metadata row is not singular".into(),
    147                 ));
    148             }
    149         };
    150 
    151         let mut state = RadrootsNostrSignerStoreState {
    152             version: u32::try_from(metadata.store_version).map_err(|_| {
    153                 RadrootsNostrSignerError::Store(format!(
    154                     "sqlite signer store version {} is out of range",
    155                     metadata.store_version
    156                 ))
    157             })?,
    158             signer_identity: metadata
    159                 .signer_identity_json
    160                 .as_deref()
    161                 .map(parse_json_field::<RadrootsIdentityPublic>)
    162                 .transpose()?,
    163             connections: Vec::new(),
    164             audit_records: Vec::new(),
    165             publish_workflows: Vec::new(),
    166         };
    167 
    168         let connection_rows: Vec<SignerConnectionRow> = query_rows(
    169             self.db.as_ref(),
    170             "SELECT connection_id, client_public_key_hex, signer_identity_json, user_identity_json, connect_secret_hash_algorithm, connect_secret_hash_digest_hex, connect_secret_consumed_at_unix, requested_permissions_json, approval_requirement, approval_state, auth_state, status, status_reason, created_at_unix, updated_at_unix, last_authenticated_at_unix, last_request_at_unix FROM signer_connection ORDER BY created_at_unix, connection_id",
    171         )?;
    172         let mut connection_indexes = BTreeMap::new();
    173         for row in connection_rows {
    174             let connection = row.into_record()?;
    175             connection_indexes.insert(
    176                 connection.connection_id.as_str().to_owned(),
    177                 state.connections.len(),
    178             );
    179             state.connections.push(connection);
    180         }
    181 
    182         let permission_rows: Vec<SignerConnectionPermissionGrantRow> = query_rows(
    183             self.db.as_ref(),
    184             "SELECT connection_id, permission, granted_at_unix FROM signer_connection_permission_grant ORDER BY connection_id, granted_at_unix, permission",
    185         )?;
    186         for row in permission_rows {
    187             let index = *connection_indexes
    188                 .get(row.connection_id.as_str())
    189                 .ok_or_else(|| {
    190                     RadrootsNostrSignerError::Store(format!(
    191                         "permission grant row references missing connection `{}`",
    192                         row.connection_id
    193                     ))
    194                 })?;
    195             state.connections[index]
    196                 .granted_permissions
    197                 .push(row.into_grant()?);
    198         }
    199 
    200         let relay_rows: Vec<SignerConnectionRelayRow> = query_rows(
    201             self.db.as_ref(),
    202             "SELECT connection_id, ordinal, relay_url FROM signer_connection_relay ORDER BY connection_id, ordinal",
    203         )?;
    204         for row in relay_rows {
    205             let index = *connection_indexes
    206                 .get(row.connection_id.as_str())
    207                 .ok_or_else(|| {
    208                     RadrootsNostrSignerError::Store(format!(
    209                         "relay row references missing connection `{}`",
    210                         row.connection_id
    211                     ))
    212                 })?;
    213             state.connections[index].relays.push(
    214                 RelayUrl::parse(row.relay_url.as_str())
    215                     .map_err(|error| RadrootsNostrSignerError::Store(error.to_string()))?,
    216             );
    217         }
    218 
    219         let auth_rows: Vec<SignerConnectionAuthChallengeRow> = query_rows(
    220             self.db.as_ref(),
    221             "SELECT connection_id, auth_url, required_at_unix, authorized_at_unix FROM signer_connection_auth_challenge",
    222         )?;
    223         for row in auth_rows {
    224             let index = *connection_indexes
    225                 .get(row.connection_id.as_str())
    226                 .ok_or_else(|| {
    227                     RadrootsNostrSignerError::Store(format!(
    228                         "auth challenge row references missing connection `{}`",
    229                         row.connection_id
    230                     ))
    231                 })?;
    232             state.connections[index].auth_challenge = Some(
    233                 RadrootsNostrSignerAuthChallenge::new(row.auth_url.as_str(), row.required_at_unix)
    234                     .map(|mut challenge| {
    235                         challenge.authorized_at_unix = row.authorized_at_unix;
    236                         challenge
    237                     })?,
    238             );
    239         }
    240 
    241         let pending_rows: Vec<SignerConnectionPendingRequestRow> = query_rows(
    242             self.db.as_ref(),
    243             "SELECT connection_id, request_message_json, created_at_unix FROM signer_connection_pending_request",
    244         )?;
    245         for row in pending_rows {
    246             let index = *connection_indexes
    247                 .get(row.connection_id.as_str())
    248                 .ok_or_else(|| {
    249                     RadrootsNostrSignerError::Store(format!(
    250                         "pending request row references missing connection `{}`",
    251                         row.connection_id
    252                     ))
    253                 })?;
    254             let request_message = parse_json_field::<RadrootsNostrConnectRequestMessage>(
    255                 row.request_message_json.as_str(),
    256             )?;
    257             state.connections[index].pending_request = Some(
    258                 RadrootsNostrSignerPendingRequest::new(request_message, row.created_at_unix)?,
    259             );
    260         }
    261 
    262         let audit_rows: Vec<SignerRequestAuditRow> = query_rows(
    263             self.db.as_ref(),
    264             "SELECT request_id, connection_id, method, decision, message, created_at_unix FROM signer_request_audit ORDER BY created_at_unix, request_id",
    265         )?;
    266         state.audit_records = audit_rows
    267             .into_iter()
    268             .map(SignerRequestAuditRow::into_record)
    269             .collect::<Result<Vec<_>, _>>()?;
    270 
    271         let workflow_rows: Vec<SignerPublishWorkflowRow> = query_rows(
    272             self.db.as_ref(),
    273             "SELECT workflow_id, connection_id, kind, state, pending_request_json, authorized_at_unix, created_at_unix, updated_at_unix FROM signer_publish_workflow ORDER BY created_at_unix, workflow_id",
    274         )?;
    275         state.publish_workflows = workflow_rows
    276             .into_iter()
    277             .map(SignerPublishWorkflowRow::into_record)
    278             .collect::<Result<Vec<_>, _>>()?;
    279 
    280         Ok(state)
    281     }
    282 
    283     fn save(&self, state: &RadrootsNostrSignerStoreState) -> Result<(), RadrootsNostrSignerError> {
    284         let executor = self.db.executor();
    285         executor.begin()?;
    286         let result = (|| -> Result<(), RadrootsNostrSignerError> {
    287             exec_json(executor, "DELETE FROM signer_publish_workflow", json!([]))?;
    288             exec_json(executor, "DELETE FROM signer_request_audit", json!([]))?;
    289             exec_json(executor, "DELETE FROM signer_connection", json!([]))?;
    290 
    291             exec_json(
    292                 executor,
    293                 "INSERT INTO signer_store_metadata(singleton_id, store_version, signer_identity_id, signer_identity_public_key_hex, signer_identity_json, updated_at) VALUES(1, ?, ?, ?, ?, datetime('now')) ON CONFLICT(singleton_id) DO UPDATE SET store_version = excluded.store_version, signer_identity_id = excluded.signer_identity_id, signer_identity_public_key_hex = excluded.signer_identity_public_key_hex, signer_identity_json = excluded.signer_identity_json, updated_at = excluded.updated_at",
    294                 json!([
    295                     i64::from(state.version),
    296                     state
    297                         .signer_identity
    298                         .as_ref()
    299                         .map(|identity| identity.id.to_string()),
    300                     state
    301                         .signer_identity
    302                         .as_ref()
    303                         .map(|identity| identity.public_key_hex.clone()),
    304                     state
    305                         .signer_identity
    306                         .as_ref()
    307                         .map(serde_json::to_string)
    308                         .transpose()?,
    309                 ]),
    310             )?;
    311 
    312             for connection in &state.connections {
    313                 exec_json(
    314                     executor,
    315                     "INSERT INTO signer_connection(connection_id, client_public_key_hex, signer_identity_id, signer_identity_public_key_hex, signer_identity_json, user_identity_id, user_identity_public_key_hex, user_identity_json, connect_secret_hash_algorithm, connect_secret_hash_digest_hex, connect_secret_consumed_at_unix, requested_permissions_json, approval_requirement, approval_state, auth_state, status, status_reason, created_at_unix, updated_at_unix, last_authenticated_at_unix, last_request_at_unix) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
    316                     json!([
    317                         connection.connection_id.as_str(),
    318                         connection.client_public_key.to_hex(),
    319                         connection.signer_identity.id.to_string(),
    320                         connection.signer_identity.public_key_hex.clone(),
    321                         serde_json::to_string(&connection.signer_identity)?,
    322                         connection.user_identity.id.to_string(),
    323                         connection.user_identity.public_key_hex.clone(),
    324                         serde_json::to_string(&connection.user_identity)?,
    325                         connection
    326                             .connect_secret_hash
    327                             .as_ref()
    328                             .map(|hash| secret_digest_algorithm_label(hash)),
    329                         connection
    330                             .connect_secret_hash
    331                             .as_ref()
    332                             .map(|hash| hash.digest_hex.clone()),
    333                         connection.connect_secret_consumed_at_unix,
    334                         serde_json::to_string(&connection.requested_permissions)?,
    335                         approval_requirement_label(connection.approval_requirement),
    336                         approval_state_label(connection.approval_state),
    337                         auth_state_label(connection.auth_state),
    338                         connection_status_label(connection.status),
    339                         connection.status_reason.clone(),
    340                         connection.created_at_unix,
    341                         connection.updated_at_unix,
    342                         connection.last_authenticated_at_unix,
    343                         connection.last_request_at_unix,
    344                     ]),
    345                 )?;
    346 
    347                 for grant in &connection.granted_permissions {
    348                     exec_json(
    349                         executor,
    350                         "INSERT INTO signer_connection_permission_grant(connection_id, permission, granted_at_unix) VALUES(?, ?, ?)",
    351                         json!([
    352                             connection.connection_id.as_str(),
    353                             grant.permission.to_string(),
    354                             grant.granted_at_unix,
    355                         ]),
    356                     )?;
    357                 }
    358 
    359                 for (ordinal, relay) in connection.relays.iter().enumerate() {
    360                     exec_json(
    361                         executor,
    362                         "INSERT INTO signer_connection_relay(connection_id, ordinal, relay_url) VALUES(?, ?, ?)",
    363                         json!([
    364                             connection.connection_id.as_str(),
    365                             i64::try_from(ordinal).map_err(|_| {
    366                                 RadrootsNostrSignerError::Store(format!(
    367                                     "relay ordinal for connection `{}` is out of range",
    368                                     connection.connection_id
    369                                 ))
    370                             })?,
    371                             relay.as_str(),
    372                         ]),
    373                     )?;
    374                 }
    375 
    376                 if let Some(challenge) = connection.auth_challenge.as_ref() {
    377                     exec_json(
    378                         executor,
    379                         "INSERT INTO signer_connection_auth_challenge(connection_id, auth_url, required_at_unix, authorized_at_unix) VALUES(?, ?, ?, ?)",
    380                         json!([
    381                             connection.connection_id.as_str(),
    382                             challenge.auth_url,
    383                             challenge.required_at_unix,
    384                             challenge.authorized_at_unix,
    385                         ]),
    386                     )?;
    387                 }
    388 
    389                 if let Some(pending_request) = connection.pending_request.as_ref() {
    390                     exec_json(
    391                         executor,
    392                         "INSERT INTO signer_connection_pending_request(connection_id, request_message_json, created_at_unix) VALUES(?, ?, ?)",
    393                         json!([
    394                             connection.connection_id.as_str(),
    395                             serde_json::to_string(&pending_request.request_message)?,
    396                             pending_request.created_at_unix,
    397                         ]),
    398                     )?;
    399                 }
    400             }
    401 
    402             for audit in &state.audit_records {
    403                 exec_json(
    404                     executor,
    405                     "INSERT INTO signer_request_audit(request_id, connection_id, method, decision, message, created_at_unix) VALUES(?, ?, ?, ?, ?, ?)",
    406                     json!([
    407                         audit.request_id.as_str(),
    408                         audit.connection_id.as_str(),
    409                         audit.method.to_string(),
    410                         request_decision_label(audit.decision),
    411                         audit.message.clone(),
    412                         audit.created_at_unix,
    413                     ]),
    414                 )?;
    415             }
    416 
    417             for workflow in &state.publish_workflows {
    418                 exec_json(
    419                     executor,
    420                     "INSERT INTO signer_publish_workflow(workflow_id, connection_id, kind, state, pending_request_json, authorized_at_unix, created_at_unix, updated_at_unix) VALUES(?, ?, ?, ?, ?, ?, ?, ?)",
    421                     json!([
    422                         workflow.workflow_id.as_str(),
    423                         workflow.connection_id.as_str(),
    424                         publish_workflow_kind_label(workflow.kind),
    425                         publish_workflow_state_label(workflow.state),
    426                         workflow
    427                             .pending_request
    428                             .as_ref()
    429                             .map(serde_json::to_string)
    430                             .transpose()?,
    431                         workflow.authorized_at_unix,
    432                         workflow.created_at_unix,
    433                         workflow.updated_at_unix,
    434                     ]),
    435                 )?;
    436             }
    437 
    438             Ok(())
    439         })();
    440 
    441         match result {
    442             Ok(()) => {
    443                 executor.commit()?;
    444                 Ok(())
    445             }
    446             Err(error) => {
    447                 let _ = executor.rollback();
    448                 Err(error)
    449             }
    450         }
    451     }
    452 }
    453 
    454 #[cfg(feature = "native")]
    455 #[derive(Debug, Deserialize)]
    456 struct SignerStoreMetadataRow {
    457     store_version: i64,
    458     signer_identity_json: Option<String>,
    459 }
    460 
    461 #[cfg(feature = "native")]
    462 #[derive(Debug, Deserialize)]
    463 struct SignerConnectionRow {
    464     connection_id: String,
    465     client_public_key_hex: String,
    466     signer_identity_json: String,
    467     user_identity_json: String,
    468     connect_secret_hash_algorithm: Option<String>,
    469     connect_secret_hash_digest_hex: Option<String>,
    470     connect_secret_consumed_at_unix: Option<u64>,
    471     requested_permissions_json: String,
    472     approval_requirement: String,
    473     approval_state: String,
    474     auth_state: String,
    475     status: String,
    476     status_reason: Option<String>,
    477     created_at_unix: u64,
    478     updated_at_unix: u64,
    479     last_authenticated_at_unix: Option<u64>,
    480     last_request_at_unix: Option<u64>,
    481 }
    482 
    483 #[cfg(feature = "native")]
    484 impl SignerConnectionRow {
    485     fn into_record(self) -> Result<RadrootsNostrSignerConnectionRecord, RadrootsNostrSignerError> {
    486         Ok(RadrootsNostrSignerConnectionRecord {
    487             connection_id: self.connection_id.parse()?,
    488             client_public_key: parse_public_key_hex(self.client_public_key_hex.as_str())?,
    489             signer_identity: parse_json_field(self.signer_identity_json.as_str())?,
    490             user_identity: parse_json_field(self.user_identity_json.as_str())?,
    491             connect_secret_hash: match (
    492                 self.connect_secret_hash_algorithm.as_deref(),
    493                 self.connect_secret_hash_digest_hex,
    494             ) {
    495                 (None, None) => None,
    496                 (Some(algorithm), Some(digest_hex)) => Some(RadrootsNostrSignerConnectSecretHash {
    497                     algorithm: parse_secret_digest_algorithm(algorithm)?,
    498                     digest_hex,
    499                 }),
    500                 _ => {
    501                     return Err(RadrootsNostrSignerError::Store(
    502                         "sqlite connection secret hash columns are inconsistent".into(),
    503                     ));
    504                 }
    505             },
    506             connect_secret_consumed_at_unix: self.connect_secret_consumed_at_unix,
    507             requested_permissions: parse_json_field(self.requested_permissions_json.as_str())?,
    508             granted_permissions: Vec::new(),
    509             relays: Vec::new(),
    510             approval_requirement: parse_approval_requirement(self.approval_requirement.as_str())?,
    511             approval_state: parse_approval_state(self.approval_state.as_str())?,
    512             auth_state: parse_auth_state(self.auth_state.as_str())?,
    513             auth_challenge: None,
    514             pending_request: None,
    515             status: parse_connection_status(self.status.as_str())?,
    516             status_reason: self.status_reason,
    517             created_at_unix: self.created_at_unix,
    518             updated_at_unix: self.updated_at_unix,
    519             last_authenticated_at_unix: self.last_authenticated_at_unix,
    520             last_request_at_unix: self.last_request_at_unix,
    521         })
    522     }
    523 }
    524 
    525 #[cfg(feature = "native")]
    526 #[derive(Debug, Deserialize)]
    527 struct SignerConnectionPermissionGrantRow {
    528     connection_id: String,
    529     permission: String,
    530     granted_at_unix: u64,
    531 }
    532 
    533 #[cfg(feature = "native")]
    534 impl SignerConnectionPermissionGrantRow {
    535     fn into_grant(self) -> Result<RadrootsNostrSignerPermissionGrant, RadrootsNostrSignerError> {
    536         Ok(RadrootsNostrSignerPermissionGrant {
    537             permission: self
    538                 .permission
    539                 .parse::<RadrootsNostrConnectPermission>()
    540                 .map_err(|error| RadrootsNostrSignerError::Store(error.to_string()))?,
    541             granted_at_unix: self.granted_at_unix,
    542         })
    543     }
    544 }
    545 
    546 #[cfg(feature = "native")]
    547 #[derive(Debug, Deserialize)]
    548 struct SignerConnectionRelayRow {
    549     connection_id: String,
    550     #[allow(dead_code)]
    551     ordinal: i64,
    552     relay_url: String,
    553 }
    554 
    555 #[cfg(feature = "native")]
    556 #[derive(Debug, Deserialize)]
    557 struct SignerConnectionAuthChallengeRow {
    558     connection_id: String,
    559     auth_url: String,
    560     required_at_unix: u64,
    561     authorized_at_unix: Option<u64>,
    562 }
    563 
    564 #[cfg(feature = "native")]
    565 #[derive(Debug, Deserialize)]
    566 struct SignerConnectionPendingRequestRow {
    567     connection_id: String,
    568     request_message_json: String,
    569     created_at_unix: u64,
    570 }
    571 
    572 #[cfg(feature = "native")]
    573 #[derive(Debug, Deserialize)]
    574 struct SignerRequestAuditRow {
    575     request_id: String,
    576     connection_id: String,
    577     method: String,
    578     decision: String,
    579     message: Option<String>,
    580     created_at_unix: u64,
    581 }
    582 
    583 #[cfg(feature = "native")]
    584 impl SignerRequestAuditRow {
    585     fn into_record(
    586         self,
    587     ) -> Result<RadrootsNostrSignerRequestAuditRecord, RadrootsNostrSignerError> {
    588         Ok(RadrootsNostrSignerRequestAuditRecord {
    589             request_id: self.request_id.parse()?,
    590             connection_id: self.connection_id.parse()?,
    591             method: self
    592                 .method
    593                 .parse::<RadrootsNostrConnectMethod>()
    594                 .map_err(|error| RadrootsNostrSignerError::Store(error.to_string()))?,
    595             decision: parse_request_decision(self.decision.as_str())?,
    596             message: self.message,
    597             created_at_unix: self.created_at_unix,
    598         })
    599     }
    600 }
    601 
    602 #[cfg(feature = "native")]
    603 #[derive(Debug, Deserialize)]
    604 struct SignerPublishWorkflowRow {
    605     workflow_id: String,
    606     connection_id: String,
    607     kind: String,
    608     state: String,
    609     pending_request_json: Option<String>,
    610     authorized_at_unix: Option<u64>,
    611     created_at_unix: u64,
    612     updated_at_unix: u64,
    613 }
    614 
    615 #[cfg(feature = "native")]
    616 impl SignerPublishWorkflowRow {
    617     fn into_record(
    618         self,
    619     ) -> Result<RadrootsNostrSignerPublishWorkflowRecord, RadrootsNostrSignerError> {
    620         Ok(RadrootsNostrSignerPublishWorkflowRecord {
    621             workflow_id: self.workflow_id.parse()?,
    622             connection_id: self.connection_id.parse()?,
    623             kind: parse_publish_workflow_kind(self.kind.as_str())?,
    624             state: parse_publish_workflow_state(self.state.as_str())?,
    625             pending_request: self
    626                 .pending_request_json
    627                 .as_deref()
    628                 .map(parse_json_field::<RadrootsNostrSignerPendingRequest>)
    629                 .transpose()?,
    630             authorized_at_unix: self.authorized_at_unix,
    631             created_at_unix: self.created_at_unix,
    632             updated_at_unix: self.updated_at_unix,
    633         })
    634     }
    635 }
    636 
    637 #[cfg(feature = "native")]
    638 fn query_rows<T: DeserializeOwned>(
    639     db: &RadrootsNostrSignerSqliteDb,
    640     sql: &str,
    641 ) -> Result<Vec<T>, RadrootsNostrSignerError> {
    642     let raw = db.executor().query_raw(sql, "[]")?;
    643     serde_json::from_str(&raw).map_err(|error| RadrootsNostrSignerError::Store(error.to_string()))
    644 }
    645 
    646 #[cfg(feature = "native")]
    647 fn exec_json(
    648     executor: &impl radroots_sql_core::SqlExecutor,
    649     sql: &str,
    650     params: Value,
    651 ) -> Result<(), RadrootsNostrSignerError> {
    652     let _ = executor.exec(sql, params.to_string().as_str())?;
    653     Ok(())
    654 }
    655 
    656 #[cfg(feature = "native")]
    657 fn parse_json_field<T: DeserializeOwned>(value: &str) -> Result<T, RadrootsNostrSignerError> {
    658     serde_json::from_str(value).map_err(|error| RadrootsNostrSignerError::Store(error.to_string()))
    659 }
    660 
    661 #[cfg(feature = "native")]
    662 fn parse_public_key_hex(value: &str) -> Result<nostr::PublicKey, RadrootsNostrSignerError> {
    663     nostr::PublicKey::parse(value)
    664         .or_else(|_| nostr::PublicKey::from_hex(value))
    665         .map_err(|error| RadrootsNostrSignerError::Store(error.to_string()))
    666 }
    667 
    668 #[cfg(feature = "native")]
    669 fn approval_requirement_label(value: RadrootsNostrSignerApprovalRequirement) -> &'static str {
    670     match value {
    671         RadrootsNostrSignerApprovalRequirement::NotRequired => "not_required",
    672         RadrootsNostrSignerApprovalRequirement::ExplicitUser => "explicit_user",
    673     }
    674 }
    675 
    676 #[cfg(feature = "native")]
    677 fn parse_approval_requirement(
    678     value: &str,
    679 ) -> Result<RadrootsNostrSignerApprovalRequirement, RadrootsNostrSignerError> {
    680     match value {
    681         "not_required" => Ok(RadrootsNostrSignerApprovalRequirement::NotRequired),
    682         "explicit_user" => Ok(RadrootsNostrSignerApprovalRequirement::ExplicitUser),
    683         other => Err(RadrootsNostrSignerError::Store(format!(
    684             "unknown sqlite approval requirement `{other}`"
    685         ))),
    686     }
    687 }
    688 
    689 #[cfg(feature = "native")]
    690 fn approval_state_label(value: RadrootsNostrSignerApprovalState) -> &'static str {
    691     match value {
    692         RadrootsNostrSignerApprovalState::NotRequired => "not_required",
    693         RadrootsNostrSignerApprovalState::Pending => "pending",
    694         RadrootsNostrSignerApprovalState::Approved => "approved",
    695         RadrootsNostrSignerApprovalState::Rejected => "rejected",
    696     }
    697 }
    698 
    699 #[cfg(feature = "native")]
    700 fn parse_approval_state(
    701     value: &str,
    702 ) -> Result<RadrootsNostrSignerApprovalState, RadrootsNostrSignerError> {
    703     match value {
    704         "not_required" => Ok(RadrootsNostrSignerApprovalState::NotRequired),
    705         "pending" => Ok(RadrootsNostrSignerApprovalState::Pending),
    706         "approved" => Ok(RadrootsNostrSignerApprovalState::Approved),
    707         "rejected" => Ok(RadrootsNostrSignerApprovalState::Rejected),
    708         other => Err(RadrootsNostrSignerError::Store(format!(
    709             "unknown sqlite approval state `{other}`"
    710         ))),
    711     }
    712 }
    713 
    714 #[cfg(feature = "native")]
    715 fn auth_state_label(value: RadrootsNostrSignerAuthState) -> &'static str {
    716     match value {
    717         RadrootsNostrSignerAuthState::NotRequired => "not_required",
    718         RadrootsNostrSignerAuthState::Pending => "pending",
    719         RadrootsNostrSignerAuthState::Authorized => "authorized",
    720     }
    721 }
    722 
    723 #[cfg(feature = "native")]
    724 fn parse_auth_state(value: &str) -> Result<RadrootsNostrSignerAuthState, RadrootsNostrSignerError> {
    725     match value {
    726         "not_required" => Ok(RadrootsNostrSignerAuthState::NotRequired),
    727         "pending" => Ok(RadrootsNostrSignerAuthState::Pending),
    728         "authorized" => Ok(RadrootsNostrSignerAuthState::Authorized),
    729         other => Err(RadrootsNostrSignerError::Store(format!(
    730             "unknown sqlite auth state `{other}`"
    731         ))),
    732     }
    733 }
    734 
    735 #[cfg(feature = "native")]
    736 fn connection_status_label(value: RadrootsNostrSignerConnectionStatus) -> &'static str {
    737     match value {
    738         RadrootsNostrSignerConnectionStatus::Pending => "pending",
    739         RadrootsNostrSignerConnectionStatus::Active => "active",
    740         RadrootsNostrSignerConnectionStatus::Rejected => "rejected",
    741         RadrootsNostrSignerConnectionStatus::Revoked => "revoked",
    742     }
    743 }
    744 
    745 #[cfg(feature = "native")]
    746 fn parse_connection_status(
    747     value: &str,
    748 ) -> Result<RadrootsNostrSignerConnectionStatus, RadrootsNostrSignerError> {
    749     match value {
    750         "pending" => Ok(RadrootsNostrSignerConnectionStatus::Pending),
    751         "active" => Ok(RadrootsNostrSignerConnectionStatus::Active),
    752         "rejected" => Ok(RadrootsNostrSignerConnectionStatus::Rejected),
    753         "revoked" => Ok(RadrootsNostrSignerConnectionStatus::Revoked),
    754         other => Err(RadrootsNostrSignerError::Store(format!(
    755             "unknown sqlite connection status `{other}`"
    756         ))),
    757     }
    758 }
    759 
    760 #[cfg(feature = "native")]
    761 fn request_decision_label(value: RadrootsNostrSignerRequestDecision) -> &'static str {
    762     match value {
    763         RadrootsNostrSignerRequestDecision::Allowed => "allowed",
    764         RadrootsNostrSignerRequestDecision::Denied => "denied",
    765         RadrootsNostrSignerRequestDecision::Challenged => "challenged",
    766     }
    767 }
    768 
    769 #[cfg(feature = "native")]
    770 fn parse_request_decision(
    771     value: &str,
    772 ) -> Result<RadrootsNostrSignerRequestDecision, RadrootsNostrSignerError> {
    773     match value {
    774         "allowed" => Ok(RadrootsNostrSignerRequestDecision::Allowed),
    775         "denied" => Ok(RadrootsNostrSignerRequestDecision::Denied),
    776         "challenged" => Ok(RadrootsNostrSignerRequestDecision::Challenged),
    777         other => Err(RadrootsNostrSignerError::Store(format!(
    778             "unknown sqlite request decision `{other}`"
    779         ))),
    780     }
    781 }
    782 
    783 #[cfg(feature = "native")]
    784 fn publish_workflow_kind_label(value: RadrootsNostrSignerPublishWorkflowKind) -> &'static str {
    785     match value {
    786         RadrootsNostrSignerPublishWorkflowKind::ConnectSecretFinalization => {
    787             "connect_secret_finalization"
    788         }
    789         RadrootsNostrSignerPublishWorkflowKind::AuthReplayFinalization => {
    790             "auth_replay_finalization"
    791         }
    792     }
    793 }
    794 
    795 #[cfg(feature = "native")]
    796 fn parse_publish_workflow_kind(
    797     value: &str,
    798 ) -> Result<RadrootsNostrSignerPublishWorkflowKind, RadrootsNostrSignerError> {
    799     match value {
    800         "connect_secret_finalization" => {
    801             Ok(RadrootsNostrSignerPublishWorkflowKind::ConnectSecretFinalization)
    802         }
    803         "auth_replay_finalization" => {
    804             Ok(RadrootsNostrSignerPublishWorkflowKind::AuthReplayFinalization)
    805         }
    806         other => Err(RadrootsNostrSignerError::Store(format!(
    807             "unknown sqlite publish workflow kind `{other}`"
    808         ))),
    809     }
    810 }
    811 
    812 #[cfg(feature = "native")]
    813 fn publish_workflow_state_label(value: RadrootsNostrSignerPublishWorkflowState) -> &'static str {
    814     match value {
    815         RadrootsNostrSignerPublishWorkflowState::PendingPublish => "pending_publish",
    816         RadrootsNostrSignerPublishWorkflowState::PublishedPendingFinalize => {
    817             "published_pending_finalize"
    818         }
    819     }
    820 }
    821 
    822 #[cfg(feature = "native")]
    823 fn parse_publish_workflow_state(
    824     value: &str,
    825 ) -> Result<RadrootsNostrSignerPublishWorkflowState, RadrootsNostrSignerError> {
    826     match value {
    827         "pending_publish" => Ok(RadrootsNostrSignerPublishWorkflowState::PendingPublish),
    828         "published_pending_finalize" => {
    829             Ok(RadrootsNostrSignerPublishWorkflowState::PublishedPendingFinalize)
    830         }
    831         other => Err(RadrootsNostrSignerError::Store(format!(
    832             "unknown sqlite publish workflow state `{other}`"
    833         ))),
    834     }
    835 }
    836 
    837 #[cfg(feature = "native")]
    838 fn secret_digest_algorithm_label(hash: &RadrootsNostrSignerConnectSecretHash) -> &'static str {
    839     match hash.algorithm {
    840         crate::model::RadrootsNostrSignerSecretDigestAlgorithm::Sha256 => "sha256",
    841     }
    842 }
    843 
    844 #[cfg(feature = "native")]
    845 fn parse_secret_digest_algorithm(
    846     value: &str,
    847 ) -> Result<crate::model::RadrootsNostrSignerSecretDigestAlgorithm, RadrootsNostrSignerError> {
    848     match value {
    849         "sha256" => Ok(crate::model::RadrootsNostrSignerSecretDigestAlgorithm::Sha256),
    850         other => Err(RadrootsNostrSignerError::Store(format!(
    851             "unknown sqlite secret digest algorithm `{other}`"
    852         ))),
    853     }
    854 }
    855 
    856 #[cfg(test)]
    857 mod tests {
    858     use super::*;
    859     #[cfg(feature = "native")]
    860     use crate::model::{
    861         RadrootsNostrSignerApprovalRequirement, RadrootsNostrSignerAuthChallenge,
    862         RadrootsNostrSignerAuthState, RadrootsNostrSignerConnectionDraft,
    863         RadrootsNostrSignerConnectionId, RadrootsNostrSignerPendingRequest,
    864         RadrootsNostrSignerPermissionGrant, RadrootsNostrSignerPublishWorkflowRecord,
    865         RadrootsNostrSignerRequestAuditRecord, RadrootsNostrSignerRequestDecision,
    866         RadrootsNostrSignerRequestId,
    867     };
    868     #[cfg(feature = "native")]
    869     use crate::test_support::{
    870         api_primary_https, fixture_alice_identity, fixture_bob_identity, fixture_carol_public_key,
    871         primary_relay, secondary_relay,
    872     };
    873     #[cfg(feature = "native")]
    874     use radroots_nostr_connect::prelude::{
    875         RadrootsNostrConnectMethod, RadrootsNostrConnectPermission, RadrootsNostrConnectRequest,
    876         RadrootsNostrConnectRequestMessage,
    877     };
    878     use std::thread;
    879 
    880     #[test]
    881     fn file_store_round_trip_and_path_accessor() {
    882         let temp = tempfile::tempdir().expect("tempdir");
    883         let path = temp.path().join("signer.json");
    884         let store = RadrootsNostrFileSignerStore::new(path.as_path());
    885 
    886         assert_eq!(store.path(), path.as_path());
    887         store
    888             .save(&RadrootsNostrSignerStoreState::default())
    889             .expect("save");
    890         let loaded = store.load().expect("load");
    891         assert_eq!(
    892             loaded.version,
    893             RadrootsNostrSignerStoreState::default().version
    894         );
    895         assert!(loaded.connections.is_empty());
    896     }
    897 
    898     #[test]
    899     fn file_store_load_missing_and_reports_parse_errors() {
    900         let temp = tempfile::tempdir().expect("tempdir");
    901         let missing = RadrootsNostrFileSignerStore::new(temp.path().join("missing.json"));
    902         let loaded = missing.load().expect("missing load");
    903         assert!(loaded.connections.is_empty());
    904 
    905         let path = temp.path().join("invalid.json");
    906         std::fs::write(&path, "{").expect("write invalid json");
    907         let store = RadrootsNostrFileSignerStore::new(path.as_path());
    908         let err = store.load().expect_err("invalid json");
    909         assert!(err.to_string().starts_with("store error:"));
    910     }
    911 
    912     #[test]
    913     fn file_store_save_reports_parse_error() {
    914         let temp = tempfile::tempdir().expect("tempdir");
    915         let path = temp.path().join("invalid-save.json");
    916         std::fs::write(&path, "{").expect("write invalid json");
    917         let store = RadrootsNostrFileSignerStore::new(path.as_path());
    918         let err = store
    919             .save(&RadrootsNostrSignerStoreState::default())
    920             .expect_err("invalid save");
    921         assert!(err.to_string().starts_with("store error:"));
    922     }
    923 
    924     #[cfg(unix)]
    925     #[test]
    926     fn file_store_save_reports_write_error() {
    927         use std::os::unix::fs::PermissionsExt;
    928 
    929         let temp = tempfile::tempdir().expect("tempdir");
    930         let path = temp.path().join("signer.json");
    931         let json =
    932             serde_json::to_string(&RadrootsNostrSignerStoreState::default()).expect("serialize");
    933         std::fs::write(&path, json).expect("write json");
    934         let store = RadrootsNostrFileSignerStore::new(path.as_path());
    935 
    936         let mut perms = std::fs::metadata(temp.path())
    937             .expect("dir metadata")
    938             .permissions();
    939         perms.set_mode(0o500);
    940         std::fs::set_permissions(temp.path(), perms).expect("set perms");
    941 
    942         let err = store
    943             .save(&RadrootsNostrSignerStoreState::default())
    944             .expect_err("read-only save");
    945         assert!(err.to_string().starts_with("store error:"));
    946 
    947         let mut perms = std::fs::metadata(temp.path())
    948             .expect("dir metadata")
    949             .permissions();
    950         perms.set_mode(0o700);
    951         std::fs::set_permissions(temp.path(), perms).expect("restore perms");
    952     }
    953 
    954     #[test]
    955     fn memory_store_round_trip_and_poison_errors() {
    956         let store = RadrootsNostrMemorySignerStore::new();
    957         let state = RadrootsNostrSignerStoreState::default();
    958         store.save(&state).expect("save");
    959         let loaded = store.load().expect("load");
    960         assert_eq!(loaded.version, state.version);
    961 
    962         let shared = store.state.clone();
    963         let _ = thread::spawn(move || {
    964             let _guard = shared.write().expect("write");
    965             panic!("poison memory store");
    966         })
    967         .join();
    968 
    969         let load = store.load().expect_err("poisoned load");
    970         let save = store.save(&state).expect_err("poisoned save");
    971         assert!(load.to_string().contains("memory store lock poisoned"));
    972         assert!(save.to_string().contains("memory store lock poisoned"));
    973     }
    974 
    975     #[cfg(feature = "native")]
    976     fn sample_request_message(id: &str) -> RadrootsNostrConnectRequestMessage {
    977         RadrootsNostrConnectRequestMessage::new(id, RadrootsNostrConnectRequest::Ping)
    978     }
    979 
    980     #[cfg(feature = "native")]
    981     fn sample_sqlite_state() -> RadrootsNostrSignerStoreState {
    982         let signer_identity = fixture_alice_identity();
    983         let user_identity = fixture_bob_identity();
    984         let connection_id = RadrootsNostrSignerConnectionId::parse("conn-sqlite").expect("id");
    985         let mut connection = RadrootsNostrSignerConnectionRecord::new(
    986             connection_id.clone(),
    987             signer_identity.clone(),
    988             RadrootsNostrSignerConnectionDraft::new(fixture_carol_public_key(), user_identity)
    989                 .with_connect_secret("sqlite-secret")
    990                 .with_relays(vec![primary_relay(), secondary_relay()])
    991                 .with_requested_permissions(
    992                     vec![
    993                         RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Ping),
    994                         RadrootsNostrConnectPermission::with_parameter(
    995                             RadrootsNostrConnectMethod::SignEvent,
    996                             "kind:1",
    997                         ),
    998                     ]
    999                     .into(),
   1000                 )
   1001                 .with_approval_requirement(RadrootsNostrSignerApprovalRequirement::ExplicitUser),
   1002             100,
   1003         );
   1004         connection.approval_state = crate::model::RadrootsNostrSignerApprovalState::Approved;
   1005         connection.auth_state = RadrootsNostrSignerAuthState::Pending;
   1006         connection.status = crate::model::RadrootsNostrSignerConnectionStatus::Active;
   1007         connection.status_reason = Some("approved by operator".to_owned());
   1008         connection.updated_at_unix = 140;
   1009         connection.last_authenticated_at_unix = Some(130);
   1010         connection.last_request_at_unix = Some(135);
   1011         connection.mark_connect_secret_consumed(125);
   1012         connection.granted_permissions = vec![
   1013             RadrootsNostrSignerPermissionGrant::new(
   1014                 RadrootsNostrConnectPermission::new(RadrootsNostrConnectMethod::Ping),
   1015                 110,
   1016             ),
   1017             RadrootsNostrSignerPermissionGrant::new(
   1018                 RadrootsNostrConnectPermission::with_parameter(
   1019                     RadrootsNostrConnectMethod::SignEvent,
   1020                     "kind:1",
   1021                 ),
   1022                 111,
   1023             ),
   1024         ];
   1025         connection.auth_challenge = Some(
   1026             RadrootsNostrSignerAuthChallenge::new(
   1027                 format!("{}/challenge", api_primary_https()).as_str(),
   1028                 120,
   1029             )
   1030             .expect("challenge"),
   1031         );
   1032         connection.pending_request = Some(
   1033             RadrootsNostrSignerPendingRequest::new(sample_request_message("req-sqlite"), 121)
   1034                 .expect("pending request"),
   1035         );
   1036 
   1037         RadrootsNostrSignerStoreState {
   1038             version: 1,
   1039             signer_identity: Some(signer_identity),
   1040             connections: vec![connection.clone()],
   1041             audit_records: vec![RadrootsNostrSignerRequestAuditRecord::new(
   1042                 RadrootsNostrSignerRequestId::parse("audit-1").expect("request id"),
   1043                 connection_id,
   1044                 RadrootsNostrConnectMethod::Ping,
   1045                 RadrootsNostrSignerRequestDecision::Allowed,
   1046                 Some("permitted".to_owned()),
   1047                 150,
   1048             )],
   1049             publish_workflows: vec![
   1050                 RadrootsNostrSignerPublishWorkflowRecord::new_connect_secret_finalization(
   1051                     connection.connection_id.clone(),
   1052                     151,
   1053                 ),
   1054                 RadrootsNostrSignerPublishWorkflowRecord::new_auth_replay_finalization(
   1055                     connection.connection_id.clone(),
   1056                     RadrootsNostrSignerPendingRequest::new(
   1057                         sample_request_message("req-replay"),
   1058                         152,
   1059                     )
   1060                     .expect("auth replay pending request"),
   1061                     153,
   1062                 ),
   1063             ],
   1064         }
   1065     }
   1066 
   1067     #[cfg(feature = "native")]
   1068     #[test]
   1069     fn sqlite_store_round_trip_on_memory_backend() {
   1070         let store = RadrootsNostrSqliteSignerStore::open_memory().expect("open memory store");
   1071         let state = sample_sqlite_state();
   1072 
   1073         store.save(&state).expect("save sqlite state");
   1074         let loaded = store.load().expect("load sqlite state");
   1075 
   1076         assert_eq!(
   1077             serde_json::to_value(&loaded).expect("serialize loaded"),
   1078             serde_json::to_value(&state).expect("serialize state")
   1079         );
   1080     }
   1081 
   1082     #[cfg(feature = "native")]
   1083     #[test]
   1084     fn sqlite_store_persists_to_disk_and_recovers_after_reopen() {
   1085         let temp = tempfile::tempdir().expect("tempdir");
   1086         let path = temp.path().join("signer.sqlite");
   1087         let state = sample_sqlite_state();
   1088 
   1089         let store = RadrootsNostrSqliteSignerStore::open(&path).expect("open sqlite store");
   1090         store.save(&state).expect("save sqlite state");
   1091 
   1092         let reopened = RadrootsNostrSqliteSignerStore::open(&path).expect("reopen sqlite store");
   1093         let loaded = reopened.load().expect("load reopened sqlite state");
   1094 
   1095         assert_eq!(
   1096             serde_json::to_value(&loaded).expect("serialize loaded"),
   1097             serde_json::to_value(&state).expect("serialize state")
   1098         );
   1099     }
   1100 }