core.rs (55819B)
1 use std::path::PathBuf; 2 3 use serde::Serialize; 4 use serde_json::{Value, json}; 5 6 use crate::cli::global::LocalExportFormatArg; 7 use crate::ops::{ 8 AccountAttachSecretRequest, AccountAttachSecretResult, AccountCreateRequest, 9 AccountCreateResult, AccountGetRequest, AccountGetResult, AccountImportRequest, 10 AccountImportResult, AccountListRequest, AccountListResult, AccountRemoveRequest, 11 AccountRemoveResult, AccountSelectionClearRequest, AccountSelectionClearResult, 12 AccountSelectionGetRequest, AccountSelectionGetResult, AccountSelectionUpdateRequest, 13 AccountSelectionUpdateResult, ConfigGetRequest, ConfigGetResult, HealthCheckRunRequest, 14 HealthCheckRunResult, HealthStatusGetRequest, HealthStatusGetResult, OperationAdapterError, 15 OperationRequest, OperationRequestData, OperationRequestPayload, OperationResult, 16 OperationResultData, OperationService, StoreBackupCreateRequest, StoreBackupCreateResult, 17 StoreBackupRestoreRequest, StoreBackupRestoreResult, StoreExportRequest, StoreExportResult, 18 StoreInitRequest, StoreInitResult, StoreStatusGetRequest, StoreStatusGetResult, 19 WorkspaceGetRequest, WorkspaceGetResult, WorkspaceInitRequest, WorkspaceInitResult, 20 }; 21 use crate::runtime::RuntimeError; 22 use crate::runtime::account::{ 23 AccountResolution, AccountRuntimeFailure, account_resolution_view, account_summary_view, 24 attach_identity_secret, clear_default_account, create_or_migrate_default_account, 25 import_public_identity, preview_account_removal, preview_identity_secret_attachment, 26 preview_public_identity_import, remove_account, resolve_account_resolution, 27 resolve_account_selector, secret_backend_status, select_account, snapshot, 28 unresolved_account_reason, 29 }; 30 use crate::runtime::config::{PublishTransport, RuntimeConfig, SignerBackend}; 31 use crate::runtime::logging::LoggingState; 32 use crate::runtime::sdk::CliSdkAdapterError; 33 use crate::runtime::signer::resolve_signer_status; 34 use crate::view::runtime::{ 35 CommandDisposition, LocalBackupView, LocalRestoreView, PublishProviderRuntimeView, 36 PublishRelayRuntimeView, PublishRuntimeView, 37 }; 38 39 pub struct CoreOperationService<'a> { 40 config: &'a RuntimeConfig, 41 logging: &'a LoggingState, 42 } 43 44 impl<'a> CoreOperationService<'a> { 45 pub fn new(config: &'a RuntimeConfig, logging: &'a LoggingState) -> Self { 46 Self { config, logging } 47 } 48 } 49 50 impl OperationService<WorkspaceInitRequest> for CoreOperationService<'_> { 51 type Result = WorkspaceInitResult; 52 53 fn execute( 54 &self, 55 request: OperationRequest<WorkspaceInitRequest>, 56 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 57 if request.context.dry_run { 58 let local = map_runtime(crate::runtime::store::init_preflight(self.config))?; 59 return json_operation_result::<WorkspaceInitResult>(json!({ 60 "state": local.state, 61 "profile": self.config.paths.profile, 62 "local": local, 63 })); 64 } 65 66 let local = map_runtime(crate::runtime::store::init(self.config))?; 67 json_operation_result::<WorkspaceInitResult>(json!({ 68 "state": local.state, 69 "profile": self.config.paths.profile, 70 "local": local, 71 })) 72 } 73 } 74 75 impl OperationService<WorkspaceGetRequest> for CoreOperationService<'_> { 76 type Result = WorkspaceGetResult; 77 78 fn execute( 79 &self, 80 _request: OperationRequest<WorkspaceGetRequest>, 81 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 82 json_operation_result::<WorkspaceGetResult>(json!({ 83 "profile": self.config.paths.profile, 84 "profile_source": self.config.paths.profile_source, 85 "root_source": self.config.paths.root_source, 86 "app_namespace": self.config.paths.app_namespace, 87 "workspace_config_path": self.config.paths.workspace_config_path.as_ref().map(|path| path.display().to_string()), 88 "app_config_path": self.config.paths.app_config_path.display().to_string(), 89 "app_data_root": self.config.paths.app_data_root.display().to_string(), 90 "app_logs_root": self.config.paths.app_logs_root.display().to_string(), 91 "local_root": self.config.local.root.display().to_string(), 92 "replica_db_path": self.config.local.replica_db_path.display().to_string(), 93 })) 94 } 95 } 96 97 impl OperationService<HealthStatusGetRequest> for CoreOperationService<'_> { 98 type Result = HealthStatusGetResult; 99 100 fn execute( 101 &self, 102 request: OperationRequest<HealthStatusGetRequest>, 103 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 104 let store = map_sdk_adapter( 105 request.operation_id(), 106 crate::runtime::store::status(self.config), 107 )?; 108 let account = map_runtime(resolve_account_resolution(self.config))?; 109 let publish = publish_runtime_view(self.config, true, &account); 110 let signer = signer_health_view(self.config, &account); 111 let state = health_status_state(&store.state, &publish); 112 let actions = health_actions(self.config, store.state.as_str(), &account, &publish); 113 json_operation_result::<HealthStatusGetResult>(json!({ 114 "state": state, 115 "store": store, 116 "account_resolution": account_resolution_view(&account), 117 "signer": signer, 118 "publish": publish, 119 "logging": { 120 "initialized": self.logging.initialized, 121 "current_file": self.logging.current_file.as_ref().map(|path| path.display().to_string()), 122 }, 123 "actions": actions, 124 })) 125 } 126 } 127 128 impl OperationService<HealthCheckRunRequest> for CoreOperationService<'_> { 129 type Result = HealthCheckRunResult; 130 131 fn execute( 132 &self, 133 request: OperationRequest<HealthCheckRunRequest>, 134 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 135 let store = map_sdk_adapter( 136 request.operation_id(), 137 crate::runtime::store::status(self.config), 138 )?; 139 let account = map_runtime(resolve_account_resolution(self.config))?; 140 let account_reason = if account.resolved_account.is_some() { 141 None 142 } else { 143 Some(map_runtime(unresolved_account_reason(self.config))?) 144 }; 145 let publish = publish_runtime_view(self.config, true, &account); 146 let signer = signer_health_view(self.config, &account); 147 let state = health_check_state(&store.state, account.resolved_account.is_some(), &publish); 148 let actions = health_actions(self.config, store.state.as_str(), &account, &publish); 149 json_operation_result::<HealthCheckRunResult>(json!({ 150 "state": state, 151 "account_resolution": account_resolution_view(&account), 152 "checks": { 153 "workspace": { 154 "state": "ready", 155 "profile": self.config.paths.profile, 156 }, 157 "store": { 158 "state": store.state, 159 "source": store.source, 160 "canonical_store": store.canonical_store, 161 "sdk_storage": store.sdk_storage, 162 "sdk_root": store.sdk_root, 163 "sdk_existed_before_open": store.sdk_existed_before_open, 164 "event_store": store.event_store, 165 "outbox": store.outbox, 166 "integrity": store.integrity, 167 "legacy_replica": store.legacy_replica, 168 "reason": store.reason, 169 }, 170 "account": { 171 "state": if account.resolved_account.is_some() { "ready" } else { "unconfigured" }, 172 "reason": account_reason, 173 }, 174 "signer": signer, 175 "publish": { 176 "state": publish.state, 177 "transport": publish.transport, 178 "executable": publish.executable, 179 "reason": publish.reason, 180 }, 181 }, 182 "actions": actions, 183 })) 184 } 185 } 186 187 impl OperationService<ConfigGetRequest> for CoreOperationService<'_> { 188 type Result = ConfigGetResult; 189 190 fn execute( 191 &self, 192 _request: OperationRequest<ConfigGetRequest>, 193 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 194 let account = map_runtime(resolve_account_resolution(self.config))?; 195 let publish = publish_runtime_view(self.config, true, &account); 196 let write_plane = 197 crate::runtime::provider::resolve_write_plane_provider(self.config, &publish); 198 let actions = config_actions(self.config, &account, &publish); 199 let mut result = json!({ 200 "output": { 201 "format": self.config.output.format.as_str(), 202 "verbosity": self.config.output.verbosity.as_str(), 203 "color": self.config.output.color, 204 "dry_run": self.config.output.dry_run, 205 }, 206 "interaction": { 207 "input_enabled": self.config.interaction.input_enabled, 208 "prompts_allowed": self.config.interaction.prompts_allowed, 209 "confirmations_allowed": self.config.interaction.confirmations_allowed, 210 }, 211 "paths": { 212 "profile": self.config.paths.profile, 213 "app_config_path": self.config.paths.app_config_path.display().to_string(), 214 "workspace_config_path": self.config.paths.workspace_config_path.as_ref().map(|path| path.display().to_string()), 215 "app_data_root": self.config.paths.app_data_root.display().to_string(), 216 "app_logs_root": self.config.paths.app_logs_root.display().to_string(), 217 }, 218 "account": { 219 "selector": self.config.account.selector, 220 "store_path": self.config.account.store_path.display().to_string(), 221 "secrets_dir": self.config.account.secrets_dir.display().to_string(), 222 }, 223 "account_resolution": account_resolution_view(&account), 224 "signer": { 225 "mode": self.config.signer.backend.as_str(), 226 }, 227 "publish": publish, 228 "relay": { 229 "count": self.config.relay.urls.len(), 230 "urls": self.config.relay.urls, 231 "source": self.config.relay.source.as_str(), 232 }, 233 "myc": { 234 "executable": self.config.myc.executable.display().to_string(), 235 "status_timeout_ms": self.config.myc.status_timeout_ms, 236 }, 237 "hyf": { 238 "enabled": self.config.hyf.enabled, 239 "executable": self.config.hyf.executable.display().to_string(), 240 }, 241 "write_plane": { 242 "provider_runtime_id": write_plane.provider_runtime_id, 243 "binding_model": write_plane.binding_model, 244 "state": write_plane.state, 245 "provenance": write_plane.provenance, 246 "source": write_plane.source, 247 "target_kind": write_plane.target_kind, 248 "target": write_plane.target, 249 "detail": write_plane.detail, 250 }, 251 "local": { 252 "root": self.config.local.root.display().to_string(), 253 "replica_db_path": self.config.local.replica_db_path.display().to_string(), 254 "backups_dir": self.config.local.backups_dir.display().to_string(), 255 "exports_dir": self.config.local.exports_dir.display().to_string(), 256 }, 257 "actions": actions, 258 }); 259 if matches!( 260 self.config.publish.transport, 261 PublishTransport::RadrootsdProxy 262 ) { 263 result["radrootsd_proxy"] = json!({ 264 "url": self.config.publish.radrootsd_proxy.url, 265 "token_file_configured": self.config.publish.radrootsd_proxy.token_file.is_some(), 266 "token_secret_id_configured": self.config.publish.radrootsd_proxy.token_secret_id.is_some(), 267 }); 268 } 269 json_operation_result::<ConfigGetResult>(result) 270 } 271 } 272 273 impl OperationService<AccountCreateRequest> for CoreOperationService<'_> { 274 type Result = AccountCreateResult; 275 276 fn execute( 277 &self, 278 request: OperationRequest<AccountCreateRequest>, 279 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 280 if request.context.dry_run { 281 let secret_backend = secret_backend_status(self.config); 282 if secret_backend.state != "ready" { 283 return Err(OperationAdapterError::OperationUnavailable { 284 operation_id: request.operation_id().to_owned(), 285 message: secret_backend 286 .reason 287 .unwrap_or_else(|| "account secret backend is not available".to_owned()), 288 }); 289 } 290 return json_operation_result::<AccountCreateResult>(json!({ 291 "state": "dry_run", 292 "store_path": self.config.account.store_path.display().to_string(), 293 "secrets_dir": self.config.account.secrets_dir.display().to_string(), 294 "secret_backend": { 295 "state": secret_backend.state, 296 "active_backend": secret_backend.active_backend, 297 "used_fallback": secret_backend.used_fallback, 298 }, 299 })); 300 } 301 302 let result = map_runtime(create_or_migrate_default_account(self.config))?; 303 json_operation_result::<AccountCreateResult>(json!({ 304 "state": match result.mode { 305 crate::runtime::account::AccountCreateMode::Created => "created", 306 crate::runtime::account::AccountCreateMode::Migrated => "migrated", 307 }, 308 "account": account_summary_view(&result.account), 309 })) 310 } 311 } 312 313 impl OperationService<AccountImportRequest> for CoreOperationService<'_> { 314 type Result = AccountImportResult; 315 316 fn execute( 317 &self, 318 request: OperationRequest<AccountImportRequest>, 319 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 320 let path = required_path(&request, "path")?; 321 let make_default = bool_input(&request, "default").unwrap_or(false); 322 if request.context.dry_run { 323 let account = map_expected_runtime( 324 request.operation_id(), 325 preview_public_identity_import(self.config, path.as_path(), make_default), 326 )?; 327 return json_operation_result::<AccountImportResult>(json!({ 328 "state": "dry_run", 329 "path": path.display().to_string(), 330 "default": make_default, 331 "account": account_summary_view(&account), 332 })); 333 } 334 if request.context.requires_approval_token() { 335 return Err(OperationAdapterError::approval_required( 336 request.operation_id(), 337 )); 338 } 339 340 let account = map_expected_runtime( 341 request.operation_id(), 342 import_public_identity(self.config, path.as_path(), make_default), 343 )?; 344 json_operation_result::<AccountImportResult>(json!({ 345 "state": "imported", 346 "account": account_summary_view(&account), 347 })) 348 } 349 } 350 351 impl OperationService<AccountAttachSecretRequest> for CoreOperationService<'_> { 352 type Result = AccountAttachSecretResult; 353 354 fn execute( 355 &self, 356 request: OperationRequest<AccountAttachSecretRequest>, 357 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 358 let selector = required_string(&request, "selector")?; 359 let path = required_path(&request, "path")?; 360 let make_default = bool_input(&request, "default").unwrap_or(false); 361 if request.context.dry_run { 362 let secret_backend = account_secret_backend_ready(request.operation_id(), self.config)?; 363 let account = map_expected_runtime( 364 request.operation_id(), 365 preview_identity_secret_attachment( 366 self.config, 367 selector.as_str(), 368 path.as_path(), 369 make_default, 370 ), 371 )?; 372 return json_operation_result::<AccountAttachSecretResult>(json!({ 373 "state": "dry_run", 374 "path": path.display().to_string(), 375 "default": make_default, 376 "secret_backend": { 377 "state": secret_backend.state, 378 "active_backend": secret_backend.active_backend, 379 "used_fallback": secret_backend.used_fallback, 380 }, 381 "account": account_summary_view(&account), 382 })); 383 } 384 if request.context.requires_approval_token() { 385 return Err(OperationAdapterError::approval_required( 386 request.operation_id(), 387 )); 388 } 389 390 let secret_backend = account_secret_backend_ready(request.operation_id(), self.config)?; 391 let account = map_expected_runtime( 392 request.operation_id(), 393 attach_identity_secret(self.config, selector.as_str(), path.as_path(), make_default), 394 )?; 395 json_operation_result::<AccountAttachSecretResult>(json!({ 396 "state": "secret_attached", 397 "default": make_default, 398 "secret_backend": { 399 "state": secret_backend.state, 400 "active_backend": secret_backend.active_backend, 401 "used_fallback": secret_backend.used_fallback, 402 }, 403 "account": account_summary_view(&account), 404 })) 405 } 406 } 407 408 impl OperationService<AccountGetRequest> for CoreOperationService<'_> { 409 type Result = AccountGetResult; 410 411 fn execute( 412 &self, 413 request: OperationRequest<AccountGetRequest>, 414 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 415 let scoped; 416 let config = if let Some(selector) = string_input(&request, "selector") { 417 scoped = selected_config(self.config, selector); 418 &scoped 419 } else { 420 self.config 421 }; 422 let resolution = resolve_account_resolution(config).map_err(|error| { 423 OperationAdapterError::unconfigured(request.operation_id(), error.to_string()) 424 })?; 425 let reason = if resolution.resolved_account.is_some() { 426 None 427 } else { 428 Some(map_runtime(unresolved_account_reason(config))?) 429 }; 430 json_operation_result::<AccountGetResult>(json!({ 431 "state": if resolution.resolved_account.is_some() { "ready" } else { "unconfigured" }, 432 "reason": reason, 433 "account_resolution": account_resolution_view(&resolution), 434 })) 435 } 436 } 437 438 impl OperationService<AccountListRequest> for CoreOperationService<'_> { 439 type Result = AccountListResult; 440 441 fn execute( 442 &self, 443 _request: OperationRequest<AccountListRequest>, 444 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 445 let snapshot = map_runtime(snapshot(self.config))?; 446 let accounts = snapshot 447 .accounts 448 .iter() 449 .map(account_summary_view) 450 .collect::<Vec<_>>(); 451 json_operation_result::<AccountListResult>(json!({ 452 "source": crate::runtime::account::SHARED_ACCOUNT_STORE_SOURCE, 453 "count": accounts.len(), 454 "accounts": accounts, 455 })) 456 } 457 } 458 459 impl OperationService<AccountRemoveRequest> for CoreOperationService<'_> { 460 type Result = AccountRemoveResult; 461 462 fn execute( 463 &self, 464 request: OperationRequest<AccountRemoveRequest>, 465 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 466 let selector = required_string(&request, "selector")?; 467 if request.context.dry_run { 468 let preview = 469 preview_account_removal(self.config, selector.as_str()).map_err(|error| { 470 OperationAdapterError::unconfigured(request.operation_id(), error.to_string()) 471 })?; 472 return json_operation_result::<AccountRemoveResult>(json!({ 473 "state": "dry_run", 474 "removed_account": account_summary_view(&preview.account), 475 "default_would_clear": preview.default_would_clear, 476 "remaining_account_count": preview.remaining_account_count, 477 })); 478 } 479 if request.context.requires_approval_token() { 480 return Err(OperationAdapterError::approval_required( 481 request.operation_id(), 482 )); 483 } 484 485 let result = remove_account(self.config, selector.as_str()).map_err(|error| { 486 OperationAdapterError::unconfigured(request.operation_id(), error.to_string()) 487 })?; 488 json_operation_result::<AccountRemoveResult>(json!({ 489 "state": "removed", 490 "removed_account": account_summary_view(&result.removed_account), 491 "default_cleared": result.default_cleared, 492 "remaining_account_count": result.remaining_account_count, 493 })) 494 } 495 } 496 497 impl OperationService<AccountSelectionGetRequest> for CoreOperationService<'_> { 498 type Result = AccountSelectionGetResult; 499 500 fn execute( 501 &self, 502 _request: OperationRequest<AccountSelectionGetRequest>, 503 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 504 let resolution = map_runtime(resolve_account_resolution(self.config))?; 505 json_operation_result::<AccountSelectionGetResult>(json!({ 506 "account_resolution": account_resolution_view(&resolution), 507 })) 508 } 509 } 510 511 impl OperationService<AccountSelectionUpdateRequest> for CoreOperationService<'_> { 512 type Result = AccountSelectionUpdateResult; 513 514 fn execute( 515 &self, 516 request: OperationRequest<AccountSelectionUpdateRequest>, 517 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 518 let selector = required_string(&request, "selector")?; 519 if request.context.dry_run { 520 let account = 521 resolve_account_selector(self.config, selector.as_str()).map_err(|error| { 522 OperationAdapterError::unconfigured(request.operation_id(), error.to_string()) 523 })?; 524 return json_operation_result::<AccountSelectionUpdateResult>(json!({ 525 "state": "dry_run", 526 "account": account_summary_view(&account), 527 })); 528 } 529 530 let account = select_account(self.config, selector.as_str()).map_err(|error| { 531 OperationAdapterError::unconfigured(request.operation_id(), error.to_string()) 532 })?; 533 json_operation_result::<AccountSelectionUpdateResult>(json!({ 534 "state": "default", 535 "account": account_summary_view(&account), 536 })) 537 } 538 } 539 540 impl OperationService<AccountSelectionClearRequest> for CoreOperationService<'_> { 541 type Result = AccountSelectionClearResult; 542 543 fn execute( 544 &self, 545 request: OperationRequest<AccountSelectionClearRequest>, 546 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 547 if request.context.dry_run { 548 let resolution = map_runtime(resolve_account_resolution(self.config))?; 549 let account_snapshot = map_runtime(snapshot(self.config))?; 550 return json_operation_result::<AccountSelectionClearResult>(json!({ 551 "state": "dry_run", 552 "cleared_account": resolution.default_account.as_ref().map(account_summary_view), 553 "remaining_account_count": account_snapshot.accounts.len(), 554 })); 555 } 556 557 let result = map_runtime(clear_default_account(self.config))?; 558 json_operation_result::<AccountSelectionClearResult>(json!({ 559 "state": if result.cleared_account.is_some() { "cleared" } else { "already_clear" }, 560 "cleared_account": result.cleared_account.as_ref().map(account_summary_view), 561 "remaining_account_count": result.remaining_account_count, 562 })) 563 } 564 } 565 566 impl OperationService<StoreInitRequest> for CoreOperationService<'_> { 567 type Result = StoreInitResult; 568 569 fn execute( 570 &self, 571 request: OperationRequest<StoreInitRequest>, 572 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 573 if request.context.dry_run { 574 let view = map_runtime(crate::runtime::store::init_preflight(self.config))?; 575 return serialized_operation_result::<StoreInitResult, _>(&view); 576 } 577 578 let view = map_runtime(crate::runtime::store::init(self.config))?; 579 serialized_operation_result::<StoreInitResult, _>(&view) 580 } 581 } 582 583 impl OperationService<StoreStatusGetRequest> for CoreOperationService<'_> { 584 type Result = StoreStatusGetResult; 585 586 fn execute( 587 &self, 588 request: OperationRequest<StoreStatusGetRequest>, 589 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 590 let view = map_sdk_adapter( 591 request.operation_id(), 592 crate::runtime::store::status(self.config), 593 )?; 594 serialized_operation_result::<StoreStatusGetResult, _>(&view) 595 } 596 } 597 598 impl OperationService<StoreExportRequest> for CoreOperationService<'_> { 599 type Result = StoreExportResult; 600 601 fn execute( 602 &self, 603 request: OperationRequest<StoreExportRequest>, 604 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 605 let output = optional_path(&request, "output") 606 .unwrap_or_else(|| self.config.local.exports_dir.join("store-export.json")); 607 let format = match string_input(&request, "format").as_deref() { 608 Some("ndjson") => LocalExportFormatArg::Ndjson, 609 Some("json") | None => LocalExportFormatArg::Json, 610 Some(other) => { 611 return Err(invalid_input( 612 request.operation_id(), 613 format!("format must be `json` or `ndjson`, got `{other}`"), 614 )); 615 } 616 }; 617 if request.context.dry_run { 618 return Err(invalid_input( 619 request.operation_id(), 620 "`radroots store export` does not support --dry-run".to_owned(), 621 )); 622 } 623 624 let view = map_runtime(crate::runtime::store::export( 625 self.config, 626 format, 627 output.as_path(), 628 ))?; 629 serialized_operation_result::<StoreExportResult, _>(&view) 630 } 631 } 632 633 impl OperationService<StoreBackupCreateRequest> for CoreOperationService<'_> { 634 type Result = StoreBackupCreateResult; 635 636 fn execute( 637 &self, 638 request: OperationRequest<StoreBackupCreateRequest>, 639 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 640 let output = optional_path(&request, "output") 641 .unwrap_or_else(|| self.config.local.backups_dir.join("sdk-store-backup")); 642 if request.context.dry_run { 643 let view = map_sdk_adapter( 644 request.operation_id(), 645 crate::runtime::store::backup_preflight(self.config, output.as_path()), 646 )?; 647 return local_backup_result(request.operation_id(), &view); 648 } 649 650 let view = map_sdk_adapter( 651 request.operation_id(), 652 crate::runtime::store::backup(self.config, output.as_path()), 653 )?; 654 local_backup_result(request.operation_id(), &view) 655 } 656 } 657 658 impl OperationService<StoreBackupRestoreRequest> for CoreOperationService<'_> { 659 type Result = StoreBackupRestoreResult; 660 661 fn execute( 662 &self, 663 request: OperationRequest<StoreBackupRestoreRequest>, 664 ) -> Result<OperationResult<Self::Result>, OperationAdapterError> { 665 let source = required_path(&request, "source")?; 666 let destination = optional_path(&request, "destination"); 667 let overwrite = bool_input(&request, "overwrite").unwrap_or(false); 668 if overwrite && request.context.requires_approval_token() { 669 return Err(OperationAdapterError::approval_required( 670 request.operation_id(), 671 )); 672 } 673 674 let view = map_sdk_adapter( 675 request.operation_id(), 676 crate::runtime::store::restore( 677 self.config, 678 source.as_path(), 679 destination.as_deref(), 680 overwrite, 681 request.context.dry_run, 682 ), 683 )?; 684 local_restore_result(request.operation_id(), &view) 685 } 686 } 687 688 fn serialized_operation_result<R, T>(value: &T) -> Result<OperationResult<R>, OperationAdapterError> 689 where 690 R: OperationResultData, 691 T: Serialize, 692 { 693 OperationResult::new(R::from_serializable(value)?) 694 } 695 696 fn json_operation_result<R>(value: Value) -> Result<OperationResult<R>, OperationAdapterError> 697 where 698 R: OperationResultData, 699 { 700 OperationResult::new(R::from_value(value)) 701 } 702 703 fn map_runtime<T>(result: Result<T, RuntimeError>) -> Result<T, OperationAdapterError> { 704 result.map_err(|error| OperationAdapterError::Runtime(error.to_string())) 705 } 706 707 fn map_sdk_adapter<T>( 708 operation_id: &str, 709 result: Result<T, CliSdkAdapterError>, 710 ) -> Result<T, OperationAdapterError> { 711 result.map_err(|error| OperationAdapterError::sdk_adapter_failure(operation_id, error)) 712 } 713 714 fn account_secret_backend_ready( 715 operation_id: &str, 716 config: &RuntimeConfig, 717 ) -> Result<crate::runtime::account::AccountSecretBackendStatus, OperationAdapterError> { 718 let secret_backend = secret_backend_status(config); 719 if secret_backend.state == "ready" { 720 return Ok(secret_backend); 721 } 722 723 Err(OperationAdapterError::OperationUnavailable { 724 operation_id: operation_id.to_owned(), 725 message: secret_backend 726 .reason 727 .unwrap_or_else(|| "account secret backend is not available".to_owned()), 728 }) 729 } 730 731 fn map_expected_runtime<T>( 732 operation_id: &str, 733 result: Result<T, RuntimeError>, 734 ) -> Result<T, OperationAdapterError> { 735 result.map_err(|error| OperationAdapterError::runtime_failure(operation_id, error)) 736 } 737 738 fn local_backup_result( 739 operation_id: &str, 740 view: &LocalBackupView, 741 ) -> Result<OperationResult<StoreBackupCreateResult>, OperationAdapterError> { 742 match view.disposition() { 743 CommandDisposition::Success => { 744 serialized_operation_result::<StoreBackupCreateResult, _>(view) 745 } 746 disposition => Err(OperationAdapterError::from_command_disposition( 747 operation_id, 748 disposition, 749 view.reason.clone().unwrap_or_else(|| match disposition { 750 CommandDisposition::Success => "store backup succeeded".to_owned(), 751 CommandDisposition::NotFound => "store backup target was not found".to_owned(), 752 CommandDisposition::ValidationFailed => "store backup validation failed".to_owned(), 753 CommandDisposition::Unconfigured => "store backup is unconfigured".to_owned(), 754 CommandDisposition::ExternalUnavailable => "store backup is unavailable".to_owned(), 755 CommandDisposition::Unsupported => "store backup is unsupported".to_owned(), 756 CommandDisposition::InternalError => "store backup failed".to_owned(), 757 }), 758 )), 759 } 760 } 761 762 fn local_restore_result( 763 operation_id: &str, 764 view: &LocalRestoreView, 765 ) -> Result<OperationResult<StoreBackupRestoreResult>, OperationAdapterError> { 766 match view.disposition() { 767 CommandDisposition::Success => { 768 serialized_operation_result::<StoreBackupRestoreResult, _>(view) 769 } 770 disposition => Err(OperationAdapterError::from_command_disposition( 771 operation_id, 772 disposition, 773 view.reason.clone().unwrap_or_else(|| match disposition { 774 CommandDisposition::Success => "store restore succeeded".to_owned(), 775 CommandDisposition::NotFound => "store restore source was not found".to_owned(), 776 CommandDisposition::ValidationFailed => { 777 "store restore validation failed".to_owned() 778 } 779 CommandDisposition::Unconfigured => "store restore is unconfigured".to_owned(), 780 CommandDisposition::ExternalUnavailable => { 781 "store restore is unavailable".to_owned() 782 } 783 CommandDisposition::Unsupported => "store restore is unsupported".to_owned(), 784 CommandDisposition::InternalError => "store restore failed".to_owned(), 785 }), 786 )), 787 } 788 } 789 790 fn selected_config(config: &RuntimeConfig, selector: String) -> RuntimeConfig { 791 let mut config = config.clone(); 792 config.account.selector = Some(selector); 793 config 794 } 795 796 fn publish_runtime_view( 797 config: &RuntimeConfig, 798 signed_write_required: bool, 799 account: &AccountResolution, 800 ) -> PublishRuntimeView { 801 let relay_ready = !config.relay.urls.is_empty(); 802 let source = config.publish.source.as_str().to_owned(); 803 let relay = PublishRelayRuntimeView { 804 ready: relay_ready, 805 count: config.relay.urls.len(), 806 source: config.relay.source.as_str().to_owned(), 807 }; 808 809 match config.publish.transport { 810 PublishTransport::DirectNostrRelay => { 811 let (state, executable, reason) = direct_nostr_relay_publish_readiness( 812 config, 813 relay_ready, 814 signed_write_required, 815 account, 816 ); 817 PublishRuntimeView { 818 transport: config.publish.transport.as_str().to_owned(), 819 source, 820 transport_family: config.publish.transport.transport_family().to_owned(), 821 state: state.to_owned(), 822 executable, 823 reason: reason.clone(), 824 signed_write_required, 825 relay, 826 provider: PublishProviderRuntimeView { 827 provider_runtime_id: "direct_nostr_relay".to_owned(), 828 state: state.to_owned(), 829 source: config.relay.source.as_str().to_owned(), 830 reason, 831 }, 832 } 833 } 834 PublishTransport::RadrootsdProxy => { 835 let (state, executable, reason) = radrootsd_publish_readiness(config); 836 PublishRuntimeView { 837 transport: config.publish.transport.as_str().to_owned(), 838 source, 839 transport_family: config.publish.transport.transport_family().to_owned(), 840 state: state.to_owned(), 841 executable, 842 reason: reason.clone(), 843 signed_write_required, 844 relay, 845 provider: PublishProviderRuntimeView { 846 provider_runtime_id: "radrootsd_proxy".to_owned(), 847 state: state.to_owned(), 848 source: "publish transport · local first".to_owned(), 849 reason, 850 }, 851 } 852 } 853 } 854 } 855 856 fn direct_nostr_relay_publish_readiness( 857 config: &RuntimeConfig, 858 relay_ready: bool, 859 signed_write_required: bool, 860 account: &AccountResolution, 861 ) -> (&'static str, bool, Option<String>) { 862 if !relay_ready { 863 return ( 864 "unconfigured", 865 false, 866 Some( 867 "direct_nostr_relay publish transport requires at least one configured relay for writes" 868 .to_owned(), 869 ), 870 ); 871 } 872 873 if !signed_write_required { 874 return ("ready", true, None); 875 } 876 877 if matches!(config.signer.backend, SignerBackend::Myc) { 878 let signer = resolve_signer_status(config); 879 return if signer.state == "ready" { 880 ("ready", true, None) 881 } else { 882 ("unconfigured", false, signer.reason) 883 }; 884 } 885 886 let Some(resolved_account) = account.resolved_account.as_ref() else { 887 return ( 888 "unconfigured", 889 false, 890 Some( 891 "direct_nostr_relay publish transport requires a selected or default write-capable local account for signed writes" 892 .to_owned(), 893 ), 894 ); 895 }; 896 897 if !resolved_account.write_capable { 898 return ( 899 "unconfigured", 900 false, 901 Some( 902 AccountRuntimeFailure::watch_only(&resolved_account.record.account_id).to_string(), 903 ), 904 ); 905 } 906 907 ("ready", true, None) 908 } 909 910 fn radrootsd_publish_readiness(config: &RuntimeConfig) -> (&'static str, bool, Option<String>) { 911 if config.publish.radrootsd_proxy.token_file.is_none() 912 && config.publish.radrootsd_proxy.token_secret_id.is_none() 913 { 914 return ( 915 "unconfigured", 916 false, 917 Some("radrootsd_proxy publish transport requires a configured token file or token secret id".to_owned()), 918 ); 919 } 920 921 if matches!(config.signer.backend, SignerBackend::Myc) { 922 let signer = resolve_signer_status(config); 923 return if signer.state == "ready" { 924 ("ready", true, None) 925 } else { 926 ("unconfigured", false, signer.reason) 927 }; 928 } 929 930 ("ready", true, None) 931 } 932 933 fn signer_health_view(config: &RuntimeConfig, account: &AccountResolution) -> Value { 934 match config.signer.backend { 935 SignerBackend::Local => { 936 let write_capable = account 937 .resolved_account 938 .as_ref() 939 .map(|account| account.write_capable) 940 .unwrap_or(false); 941 json!({ 942 "state": if write_capable { "ready" } else { "unconfigured" }, 943 "backend": config.signer.backend.as_str(), 944 "write_capable_account": write_capable, 945 "reason": if write_capable { 946 Value::Null 947 } else { 948 json!("local signer requires a selected or default write-capable local account") 949 }, 950 }) 951 } 952 SignerBackend::Myc => { 953 let signer = resolve_signer_status(config); 954 json!({ 955 "state": signer.state, 956 "backend": config.signer.backend.as_str(), 957 "write_capable_account": signer.reason.is_none(), 958 "reason": signer.reason, 959 "binding_state": signer.binding.state, 960 }) 961 } 962 } 963 } 964 965 fn health_status_state(store_state: &str, publish: &PublishRuntimeView) -> &'static str { 966 if store_state == "ready" && publish_runtime_ready(publish) { 967 "ready" 968 } else { 969 "needs_attention" 970 } 971 } 972 973 fn health_check_state( 974 store_state: &str, 975 account_ready: bool, 976 publish: &PublishRuntimeView, 977 ) -> &'static str { 978 if store_state == "ready" && account_ready && publish_runtime_ready(publish) { 979 "ready" 980 } else { 981 "needs_attention" 982 } 983 } 984 985 fn publish_runtime_ready(publish: &PublishRuntimeView) -> bool { 986 !publish.signed_write_required || publish.executable 987 } 988 989 fn health_actions( 990 config: &RuntimeConfig, 991 store_state: &str, 992 account: &AccountResolution, 993 publish: &PublishRuntimeView, 994 ) -> Vec<String> { 995 let mut actions = Vec::new(); 996 if store_state != "ready" { 997 push_unique(&mut actions, "radroots store status get"); 998 } 999 if let Some(resolved) = account.resolved_account.as_ref() { 1000 if !resolved.write_capable { 1001 push_unique(&mut actions, "radroots account attach-secret"); 1002 } 1003 } else { 1004 push_unique(&mut actions, "radroots account create"); 1005 } 1006 for action in publish_recovery_actions(config, account, publish) { 1007 push_unique(&mut actions, action); 1008 } 1009 actions 1010 } 1011 1012 fn config_actions( 1013 config: &RuntimeConfig, 1014 account: &AccountResolution, 1015 publish: &PublishRuntimeView, 1016 ) -> Vec<String> { 1017 publish_recovery_actions(config, account, publish) 1018 } 1019 1020 fn publish_recovery_actions( 1021 config: &RuntimeConfig, 1022 account: &AccountResolution, 1023 publish: &PublishRuntimeView, 1024 ) -> Vec<String> { 1025 if publish.state == "ready" { 1026 return Vec::new(); 1027 } 1028 1029 let mut actions = Vec::new(); 1030 match config.publish.transport { 1031 PublishTransport::DirectNostrRelay => { 1032 if config.relay.urls.is_empty() { 1033 push_unique( 1034 &mut actions, 1035 "radroots --relay wss://relay.example.com sync pull", 1036 ); 1037 } 1038 if publish.signed_write_required { 1039 if matches!(config.signer.backend, SignerBackend::Myc) { 1040 push_unique(&mut actions, "radroots signer status get"); 1041 } else if let Some(resolved) = account.resolved_account.as_ref() { 1042 if !resolved.write_capable { 1043 push_unique(&mut actions, "radroots account attach-secret"); 1044 } 1045 } else { 1046 push_unique(&mut actions, "radroots account create"); 1047 } 1048 } 1049 } 1050 PublishTransport::RadrootsdProxy => { 1051 if self::proxy_token_configured(config) { 1052 if publish.signed_write_required 1053 && matches!(config.signer.backend, SignerBackend::Myc) 1054 { 1055 push_unique(&mut actions, "radroots signer status get"); 1056 } 1057 } else { 1058 push_unique( 1059 &mut actions, 1060 "configure RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_FILE or RADROOTS_CLI_RADROOTSD_PROXY_TOKEN_SECRET_ID", 1061 ); 1062 } 1063 } 1064 } 1065 actions 1066 } 1067 1068 fn proxy_token_configured(config: &RuntimeConfig) -> bool { 1069 config.publish.radrootsd_proxy.token_file.is_some() 1070 || config.publish.radrootsd_proxy.token_secret_id.is_some() 1071 } 1072 1073 fn push_unique(actions: &mut Vec<String>, action: impl Into<String>) { 1074 let action = action.into(); 1075 if !actions.contains(&action) { 1076 actions.push(action); 1077 } 1078 } 1079 1080 fn required_string<P>( 1081 request: &OperationRequest<P>, 1082 key: &str, 1083 ) -> Result<String, OperationAdapterError> 1084 where 1085 P: OperationRequestPayload + OperationRequestData, 1086 { 1087 string_input(request, key).ok_or_else(|| { 1088 invalid_input( 1089 request.operation_id(), 1090 format!("missing required `{key}` input"), 1091 ) 1092 }) 1093 } 1094 1095 fn required_path<P>( 1096 request: &OperationRequest<P>, 1097 key: &str, 1098 ) -> Result<PathBuf, OperationAdapterError> 1099 where 1100 P: OperationRequestPayload + OperationRequestData, 1101 { 1102 optional_path(request, key).ok_or_else(|| { 1103 invalid_input( 1104 request.operation_id(), 1105 format!("missing required `{key}` input"), 1106 ) 1107 }) 1108 } 1109 1110 fn optional_path<P>(request: &OperationRequest<P>, key: &str) -> Option<PathBuf> 1111 where 1112 P: OperationRequestPayload + OperationRequestData, 1113 { 1114 string_input(request, key).map(PathBuf::from) 1115 } 1116 1117 fn string_input<P>(request: &OperationRequest<P>, key: &str) -> Option<String> 1118 where 1119 P: OperationRequestPayload + OperationRequestData, 1120 { 1121 request 1122 .payload 1123 .input() 1124 .get(key) 1125 .and_then(Value::as_str) 1126 .map(str::to_owned) 1127 } 1128 1129 fn bool_input<P>(request: &OperationRequest<P>, key: &str) -> Option<bool> 1130 where 1131 P: OperationRequestPayload + OperationRequestData, 1132 { 1133 request.payload.input().get(key).and_then(Value::as_bool) 1134 } 1135 1136 fn invalid_input(operation_id: &str, message: String) -> OperationAdapterError { 1137 OperationAdapterError::InvalidInput { 1138 operation_id: operation_id.to_owned(), 1139 message, 1140 } 1141 } 1142 1143 #[cfg(test)] 1144 mod tests { 1145 use std::path::{Path, PathBuf}; 1146 1147 use radroots_runtime_paths::RadrootsMigrationReport; 1148 use radroots_secret_vault::RadrootsSecretBackend; 1149 use serde_json::{Map, Value}; 1150 use tempfile::tempdir; 1151 1152 use super::CoreOperationService; 1153 use crate::ops::{ 1154 AccountAttachSecretRequest, AccountCreateRequest, AccountImportRequest, AccountListRequest, 1155 AccountRemoveRequest, OperationAdapter, OperationContext, OperationData, OperationRequest, 1156 StoreStatusGetRequest, WorkspaceGetRequest, 1157 }; 1158 use crate::runtime::config::{ 1159 AccountConfig, AccountSecretContractConfig, HyfConfig, IdentityConfig, InteractionConfig, 1160 LocalConfig, LoggingConfig, MigrationConfig, MycConfig, OutputConfig, OutputFormat, 1161 PathsConfig, PublishConfig, PublishTransport, PublishTransportSource, RelayConfig, 1162 RelayConfigSource, RelayPublishPolicy, RpcConfig, RuntimeConfig, SignerBackend, 1163 SignerConfig, Verbosity, 1164 }; 1165 use crate::runtime::logging::LoggingState; 1166 1167 #[test] 1168 fn core_service_envelopes_workspace_get() { 1169 let dir = tempdir().expect("tempdir"); 1170 let config = sample_config(dir.path()); 1171 let logging = LoggingState { 1172 initialized: true, 1173 current_file: None, 1174 }; 1175 let service = OperationAdapter::new(CoreOperationService::new(&config, &logging)); 1176 let request = 1177 OperationRequest::new(OperationContext::default(), WorkspaceGetRequest::default()) 1178 .expect("workspace request"); 1179 let result = service.execute(request).expect("workspace result"); 1180 let envelope = result 1181 .to_envelope(OperationContext::default().envelope_context("req_workspace")) 1182 .expect("workspace envelope"); 1183 1184 assert_eq!(envelope.operation_id, "workspace.get"); 1185 assert_eq!(envelope.kind, "workspace.get"); 1186 assert_eq!(envelope.request_id, "req_workspace"); 1187 assert_eq!(envelope.result["profile"], "interactive_user"); 1188 assert_eq!( 1189 envelope.result["replica_db_path"], 1190 config.local.replica_db_path.display().to_string() 1191 ); 1192 } 1193 1194 #[test] 1195 fn core_service_backs_store_status() { 1196 let dir = tempdir().expect("tempdir"); 1197 let config = sample_config(dir.path()); 1198 let logging = LoggingState { 1199 initialized: false, 1200 current_file: None, 1201 }; 1202 let service = OperationAdapter::new(CoreOperationService::new(&config, &logging)); 1203 let request = OperationRequest::new( 1204 OperationContext::default(), 1205 StoreStatusGetRequest::default(), 1206 ) 1207 .expect("store status request"); 1208 let result = service.execute(request).expect("store status result"); 1209 let envelope = result 1210 .to_envelope(OperationContext::default().envelope_context("req_store")) 1211 .expect("store status envelope"); 1212 1213 assert_eq!(envelope.operation_id, "store.status.get"); 1214 assert_eq!(envelope.result["state"], "ready"); 1215 assert_eq!( 1216 envelope.result["source"], 1217 "SDK canonical event store and outbox" 1218 ); 1219 assert_eq!(envelope.result["canonical_store"], "sdk"); 1220 assert_eq!(envelope.result["sdk_storage"], "directory"); 1221 assert_eq!( 1222 envelope.result["legacy_replica"]["source"], 1223 "legacy local replica · derived/migration source" 1224 ); 1225 assert_eq!(envelope.result["legacy_replica"]["state"], "unconfigured"); 1226 assert_eq!( 1227 envelope.result["event_store"]["store"]["integrity_ok"], 1228 true 1229 ); 1230 assert_eq!(envelope.result["outbox"]["store"]["integrity_ok"], true); 1231 } 1232 1233 #[test] 1234 fn core_service_backs_account_create_and_list() { 1235 let dir = tempdir().expect("tempdir"); 1236 let config = sample_config(dir.path()); 1237 let logging = LoggingState { 1238 initialized: false, 1239 current_file: None, 1240 }; 1241 let service = OperationAdapter::new(CoreOperationService::new(&config, &logging)); 1242 let create = 1243 OperationRequest::new(OperationContext::default(), AccountCreateRequest::default()) 1244 .expect("account create request"); 1245 let create_result = service.execute(create).expect("account create result"); 1246 let create_envelope = create_result 1247 .to_envelope(OperationContext::default().envelope_context("req_create")) 1248 .expect("account create envelope"); 1249 1250 assert_eq!(create_envelope.operation_id, "account.create"); 1251 assert_eq!(create_envelope.result["state"], "created"); 1252 assert!(create_envelope.result["account"]["id"].is_string()); 1253 1254 let list = 1255 OperationRequest::new(OperationContext::default(), AccountListRequest::default()) 1256 .expect("account list request"); 1257 let list_result = service.execute(list).expect("account list result"); 1258 let list_envelope = list_result 1259 .to_envelope(OperationContext::default().envelope_context("req_list")) 1260 .expect("account list envelope"); 1261 1262 assert_eq!(list_envelope.operation_id, "account.list"); 1263 assert_eq!(list_envelope.result["count"], 1); 1264 assert_eq!(list_envelope.result["accounts"][0]["is_default"], true); 1265 } 1266 1267 #[test] 1268 fn core_required_account_approvals_return_approval_error() { 1269 let dir = tempdir().expect("tempdir"); 1270 let config = sample_config(dir.path()); 1271 let logging = LoggingState { 1272 initialized: false, 1273 current_file: None, 1274 }; 1275 let service = OperationAdapter::new(CoreOperationService::new(&config, &logging)); 1276 let import = OperationRequest::new( 1277 OperationContext::default(), 1278 AccountImportRequest::from_data(data(&[("path", "account.json")])), 1279 ) 1280 .expect("account import request"); 1281 let import_error = service.execute(import).expect_err("approval required"); 1282 assert_eq!(import_error.to_output_error().code, "approval_required"); 1283 assert_eq!(import_error.to_output_error().exit_code, 6); 1284 1285 let attach_secret = OperationRequest::new( 1286 OperationContext::default(), 1287 AccountAttachSecretRequest::from_data(data(&[ 1288 ("selector", "acct_test"), 1289 ("path", "account.json"), 1290 ])), 1291 ) 1292 .expect("account attach-secret request"); 1293 let attach_secret_error = service 1294 .execute(attach_secret) 1295 .expect_err("approval required"); 1296 assert_eq!( 1297 attach_secret_error.to_output_error().code, 1298 "approval_required" 1299 ); 1300 assert_eq!(attach_secret_error.to_output_error().exit_code, 6); 1301 1302 let remove = OperationRequest::new( 1303 OperationContext::default(), 1304 AccountRemoveRequest::from_data(data(&[("selector", "acct_test")])), 1305 ) 1306 .expect("account remove request"); 1307 let remove_error = service.execute(remove).expect_err("approval required"); 1308 assert_eq!(remove_error.to_output_error().code, "approval_required"); 1309 assert_eq!(remove_error.to_output_error().exit_code, 6); 1310 } 1311 1312 fn sample_config(root: &Path) -> RuntimeConfig { 1313 let data = root.join("data"); 1314 let logs = root.join("logs"); 1315 let secrets = root.join("secrets"); 1316 RuntimeConfig { 1317 output: OutputConfig { 1318 format: OutputFormat::Human, 1319 verbosity: Verbosity::Normal, 1320 color: true, 1321 dry_run: false, 1322 }, 1323 interaction: InteractionConfig { 1324 input_enabled: true, 1325 assume_yes: false, 1326 stdin_tty: false, 1327 stdout_tty: false, 1328 prompts_allowed: false, 1329 confirmations_allowed: false, 1330 }, 1331 paths: PathsConfig { 1332 profile: "interactive_user".into(), 1333 profile_source: "test".into(), 1334 allowed_profiles: vec!["interactive_user".into(), "repo_local".into()], 1335 root_source: "test".into(), 1336 repo_local_root: None, 1337 repo_local_root_source: None, 1338 subordinate_path_override_source: "runtime_config".into(), 1339 app_namespace: "apps/cli".into(), 1340 shared_accounts_namespace: "shared/accounts".into(), 1341 shared_identities_namespace: "shared/identities".into(), 1342 app_config_path: root.join("config/apps/cli/config.toml"), 1343 workspace_config_path: None, 1344 app_data_root: data.join("apps/cli"), 1345 app_logs_root: logs.join("apps/cli"), 1346 shared_accounts_data_root: data.join("shared/accounts"), 1347 shared_accounts_secrets_root: secrets.join("shared/accounts"), 1348 default_identity_path: secrets.join("shared/identities/default.json"), 1349 }, 1350 migration: MigrationConfig { 1351 report: RadrootsMigrationReport::empty(), 1352 }, 1353 logging: LoggingConfig { 1354 filter: "info".into(), 1355 directory: None, 1356 stdout: false, 1357 }, 1358 account: AccountConfig { 1359 selector: None, 1360 store_path: data.join("shared/accounts/store.json"), 1361 secrets_dir: secrets.join("shared/accounts"), 1362 secret_backend: RadrootsSecretBackend::EncryptedFile, 1363 secret_fallback: None, 1364 }, 1365 account_secret_contract: AccountSecretContractConfig { 1366 default_backend: "host_vault".into(), 1367 default_fallback: Some("encrypted_file".into()), 1368 allowed_backends: vec!["host_vault".into(), "encrypted_file".into()], 1369 host_vault_policy: Some("desktop".into()), 1370 uses_protected_store: true, 1371 }, 1372 identity: IdentityConfig { 1373 path: secrets.join("shared/identities/default.json"), 1374 }, 1375 signer: SignerConfig { 1376 backend: SignerBackend::Local, 1377 }, 1378 publish: PublishConfig { 1379 transport: PublishTransport::DirectNostrRelay, 1380 source: PublishTransportSource::Defaults, 1381 radrootsd_proxy: crate::runtime::config::RadrootsdProxyConfig::default(), 1382 }, 1383 relay: RelayConfig { 1384 urls: Vec::new(), 1385 publish_policy: RelayPublishPolicy::Any, 1386 source: RelayConfigSource::Defaults, 1387 }, 1388 local: LocalConfig { 1389 root: data.join("apps/cli/replica"), 1390 replica_db_path: data.join("apps/cli/replica/replica.sqlite"), 1391 backups_dir: data.join("apps/cli/replica/backups"), 1392 exports_dir: data.join("apps/cli/replica/exports"), 1393 }, 1394 myc: MycConfig { 1395 executable: PathBuf::from("myc"), 1396 status_timeout_ms: 2_000, 1397 }, 1398 hyf: HyfConfig { 1399 enabled: false, 1400 executable: PathBuf::from("hyfd"), 1401 }, 1402 rpc: RpcConfig { 1403 url: "http://127.0.0.1:7070".into(), 1404 }, 1405 rhi: crate::runtime::config::RhiConfig { 1406 trusted_worker_pubkeys: Vec::new(), 1407 }, 1408 capability_bindings: Vec::new(), 1409 } 1410 } 1411 1412 fn data(entries: &[(&str, &str)]) -> OperationData { 1413 entries 1414 .iter() 1415 .map(|(key, value)| ((*key).to_owned(), Value::String((*value).to_owned()))) 1416 .collect::<Map<String, Value>>() 1417 } 1418 }