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 }